Compare commits

...

86 Commits

Author SHA1 Message Date
Jasmin Bom 39d686b1a1 Tiny spelling fix in CHANGES.rst [ci skip] 2019-02-14 12:31:00 +01:00
Kirill Vasin 60f2044bbd Entry returns None ends conversation (#1270)
* Fix unresolvable promises

* added async test and description

* added test_none_on_first_message for conv_handler

* Small change in ConversationHandler docstring

* Fix test to work with new commandhandler
2019-02-14 12:29:58 +01:00
Jasmin Bom dda7ca18cd Update CHANGES.rst
Also include #1270 even though not merged yet, but it should be very soon :)
2019-02-14 12:03:20 +01:00
Jasmin Bom 07c51d236b Fix spelling error in example notice 2019-02-14 11:53:09 +01:00
Jasmin Bom 9f1eccf569 Merge branch 'master' into V12 2019-02-14 11:52:31 +01:00
Eldinnie cd7c642f49 Add WAITING state and behavior (#1344)
* Add WAITING state and behavior

* Remove `run_async_timeout` and `timed_out_behavior` arguments
* replace with `WAITING` constant and behavior from states
* never wait for promise to resolve (will hang up entire update queue
* see #1250 for discussion

* Fixing pytest version to 4.2.0

Pytest 4.2.1 has a weird bug on top level collect in 4.2.1 Fixing version to 4.2.0
2019-02-14 11:00:21 +01:00
Bibo-Joshi f7abb21323 Adjust persistence on exit behaviour (#1312)
* Adjust persistence of exit behaviour

* Fix binary operators in on_flush

* Fix docstring

* Add test
2019-02-13 23:30:29 +01:00
Eldinnie 7e2dbdd4b3 Fix #1297 (#1342)
* Fix #1297

This makes a deepcopy of the user_data and chat_data dict as suggested by @Bibo-Joshi

* Fix dictpersistence aswel.
2019-02-13 23:28:48 +01:00
Jasmin Bom b64698e4b6 Use warnings.warn for conversationhandler warnings. (#1343)
* FIXED: ConversationHandler errors were logged to root logger

* Use warnings.warn instead of self.logger.warning.
2019-02-13 23:28:23 +01:00
Ambro d0936f76ad Only one warning for multiple CallbackqueryHandler's on ConversationHandler (#1319) 2019-02-13 22:08:49 +01:00
Jasmin Bom da342af7ed Small flake8 fixes 2019-02-13 16:04:48 +01:00
Jasmin Bom 446c54cf8d Bump to version 12.0.0b1 2019-02-13 13:41:04 +01:00
Jasmin Bom f5bfe2f29c Update example comments and docstrings to note the V12 beta 2019-02-13 13:38:07 +01:00
Eldinnie b02b68880f Make dispatcher use one context per update (#1283)
* Make dispatcher use one context per update

It gives user the option to `overload` context with their own properties in a lower group handler if they like

* Improve callbackcontext & run_async docstring

- Add note about how you can add custom attributes to context.
- Add warnings about how run_async and custom attributes should not be used together.

* Small documentation improvements. [ci skip]
2019-02-13 12:18:37 +01:00
Eldinnie 2c5eade4f0 Update Filters, CommandHandler and MessageHandler (#1221)
* update_filter attribute on filters

Makes it possible to have filters work on an update instead of message, while keeping behavior for current filters

* add update_type filter

* Messagehandler rework

- remove allow_edited (deprecated for a while)
- set deprecated defaults to None
- Raise deprecation warning when they're used
- add sensible defaults for filters.
- rework tests

* Commandhandler rework

* Remove deprecation test from new handler

* Some tweaks per CR

- rename update_types -> updates
- added some clarification to docstrings

* run webhook set test only on 3.6 on appveyor

* update_filter attribute on filters

Makes it possible to have filters work on an update instead of message, while keeping behavior for current filters

* add update_type filter

* Messagehandler rework

- remove allow_edited (deprecated for a while)
- set deprecated defaults to None
- Raise deprecation warning when they're used
- add sensible defaults for filters.
- rework tests

* Commandhandler rework

* Remove deprecation test from new handler

* Some tweaks per CR

- rename update_types -> updates
- added some clarification to docstrings

* run webhook set test only on 3.6 on appveyor

* Changes per CR

* Update travis to build v12

* small doc update

* try to make ci build version branches

* doc for BaseFilter

* Modify regexfilter and mergedfilter

Now returns a list of match objects for every regexfilter

* Change callbackcontext (+ docs)

* integrate in CommandHandler and PrefixHandler

* integrate in MessageHandler

* cbqhandler, iqhandler and srhandler

* make regexhandler a shell over MessageHandler

And raise deprecationWarning on creation

* clean up code and add some comments

* Rework based on internal group feedback

- use data_filter instead of regex_filter on BaseFilter
- have these filters return a dict that is then updated onto CallbackContext instead of using a list is before
- Add a .match property on CallbackContext that returns .matches[0] or None

* Fix and add test for callbackcontext.match

* Lots of documentation fixes and improvements [ci skip]
2019-02-13 12:07:25 +01:00
Jasmin Bom 950ec35970 Remove message decorator to fix default timeouts. (#1156)
* Remove message decorator to fix default timeouts.

* Make message wrapper method private.

* Make tests pass

* Fix callbackquery shortcuts

Closes #1180

* Fix callbackquery shortcut tests

* Fix merge

* Address CR

* Add missing default timeout=20 for some bot file uploads

* Fix wrong return value in convhandler

Probably stems from a combination of bad merge plus quickly merged hacktoberfest PR.
2019-02-13 11:37:13 +01:00
Jasmin Bom d33e1d9913 Small flake8 fix 2019-02-09 18:45:34 +01:00
Jasmin Bom 2e203e41e4 Merge branch 'master' into V12 2019-02-09 18:45:00 +01:00
Jasmin Bom e54a3188ce Revert "Fix bug: unable to save jobs with timezone aware dates (#1308)"
This reverts commit 23fe991b

See https://github.com/python-telegram-bot/python-telegram-bot/pull/1308 for more details.

NOTE: Keeping Ambro17 in AUTHORS.rst as I'm pretty sure they've contributed more since then :)
2019-02-09 18:44:04 +01:00
Jasmin Bom 710f43a23a travis: Use xenial dist for pypy
Cryptography started complaning about old openssl - hopefully xenial has a newer version
2019-02-09 18:26:17 +01:00
Jasmin Bom 27b757df32 Reorder some stickset tests to hopefully make them pass 2019-02-09 18:21:12 +01:00
Jasmin Bom 66e43c5932 Fix a bunch of flake8 W504 errors 2019-02-08 20:55:40 +01:00
Jasmin Bom 7fcbfc19f5 Mark location sending test as xfail as it seems to fail randomly 2019-02-08 20:22:44 +01:00
Jasmin Bom a60c07f549 Update test animation size
Telegram must've again chagned internal stuff
2019-02-08 20:21:24 +01:00
Jasmin Bom 1b52e6148e Merge branch 'master' into V12 2019-02-08 12:49:28 +01:00
Ambro 487bce18dd Improve regex filter docstring and avoid compiling compiled regex (#1314)
* Improve regex docstring and add test case

* Add Ambro17 as contributor

* rename regex filter

* Fix sphinx documentation for Filters.regex

* Add spacing to render Note with blue background
2019-02-08 11:12:49 +01:00
Gregory Petukhov 5c45e469d5 Fix #1335: Message.MESSAGE_TYPES does not contain "left_chat_member" (#1336) 2019-02-08 11:02:54 +01:00
Jasmin Bom 25e5449e97 Fix TestDispatcher::test_error_handler using pytest >= 4.0 2019-01-30 20:56:15 +01:00
Jasmin Bom a8bade4d73 Fix a bunch of tests
Looks like it's once again time for: Telegram changed some weird internal stuff and how thumbnails take up either 4 times as much space or half as much space. Oh and they also seemingly randomized the width and height of said thumbnails....
2019-01-30 20:40:53 +01:00
Jasmin Bom e08afe7fb2 Fix flake8 errors that only show in CI?? 2019-01-30 20:20:35 +01:00
Jasmin Bom 9817310788 Reflow docstrings in replykeyboardmarkup to satisfy flake8 2019-01-30 19:50:33 +01:00
Jasmin Bom ed33c4a7a9 Merge remote-tracking branch 'origin/master' 2019-01-30 19:45:37 +01:00
Jasmin Bom 1ee53e9e17 Updating the pylint that pre-commit uses
Fixes #1321
2019-01-30 19:42:57 +01:00
Gregory Petukhov 3e8d71582d Fix #1328: custom timeout argument does not work (#1330)
* Fix #1328: custom timeout argument does not work

* Remove unused import
2019-01-30 19:38:15 +01:00
Tanuj c03160c07f Add convenience classmethods for InlineKeyboardMarkup (fixes #1186) (#1260)
* Add convenience classmethods for InlineKeyboardMarkup (#1186)

* Switch to row and column methods

* Also add convenience classmethods for ReplyKeyboardMarkup

* Add some simple tests
2019-01-04 21:04:45 +01:00
Ambro 23fe991b85 Fix bug: unable to save jobs with timezone aware dates (#1308)
* Fix bug on jobs with timezone aware dates

* Add Ambro17 as colaborator
2019-01-04 20:29:07 +01:00
Bibo-Joshi 7eeb670a59 Fix check for effective chat/user in persistence (#1303) 2018-12-05 00:12:43 +01:00
Bibo-Joshi f23298a13b Fix typos in telegram/bot.py (#1305) 2018-12-04 15:06:48 +01:00
Konstantin Zemlyak 92f407bfb3 Update setup.py (#1306) 2018-12-04 15:06:22 +01:00
Steve Sandke 0cf0cccbc5 Fix description for JobQueue's run_daily and run_repeating methods. (#1299) 2018-11-22 13:03:58 +01:00
Jasmin Bom 378784f55e Fix persistence with non telegram.Update updates (#1271)
* Allow persistence with no telegram.Update updates

For use with TypeHandler

* Add test
2018-11-09 11:44:20 +01:00
Pieter Schutz 384173115f Merge branch 'master' into V12 2018-11-01 11:45:51 +01:00
Pieter Schutz ea5b301b59 fix pre-commit hooks 2018-11-01 11:08:09 +01:00
Pieter Schutz 9b66681ee4 fix pre-commit hooks 2018-11-01 11:08:09 +01:00
Pieter Schutz 92e7427689 Merge remote-tracking branch 'origin/master' 2018-11-01 10:53:46 +01:00
Pieter Schutz f82ceee777 Merge remote-tracking branch 'origin/master' 2018-11-01 10:53:46 +01:00
Pieter Schutz d2e2fe9ccc fix last flake8 errors 2018-11-01 10:52:36 +01:00
Pieter Schutz 76a72e9742 fix last flake8 errors 2018-11-01 10:52:36 +01:00
Noam Meltzer cf69a234d4 fix accidental commit to submodule config 2018-11-01 11:25:52 +02:00
Noam Meltzer eaf6dc2b88 fix accidental commit to submodule config 2018-11-01 11:25:52 +02:00
Noam Meltzer 9596343efd pep8 fixes 2018-11-01 11:18:07 +02:00
Noam Meltzer 30cc0f8cf9 pep8 fixes 2018-11-01 11:18:07 +02:00
Pieter Schutz f252436cd4 Merge remote-tracking branch 'origin/master' into V12 2018-11-01 09:32:55 +01:00
Jasmin Bom c9630ee8c5 Add Conflict error (HTTP error code 409) (#1154)
* Add conflicting bot id to conflict error message.

* Add test and comment to conflict error

* Don't extract bot id in Conflict exception per PR comments
2018-10-29 22:08:52 +01:00
Eldinnie 9d99660ba9 Change MAX_CAPTION_LENGTH to 1024 (#1262)
* update MAX_CAPTION_LENGTH

Telegram silently changed the max length for captions to 1024 chars.

* Update test_constants.py

* change docstrings to reflect new length

* remove message

* clear message and proper match
2018-10-16 19:51:57 +02:00
Jasmin Bom eca0ccf6b3 Update tests to support new test bots 2018-10-14 12:15:27 +02:00
Pavel Shakhov 9ece7fdb1c Mistake in MessageQueue.__call__'s docstring (#1249) 2018-10-08 15:36:59 +02:00
Evan Haberecht 4861d1a20d Removed unneccessary else and replaced with comment (#1247)
* Resolved issue #1163: Removed unneccessary else and replaced with comment

* Added myself to AUTHORS.rst
2018-10-08 08:18:33 +02:00
simonvorobjev dbf364e168 Allowed to use Handlers on conversation timeout (#1217)
* handler for ConversationHandler.END (timeout one) #1136

* review fixes

* review fixes

* review fixes

* review fixes

* docs and tests

* fixing stuff

* Fix problem

* fix conftest

* now it should work

* Add ConversationTimeoutContext

As discussed in the developers group. Use a class as the jobs context over using a dict.

* less verbosity
2018-10-04 08:58:40 +02:00
dbxnr d6d0dec6e0 remove extra else clause (#1239)
Fix #1236
2018-10-02 11:29:30 +02:00
Eldinnie 0bbca65a95 add sleep to bailout error test (#1241)
Hopefully it won;t hit the race condition so often on appveyor
2018-10-02 09:00:38 +02:00
Holden Oullette 1d715f0d36 Removed empty quotes (#1237) 2018-10-01 23:29:46 +02:00
Eldinnie f6f8667d6c add codacy badge to readme (#1232) 2018-10-01 22:16:48 +02:00
Pieter Schutz 8731365911 Merge branch 'master' into V12 2018-10-01 21:12:21 +02:00
Jasmin Bom d9ae4be2b3 Add 3.7 to travis and make pypy allowed_failures (#1215)
* Add 3.7 to travis build matrix using workaround

See https://github.com/travis-ci/travis-ci/issues/9815 for workaround discussion

* Add 3.7 to pypi classifiers

* Format build matrix differently

* Try adding pypy6.0.0 to travis build matrix

* Add py3.7 to appveyor

* Try pypy 5.10.1 instead

6.0.0 isn't on travis yet: https://github.com/travis-ci/travis-ci/issues/9542

* pypy2-5.10.0 isn't on travis yet either...

* allow failures on travis pypy
2018-10-01 20:50:44 +02:00
Pieter Schutz b5a3d7852a try to make ci build version branches 2018-10-01 11:48:10 +02:00
Pieter Schutz 3d8ab23d66 try to make ci build version branches 2018-10-01 11:46:48 +02:00
Pieter Schutz 6c36316aed Change yamls to build v12 on travis an appveyor 2018-10-01 10:25:06 +02:00
Jasmin Bom 4c66ba3a8d Allow filenames without dots in them when sending files (#1228)
* Fix issue 1227 by allowing filenames without dots in them

* Touch new test data file

* Satisfy flake8
2018-09-30 12:07:17 +02:00
reablaz c714a177d1 Update data.py to be compatible with example (#1213)
* Update data.py to be compatible with example

for now, if you process personal_info with example code, then you got an error if there is no set option to get native fist and last name.

setting default value will allow to process personal_info without native name/surname transation

* fixing line length

i hope i understood right this. sorry for delay, just starting using github!
2018-09-26 15:46:25 +02:00
Ehsan 3829666a53 add text mention for message parse (#1206)
* add text mention for message parse

* add author

* Update .gitignore
2018-09-25 20:07:55 +02:00
Pieter Schutz 9c1b493f37 Make persistence example context-based 2018-09-21 13:57:44 +02:00
Pieter Schutz af2d716129 Fix test_persistence
* To work with new CommandHandler
* To make tests context-based
2018-09-21 13:57:36 +02:00
Jasmin Bom 5252a493cb Pass check result into handle_update
Missed during merge
2018-09-21 09:20:35 +02:00
Jasmin Bom e75615cbf6 Revert "Revert "CommandHandler overhaul and PrefixHandler added (#1114)""
This reverts commit 9e2357b
2018-09-21 08:57:43 +02:00
Jasmin Bom cf95027308 Revert "Revert "Context based callbacks (#1100)""
This reverts commit f8a17cd
2018-09-21 08:57:01 +02:00
Eldinnie 439790375e Persistence (#1017)
* BasePersistence

* basic construct

* Keep working

* Continue work

Add tests for Basepersistence

* Finish up BasePersistence and implementation

* PickelPersistence and start tests

* Finishing up

* Oops, left in some typings

* Compatibilty issues regarding py2 solved

For Py2 compatibility

* increasing coverage

* Small changes due to CR

* All persistence tests in one file

* add DictPersistence

* Last changes per CR

* forgot change

* changes per CR

* call update_* only with relevant data

As discussed with @jsmnbom

* Add conversationbot Example

* should not have committed API-key
2018-09-20 22:50:40 +02:00
Marcelo G. de Andrade b9f56ca479 fixes comment on examples/conversationbot2.py (#1216) 2018-09-12 21:27:35 +02:00
Jasmin Bom e247fa7c2c Fix uploading files with unicode filenames (#1214)
* Patch urllib3RequestField to make it *not* support RFC2231

* Add new required test data file

* Fix on py2

* Remove weird legacy code from inputfile

Not needed anymore, and also makes it not work... so that was not great
2018-09-10 21:08:05 +02:00
Kirill Vasin b8c288ff4a Make ConversationHandler and run_async work together properly (#1176)
* #1175

* fix issue with timeout

* exception in promise resolve is treated as None result

* removed useless variables
2018-09-10 20:05:45 +02:00
Jasmin Bom bbe633e571 Remove GAE support message
We still kinda support it, but not more so than other providers.
2018-09-10 15:58:25 +02:00
Kirill Vasin f2b06728e9 Replace http.server with Tornado (#1191)
Fixes #1189
2018-09-08 23:25:48 +03:00
Jasmin Bom b2fb4264a3 Small test fixes to make them more stable on travis CI 2018-09-07 16:07:27 +02:00
Noam Meltzer 19591c955a Allow SOCKSConnection to parse username and password from URL (#1211) 2018-09-07 15:21:01 +02:00
Jasmin Bom 259a1faedc Releasing v11.1 2018-09-01 17:27:35 +02:00
Jasmin Bom 09bdb88822 Bot api 4.1 (#1198)
* Fix passport decryption failing at random times

Sometimes a decrypted secret was being treated as b64 and therefore got decoded even further. Fix by decoding b64 right before call to decrypt so we have better control of when not to do it

* Bot api 4.1

Telegram passport 1.1
Added support for middle names.
Added support for translations for documents
Add errors for translations for documents
Added support for requesting names in the language of the user's country of residence
Replaced the payload parameter with the new parameter nonce

NOTE: Scope stuff is NOT implemented, as we wanna STRONGLY encourage users to use the telegram provided SDKs anyway (and not generate telegram auth links in their bot, but rather on a server)

* Minor fixes

* Add hash to EncryptedPassportElement

For use with PassportElementErrorUnspecified apparently
2018-09-01 16:58:08 +02:00
170 changed files with 7001 additions and 2260 deletions
+4 -2
View File
@@ -6,12 +6,14 @@
args:
- --diff
- repo: git://github.com/pre-commit/pre-commit-hooks
sha: v1.2.0
sha: v2.0.0
hooks:
- id: flake8
exclude: ^(setup.py|docs/source/conf.py)$
args:
- --ignore=W605,W503
- repo: git://github.com/pre-commit/mirrors-pylint
sha: v1.7.1
sha: v2.2.2
hooks:
- id: pylint
files: ^telegram/.*\.py$
+17 -7
View File
@@ -1,11 +1,20 @@
language: python
python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
- "3.7-dev"
- "pypy-5.7.1"
matrix:
include:
- python: 2.7
- python: 3.4
- python: 3.5
- python: 3.6
- python: 3.7
dist: xenial
sudo: true
- python: pypy2.7-5.10.0
dist: xenial
- python: pypy3.5-5.10.1
dist: xenial
allow_failures:
- python: pypy2.7-5.10.0
- python: pypy3.5-5.10.1
dist: trusty
sudo: false
@@ -13,6 +22,7 @@ sudo: false
branches:
only:
- master
- /^[vV]\d+$/
cache:
directories:
+6
View File
@@ -16,18 +16,22 @@ Contributors
The following wonderful people contributed directly or indirectly to this project:
- `Alateas <https://github.com/alateas>`_
- `Ambro17 <https://github.com/Ambro17>`_
- `Anton Tagunov <https://github.com/anton-tagunov>`_
- `Avanatiker <https://github.com/Avanatiker>`_
- `Balduro <https://github.com/Balduro>`_
- `Bibo-Joshi <https://github.com/Bibo-Joshi>`_
- `bimmlerd <https://github.com/bimmlerd>`_
- `d-qoi <https://github.com/d-qoi>`_
- `daimajia <https://github.com/daimajia>`_
- `Daniel Reed <https://github.com/nmlorg>`_
- `Ehsan Online <https://github.com/ehsanonline>`_
- `Eli Gao <https://github.com/eligao>`_
- `Emilio Molinari <https://github.com/xates>`_
- `ErgoZ Riftbit Vaper <https://github.com/ergoz>`_
- `Eugene Lisitsky <https://github.com/lisitsky>`_
- `Eugenio Panadero <https://github.com/azogue>`_
- `Evan Haberecht <https://github.com/habereet>`_
- `evgfilim1 <https://github.com/evgfilim1>`_
- `franciscod <https://github.com/franciscod>`_
- `Hugo Damer <https://github.com/HakimusGIT>`_
@@ -42,6 +46,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Joscha Götzer <https://github.com/Rostgnom>`_
- `jossalgon <https://github.com/jossalgon>`_
- `JRoot3D <https://github.com/JRoot3D>`_
- `Kirill Vasin <https://github.com/vasinkd>`_
- `Kjwon15 <https://github.com/kjwon15>`_
- `Li-aung Yip <https://github.com/LiaungYip>`_
- `macrojames <https://github.com/macrojames>`_
@@ -67,6 +72,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Trainer Jono <https://github.com/Tr-Jono>`_
- `Valentijn <https://github.com/Faalentijn>`_
- `voider1 <https://github.com/voider1>`_
- `Vorobjev Simon <https://github.com/simonvorobjev>`_
- `Wagner Macedo <https://github.com/wagnerluis1982>`_
- `wjt <https://github.com/wjt>`_
+135
View File
@@ -2,6 +2,141 @@
Changes
=======
Version 12.0.0b1
================
*Released 2019-02-13*
First beta release ever.
It has been so long since last release that we would like to test the impact before a final release.
*We do NOT recommend using this beta release in production.*
**Major changes:**
- Context based callbacks
- Persistence
- PrefixHandler added (Handler overhaul)
- Deprecation of RegexHandler and edited_messages, channel_post, etc. arguments (Filter overhaul)
- Various ConversationHandler changes and fixes
**See the wiki page at https://git.io/fxJuV for a detailed guide on how to migrate from version 11 to version 12.**
Context based callbacks (`#1100`_)
----------------------------------
- Use of ``pass_`` in handlers is deprecated.
- Instead use ``use_context=True`` on ``Updater`` or ``Dispatcher`` and change callback from (bot, update, others...) to (update, context).
- This also applies to error handlers ``Dispatcher.add_error_handler`` and JobQueue jobs (change (bot, job) to (context) here).
- For users with custom handlers subclassing Handler, this is mostly backwards compatible, but to use the new context based callbacks you need to implement the new collect_additional_context method.
- Passing bot to ``JobQueue.__init__`` is deprecated. Use JobQueue.set_dispatcher with a dispatcher instead.
- Dispatcher makes sure to use a single `CallbackContext` for a entire update. This means that if an update is handled by multiple handlers (by using the group argument), you can add custom arguments to the `CallbackContext` in a lower group handler and use it in higher group handler. NOTE: Never use with @run_async, see docs for more info. (`#1283`_)
- If you have custom handlers they will need to be updated to support the changes in this release.
- Update all examples to use context based callbacks.
Persistence (`#1017`_)
----------------------
- Added PicklePersistence and DictPersistence for adding persistence to your bots.
- BasePersistence can be subclassed for all your persistence needs.
- Add a new example that shows a persistent ConversationHandler bot
Handler overhaul (`#1114`_)
---------------------------
- CommandHandler now only triggers on actual commands as defined by telegram servers (everything that the clients mark as a tabable link).
- PrefixHandler can be used if you need to trigger on prefixes (like all messages starting with a "/" (old CommandHandler behaviour) or even custom prefixes like "#" or "!").
Filter overhaul (`#1221`_)
--------------------------
- RegexHandler is deprecated and should be replaced with a MessageHandler with a regex filter.
- Use update filters to filter update types instead of arguments (message_updates, channel_post_updates and edited_updates) on the handlers.
- Completely remove allow_edited argument - it has been deprecated for a while.
- data_filters now exist which allows filters that return data into the callback function. This is how the regex filter is implemented.
- All this means that it no longer possible to use a list of filters in a handler. Use bitwise operators instead!
ConversationHandler
-------------------
- Remove ``run_async_timeout`` and ``timed_out_behavior`` arguments (`#1344`_)
- Replace with ``WAITING`` constant and behavior from states (`#1344`_)
- Only emit one warning for multiple CallbackQueryHandlers in a ConversationHandler (`#1319`_)
- Use warnings.warn for ConversationHandler warnings (`#1343`_)
- Fix unresolvable promises (`#1270`_)
Bug fixes & improvements
------------------------
- Handlers should be faster due to deduped logic.
- Avoid compiling compiled regex in regex filter. (`#1314`_)
- Add missing ``left_chat_member`` to Message.MESSAGE_TYPES (`#1336`_)
- Make custom timeouts actually work properly (`#1330`_)
- Add convenience classmethods (from_button, from_row and from_column) to InlineKeyboardMarkup
- Small typo fix in setup.py (`#1306`_)
- Add Conflict error (HTTP error code 409) (`#1154`_)
- Change MAX_CAPTION_LENGTH to 1024 (`#1262`_)
- Remove some unnecessary clauses (`#1247`_, `#1239`_)
- Allow filenames without dots in them when sending files (`#1228`_)
- Fix uploading files with unicode filenames (`#1214`_)
- Replace http.server with Tornado (`#1191`_)
- Allow SOCKSConnection to parse username and password from URL (`#1211`_)
- Fix for arguments in passport/data.py (`#1213`_)
- Improve message entity parsing by adding text_mention (`#1206`_)
.. _`#1100`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1100
.. _`#1283`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1283
.. _`#1017`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1017
.. _`#1325`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1325
.. _`#1301`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1301
.. _`#1312`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1312
.. _`#1324`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1324
.. _`#1114`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1114
.. _`#1221`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1221
.. _`#1314`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1314
.. _`#1336`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1336
.. _`#1330`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1330
.. _`#1306`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1306
.. _`#1154`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1154
.. _`#1262`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1262
.. _`#1247`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1247
.. _`#1239`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1239
.. _`#1228`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1228
.. _`#1214`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1214
.. _`#1191`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1191
.. _`#1211`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1211
.. _`#1213`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1213
.. _`#1206`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1206
.. _`#1344`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1344
.. _`#1319`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1319
.. _`#1343`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1343
.. _`#1270`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1270
Internal improvements
---------------------
- Finally fix our CI builds mostly (too many commits and PRs to list)
- Use multiple bots for CI to improve testing times significantly.
- Allow pypy to fail in CI.
- Remove the last CamelCase CheckUpdate methods from the handlers we missed earlier.
Pre-2019 (up and including to version 11.1.0)
=============================================
**2018-09-01**
*Released 11.1.0*
Fixes and updates for Telegram Passport: (`#1198`_)
- Fix passport decryption failing at random times
- Added support for middle names.
- Added support for translations for documents
- Add errors for translations for documents
- Added support for requesting names in the language of the user's country of residence
- Replaced the payload parameter with the new parameter nonce
- Add hash to EncryptedPassportElement
.. _`#1198`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1198
**2018-08-29**
*Released 11.0.0*
+20 -3
View File
@@ -46,6 +46,10 @@ We have a vibrant community of developers helping each other in our `Telegram gr
:target: http://isitmaintained.com/project/python-telegram-bot/python-telegram-bot
:alt: Median time to resolve an issue
.. image:: https://api.codacy.com/project/badge/Grade/99d901eaa09b44b4819aec05c330c968
:target: https://www.codacy.com/app/python-telegram-bot/python-telegram-bot?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=python-telegram-bot/python-telegram-bot&amp;utm_campaign=Badge_Grade
:alt: Code quality
.. image:: https://img.shields.io/badge/Telegram-Group-blue.svg
:target: https://telegram.me/pythontelegrambotgroup
:alt: Telegram Group
@@ -85,7 +89,6 @@ Introduction
This library provides a pure Python interface for the
`Telegram Bot API <https://core.telegram.org/bots/api>`_.
It's compatible with Python versions 2.7, 3.3+ and `PyPy <http://pypy.org/>`_.
It also works with `Google App Engine <https://cloud.google.com/appengine>`_.
In addition to the pure API implementation, this library features a number of high-level classes to
make the development of bots easy and straightforward. These classes are contained in the
@@ -95,13 +98,27 @@ make the development of bots easy and straightforward. These classes are contain
Telegram API support
====================
All types and methods of the Telegram Bot API **4.0** are supported.
All types and methods of the Telegram Bot API **4.1** are supported.
==========
Installing
==========
You can install or upgrade python-telegram-bot with:
**Beta note**
The newest stable release is currently version 11.1.0.
The newest release is a beta release for version 12.
Install or upgrade with:
.. code:: shell
$ pip install python-telegram-bot=12.0.0b1 --upgrade
See CHANGES.rst for the changelog and make sure to report any bugs you find!
You can install or upgrade the stable python-telegram-bot with:
.. code:: shell
+2
View File
@@ -10,10 +10,12 @@ environment:
- PYTHON: "C:\\Python34"
- PYTHON: "C:\\Python35"
- PYTHON: "C:\\Python36"
- PYTHON: "C:\\Python37"
branches:
only:
- master
- /^[vV]\d+$/
skip_branch_with_pr: true
+2 -2
View File
@@ -58,9 +58,9 @@ author = u'Leandro Toledo'
# built documents.
#
# The short X.Y version.
version = '11.0' # telegram.__version__[:3]
version = '12.0' # telegram.__version__[:3]
# The full version, including alpha/beta/rc tags.
release = '11.0.0' # telegram.__version__
release = '12.0.0b1' # telegram.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -0,0 +1,6 @@
telegram.ext.BasePersistence
============================
.. autoclass:: telegram.ext.BasePersistence
:members:
:show-inheritance:
@@ -0,0 +1,5 @@
telegram.ext.CallbackContext
============================
.. autoclass:: telegram.ext.CallbackContext
:members:
@@ -0,0 +1,6 @@
telegram.ext.DictPersistence
============================
.. autoclass:: telegram.ext.DictPersistence
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
telegram.ext.PicklePersistence
==============================
.. autoclass:: telegram.ext.PicklePersistence
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
telegram.ext.PrefixHandler
===========================
.. autoclass:: telegram.ext.PrefixHandler
:members:
:show-inheritance:
+11
View File
@@ -10,6 +10,7 @@ telegram.ext package
telegram.ext.jobqueue
telegram.ext.messagequeue
telegram.ext.delayqueue
telegram.ext.callbackcontext
Handlers
--------
@@ -24,8 +25,18 @@ Handlers
telegram.ext.inlinequeryhandler
telegram.ext.messagehandler
telegram.ext.precheckoutqueryhandler
telegram.ext.prefixhandler
telegram.ext.regexhandler
telegram.ext.shippingqueryhandler
telegram.ext.stringcommandhandler
telegram.ext.stringregexhandler
telegram.ext.typehandler
Persistence
-----------
.. toctree::
telegram.ext.basepersistence
telegram.ext.picklepersistence
telegram.ext.dictpersistence
+3
View File
@@ -25,5 +25,8 @@ A basic example of an [inline bot](https://core.telegram.org/bots/inline). Don't
### [`paymentbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/paymentbot.py)
A basic example of a bot that can accept payments. Don't forget to enable and configure payments with [@BotFather](https://telegram.me/BotFather).
### [`persistentconversationbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/persistentconversationbot.py)
A basic example of a bot store conversation state and user_data over multiple restarts.
## Pure API
The [`echobot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot.py) example uses only the pure, "bare-metal" API wrapper.
+22 -19
View File
@@ -1,11 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Simple Bot to reply to Telegram messages
# This program is dedicated to the public domain under the CC0 license.
"""
This Bot uses the Updater class to handle the bot.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""
First, a few callback functions are defined. Then, those functions are passed to
the Dispatcher and registered at their respective places.
Then, the bot is started and runs until we press Ctrl-C on the command line.
@@ -17,12 +18,12 @@ Press Ctrl-C on the command line or send a signal to the process to stop the
bot.
"""
import logging
from telegram import (ReplyKeyboardMarkup, ReplyKeyboardRemove)
from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters, RegexHandler,
ConversationHandler)
import logging
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
@@ -32,7 +33,7 @@ logger = logging.getLogger(__name__)
GENDER, PHOTO, LOCATION, BIO = range(4)
def start(bot, update):
def start(update, context):
reply_keyboard = [['Boy', 'Girl', 'Other']]
update.message.reply_text(
@@ -44,7 +45,7 @@ def start(bot, update):
return GENDER
def gender(bot, update):
def gender(update, context):
user = update.message.from_user
logger.info("Gender of %s: %s", user.first_name, update.message.text)
update.message.reply_text('I see! Please send me a photo of yourself, '
@@ -54,9 +55,9 @@ def gender(bot, update):
return PHOTO
def photo(bot, update):
def photo(update, context):
user = update.message.from_user
photo_file = bot.get_file(update.message.photo[-1].file_id)
photo_file = update.message.photo[-1].get_file()
photo_file.download('user_photo.jpg')
logger.info("Photo of %s: %s", user.first_name, 'user_photo.jpg')
update.message.reply_text('Gorgeous! Now, send me your location please, '
@@ -65,7 +66,7 @@ def photo(bot, update):
return LOCATION
def skip_photo(bot, update):
def skip_photo(update, context):
user = update.message.from_user
logger.info("User %s did not send a photo.", user.first_name)
update.message.reply_text('I bet you look great! Now, send me your location please, '
@@ -74,7 +75,7 @@ def skip_photo(bot, update):
return LOCATION
def location(bot, update):
def location(update, context):
user = update.message.from_user
user_location = update.message.location
logger.info("Location of %s: %f / %f", user.first_name, user_location.latitude,
@@ -85,7 +86,7 @@ def location(bot, update):
return BIO
def skip_location(bot, update):
def skip_location(update, context):
user = update.message.from_user
logger.info("User %s did not send a location.", user.first_name)
update.message.reply_text('You seem a bit paranoid! '
@@ -94,7 +95,7 @@ def skip_location(bot, update):
return BIO
def bio(bot, update):
def bio(update, context):
user = update.message.from_user
logger.info("Bio of %s: %s", user.first_name, update.message.text)
update.message.reply_text('Thank you! I hope we can talk again some day.')
@@ -102,7 +103,7 @@ def bio(bot, update):
return ConversationHandler.END
def cancel(bot, update):
def cancel(update, context):
user = update.message.from_user
logger.info("User %s canceled the conversation.", user.first_name)
update.message.reply_text('Bye! I hope we can talk again some day.',
@@ -111,14 +112,16 @@ def cancel(bot, update):
return ConversationHandler.END
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():
# Create the EventHandler and pass it your bot's token.
updater = Updater("TOKEN")
# Create the Updater and pass it your bot's token.
# Make sure to set use_context=True to use the new context based callbacks
# Post version 12 this will no longer be necessary
updater = Updater("TOKEN", use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
+20 -15
View File
@@ -1,11 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Simple Bot to reply to Telegram messages
# This program is dedicated to the public domain under the CC0 license.
"""
This Bot uses the Updater class to handle the bot.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""
First, a few callback functions are defined. Then, those functions are passed to
the Dispatcher and registered at their respective places.
Then, the bot is started and runs until we press Ctrl-C on the command line.
@@ -17,12 +18,12 @@ Press Ctrl-C on the command line or send a signal to the process to stop the
bot.
"""
import logging
from telegram import ReplyKeyboardMarkup
from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters, RegexHandler,
ConversationHandler)
import logging
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
@@ -46,7 +47,7 @@ def facts_to_str(user_data):
return "\n".join(facts).join(['\n', '\n'])
def start(bot, update):
def start(update, context):
update.message.reply_text(
"Hi! My name is Doctor Botter. I will hold a more complex conversation with you. "
"Why don't you tell me something about yourself?",
@@ -55,23 +56,24 @@ def start(bot, update):
return CHOOSING
def regular_choice(bot, update, user_data):
def regular_choice(update, context):
text = update.message.text
user_data['choice'] = text
context.user_data['choice'] = text
update.message.reply_text(
'Your {}? Yes, I would love to hear about that!'.format(text.lower()))
return TYPING_REPLY
def custom_choice(bot, update):
def custom_choice(update, context):
update.message.reply_text('Alright, please send me the category first, '
'for example "Most impressive skill"')
return TYPING_CHOICE
def received_information(bot, update, user_data):
def received_information(update, context):
user_data = context.user_data
text = update.message.text
category = user_data['choice']
user_data[category] = text
@@ -85,7 +87,8 @@ def received_information(bot, update, user_data):
return CHOOSING
def done(bot, update, user_data):
def done(update, context):
user_data = context.user_data
if 'choice' in user_data:
del user_data['choice']
@@ -97,19 +100,21 @@ def done(bot, update, user_data):
return ConversationHandler.END
def error(bot, update, error):
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, error)
def main():
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
# Make sure to set use_context=True to use the new context based callbacks
# Post version 12 this will no longer be necessary
updater = Updater("TOKEN", use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
# Add conversation handler with the states GENDER, PHOTO, LOCATION and BIO
# Add conversation handler with the states CHOOSING, TYPING_CHOICE and TYPING_REPLY
conv_handler = ConversationHandler(
entry_points=[CommandHandler('start', start)],
+18 -13
View File
@@ -1,11 +1,13 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This program is dedicated to the public domain under the CC0 license.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""Simple Bot to reply to Telegram messages.
This program is dedicated to the public domain under the CC0 license.
This Bot uses the Updater class to handle the bot.
"""
Simple Bot to reply to Telegram messages.
First, a few handler functions are defined. Then, those functions are passed to
the Dispatcher and registered at their respective places.
@@ -17,9 +19,10 @@ Press Ctrl-C on the command line or send a signal to the process to stop the
bot.
"""
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters
import logging
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
@@ -29,30 +32,32 @@ logger = logging.getLogger(__name__)
# Define a few command handlers. These usually take the two arguments bot and
# update. Error handlers also receive the raised TelegramError object in error.
def start(bot, update):
def start(update, context):
"""Send a message when the command /start is issued."""
update.message.reply_text('Hi!')
def help(bot, update):
def help(update, context):
"""Send a message when the command /help is issued."""
update.message.reply_text('Help!')
def echo(bot, update):
def echo(update, context):
"""Echo the user message."""
update.message.reply_text(update.message.text)
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():
"""Start the bot."""
# Create the EventHandler and pass it your bot's token.
updater = Updater("TOKEN")
# Create the Updater and pass it your bot's token.
# Make sure to set use_context=True to use the new context based callbacks
# Post version 12 this will no longer be necessary
updater = Updater("TOKEN", use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
+16 -15
View File
@@ -1,12 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This program is dedicated to the public domain under the CC0 license.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""Simple Bot to reply to Telegram messages.
This program is dedicated to the public domain under the CC0 license.
This Bot uses the Updater class to handle the bot.
"""
First, a few handler functions are defined. Then, those functions are passed to
the Dispatcher and registered at their respective places.
Then, the bot is started and runs until we press Ctrl-C on the command line.
@@ -16,14 +16,13 @@ Basic inline bot example. Applies different text transformations.
Press Ctrl-C on the command line or send a signal to the process to stop the
bot.
"""
import logging
from uuid import uuid4
from telegram.utils.helpers import escape_markdown
from telegram import InlineQueryResultArticle, ParseMode, \
InputTextMessageContent
from telegram.ext import Updater, InlineQueryHandler, CommandHandler
import logging
from telegram.utils.helpers import escape_markdown
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
@@ -34,17 +33,17 @@ logger = logging.getLogger(__name__)
# Define a few command handlers. These usually take the two arguments bot and
# update. Error handlers also receive the raised TelegramError object in error.
def start(bot, update):
def start(update, context):
"""Send a message when the command /start is issued."""
update.message.reply_text('Hi!')
def help(bot, update):
def help(update, context):
"""Send a message when the command /help is issued."""
update.message.reply_text('Help!')
def inlinequery(bot, update):
def inlinequery(update, context):
"""Handle the inline query."""
query = update.inline_query.query
results = [
@@ -69,14 +68,16 @@ def inlinequery(bot, update):
update.inline_query.answer(results)
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():
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
# Make sure to set use_context=True to use the new context based callbacks
# Post version 12 this will no longer be necessary
updater = Updater("TOKEN", use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
+17 -11
View File
@@ -1,10 +1,16 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Basic example for a bot that uses inline keyboards.
# This program is dedicated to the public domain under the CC0 license.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""
Basic example for a bot that uses inline keyboards.
"""
import logging
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler
@@ -13,7 +19,7 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s
logger = logging.getLogger(__name__)
def start(bot, update):
def start(update, context):
keyboard = [[InlineKeyboardButton("Option 1", callback_data='1'),
InlineKeyboardButton("Option 2", callback_data='2')],
@@ -24,26 +30,26 @@ def start(bot, update):
update.message.reply_text('Please choose:', reply_markup=reply_markup)
def button(bot, update):
def button(update, context):
query = update.callback_query
bot.edit_message_text(text="Selected option: {}".format(query.data),
chat_id=query.message.chat_id,
message_id=query.message.message_id)
query.edit_message_text(text="Selected option: {}".format(query.data))
def help(bot, update):
def help(update, context):
update.message.reply_text("Use /start to test this bot.")
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():
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN")
# Make sure to set use_context=True to use the new context based callbacks
# Post version 12 this will no longer be necessary
updater = Updater("TOKEN", use_context=True)
updater.dispatcher.add_handler(CommandHandler('start', start))
updater.dispatcher.add_handler(CallbackQueryHandler(button))
+20 -6
View File
@@ -1,9 +1,14 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Simple Bot to print/download all incoming passport data
# This program is dedicated to the public domain under the CC0 license.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""
Simple Bot to print/download all incoming passport data
See https://telegram.org/blog/passport for info about what telegram passport is.
See https://git.io/fAvYd for how to use Telegram Passport properly with python-telegram-bot.
@@ -24,9 +29,9 @@ def msg(bot, update):
# If we received any passport data
passport_data = update.message.passport_data
if passport_data:
# If our payload doesn't match what we think, this Update did not originate from us
# Ideally you would randomize the payload on the server
if passport_data.decrypted_credentials.payload != 'thisisatest':
# If our nonce doesn't match what we think, this Update did not originate from us
# Ideally you would randomize the nonce on the server
if passport_data.decrypted_credentials.nonce != 'thisisatest':
return
# Print the decrypted credential data
@@ -39,7 +44,7 @@ def msg(bot, update):
elif data.type == 'email':
print('Email: ', data.email)
if data.type in ('personal_details', 'passport', 'driver_license', 'identity_card',
'identity_passport', 'address'):
'internal_passport', 'address'):
print(data.type, data.data)
if data.type in ('utility_bill', 'bank_statement', 'rental_agreement',
'passport_registration', 'temporary_registration'):
@@ -65,6 +70,15 @@ def msg(bot, update):
file = data.selfie.get_file()
print(data.type, file)
file.download()
if data.type in ('passport', 'driver_license', 'identity_card',
'internal_passport', 'utility_bill', 'bank_statement',
'rental_agreement', 'passport_registration',
'temporary_registration'):
print(data.type, len(data.translation), 'translation')
for file in data.translation:
actual_file = file.get_file()
print(actual_file)
actual_file.download()
def error(bot, update, error):
+30 -26
View File
@@ -1,15 +1,20 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This program is dedicated to the public domain under the CC0 license.
#
"""Basic example for a bot that can receive payment from user.
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
This program is dedicated to the public domain under the CC0 license.
"""
Basic example for a bot that can receive payment from user.
"""
import logging
from telegram import (LabeledPrice, ShippingOption)
from telegram.ext import (Updater, CommandHandler, MessageHandler,
Filters, PreCheckoutQueryHandler, ShippingQueryHandler)
import logging
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
@@ -18,18 +23,18 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s
logger = logging.getLogger(__name__)
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 start_callback(bot, update):
def start_callback(update, context):
msg = "Use /shipping to get an invoice for shipping-payment, "
msg += "or /noshipping for an invoice without shipping."
update.message.reply_text(msg)
def start_with_shipping_callback(bot, update):
def start_with_shipping_callback(update, context):
chat_id = update.message.chat_id
title = "Payment Example"
description = "Payment Example using python-telegram-bot"
@@ -47,13 +52,13 @@ def start_with_shipping_callback(bot, update):
# optionally pass need_name=True, need_phone_number=True,
# need_email=True, need_shipping_address=True, is_flexible=True
bot.sendInvoice(chat_id, title, description, payload,
provider_token, start_parameter, currency, prices,
need_name=True, need_phone_number=True,
need_email=True, need_shipping_address=True, is_flexible=True)
context.bot.send_invoice(chat_id, title, description, payload,
provider_token, start_parameter, currency, prices,
need_name=True, need_phone_number=True,
need_email=True, need_shipping_address=True, is_flexible=True)
def start_without_shipping_callback(bot, update):
def start_without_shipping_callback(update, context):
chat_id = update.message.chat_id
title = "Payment Example"
description = "Payment Example using python-telegram-bot"
@@ -70,17 +75,16 @@ def start_without_shipping_callback(bot, update):
# optionally pass need_name=True, need_phone_number=True,
# need_email=True, need_shipping_address=True, is_flexible=True
bot.sendInvoice(chat_id, title, description, payload,
provider_token, start_parameter, currency, prices)
context.bot.send_invoice(chat_id, title, description, payload,
provider_token, start_parameter, currency, prices)
def shipping_callback(bot, update):
def shipping_callback(update, context):
query = update.shipping_query
# check the payload, is this from your bot?
if query.invoice_payload != 'Custom-Payload':
# answer False pre_checkout_query
bot.answer_shipping_query(shipping_query_id=query.id, ok=False,
error_message="Something went wrong...")
query.answer(ok=False, error_message="Something went wrong...")
return
else:
options = list()
@@ -89,31 +93,31 @@ def shipping_callback(bot, update):
# an array of LabeledPrice objects
price_list = [LabeledPrice('B1', 150), LabeledPrice('B2', 200)]
options.append(ShippingOption('2', 'Shipping Option B', price_list))
bot.answer_shipping_query(shipping_query_id=query.id, ok=True,
shipping_options=options)
query.answer(ok=True, shipping_options=options)
# after (optional) shipping, it's the pre-checkout
def precheckout_callback(bot, update):
def precheckout_callback(update, context):
query = update.pre_checkout_query
# check the payload, is this from your bot?
if query.invoice_payload != 'Custom-Payload':
# answer False pre_checkout_query
bot.answer_pre_checkout_query(pre_checkout_query_id=query.id, ok=False,
error_message="Something went wrong...")
query.answer(ok=False, error_message="Something went wrong...")
else:
bot.answer_pre_checkout_query(pre_checkout_query_id=query.id, ok=True)
query.answer(ok=True)
# finally, after contacting to the payment provider...
def successful_payment_callback(bot, update):
def successful_payment_callback(update, context):
# do something after successful receive of payment?
update.message.reply_text("Thank you for your payment!")
def main():
# Create the EventHandler and pass it your bot's token.
updater = Updater(token="BOT_TOKEN")
# Create the Updater and pass it your bot's token.
# Make sure to set use_context=True to use the new context based callbacks
# Post version 12 this will no longer be necessary
updater = Updater("TOKEN", use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This program is dedicated to the public domain under the CC0 license.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""
First, a few callback functions are defined. Then, those functions are passed to
the Dispatcher and registered at their respective places.
Then, the bot is started and runs until we press Ctrl-C on the command line.
Usage:
Example of a bot-user conversation using ConversationHandler.
Send /start to initiate the conversation.
Press Ctrl-C on the command line or send a signal to the process to stop the
bot.
"""
from telegram import ReplyKeyboardMarkup
from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters, RegexHandler,
ConversationHandler, PicklePersistence)
import logging
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logger = logging.getLogger(__name__)
CHOOSING, TYPING_REPLY, TYPING_CHOICE = range(3)
reply_keyboard = [['Age', 'Favourite colour'],
['Number of siblings', 'Something else...'],
['Done']]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
def facts_to_str(user_data):
facts = list()
for key, value in user_data.items():
facts.append('{} - {}'.format(key, value))
return "\n".join(facts).join(['\n', '\n'])
def start(update, context):
reply_text = "Hi! My name is Doctor Botter."
if context.user_data:
reply_text += " You already told me your {}. Why don't you tell me something more " \
"about yourself? Or change enything I " \
"already know.".format(", ".join(context.user_data.keys()))
else:
reply_text += " I will hold a more complex conversation with you. Why don't you tell me " \
"something about yourself?"
update.message.reply_text(reply_text, reply_markup=markup)
return CHOOSING
def regular_choice(update, context):
text = update.message.text
context.user_data['choice'] = text
if context.user_data.get(text):
reply_text = 'Your {}, I already know the following ' \
'about that: {}'.format(text.lower(), context.user_data[text.lower()])
else:
reply_text = 'Your {}? Yes, I would love to hear about that!'.format(text.lower())
update.message.reply_text(reply_text)
return TYPING_REPLY
def custom_choice(update, context):
update.message.reply_text('Alright, please send me the category first, '
'for example "Most impressive skill"')
return TYPING_CHOICE
def received_information(update, context):
text = update.message.text
category = context.user_data['choice']
context.user_data[category] = text.lower()
del context.user_data['choice']
update.message.reply_text("Neat! Just so you know, this is what you already told me:"
"{}"
"You can tell me more, or change your opinion on "
"something.".format(facts_to_str(context.user_data)),
reply_markup=markup)
return CHOOSING
def show_data(update, context):
update.message.reply_text("This is what you already told me:"
"{}".format(facts_to_str(context.user_data)))
def done(update, context):
if 'choice' in context.user_data:
del context.user_data['choice']
update.message.reply_text("I learned these facts about you:"
"{}"
"Until next time!".format(facts_to_str(context.user_data)))
return ConversationHandler.END
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, error)
def main():
# Create the Updater and pass it your bot's token.
pp = PicklePersistence(filename='conversationbot')
updater = Updater("TOKEN", persistence=pp, use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
# Add conversation handler with the states CHOOSING, TYPING_CHOICE and TYPING_REPLY
conv_handler = ConversationHandler(
entry_points=[CommandHandler('start', start)],
states={
CHOOSING: [RegexHandler('^(Age|Favourite colour|Number of siblings)$',
regular_choice),
RegexHandler('^Something else...$',
custom_choice),
],
TYPING_CHOICE: [MessageHandler(Filters.text,
regular_choice),
],
TYPING_REPLY: [MessageHandler(Filters.text,
received_information),
],
},
fallbacks=[RegexHandler('^Done$', done)],
name="my_conversation",
persistent=True
)
dp.add_handler(conv_handler)
show_data_handler = CommandHandler('show_data', show_data)
dp.add_handler(show_data_handler)
# log all errors
dp.add_error_handler(error)
# Start the Bot
updater.start_polling()
# Run the bot until you press Ctrl-C or the process receives SIGINT,
# SIGTERM or SIGABRT. This should be used most of the time, since
# start_polling() is non-blocking and will stop the bot gracefully.
updater.idle()
if __name__ == '__main__':
main()
+27 -19
View File
@@ -1,10 +1,13 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Simple Bot to send timed Telegram messages.
# This program is dedicated to the public domain under the CC0 license.
#
# THIS EXAMPLE HAS BEEN UPDATED TO WORK WITH THE BETA VERSION 12 OF PYTHON-TELEGRAM-BOT.
# If you're still using version 11.1.0, please see the examples at
# https://github.com/python-telegram-bot/python-telegram-bot/tree/v11.1.0/examples
"""
Simple Bot to send timed Telegram messages.
This Bot uses the Updater class to handle the bot and the JobQueue to send
timed messages.
@@ -19,9 +22,10 @@ Press Ctrl-C on the command line or send a signal to the process to stop the
bot.
"""
from telegram.ext import Updater, CommandHandler
import logging
from telegram.ext import Updater, CommandHandler
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
@@ -31,28 +35,29 @@ logger = logging.getLogger(__name__)
# Define a few command handlers. These usually take the two arguments bot and
# update. Error handlers also receive the raised TelegramError object in error.
def start(bot, update):
def start(update, context):
update.message.reply_text('Hi! Use /set <seconds> to set a timer')
def alarm(bot, job):
def alarm(context):
"""Send the alarm message."""
bot.send_message(job.context, text='Beep!')
job = context.job
context.bot.send_message(job.context, text='Beep!')
def set_timer(bot, update, args, job_queue, chat_data):
def set_timer(update, context):
"""Add a job to the queue."""
chat_id = update.message.chat_id
try:
# args[0] should contain the time for the timer in seconds
due = int(args[0])
due = int(context.args[0])
if due < 0:
update.message.reply_text('Sorry we can not go back to future!')
return
# Add job to queue
job = job_queue.run_once(alarm, due, context=chat_id)
chat_data['job'] = job
job = context.job_queue.run_once(alarm, due, context=chat_id)
context.chat_data['job'] = job
update.message.reply_text('Timer successfully set!')
@@ -60,27 +65,30 @@ def set_timer(bot, update, args, job_queue, chat_data):
update.message.reply_text('Usage: /set <seconds>')
def unset(bot, update, chat_data):
def unset(update, context):
"""Remove the job if the user changed their mind."""
if 'job' not in chat_data:
if 'job' not in context.chat_data:
update.message.reply_text('You have no active timer')
return
job = chat_data['job']
job = context.chat_data['job']
job.schedule_removal()
del chat_data['job']
del context.chat_data['job']
update.message.reply_text('Timer successfully unset!')
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():
"""Run bot."""
updater = Updater("TOKEN")
# Create the Updater and pass it your bot's token.
# Make sure to set use_context=True to use the new context based callbacks
# Post version 12 this will no longer be necessary
updater = Updater("TOKEN", use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
+1 -1
View File
@@ -5,6 +5,6 @@ flaky
yapf
pre-commit
beautifulsoup4
pytest
pytest==4.2.0
pytest-timeout
wheel
+1
View File
@@ -1,3 +1,4 @@
future>=0.16.0
certifi
tornado>=5.1
cryptography
+1
View File
@@ -27,6 +27,7 @@ addopts = --no-success-flaky-report -rsxX
filterwarnings =
error
ignore::DeprecationWarning
ignore::telegram.utils.deprecate.TelegramDeprecationWarning
[coverage:run]
branch = True
+2 -1
View File
@@ -55,5 +55,6 @@ with codecs.open('README.rst', 'r', 'utf-8') as fd:
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6'
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7'
],)
+7 -3
View File
@@ -109,13 +109,16 @@ from .passport.passportelementerrors import (PassportElementError,
PassportElementErrorFiles,
PassportElementErrorFrontSide,
PassportElementErrorReverseSide,
PassportElementErrorSelfie)
PassportElementErrorSelfie,
PassportElementErrorTranslationFile,
PassportElementErrorTranslationFiles,
PassportElementErrorUnspecified)
from .passport.credentials import (Credentials,
DataCredentials,
SecureData,
FileCredentials,
TelegramDecryptionError)
from .version import __version__ # flake8: noqa
from .version import __version__ # noqa: F401
__author__ = 'devs@python-telegram-bot.org'
@@ -148,5 +151,6 @@ __all__ = [
'Credentials', 'DataCredentials', 'SecureData', 'FileCredentials', 'IdDocumentData',
'PersonalDetails', 'ResidentialAddress', 'InputMediaVideo', 'InputMediaAnimation',
'InputMediaAudio', 'InputMediaDocument', 'TelegramDecryptionError',
'PassportElementErrorSelfie'
'PassportElementErrorSelfie', 'PassportElementErrorTranslationFile',
'PassportElementErrorTranslationFiles', 'PassportElementErrorUnspecified'
]
+100 -96
View File
@@ -21,6 +21,7 @@
"""This module contains an object that represents a Telegram Bot."""
import functools
try:
import ujson as json
except ImportError:
@@ -70,33 +71,6 @@ def log(func):
return decorator
def message(func):
@functools.wraps(func)
def decorator(self, *args, **kwargs):
url, data = func(self, *args, **kwargs)
if kwargs.get('reply_to_message_id'):
data['reply_to_message_id'] = kwargs.get('reply_to_message_id')
if kwargs.get('disable_notification'):
data['disable_notification'] = kwargs.get('disable_notification')
if kwargs.get('reply_markup'):
reply_markup = kwargs.get('reply_markup')
if isinstance(reply_markup, ReplyMarkup):
data['reply_markup'] = reply_markup.to_json()
else:
data['reply_markup'] = reply_markup
result = self._request.post(url, data, timeout=kwargs.get('timeout'))
if result is True:
return result
return Message.de_json(result, self)
return decorator
class Bot(TelegramObject):
"""This object represents a Telegram Bot.
@@ -132,6 +106,27 @@ class Bot(TelegramObject):
password=private_key_password,
backend=default_backend())
def _message(self, url, data, reply_to_message_id=None, disable_notification=None,
reply_markup=None, timeout=None, **kwargs):
if reply_to_message_id is not None:
data['reply_to_message_id'] = reply_to_message_id
if disable_notification is not None:
data['disable_notification'] = disable_notification
if reply_markup is not None:
if isinstance(reply_markup, ReplyMarkup):
data['reply_markup'] = reply_markup.to_json()
else:
data['reply_markup'] = reply_markup
result = self._request.post(url, data, timeout=timeout)
if result is True:
return result
return Message.de_json(result, self)
@property
def request(self):
return self._request
@@ -208,7 +203,6 @@ class Bot(TelegramObject):
return self.bot
@log
@message
def send_message(self,
chat_id,
text,
@@ -259,7 +253,9 @@ class Bot(TelegramObject):
if disable_web_page_preview:
data['disable_web_page_preview'] = disable_web_page_preview
return url, data
return self._message(url, data, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
timeout=timeout, **kwargs)
@log
def delete_message(self, chat_id, message_id, timeout=None, **kwargs):
@@ -298,7 +294,6 @@ class Bot(TelegramObject):
return result
@log
@message
def forward_message(self,
chat_id,
from_chat_id,
@@ -340,10 +335,10 @@ class Bot(TelegramObject):
if message_id:
data['message_id'] = message_id
return url, data
return self._message(url, data, disable_notification=disable_notification,
timeout=timeout, **kwargs)
@log
@message
def send_photo(self,
chat_id,
photo,
@@ -369,7 +364,7 @@ class Bot(TelegramObject):
Internet, or upload a new photo using multipart/form-data. Lastly you can pass
an existing :class:`telegram.PhotoSize` object to send.
caption (:obj:`str`, optional): Photo caption (may also be used when resending photos
by file_id), 0-200 characters.
by file_id), 0-1024 characters.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
show bold, italic, fixed-width text or inline URLs in the media caption. See the
constants in :class:`telegram.ParseMode` for the available modes.
@@ -404,10 +399,11 @@ class Bot(TelegramObject):
if parse_mode:
data['parse_mode'] = parse_mode
return url, data
return self._message(url, data, timeout=timeout, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
**kwargs)
@log
@message
def send_audio(self,
chat_id,
audio,
@@ -442,7 +438,7 @@ class Bot(TelegramObject):
(recommended), pass an HTTP URL as a String for Telegram to get an audio file from
the Internet, or upload a new one using multipart/form-data. Lastly you can pass
an existing :class:`telegram.Audio` object to send.
caption (:obj:`str`, optional): Audio caption, 0-200 characters.
caption (:obj:`str`, optional): Audio caption, 0-1024 characters.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
show bold, italic, fixed-width text or inline URLs in the media caption. See the
constants in :class:`telegram.ParseMode` for the available modes.
@@ -494,10 +490,11 @@ class Bot(TelegramObject):
thumb = InputFile(thumb, attach=True)
data['thumb'] = thumb
return url, data
return self._message(url, data, timeout=timeout, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
**kwargs)
@log
@message
def send_document(self,
chat_id,
document,
@@ -527,7 +524,7 @@ class Bot(TelegramObject):
filename (:obj:`str`, optional): File name that shows in telegram message (it is useful
when you send file generated by temp module, for example). Undocumented.
caption (:obj:`str`, optional): Document caption (may also be used when resending
documents by file_id), 0-200 characters.
documents by file_id), 0-1024 characters.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
show bold, italic, fixed-width text or inline URLs in the media caption. See the
constants in :class:`telegram.ParseMode` for the available modes.
@@ -570,10 +567,11 @@ class Bot(TelegramObject):
thumb = InputFile(thumb, attach=True)
data['thumb'] = thumb
return url, data
return self._message(url, data, timeout=timeout, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
**kwargs)
@log
@message
def send_sticker(self,
chat_id,
sticker,
@@ -622,10 +620,11 @@ class Bot(TelegramObject):
data = {'chat_id': chat_id, 'sticker': sticker}
return url, data
return self._message(url, data, timeout=timeout, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
**kwargs)
@log
@message
def send_video(self,
chat_id,
video,
@@ -661,7 +660,7 @@ class Bot(TelegramObject):
width (:obj:`int`, optional): Video width.
height (:obj:`int`, optional): Video height.
caption (:obj:`str`, optional): Video caption (may also be used when resending videos
by file_id), 0-200 characters.
by file_id), 0-1024 characters.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
show bold, italic, fixed-width text or inline URLs in the media caption. See the
constants in :class:`telegram.ParseMode` for the available modes.
@@ -714,10 +713,11 @@ class Bot(TelegramObject):
thumb = InputFile(thumb, attach=True)
data['thumb'] = thumb
return url, data
return self._message(url, data, timeout=timeout, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
**kwargs)
@log
@message
def send_video_note(self,
chat_id,
video_note,
@@ -784,10 +784,11 @@ class Bot(TelegramObject):
thumb = InputFile(thumb, attach=True)
data['thumb'] = thumb
return url, data
return self._message(url, data, timeout=timeout, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
**kwargs)
@log
@message
def send_animation(self,
chat_id,
animation,
@@ -821,7 +822,7 @@ class Bot(TelegramObject):
A thumbnail's width and height should not exceed 90. Ignored if the file is not
is passed as a string or file_id.
caption (:obj:`str`, optional): Animation caption (may also be used when resending
animations by file_id), 0-200 characters.
animations by file_id), 0-1024 characters.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
show bold, italic, fixed-width text or inline URLs in the media caption. See the
constants in :class:`telegram.ParseMode` for the available modes.
@@ -866,10 +867,11 @@ class Bot(TelegramObject):
if parse_mode:
data['parse_mode'] = parse_mode
return url, data
return self._message(url, data, timeout=timeout, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
**kwargs)
@log
@message
def send_voice(self,
chat_id,
voice,
@@ -898,7 +900,7 @@ class Bot(TelegramObject):
(recommended), pass an HTTP URL as a String for Telegram to get an voice file from
the Internet, or upload a new one using multipart/form-data. Lastly you can pass
an existing :class:`telegram.Voice` object to send.
caption (:obj:`str`, optional): Voice message caption, 0-200 characters.
caption (:obj:`str`, optional): Voice message caption, 0-1024 characters.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to
show bold, italic, fixed-width text or inline URLs in the media caption. See the
constants in :class:`telegram.ParseMode` for the available modes.
@@ -936,7 +938,9 @@ class Bot(TelegramObject):
if parse_mode:
data['parse_mode'] = parse_mode
return url, data
return self._message(url, data, timeout=timeout, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
**kwargs)
@log
def send_media_group(self,
@@ -981,7 +985,6 @@ class Bot(TelegramObject):
return [Message.de_json(res, self) for res in result]
@log
@message
def send_location(self,
chat_id,
latitude=None,
@@ -1044,10 +1047,11 @@ class Bot(TelegramObject):
if live_period:
data['live_period'] = live_period
return url, data
return self._message(url, data, timeout=timeout, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
**kwargs)
@log
@message
def edit_message_live_location(self,
chat_id=None,
message_id=None,
@@ -1056,6 +1060,7 @@ class Bot(TelegramObject):
longitude=None,
location=None,
reply_markup=None,
timeout=None,
**kwargs):
"""Use this method to edit live location messages sent by the bot or via the bot
(for inline bots). A location can be edited until its :attr:`live_period` expires or
@@ -1107,15 +1112,15 @@ class Bot(TelegramObject):
if inline_message_id:
data['inline_message_id'] = inline_message_id
return url, data
return self._message(url, data, timeout=timeout, reply_markup=reply_markup, **kwargs)
@log
@message
def stop_message_live_location(self,
chat_id=None,
message_id=None,
inline_message_id=None,
reply_markup=None,
timeout=None,
**kwargs):
"""Use this method to stop updating a live location message sent by the bot or via the bot
(for inline bots) before live_period expires.
@@ -1149,10 +1154,9 @@ class Bot(TelegramObject):
if inline_message_id:
data['inline_message_id'] = inline_message_id
return url, data
return self._message(url, data, timeout=timeout, reply_markup=reply_markup, **kwargs)
@log
@message
def send_venue(self,
chat_id,
latitude=None,
@@ -1232,10 +1236,11 @@ class Bot(TelegramObject):
if foursquare_type:
data['foursquare_type'] = foursquare_type
return url, data
return self._message(url, data, timeout=timeout, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
**kwargs)
@log
@message
def send_contact(self,
chat_id,
phone_number=None,
@@ -1301,10 +1306,11 @@ class Bot(TelegramObject):
if vcard:
data['vcard'] = vcard
return url, data
return self._message(url, data, timeout=timeout, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
**kwargs)
@log
@message
def send_game(self,
chat_id,
game_short_name,
@@ -1343,7 +1349,9 @@ class Bot(TelegramObject):
data = {'chat_id': chat_id, 'game_short_name': game_short_name}
return url, data
return self._message(url, data, timeout=timeout, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
**kwargs)
@log
def send_chat_action(self, chat_id, action, timeout=None, **kwargs):
@@ -1677,7 +1685,6 @@ class Bot(TelegramObject):
return result
@log
@message
def edit_message_text(self,
text,
chat_id=None,
@@ -1736,10 +1743,9 @@ class Bot(TelegramObject):
if disable_web_page_preview:
data['disable_web_page_preview'] = disable_web_page_preview
return url, data
return self._message(url, data, timeout=timeout, reply_markup=reply_markup, **kwargs)
@log
@message
def edit_message_caption(self,
chat_id=None,
message_id=None,
@@ -1755,7 +1761,7 @@ class Bot(TelegramObject):
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target`channel (in the format @channelusername).
of the target channel (in the format @channelusername).
message_id (:obj:`int`, optional): Required if inline_message_id is not specified.
Identifier of the sent message.
inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not
@@ -1800,10 +1806,9 @@ class Bot(TelegramObject):
if inline_message_id:
data['inline_message_id'] = inline_message_id
return url, data
return self._message(url, data, timeout=timeout, reply_markup=reply_markup, **kwargs)
@log
@message
def edit_message_media(self,
chat_id=None,
message_id=None,
@@ -1821,7 +1826,7 @@ class Bot(TelegramObject):
Args:
chat_id (:obj:`int` | :obj:`str`, optional): Unique identifier for the target chat or
username of the target`channel (in the format @channelusername).
username of the target channel (in the format @channelusername).
message_id (:obj:`int`, optional): Required if inline_message_id is not specified.
Identifier of the sent message.
inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not
@@ -1853,10 +1858,9 @@ class Bot(TelegramObject):
if inline_message_id:
data['inline_message_id'] = inline_message_id
return url, data
return self._message(url, data, timeout=timeout, reply_markup=reply_markup, **kwargs)
@log
@message
def edit_message_reply_markup(self,
chat_id=None,
message_id=None,
@@ -1870,7 +1874,7 @@ class Bot(TelegramObject):
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target`channel (in the format @channelusername).
of the target channel (in the format @channelusername).
message_id (:obj:`int`, optional): Required if inline_message_id is not specified.
Identifier of the sent message.
inline_message_id (:obj:`str`, optional): Required if chat_id and message_id are not
@@ -1907,7 +1911,7 @@ class Bot(TelegramObject):
if inline_message_id:
data['inline_message_id'] = inline_message_id
return url, data
return self._message(url, data, timeout=timeout, reply_markup=reply_markup, **kwargs)
@log
def get_updates(self,
@@ -2104,7 +2108,7 @@ class Bot(TelegramObject):
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target`channel (in the format @channelusername).
of the target channel (in the format @channelusername).
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
@@ -2134,7 +2138,7 @@ class Bot(TelegramObject):
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target`channel (in the format @channelusername).
of the target channel (in the format @channelusername).
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
@@ -2166,7 +2170,7 @@ class Bot(TelegramObject):
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target`channel (in the format @channelusername).
of the target channel (in the format @channelusername).
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
@@ -2194,7 +2198,7 @@ class Bot(TelegramObject):
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target`channel (in the format @channelusername).
of the target channel (in the format @channelusername).
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
@@ -2222,7 +2226,7 @@ class Bot(TelegramObject):
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target`channel (in the format @channelusername).
of the target channel (in the format @channelusername).
user_id (:obj:`int`): Unique identifier of the target user.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
@@ -2326,7 +2330,6 @@ class Bot(TelegramObject):
return WebhookInfo.de_json(result, self)
@log
@message
def set_game_score(self,
user_id,
score,
@@ -2385,7 +2388,7 @@ class Bot(TelegramObject):
if disable_edit_message is not None:
data['disable_edit_message'] = disable_edit_message
return url, data
return self._message(url, data, timeout=timeout, **kwargs)
@log
def get_game_high_scores(self,
@@ -2436,7 +2439,6 @@ class Bot(TelegramObject):
return [GameHighScore.de_json(hs, self) for hs in result]
@log
@message
def send_invoice(self,
chat_id,
title,
@@ -2560,7 +2562,9 @@ class Bot(TelegramObject):
if send_email_to_provider is not None:
data['send_email_to_provider'] = send_email_to_provider
return url, data
return self._message(url, data, timeout=timeout, disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id, reply_markup=reply_markup,
**kwargs)
@log
def answer_shipping_query(self,
@@ -2816,7 +2820,7 @@ class Bot(TelegramObject):
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target`channel (in the format @channelusername).
of the target channel (in the format @channelusername).
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
@@ -2839,7 +2843,7 @@ class Bot(TelegramObject):
return result
@log
def set_chat_photo(self, chat_id, photo, timeout=None, **kwargs):
def set_chat_photo(self, chat_id, photo, timeout=20, **kwargs):
"""Use this method to set a new profile photo for the chat.
Photos can't be changed for private chats. The bot must be an administrator in the chat
@@ -2847,7 +2851,7 @@ class Bot(TelegramObject):
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target`channel (in the format @channelusername).
of the target channel (in the format @channelusername).
photo (`filelike object`): New chat photo.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
@@ -2886,7 +2890,7 @@ class Bot(TelegramObject):
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target`channel (in the format @channelusername).
of the target channel (in the format @channelusername).
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
@@ -2921,7 +2925,7 @@ class Bot(TelegramObject):
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target`channel (in the format @channelusername).
of the target channel (in the format @channelusername).
title (:obj:`str`): New chat title, 1-255 characters.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
@@ -2956,7 +2960,7 @@ class Bot(TelegramObject):
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target`channel (in the format @channelusername).
of the target channel (in the format @channelusername).
description (:obj:`str`): New chat description, 1-255 characters.
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
@@ -2988,7 +2992,7 @@ class Bot(TelegramObject):
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target`channel (in the format @channelusername).
of the target channel (in the format @channelusername).
message_id (:obj:`int`): Identifier of a message to pin.
disable_notification (:obj:`bool`, optional): Pass True, if it is not necessary to send
a notification to all group members about the new pinned message.
@@ -3024,7 +3028,7 @@ class Bot(TelegramObject):
Args:
chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target chat or username
of the target`channel (in the format @channelusername).
of the target channel (in the format @channelusername).
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
@@ -3075,7 +3079,7 @@ class Bot(TelegramObject):
return StickerSet.de_json(result, self)
@log
def upload_sticker_file(self, user_id, png_sticker, timeout=None, **kwargs):
def upload_sticker_file(self, user_id, png_sticker, timeout=20, **kwargs):
"""
Use this method to upload a .png file with a sticker for later use in
:attr:`create_new_sticker_set` and :attr:`add_sticker_to_set` methods (can be used multiple
@@ -3116,7 +3120,7 @@ class Bot(TelegramObject):
@log
def create_new_sticker_set(self, user_id, name, title, png_sticker, emojis,
contains_masks=None, mask_position=None, timeout=None, **kwargs):
contains_masks=None, mask_position=None, timeout=20, **kwargs):
"""Use this method to create new sticker set owned by a user.
The bot will be able to edit the created sticker set.
@@ -3176,7 +3180,7 @@ class Bot(TelegramObject):
@log
def add_sticker_to_set(self, user_id, name, png_sticker, emojis, mask_position=None,
timeout=None, **kwargs):
timeout=20, **kwargs):
"""Use this method to add a new sticker to a set created by the bot.
Note:
+28 -19
View File
@@ -116,16 +116,16 @@ class CallbackQuery(TelegramObject):
"""
return self.bot.answerCallbackQuery(self.id, *args, **kwargs)
def edit_message_text(self, *args, **kwargs):
def edit_message_text(self, text, *args, **kwargs):
"""Shortcut for either::
bot.edit_message_text(chat_id=update.callback_query.message.chat_id,
bot.edit_message_text(text, chat_id=update.callback_query.message.chat_id,
message_id=update.callback_query.message.message_id,
*args, **kwargs)
or::
bot.edit_message_text(inline_message_id=update.callback_query.inline_message_id,
bot.edit_message_text(text, inline_message_id=update.callback_query.inline_message_id,
*args, **kwargs)
Returns:
@@ -134,22 +134,24 @@ class CallbackQuery(TelegramObject):
"""
if self.inline_message_id:
return self.bot.edit_message_text(
inline_message_id=self.inline_message_id, *args, **kwargs)
return self.bot.edit_message_text(text, inline_message_id=self.inline_message_id,
*args, **kwargs)
else:
return self.bot.edit_message_text(
chat_id=self.message.chat_id, message_id=self.message.message_id, *args, **kwargs)
return self.bot.edit_message_text(text, chat_id=self.message.chat_id,
message_id=self.message.message_id, *args, **kwargs)
def edit_message_caption(self, *args, **kwargs):
def edit_message_caption(self, caption, *args, **kwargs):
"""Shortcut for either::
bot.edit_message_caption(chat_id=update.callback_query.message.chat_id,
bot.edit_message_caption(caption=caption,
chat_id=update.callback_query.message.chat_id,
message_id=update.callback_query.message.message_id,
*args, **kwargs)
or::
bot.edit_message_caption(inline_message_id=update.callback_query.inline_message_id,
bot.edit_message_caption(caption=caption
inline_message_id=update.callback_query.inline_message_id,
*args, **kwargs)
Returns:
@@ -158,22 +160,26 @@ class CallbackQuery(TelegramObject):
"""
if self.inline_message_id:
return self.bot.edit_message_caption(
inline_message_id=self.inline_message_id, *args, **kwargs)
return self.bot.edit_message_caption(caption=caption,
inline_message_id=self.inline_message_id,
*args, **kwargs)
else:
return self.bot.edit_message_caption(
chat_id=self.message.chat_id, message_id=self.message.message_id, *args, **kwargs)
return self.bot.edit_message_caption(caption=caption, chat_id=self.message.chat_id,
message_id=self.message.message_id,
*args, **kwargs)
def edit_message_reply_markup(self, *args, **kwargs):
def edit_message_reply_markup(self, reply_markup, *args, **kwargs):
"""Shortcut for either::
bot.edit_message_replyMarkup(chat_id=update.callback_query.message.chat_id,
message_id=update.callback_query.message.message_id,
reply_markup=reply_markup,
*args, **kwargs)
or::
bot.edit_message_reply_markup(inline_message_id=update.callback_query.inline_message_id,
reply_markup=reply_markup,
*args, **kwargs)
Returns:
@@ -182,8 +188,11 @@ class CallbackQuery(TelegramObject):
"""
if self.inline_message_id:
return self.bot.edit_message_reply_markup(
inline_message_id=self.inline_message_id, *args, **kwargs)
return self.bot.edit_message_reply_markup(reply_markup=reply_markup,
inline_message_id=self.inline_message_id,
*args, **kwargs)
else:
return self.bot.edit_message_reply_markup(
chat_id=self.message.chat_id, message_id=self.message.message_id, *args, **kwargs)
return self.bot.edit_message_reply_markup(reply_markup=reply_markup,
chat_id=self.message.chat_id,
message_id=self.message.message_id,
*args, **kwargs)
+2 -2
View File
@@ -21,7 +21,7 @@ The following constants were extracted from the
Attributes:
MAX_MESSAGE_LENGTH (:obj:`int`): 4096
MAX_CAPTION_LENGTH (:obj:`int`): 200
MAX_CAPTION_LENGTH (:obj:`int`): 1024
SUPPORTED_WEBHOOK_PORTS (List[:obj:`int`]): [443, 80, 88, 8443]
MAX_FILESIZE_DOWNLOAD (:obj:`int`): In bytes (20MB)
MAX_FILESIZE_UPLOAD (:obj:`int`): In bytes (50MB)
@@ -40,7 +40,7 @@ Attributes:
"""
MAX_MESSAGE_LENGTH = 4096
MAX_CAPTION_LENGTH = 200
MAX_CAPTION_LENGTH = 1024
# constants above this line are tested
+13 -2
View File
@@ -57,7 +57,6 @@ class Unauthorized(TelegramError):
class InvalidToken(TelegramError):
def __init__(self):
super(InvalidToken, self).__init__('Invalid token')
@@ -71,7 +70,6 @@ class BadRequest(NetworkError):
class TimedOut(NetworkError):
def __init__(self):
super(TimedOut, self).__init__('Timed out')
@@ -100,3 +98,16 @@ class RetryAfter(TelegramError):
super(RetryAfter,
self).__init__('Flood control exceeded. Retry in {} seconds'.format(retry_after))
self.retry_after = float(retry_after)
class Conflict(TelegramError):
"""
Raised when a long poll or webhook conflicts with another one.
Args:
msg (:obj:`str`): The message from telegrams server.
"""
def __init__(self, msg):
super(Conflict, self).__init__(msg)
+9 -4
View File
@@ -18,16 +18,20 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""Extensions over the Telegram Bot API to facilitate bot making"""
from .basepersistence import BasePersistence
from .picklepersistence import PicklePersistence
from .dictpersistence import DictPersistence
from .handler import Handler
from .callbackcontext import CallbackContext
from .dispatcher import Dispatcher, DispatcherHandlerStop, run_async
from .jobqueue import JobQueue, Job
from .updater import Updater
from .callbackqueryhandler import CallbackQueryHandler
from .choseninlineresulthandler import ChosenInlineResultHandler
from .commandhandler import CommandHandler
from .handler import Handler
from .inlinequeryhandler import InlineQueryHandler
from .messagehandler import MessageHandler
from .filters import BaseFilter, Filters
from .messagehandler import MessageHandler
from .commandhandler import CommandHandler, PrefixHandler
from .regexhandler import RegexHandler
from .stringcommandhandler import StringCommandHandler
from .stringregexhandler import StringRegexHandler
@@ -43,4 +47,5 @@ __all__ = ('Dispatcher', 'JobQueue', 'Job', 'Updater', 'CallbackQueryHandler',
'MessageHandler', 'BaseFilter', 'Filters', 'RegexHandler', 'StringCommandHandler',
'StringRegexHandler', 'TypeHandler', 'ConversationHandler',
'PreCheckoutQueryHandler', 'ShippingQueryHandler', 'MessageQueue', 'DelayQueue',
'DispatcherHandlerStop', 'run_async')
'DispatcherHandlerStop', 'run_async', 'CallbackContext', 'BasePersistence',
'PicklePersistence', 'DictPersistence', 'PrefixHandler')
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2018
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the BasePersistence class."""
class BasePersistence(object):
"""Interface class for adding persistence to your bot.
Subclass this object for different implementations of a persistent bot.
All relevant methods must be overwritten. This means:
* If :attr:`store_chat_data` is ``True`` you must overwrite :meth:`get_chat_data` and
:meth:`update_chat_data`.
* If :attr:`store_user_data` is ``True`` you must overwrite :meth:`get_user_data` and
:meth:`update_user_data`.
* If you want to store conversation data with :class:`telegram.ext.ConversationHandler`, you
must overwrite :meth:`get_conversations` and :meth:`update_conversation`.
* :meth:`flush` will be called when the bot is shutdown.
Attributes:
store_user_data (:obj:`bool`): Optional, Whether user_data should be saved by this
persistence class.
store_chat_data (:obj:`bool`): Optional. Whether chat_data should be saved by this
persistence class.
Args:
store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this
persistence class. Default is ``True``.
store_chat_data (:obj:`bool`, optional): Whether chat_data should be saved by this
persistence class. Default is ``True`` .
"""
def __init__(self, store_user_data=True, store_chat_data=True):
self.store_user_data = store_user_data
self.store_chat_data = store_chat_data
def get_user_data(self):
""""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
persistence object. It should return the user_data if stored, or an empty
``defaultdict(dict)``.
Returns:
:obj:`defaultdict`: The restored user data.
"""
raise NotImplementedError
def get_chat_data(self):
""""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
persistence object. It should return the chat_data if stored, or an empty
``defaultdict(dict)``.
Returns:
:obj:`defaultdict`: The restored chat data.
"""
raise NotImplementedError
def get_conversations(self, name):
""""Will be called by :class:`telegram.ext.Dispatcher` when a
:class:`telegram.ext.ConversationHandler` is added if
:attr:`telegram.ext.ConversationHandler.persistent` is ``True``.
It should return the conversations for the handler with `name` or an empty ``dict``
Args:
name (:obj:`str`): The handlers name.
Returns:
:obj:`dict`: The restored conversations for the handler.
"""
raise NotImplementedError
def update_conversation(self, name, key, new_state):
"""Will be called when a :attr:`telegram.ext.ConversationHandler.update_state`
is called. this allows the storeage of the new state in the persistence.
Args:
name (:obj:`str`): The handlers name.
key (:obj:`tuple`): The key the state is changed for.
new_state (:obj:`tuple` | :obj:`any`): The new state for the given key.
"""
raise NotImplementedError
def update_user_data(self, user_id, data):
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
handled an update.
Args:
user_id (:obj:`int`): The user the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data`[user_id].
"""
raise NotImplementedError
def update_chat_data(self, chat_id, data):
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
handled an update.
Args:
chat_id (:obj:`int`): The chat the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data`[user_id].
"""
raise NotImplementedError
def flush(self):
"""Will be called by :class:`telegram.ext.Updater` upon receiving a stop signal. Gives the
persistence a chance to finish up saving or close a database connection gracefully. If this
is not of any importance just pass will be sufficient.
"""
pass
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2018
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the CallbackContext class."""
from telegram import Update
class CallbackContext(object):
"""
This is a context object passed to the callback called by :class:`telegram.ext.Handler`
or by the :class:`telegram.ext.Dispatcher` in an error handler added by
:attr:`telegram.ext.Dispatcher.add_error_handler` or to the callback of a
:class:`telegram.ext.Job`.
Note:
:class:`telegram.ext.Dispatcher` will create a single context for an entire update. This
means that if you got 2 handlers in different groups and they both get called, they will
get passed the same `CallbackContext` object (of course with proper attributes like
`.matches` differing). This allows you to add custom attributes in a lower handler group
callback, and then subsequently access those attributes in a higher handler group callback.
Note that the attributes on `CallbackContext` might change in the future, so make sure to
use a fairly unique name for the attributes.
Warning:
Do not combine custom attributes and @run_async. Due to how @run_async works, it will
almost certainly execute the callbacks for an update out of order, and the attributes
that you think you added will not be present.
Attributes:
chat_data (:obj:`dict`, optional): A dict that can be used to keep any data in. For each
update from the same chat it will be the same ``dict``.
user_data (:obj:`dict`, optional): A dict that can be used to keep any data in. For each
update from the same user it will be the same ``dict``.
matches (List[:obj:`re match object`], optional): If the associated update originated from
a regex-supported handler or had a :class:`Filters.regex`, this will contain a list of
match objects for every pattern where ``re.search(pattern, string)`` returned a match.
args (List[:obj:`str`], optional): Arguments passed to a command if the associated update
is handled by :class:`telegram.ext.CommandHandler`, :class:`telegram.ext.PrefixHandler`
or :class:`telegram.ext.StringCommandHandler`. It contains a list of the words in the
text after the command, using any whitespace string as a delimiter.
error (:class:`telegram.TelegramError`, optional): The Telegram error that was raised.
Only present when passed to a error handler registered with
:attr:`telegram.ext.Dispatcher.add_error_handler`.
job (:class:`telegram.ext.Job`): The job that that originated this callback.
Only present when passed to the callback of :class:`telegram.ext.Job`.
"""
def __init__(self, dispatcher):
"""
Args:
dispatcher (:class:`telegram.ext.Dispatcher`):
"""
if not dispatcher.use_context:
raise ValueError('CallbackContext should not be used with a non context aware '
'dispatcher!')
self._dispatcher = dispatcher
self.chat_data = None
self.user_data = None
self.args = None
self.matches = None
self.error = None
self.job = None
@classmethod
def from_error(cls, update, error, dispatcher):
self = cls.from_update(update, dispatcher)
self.error = error
return self
@classmethod
def from_update(cls, update, dispatcher):
self = cls(dispatcher)
if update is not None and isinstance(update, Update):
chat = update.effective_chat
user = update.effective_user
if chat:
self.chat_data = dispatcher.chat_data[chat.id]
if user:
self.user_data = dispatcher.user_data[user.id]
return self
@classmethod
def from_job(cls, job, dispatcher):
self = cls(dispatcher)
self.job = job
return self
def update(self, data):
self.__dict__.update(data)
@property
def bot(self):
""":class:`telegram.Bot`: The bot associated with this context."""
return self._dispatcher.bot
@property
def job_queue(self):
"""
:class:`telegram.ext.JobQueue`: The ``JobQueue`` used by the
:class:`telegram.ext.Dispatcher` and (usually) the :class:`telegram.ext.Updater`
associated with this context.
"""
return self._dispatcher.job_queue
@property
def update_queue(self):
"""
:class:`queue.Queue`: The ``Queue`` instance used by the
:class:`telegram.ext.Dispatcher` and (usually) the :class:`telegram.ext.Updater`
associated with this context.
"""
return self._dispatcher.update_queue
@property
def match(self):
"""
`Regex match type`: The first match from :attr:`matches`.
Useful if you are only filtering using a single regex filter.
Returns `None` if :attr:`matches` is empty.
"""
try:
return self.matches[0] # pylint: disable=unsubscriptable-object
except (IndexError, TypeError):
return None
+35 -24
View File
@@ -33,19 +33,19 @@ class CallbackQueryHandler(Handler):
Attributes:
callback (:obj:`callable`): The callback function for this handler.
pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
passed to the callback function.
pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
pattern (:obj:`str` | `Pattern`): Optional. Regex pattern to test
:attr:`telegram.CallbackQuery.data` against.
pass_groups (:obj:`bool`): Optional. Determines whether ``groups`` will be passed to the
pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the
callback function.
pass_groupdict (:obj:`bool`): Optional. Determines whether ``groupdict``. will be passed to
pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to
the callback function.
pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
the callback function.
pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
Note:
@@ -54,31 +54,45 @@ class CallbackQueryHandler(Handler):
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Args:
callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
It will be called when the :attr:`check_update` has determined that an update should be
processed by this handler.
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
Callback signature for context based API:
``def callback(update: Update, context: CallbackContext)``
The return value of the callback is usually ignored except for the special case of
:class:`telegram.ext.ConversationHandler`.
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pattern (:obj:`str` | `Pattern`, optional): Regex pattern. If not ``None``, ``re.match``
is used on :attr:`telegram.CallbackQuery.data` to determine if an update should be
handled by this handler.
pass_groups (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groups()`` as a keyword argument called ``groups``.
Default is ``False``
DEPRECATED: Please switch to context based callbacks.
pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``.
Default is ``False``
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``chat_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
"""
@@ -119,25 +133,22 @@ class CallbackQueryHandler(Handler):
if self.pattern:
if update.callback_query.data:
match = re.match(self.pattern, update.callback_query.data)
return bool(match)
if match:
return match
else:
return True
def handle_update(self, update, dispatcher):
"""Send the update to the :attr:`callback`.
Args:
update (:class:`telegram.Update`): Incoming telegram update.
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
"""
optional_args = self.collect_optional_args(dispatcher, update)
def collect_optional_args(self, dispatcher, update=None, check_result=None):
optional_args = super(CallbackQueryHandler, self).collect_optional_args(dispatcher,
update,
check_result)
if self.pattern:
match = re.match(self.pattern, update.callback_query.data)
if self.pass_groups:
optional_args['groups'] = match.groups()
optional_args['groups'] = check_result.groups()
if self.pass_groupdict:
optional_args['groupdict'] = match.groupdict()
optional_args['groupdict'] = check_result.groupdict()
return optional_args
return self.callback(dispatcher.bot, update, **optional_args)
def collect_additional_context(self, context, update, dispatcher, check_result):
if self.pattern:
context.matches = [check_result]
+20 -39
View File
@@ -18,9 +18,8 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the ChosenInlineResultHandler class."""
from .handler import Handler
from telegram import Update
from telegram.utils.deprecate import deprecate
from .handler import Handler
class ChosenInlineResultHandler(Handler):
@@ -28,13 +27,13 @@ class ChosenInlineResultHandler(Handler):
Attributes:
callback (:obj:`callable`): The callback function for this handler.
pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
passed to the callback function.
pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
the callback function.
pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
Note:
@@ -43,38 +42,37 @@ class ChosenInlineResultHandler(Handler):
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Args:
callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
It will be called when the :attr:`check_update` has determined that an update should be
processed by this handler.
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
Callback signature for context based API:
``def callback(update: Update, context: CallbackContext)``
The return value of the callback is usually ignored except for the special case of
:class:`telegram.ext.ConversationHandler`.
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``chat_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
"""
def __init__(self,
callback,
pass_update_queue=False,
pass_job_queue=False,
pass_user_data=False,
pass_chat_data=False):
super(ChosenInlineResultHandler, self).__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
pass_chat_data=pass_chat_data)
def check_update(self, update):
"""Determines whether an update should be passed to this handlers :attr:`callback`.
@@ -86,20 +84,3 @@ class ChosenInlineResultHandler(Handler):
"""
return isinstance(update, Update) and update.chosen_inline_result
def handle_update(self, update, dispatcher):
"""Send the update to the :attr:`callback`.
Args:
update (:class:`telegram.Update`): Incoming telegram update.
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
"""
optional_args = self.collect_optional_args(dispatcher, update)
return self.callback(dispatcher.bot, update, **optional_args)
# old non-PEP8 Handler methods
m = "telegram.ChosenInlineResultHandler."
checkUpdate = deprecate(check_update, m + "checkUpdate", m + "check_update")
handleUpdate = deprecate(handle_update, m + "handleUpdate", m + "handle_update")
+228 -54
View File
@@ -16,38 +16,48 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the CommandHandler class."""
"""This module contains the CommandHandler and PrefixHandler classes."""
import re
import warnings
from future.utils import string_types
from telegram.ext import Filters
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram import Update, MessageEntity
from .handler import Handler
from telegram import Update
class CommandHandler(Handler):
"""Handler class to handle Telegram commands.
Commands are Telegram messages that start with ``/``, optionally followed by an ``@`` and the
bot's name and/or some additional text.
bot's name and/or some additional text. The handler will add a ``list`` to the
:class:`CallbackContext` named :attr:`CallbackContext.args`. It will contain a list of strings,
which is the text following the command split on single or consecutive whitespace characters.
By default the handler listens to messages as well as edited messages. To change this behavior
use ``~Filters.update.edited_message`` in the filter argument.
Attributes:
command (:obj:`str` | List[:obj:`str`]): The command or list of commands this handler
should listen for.
should listen for. Limitations are the same as described here
https://core.telegram.org/bots#commands
callback (:obj:`callable`): The callback function for this handler.
filters (:class:`telegram.ext.BaseFilter`): Optional. Only allow updates with these
Filters.
allow_edited (:obj:`bool`): Optional. Determines Whether the handler should also accept
allow_edited (:obj:`bool`): Determines Whether the handler should also accept
edited messages.
pass_args (:obj:`bool`): Optional. Determines whether the handler should be passed
pass_args (:obj:`bool`): Determines whether the handler should be passed
``args``.
pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
passed to the callback function.
pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
the callback function.
pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
Note:
@@ -56,42 +66,60 @@ class CommandHandler(Handler):
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Args:
command (:obj:`str` | List[:obj:`str`]): The command or list of commands this handler
should listen for.
callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
It will be called when the :attr:`check_update` has determined that an update should be
processed by this handler.
should listen for. Limitations are the same as described here
https://core.telegram.org/bots#commands
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
Callback signature for context based API:
``def callback(update: Update, context: CallbackContext)``
The return value of the callback is usually ignored except for the special case of
:class:`telegram.ext.ConversationHandler`.
filters (:class:`telegram.ext.BaseFilter`, optional): A filter inheriting from
:class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in
:class:`telegram.ext.filters.Filters`. Filters can be combined using bitwise
operators (& for and, | for or, ~ for not).
allow_edited (:obj:`bool`, optional): Determines whether the handler should also accept
edited messages. Default is ``False``.
DEPRECATED: Edited is allowed by default. To change this behavior use
``~Filters.update.edited_message``.
pass_args (:obj:`bool`, optional): Determines whether the handler should be passed the
arguments passed to the command as a keyword argument called ``args``. It will contain
a list of strings, which is the text following the command split on single or
consecutive whitespace characters. Default is ``False``
DEPRECATED: Please switch to context based callbacks.
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``chat_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
Raises:
ValueError - when command is too long or has illegal chars.
"""
def __init__(self,
command,
callback,
filters=None,
allow_edited=False,
allow_edited=None,
pass_args=False,
pass_update_queue=False,
pass_job_queue=False,
@@ -108,16 +136,22 @@ class CommandHandler(Handler):
self.command = [command.lower()]
else:
self.command = [x.lower() for x in command]
self.filters = filters
self.allow_edited = allow_edited
self.pass_args = pass_args
for comm in self.command:
if not re.match(r'^[\da-z_]{1,32}$', comm):
raise ValueError('Command is not a valid bot command')
# We put this up here instead of with the rest of checking code
# in check_update since we don't wanna spam a ton
if isinstance(self.filters, list):
warnings.warn('Using a list of filters in MessageHandler is getting '
'deprecated, please use bitwise operators (& and |) '
'instead. More info: https://git.io/vPTbc.')
if filters:
self.filters = Filters.update.messages & filters
else:
self.filters = Filters.update.messages
if allow_edited is not None:
warnings.warn('allow_edited is deprecated. See https://git.io/fxJuV for more info',
TelegramDeprecationWarning,
stacklevel=2)
if not allow_edited:
self.filters &= ~Filters.update.edited_message
self.pass_args = pass_args
def check_update(self, update):
"""Determines whether an update should be passed to this handlers :attr:`callback`.
@@ -126,48 +160,188 @@ class CommandHandler(Handler):
update (:class:`telegram.Update`): Incoming telegram update.
Returns:
:obj:`bool`
:obj:`list`: The list of args for the handler
"""
if (isinstance(update, Update)
and (update.message or update.edited_message and self.allow_edited)):
message = update.message or update.edited_message
if isinstance(update, Update) and update.effective_message:
message = update.effective_message
if message.text and message.text.startswith('/') and len(message.text) > 1:
first_word = message.text_html.split(None, 1)[0]
if len(first_word) > 1 and first_word.startswith('/'):
command = first_word[1:].split('@')
command.append(
message.bot.username) # in case the command was sent without a username
if (message.entities and message.entities[0].type == MessageEntity.BOT_COMMAND
and message.entities[0].offset == 0):
command = message.text[1:message.entities[0].length]
args = message.text.split()[1:]
command = command.split('@')
command.append(message.bot.username)
if not (command[0].lower() in self.command
and command[1].lower() == message.bot.username.lower()):
return False
if not (command[0].lower() in self.command
and command[1].lower() == message.bot.username.lower()):
return None
if self.filters is None:
res = True
elif isinstance(self.filters, list):
res = any(func(message) for func in self.filters)
else:
res = self.filters(message)
filter_result = self.filters(update)
if filter_result:
return args, filter_result
else:
return False
return res
def collect_optional_args(self, dispatcher, update=None, check_result=None):
optional_args = super(CommandHandler, self).collect_optional_args(dispatcher, update)
if self.pass_args:
optional_args['args'] = check_result[0]
return optional_args
return False
def collect_additional_context(self, context, update, dispatcher, check_result):
context.args = check_result[0]
if isinstance(check_result[1], dict):
context.update(check_result[1])
def handle_update(self, update, dispatcher):
"""Send the update to the :attr:`callback`.
class PrefixHandler(CommandHandler):
"""Handler class to handle custom prefix commands
This is a intermediate handler between :class:`MessageHandler` and :class:`CommandHandler`.
It supports configurable commands with the same options as CommandHandler. It will respond to
every combination of :attr:`prefix` and :attr:`command`. It will add a ``list`` to the
:class:`CallbackContext` named :attr:`CallbackContext.args`. It will contain a list of strings,
which is the text following the command split on single or consecutive whitespace characters.
Examples::
Single prefix and command:
PrefixHandler('!', 'test', callback) will respond to '!test'.
Multiple prefixes, single command:
PrefixHandler(['!', '#'], 'test', callback) will respond to '!test' and
'#test'.
Miltiple prefixes and commands:
PrefixHandler(['!', '#'], ['test', 'help`], callback) will respond to '!test',
'#test', '!help' and '#help'.
By default the handler listens to messages as well as edited messages. To change this behavior
use ~``Filters.update.edited_message``.
Attributes:
prefix (:obj:`str` | List[:obj:`str`]): The prefix(es) that will precede :attr:`command`.
command (:obj:`str` | List[:obj:`str`]): The command or list of commands this handler
should listen for.
callback (:obj:`callable`): The callback function for this handler.
filters (:class:`telegram.ext.BaseFilter`): Optional. Only allow updates with these
Filters.
pass_args (:obj:`bool`): Determines whether the handler should be passed
``args``.
pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
passed to the callback function.
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
the callback function.
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
can use to keep any data in will be sent to the :attr:`callback` function. Related to
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Args:
prefix (:obj:`str` | List[:obj:`str`]): The prefix(es) that will precede :attr:`command`.
command (:obj:`str` | List[:obj:`str`]): The command or list of commands this handler
should listen for.
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
Callback signature for context based API:
``def callback(update: Update, context: CallbackContext)``
The return value of the callback is usually ignored except for the special case of
:class:`telegram.ext.ConversationHandler`.
filters (:class:`telegram.ext.BaseFilter`, optional): A filter inheriting from
:class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in
:class:`telegram.ext.filters.Filters`. Filters can be combined using bitwise
operators (& for and, | for or, ~ for not).
pass_args (:obj:`bool`, optional): Determines whether the handler should be passed the
arguments passed to the command as a keyword argument called ``args``. It will contain
a list of strings, which is the text following the command split on single or
consecutive whitespace characters. Default is ``False``
DEPRECATED: Please switch to context based callbacks.
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``chat_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
"""
def __init__(self,
prefix,
command,
callback,
filters=None,
pass_args=False,
pass_update_queue=False,
pass_job_queue=False,
pass_user_data=False,
pass_chat_data=False):
super(PrefixHandler, self).__init__(
'nocommand', callback, filters=filters, allow_edited=None, pass_args=pass_args,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
pass_chat_data=pass_chat_data)
if isinstance(prefix, string_types):
self.prefix = [prefix.lower()]
else:
self.prefix = prefix
if isinstance(command, string_types):
self.command = [command.lower()]
else:
self.command = command
self.command = [x.lower() + y.lower() for x in self.prefix for y in self.command]
def check_update(self, update):
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
update (:class:`telegram.Update`): Incoming telegram update.
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
Returns:
:obj:`list`: The list of args for the handler
"""
optional_args = self.collect_optional_args(dispatcher, update)
if isinstance(update, Update) and update.effective_message:
message = update.effective_message
message = update.message or update.edited_message
text_list = message.text.split()
if text_list[0].lower() not in self.command:
return None
filter_result = self.filters(update)
if filter_result:
return text_list[1:], filter_result
else:
return False
if self.pass_args:
optional_args['args'] = message.text.split()[1:]
return self.callback(dispatcher.bot, update, **optional_args)
def collect_additional_context(self, context, update, dispatcher, check_result):
context.args = check_result[0]
if isinstance(check_result[1], dict):
context.update(check_result[1])
+123 -91
View File
@@ -19,6 +19,7 @@
"""This module contains the ConversationHandler."""
import logging
import warnings
from telegram import Update
from telegram.ext import (Handler, CallbackQueryHandler, InlineQueryHandler,
@@ -26,6 +27,13 @@ from telegram.ext import (Handler, CallbackQueryHandler, InlineQueryHandler,
from telegram.utils.promise import Promise
class _ConversationTimeoutContext(object):
def __init__(self, conversation_key, update, dispatcher):
self.conversation_key = conversation_key
self.update = update
self.dispatcher = dispatcher
class ConversationHandler(Handler):
"""
A handler to hold a conversation with a single user by managing four collections of other
@@ -38,8 +46,10 @@ class ConversationHandler(Handler):
The second collection, a ``dict`` named :attr:`states`, contains the different conversation
steps and one or more associated handlers that should be used if the user sends a message when
the conversation with them is currently in that state. You will probably use mostly
:class:`telegram.ext.MessageHandler` and :class:`telegram.ext.RegexHandler` here.
the conversation with them is currently in that state. Here you can also define a state for
:attr:`TIMEOUT` to define the behavior when :attr:`conversation_timeout` is exceeded, and a
state for :attr:`WAITING` to define behavior when a new update is received while the previous
``@run_async`` decorated handler is not finished.
The third collection, a ``list`` named :attr:`fallbacks`, is used if the user is currently in a
conversation but the state has either no associated handler or the handler that is associated
@@ -54,8 +64,10 @@ class ConversationHandler(Handler):
To change the state of conversation, the callback function of a handler must return the new
state after responding to the user. If it does not return anything (returning ``None`` by
default), the state will not change. To end the conversation, the callback function must
return :attr:`END` or ``-1``.
default), the state will not change. If an entry point callback function returns None,
the conversation ends immediately after the execution of this callback function.
To end the conversation, the callback function must return :attr:`END` or ``-1``. To
handle the conversation timeout, use handler :attr:`TIMEOUT` or ``-2``.
Attributes:
entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
@@ -66,19 +78,21 @@ class ConversationHandler(Handler):
fallbacks (List[:class:`telegram.ext.Handler`]): A list of handlers that might be used if
the user is in a conversation, but every handler for their current state returned
``False`` on :attr:`check_update`.
allow_reentry (:obj:`bool`): Optional. Determines if a user can restart a conversation with
allow_reentry (:obj:`bool`): Determines if a user can restart a conversation with
an entry point.
run_async_timeout (:obj:`float`): Optional. The time-out for ``run_async`` decorated
Handlers.
timed_out_behavior (List[:class:`telegram.ext.Handler`]): Optional. A list of handlers that
might be used if the wait for ``run_async`` timed out.
per_chat (:obj:`bool`): Optional. If the conversationkey should contain the Chat's ID.
per_user (:obj:`bool`): Optional. If the conversationkey should contain the User's ID.
per_message (:obj:`bool`): Optional. If the conversationkey should contain the Message's
per_chat (:obj:`bool`): If the conversationkey should contain the Chat's ID.
per_user (:obj:`bool`): If the conversationkey should contain the User's ID.
per_message (:obj:`bool`): If the conversationkey should contain the Message's
ID.
conversation_timeout (:obj:`float`|:obj:`datetime.timedelta`): Optional. When this handler
is inactive more than this timeout (in seconds), it will be automatically ended. If
this value is 0 (default), there will be no timeout.
this value is 0 (default), there will be no timeout. when it's triggered. The last
received update will be handled by ALL the handler's who's `check_update` method
returns True that are in the state :attr:`ConversationHandler.TIMEOUT`.
name (:obj:`str`): Optional. The name for this conversationhandler. Required for
persistence
persistent (:obj:`bool`): Optional. If the conversations dict for this handler should be
saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater`
Args:
entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
@@ -95,24 +109,21 @@ class ConversationHandler(Handler):
returns ``True`` will be used. If all return ``False``, the update is not handled.
allow_reentry (:obj:`bool`, optional): If set to ``True``, a user that is currently in a
conversation can restart the conversation by triggering one of the entry points.
run_async_timeout (:obj:`float`, optional): If the previous handler for this user was
running asynchronously using the ``run_async`` decorator, it might not be finished when
the next message arrives. This timeout defines how long the conversation handler should
wait for the next state to be computed. The default is ``None`` which means it will
wait indefinitely.
timed_out_behavior (List[:class:`telegram.ext.Handler`], optional): A list of handlers that
might be used if the wait for ``run_async`` timed out. The first handler which
:attr:`check_update` method returns ``True`` will be used. If all return ``False``,
the update is not handled.
per_chat (:obj:`bool`, optional): If the conversationkey should contain the Chat's ID.
Default is ``True``.
per_user (:obj:`bool`, optional): If the conversationkey should contain the User's ID.
Default is ``True``.
per_message (:obj:`bool`, optional): If the conversationkey should contain the Message's
ID. Default is ``False``.
conversation_timeout (:obj:`float`|:obj:`datetime.timedelta`, optional): When this handler
is inactive more than this timeout (in seconds), it will be automatically ended. If
this value is 0 or None (default), there will be no timeout.
conversation_timeout (:obj:`float` | :obj:`datetime.timedelta`, optional): When this
handler is inactive more than this timeout (in seconds), it will be automatically
ended. If this value is 0 or None (default), there will be no timeout. The last
received update will be handled by ALL the handler's who's `check_update` method
returns True that are in the state :attr:`ConversationHandler.TIMEOUT`.
name (:obj:`str`, optional): The name for this conversationhandler. Required for
persistence
persistent (:obj:`bool`, optional): If the conversations dict for this handler should be
saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater`
Raises:
ValueError
@@ -120,35 +131,43 @@ class ConversationHandler(Handler):
"""
END = -1
""":obj:`int`: Used as a constant to return when a conversation is ended."""
TIMEOUT = -2
""":obj:`int`: Used as a constant to handle state when a conversation is timed out."""
WAITING = -3
""":obj:`int`: Used as a constant to handle state when a conversation is still waiting on the
previous ``@run_sync`` decorated running handler to finish."""
def __init__(self,
entry_points,
states,
fallbacks,
allow_reentry=False,
run_async_timeout=None,
timed_out_behavior=None,
per_chat=True,
per_user=True,
per_message=False,
conversation_timeout=None):
conversation_timeout=None,
name=None,
persistent=False):
self.entry_points = entry_points
self.states = states
self.fallbacks = fallbacks
self.allow_reentry = allow_reentry
self.run_async_timeout = run_async_timeout
self.timed_out_behavior = timed_out_behavior
self.per_user = per_user
self.per_chat = per_chat
self.per_message = per_message
self.conversation_timeout = conversation_timeout
self.name = name
if persistent and not self.name:
raise ValueError("Conversations can't be persistent when handler is unnamed.")
self.persistent = persistent
self.persistence = None
""":obj:`telegram.ext.BasePersistance`: The persistence used to store conversations.
Set by dispatcher"""
self.timeout_jobs = dict()
self.conversations = dict()
self.current_conversation = None
self.current_handler = None
self.logger = logging.getLogger(__name__)
@@ -156,8 +175,8 @@ class ConversationHandler(Handler):
raise ValueError("'per_user', 'per_chat' and 'per_message' can't all be 'False'")
if self.per_message and not self.per_chat:
logging.warning("If 'per_message=True' is used, 'per_chat=True' should also be used, "
"since message IDs are not globally unique.")
warnings.warn("If 'per_message=True' is used, 'per_chat=True' should also be used, "
"since message IDs are not globally unique.")
all_handlers = list()
all_handlers.extend(entry_points)
@@ -169,20 +188,23 @@ class ConversationHandler(Handler):
if self.per_message:
for handler in all_handlers:
if not isinstance(handler, CallbackQueryHandler):
logging.warning("If 'per_message=True', all entry points and state handlers"
" must be 'CallbackQueryHandler', since no other handlers "
"have a message context.")
warnings.warn("If 'per_message=True', all entry points and state handlers"
" must be 'CallbackQueryHandler', since no other handlers "
"have a message context.")
break
else:
for handler in all_handlers:
if isinstance(handler, CallbackQueryHandler):
logging.warning("If 'per_message=False', 'CallbackQueryHandler' will not be "
"tracked for every message.")
warnings.warn("If 'per_message=False', 'CallbackQueryHandler' will not be "
"tracked for every message.")
break
if self.per_chat:
for handler in all_handlers:
if isinstance(handler, (InlineQueryHandler, ChosenInlineResultHandler)):
logging.warning("If 'per_chat=True', 'InlineQueryHandler' can not be used, "
"since inline queries have no chat context.")
warnings.warn("If 'per_chat=True', 'InlineQueryHandler' can not be used, "
"since inline queries have no chat context.")
break
def _get_key(self, update):
chat = update.effective_chat
@@ -215,44 +237,41 @@ class ConversationHandler(Handler):
"""
# Ignore messages in channels
if (not isinstance(update, Update) or
update.channel_post or
self.per_chat and not update.effective_chat or
self.per_message and not update.callback_query or
update.callback_query and self.per_chat and not update.callback_query.message):
return False
if (not isinstance(update, Update)
or update.channel_post
or self.per_chat and not update.effective_chat
or self.per_message and not update.callback_query
or update.callback_query and self.per_chat and not update.callback_query.message):
return None
key = self._get_key(update)
state = self.conversations.get(key)
# Resolve promises
if isinstance(state, tuple) and len(state) is 2 and isinstance(state[1], Promise):
if isinstance(state, tuple) and len(state) == 2 and isinstance(state[1], Promise):
self.logger.debug('waiting for promise...')
old_state, new_state = state
error = False
try:
res = new_state.result(timeout=self.run_async_timeout)
except Exception as exc:
self.logger.exception("Promise function raised exception")
self.logger.exception("{}".format(exc))
error = True
if not error and new_state.done.is_set():
self.update_state(res, key)
state = self.conversations.get(key)
if new_state.done.wait(0):
try:
res = new_state.result(0)
res = res if res is not None else old_state
except Exception as exc:
self.logger.exception("Promise function raised exception")
self.logger.exception("{}".format(exc))
res = old_state
finally:
if res is None and old_state is None:
res = self.END
self.update_state(res, key)
state = self.conversations.get(key)
else:
for candidate in (self.timed_out_behavior or []):
if candidate.check_update(update):
# Save the current user and the selected handler for handle_update
self.current_conversation = key
self.current_handler = candidate
return True
else:
return False
handlers = self.states.get(self.WAITING, [])
for handler in handlers:
check = handler.check_update(update)
if check is not None and check is not False:
return key, handler, check
return None
self.logger.debug('selecting conversation %s with state %s' % (str(key), str(state)))
@@ -261,73 +280,86 @@ class ConversationHandler(Handler):
# Search entry points for a match
if state is None or self.allow_reentry:
for entry_point in self.entry_points:
if entry_point.check_update(update):
check = entry_point.check_update(update)
if check is not None and check is not False:
handler = entry_point
break
else:
if state is None:
return False
return None
# Get the handler list for current state, if we didn't find one yet and we're still here
if state is not None and not handler:
handlers = self.states.get(state)
for candidate in (handlers or []):
if candidate.check_update(update):
check = candidate.check_update(update)
if check is not None and check is not False:
handler = candidate
break
# Find a fallback handler if all other handlers fail
else:
for fallback in self.fallbacks:
if fallback.check_update(update):
check = fallback.check_update(update)
if check is not None and check is not False:
handler = fallback
break
else:
return False
return None
# Save the current user and the selected handler for handle_update
self.current_conversation = key
self.current_handler = handler
return key, handler, check
return True
def handle_update(self, update, dispatcher):
def handle_update(self, update, dispatcher, check_result, context=None):
"""Send the update to the callback for the current state and Handler
Args:
check_result: The result from check_update. For this handler it's a tuple of key,
handler, and the handler's check result.
update (:class:`telegram.Update`): Incoming telegram update.
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
"""
new_state = self.current_handler.handle_update(update, dispatcher)
timeout_job = self.timeout_jobs.pop(self.current_conversation, None)
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[self.current_conversation] = dispatcher.job_queue.run_once(
self.timeout_jobs[conversation_key] = dispatcher.job_queue.run_once(
self._trigger_timeout, self.conversation_timeout,
context=self.current_conversation
)
context=_ConversationTimeoutContext(conversation_key, update, dispatcher))
self.update_state(new_state, self.current_conversation)
self.update_state(new_state, conversation_key)
def update_state(self, new_state, key):
if new_state == self.END:
if key in self.conversations:
# If there is no key in conversations, nothing is done.
del self.conversations[key]
else:
pass
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))
elif new_state is not None:
self.conversations[key] = new_state
if self.persistent:
self.persistence.update_conversation(self.name, key, new_state)
def _trigger_timeout(self, bot, job):
del self.timeout_jobs[job.context]
self.update_state(self.END, job.context)
self.logger.debug('conversation timeout was triggered!')
del self.timeout_jobs[job.context.conversation_key]
handlers = self.states.get(self.TIMEOUT, [])
for handler in handlers:
check = handler.check_update(job.context.update)
if check is not None and check is not False:
handler.handle_update(job.context.update, job.context.dispatcher, check)
self.update_state(self.END, job.context.conversation_key)
+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2018
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the DictPersistence class."""
from copy import deepcopy
from telegram.utils.helpers import decode_user_chat_data_from_json,\
decode_conversations_from_json, enocde_conversations_to_json
try:
import ujson as json
except ImportError:
import json
from collections import defaultdict
from telegram.ext import BasePersistence
class DictPersistence(BasePersistence):
"""Using python's dicts and json for making you bot persistent.
Attributes:
store_user_data (:obj:`bool`): Whether user_data should be saved by this
persistence class.
store_chat_data (:obj:`bool`): Whether chat_data should be saved by this
persistence class.
Args:
store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this
persistence class. Default is ``True``.
store_chat_data (:obj:`bool`, optional): Whether user_data should be saved by this
persistence class. Default is ``True``.
user_data_json (:obj:`str`, optional): Json string that will be used to reconstruct
user_data on creating this persistence. Default is ``""``.
chat_data_json (:obj:`str`, optional): Json string that will be used to reconstruct
chat_data on creating this persistence. Default is ``""``.
conversations_json (:obj:`str`, optional): Json string that will be used to reconstruct
conversation on creating this persistence. Default is ``""``.
"""
def __init__(self, store_user_data=True, store_chat_data=True, user_data_json='',
chat_data_json='', conversations_json=''):
self.store_user_data = store_user_data
self.store_chat_data = store_chat_data
self._user_data = None
self._chat_data = None
self._conversations = None
self._user_data_json = None
self._chat_data_json = None
self._conversations_json = None
if user_data_json:
try:
self._user_data = decode_user_chat_data_from_json(user_data_json)
self._user_data_json = user_data_json
except (ValueError, AttributeError):
raise TypeError("Unable to deserialize user_data_json. Not valid JSON")
if chat_data_json:
try:
self._chat_data = decode_user_chat_data_from_json(chat_data_json)
self._chat_data_json = chat_data_json
except (ValueError, AttributeError):
raise TypeError("Unable to deserialize chat_data_json. Not valid JSON")
if conversations_json:
try:
self._conversations = decode_conversations_from_json(conversations_json)
self._conversations_json = conversations_json
except (ValueError, AttributeError):
raise TypeError("Unable to deserialize conversations_json. Not valid JSON")
@property
def user_data(self):
""":obj:`dict`: The user_data as a dict"""
return self._user_data
@property
def user_data_json(self):
""":obj:`str`: The user_data serialized as a JSON-string."""
if self._user_data_json:
return self._user_data_json
else:
return json.dumps(self.user_data)
@property
def chat_data(self):
""":obj:`dict`: The chat_data as a dict"""
return self._chat_data
@property
def chat_data_json(self):
""":obj:`str`: The chat_data serialized as a JSON-string."""
if self._chat_data_json:
return self._chat_data_json
else:
return json.dumps(self.chat_data)
@property
def conversations(self):
""":obj:`dict`: The conversations as a dict"""
return self._conversations
@property
def conversations_json(self):
""":obj:`str`: The conversations serialized as a JSON-string."""
if self._conversations_json:
return self._conversations_json
else:
return enocde_conversations_to_json(self.conversations)
def get_user_data(self):
"""Returns the user_data created from the ``user_data_json`` or an empty defaultdict.
Returns:
:obj:`defaultdict`: The restored user data.
"""
if self.user_data:
pass
else:
self._user_data = defaultdict(dict)
return deepcopy(self.user_data)
def get_chat_data(self):
"""Returns the chat_data created from the ``chat_data_json`` or an empty defaultdict.
Returns:
:obj:`defaultdict`: The restored user data.
"""
if self.chat_data:
pass
else:
self._chat_data = defaultdict(dict)
return deepcopy(self.chat_data)
def get_conversations(self, name):
"""Returns the conversations created from the ``conversations_json`` or an empty
defaultdict.
Returns:
:obj:`defaultdict`: The restored user data.
"""
if self.conversations:
pass
else:
self._conversations = {}
return self.conversations.get(name, {}).copy()
def update_conversation(self, name, key, new_state):
"""Will update the conversations for the given handler.
Args:
name (:obj:`str`): The handlers name.
key (:obj:`tuple`): The key the state is changed for.
new_state (:obj:`tuple` | :obj:`any`): The new state for the given key.
"""
if self._conversations.setdefault(name, {}).get(key) == new_state:
return
self._conversations[name][key] = new_state
self._conversations_json = None
def update_user_data(self, user_id, data):
"""Will update the user_data (if changed).
Args:
user_id (:obj:`int`): The user the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data`[user_id].
"""
if self._user_data.get(user_id) == data:
return
self._user_data[user_id] = data
self._user_data_json = None
def update_chat_data(self, chat_id, data):
"""Will update the chat_data (if changed).
Args:
chat_id (:obj:`int`): The chat the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data`[chat_id].
"""
if self._chat_data.get(chat_id) == data:
return
self._chat_data[chat_id] = data
self._chat_data_json = None
+108 -12
View File
@@ -19,6 +19,7 @@
"""This module contains the Dispatcher class."""
import logging
import warnings
import weakref
from functools import wraps
from threading import Thread, Lock, Event, current_thread, BoundedSemaphore
@@ -30,24 +31,30 @@ from queue import Queue, Empty
from future.builtins import range
from telegram import TelegramError
from telegram import TelegramError, Update
from telegram.ext.handler import Handler
from telegram.ext.callbackcontext import CallbackContext
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.utils.promise import Promise
from telegram.ext import BasePersistence
logging.getLogger(__name__).addHandler(logging.NullHandler())
DEFAULT_GROUP = 0
def run_async(func):
"""Function decorator that will run the function in a new thread.
"""
Function decorator that will run the function in a new thread.
Will run :attr:`telegram.ext.Dispatcher.run_async`.
Using this decorator is only possible when only a single Dispatcher exist in the system.
Note: Use this decorator to run handlers asynchronously.
Warning:
If you're using @run_async you cannot rely on adding custom attributes to
:class:`telegram.ext.CallbackContext`s. See its docs for more info.
"""
@wraps(func)
def async_func(*args, **kwargs):
return Dispatcher.get_instance().run_async(func, *args, **kwargs)
@@ -70,6 +77,10 @@ class Dispatcher(object):
instance to pass onto handler callbacks.
workers (:obj:`int`): Number of maximum concurrent worker threads for the ``@run_async``
decorator.
user_data (:obj:`defaultdict`): A dictionary handlers can use to store data for the user.
chat_data (:obj:`defaultdict`): A dictionary handlers can use to store data for the chat.
persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to
store data that should be persistent over restarts
Args:
bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers.
@@ -78,6 +89,11 @@ class Dispatcher(object):
instance to pass onto handler callbacks.
workers (:obj:`int`, optional): Number of maximum concurrent worker threads for the
``@run_async`` decorator. defaults to 4.
persistence (:class:`telegram.ext.BasePersistence`, optional): The persistence class to
store data that should be persistent over restarts
use_context (:obj:`bool`, optional): If set to ``True`` Use the context based callback API.
During the deprecation period of the old API the default is ``False``. **New users**:
set this to ``True``.
"""
@@ -86,16 +102,44 @@ class Dispatcher(object):
__singleton = None
logger = logging.getLogger(__name__)
def __init__(self, bot, update_queue, workers=4, exception_event=None, job_queue=None):
def __init__(self,
bot,
update_queue,
workers=4,
exception_event=None,
job_queue=None,
persistence=None,
use_context=False):
self.bot = bot
self.update_queue = update_queue
self.job_queue = job_queue
self.workers = workers
self.use_context = use_context
if not use_context:
warnings.warn('Old Handler API is deprecated - see https://git.io/fxJuV for details',
TelegramDeprecationWarning, stacklevel=3)
self.user_data = defaultdict(dict)
""":obj:`dict`: A dictionary handlers can use to store data for the user."""
self.chat_data = defaultdict(dict)
""":obj:`dict`: A dictionary handlers can use to store data for the chat."""
if persistence:
if not isinstance(persistence, BasePersistence):
raise TypeError("persistence should be based on telegram.ext.BasePersistence")
self.persistence = persistence
if self.persistence.store_user_data:
self.user_data = self.persistence.get_user_data()
if not isinstance(self.user_data, defaultdict):
raise ValueError("user_data must be of type defaultdict")
if self.persistence.store_chat_data:
self.chat_data = self.persistence.get_chat_data()
if not isinstance(self.chat_data, defaultdict):
raise ValueError("chat_data must be of type defaultdict")
else:
self.persistence = None
self.job_queue = job_queue
self.handlers = {}
"""Dict[:obj:`int`, List[:class:`telegram.ext.Handler`]]: Holds the handlers per group."""
self.groups = []
@@ -168,6 +212,10 @@ class Dispatcher(object):
def run_async(self, func, *args, **kwargs):
"""Queue a function (with given args/kwargs) to be run asynchronously.
Warning:
If you're using @run_async you cannot rely on adding custom attributes to
:class:`telegram.ext.CallbackContext`s. See its docs for more info.
Args:
func (:obj:`callable`): The function to run in the thread.
*args (:obj:`tuple`, optional): Arguments to `func`.
@@ -273,11 +321,32 @@ class Dispatcher(object):
self.logger.exception('An uncaught error was raised while handling the error')
return
context = None
for group in self.groups:
try:
for handler in (x for x in self.handlers[group] if x.check_update(update)):
handler.handle_update(update, self)
break
for handler in self.handlers[group]:
check = handler.check_update(update)
if check is not None and check is not False:
if not context and self.use_context:
context = CallbackContext.from_update(update, self)
handler.handle_update(update, self, check, context)
if self.persistence and isinstance(update, Update):
if self.persistence.store_chat_data and update.effective_chat:
chat_id = update.effective_chat.id
try:
self.persistence.update_chat_data(chat_id,
self.chat_data[chat_id])
except Exception:
self.logger.exception('Saving chat data raised an error')
if self.persistence.store_user_data and update.effective_user:
user_id = update.effective_user.id
try:
self.persistence.update_user_data(user_id,
self.user_data[user_id])
except Exception:
self.logger.exception('Saving user data raised an error')
break
# Stop processing with any other handler.
except DispatcherHandlerStop:
@@ -324,11 +393,20 @@ class Dispatcher(object):
group (:obj:`int`, optional): The group identifier. Default is 0.
"""
# Unfortunately due to circular imports this has to be here
from .conversationhandler import ConversationHandler
if not isinstance(handler, Handler):
raise TypeError('handler is not an instance of {0}'.format(Handler.__name__))
if not isinstance(group, int):
raise TypeError('group is not int')
if isinstance(handler, ConversationHandler) and handler.persistent:
if not self.persistence:
raise ValueError(
"Conversationhandler {} can not be persistent if dispatcher has no "
"persistence".format(handler.name))
handler.conversations = self.persistence.get_conversations(handler.name)
handler.persistence = self.persistence
if group not in self.handlers:
self.handlers[group] = list()
@@ -351,13 +429,28 @@ class Dispatcher(object):
del self.handlers[group]
self.groups.remove(group)
def update_persistence(self):
"""Update :attr:`user_data` and :attr:`chat_data` in :attr:`persistence`.
"""
if self.persistence:
for chat_id in self.chat_data:
self.persistence.update_chat_data(chat_id, self.chat_data[chat_id])
for user_id in self.user_data:
self.persistence.update_user_data(user_id, self.user_data[user_id])
def add_error_handler(self, callback):
"""Registers an error handler in the Dispatcher.
Args:
callback (:obj:`callable`): A function that takes ``Bot, Update, TelegramError`` as
arguments.
callback (:obj:`callable`): The callback function for this error handler. Will be
called when an error is raised Callback signature for context based API:
``def callback(update: Update, context: CallbackContext)``
The error that happened will be present in context.error.
Note:
See https://git.io/fxJuV for more info about switching to context based API.
"""
self.error_handlers.append(callback)
@@ -381,7 +474,10 @@ class Dispatcher(object):
"""
if self.error_handlers:
for callback in self.error_handlers:
callback(self.bot, update, error)
if self.use_context:
callback(update, CallbackContext.from_error(update, error, self))
else:
callback(self.bot, update, error)
else:
self.logger.exception(
+181 -32
View File
@@ -19,9 +19,11 @@
"""This module contains the Filters for use with the MessageHandler class."""
import re
from telegram import Chat
from future.utils import string_types
from telegram import Chat
class BaseFilter(object):
"""Base class for all Message Filters.
@@ -56,13 +58,23 @@ class BaseFilter(object):
Attributes:
name (:obj:`str`): Name for this filter. Defaults to the type of filter.
update_filter (:obj:`bool`): Whether this filter should work on update. If ``False`` it
will run the filter on :attr:`update.effective_message``. Default is ``False``.
data_filter (:obj:`bool`): Whether this filter is a data filter. A data filter should
return a dict with lists. The dict will be merged with
:class:`telegram.ext.CallbackContext`'s internal dict in most cases
(depends on the handler).
"""
name = None
update_filter = False
data_filter = False
def __call__(self, message):
return self.filter(message)
def __call__(self, update):
if self.update_filter:
return self.filter(update)
else:
return self.filter(update.effective_message)
def __and__(self, other):
return MergedFilter(self, and_filter=other)
@@ -79,14 +91,18 @@ class BaseFilter(object):
self.name = self.__class__.__name__
return self.name
def filter(self, message):
def filter(self, update):
"""This method must be overwritten.
Note:
If :attr:`update_filter` is false then the first argument is `message` and of
type :class:`telegram.Message`.
Args:
message (:class:`telegram.Message`): The message that is tested.
update (:class:`telegram.Update`): The update that is tested.
Returns:
:obj:`bool`
:obj:`dict` or :obj:`bool`
"""
@@ -100,12 +116,13 @@ class InvertedFilter(BaseFilter):
f: The filter to invert.
"""
update_filter = True
def __init__(self, f):
self.f = f
def filter(self, message):
return not self.f(message)
def filter(self, update):
return not bool(self.f(update))
def __repr__(self):
return "<inverted {}>".format(self.f)
@@ -120,17 +137,60 @@ class MergedFilter(BaseFilter):
or_filter: Optional filter to "or" with base_filter. Mutually exclusive with and_filter.
"""
update_filter = True
def __init__(self, base_filter, and_filter=None, or_filter=None):
self.base_filter = base_filter
if self.base_filter.data_filter:
self.data_filter = True
self.and_filter = and_filter
if (self.and_filter
and not isinstance(self.and_filter, bool)
and self.and_filter.data_filter):
self.data_filter = True
self.or_filter = or_filter
if (self.or_filter
and not isinstance(self.and_filter, bool)
and self.or_filter.data_filter):
self.data_filter = True
def filter(self, message):
def _merge(self, base_output, comp_output):
base = base_output if isinstance(base_output, dict) else {}
comp = comp_output if isinstance(comp_output, dict) else {}
for k in comp.keys():
# Make sure comp values are lists
comp_value = comp[k] if isinstance(comp[k], list) else []
try:
# If base is a list then merge
if isinstance(base[k], list):
base[k] += comp_value
else:
base[k] = [base[k]] + comp_value
except KeyError:
base[k] = comp_value
return base
def filter(self, update):
base_output = self.base_filter(update)
# We need to check if the filters are data filters and if so return the merged data.
# If it's not a data filter or an or_filter but no matches return bool
if self.and_filter:
return self.base_filter(message) and self.and_filter(message)
comp_output = self.and_filter(update)
if base_output and comp_output:
if self.data_filter:
merged = self._merge(base_output, comp_output)
if merged:
return merged
return True
elif self.or_filter:
return self.base_filter(message) or self.or_filter(message)
comp_output = self.or_filter(update)
if base_output or comp_output:
if self.data_filter:
merged = self._merge(base_output, comp_output)
if merged:
return merged
return True
return False
def __repr__(self):
return "<{} {} {}>".format(self.base_filter, "and" if self.and_filter else "or",
@@ -175,32 +235,39 @@ class Filters(object):
class regex(BaseFilter):
"""
Filters updates by searching for an occurence of ``pattern`` in the message text.
Filters updates by searching for an occurrence of ``pattern`` in the message text.
The ``re.search`` function is used to determine whether an update should be filtered.
Refer to the documentation of the ``re`` module for more information.
Note: Does not allow passing groups or a groupdict like the ``RegexHandler`` yet,
but this will probably be implemented in a future update, gradually phasing out the
RegexHandler (see https://github.com/python-telegram-bot/python-telegram-bot/issues/835).
To get the groups and groupdict matched, see :attr:`telegram.ext.CallbackContext.matches`.
Examples:
Example ``CommandHandler("start", deep_linked_callback, Filters.regex('parameter'))``
Use ``MessageHandler(Filters.regex(r'help'), callback)`` to capture all messages that
contain the word help. You can also use
``MessageHandler(Filters.regex(re.compile(r'help', re.IGNORECASE), callback)`` if
you want your pattern to be case insensitive. This approach is recommended
if you need to specify flags on your pattern.
Args:
pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
"""
data_filter = True
def __init__(self, pattern):
self.pattern = re.compile(pattern)
if isinstance(pattern, string_types):
pattern = re.compile(pattern)
self.pattern = pattern
self.name = 'Filters.regex({})'.format(self.pattern)
# TODO: Once the callback revamp (#1026) is done, the regex filter should be able to pass
# the matched groups and groupdict to the context object.
def filter(self, message):
"""""" # remove method from docs
if message.text:
return bool(self.pattern.search(message.text))
return False
match = self.pattern.search(message.text)
if match:
return {'matches': [match]}
return {}
class _Reply(BaseFilter):
name = 'Filters.reply'
@@ -246,6 +313,7 @@ class Filters(object):
self.name = "Filters.document.category('{}')".format(self.category)
def filter(self, message):
"""""" # remove method from docs
if message.document:
return message.document.mime_type.startswith(self.category)
@@ -277,6 +345,7 @@ class Filters(object):
self.name = "Filters.document.mime_type('{}')".format(self.mimetype)
def filter(self, message):
"""""" # remove method from docs
if message.document:
return message.document.mime_type == self.mimetype
@@ -391,6 +460,7 @@ class Filters(object):
``Filters.status_update`` for all status update messages.
"""
update_filter = True
class _NewChatMembers(BaseFilter):
name = 'Filters.status_update.new_chat_members'
@@ -441,8 +511,8 @@ class Filters(object):
name = 'Filters.status_update.chat_created'
def filter(self, message):
return bool(message.group_chat_created or message.supergroup_chat_created or
message.channel_chat_created)
return bool(message.group_chat_created or message.supergroup_chat_created
or message.channel_chat_created)
chat_created = _ChatCreated()
""":obj:`Filter`: Messages that contain :attr:`telegram.Message.group_chat_created`,
@@ -480,11 +550,11 @@ class Filters(object):
name = 'Filters.status_update'
def filter(self, message):
return bool(self.new_chat_members(message) or self.left_chat_member(message) or
self.new_chat_title(message) or self.new_chat_photo(message) or
self.delete_chat_photo(message) or self.chat_created(message) or
self.migrate(message) or self.pinned_message(message) or
self.connected_website(message))
return bool(self.new_chat_members(message) or self.left_chat_member(message)
or self.new_chat_title(message) or self.new_chat_photo(message)
or self.delete_chat_photo(message) or self.chat_created(message)
or self.migrate(message) or self.pinned_message(message)
or self.connected_website(message))
status_update = _StatusUpdate()
"""Subset for messages containing a status update.
@@ -552,6 +622,7 @@ class Filters(object):
self.name = 'Filters.entity({})'.format(self.entity_type)
def filter(self, message):
"""""" # remove method from docs
return any(entity.type == self.entity_type for entity in message.entities)
class caption_entity(BaseFilter):
@@ -573,6 +644,7 @@ class Filters(object):
self.name = 'Filters.caption_entity({})'.format(self.entity_type)
def filter(self, message):
"""""" # remove method from docs
return any(entity.type == self.entity_type for entity in message.caption_entities)
class _Private(BaseFilter):
@@ -624,12 +696,13 @@ class Filters(object):
self.usernames = [user.replace('@', '') for user in username]
def filter(self, message):
"""""" # remove method from docs
if self.user_ids is not None:
return bool(message.from_user and message.from_user.id in self.user_ids)
else:
# self.usernames is not None
return bool(message.from_user and message.from_user.username and
message.from_user.username in self.usernames)
return bool(message.from_user and message.from_user.username
and message.from_user.username in self.usernames)
class chat(BaseFilter):
"""Filters messages to allow only those which are from specified chat ID.
@@ -662,6 +735,7 @@ class Filters(object):
self.usernames = [chat.replace('@', '') for chat in username]
def filter(self, message):
"""""" # remove method from docs
if self.chat_ids is not None:
return bool(message.chat_id in self.chat_ids)
else:
@@ -719,5 +793,80 @@ class Filters(object):
self.name = 'Filters.language({})'.format(self.lang)
def filter(self, message):
"""""" # remove method from docs
return message.from_user.language_code and any(
[message.from_user.language_code.startswith(x) for x in self.lang])
class _UpdateType(BaseFilter):
update_filter = True
class _Message(BaseFilter):
update_filter = True
def filter(self, update):
return update.message is not None
message = _Message()
class _EditedMessage(BaseFilter):
update_filter = True
def filter(self, update):
return update.edited_message is not None
edited_message = _EditedMessage()
class _Messages(BaseFilter):
update_filter = True
def filter(self, update):
return update.message is not None or update.edited_message is not None
messages = _Messages()
class _ChannelPost(BaseFilter):
update_filter = True
def filter(self, update):
return update.channel_post is not None
channel_post = _ChannelPost()
class _EditedChannelPost(BaseFilter):
update_filter = True
def filter(self, update):
return update.edited_channel_post is not None
edited_channel_post = _EditedChannelPost()
class _ChannelPosts(BaseFilter):
update_filter = True
def filter(self, update):
return update.channel_post is not None or update.edited_channel_post is not None
channel_posts = _ChannelPosts()
def filter(self, update):
return self.messages(update) or self.channel_posts(update)
update = _UpdateType()
"""Subset for filtering the type of update.
Examples:
Use these filters like: ``Filters.update.message`` or
``Filters.update.channel_posts`` etc. Or use just ``Filters.update`` for all
types.
Attributes:
message (:obj:`Filter`): Updates with :attr:`telegram.Update.message`
edited_message (:obj:`Filter`): Updates with :attr:`telegram.Update.edited_message`
messages (:obj:`Filter`): Updates with either :attr:`telegram.Update.message` or
:attr:`telegram.Update.edited_message`
channel_post (:obj:`Filter`): Updates with :attr:`telegram.Update.channel_post`
edited_channel_post (:obj:`Filter`): Updates with
:attr:`telegram.Update.edited_channel_post`
channel_posts (:obj:`Filter`): Updates with either :attr:`telegram.Update.channel_post` or
:attr:`telegram.Update.edited_channel_post`
"""
+56 -17
View File
@@ -24,13 +24,13 @@ class Handler(object):
Attributes:
callback (:obj:`callable`): The callback function for this handler.
pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
passed to the callback function.
pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
the callback function.
pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
Note:
@@ -39,22 +39,34 @@ class Handler(object):
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Args:
callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
It will be called when the :attr:`check_update` has determined that an update should be
processed by this handler.
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
Callback signature for context based API:
``def callback(update: Update, context: CallbackContext)``
The return value of the callback is usually ignored except for the special case of
:class:`telegram.ext.ConversationHandler`.
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``chat_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
"""
@@ -79,31 +91,58 @@ class Handler(object):
update (:obj:`str` | :class:`telegram.Update`): The update to be tested.
Returns:
:obj:`bool`
Either ``None`` or ``False`` if the update should not be handled. Otherwise an object
that will be passed to :attr:`handle_update` and :attr:`collect_additional_context`
when the update gets handled.
"""
raise NotImplementedError
def handle_update(self, update, dispatcher):
def handle_update(self, update, dispatcher, check_result, context=None):
"""
This method is called if it was determined that an update should indeed
be handled by this instance. It should also be overridden, but in most
cases call ``self.callback(dispatcher.bot, update)``, possibly along with
optional arguments. To work with the ``ConversationHandler``, this method should return the
value returned from ``self.callback``
be handled by this instance. Calls :attr:`self.callback` along with its respectful
arguments. To work with the :class:`telegram.ext.ConversationHandler`, this method
returns the value returned from ``self.callback``.
Note that it can be overridden if needed by the subclassing handler.
Args:
update (:obj:`str` | :class:`telegram.Update`): The update to be handled.
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher to collect optional args.
dispatcher (:class:`telegram.ext.Dispatcher`): The calling dispatcher.
check_result: The result from :attr:`check_update`.
"""
raise NotImplementedError
if context:
self.collect_additional_context(context, update, dispatcher, check_result)
return self.callback(update, context)
else:
optional_args = self.collect_optional_args(dispatcher, update, check_result)
return self.callback(dispatcher.bot, update, **optional_args)
def collect_optional_args(self, dispatcher, update=None):
"""Prepares the optional arguments that are the same for all types of handlers.
def collect_additional_context(self, context, update, dispatcher, check_result):
"""Prepares additional arguments for the context. Override if needed.
Args:
context (:class:`telegram.ext.CallbackContext`): The context object.
update (:class:`telegram.Update`): The update to gather chat/user id from.
dispatcher (:class:`telegram.ext.Dispatcher`): The calling dispatcher.
check_result: The result (return value) from :attr:`check_update`.
"""
pass
def collect_optional_args(self, dispatcher, update=None, check_result=None):
"""
Prepares the optional arguments. If the handler has additional optional args,
it should subclass this method, but remember to call this super method.
DEPRECATED: This method is being replaced by new context based callbacks. Please see
https://git.io/fxJuV for more info.
Args:
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher.
update (:class:`telegram.Update`): The update to gather chat/user id from.
check_result: The result from check_update
"""
optional_args = dict()
+36 -31
View File
@@ -22,7 +22,6 @@ import re
from future.utils import string_types
from telegram import Update
from telegram.utils.deprecate import deprecate
from .handler import Handler
@@ -33,19 +32,19 @@ class InlineQueryHandler(Handler):
Attributes:
callback (:obj:`callable`): The callback function for this handler.
pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
passed to the callback function.
pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
pattern (:obj:`str` | :obj:`Pattern`): Optional. Regex pattern to test
:attr:`telegram.InlineQuery.query` against.
pass_groups (:obj:`bool`): Optional. Determines whether ``groups`` will be passed to the
pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the
callback function.
pass_groupdict (:obj:`bool`): Optional. Determines whether ``groupdict``. will be passed to
pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to
the callback function.
pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
the callback function.
pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
Note:
@@ -54,31 +53,46 @@ class InlineQueryHandler(Handler):
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Args:
callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
It will be called when the :attr:`check_update` has determined that an update should be
processed by this handler.
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
Callback signature for context based API:
``def callback(update: Update, context: CallbackContext)``
The return value of the callback is usually ignored except for the special case of
:class:`telegram.ext.ConversationHandler`.
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pattern (:obj:`str` | :obj:`Pattern`, optional): Regex pattern. If not ``None``,
``re.match`` is used on :attr:`telegram.InlineQuery.query` to determine if an update
should be handled by this handler.
pass_groups (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groups()`` as a keyword argument called ``groups``.
Default is ``False``
DEPRECATED: Please switch to context based callbacks.
pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``.
Default is ``False``
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``chat_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
"""
def __init__(self,
@@ -113,37 +127,28 @@ class InlineQueryHandler(Handler):
Returns:
:obj:`bool`
"""
if isinstance(update, Update) and update.inline_query:
if self.pattern:
if update.inline_query.query:
match = re.match(self.pattern, update.inline_query.query)
return bool(match)
if match:
return match
else:
return True
def handle_update(self, update, dispatcher):
"""
Send the update to the :attr:`callback`.
Args:
update (:class:`telegram.Update`): Incoming telegram update.
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
"""
optional_args = self.collect_optional_args(dispatcher, update)
def collect_optional_args(self, dispatcher, update=None, check_result=None):
optional_args = super(InlineQueryHandler, self).collect_optional_args(dispatcher,
update, check_result)
if self.pattern:
match = re.match(self.pattern, update.inline_query.query)
if self.pass_groups:
optional_args['groups'] = match.groups()
optional_args['groups'] = check_result.groups()
if self.pass_groupdict:
optional_args['groupdict'] = match.groupdict()
optional_args['groupdict'] = check_result.groupdict()
return optional_args
return self.callback(dispatcher.bot, update, **optional_args)
# old non-PEP8 Handler methods
m = "telegram.InlineQueryHandler."
checkUpdate = deprecate(check_update, m + "checkUpdate", m + "check_update")
handleUpdate = deprecate(handle_update, m + "handleUpdate", m + "handle_update")
def collect_additional_context(self, context, update, dispatcher, check_result):
if self.pattern:
context.matches = [check_result]
+34 -16
View File
@@ -18,13 +18,17 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes JobQueue and Job."""
import datetime
import logging
import time
import datetime
import warnings
import weakref
from numbers import Number
from threading import Thread, Lock, Event
from queue import PriorityQueue, Empty
from threading import Thread, Lock, Event
from telegram.ext.callbackcontext import CallbackContext
from telegram.utils.deprecate import TelegramDeprecationWarning
class Days(object):
@@ -37,16 +41,24 @@ class JobQueue(object):
Attributes:
_queue (:obj:`PriorityQueue`): The queue that holds the Jobs.
bot (:class:`telegram.Bot`): Bot that's send to the handlers.
Args:
bot (:class:`telegram.Bot`): The bot instance that should be passed to the jobs.
DEPRECATED: Use set_dispatcher instead.
"""
def __init__(self, bot):
def __init__(self, bot=None):
self._queue = PriorityQueue()
self.bot = bot
if bot:
warnings.warn("Passing bot to jobqueue is deprecated. Please use set_dispatcher "
"instead!", TelegramDeprecationWarning, stacklevel=2)
class MockDispatcher(object):
def __init__(self):
self.bot = bot
self.use_context = False
self._dispatcher = MockDispatcher()
else:
self._dispatcher = None
self.logger = logging.getLogger(self.__class__.__name__)
self.__start_lock = Lock()
self.__next_peek_lock = Lock() # to protect self._next_peek & self.__tick
@@ -55,6 +67,9 @@ class JobQueue(object):
self._next_peek = None
self._running = False
def set_dispatcher(self, dispatcher):
self._dispatcher = dispatcher
def _put(self, job, next_t=None, last_t=None):
if next_t is None:
next_t = job.interval
@@ -90,7 +105,7 @@ class JobQueue(object):
Args:
callback (:obj:`callable`): The callback function that should be executed by the new
job. It should take ``bot, job`` as parameters, where ``job`` is the
:class:`telegram.ext.Job` instance. It can be used to access it's
:class:`telegram.ext.Job` instance. It can be used to access its
``job.context`` or change it to a repeating job.
when (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
:obj:`datetime.datetime` | :obj:`datetime.time`):
@@ -122,12 +137,12 @@ class JobQueue(object):
return job
def run_repeating(self, callback, interval, first=None, context=None, name=None):
"""Creates a new ``Job`` that runs once and adds it to the queue.
"""Creates a new ``Job`` that runs at specified intervals and adds it to the queue.
Args:
callback (:obj:`callable`): The callback function that should be executed by the new
job. It should take ``bot, job`` as parameters, where ``job`` is the
:class:`telegram.ext.Job` instance. It can be used to access it's
:class:`telegram.ext.Job` instance. It can be used to access its
``Job.context`` or change it to a repeating job.
interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`): The interval in which
the job will run. If it is an :obj:`int` or a :obj:`float`, it will be interpreted
@@ -168,12 +183,12 @@ class JobQueue(object):
return job
def run_daily(self, callback, time, days=Days.EVERY_DAY, context=None, name=None):
"""Creates a new ``Job`` that runs once and adds it to the queue.
"""Creates a new ``Job`` that runs on a daily basis and adds it to the queue.
Args:
callback (:obj:`callable`): The callback function that should be executed by the new
job. It should take ``bot, job`` as parameters, where ``job`` is the
:class:`telegram.ext.Job` instance. It can be used to access it's ``Job.context``
: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.
days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should
@@ -242,7 +257,7 @@ class JobQueue(object):
current_week_day = datetime.datetime.now().weekday()
if any(day == current_week_day for day in job.days):
self.logger.debug('Running job %s', job.name)
job.run(self.bot)
job.run(self._dispatcher)
except Exception:
self.logger.exception('An uncaught error was raised while executing job %s',
@@ -367,9 +382,12 @@ class Job(object):
self._enabled = Event()
self._enabled.set()
def run(self, bot):
def run(self, dispatcher):
"""Executes the callback function."""
self.callback(bot, self)
if dispatcher.use_context:
self.callback(CallbackContext.from_job(self, dispatcher))
else:
self.callback(dispatcher.bot, self)
def schedule_removal(self):
"""
+74 -71
View File
@@ -20,7 +20,10 @@
"""This module contains the MessageHandler class."""
import warnings
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram import Update
from telegram.ext import Filters
from .handler import Handler
@@ -31,22 +34,20 @@ class MessageHandler(Handler):
filters (:obj:`Filter`): Only allow updates with these Filters. See
:mod:`telegram.ext.filters` for a full list of all available filters.
callback (:obj:`callable`): The callback function for this handler.
pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
passed to the callback function.
pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
the callback function.
pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
message_updates (:obj:`bool`): Optional. Should "normal" message updates be handled?
Default is ``True``.
channel_post_updates (:obj:`bool`): Optional. Should channel posts updates be handled?
Default is ``True``.
edited_updates (:obj:`bool`): Optional. Should "edited" message updates be handled?
Default is ``False``.
allow_edited (:obj:`bool`): Optional. If the handler should also accept edited messages.
Default is ``False`` - Deprecated. use edited_updates instead.
message_updates (:obj:`bool`): Should "normal" message updates be handled?
Default is ``None``.
channel_post_updates (:obj:`bool`): Should channel posts updates be handled?
Default is ``None``.
edited_updates (:obj:`bool`): Should "edited" message updates be handled?
Default is ``None``.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
@@ -54,34 +55,51 @@ class MessageHandler(Handler):
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Args:
filters (:class:`telegram.ext.BaseFilter`, optional): A filter inheriting from
:class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in
:class:`telegram.ext.filters.Filters`. Filters can be combined using bitwise
operators (& for and, | for or, ~ for not).
callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
It will be called when the :attr:`check_update` has determined that an update should be
processed by this handler.
operators (& for and, | for or, ~ for not). Default is
:attr:`telegram.ext.filters.Filters.update`. This defaults to all message_type updates
being: ``message``, ``edited_message``, ``channel_post`` and ``edited_channel_post``.
If you don't want or need any of those pass ``~Filters.update.*`` in the filter
argument.
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
Callback signature for context based API:
``def callback(update: Update, context: CallbackContext)``
The return value of the callback is usually ignored except for the special case of
:class:`telegram.ext.ConversationHandler`.
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``chat_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
message_updates (:obj:`bool`, optional): Should "normal" message updates be handled?
Default is ``True``.
Default is ``None``.
DEPRECATED: Please switch to filters for update filtering.
channel_post_updates (:obj:`bool`, optional): Should channel posts updates be handled?
Default is ``True``.
Default is ``None``.
DEPRECATED: Please switch to filters for update filtering.
edited_updates (:obj:`bool`, optional): Should "edited" message updates be handled? Default
is ``False``.
allow_edited (:obj:`bool`, optional): If the handler should also accept edited messages.
Default is ``False`` - Deprecated. use edited_updates instead.
is ``None``.
DEPRECATED: Please switch to filters for update filtering.
Raises:
ValueError
@@ -91,20 +109,13 @@ class MessageHandler(Handler):
def __init__(self,
filters,
callback,
allow_edited=False,
pass_update_queue=False,
pass_job_queue=False,
pass_user_data=False,
pass_chat_data=False,
message_updates=True,
channel_post_updates=True,
edited_updates=False):
if not message_updates and not channel_post_updates and not edited_updates:
raise ValueError(
'message_updates, channel_post_updates and edited_updates are all False')
if allow_edited:
warnings.warn('allow_edited is getting deprecated, please use edited_updates instead')
edited_updates = allow_edited
message_updates=None,
channel_post_updates=None,
edited_updates=None):
super(MessageHandler, self).__init__(
callback,
@@ -112,22 +123,36 @@ class MessageHandler(Handler):
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
pass_chat_data=pass_chat_data)
if message_updates is False and channel_post_updates is False and edited_updates is False:
raise ValueError(
'message_updates, channel_post_updates and edited_updates are all False')
self.filters = filters
self.message_updates = message_updates
self.channel_post_updates = channel_post_updates
self.edited_updates = edited_updates
if self.filters is not None:
self.filters &= Filters.update
else:
self.filters = Filters.update
if message_updates is not None:
warnings.warn('message_updates is deprecated. See https://git.io/fxJuV for more info',
TelegramDeprecationWarning,
stacklevel=2)
if message_updates is False:
self.filters &= ~Filters.update.message
# We put this up here instead of with the rest of checking code
# in check_update since we don't wanna spam a ton
if isinstance(self.filters, list):
warnings.warn('Using a list of filters in MessageHandler is getting '
'deprecated, please use bitwise operators (& and |) '
'instead. More info: https://git.io/vPTbc.')
if channel_post_updates is not None:
warnings.warn('channel_post_updates is deprecated. See https://git.io/fxJuV '
'for more info',
TelegramDeprecationWarning,
stacklevel=2)
if channel_post_updates is False:
self.filters &= ~Filters.update.channel_post
def _is_allowed_update(self, update):
return any([self.message_updates and update.message,
self.edited_updates and (update.edited_message or update.edited_channel_post),
self.channel_post_updates and update.channel_post])
if edited_updates is not None:
warnings.warn('edited_updates is deprecated. See https://git.io/fxJuV for more info',
TelegramDeprecationWarning,
stacklevel=2)
if edited_updates is False:
self.filters &= ~(Filters.update.edited_message
| Filters.update.edited_channel_post)
def check_update(self, update):
"""Determines whether an update should be passed to this handlers :attr:`callback`.
@@ -139,31 +164,9 @@ class MessageHandler(Handler):
:obj:`bool`
"""
if isinstance(update, Update) and self._is_allowed_update(update):
if isinstance(update, Update) and update.effective_message:
return self.filters(update)
if not self.filters:
res = True
else:
message = update.effective_message
if isinstance(self.filters, list):
res = any(func(message) for func in self.filters)
else:
res = self.filters(message)
else:
res = False
return res
def handle_update(self, update, dispatcher):
"""Send the update to the :attr:`callback`.
Args:
update (:class:`telegram.Update`): Incoming telegram update.
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
"""
optional_args = self.collect_optional_args(dispatcher, update)
return self.callback(dispatcher.bot, update, **optional_args)
def collect_additional_context(self, context, update, dispatcher, check_result):
if isinstance(check_result, dict):
context.update(check_result)
+1 -1
View File
@@ -247,7 +247,7 @@ class MessageQueue(object):
is_group_msg (:obj:`bool`, optional): Defines whether ``promise`` would be processed in
group*+*all* ``DelayQueue``s (if set to ``True``), or only through *all*
``DelayQueue`` (if set to ``False``), resulting in needed delays to avoid
hitting specified limits. Defaults to ``True``.
hitting specified limits. Defaults to ``False``.
Notes:
Method is designed to accept ``telegram.utils.promise.Promise`` as ``promise``
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2018
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the PicklePersistence class."""
import pickle
from collections import defaultdict
from copy import deepcopy
from telegram.ext import BasePersistence
class PicklePersistence(BasePersistence):
"""Using python's builtin pickle for making you bot persistent.
Attributes:
filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file`
is false this will be used as a prefix.
store_user_data (:obj:`bool`): Optional. Whether user_data should be saved by this
persistence class.
store_chat_data (:obj:`bool`): Optional. Whether user_data should be saved by this
persistence class.
single_file (:obj:`bool`): Optional. When ``False`` will store 3 sperate files of
`filename_user_data`, `filename_chat_data` and `filename_conversations`. Default is
``True``.
on_flush (:obj:`bool`, optional): When ``True`` will only save to file when :meth:`flush`
is called and keep data in memory until that happens. When ``False`` will store data
on any transaction *and* on call fo :meth:`flush`. Default is ``False``.
Args:
filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file`
is false this will be used as a prefix.
store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this
persistence class. Default is ``True``.
store_chat_data (:obj:`bool`, optional): Whether user_data should be saved by this
persistence class. Default is ``True``.
single_file (:obj:`bool`, optional): When ``False`` will store 3 sperate files of
`filename_user_data`, `filename_chat_data` and `filename_conversations`. Default is
``True``.
on_flush (:obj:`bool`, optional): When ``True`` will only save to file when :meth:`flush`
is called and keep data in memory until that happens. When ``False`` will store data
on any transaction *and* on call fo :meth:`flush`. Default is ``False``.
"""
def __init__(self, filename, store_user_data=True, store_chat_data=True, singe_file=True,
on_flush=False):
self.filename = filename
self.store_user_data = store_user_data
self.store_chat_data = store_chat_data
self.single_file = singe_file
self.on_flush = on_flush
self.user_data = None
self.chat_data = None
self.conversations = None
def load_singlefile(self):
try:
filename = self.filename
with open(self.filename, "rb") as f:
all = pickle.load(f)
self.user_data = defaultdict(dict, all['user_data'])
self.chat_data = defaultdict(dict, all['chat_data'])
self.conversations = all['conversations']
except IOError:
self.conversations = {}
self.user_data = defaultdict(dict)
self.chat_data = defaultdict(dict)
except pickle.UnpicklingError:
raise TypeError("File {} does not contain valid pickle data".format(filename))
except Exception:
raise TypeError("Something went wrong unpickling {}".format(filename))
def load_file(self, filename):
try:
with open(filename, "rb") as f:
return pickle.load(f)
except IOError:
return None
except pickle.UnpicklingError:
raise TypeError("File {} does not contain valid pickle data".format(filename))
except Exception:
raise TypeError("Something went wrong unpickling {}".format(filename))
def dump_singlefile(self):
with open(self.filename, "wb") as f:
all = {'conversations': self.conversations, 'user_data': self.user_data,
'chat_data': self.chat_data}
pickle.dump(all, f)
def dump_file(self, filename, data):
with open(filename, "wb") as f:
pickle.dump(data, f)
def get_user_data(self):
"""Returns the user_data from the pickle file if it exsists or an empty defaultdict.
Returns:
:obj:`defaultdict`: The restored user data.
"""
if self.user_data:
pass
elif not self.single_file:
filename = "{}_user_data".format(self.filename)
data = self.load_file(filename)
if not data:
data = defaultdict(dict)
else:
data = defaultdict(dict, data)
self.user_data = data
else:
self.load_singlefile()
return deepcopy(self.user_data)
def get_chat_data(self):
"""Returns the chat_data from the pickle file if it exsists or an empty defaultdict.
Returns:
:obj:`defaultdict`: The restored chat data.
"""
if self.chat_data:
pass
elif not self.single_file:
filename = "{}_chat_data".format(self.filename)
data = self.load_file(filename)
if not data:
data = defaultdict(dict)
else:
data = defaultdict(dict, data)
self.chat_data = data
else:
self.load_singlefile()
return deepcopy(self.chat_data)
def get_conversations(self, name):
"""Returns the conversations from the pickle file if it exsists or an empty defaultdict.
Args:
name (:obj:`str`): The handlers name.
Returns:
:obj:`dict`: The restored conversations for the handler.
"""
if self.conversations:
pass
elif not self.single_file:
filename = "{}_conversations".format(self.filename)
data = self.load_file(filename)
if not data:
data = {name: {}}
self.conversations = data
else:
self.load_singlefile()
return self.conversations.get(name, {}).copy()
def update_conversation(self, name, key, new_state):
"""Will update the conversations for the given handler and depending on :attr:`on_flush`
save the pickle file.
Args:
name (:obj:`str`): The handlers name.
key (:obj:`tuple`): The key the state is changed for.
new_state (:obj:`tuple` | :obj:`any`): The new state for the given key.
"""
if self.conversations.setdefault(name, {}).get(key) == new_state:
return
self.conversations[name][key] = new_state
if not self.on_flush:
if not self.single_file:
filename = "{}_conversations".format(self.filename)
self.dump_file(filename, self.conversations)
else:
self.dump_singlefile()
def update_user_data(self, user_id, data):
"""Will update the user_data (if changed) and depending on :attr:`on_flush` save the
pickle file.
Args:
user_id (:obj:`int`): The user the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data`[user_id].
"""
if self.user_data.get(user_id) == data:
return
self.user_data[user_id] = data
if not self.on_flush:
if not self.single_file:
filename = "{}_user_data".format(self.filename)
self.dump_file(filename, self.user_data)
else:
self.dump_singlefile()
def update_chat_data(self, chat_id, data):
"""Will update the chat_data (if changed) and depending on :attr:`on_flush` save the
pickle file.
Args:
chat_id (:obj:`int`): The chat the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data`[chat_id].
"""
if self.chat_data.get(chat_id) == data:
return
self.chat_data[chat_id] = data
if not self.on_flush:
if not self.single_file:
filename = "{}_chat_data".format(self.filename)
self.dump_file(filename, self.chat_data)
else:
self.dump_singlefile()
def flush(self):
""" Will save all data in memory to pickle file(s).
"""
if self.single_file:
if self.user_data or self.chat_data or self.conversations:
self.dump_singlefile()
else:
if self.user_data:
self.dump_file("{}_user_data".format(self.filename), self.user_data)
if self.chat_data:
self.dump_file("{}_chat_data".format(self.filename), self.chat_data)
if self.conversations:
self.dump_file("{}_conversations".format(self.filename), self.conversations)
+19 -31
View File
@@ -27,13 +27,13 @@ class PreCheckoutQueryHandler(Handler):
Attributes:
callback (:obj:`callable`): The callback function for this handler.
pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
passed to the callback function.
pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
the callback function.
pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
Note:
@@ -42,38 +42,37 @@ class PreCheckoutQueryHandler(Handler):
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Args:
callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
It will be called when the :attr:`check_update` has determined that an update should be
processed by this handler.
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
Callback signature for context based API:
``def callback(update: Update, context: CallbackContext)``
The return value of the callback is usually ignored except for the special case of
:class:`telegram.ext.ConversationHandler`.
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
DEPRECATED: Please switch to context based callbacks.
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``chat_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
"""
def __init__(self,
callback,
pass_update_queue=False,
pass_job_queue=False,
pass_user_data=False,
pass_chat_data=False):
super(PreCheckoutQueryHandler, self).__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
pass_chat_data=pass_chat_data)
def check_update(self, update):
"""Determines whether an update should be passed to this handlers :attr:`callback`.
@@ -85,14 +84,3 @@ class PreCheckoutQueryHandler(Handler):
"""
return isinstance(update, Update) and update.pre_checkout_query
def handle_update(self, update, dispatcher):
"""Send the update to the :attr:`callback`.
Args:
update (:class:`telegram.Update`): Incoming telegram update.
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
"""
optional_args = self.collect_optional_args(dispatcher, update)
return self.callback(dispatcher.bot, update, **optional_args)
+39 -80
View File
@@ -19,16 +19,14 @@
# TODO: Remove allow_edited
"""This module contains the RegexHandler class."""
import re
import warnings
from future.utils import string_types
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram import Update
from .handler import Handler
from telegram.ext import MessageHandler, Filters
class RegexHandler(Handler):
class RegexHandler(MessageHandler):
"""Handler class to handle Telegram updates based on a regex.
It uses a regular expression to check text messages. Read the documentation of the ``re``
@@ -38,30 +36,34 @@ class RegexHandler(Handler):
Attributes:
pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
callback (:obj:`callable`): The callback function for this handler.
pass_groups (:obj:`bool`): Optional. Determines whether ``groups`` will be passed to the
pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the
callback function.
pass_groupdict (:obj:`bool`): Optional. Determines whether ``groupdict``. will be passed to
pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to
the callback function.
pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
passed to the callback function.
pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
the callback function.
pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
can use to keep any data in will be sent to the :attr:`callback` function. Related to
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
This handler is being deprecated. For the same usecase use:
``MessageHandler(Filters.regex(r'pattern'), callback)``
Args:
pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
It will be called when the :attr:`check_update` has determined that an update should be
processed by this handler.
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
Callback signature for context based API:
``def callback(update: Update, context: CallbackContext)``
The return value of the callback is usually ignored except for the special case of
:class:`telegram.ext.ConversationHandler`.
pass_groups (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groups()`` as a keyword argument called ``groups``.
Default is ``False``
@@ -86,8 +88,6 @@ class RegexHandler(Handler):
Default is ``True``.
edited_updates (:obj:`bool`, optional): Should "edited" message updates be handled? Default
is ``False``.
allow_edited (:obj:`bool`, optional): If the handler should also accept edited messages.
Default is ``False`` - Deprecated. use edited_updates instead.
Raises:
ValueError
@@ -106,68 +106,27 @@ class RegexHandler(Handler):
allow_edited=False,
message_updates=True,
channel_post_updates=False,
edited_updates=False
):
if not message_updates and not channel_post_updates and not edited_updates:
raise ValueError(
'message_updates, channel_post_updates and edited_updates are all False')
if allow_edited:
warnings.warn('allow_edited is getting deprecated, please use edited_updates instead')
edited_updates = allow_edited
super(RegexHandler, self).__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
pass_chat_data=pass_chat_data)
if isinstance(pattern, string_types):
pattern = re.compile(pattern)
self.pattern = pattern
edited_updates=False):
warnings.warn('RegexHandler is deprecated. See https://git.io/fxJuV for more info',
TelegramDeprecationWarning,
stacklevel=2)
super(RegexHandler, self).__init__(Filters.regex(pattern),
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
pass_chat_data=pass_chat_data,
message_updates=message_updates,
channel_post_updates=channel_post_updates,
edited_updates=edited_updates)
self.pass_groups = pass_groups
self.pass_groupdict = pass_groupdict
self.allow_edited = allow_edited
self.message_updates = message_updates
self.channel_post_updates = channel_post_updates
self.edited_updates = edited_updates
def check_update(self, update):
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
update (:class:`telegram.Update`): Incoming telegram update.
Returns:
:obj:`bool`
"""
if not isinstance(update, Update) and not update.effective_message:
return False
if any([self.message_updates and update.message,
self.edited_updates and (update.edited_message or update.edited_channel_post),
self.channel_post_updates and update.channel_post]) and \
update.effective_message.text:
match = re.match(self.pattern, update.effective_message.text)
return bool(match)
return False
def handle_update(self, update, dispatcher):
"""Send the update to the :attr:`callback`.
Args:
update (:class:`telegram.Update`): Incoming telegram update.
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
"""
optional_args = self.collect_optional_args(dispatcher, update)
match = re.match(self.pattern, update.effective_message.text)
def collect_optional_args(self, dispatcher, update=None, check_result=None):
optional_args = super(RegexHandler, self).collect_optional_args(dispatcher, update,
check_result)
if self.pass_groups:
optional_args['groups'] = match.groups()
optional_args['groups'] = check_result['matches'][0].groups()
if self.pass_groupdict:
optional_args['groupdict'] = match.groupdict()
return self.callback(dispatcher.bot, update, **optional_args)
optional_args['groupdict'] = check_result['matches'][0].groupdict()
return optional_args
+19 -31
View File
@@ -27,13 +27,13 @@ class ShippingQueryHandler(Handler):
Attributes:
callback (:obj:`callable`): The callback function for this handler.
pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
passed to the callback function.
pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
pass_user_data (:obj:`bool`): Optional. Determines whether ``user_data`` will be passed to
pass_user_data (:obj:`bool`): Determines whether ``user_data`` will be passed to
the callback function.
pass_chat_data (:obj:`bool`): Optional. Determines whether ``chat_data`` will be passed to
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
Note:
@@ -42,38 +42,37 @@ class ShippingQueryHandler(Handler):
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Args:
callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
It will be called when the :attr:`check_update` has determined that an update should be
processed by this handler.
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
Callback signature for context based API:
``def callback(update: Update, context: CallbackContext)``
The return value of the callback is usually ignored except for the special case of
:class:`telegram.ext.ConversationHandler`.
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``chat_data`` will be passed to the callback function. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
"""
def __init__(self,
callback,
pass_update_queue=False,
pass_job_queue=False,
pass_user_data=False,
pass_chat_data=False):
super(ShippingQueryHandler, self).__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
pass_chat_data=pass_chat_data)
def check_update(self, update):
"""Determines whether an update should be passed to this handlers :attr:`callback`.
@@ -85,14 +84,3 @@ class ShippingQueryHandler(Handler):
"""
return isinstance(update, Update) and update.shipping_query
def handle_update(self, update, dispatcher):
"""Send the update to the :attr:`callback`.
Args:
update (:class:`telegram.Update`): Incoming telegram update.
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
"""
optional_args = self.collect_optional_args(dispatcher, update)
return self.callback(dispatcher.bot, update, **optional_args)
+30 -26
View File
@@ -33,31 +33,37 @@ class StringCommandHandler(Handler):
Attributes:
command (:obj:`str`): The command this handler should listen for.
callback (:obj:`callable`): The callback function for this handler.
pass_args (:obj:`bool`): Optional. Determines whether the handler should be passed
pass_args (:obj:`bool`): Determines whether the handler should be passed
``args``.
pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
passed to the callback function.
pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
Args:
command (:obj:`str`): The command this handler should listen for.
callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
It will be called when the :attr:`check_update` has determined that a command should be
processed by this handler.
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
Callback signature for context based API:
``def callback(update: Update, context: CallbackContext)``
The return value of the callback is usually ignored except for the special case of
:class:`telegram.ext.ConversationHandler`.
pass_args (:obj:`bool`, optional): Determines whether the handler should be passed the
arguments passed to the command as a keyword argument called ``args``. It will contain
a list of strings, which is the text following the command split on single or
consecutive whitespace characters. Default is ``False``
DEPRECATED: Please switch to context based callbacks.
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
"""
@@ -68,7 +74,9 @@ class StringCommandHandler(Handler):
pass_update_queue=False,
pass_job_queue=False):
super(StringCommandHandler, self).__init__(
callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue)
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue)
self.command = command
self.pass_args = pass_args
@@ -76,28 +84,24 @@ class StringCommandHandler(Handler):
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
update (:obj:`str`): An incomming command.
update (:obj:`str`): An incoming command.
Returns:
:obj:`bool`
"""
if isinstance(update, string_types) and update.startswith('/'):
args = update[1:].split(' ')
if args[0] == self.command:
return args[1:]
return (isinstance(update, string_types) and update.startswith('/')
and update[1:].split(' ')[0] == self.command)
def handle_update(self, update, dispatcher):
"""Send the update to the :attr:`callback`.
Args:
update (:obj:`str`): An incomming command.
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the command.
"""
optional_args = self.collect_optional_args(dispatcher)
def collect_optional_args(self, dispatcher, update=None, check_result=None):
optional_args = super(StringCommandHandler, self).collect_optional_args(dispatcher,
update,
check_result)
if self.pass_args:
optional_args['args'] = update.split()[1:]
optional_args['args'] = check_result
return optional_args
return self.callback(dispatcher.bot, update, **optional_args)
def collect_additional_context(self, context, update, dispatcher, check_result):
context.args = check_result
+36 -26
View File
@@ -38,34 +38,43 @@ class StringRegexHandler(Handler):
Attributes:
pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
callback (:obj:`callable`): The callback function for this handler.
pass_groups (:obj:`bool`): Optional. Determines whether ``groups`` will be passed to the
pass_groups (:obj:`bool`): Determines whether ``groups`` will be passed to the
callback function.
pass_groupdict (:obj:`bool`): Optional. Determines whether ``groupdict``. will be passed to
pass_groupdict (:obj:`bool`): Determines whether ``groupdict``. will be passed to
the callback function.
pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
passed to the callback function.
pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
Args:
pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
It will be called when the :attr:`check_update` has determined that an update should be
processed by this handler.
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
Callback signature for context based API:
``def callback(update: Update, context: CallbackContext)``
The return value of the callback is usually ignored except for the special case of
:class:`telegram.ext.ConversationHandler`.
pass_groups (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groups()`` as a keyword argument called ``groups``.
Default is ``False``
DEPRECATED: Please switch to context based callbacks.
pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``.
Default is ``False``
DEPRECATED: Please switch to context based callbacks.
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
"""
@@ -77,7 +86,9 @@ class StringRegexHandler(Handler):
pass_update_queue=False,
pass_job_queue=False):
super(StringRegexHandler, self).__init__(
callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue)
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue)
if isinstance(pattern, string_types):
pattern = re.compile(pattern)
@@ -90,28 +101,27 @@ class StringRegexHandler(Handler):
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
update (:obj:`str`): An incomming command.
update (:obj:`str`): An incoming command.
Returns:
:obj:`bool`
"""
return isinstance(update, string_types) and bool(re.match(self.pattern, update))
if isinstance(update, string_types):
match = re.match(self.pattern, update)
if match:
return match
def handle_update(self, update, dispatcher):
"""Send the update to the :attr:`callback`.
def collect_optional_args(self, dispatcher, update=None, check_result=None):
optional_args = super(StringRegexHandler, self).collect_optional_args(dispatcher,
update, check_result)
if self.pattern:
if self.pass_groups:
optional_args['groups'] = check_result.groups()
if self.pass_groupdict:
optional_args['groupdict'] = check_result.groupdict()
return optional_args
Args:
update (:obj:`str`): An incomming command.
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the command.
"""
optional_args = self.collect_optional_args(dispatcher)
match = re.match(self.pattern, update)
if self.pass_groups:
optional_args['groups'] = match.groups()
if self.pass_groupdict:
optional_args['groupdict'] = match.groupdict()
return self.callback(dispatcher.bot, update, **optional_args)
def collect_additional_context(self, context, update, dispatcher, check_result):
if self.pattern:
context.matches = [check_result]
+21 -22
View File
@@ -27,36 +27,48 @@ class TypeHandler(Handler):
Attributes:
type (:obj:`type`): The ``type`` of updates this handler should process.
callback (:obj:`callable`): The callback function for this handler.
strict (:obj:`bool`): Optional. Use ``type`` instead of ``isinstance``.
Default is ``False``
pass_update_queue (:obj:`bool`): Optional. Determines whether ``update_queue`` will be
strict (:obj:`bool`): Use ``type`` instead of ``isinstance``. Default is ``False``.
pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
passed to the callback function.
pass_job_queue (:obj:`bool`): Optional. Determines whether ``job_queue`` will be passed to
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
Args:
type (:obj:`type`): The ``type`` of updates this handler should process, as
determined by ``isinstance``
callback (:obj:`callable`): A function that takes ``bot, update`` as positional arguments.
It will be called when the :attr:`check_update` has determined that an update should be
processed by this handler.
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
Callback signature for context based API:
``def callback(update: Update, context: CallbackContext)``
The return value of the callback is usually ignored except for the special case of
:class:`telegram.ext.ConversationHandler`.
strict (:obj:`bool`, optional): Use ``type`` instead of ``isinstance``.
Default is ``False``
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
DEPRECATED: Please switch to context based callbacks.
"""
def __init__(self, type, callback, strict=False, pass_update_queue=False,
def __init__(self,
type,
callback,
strict=False,
pass_update_queue=False,
pass_job_queue=False):
super(TypeHandler, self).__init__(
callback, pass_update_queue=pass_update_queue, pass_job_queue=pass_job_queue)
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue)
self.type = type
self.strict = strict
@@ -70,20 +82,7 @@ class TypeHandler(Handler):
:obj:`bool`
"""
if not self.strict:
return isinstance(update, self.type)
else:
return type(update) is self.type
def handle_update(self, update, dispatcher):
"""Send the update to the :attr:`callback`.
Args:
update (:class:`telegram.Update`): Incoming telegram update.
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
"""
optional_args = self.collect_optional_args(dispatcher)
return self.callback(dispatcher.bot, update, **optional_args)
+38 -30
View File
@@ -19,11 +19,9 @@
"""This module contains the class Updater, which tries to make creating Telegram bots intuitive."""
import logging
import os
import ssl
from threading import Thread, Lock, current_thread, Event
from time import sleep
import subprocess
from signal import signal, SIGINT, SIGTERM, SIGABRT
from queue import Queue
@@ -32,7 +30,7 @@ from telegram.ext import Dispatcher, JobQueue
from telegram.error import Unauthorized, InvalidToken, RetryAfter, TimedOut
from telegram.utils.helpers import get_signal_name
from telegram.utils.request import Request
from telegram.utils.webhookhandler import (WebhookServer, WebhookHandler)
from telegram.utils.webhookhandler import (WebhookServer, WebhookAppClass)
logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -57,6 +55,9 @@ class Updater(object):
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that handles the updates and
dispatches them to the handlers.
running (:obj:`bool`): Indicates if the updater is running.
persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to
store data that should be persistent over restarts.
use_context (:obj:`bool`, optional): ``True`` if using context based callbacks.
Args:
token (:obj:`str`, optional): The bot's token given by the @BotFather.
@@ -75,6 +76,11 @@ class Updater(object):
`telegram.utils.request.Request` object (ignored if `bot` argument is used). The
request_kwargs are very useful for the advanced users who would like to control the
default timeouts and/or control the proxy used for http communication.
use_context (:obj:`bool`, optional): If set to ``True`` Use the context based callback API.
During the deprecation period of the old API the default is ``False``. **New users**:
set this to ``True``.
persistence (:class:`telegram.ext.BasePersistence`, optional): The persistence class to
store data that should be persistent over restarts.
Note:
You must supply either a :attr:`bot` or a :attr:`token` argument.
@@ -94,7 +100,9 @@ class Updater(object):
private_key=None,
private_key_password=None,
user_sig_handler=None,
request_kwargs=None):
request_kwargs=None,
persistence=None,
use_context=False):
if (token is None) and (bot is None):
raise ValueError('`token` or `bot` must be passed')
@@ -129,14 +137,18 @@ class Updater(object):
private_key_password=private_key_password)
self.user_sig_handler = user_sig_handler
self.update_queue = Queue()
self.job_queue = JobQueue(self.bot)
self.job_queue = JobQueue()
self.__exception_event = Event()
self.persistence = persistence
self.dispatcher = Dispatcher(
self.bot,
self.update_queue,
job_queue=self.job_queue,
workers=workers,
exception_event=self.__exception_event)
exception_event=self.__exception_event,
persistence=persistence,
use_context=use_context)
self.job_queue.set_dispatcher(self.dispatcher)
self.last_update_id = 0
self.running = False
self.is_idle = False
@@ -356,13 +368,24 @@ class Updater(object):
if not url_path.startswith('/'):
url_path = '/{0}'.format(url_path)
# Create Tornado app instance
app = WebhookAppClass(url_path, self.bot, self.update_queue)
# Form SSL Context
# An SSLError is raised if the private key does not match with the certificate
if use_ssl:
try:
ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_ctx.load_cert_chain(cert, key)
except ssl.SSLError:
raise TelegramError('Invalid SSL Certificate')
else:
ssl_ctx = None
# Create and start server
self.httpd = WebhookServer((listen, port), WebhookHandler, self.update_queue, url_path,
self.bot)
self.httpd = WebhookServer(port, app, ssl_ctx)
if use_ssl:
self._check_ssl_cert(cert, key)
# DO NOT CHANGE: Only set webhook if SSL is handled by library
if not webhook_url:
webhook_url = self._gen_webhook_url(listen, port, url_path)
@@ -377,26 +400,7 @@ class Updater(object):
self.logger.warning("cleaning updates is not supported if "
"SSL-termination happens elsewhere; skipping")
self.httpd.serve_forever(poll_interval=1)
def _check_ssl_cert(self, cert, key):
# Check SSL-Certificate with openssl, if possible
try:
exit_code = subprocess.call(
["openssl", "x509", "-text", "-noout", "-in", cert],
stdout=open(os.devnull, 'wb'),
stderr=subprocess.STDOUT)
except OSError:
exit_code = 0
if exit_code == 0:
try:
self.httpd.socket = ssl.wrap_socket(
self.httpd.socket, certfile=cert, keyfile=key, server_side=True)
except ssl.SSLError as error:
self.logger.exception('Failed to init SSL socket')
raise TelegramError(str(error))
else:
raise TelegramError('SSL Certificate invalid')
self.httpd.serve_forever()
@staticmethod
def _gen_webhook_url(listen, port, url_path):
@@ -495,6 +499,10 @@ class Updater(object):
if self.running:
self.logger.info('Received signal {} ({}), stopping...'.format(
signum, get_signal_name(signum)))
if self.persistence:
# Update user_data and chat_data before flushing
self.dispatcher.update_persistence()
self.persistence.flush()
self.stop()
if self.user_sig_handler:
self.user_sig_handler(signum, frame)
+7 -2
View File
@@ -17,6 +17,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/].
"""This module contains an object that represents a Telegram File."""
from base64 import b64decode
from os.path import basename
from future.backports.urllib import parse as urllib_parse
@@ -107,7 +108,9 @@ class File(TelegramObject):
if out:
buf = self.bot.request.retrieve(url)
if self._credentials:
buf = decrypt(self._credentials.secret, self._credentials.hash, buf, file=True)
buf = decrypt(b64decode(self._credentials.secret),
b64decode(self._credentials.hash),
buf)
out.write(buf)
return out
else:
@@ -118,7 +121,9 @@ class File(TelegramObject):
buf = self.bot.request.retrieve(url, timeout=timeout)
if self._credentials:
buf = decrypt(self._credentials.secret, self._credentials.hash, buf, file=True)
buf = decrypt(b64decode(self._credentials.secret),
b64decode(self._credentials.hash),
buf)
with open(filename, 'wb') as fobj:
fobj.write(buf)
return filename
+4 -9
View File
@@ -22,7 +22,6 @@
import imghdr
import mimetypes
import os
import sys
from uuid import uuid4
from telegram import TelegramError
@@ -56,9 +55,9 @@ class InputFile(object):
if filename:
self.filename = filename
elif (hasattr(obj, 'name') and
not isinstance(obj.name, int) and # py3
obj.name != '<fdopen>'): # py2
elif (hasattr(obj, 'name')
and not isinstance(obj.name, int) # py3
and obj.name != '<fdopen>'): # py2
# on py2.7, pylint fails to understand this properly
# pylint: disable=E1101
self.filename = os.path.basename(obj.name)
@@ -71,13 +70,9 @@ class InputFile(object):
self.filename)[0] or DEFAULT_MIME_TYPE
else:
self.mimetype = DEFAULT_MIME_TYPE
if not self.filename or '.' not in self.filename:
if not self.filename:
self.filename = self.mimetype.replace('/', '.')
if sys.version_info < (3,):
if isinstance(self.filename, unicode): # flake8: noqa pylint: disable=E0602
self.filename = self.filename.encode('utf-8', 'replace')
@property
def field_tuple(self):
return self.filename, self.input_file_content, self.mimetype
+10 -10
View File
@@ -44,7 +44,7 @@ class InputMediaAnimation(InputMedia):
file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
A thumbnail's width and height should not exceed 90. Ignored if the file is not
is passed as a string or file_id.
caption (:obj:`str`): Optional. Caption of the animation to be sent, 0-200 characters.
caption (:obj:`str`): Optional. Caption of the animation to be sent, 0-1024 characters.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -61,7 +61,7 @@ class InputMediaAnimation(InputMedia):
file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
A thumbnail's width and height should not exceed 90. Ignored if the file is not
is passed as a string or file_id.
caption (:obj:`str`, optional): Caption of the animation to be sent, 0-200 characters.
caption (:obj:`str`, optional): Caption of the animation to be sent, 0-1024 characters.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -114,7 +114,7 @@ class InputMediaPhoto(InputMedia):
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the
Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the
Internet. Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
caption (:obj:`str`): Optional. Caption of the photo to be sent, 0-200 characters.
caption (:obj:`str`): Optional. Caption of the photo to be sent, 0-1024 characters.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -123,7 +123,7 @@ class InputMediaPhoto(InputMedia):
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the
Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the
Internet. Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
caption (:obj:`str`, optional ): Caption of the photo to be sent, 0-200 characters.
caption (:obj:`str`, optional ): Caption of the photo to be sent, 0-1024 characters.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -153,7 +153,7 @@ class InputMediaVideo(InputMedia):
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
Lastly you can pass an existing :class:`telegram.Video` object to send.
caption (:obj:`str`): Optional. Caption of the video to be sent, 0-200 characters.
caption (:obj:`str`): Optional. Caption of the video to be sent, 0-1024 characters.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -171,7 +171,7 @@ class InputMediaVideo(InputMedia):
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
Lastly you can pass an existing :class:`telegram.Video` object to send.
caption (:obj:`str`, optional): Caption of the video to be sent, 0-200 characters.
caption (:obj:`str`, optional): Caption of the video to be sent, 0-1024 characters.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -232,7 +232,7 @@ class InputMediaAudio(InputMedia):
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
Lastly you can pass an existing :class:`telegram.Audio` object to send.
caption (:obj:`str`): Optional. Caption of the audio to be sent, 0-200 characters.
caption (:obj:`str`): Optional. Caption of the audio to be sent, 0-1024 characters.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -249,7 +249,7 @@ class InputMediaAudio(InputMedia):
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
Lastly you can pass an existing :class:`telegram.Document` object to send.
caption (:obj:`str`, optional): Caption of the audio to be sent, 0-200 characters.
caption (:obj:`str`, optional): Caption of the audio to be sent, 0-1024 characters.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -307,7 +307,7 @@ class InputMediaDocument(InputMedia):
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
Lastly you can pass an existing :class:`telegram.Document` object to send.
caption (:obj:`str`): Optional. Caption of the document to be sent, 0-200 characters.
caption (:obj:`str`): Optional. Caption of the document to be sent, 0-1024 characters.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -320,7 +320,7 @@ class InputMediaDocument(InputMedia):
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
Lastly you can pass an existing :class:`telegram.Document` object to send.
caption (:obj:`str`, optional): Caption of the document to be sent, 0-200 characters.
caption (:obj:`str`, optional): Caption of the document to be sent, 0-1024 characters.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+48
View File
@@ -48,3 +48,51 @@ class InlineKeyboardMarkup(ReplyMarkup):
data['inline_keyboard'].append([x.to_dict() for x in inline_keyboard])
return data
@classmethod
def from_button(cls, button, **kwargs):
"""Shortcut for::
InlineKeyboardMarkup([[button]], **kwargs)
Return an InlineKeyboardMarkup from a single InlineKeyboardButton
Args:
button (:class:`telegram.InlineKeyboardButton`): The button to use in the markup
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
return cls([[button]], **kwargs)
@classmethod
def from_row(cls, button_row, **kwargs):
"""Shortcut for::
InlineKeyboardMarkup([button_row], **kwargs)
Return an InlineKeyboardMarkup from a single row of InlineKeyboardButtons
Args:
button_row (List[:class:`telegram.InlineKeyboardButton`]): The button to use in the
markup
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
return cls([button_row], **kwargs)
@classmethod
def from_column(cls, button_column, **kwargs):
"""Shortcut for::
InlineKeyboardMarkup([[button] for button in button_column], **kwargs)
Return an InlineKeyboardMarkup from a single column of InlineKeyboardButtons
Args:
button_column (List[:class:`telegram.InlineKeyboardButton`]): The button to use in the
markup
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
button_grid = [[button] for button in button_column]
return cls(button_grid, **kwargs)
@@ -31,7 +31,7 @@ class InlineQueryResultCachedAudio(InlineQueryResult):
type (:obj:`str`): 'audio'.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
audio_file_id (:obj:`str`): A valid file identifier for the audio file.
caption (:obj:`str`): Optional. Caption, 0-200 characters
caption (:obj:`str`): Optional. Caption, 0-1024 characters
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -43,7 +43,7 @@ class InlineQueryResultCachedAudio(InlineQueryResult):
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
audio_file_id (:obj:`str`): A valid file identifier for the audio file.
caption (:obj:`str`, optional): Caption, 0-200 characters
caption (:obj:`str`, optional): Caption, 0-1024 characters
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -33,7 +33,7 @@ class InlineQueryResultCachedDocument(InlineQueryResult):
title (:obj:`str`): Title for the result.
document_file_id (:obj:`str`): A valid file identifier for the file.
description (:obj:`str`): Optional. Short description of the result.
caption (:obj:`str`): Optional. Caption, 0-200 characters
caption (:obj:`str`): Optional. Caption, 0-1024 characters
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption.. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -47,7 +47,7 @@ class InlineQueryResultCachedDocument(InlineQueryResult):
title (:obj:`str`): Title for the result.
document_file_id (:obj:`str`): A valid file identifier for the file.
description (:obj:`str`, optional): Short description of the result.
caption (:obj:`str`, optional): Caption, 0-200 characters
caption (:obj:`str`, optional): Caption, 0-1024 characters
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption.. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -33,7 +33,7 @@ class InlineQueryResultCachedGif(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
gif_file_id (:obj:`str`): A valid file identifier for the GIF file.
title (:obj:`str`): Optional. Title for the result.
caption (:obj:`str`): Optional. Caption, 0-200 characters
caption (:obj:`str`): Optional. Caption, 0-1024 characters
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -46,7 +46,7 @@ class InlineQueryResultCachedGif(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
gif_file_id (:obj:`str`): A valid file identifier for the GIF file.
title (:obj:`str`, optional): Title for the result.caption (:obj:`str`, optional):
caption (:obj:`str`, optional): Caption, 0-200 characters
caption (:obj:`str`, optional): Caption, 0-1024 characters
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -33,7 +33,7 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
mpeg4_file_id (:obj:`str`): A valid file identifier for the MP4 file.
title (:obj:`str`): Optional. Title for the result.
caption (:obj:`str`): Optional. Caption, 0-200 characters
caption (:obj:`str`): Optional. Caption, 0-1024 characters
parse_mode (:obj:`str`): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -46,7 +46,7 @@ class InlineQueryResultCachedMpeg4Gif(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
mpeg4_file_id (:obj:`str`): A valid file identifier for the MP4 file.
title (:obj:`str`, optional): Title for the result.
caption (:obj:`str`, optional): Caption, 0-200 characters
caption (:obj:`str`, optional): Caption, 0-1024 characters
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -34,7 +34,7 @@ class InlineQueryResultCachedPhoto(InlineQueryResult):
photo_file_id (:obj:`str`): A valid file identifier of the photo.
title (:obj:`str`): Optional. Title for the result.
description (:obj:`str`): Optional. Short description of the result.
caption (:obj:`str`): Optional. Caption, 0-200 characters
caption (:obj:`str`): Optional. Caption, 0-1024 characters
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -48,7 +48,7 @@ class InlineQueryResultCachedPhoto(InlineQueryResult):
photo_file_id (:obj:`str`): A valid file identifier of the photo.
title (:obj:`str`, optional): Title for the result.
description (:obj:`str`, optional): Short description of the result.
caption (:obj:`str`, optional): Caption, 0-200 characters
caption (:obj:`str`, optional): Caption, 0-1024 characters
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -34,7 +34,7 @@ class InlineQueryResultCachedVideo(InlineQueryResult):
video_file_id (:obj:`str`): A valid file identifier for the video file.
title (:obj:`str`): Title for the result.
description (:obj:`str`): Optional. Short description of the result.
caption (:obj:`str`): Optional. Caption, 0-200 characters.
caption (:obj:`str`): Optional. Caption, 0-1024 characters.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -48,7 +48,7 @@ class InlineQueryResultCachedVideo(InlineQueryResult):
video_file_id (:obj:`str`): A valid file identifier for the video file.
title (:obj:`str`): Title for the result.
description (:obj:`str`, optional): Short description of the result.
caption (:obj:`str`, optional): Caption, 0-200 characters.
caption (:obj:`str`, optional): Caption, 0-1024 characters.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -32,7 +32,7 @@ class InlineQueryResultCachedVoice(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
voice_file_id (:obj:`str`): A valid file identifier for the voice message.
title (:obj:`str`): Voice message title.
caption (:obj:`str`): Optional. Caption, 0-200 characters.
caption (:obj:`str`): Optional. Caption, 0-1024 characters.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -45,7 +45,7 @@ class InlineQueryResultCachedVoice(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
voice_file_id (:obj:`str`): A valid file identifier for the voice message.
title (:obj:`str`): Voice message title.
caption (:obj:`str`, optional): Caption, 0-200 characters.
caption (:obj:`str`, optional): Caption, 0-1024 characters.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+2 -2
View File
@@ -32,7 +32,7 @@ class InlineQueryResultDocument(InlineQueryResult):
type (:obj:`str`): 'document'.
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
title (:obj:`str`): Title for the result.
caption (:obj:`str`): Optional. Caption, 0-200 characters
caption (:obj:`str`): Optional. Caption, 0-1024 characters
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -51,7 +51,7 @@ class InlineQueryResultDocument(InlineQueryResult):
Args:
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
title (:obj:`str`): Title for the result.
caption (:obj:`str`, optional): Caption, 0-200 characters
caption (:obj:`str`, optional): Caption, 0-1024 characters
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+2 -2
View File
@@ -36,7 +36,7 @@ class InlineQueryResultGif(InlineQueryResult):
gif_duration (:obj:`int`): Optional. Duration of the GIF.
thumb_url (:obj:`str`): URL of the static thumbnail for the result (jpeg or gif).
title (:obj:`str`): Optional. Title for the result.
caption (:obj:`str`): Optional. Caption, 0-200 characters
caption (:obj:`str`): Optional. Caption, 0-1024 characters
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -53,7 +53,7 @@ class InlineQueryResultGif(InlineQueryResult):
gif_duration (:obj:`int`, optional): Duration of the GIF
thumb_url (:obj:`str`): URL of the static thumbnail for the result (jpeg or gif).
title (:obj:`str`, optional): Title for the result.caption (:obj:`str`, optional):
caption (:obj:`str`, optional): Caption, 0-200 characters
caption (:obj:`str`, optional): Caption, 0-1024 characters
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+2 -2
View File
@@ -37,7 +37,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
mpeg4_duration (:obj:`int`): Optional. Video duration.
thumb_url (:obj:`str`): URL of the static thumbnail (jpeg or gif) for the result.
title (:obj:`str`): Optional. Title for the result.
caption (:obj:`str`): Optional. Caption, 0-200 characters
caption (:obj:`str`): Optional. Caption, 0-1024 characters
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -54,7 +54,7 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
mpeg4_duration (:obj:`int`, optional): Video duration.
thumb_url (:obj:`str`): URL of the static thumbnail (jpeg or gif) for the result.
title (:obj:`str`, optional): Title for the result.
caption (:obj:`str`, optional): Caption, 0-200 characters
caption (:obj:`str`, optional): Caption, 0-1024 characters
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+2 -2
View File
@@ -37,7 +37,7 @@ class InlineQueryResultPhoto(InlineQueryResult):
photo_height (:obj:`int`): Optional. Height of the photo.
title (:obj:`str`): Optional. Title for the result.
description (:obj:`str`): Optional. Short description of the result.
caption (:obj:`str`): Optional. Caption, 0-200 characters
caption (:obj:`str`): Optional. Caption, 0-1024 characters
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -55,7 +55,7 @@ class InlineQueryResultPhoto(InlineQueryResult):
photo_height (:obj:`int`, optional): Height of the photo.
title (:obj:`str`, optional): Title for the result.
description (:obj:`str`, optional): Short description of the result.
caption (:obj:`str`, optional): Caption, 0-200 characters
caption (:obj:`str`, optional): Caption, 0-1024 characters
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+2 -2
View File
@@ -35,7 +35,7 @@ class InlineQueryResultVideo(InlineQueryResult):
mime_type (:obj:`str`): Mime type of the content of video url, "text/html" or "video/mp4".
thumb_url (:obj:`str`): URL of the thumbnail (jpeg only) for the video.
title (:obj:`str`): Title for the result.
caption (:obj:`str`): Optional. Caption, 0-200 characters
caption (:obj:`str`): Optional. Caption, 0-1024 characters
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -54,7 +54,7 @@ class InlineQueryResultVideo(InlineQueryResult):
mime_type (:obj:`str`): Mime type of the content of video url, "text/html" or "video/mp4".
thumb_url (:obj:`str`): URL of the thumbnail (jpeg only) for the video.
title (:obj:`str`): Title for the result.
caption (:obj:`str`, optional): Caption, 0-200 characters.
caption (:obj:`str`, optional): Caption, 0-1024 characters.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
+2 -2
View File
@@ -33,7 +33,7 @@ class InlineQueryResultVoice(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
voice_url (:obj:`str`): A valid URL for the voice recording.
title (:obj:`str`): Voice message title.
caption (:obj:`str`): Optional. Caption, 0-200 characters.
caption (:obj:`str`): Optional. Caption, 0-1024 characters.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption.. See the constants
in :class:`telegram.ParseMode` for the available modes.
@@ -47,7 +47,7 @@ class InlineQueryResultVoice(InlineQueryResult):
id (:obj:`str`): Unique identifier for this result, 1-64 bytes.
voice_url (:obj:`str`): A valid URL for the voice recording.
title (:obj:`str`): Voice message title.
caption (:obj:`str`, optional): Caption, 0-200 characters.
caption (:obj:`str`, optional): Caption, 0-1024 characters.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption.. See the constants
in :class:`telegram.ParseMode` for the available modes.
+15 -8
View File
@@ -73,7 +73,8 @@ class Message(TelegramObject):
video_note (:class:`telegram.VideoNote`): Optional. Information about the video message.
new_chat_members (List[:class:`telegram.User`]): Optional. Information about new members to
the chat. (the bot itself may be one of these members).
caption (:obj:`str`): Optional. Caption for the document, photo or video, 0-200 characters.
caption (:obj:`str`): Optional. Caption for the document, photo or video, 0-1024
characters.
contact (:class:`telegram.Contact`): Optional. Information about the contact.
location (:class:`telegram.Location`): Optional. Information about the location.
venue (:class:`telegram.Venue`): Optional. Information about the venue.
@@ -154,7 +155,8 @@ class Message(TelegramObject):
new_chat_members (List[:class:`telegram.User`], optional): New members that were added to
the group or supergroup and information about them (the bot itself may be one of these
members).
caption (:obj:`str`, optional): Caption for the document, photo or video, 0-200 characters.
caption (:obj:`str`, optional): Caption for the document, photo or video, 0-1024
characters.
contact (:class:`telegram.Contact`, optional): Message is a shared contact, information
about the contact.
location (:class:`telegram.Location`, optional): Message is a shared location, information
@@ -207,10 +209,11 @@ class Message(TelegramObject):
ATTACHMENT_TYPES = ['audio', 'game', 'animation', 'document', 'photo', 'sticker', 'video',
'voice', 'video_note', 'contact', 'location', 'venue', 'invoice',
'successful_payment']
MESSAGE_TYPES = ['text', 'new_chat_members', 'new_chat_title', 'new_chat_photo',
'delete_chat_photo', 'group_chat_created', 'supergroup_chat_created',
'channel_chat_created', 'migrate_to_chat_id', 'migrate_from_chat_id',
'pinned_message', 'passport_data'] + ATTACHMENT_TYPES
MESSAGE_TYPES = ['text', 'new_chat_members', 'left_chat_member', 'new_chat_title',
'new_chat_photo', 'delete_chat_photo', 'group_chat_created',
'supergroup_chat_created', 'channel_chat_created', 'migrate_to_chat_id',
'migrate_from_chat_id', 'pinned_message',
'passport_data'] + ATTACHMENT_TYPES
def __init__(self,
message_id,
@@ -943,7 +946,9 @@ class Message(TelegramObject):
if entity.type == MessageEntity.TEXT_LINK:
insert = '<a href="{}">{}</a>'.format(entity.url, text)
elif (entity.type == MessageEntity.URL) and urled:
elif entity.type == MessageEntity.TEXT_MENTION and entity.user:
insert = '<a href="tg://user?id={}">{}</a>'.format(entity.user.id, text)
elif entity.type == MessageEntity.URL and urled:
insert = '<a href="{0}">{0}</a>'.format(text)
elif entity.type == MessageEntity.BOLD:
insert = '<b>' + text + '</b>'
@@ -1040,7 +1045,9 @@ class Message(TelegramObject):
if entity.type == MessageEntity.TEXT_LINK:
insert = '[{}]({})'.format(text, entity.url)
elif (entity.type == MessageEntity.URL) and urled:
elif entity.type == MessageEntity.TEXT_MENTION and entity.user:
insert = '[{}](tg://user?id={})'.format(text, entity.user.id)
elif entity.type == MessageEntity.URL and urled:
insert = '[{0}]({0})'.format(text)
elif entity.type == MessageEntity.BOLD:
insert = '*' + text + '*'
+19 -26
View File
@@ -16,7 +16,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 binascii
try:
import ujson as json
except ImportError:
@@ -44,7 +43,7 @@ class TelegramDecryptionError(TelegramError):
"{}".format(message))
def decrypt(secret, hash, data, file=False):
def decrypt(secret, hash, data):
"""
Decrypt per telegram docs at https://core.telegram.org/passport.
@@ -65,20 +64,6 @@ def decrypt(secret, hash, data, file=False):
:obj:`bytes`: The decrypted data as bytes.
"""
# First make sure that if secret, hash, or data was base64 encoded, to decode it into bytes
try:
secret = b64decode(secret)
except (binascii.Error, TypeError):
pass
try:
hash = b64decode(hash)
except (binascii.Error, TypeError):
pass
if not file:
try:
data = b64decode(data)
except (binascii.Error, TypeError):
pass
# Make a SHA512 hash of secret + update
digest = Hash(SHA512(), backend=default_backend())
digest.update(secret + hash)
@@ -113,14 +98,14 @@ class EncryptedCredentials(TelegramObject):
Attributes:
data (:class:`telegram.Credentials` or :obj:`str`): Decrypted data with unique user's
payload, data hashes and secrets used for EncryptedPassportElement decryption and
nonce, data hashes and secrets used for EncryptedPassportElement decryption and
authentication or base64 encrypted data.
hash (:obj:`str`): Base64-encoded data hash for data authentication.
secret (:obj:`str`): Decrypted or encrypted secret used for decryption.
Args:
data (:class:`telegram.Credentials` or :obj:`str`): Decrypted data with unique user's
payload, data hashes and secrets used for EncryptedPassportElement decryption and
nonce, data hashes and secrets used for EncryptedPassportElement decryption and
authentication or base64 encrypted data.
hash (:obj:`str`): Base64-encoded data hash for data authentication.
secret (:obj:`str`): Decrypted or encrypted secret used for decryption.
@@ -184,8 +169,8 @@ class EncryptedCredentials(TelegramObject):
def decrypted_data(self):
"""
:class:`telegram.Credentials`: Lazily decrypt and return credentials data. This object
also contains the user specified payload as
`decrypted_data.payload`.
also contains the user specified nonce as
`decrypted_data.nonce`.
Raises:
telegram.TelegramDecryptionError: Decryption failed. Usually due to bad
@@ -193,8 +178,8 @@ class EncryptedCredentials(TelegramObject):
"""
if self._decrypted_data is None:
self._decrypted_data = Credentials.de_json(decrypt_json(self.decrypted_secret,
self.hash,
self.data),
b64decode(self.hash),
b64decode(self.data)),
self.bot)
return self._decrypted_data
@@ -203,13 +188,13 @@ class Credentials(TelegramObject):
"""
Attributes:
secure_data (:class:`telegram.SecureData`): Credentials for encrypted data
payload (:obj:`str`): Bot-specified payload
nonce (:obj:`str`): Bot-specified nonce
"""
def __init__(self, secure_data, payload, bot=None, **kwargs):
def __init__(self, secure_data, nonce, bot=None, **kwargs):
# Required
self.secure_data = secure_data
self.payload = payload
self.nonce = nonce
self.bot = bot
@@ -319,7 +304,11 @@ class SecureValue(TelegramObject):
selfie (:class:`telegram.FileCredentials`, optional): Credentials for encrypted selfie
of the user with a document. Can be available for "passport", "driver_license",
"identity_card" and "internal_passport".
files (:class:`telegram.Array of FileCredentials`, optional): Credentials for encrypted
translation (List[:class:`telegram.FileCredentials`], optional): Credentials for an
encrypted translation of the document. Available for "passport", "driver_license",
"identity_card", "internal_passport", "utility_bill", "bank_statement",
"rental_agreement", "passport_registration" and "temporary_registration".
files (List[:class:`telegram.FileCredentials`], optional): Credentials for encrypted
files. Available for "utility_bill", "bank_statement", "rental_agreement",
"passport_registration" and "temporary_registration" types.
@@ -331,6 +320,7 @@ class SecureValue(TelegramObject):
reverse_side=None,
selfie=None,
files=None,
translation=None,
bot=None,
**kwargs):
self.data = data
@@ -338,6 +328,7 @@ class SecureValue(TelegramObject):
self.reverse_side = reverse_side
self.selfie = selfie
self.files = files
self.translation = translation
self.bot = bot
@@ -351,6 +342,7 @@ class SecureValue(TelegramObject):
data['reverse_side'] = FileCredentials.de_json(data.get('reverse_side'), bot=bot)
data['selfie'] = FileCredentials.de_json(data.get('selfie'), bot=bot)
data['files'] = FileCredentials.de_list(data.get('files'), bot=bot)
data['translation'] = FileCredentials.de_list(data.get('translation'), bot=bot)
return cls(bot=bot, **data)
@@ -358,6 +350,7 @@ class SecureValue(TelegramObject):
data = super(SecureValue, self).to_dict()
data['files'] = [p.to_dict() for p in self.files]
data['translation'] = [p.to_dict() for p in self.translation]
return data
+12 -1
View File
@@ -25,23 +25,34 @@ class PersonalDetails(TelegramObject):
Attributes:
first_name (:obj:`str`): First Name.
middle_name (:obj:`str`): Optional. First Name.
last_name (:obj:`str`): Last Name.
birth_date (:obj:`str`): Date of birth in DD.MM.YYYY format.
gender (:obj:`str`): Gender, male or female.
country_code (:obj:`str`): Citizenship (ISO 3166-1 alpha-2 country code).
residence_country_code (:obj:`str`): Country of residence (ISO 3166-1 alpha-2 country
code).
first_name (:obj:`str`): First Name in the language of the user's country of residence.
middle_name (:obj:`str`): Optional. Middle Name in the language of the user's country of
residence.
last_name (:obj:`str`): Last Name in the language of the user's country of residence.
"""
def __init__(self, first_name, last_name, birth_date, gender, country_code,
residence_country_code, bot=None, **kwargs):
residence_country_code, first_name_native=None,
last_name_native=None, middle_name=None,
middle_name_native=None, bot=None, **kwargs):
# Required
self.first_name = first_name
self.last_name = last_name
self.middle_name = middle_name
self.birth_date = birth_date
self.gender = gender
self.country_code = country_code
self.residence_country_code = residence_country_code
self.first_name_native = first_name_native
self.last_name_native = last_name_native
self.middle_name_native = middle_name_native
self.bot = bot
+44 -20
View File
@@ -17,6 +17,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/].
"""This module contains an object that represents a Telegram EncryptedPassportElement."""
from base64 import b64decode
from telegram import (TelegramObject, PassportFile, PersonalDetails, IdDocumentData,
ResidentialAddress)
@@ -43,15 +44,22 @@ class EncryptedPassportElement(TelegramObject):
files (List[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted files
with documents provided by the user, available for "utility_bill", "bank_statement",
"rental_agreement", "passport_registration" and "temporary_registration" types.
front_side (:class:`PassportFile`): Optional. Encrypted/decrypted file with the front side
of the document, provided by the user. Available for "passport", "driver_license",
"identity_card" and "internal_passport".
reverse_side (:class:`PassportFile`): Optional. Encrypted/decrypted file with the reverse
side of the document, provided by the user. Available for "driver_license" and
"identity_card".
selfie (:class:`PassportFile`): Optional. Encrypted/decrypted file with the selfie of the
user holding a document, provided by the user; available for "passport",
front_side (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the
front side of the document, provided by the user. Available for "passport",
"driver_license", "identity_card" and "internal_passport".
reverse_side (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the
reverse side of the document, provided by the user. Available for "driver_license" and
"identity_card".
selfie (:class:`telegram.PassportFile`): Optional. Encrypted/decrypted file with the
selfie of the user holding a document, provided by the user; available for "passport",
"driver_license", "identity_card" and "internal_passport".
translation (List[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted
files with translated versions of documents provided by the user. Available if
requested for "passport", "driver_license", "identity_card", "internal_passport",
"utility_bill", "bank_statement", "rental_agreement", "passport_registration" and
"temporary_registration" types.
hash (:obj:`str`): Base64-encoded element hash for using in
:class:`telegram.PassportElementErrorUnspecified`.
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
Args:
@@ -69,15 +77,22 @@ class EncryptedPassportElement(TelegramObject):
files (List[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted files
with documents provided by the user, available for "utility_bill", "bank_statement",
"rental_agreement", "passport_registration" and "temporary_registration" types.
front_side (:class:`PassportFile`, optional): Encrypted/decrypted file with the front side
of the document, provided by the user. Available for "passport", "driver_license",
"identity_card" and "internal_passport".
reverse_side (:class:`PassportFile`, optional): Encrypted/decrypted file with the reverse
side of the document, provided by the user. Available for "driver_license" and
"identity_card".
selfie (:class:`PassportFile`, optional): Encrypted/decrypted file with the selfie of the
user holding a document, provided by the user; available for "passport",
front_side (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the
front side of the document, provided by the user. Available for "passport",
"driver_license", "identity_card" and "internal_passport".
reverse_side (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the
reverse side of the document, provided by the user. Available for "driver_license" and
"identity_card".
selfie (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the
selfie of the user holding a document, provided by the user; available for "passport",
"driver_license", "identity_card" and "internal_passport".
translation (List[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted
files with translated versions of documents provided by the user. Available if
requested for "passport", "driver_license", "identity_card", "internal_passport",
"utility_bill", "bank_statement", "rental_agreement", "passport_registration" and
"temporary_registration" types.
hash (:obj:`str`): Base64-encoded element hash for using in
:class:`telegram.PassportElementErrorUnspecified`.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
@@ -95,6 +110,8 @@ class EncryptedPassportElement(TelegramObject):
front_side=None,
reverse_side=None,
selfie=None,
translation=None,
hash=None,
bot=None,
credentials=None,
**kwargs):
@@ -108,6 +125,8 @@ class EncryptedPassportElement(TelegramObject):
self.front_side = front_side
self.reverse_side = reverse_side
self.selfie = selfie
self.translation = translation
self.hash = hash
self._id_attrs = (self.type, self.data, self.phone_number, self.email, self.files,
self.front_side, self.reverse_side, self.selfie)
@@ -125,6 +144,7 @@ class EncryptedPassportElement(TelegramObject):
data['front_side'] = PassportFile.de_json(data.get('front_side'), bot)
data['reverse_side'] = PassportFile.de_json(data.get('reverse_side'), bot)
data['selfie'] = PassportFile.de_json(data.get('selfie'), bot)
data['translation'] = PassportFile.de_list(data.get('translation'), bot) or None
return cls(bot=bot, **data)
@@ -141,9 +161,9 @@ class EncryptedPassportElement(TelegramObject):
if secure_data.data is not None:
# If not already decrypted
if not isinstance(data['data'], dict):
data['data'] = decrypt_json(secure_data.data.secret,
secure_data.data.hash,
data['data'])
data['data'] = decrypt_json(b64decode(secure_data.data.secret),
b64decode(secure_data.data.hash),
b64decode(data['data']))
if data['type'] == 'personal_details':
data['data'] = PersonalDetails.de_json(data['data'], bot=bot)
elif data['type'] in ('passport', 'internal_passport',
@@ -153,13 +173,15 @@ class EncryptedPassportElement(TelegramObject):
data['data'] = ResidentialAddress.de_json(data['data'], bot=bot)
data['files'] = PassportFile.de_list_decrypted(data.get('files'), bot,
secure_data) or None
secure_data.files) or None
data['front_side'] = PassportFile.de_json_decrypted(data.get('front_side'), bot,
secure_data.front_side)
data['reverse_side'] = PassportFile.de_json_decrypted(data.get('reverse_side'), bot,
secure_data.reverse_side)
data['selfie'] = PassportFile.de_json_decrypted(data.get('selfie'), bot,
secure_data.selfie)
data['translation'] = PassportFile.de_list_decrypted(data.get('translation'), bot,
secure_data.translation) or None
return cls(bot=bot, **data)
@@ -179,5 +201,7 @@ class EncryptedPassportElement(TelegramObject):
if self.files:
data['files'] = [p.to_dict() for p in self.files]
if self.translation:
data['translation'] = [p.to_dict() for p in self.translation]
return data
+107 -2
View File
@@ -148,8 +148,8 @@ class PassportElementErrorFiles(PassportElementError):
super(PassportElementErrorFiles, self).__init__('files', type, message)
self.file_hashes = file_hashes
self._id_attrs = ((self.source, self.type, self.message) +
tuple([file_hash for file_hash in file_hashes]))
self._id_attrs = ((self.source, self.type, self.message)
+ tuple([file_hash for file_hash in file_hashes]))
class PassportElementErrorFrontSide(PassportElementError):
@@ -250,3 +250,108 @@ class PassportElementErrorSelfie(PassportElementError):
self.file_hash = file_hash
self._id_attrs = (self.source, self.type, self.file_hash, self.message)
class PassportElementErrorTranslationFile(PassportElementError):
"""
Represents an issue with one of the files that constitute the translation of a document.
The error is considered resolved when the file changes.
Attributes:
type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue,
one of "passport", "driver_license", "identity_card", "internal_passport",
"utility_bill", "bank_statement", "rental_agreement", "passport_registration",
"temporary_registration".
file_hash (:obj:`str`): Base64-encoded hash of the file.
message (:obj:`str`): Error message.
Args:
type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue,
one of "passport", "driver_license", "identity_card", "internal_passport",
"utility_bill", "bank_statement", "rental_agreement", "passport_registration",
"temporary_registration".
file_hash (:obj:`str`): Base64-encoded hash of the file.
message (:obj:`str`): Error message.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
def __init__(self,
type,
file_hash,
message,
**kwargs):
# Required
super(PassportElementErrorTranslationFile, self).__init__('translation_file',
type, message)
self.file_hash = file_hash
self._id_attrs = (self.source, self.type, self.file_hash, self.message)
class PassportElementErrorTranslationFiles(PassportElementError):
"""
Represents an issue with the translated version of a document. The error is considered
resolved when a file with the document translation change.
Attributes:
type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue,
one of "passport", "driver_license", "identity_card", "internal_passport",
"utility_bill", "bank_statement", "rental_agreement", "passport_registration",
"temporary_registration"
file_hash (:obj:`str`): Base64-encoded file hash.
message (:obj:`str`): Error message.
Args:
type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue,
one of "passport", "driver_license", "identity_card", "internal_passport",
"utility_bill", "bank_statement", "rental_agreement", "passport_registration",
"temporary_registration"
file_hashes (List[:obj:`str`]): List of base64-encoded file hashes.
message (:obj:`str`): Error message.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
def __init__(self,
type,
file_hashes,
message,
**kwargs):
# Required
super(PassportElementErrorTranslationFiles, self).__init__('translation_files',
type, message)
self.file_hashes = file_hashes
self._id_attrs = ((self.source, self.type, self.message)
+ tuple([file_hash for file_hash in file_hashes]))
class PassportElementErrorUnspecified(PassportElementError):
"""
Represents an issue in an unspecified place. The error is considered resolved when new
data is added.
Attributes:
type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue.
element_hash (:obj:`str`): Base64-encoded element hash.
message (:obj:`str`): Error message.
Args:
type (:obj:`str`): Type of element of the user's Telegram Passport which has the issue.
element_hash (:obj:`str`): Base64-encoded element hash.
message (:obj:`str`): Error message.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
def __init__(self,
type,
element_hash,
message,
**kwargs):
# Required
super(PassportElementErrorUnspecified, self).__init__('unspecified', type, message)
self.element_hash = element_hash
self._id_attrs = (self.source, self.type, self.element_hash, self.message)
+1 -1
View File
@@ -84,7 +84,7 @@ class PassportFile(TelegramObject):
if not data:
return []
return [cls.de_json_decrypted(passport_file, bot, credentials.files[i])
return [cls.de_json_decrypted(passport_file, bot, credentials[i])
for i, passport_file in enumerate(data)]
def get_file(self, timeout=None, **kwargs):
+128
View File
@@ -85,3 +85,131 @@ class ReplyKeyboardMarkup(ReplyMarkup):
r.append(button) # str
data['keyboard'].append(r)
return data
@classmethod
def from_button(cls,
button,
resize_keyboard=False,
one_time_keyboard=False,
selective=False,
**kwargs):
"""Shortcut for::
ReplyKeyboardMarkup([[button]], **kwargs)
Return an ReplyKeyboardMarkup from a single KeyboardButton
Args:
button (:class:`telegram.KeyboardButton` | :obj:`str`): The button to use in the markup
resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard
vertically for optimal fit (e.g., make the keyboard smaller if there are just two
rows of buttons). Defaults to false, in which case the custom keyboard is always of
the same height as the app's standard keyboard.
Defaults to ``False``
one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as
soon as it's been used. The keyboard will still be available, but clients will
automatically display the usual letter-keyboard in the chat - the user can press
a special button in the input field to see the custom keyboard again.
Defaults to ``False``.
selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard
to specific users only. Targets:
1) users that are @mentioned in the text of the Message object
2) if the bot's message is a reply (has reply_to_message_id), sender of the
original message.
Defaults to ``False``.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
return cls([[button]],
resize_keyboard=resize_keyboard,
one_time_keyboard=one_time_keyboard,
selective=selective,
**kwargs)
@classmethod
def from_row(cls,
button_row,
resize_keyboard=False,
one_time_keyboard=False,
selective=False,
**kwargs):
"""Shortcut for::
ReplyKeyboardMarkup([button_row], **kwargs)
Return an ReplyKeyboardMarkup from a single row of KeyboardButtons
Args:
button_row (List[:class:`telegram.KeyboardButton` | :obj:`str`]): The button to use in
the markup
resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard
vertically for optimal fit (e.g., make the keyboard smaller if there are just two
rows of buttons). Defaults to false, in which case the custom keyboard is always of
the same height as the app's standard keyboard.
Defaults to ``False``
one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as
soon as it's been used. The keyboard will still be available, but clients will
automatically display the usual letter-keyboard in the chat - the user can press
a special button in the input field to see the custom keyboard again.
Defaults to ``False``.
selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard
to specific users only. Targets:
1) users that are @mentioned in the text of the Message object
2) if the bot's message is a reply (has reply_to_message_id), sender of the
original message.
Defaults to ``False``.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
return cls([button_row],
resize_keyboard=resize_keyboard,
one_time_keyboard=one_time_keyboard,
selective=selective,
**kwargs)
@classmethod
def from_column(cls,
button_column,
resize_keyboard=False,
one_time_keyboard=False,
selective=False,
**kwargs):
"""Shortcut for::
ReplyKeyboardMarkup([[button] for button in button_column], **kwargs)
Return an ReplyKeyboardMarkup from a single column of KeyboardButtons
Args:
button_column (List[:class:`telegram.KeyboardButton` | :obj:`str`]): The button to use
in the markup
resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard
vertically for optimal fit (e.g., make the keyboard smaller if there are just two
rows of buttons). Defaults to false, in which case the custom keyboard is always of
the same height as the app's standard keyboard.
Defaults to ``False``
one_time_keyboard (:obj:`bool`, optional): Requests clients to hide the keyboard as
soon as it's been used. The keyboard will still be available, but clients will
automatically display the usual letter-keyboard in the chat - the user can press
a special button in the input field to see the custom keyboard again.
Defaults to ``False``.
selective (:obj:`bool`, optional): Use this parameter if you want to show the keyboard
to specific users only. Targets:
1) users that are @mentioned in the text of the Message object
2) if the bot's message is a reply (has reply_to_message_id), sender of the
original message.
Defaults to ``False``.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
button_grid = [[button] for button in button_column]
return cls(button_grid,
resize_keyboard=resize_keyboard,
one_time_keyboard=one_time_keyboard,
selective=selective,
**kwargs)
+68
View File
@@ -17,6 +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."""
from collections import defaultdict
try:
import ujson as json
except ImportError:
import json
from html import escape
import re
@@ -139,3 +145,65 @@ def effective_message_type(entity):
return i
return None
def enocde_conversations_to_json(conversations):
"""Helper method to encode a conversations dict (that uses tuples as keys) to a
JSON-serializable way. Use :attr:`_decode_conversations_from_json` to decode.
Args:
conversations (:obj:`dict`): The conversations dict to transofrm to JSON.
Returns:
:obj:`str`: The JSON-serialized conversations dict
"""
tmp = {}
for handler, states in conversations.items():
tmp[handler] = {}
for key, state in states.items():
tmp[handler][json.dumps(key)] = state
return json.dumps(tmp)
def decode_conversations_from_json(json_string):
"""Helper method to decode a conversations dict (that uses tuples as keys) from a
JSON-string created with :attr:`_encode_conversations_to_json`.
Args:
json_string (:obj:`str`): The conversations dict as JSON string.
Returns:
:obj:`dict`: The conversations dict after decoding
"""
tmp = json.loads(json_string)
conversations = {}
for handler, states in tmp.items():
conversations[handler] = {}
for key, state in states.items():
conversations[handler][tuple(json.loads(key))] = state
return conversations
def decode_user_chat_data_from_json(data):
"""Helper method to decode chat or user data (that uses ints as keys) from a
JSON-string.
Args:
data (:obj:`str`): The user/chat_data dict as JSON string.
Returns:
:obj:`dict`: The user/chat_data defaultdict after decoding
"""
tmp = defaultdict(dict)
decoded_data = json.loads(data)
for user, data in decoded_data.items():
user = int(user)
tmp[user] = {}
for key, value in data.items():
try:
key = int(key)
except ValueError:
pass
tmp[user][key] = value
return tmp
+20 -2
View File
@@ -36,6 +36,7 @@ try:
import telegram.vendor.ptb_urllib3.urllib3.contrib.appengine as appengine
from telegram.vendor.ptb_urllib3.urllib3.connection import HTTPConnection
from telegram.vendor.ptb_urllib3.urllib3.util.timeout import Timeout
from telegram.vendor.ptb_urllib3.urllib3.fields import RequestField
except ImportError: # pragma: no cover
warnings.warn("python-telegram-bot wasn't properly installed. Please refer to README.rst on "
"how to properly install.")
@@ -43,7 +44,21 @@ except ImportError: # pragma: no cover
from telegram import (InputFile, TelegramError, InputMedia)
from telegram.error import (Unauthorized, NetworkError, TimedOut, BadRequest, ChatMigrated,
RetryAfter, InvalidToken)
RetryAfter, InvalidToken, Conflict)
def _render_part(self, name, value):
"""
Monkey patch urllib3.urllib3.fields.RequestField to make it *not* support RFC2231 compliant
Content-Disposition headers since telegram servers don't understand it. Instead just escape
\ and " and replace any \n and \r with a space.
"""
value = value.replace(u'\\', u'\\\\').replace(u'"', u'\\"')
value = value.replace(u'\r', u' ').replace(u'\n', u' ')
return u'%s="%s"' % (name, value)
RequestField._render_part = _render_part
logging.getLogger('urllib3').setLevel(logging.WARNING)
@@ -223,6 +238,8 @@ class Request(object):
raise BadRequest(message)
elif resp.status == 404:
raise InvalidToken()
elif resp.status == 409:
raise Conflict(message)
elif resp.status == 413:
raise NetworkError('File too large. Check telegram api limits '
'https://core.telegram.org/bots/api#senddocument')
@@ -306,7 +323,8 @@ class Request(object):
else:
result = self._request_wrapper('POST', url,
body=json.dumps(data).encode('utf-8'),
headers={'Content-Type': 'application/json'})
headers={'Content-Type': 'application/json'},
**urlopen_kwargs)
return self._parse(result)
+55 -71
View File
@@ -17,7 +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 logging
from telegram import Update
from future.utils import bytes_to_native_str
from threading import Lock
@@ -25,39 +24,35 @@ try:
import ujson as json
except ImportError:
import json
try:
import BaseHTTPServer
except ImportError:
import http.server as BaseHTTPServer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
import tornado.web
import tornado.iostream
logging.getLogger(__name__).addHandler(logging.NullHandler())
class _InvalidPost(Exception):
class WebhookServer(object):
def __init__(self, http_code):
self.http_code = http_code
super(_InvalidPost, self).__init__()
class WebhookServer(BaseHTTPServer.HTTPServer, object):
def __init__(self, server_address, RequestHandlerClass, update_queue, webhook_path, bot):
super(WebhookServer, self).__init__(server_address, RequestHandlerClass)
def __init__(self, port, webhook_app, ssl_ctx):
self.http_server = HTTPServer(webhook_app, ssl_options=ssl_ctx)
self.port = port
self.loop = None
self.logger = logging.getLogger(__name__)
self.update_queue = update_queue
self.webhook_path = webhook_path
self.bot = bot
self.is_running = False
self.server_lock = Lock()
self.shutdown_lock = Lock()
def serve_forever(self, poll_interval=0.5):
def serve_forever(self):
with self.server_lock:
IOLoop().make_current()
self.is_running = True
self.logger.debug('Webhook Server started.')
super(WebhookServer, self).serve_forever(poll_interval)
self.http_server.listen(self.port)
self.loop = IOLoop.current()
self.loop.start()
self.logger.debug('Webhook Server stopped.')
self.is_running = False
def shutdown(self):
with self.shutdown_lock:
@@ -65,8 +60,7 @@ class WebhookServer(BaseHTTPServer.HTTPServer, object):
self.logger.warning('Webhook Server already stopped.')
return
else:
super(WebhookServer, self).shutdown()
self.is_running = False
self.loop.add_callback(self.loop.stop)
def handle_error(self, request, client_address):
"""Handle an error gracefully."""
@@ -74,64 +68,52 @@ class WebhookServer(BaseHTTPServer.HTTPServer, object):
client_address, exc_info=True)
class WebhookAppClass(tornado.web.Application):
def __init__(self, webhook_path, bot, update_queue):
self.shared_objects = {"bot": bot, "update_queue": update_queue}
handlers = [
(r"{0}/?".format(webhook_path), WebhookHandler,
self.shared_objects)
] # noqa
tornado.web.Application.__init__(self, handlers)
def log_request(self, handler):
pass
# WebhookHandler, process webhook calls
# Based on: https://github.com/eternnoir/pyTelegramBotAPI/blob/master/
# examples/webhook_examples/webhook_cpython_echo_bot.py
class WebhookHandler(BaseHTTPServer.BaseHTTPRequestHandler, object):
server_version = 'WebhookHandler/1.0'
class WebhookHandler(tornado.web.RequestHandler):
SUPPORTED_METHODS = ["POST"]
def __init__(self, request, client_address, server):
def __init__(self, application, request, **kwargs):
super(WebhookHandler, self).__init__(application, request, **kwargs)
self.logger = logging.getLogger(__name__)
super(WebhookHandler, self).__init__(request, client_address, server)
def do_HEAD(self):
self.send_response(200)
self.end_headers()
def initialize(self, bot, update_queue):
self.bot = bot
self.update_queue = update_queue
def do_GET(self):
self.send_response(200)
self.end_headers()
def set_default_headers(self):
self.set_header("Content-Type", 'application/json; charset="utf-8"')
def do_POST(self):
def post(self):
self.logger.debug('Webhook triggered')
try:
self._validate_post()
clen = self._get_content_len()
except _InvalidPost as e:
self.send_error(e.http_code)
self.end_headers()
else:
buf = self.rfile.read(clen)
json_string = bytes_to_native_str(buf)
self.send_response(200)
self.end_headers()
self.logger.debug('Webhook received data: ' + json_string)
update = Update.de_json(json.loads(json_string), self.server.bot)
self.logger.debug('Received Update with ID %d on Webhook' % update.update_id)
self.server.update_queue.put(update)
self._validate_post()
json_string = bytes_to_native_str(self.request.body)
data = json.loads(json_string)
self.set_status(200)
self.logger.debug('Webhook received data: ' + json_string)
update = Update.de_json(data, self.bot)
self.logger.debug('Received Update with ID %d on Webhook' % update.update_id)
self.update_queue.put(update)
def _validate_post(self):
if not (self.path == self.server.webhook_path and 'content-type' in self.headers and
self.headers['content-type'] == 'application/json'):
raise _InvalidPost(403)
ct_header = self.request.headers.get("Content-Type", None)
if ct_header != 'application/json':
raise tornado.web.HTTPError(403)
def _get_content_len(self):
clen = self.headers.get('content-length')
if clen is None:
raise _InvalidPost(411)
try:
clen = int(clen)
except ValueError:
raise _InvalidPost(403)
if clen < 0:
raise _InvalidPost(403)
return clen
def log_message(self, format, *args):
def write_error(self, status_code, **kwargs):
"""Log an arbitrary message.
This is used by all other logging functions.
@@ -145,4 +127,6 @@ class WebhookHandler(BaseHTTPServer.BaseHTTPRequestHandler, object):
The client ip is prefixed to every message.
"""
self.logger.debug("%s - - %s" % (self.address_string(), format % args))
super(WebhookHandler, self).write_error(status_code, **kwargs)
self.logger.debug("%s - - %s" % (self.request.remote_ip, "Exception in WebhookHandler"),
exc_info=kwargs['exc_info'])
+1 -1
View File
@@ -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__ = '11.0.0'
__version__ = '12.0.0b1'
+23 -12
View File
@@ -18,26 +18,37 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""Provide a bot to tests"""
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
FALLBACKS = {
'token': '133505823:AAHZFMHno3mzVLErU5b5jJvaeG--qUyLyG0',
'payment_provider_token': '284685063:TEST:ZGJlMmQxZDI3ZTc3',
'chat_id': '12173560',
'group_id': '-49740850',
'channel_id': '@pythontelegrambottests'
}
# These bots are only able to talk in our test chats, so they are quite useless for other
# purposes than testing.
FALLBACKS = [
{
'token': '579694714:AAHRLL5zBVy4Blx2jRFKe1HlfnXCg08WuLY',
'payment_provider_token': '284685063:TEST:NjQ0NjZlNzI5YjJi',
'chat_id': '675666224',
'group_id': '-269513406',
'channel_id': '@pythontelegrambottests'
}, {
'token': '558194066:AAEEylntuKSLXj9odiv3TnX7Z5KY2J3zY3M',
'payment_provider_token': '284685063:TEST:YjEwODQwMTFmNDcy',
'chat_id': '675666224',
'group_id': '-269513406',
'channel_id': '@pythontelegrambottests'
}
]
def get(name, fallback):
full_name = '{0}-{1}-{2[0]}{2[1]}'.format(name, python_implementation(),
full_name = '{0}_{1}_{2[0]}{2[1]}'.format(name, python_implementation(),
sys.version_info).upper()
# First try fullnames such as
# TOKEN-CPYTHON-33
# CHAT_ID-PYPY-27
# First try full_names such as
# TOKEN_CPYTHON_33
# CHAT_ID_PYPY_27
val = os.getenv(full_name)
if val:
return val
@@ -52,4 +63,4 @@ def get(name, fallback):
def get_bot():
return {k: get(k, v) for k, v in FALLBACKS.items()}
return {k: get(k, v) for k, v in random.choice(FALLBACKS).items()}
+12 -1
View File
@@ -72,7 +72,8 @@ def provider_token(bot_info):
def create_dp(bot):
# Dispatcher is heavy to init (due to many threads and such) so we have a single session
# scoped one here, but before each test, reset it (dp fixture below)
dispatcher = Dispatcher(bot, Queue(), job_queue=JobQueue(bot), workers=2)
dispatcher = Dispatcher(bot, Queue(), job_queue=JobQueue(), workers=2, use_context=False)
dispatcher.job_queue.set_dispatcher(dispatcher)
thr = Thread(target=dispatcher.start)
thr.start()
sleep(2)
@@ -96,6 +97,7 @@ def dp(_dp):
_dp.update_queue.get(False)
_dp.chat_data = defaultdict(dict)
_dp.user_data = defaultdict(dict)
_dp.persistence = None
_dp.handlers = {}
_dp.groups = []
_dp.error_handlers = []
@@ -103,12 +105,21 @@ def dp(_dp):
_dp.__exception_event = Event()
_dp.__async_queue = Queue()
_dp.__async_threads = set()
_dp.persistence = None
_dp.use_context = False
if _dp._Dispatcher__singleton_semaphore.acquire(blocking=0):
Dispatcher._set_singleton(_dp)
yield _dp
Dispatcher._Dispatcher__singleton_semaphore.release()
@pytest.fixture(scope='function')
def cdp(dp):
dp.use_context = True
yield dp
dp.use_context = False
@pytest.fixture(scope='function')
def updater(bot):
up = Updater(bot=bot, workers=2)
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+4 -4
View File
@@ -44,13 +44,13 @@ class TestAnimation(object):
duration = 1
file_name = 'game.gif.mp4'
mime_type = 'video/mp4'
file_size = 4135
file_size = 4127
caption = "Test *animation*"
def test_creation(self, animation):
assert isinstance(animation, Animation)
assert isinstance(animation.file_id, str)
assert animation.file_id is not ''
assert animation.file_id != ''
def test_expected_values(self, animation):
assert animation.file_size == self.file_size
@@ -72,8 +72,8 @@ class TestAnimation(object):
assert message.animation.file_name == animation.file_name
assert message.animation.mime_type == animation.mime_type
assert message.animation.file_size == animation.file_size
assert message.animation.thumb.width == 50
assert message.animation.thumb.height == 50
assert message.animation.thumb.width == 320
assert message.animation.thumb.height == 180
@flaky(3, 1)
def test_resend(self, bot, chat_id, animation):
+2 -2
View File
@@ -48,7 +48,7 @@ class TestAudio(object):
audio_file_url = 'https://goo.gl/3En24v'
mime_type = 'audio/mpeg'
file_size = 122920
thumb_file_size = 2744
thumb_file_size = 1427
thumb_width = 50
thumb_height = 50
@@ -56,7 +56,7 @@ class TestAudio(object):
# Make sure file has been uploaded.
assert isinstance(audio, Audio)
assert isinstance(audio.file_id, str)
assert audio.file_id is not ''
assert audio.file_id != ''
def test_expected_values(self, audio):
assert audio.duration == self.duration
+60 -35
View File
@@ -17,6 +17,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 os
import sys
import time
from datetime import datetime
from platform import python_implementation
@@ -146,16 +147,16 @@ class TestBot(object):
assert message.contact.first_name == first_name
assert message.contact.last_name == last_name
@pytest.mark.skipif(os.getenv('APPVEYOR'), reason='No game made for Appveyor bot (''yet)')
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_send_game(self, bot, chat_id):
game_short_name = 'python_telegram_bot_test_game'
game_short_name = 'test_game'
message = bot.send_game(chat_id, game_short_name)
assert message.game
assert message.game.description == 'This is a test game for python-telegram-bot.'
assert message.game.animation.file_id == 'CgADAQADKwIAAvjAuQABozciVqhFDO0C'
assert message.game.description == ('A no-op test game, for python-telegram-bot '
'bot framework testing.')
assert message.game.animation.file_id != ''
assert message.game.photo[0].file_size == 851
@flaky(3, 1)
@@ -190,18 +191,16 @@ class TestBot(object):
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_get_user_profile_photos(self, bot):
chat_id = 12173560 # hardcoded Leandro's chat_id
def test_get_user_profile_photos(self, bot, chat_id):
user_profile_photos = bot.get_user_profile_photos(chat_id)
assert user_profile_photos.photos[0][0].file_size == 9999
assert user_profile_photos.photos[0][0].file_size == 5403
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_get_one_user_profile_photo(self, bot):
chat_id = 12173560 # hardcoded Leandro's chat_id
def test_get_one_user_profile_photo(self, bot, chat_id):
user_profile_photos = bot.get_user_profile_photos(chat_id, offset=0, limit=1)
assert user_profile_photos.photos[0][0].file_size == 9999
assert user_profile_photos.photos[0][0].file_size == 5403
# get_file is tested multiple times in the test_*media* modules.
@@ -275,7 +274,6 @@ class TestBot(object):
assert message.caption == 'new caption'
@pytest.mark.xfail(raises=TelegramError) # TODO: remove when #744 is merged
def test_edit_message_caption_without_required(self, bot):
with pytest.raises(ValueError, match='Both chat_id and message_id are required when'):
bot.edit_message_caption(caption='new_caption')
@@ -294,7 +292,6 @@ class TestBot(object):
assert message is not True
@pytest.mark.xfail(raises=TelegramError) # TODO: remove when #744 is merged
def test_edit_message_reply_markup_without_required(self, bot):
new_markup = InlineKeyboardMarkup([[InlineKeyboardButton(text='test', callback_data='1')]])
with pytest.raises(ValueError, match='Both chat_id and message_id are required when'):
@@ -318,6 +315,8 @@ class TestBot(object):
@flaky(3, 1)
@pytest.mark.timeout(15)
@pytest.mark.xfail
@pytest.mark.skipif(os.getenv('APPVEYOR') and (sys.version_info < (3, 6)),
reason='only run on 3.6 on appveyor')
def test_set_webhook_get_webhook_info_and_delete_webhook(self, bot):
url = 'https://python-telegram-bot.org/test/webhook'
max_connections = 7
@@ -349,7 +348,7 @@ class TestBot(object):
chat = bot.get_chat(group_id)
assert chat.type == 'group'
assert chat.title == '>>> telegram.Bot() - Developers'
assert chat.title == '>>> telegram.Bot(test)'
assert chat.id == int(group_id)
@flaky(3, 1)
@@ -370,11 +369,12 @@ class TestBot(object):
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_get_chat_member(self, bot, channel_id):
chat_member = bot.get_chat_member(channel_id, 103246792) # Eldin
def test_get_chat_member(self, bot, channel_id, chat_id):
chat_member = bot.get_chat_member(channel_id, chat_id)
assert chat_member.status == 'administrator'
assert chat_member.user.username == 'EchteEldin'
assert chat_member.user.first_name == 'PTB'
assert chat_member.user.last_name == 'Test user'
@pytest.mark.skip(reason="Not implemented yet.")
def test_set_chat_sticker_set(self):
@@ -384,12 +384,11 @@ class TestBot(object):
def test_delete_chat_sticker_set(self):
pass
@pytest.mark.skipif(os.getenv('APPVEYOR'), reason='No game made for Appveyor bot (yet)')
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_set_game_score_1(self, bot, chat_id):
# NOTE: numbering of methods assures proper order between test_set_game_scoreX methods
game_short_name = 'python_telegram_bot_test_game'
game_short_name = 'test_game'
game = bot.send_game(chat_id, game_short_name)
message = bot.set_game_score(
@@ -403,12 +402,11 @@ class TestBot(object):
assert message.game.photo[0].file_size == game.game.photo[0].file_size
assert message.game.text != game.game.text
@pytest.mark.skipif(os.getenv('APPVEYOR'), reason='No game made for Appveyor bot (yet)')
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_set_game_score_2(self, bot, chat_id):
# NOTE: numbering of methods assures proper order between test_set_game_scoreX methods
game_short_name = 'python_telegram_bot_test_game'
game_short_name = 'test_game'
game = bot.send_game(chat_id, game_short_name)
score = int(BASE_TIME) - HIGHSCORE_DELTA + 1
@@ -425,12 +423,11 @@ class TestBot(object):
assert message.game.photo[0].file_size == game.game.photo[0].file_size
assert message.game.text == game.game.text
@pytest.mark.skipif(os.getenv('APPVEYOR'), reason='No game made for Appveyor bot (yet)')
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_set_game_score_3(self, bot, chat_id):
# NOTE: numbering of methods assures proper order between test_set_game_scoreX methods
game_short_name = 'python_telegram_bot_test_game'
game_short_name = 'test_game'
game = bot.send_game(chat_id, game_short_name)
score = int(BASE_TIME) - HIGHSCORE_DELTA - 1
@@ -442,12 +439,11 @@ class TestBot(object):
chat_id=game.chat_id,
message_id=game.message_id)
@pytest.mark.skipif(os.getenv('APPVEYOR'), reason='No game made for Appveyor bot (yet)')
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_set_game_score_4(self, bot, chat_id):
# NOTE: numbering of methods assures proper order between test_set_game_scoreX methods
game_short_name = 'python_telegram_bot_test_game'
game_short_name = 'test_game'
game = bot.send_game(chat_id, game_short_name)
score = int(BASE_TIME) - HIGHSCORE_DELTA - 2
@@ -468,24 +464,22 @@ class TestBot(object):
game2 = bot.send_game(chat_id, game_short_name)
assert str(score) in game2.game.text
@pytest.mark.skipif(os.getenv('APPVEYOR'), reason='No game made for Appveyor bot (yet)')
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_set_game_score_too_low_score(self, bot, chat_id):
# We need a game to set the score for
game_short_name = 'python_telegram_bot_test_game'
game_short_name = 'test_game'
game = bot.send_game(chat_id, game_short_name)
with pytest.raises(BadRequest):
bot.set_game_score(user_id=chat_id, score=100,
chat_id=game.chat_id, message_id=game.message_id)
@pytest.mark.skipif(os.getenv('APPVEYOR'), reason='No game made for Appveyor bot (yet)')
@flaky(3, 1)
@pytest.mark.timeout(10)
def test_get_game_high_scores(self, bot, chat_id):
# We need a game to get the scores for
game_short_name = 'python_telegram_bot_test_game'
game_short_name = 'test_game'
game = bot.send_game(chat_id, game_short_name)
high_scores = bot.get_game_high_scores(chat_id, game.chat_id, game.message_id)
# We assume that the other game score tests ran within 20 sec
@@ -572,7 +566,7 @@ class TestBot(object):
@pytest.mark.timeout(10)
def test_promote_chat_member(self, bot, channel_id):
# TODO: Add bot to supergroup so this can be tested properly / give bot perms
with pytest.raises(BadRequest, match='Chat_admin_required'):
with pytest.raises(BadRequest, match='Not enough rights'):
assert bot.promote_chat_member(channel_id,
95205500,
can_change_info=True,
@@ -627,17 +621,48 @@ class TestBot(object):
# set_sticker_position_in_set and delete_sticker_from_set are tested in the
# test_sticker module.
def test_timeout_propagation(self, monkeypatch, bot, chat_id):
def test_timeout_propagation_explicit(self, monkeypatch, bot, chat_id):
from telegram.vendor.ptb_urllib3.urllib3.util.timeout import Timeout
class OkException(Exception):
pass
timeout = 500
TIMEOUT = 500
def post(*args, **kwargs):
if kwargs.get('timeout') == 500:
def request_wrapper(*args, **kwargs):
obj = kwargs.get('timeout')
if isinstance(obj, Timeout) and obj._read == TIMEOUT:
raise OkException
monkeypatch.setattr('telegram.utils.request.Request.post', post)
return b'{"ok": true, "result": []}'
monkeypatch.setattr('telegram.utils.request.Request._request_wrapper', request_wrapper)
# Test file uploading
with pytest.raises(OkException):
bot.send_photo(chat_id, open('tests/data/telegram.jpg', 'rb'), timeout=timeout)
bot.send_photo(chat_id, open('tests/data/telegram.jpg', 'rb'), timeout=TIMEOUT)
# Test JSON submition
with pytest.raises(OkException):
bot.get_chat_administrators(chat_id, timeout=TIMEOUT)
def test_timeout_propagation_implicit(self, monkeypatch, bot, chat_id):
from telegram.vendor.ptb_urllib3.urllib3.util.timeout import Timeout
class OkException(Exception):
pass
def request_wrapper(*args, **kwargs):
obj = kwargs.get('timeout')
if isinstance(obj, Timeout) and obj._read == 20:
raise OkException
return b'{"ok": true, "result": []}'
monkeypatch.setattr('telegram.utils.request.Request._request_wrapper', request_wrapper)
# Test file uploading
with pytest.raises(OkException):
bot.send_photo(chat_id, open('tests/data/telegram.jpg', 'rb'))
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2018
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import pytest
from telegram import Update, Message, Chat, User, TelegramError
from telegram.ext import CallbackContext
class TestCallbackContext(object):
def test_non_context_dp(self, dp):
with pytest.raises(ValueError):
CallbackContext(dp)
def test_from_job(self, cdp):
job = cdp.job_queue.run_once(lambda x: x, 10)
callback_context = CallbackContext.from_job(job, cdp)
assert callback_context.job is job
assert callback_context.chat_data is None
assert callback_context.user_data is None
assert callback_context.bot is cdp.bot
assert callback_context.job_queue is cdp.job_queue
assert callback_context.update_queue is cdp.update_queue
def test_from_update(self, cdp):
update = Update(0, message=Message(0, User(1, 'user', False), None, Chat(1, 'chat')))
callback_context = CallbackContext.from_update(update, cdp)
assert callback_context.chat_data == {}
assert callback_context.user_data == {}
assert callback_context.bot is cdp.bot
assert callback_context.job_queue is cdp.job_queue
assert callback_context.update_queue is cdp.update_queue
callback_context_same_user_chat = CallbackContext.from_update(update, cdp)
callback_context.chat_data['test'] = 'chat'
callback_context.user_data['test'] = 'user'
assert callback_context_same_user_chat.chat_data is callback_context.chat_data
assert callback_context_same_user_chat.user_data is callback_context.user_data
update_other_user_chat = Update(0, message=Message(0, User(2, 'user', False),
None, Chat(2, 'chat')))
callback_context_other_user_chat = CallbackContext.from_update(update_other_user_chat, cdp)
assert callback_context_other_user_chat.chat_data is not callback_context.chat_data
assert callback_context_other_user_chat.user_data is not callback_context.user_data
def test_from_update_not_update(self, cdp):
callback_context = CallbackContext.from_update(None, cdp)
assert callback_context.chat_data is None
assert callback_context.user_data is None
assert callback_context.bot is cdp.bot
assert callback_context.job_queue is cdp.job_queue
assert callback_context.update_queue is cdp.update_queue
callback_context = CallbackContext.from_update('', cdp)
assert callback_context.chat_data is None
assert callback_context.user_data is None
assert callback_context.bot is cdp.bot
assert callback_context.job_queue is cdp.job_queue
assert callback_context.update_queue is cdp.update_queue
def test_from_error(self, cdp):
error = TelegramError('test')
update = Update(0, message=Message(0, User(1, 'user', False), None, Chat(1, 'chat')))
callback_context = CallbackContext.from_error(update, error, cdp)
assert callback_context.error is error
assert callback_context.chat_data == {}
assert callback_context.user_data == {}
assert callback_context.bot is cdp.bot
assert callback_context.job_queue is cdp.job_queue
assert callback_context.update_queue is cdp.update_queue
def test_match(self, cdp):
callback_context = CallbackContext(cdp)
assert callback_context.match is None
callback_context.matches = ['test', 'blah']
assert callback_context.match == 'test'
+6 -6
View File
@@ -88,48 +88,48 @@ class TestCallbackQuery(object):
def test_edit_message_text(self, monkeypatch, callback_query):
def test(*args, **kwargs):
text = args[1] == 'test'
try:
id = kwargs['inline_message_id'] == callback_query.inline_message_id
text = kwargs['text'] == 'test'
return id and text
except KeyError:
chat_id = kwargs['chat_id'] == callback_query.message.chat_id
message_id = kwargs['message_id'] == callback_query.message.message_id
text = kwargs['text'] == 'test'
return chat_id and message_id and text
monkeypatch.setattr('telegram.Bot.edit_message_text', test)
assert callback_query.edit_message_text(text='test')
assert callback_query.edit_message_text('test')
def test_edit_message_caption(self, monkeypatch, callback_query):
def test(*args, **kwargs):
caption = kwargs['caption'] == 'new caption'
try:
id = kwargs['inline_message_id'] == callback_query.inline_message_id
caption = kwargs['caption'] == 'new caption'
return id and caption
except KeyError:
id = kwargs['chat_id'] == callback_query.message.chat_id
message = kwargs['message_id'] == callback_query.message.message_id
caption = kwargs['caption'] == 'new caption'
return id and message and caption
monkeypatch.setattr('telegram.Bot.edit_message_caption', test)
assert callback_query.edit_message_caption(caption='new caption')
assert callback_query.edit_message_caption('new caption')
def test_edit_message_reply_markup(self, monkeypatch, callback_query):
def test(*args, **kwargs):
reply_markup = kwargs['reply_markup'] == [['1', '2']]
try:
id = kwargs['inline_message_id'] == callback_query.inline_message_id
reply_markup = kwargs['reply_markup'] == [['1', '2']]
return id and reply_markup
except KeyError:
id = kwargs['chat_id'] == callback_query.message.chat_id
message = kwargs['message_id'] == callback_query.message.message_id
reply_markup = kwargs['reply_markup'] == [['1', '2']]
return id and message and reply_markup
monkeypatch.setattr('telegram.Bot.edit_message_reply_markup', test)
assert callback_query.edit_message_reply_markup(reply_markup=[['1', '2']])
assert callback_query.edit_message_reply_markup([['1', '2']])
def test_equality(self):
a = CallbackQuery(self.id, self.from_user, 'chat')
+55 -8
View File
@@ -16,11 +16,13 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
from queue import Queue
import pytest
from telegram import (Update, CallbackQuery, Bot, Message, User, Chat, InlineQuery,
ChosenInlineResult, ShippingQuery, PreCheckoutQuery)
from telegram.ext import CallbackQueryHandler
from telegram.ext import CallbackQueryHandler, CallbackContext, JobQueue
message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text')
@@ -47,7 +49,7 @@ def false_update(request):
@pytest.fixture(scope='function')
def callback_query(bot):
return Update(0, callback_query=CallbackQuery(2, None, None, data='test data'))
return Update(0, callback_query=CallbackQuery(2, User(1, '', False), None, data='test data'))
class TestCallbackQueryHandler(object):
@@ -80,6 +82,22 @@ class TestCallbackQueryHandler(object):
if groupdict is not None:
self.test_flag = groupdict == {'begin': 't', 'end': ' data'}
def callback_context(self, update, context):
self.test_flag = (isinstance(context, CallbackContext)
and isinstance(context.bot, Bot)
and isinstance(update, Update)
and isinstance(context.update_queue, Queue)
and isinstance(context.job_queue, JobQueue)
and isinstance(context.user_data, dict)
and context.chat_data is None
and isinstance(update.callback_query, CallbackQuery))
def callback_context_pattern(self, update, context):
if context.matches[0].groups():
self.test_flag = context.matches[0].groups() == ('t', ' data')
if context.matches[0].groupdict():
self.test_flag = context.matches[0].groupdict() == {'begin': 't', 'end': ' data'}
def test_basic(self, dp, callback_query):
handler = CallbackQueryHandler(self.callback_basic)
dp.add_handler(handler)
@@ -117,14 +135,16 @@ class TestCallbackQueryHandler(object):
assert self.test_flag
def test_pass_user_or_chat_data(self, dp, callback_query):
handler = CallbackQueryHandler(self.callback_data_1, pass_user_data=True)
handler = CallbackQueryHandler(self.callback_data_1,
pass_user_data=True)
dp.add_handler(handler)
dp.process_update(callback_query)
assert self.test_flag
dp.remove_handler(handler)
handler = CallbackQueryHandler(self.callback_data_1, pass_chat_data=True)
handler = CallbackQueryHandler(self.callback_data_1,
pass_chat_data=True)
dp.add_handler(handler)
self.test_flag = False
@@ -132,7 +152,8 @@ class TestCallbackQueryHandler(object):
assert self.test_flag
dp.remove_handler(handler)
handler = CallbackQueryHandler(self.callback_data_2, pass_chat_data=True,
handler = CallbackQueryHandler(self.callback_data_2,
pass_chat_data=True,
pass_user_data=True)
dp.add_handler(handler)
@@ -141,14 +162,16 @@ class TestCallbackQueryHandler(object):
assert self.test_flag
def test_pass_job_or_update_queue(self, dp, callback_query):
handler = CallbackQueryHandler(self.callback_queue_1, pass_job_queue=True)
handler = CallbackQueryHandler(self.callback_queue_1,
pass_job_queue=True)
dp.add_handler(handler)
dp.process_update(callback_query)
assert self.test_flag
dp.remove_handler(handler)
handler = CallbackQueryHandler(self.callback_queue_1, pass_update_queue=True)
handler = CallbackQueryHandler(self.callback_queue_1,
pass_update_queue=True)
dp.add_handler(handler)
self.test_flag = False
@@ -156,7 +179,8 @@ class TestCallbackQueryHandler(object):
assert self.test_flag
dp.remove_handler(handler)
handler = CallbackQueryHandler(self.callback_queue_2, pass_job_queue=True,
handler = CallbackQueryHandler(self.callback_queue_2,
pass_job_queue=True,
pass_update_queue=True)
dp.add_handler(handler)
@@ -167,3 +191,26 @@ class TestCallbackQueryHandler(object):
def test_other_update_types(self, false_update):
handler = CallbackQueryHandler(self.callback_basic)
assert not handler.check_update(false_update)
def test_context(self, cdp, callback_query):
handler = CallbackQueryHandler(self.callback_context)
cdp.add_handler(handler)
cdp.process_update(callback_query)
assert self.test_flag
def test_context_pattern(self, cdp, callback_query):
handler = CallbackQueryHandler(self.callback_context_pattern,
pattern=r'(?P<begin>.*)est(?P<end>.*)')
cdp.add_handler(handler)
cdp.process_update(callback_query)
assert self.test_flag
cdp.remove_handler(handler)
handler = CallbackQueryHandler(self.callback_context_pattern,
pattern=r'(t)est(.*)')
cdp.add_handler(handler)
cdp.process_update(callback_query)
assert self.test_flag
+31 -9
View File
@@ -16,12 +16,13 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
from queue import Queue
import pytest
from telegram import (Update, Chat, Bot, ChosenInlineResult, User, Message, CallbackQuery,
InlineQuery, ShippingQuery, PreCheckoutQuery)
from telegram.ext import ChosenInlineResultHandler
from telegram.ext import ChosenInlineResultHandler, CallbackContext, JobQueue
message = Message(1, User(1, '', False), None, Chat(1, ''), text='Text')
@@ -78,6 +79,16 @@ class TestChosenInlineResultHandler(object):
def callback_queue_2(self, bot, update, job_queue=None, update_queue=None):
self.test_flag = (job_queue is not None) and (update_queue is not None)
def callback_context(self, update, context):
self.test_flag = (isinstance(context, CallbackContext)
and isinstance(context.bot, Bot)
and isinstance(update, Update)
and isinstance(context.update_queue, Queue)
and isinstance(context.job_queue, JobQueue)
and isinstance(context.user_data, dict)
and context.chat_data is None
and isinstance(update.chosen_inline_result, ChosenInlineResult))
def test_basic(self, dp, chosen_inline_result):
handler = ChosenInlineResultHandler(self.callback_basic)
dp.add_handler(handler)
@@ -87,14 +98,16 @@ class TestChosenInlineResultHandler(object):
assert self.test_flag
def test_pass_user_or_chat_data(self, dp, chosen_inline_result):
handler = ChosenInlineResultHandler(self.callback_data_1, pass_user_data=True)
handler = ChosenInlineResultHandler(self.callback_data_1,
pass_user_data=True)
dp.add_handler(handler)
dp.process_update(chosen_inline_result)
assert self.test_flag
dp.remove_handler(handler)
handler = ChosenInlineResultHandler(self.callback_data_1, pass_chat_data=True)
handler = ChosenInlineResultHandler(self.callback_data_1,
pass_chat_data=True)
dp.add_handler(handler)
self.test_flag = False
@@ -102,8 +115,8 @@ class TestChosenInlineResultHandler(object):
assert self.test_flag
dp.remove_handler(handler)
handler = ChosenInlineResultHandler(self.callback_data_2, pass_chat_data=True,
pass_user_data=True)
handler = ChosenInlineResultHandler(self.callback_data_2,
pass_chat_data=True, pass_user_data=True)
dp.add_handler(handler)
self.test_flag = False
@@ -111,14 +124,16 @@ class TestChosenInlineResultHandler(object):
assert self.test_flag
def test_pass_job_or_update_queue(self, dp, chosen_inline_result):
handler = ChosenInlineResultHandler(self.callback_queue_1, pass_job_queue=True)
handler = ChosenInlineResultHandler(self.callback_queue_1,
pass_job_queue=True)
dp.add_handler(handler)
dp.process_update(chosen_inline_result)
assert self.test_flag
dp.remove_handler(handler)
handler = ChosenInlineResultHandler(self.callback_queue_1, pass_update_queue=True)
handler = ChosenInlineResultHandler(self.callback_queue_1,
pass_update_queue=True)
dp.add_handler(handler)
self.test_flag = False
@@ -126,8 +141,8 @@ class TestChosenInlineResultHandler(object):
assert self.test_flag
dp.remove_handler(handler)
handler = ChosenInlineResultHandler(self.callback_queue_2, pass_job_queue=True,
pass_update_queue=True)
handler = ChosenInlineResultHandler(self.callback_queue_2,
pass_job_queue=True, pass_update_queue=True)
dp.add_handler(handler)
self.test_flag = False
@@ -137,3 +152,10 @@ class TestChosenInlineResultHandler(object):
def test_other_update_types(self, false_update):
handler = ChosenInlineResultHandler(self.callback_basic)
assert not handler.check_update(false_update)
def test_context(self, cdp, chosen_inline_result):
handler = ChosenInlineResultHandler(self.callback_context)
cdp.add_handler(handler)
cdp.process_update(chosen_inline_result)
assert self.test_flag
+462 -58
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2018
@@ -16,12 +17,16 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import re
from queue import Queue
import pytest
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram import (Message, Update, Chat, Bot, User, CallbackQuery, InlineQuery,
ChosenInlineResult, ShippingQuery, PreCheckoutQuery)
from telegram.ext import CommandHandler, Filters, BaseFilter
ChosenInlineResult, ShippingQuery, PreCheckoutQuery, MessageEntity)
from telegram.ext import CommandHandler, Filters, BaseFilter, CallbackContext, JobQueue, \
PrefixHandler
message = Message(1, User(1, '', False), None, Chat(1, ''), text='test')
@@ -48,11 +53,20 @@ def false_update(request):
@pytest.fixture(scope='function')
def message(bot):
return Message(1, None, None, None, bot=bot)
return Message(message_id=1,
from_user=User(id=1, first_name='', is_bot=False),
date=None,
chat=Chat(id=1, type=''),
message='/test',
bot=bot,
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0,
length=len('/test'))])
class TestCommandHandler(object):
test_flag = False
SRE_TYPE = type(re.match("", ""))
@pytest.fixture(autouse=True)
def reset(self):
@@ -83,64 +97,139 @@ class TestCommandHandler(object):
else:
self.test_flag = args == ['one', 'two']
def callback_context(self, update, context):
self.test_flag = (isinstance(context, CallbackContext)
and isinstance(context.bot, Bot)
and isinstance(update, Update)
and isinstance(context.update_queue, Queue)
and isinstance(context.job_queue, JobQueue)
and isinstance(context.user_data, dict)
and isinstance(context.chat_data, dict)
and isinstance(update.message, Message))
def callback_context_args(self, update, context):
self.test_flag = context.args == ['one', 'two']
def callback_context_regex1(self, update, context):
if context.matches:
types = all([type(res) == self.SRE_TYPE for res in context.matches])
num = len(context.matches) == 1
self.test_flag = types and num
def callback_context_regex2(self, update, context):
if context.matches:
types = all([type(res) == self.SRE_TYPE for res in context.matches])
num = len(context.matches) == 2
self.test_flag = types and num
def test_basic(self, dp, message):
handler = CommandHandler('test', self.callback_basic)
dp.add_handler(handler)
message.text = '/test'
assert handler.check_update(Update(0, message))
dp.process_update(Update(0, message))
assert self.test_flag
message.text = '/nottest'
assert not handler.check_update(Update(0, message))
check = handler.check_update(Update(0, message))
assert check is None or check is False
message.text = 'test'
assert not handler.check_update(Update(0, message))
check = handler.check_update(Update(0, message))
assert check is None or check is False
message.text = 'not /test at start'
assert not handler.check_update(Update(0, message))
check = handler.check_update(Update(0, message))
assert check is None or check is False
message.entities = []
message.text = '/test'
check = handler.check_update(Update(0, message))
assert check is None or check is False
@pytest.mark.parametrize('command',
['way_too_longcommand1234567yes_way_toooooooLong', 'ïñválídletters',
'invalid #&* chars'],
ids=['too long', 'invalid letter', 'invalid characters'])
def test_invalid_commands(self, command):
with pytest.raises(ValueError, match='not a valid bot command'):
CommandHandler(command, self.callback_basic)
def test_command_list(self, message):
handler = CommandHandler(['test', 'start'], self.callback_basic)
handler = CommandHandler(['test', 'star'], self.callback_basic)
message.text = '/test'
assert handler.check_update(Update(0, message))
check = handler.check_update(Update(0, message))
message.text = '/start'
assert handler.check_update(Update(0, message))
message.text = '/star'
check = handler.check_update(Update(0, message))
message.text = '/stop'
assert not handler.check_update(Update(0, message))
check = handler.check_update(Update(0, message))
assert check is None or check is False
def test_edited(self, message):
handler = CommandHandler('test', self.callback_basic, allow_edited=False)
def test_deprecation_warning(self):
with pytest.warns(TelegramDeprecationWarning, match='See https://git.io/fxJuV'):
CommandHandler('test', self.callback_basic, allow_edited=True)
def test_no_edited(self, message):
handler = CommandHandler('test', self.callback_basic)
message.text = '/test'
assert handler.check_update(Update(0, message))
assert not handler.check_update(Update(0, edited_message=message))
handler.allow_edited = True
assert handler.check_update(Update(0, message))
assert handler.check_update(Update(0, edited_message=message))
check = handler.check_update(Update(0, message))
assert check is not None and check is not False
check = handler.check_update(Update(0, edited_message=message))
assert check is not None and check is not False
handler = CommandHandler('test', self.callback_basic,
filters=~Filters.update.edited_message)
check = handler.check_update(Update(0, message))
assert check is not None and check is not False
check = handler.check_update(Update(0, edited_message=message))
assert check is None or check is False
def test_edited_deprecated(self, message):
handler = CommandHandler('test', self.callback_basic,
allow_edited=False)
message.text = '/test'
check = handler.check_update(Update(0, message))
assert check is not None and check is not False
check = handler.check_update(Update(0, edited_message=message))
assert check is None or check is False
handler = CommandHandler('test', self.callback_basic,
allow_edited=True)
check = handler.check_update(Update(0, message))
assert check is not None and check is not False
check = handler.check_update(Update(0, edited_message=message))
assert check is not None and check is not False
def test_directed_commands(self, message):
handler = CommandHandler('test', self.callback_basic)
message.text = '/test@{}'.format(message.bot.username)
assert handler.check_update(Update(0, message))
message.entities[0].length = len(message.text)
check = handler.check_update(Update(0, message))
assert check is not None and check is not False
message.text = '/test@otherbot'
assert not handler.check_update(Update(0, message))
check = handler.check_update(Update(0, message))
assert check is None or check is False
def test_with_filter(self, message):
handler = CommandHandler('test', self.callback_basic, Filters.group)
message.chat = Chat(-23, 'group')
message.text = '/test'
assert handler.check_update(Update(0, message))
check = handler.check_update(Update(0, message))
assert check is not None and check is not False
message.chat = Chat(23, 'private')
assert not handler.check_update(Update(0, message))
check = handler.check_update(Update(0, message))
assert check is None or check is False
def test_pass_args(self, dp, message):
handler = CommandHandler('test', self.ch_callback_args, pass_args=True)
@@ -152,11 +241,7 @@ class TestCommandHandler(object):
self.test_flag = False
message.text = '/test@{}'.format(message.bot.username)
dp.process_update(Update(0, message=message))
assert self.test_flag
self.test_flag = False
message.text = '/test one two'
message.entities[0].length = len(message.text)
dp.process_update(Update(0, message=message))
assert self.test_flag
@@ -165,36 +250,26 @@ class TestCommandHandler(object):
dp.process_update(Update(0, message=message))
assert self.test_flag
self.test_flag = False
message.text = '/test one two'
message.entities[0].length = len('/test')
dp.process_update(Update(0, message=message))
assert self.test_flag
def test_newline(self, dp, message):
handler = CommandHandler('test', self.callback_basic)
dp.add_handler(handler)
message.text = '/test\nfoobar'
assert handler.check_update(Update(0, message))
check = handler.check_update(Update(0, message))
assert check is not None and check is not False
dp.process_update(Update(0, message))
assert self.test_flag
def test_single_char(self, dp, message):
# Regression test for https://github.com/python-telegram-bot/python-telegram-bot/issues/871
handler = CommandHandler('test', self.callback_basic)
dp.add_handler(handler)
message.text = 'a'
assert not handler.check_update(Update(0, message))
def test_single_slash(self, dp, message):
# Regression test for https://github.com/python-telegram-bot/python-telegram-bot/issues/871
handler = CommandHandler('test', self.callback_basic)
dp.add_handler(handler)
message.text = '/'
assert not handler.check_update(Update(0, message))
message.text = '/ test'
assert not handler.check_update(Update(0, message))
def test_pass_user_or_chat_data(self, dp, message):
handler = CommandHandler('test', self.callback_data_1, pass_user_data=True)
handler = CommandHandler('test', self.callback_data_1,
pass_user_data=True)
dp.add_handler(handler)
message.text = '/test'
@@ -202,7 +277,8 @@ class TestCommandHandler(object):
assert self.test_flag
dp.remove_handler(handler)
handler = CommandHandler('test', self.callback_data_1, pass_chat_data=True)
handler = CommandHandler('test', self.callback_data_1,
pass_chat_data=True)
dp.add_handler(handler)
self.test_flag = False
@@ -210,7 +286,8 @@ class TestCommandHandler(object):
assert self.test_flag
dp.remove_handler(handler)
handler = CommandHandler('test', self.callback_data_2, pass_chat_data=True,
handler = CommandHandler('test', self.callback_data_2,
pass_chat_data=True,
pass_user_data=True)
dp.add_handler(handler)
@@ -219,7 +296,8 @@ class TestCommandHandler(object):
assert self.test_flag
def test_pass_job_or_update_queue(self, dp, message):
handler = CommandHandler('test', self.callback_queue_1, pass_job_queue=True)
handler = CommandHandler('test', self.callback_queue_1,
pass_job_queue=True)
dp.add_handler(handler)
message.text = '/test'
@@ -227,7 +305,8 @@ class TestCommandHandler(object):
assert self.test_flag
dp.remove_handler(handler)
handler = CommandHandler('test', self.callback_queue_1, pass_update_queue=True)
handler = CommandHandler('test', self.callback_queue_1,
pass_update_queue=True)
dp.add_handler(handler)
self.test_flag = False
@@ -235,7 +314,8 @@ class TestCommandHandler(object):
assert self.test_flag
dp.remove_handler(handler)
handler = CommandHandler('test', self.callback_queue_2, pass_job_queue=True,
handler = CommandHandler('test', self.callback_queue_2,
pass_job_queue=True,
pass_update_queue=True)
dp.add_handler(handler)
@@ -245,7 +325,8 @@ class TestCommandHandler(object):
def test_other_update_types(self, false_update):
handler = CommandHandler('test', self.callback_basic)
assert not handler.check_update(false_update)
check = handler.check_update(false_update)
assert check is None or check is False
def test_filters_for_wrong_command(self, message):
"""Filters should not be executed if the command does not match the handler"""
@@ -259,9 +340,332 @@ class TestCommandHandler(object):
test_filter = TestFilter()
handler = CommandHandler('foo', self.callback_basic, filters=test_filter)
message.text = '/bar'
handler = CommandHandler('test', self.callback_basic,
filters=test_filter)
message.text = '/star'
handler.check_update(Update(0, message=message))
check = handler.check_update(Update(0, message=message))
assert check is None or check is False
assert not test_filter.tested
def test_context(self, cdp, message):
handler = CommandHandler('test', self.callback_context)
cdp.add_handler(handler)
message.text = '/test'
cdp.process_update(Update(0, message))
assert self.test_flag
def test_context_args(self, cdp, message):
handler = CommandHandler('test', self.callback_context_args)
cdp.add_handler(handler)
message.text = '/test'
cdp.process_update(Update(0, message))
assert not self.test_flag
message.text = '/test one two'
cdp.process_update(Update(0, message))
assert self.test_flag
def test_context_regex(self, cdp, message):
handler = CommandHandler('test', self.callback_context_regex1, Filters.regex('one two'))
cdp.add_handler(handler)
message.text = '/test'
cdp.process_update(Update(0, message))
assert not self.test_flag
message.text += ' one two'
cdp.process_update(Update(0, message))
assert self.test_flag
def test_context_multiple_regex(self, cdp, message):
handler = CommandHandler('test', self.callback_context_regex2,
Filters.regex('one') & Filters.regex('two'))
cdp.add_handler(handler)
message.text = '/test'
cdp.process_update(Update(0, message))
assert not self.test_flag
message.text += ' one two'
cdp.process_update(Update(0, message))
assert self.test_flag
par = ['!help', '!test', '#help', '#test', 'mytrig-help', 'mytrig-test']
@pytest.fixture(scope='function', params=par)
def prefixmessage(bot, request):
return Message(message_id=1,
from_user=User(id=1, first_name='', is_bot=False),
date=None,
chat=Chat(id=1, type=''),
text=request.param,
bot=bot)
class TestPrefixHandler(object):
test_flag = False
SRE_TYPE = type(re.match("", ""))
@pytest.fixture(autouse=True)
def reset(self):
self.test_flag = False
def callback_basic(self, bot, update):
test_bot = isinstance(bot, Bot)
test_update = isinstance(update, Update)
self.test_flag = test_bot and test_update
def callback_data_1(self, bot, update, user_data=None, chat_data=None):
self.test_flag = (user_data is not None) or (chat_data is not None)
def callback_data_2(self, bot, update, user_data=None, chat_data=None):
self.test_flag = (user_data is not None) and (chat_data is not None)
def callback_queue_1(self, bot, update, job_queue=None, update_queue=None):
self.test_flag = (job_queue is not None) or (update_queue is not None)
def callback_queue_2(self, bot, update, job_queue=None, update_queue=None):
self.test_flag = (job_queue is not None) and (update_queue is not None)
def ch_callback_args(self, bot, update, args):
if update.message.text in par:
self.test_flag = len(args) == 0
else:
self.test_flag = args == ['one', 'two']
def callback_context(self, update, context):
self.test_flag = (isinstance(context, CallbackContext)
and isinstance(context.bot, Bot)
and isinstance(update, Update)
and isinstance(context.update_queue, Queue)
and isinstance(context.job_queue, JobQueue)
and isinstance(context.user_data, dict)
and isinstance(context.chat_data, dict)
and isinstance(update.message, Message))
def callback_context_args(self, update, context):
self.test_flag = context.args == ['one', 'two']
def callback_context_regex1(self, update, context):
if context.matches:
types = all([type(res) == self.SRE_TYPE for res in context.matches])
num = len(context.matches) == 1
self.test_flag = types and num
def callback_context_regex2(self, update, context):
if context.matches:
types = all([type(res) == self.SRE_TYPE for res in context.matches])
num = len(context.matches) == 2
self.test_flag = types and num
def test_basic(self, dp, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic)
dp.add_handler(handler)
dp.process_update(Update(0, prefixmessage))
assert self.test_flag
prefixmessage.text = 'test'
check = handler.check_update(Update(0, prefixmessage))
assert check is None or check is False
prefixmessage.text = '#nocom'
check = handler.check_update(Update(0, prefixmessage))
assert check is None or check is False
message.text = 'not !test at start'
check = handler.check_update(Update(0, message))
assert check is None or check is False
def test_single_prefix_single_command(self, prefixmessage):
handler = PrefixHandler('!', 'test', self.callback_basic)
check = handler.check_update(Update(0, prefixmessage))
if prefixmessage.text in ['!test']:
assert check is not None and check is not False
else:
assert check is None or check is False
def test_single_prefix_multi_command(self, prefixmessage):
handler = PrefixHandler('!', ['test', 'help'], self.callback_basic)
check = handler.check_update(Update(0, prefixmessage))
if prefixmessage.text in ['!test', '!help']:
assert check is not None and check is not False
else:
assert check is None or check is False
def test_multi_prefix_single_command(self, prefixmessage):
handler = PrefixHandler(['!', '#'], 'test', self.callback_basic)
check = handler.check_update(Update(0, prefixmessage))
if prefixmessage.text in ['!test', '#test']:
assert check is not None and check is not False
else:
assert check is None or check is False
def test_no_edited(self, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic)
check = handler.check_update(Update(0, prefixmessage))
assert check is not None and check is not False
check = handler.check_update(Update(0, edited_message=prefixmessage))
assert check is not None and check is not False
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic,
filters=~Filters.update.edited_message)
check = handler.check_update(Update(0, prefixmessage))
assert check is not None and check is not False
check = handler.check_update(Update(0, edited_message=prefixmessage))
assert check is None or check is False
def test_with_filter(self, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic,
filters=Filters.group)
prefixmessage.chat = Chat(-23, 'group')
check = handler.check_update(Update(0, prefixmessage))
assert check is not None and check is not False
prefixmessage.chat = Chat(23, 'private')
check = handler.check_update(Update(0, prefixmessage))
assert check is None or check is False
def test_pass_args(self, dp, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.ch_callback_args,
pass_args=True)
dp.add_handler(handler)
dp.process_update(Update(0, message=prefixmessage))
assert self.test_flag
self.test_flag = False
prefixmessage.text += ' one two'
dp.process_update(Update(0, message=prefixmessage))
assert self.test_flag
def test_pass_user_or_chat_data(self, dp, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_data_1,
pass_user_data=True)
dp.add_handler(handler)
dp.process_update(Update(0, message=prefixmessage))
assert self.test_flag
dp.remove_handler(handler)
self.test_flag = False
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_data_1,
pass_chat_data=True)
dp.add_handler(handler)
dp.process_update(Update(0, message=prefixmessage))
assert self.test_flag
dp.remove_handler(handler)
self.test_flag = False
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_data_2,
pass_chat_data=True, pass_user_data=True)
dp.add_handler(handler)
dp.process_update(Update(0, message=prefixmessage))
assert self.test_flag
def test_pass_job_or_update_queue(self, dp, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_queue_1,
pass_job_queue=True)
dp.add_handler(handler)
dp.process_update(Update(0, message=prefixmessage))
assert self.test_flag
dp.remove_handler(handler)
self.test_flag = False
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_queue_1,
pass_update_queue=True)
dp.add_handler(handler)
dp.process_update(Update(0, message=prefixmessage))
assert self.test_flag
dp.remove_handler(handler)
self.test_flag = False
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_queue_2,
pass_job_queue=True, pass_update_queue=True)
dp.add_handler(handler)
dp.process_update(Update(0, message=prefixmessage))
assert self.test_flag
def test_other_update_types(self, false_update):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic)
check = handler.check_update(false_update)
assert check is None or check is False
def test_filters_for_wrong_command(self, prefixmessage):
"""Filters should not be executed if the command does not match the handler"""
class TestFilter(BaseFilter):
def __init__(self):
self.tested = False
def filter(self, message):
self.tested = True
test_filter = TestFilter()
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_basic,
filters=test_filter)
prefixmessage.text = '/star'
check = handler.check_update(Update(0, message=prefixmessage))
assert check is None or check is False
assert not test_filter.tested
def test_context(self, cdp, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'], self.callback_context)
cdp.add_handler(handler)
cdp.process_update(Update(0, prefixmessage))
assert self.test_flag
def test_context_args(self, cdp, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'],
self.callback_context_args)
cdp.add_handler(handler)
cdp.process_update(Update(0, prefixmessage))
assert not self.test_flag
prefixmessage.text += ' one two'
cdp.process_update(Update(0, prefixmessage))
assert self.test_flag
def test_context_regex(self, cdp, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'],
self.callback_context_regex1, Filters.regex('one two'))
cdp.add_handler(handler)
cdp.process_update(Update(0, prefixmessage))
assert not self.test_flag
prefixmessage.text += ' one two'
cdp.process_update(Update(0, prefixmessage))
assert self.test_flag
def test_context_multiple_regex(self, cdp, prefixmessage):
handler = PrefixHandler(['!', '#', 'mytrig-'], ['help', 'test'],
self.callback_context_regex2,
Filters.regex('one') & Filters.regex('two'))
cdp.add_handler(handler)
cdp.process_update(Update(0, prefixmessage))
assert not self.test_flag
prefixmessage.text += ' one two'
cdp.process_update(Update(0, prefixmessage))
assert self.test_flag
+4 -3
View File
@@ -29,8 +29,8 @@ class TestConstants(object):
def test_max_message_length(self, bot, chat_id):
bot.send_message(chat_id=chat_id, text='a' * constants.MAX_MESSAGE_LENGTH)
with pytest.raises(BadRequest, message='MAX_MESSAGE_LENGTH is no longer valid',
match='too long'):
with pytest.raises(BadRequest, match='Message is too long',
message='MAX_MESSAGE_LENGTH is no longer valid'):
bot.send_message(chat_id=chat_id, text='a' * (constants.MAX_MESSAGE_LENGTH + 1))
@flaky(3, 1)
@@ -42,6 +42,7 @@ class TestConstants(object):
assert good_msg.caption == good_caption
bad_caption = good_caption + 'Z'
with pytest.raises(BadRequest, message="Media_caption_too_long"):
with pytest.raises(BadRequest, match="Media_caption_too_long",
message='MAX_CAPTION_LENGTH is no longer valid'):
with open('tests/data/telegram.png', 'rb') as f:
bot.send_photo(photo=f, caption=bad_caption, chat_id=chat_id)
+226 -16
View File
@@ -22,8 +22,9 @@ from time import sleep
import pytest
from telegram import (CallbackQuery, Chat, ChosenInlineResult, InlineQuery, Message,
PreCheckoutQuery, ShippingQuery, Update, User)
from telegram.ext import (ConversationHandler, CommandHandler, CallbackQueryHandler)
PreCheckoutQuery, ShippingQuery, Update, User, MessageEntity)
from telegram.ext import (ConversationHandler, CommandHandler, CallbackQueryHandler,
MessageHandler, Filters, InlineQueryHandler)
@pytest.fixture(scope='class')
@@ -52,7 +53,8 @@ class TestConversationHandler(object):
self.current_state = dict()
self.entry_points = [CommandHandler('start', self.start)]
self.states = {
self.THIRSTY: [CommandHandler('brew', self.brew), CommandHandler('wait', self.start)],
self.THIRSTY: [CommandHandler('brew', self.brew),
CommandHandler('wait', self.start)],
self.BREWING: [CommandHandler('pourCoffee', self.drink)],
self.DRINKING:
[CommandHandler('startCoding', self.code),
@@ -62,9 +64,10 @@ class TestConversationHandler(object):
CommandHandler('keepCoding', self.code),
CommandHandler('gettingThirsty', self.start),
CommandHandler('drinkMore', self.drink)
],
]
}
self.fallbacks = [CommandHandler('eat', self.start)]
self.is_timeout = False
# State handlers
def _set_state(self, update, state):
@@ -81,6 +84,9 @@ class TestConversationHandler(object):
def start_end(self, bot, update):
return self._set_state(update, self.END)
def start_none(self, bot, update):
return self._set_state(update, None)
def brew(self, bot, update):
return self._set_state(update, self.BREWING)
@@ -90,34 +96,53 @@ class TestConversationHandler(object):
def code(self, bot, update):
return self._set_state(update, self.CODING)
def passout(self, bot, update):
assert update.message.text == '/brew'
self.is_timeout = True
def passout2(self, bot, update):
self.is_timeout = True
# Tests
def test_per_all_false(self):
with pytest.raises(ValueError, match="can't all be 'False'"):
ConversationHandler(self.entry_points, self.states, self.fallbacks,
per_chat=False, per_user=False, per_message=False)
def test_name_and_persistent(self, dp):
with pytest.raises(ValueError, match="when handler is unnamed"):
dp.add_handler(ConversationHandler([], {}, [], persistent=True))
c = ConversationHandler([], {}, [], name="handler", persistent=True)
assert c.name == "handler"
def test_conversation_handler(self, dp, bot, user1, user2):
handler = ConversationHandler(entry_points=self.entry_points, states=self.states,
fallbacks=self.fallbacks)
dp.add_handler(handler)
# User one, starts the state machine.
message = Message(0, user1, None, self.group, text='/start', bot=bot)
message = Message(0, user1, None, self.group, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0, length=len('/start'))],
bot=bot)
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.THIRSTY
# The user is thirsty and wants to brew coffee.
message.text = '/brew'
message.entities[0].length = len('/brew')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.BREWING
# Lets see if an invalid command makes sure, no state is changed.
message.text = '/nothing'
message.entities[0].length = len('/nothing')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.BREWING
# Lets see if the state machine still works by pouring coffee.
message.text = '/pourCoffee'
message.entities[0].length = len('/pourCoffee')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.DRINKING
@@ -133,13 +158,20 @@ class TestConversationHandler(object):
fallbacks=self.fallbacks)
dp.add_handler(handler)
message = Message(0, user1, None, self.group, text='/start', bot=bot)
message = Message(0, user1, None, self.group, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0, length=len('/start'))],
bot=bot)
dp.process_update(Update(update_id=0, message=message))
message.text = '/brew'
message.entities[0].length = len('/brew')
dp.process_update(Update(update_id=0, message=message))
message.text = '/pourCoffee'
message.entities[0].length = len('/pourCoffee')
dp.process_update(Update(update_id=0, message=message))
message.text = '/end'
message.entities[0].length = len('/end')
caplog.clear()
with caplog.at_level(logging.ERROR):
dp.process_update(Update(update_id=0, message=message))
assert len(caplog.records) == 0
@@ -153,23 +185,29 @@ class TestConversationHandler(object):
dp.add_handler(handler)
# first check if fallback will not trigger start when not started
message = Message(0, user1, None, self.group, text='/eat', bot=bot)
message = Message(0, user1, None, self.group, text='/eat',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0, length=len('/eat'))],
bot=bot)
dp.process_update(Update(update_id=0, message=message))
with pytest.raises(KeyError):
self.current_state[user1.id]
# User starts the state machine.
message.text = '/start'
message.entities[0].length = len('/start')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.THIRSTY
# The user is thirsty and wants to brew coffee.
message.text = '/brew'
message.entities[0].length = len('/brew')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.BREWING
# Now a fallback command is issued
message.text = '/eat'
message.entities[0].length = len('/eat')
dp.process_update(Update(update_id=0, message=message))
assert self.current_state[user1.id] == self.THIRSTY
@@ -182,17 +220,22 @@ class TestConversationHandler(object):
dp.add_handler(handler)
# User one, starts the state machine.
message = Message(0, user1, None, self.group, text='/start', bot=bot)
message = Message(0, user1, None, self.group, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0, length=len('/start'))],
bot=bot)
dp.process_update(Update(update_id=0, message=message))
# The user is thirsty and wants to brew coffee.
message.text = '/brew'
message.entities[0].length = len('/brew')
dp.process_update(Update(update_id=0, message=message))
# Let's now verify that for another user, who did not start yet,
# the state will be changed because they are in the same group.
message.from_user = user2
message.text = '/pourCoffee'
message.entities[0].length = len('/pourCoffee')
dp.process_update(Update(update_id=0, message=message))
assert handler.conversations[(self.group.id,)] == self.DRINKING
@@ -206,17 +249,22 @@ class TestConversationHandler(object):
dp.add_handler(handler)
# User one, starts the state machine.
message = Message(0, user1, None, self.group, text='/start', bot=bot)
message = Message(0, user1, None, self.group, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0, length=len('/start'))],
bot=bot)
dp.process_update(Update(update_id=0, message=message))
# The user is thirsty and wants to brew coffee.
message.text = '/brew'
message.entities[0].length = len('/brew')
dp.process_update(Update(update_id=0, message=message))
# Let's now verify that for the same user in a different group, the state will still be
# updated
message.chat = self.second_group
message.text = '/pourCoffee'
message.entities[0].length = len('/pourCoffee')
dp.process_update(Update(update_id=0, message=message))
assert handler.conversations[(user1.id,)] == self.DRINKING
@@ -260,11 +308,15 @@ class TestConversationHandler(object):
def test_end_on_first_message(self, dp, bot, user1):
handler = ConversationHandler(
entry_points=[CommandHandler('start', self.start_end)], states={}, fallbacks=[])
entry_points=[CommandHandler('start', self.start_end)], states={},
fallbacks=[])
dp.add_handler(handler)
# User starts the state machine and immediately ends it.
message = Message(0, user1, None, self.group, text='/start', bot=bot)
message = Message(0, user1, None, self.group, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0, length=len('/start'))],
bot=bot)
dp.process_update(Update(update_id=0, message=message))
assert len(handler.conversations) == 0
@@ -272,12 +324,51 @@ class TestConversationHandler(object):
start_end_async = (lambda bot, update: dp.run_async(self.start_end, bot, update))
handler = ConversationHandler(
entry_points=[CommandHandler('start', start_end_async)], states={}, fallbacks=[])
entry_points=[CommandHandler('start', start_end_async)], states={},
fallbacks=[])
dp.add_handler(handler)
# User starts the state machine with an async function that immediately ends the
# conversation. Async results are resolved when the users state is queried next time.
message = Message(0, user1, None, self.group, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0, length=len('/start'))],
bot=bot)
dp.update_queue.put(Update(update_id=0, message=message))
sleep(.1)
# Assert that the Promise has been accepted as the new state
assert len(handler.conversations) == 1
message.text = 'resolve promise pls'
message.entities[0].length = len('resolve promise pls')
dp.update_queue.put(Update(update_id=0, message=message))
sleep(.1)
# Assert that the Promise has been resolved and the conversation ended.
assert len(handler.conversations) == 0
def test_none_on_first_message(self, dp, bot, user1):
handler = ConversationHandler(
entry_points=[CommandHandler('start', self.start_none)], states={}, fallbacks=[])
dp.add_handler(handler)
# User starts the state machine and a callback function returns None
message = Message(0, user1, None, self.group, text='/start', bot=bot)
dp.process_update(Update(update_id=0, message=message))
assert len(handler.conversations) == 0
def test_none_on_first_message_async(self, dp, bot, user1):
start_none_async = (lambda bot, update: dp.run_async(self.start_none, bot, update))
handler = ConversationHandler(
entry_points=[CommandHandler('start', start_none_async)], states={}, fallbacks=[])
dp.add_handler(handler)
# User starts the state machine with an async function that returns None
# Async results are resolved when the users state is queried next time.
message = Message(0, user1, None, self.group, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0, length=len('/start'))],
bot=bot)
dp.update_queue.put(Update(update_id=0, message=message))
sleep(.1)
# Assert that the Promise has been accepted as the new state
@@ -291,7 +382,8 @@ class TestConversationHandler(object):
def test_per_chat_message_without_chat(self, bot, user1):
handler = ConversationHandler(
entry_points=[CommandHandler('start', self.start_end)], states={}, fallbacks=[])
entry_points=[CommandHandler('start', self.start_end)], states={},
fallbacks=[])
cbq = CallbackQuery(0, user1, None, None, bot=bot)
update = Update(0, callback_query=cbq)
assert not handler.check_update(update)
@@ -325,7 +417,10 @@ class TestConversationHandler(object):
dp.add_handler(handler)
# Start state machine, then reach timeout
message = Message(0, user1, None, self.group, text='/start', bot=bot)
message = Message(0, user1, None, self.group, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0, length=len('/start'))],
bot=bot)
dp.process_update(Update(update_id=0, message=message))
assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY
sleep(0.5)
@@ -336,6 +431,7 @@ class TestConversationHandler(object):
dp.process_update(Update(update_id=1, message=message))
assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY
message.text = '/brew'
message.entities[0].length = len('/brew')
dp.job_queue.tick()
dp.process_update(Update(update_id=2, message=message))
assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING
@@ -355,19 +451,24 @@ class TestConversationHandler(object):
# t=.6 /pourCoffee (timeout=1.1)
# t=.75 second timeout
# t=1.1 actual timeout
message = Message(0, user1, None, self.group, text='/start', bot=bot)
message = Message(0, user1, None, self.group, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0, length=len('/start'))],
bot=bot)
dp.process_update(Update(update_id=0, message=message))
assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY
sleep(0.25) # t=.25
dp.job_queue.tick()
assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY
message.text = '/brew'
message.entities[0].length = len('/brew')
dp.process_update(Update(update_id=0, message=message))
assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING
sleep(0.35) # t=.6
dp.job_queue.tick()
assert handler.conversations.get((self.group.id, user1.id)) == self.BREWING
message.text = '/pourCoffee'
message.entities[0].length = len('/pourCoffee')
dp.process_update(Update(update_id=0, message=message))
assert handler.conversations.get((self.group.id, user1.id)) == self.DRINKING
sleep(.4) # t=1
@@ -383,15 +484,21 @@ class TestConversationHandler(object):
dp.add_handler(handler)
# Start state machine, do something as second user, then reach timeout
message = Message(0, user1, None, self.group, text='/start', bot=bot)
message = Message(0, user1, None, self.group, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0,
length=len('/start'))],
bot=bot)
dp.process_update(Update(update_id=0, message=message))
assert handler.conversations.get((self.group.id, user1.id)) == self.THIRSTY
message.text = '/brew'
message.entities[0].length = len('/brew')
message.entities[0].length = len('/brew')
message.from_user = user2
dp.job_queue.tick()
dp.process_update(Update(update_id=0, message=message))
assert handler.conversations.get((self.group.id, user2.id)) is None
message.text = '/start'
message.entities[0].length = len('/start')
dp.job_queue.tick()
dp.process_update(Update(update_id=0, message=message))
assert handler.conversations.get((self.group.id, user2.id)) == self.THIRSTY
@@ -399,3 +506,106 @@ class TestConversationHandler(object):
dp.job_queue.tick()
assert handler.conversations.get((self.group.id, user1.id)) is None
assert handler.conversations.get((self.group.id, user2.id)) is None
def test_conversation_handler_timeout_state(self, dp, bot, user1):
states = self.states
states.update({ConversationHandler.TIMEOUT: [
CommandHandler('brew', self.passout),
MessageHandler(~Filters.regex('oding'), self.passout2)
]})
handler = ConversationHandler(entry_points=self.entry_points, states=states,
fallbacks=self.fallbacks, conversation_timeout=0.5)
dp.add_handler(handler)
# CommandHandler timeout
message = Message(0, user1, None, self.group, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0,
length=len('/start'))],
bot=bot)
dp.process_update(Update(update_id=0, message=message))
message.text = '/brew'
message.entities[0].length = len('/brew')
dp.process_update(Update(update_id=0, message=message))
sleep(0.5)
dp.job_queue.tick()
assert handler.conversations.get((self.group.id, user1.id)) is None
assert self.is_timeout
# MessageHandler timeout
self.is_timeout = False
message.text = '/start'
message.entities[0].length = len('/start')
dp.process_update(Update(update_id=1, message=message))
sleep(0.5)
dp.job_queue.tick()
assert handler.conversations.get((self.group.id, user1.id)) is None
assert self.is_timeout
# Timeout but no valid handler
self.is_timeout = False
dp.process_update(Update(update_id=0, message=message))
message.text = '/brew'
message.entities[0].length = len('/brew')
dp.process_update(Update(update_id=0, message=message))
message.text = '/startCoding'
message.entities[0].length = len('/startCoding')
dp.process_update(Update(update_id=0, message=message))
sleep(0.5)
dp.job_queue.tick()
assert handler.conversations.get((self.group.id, user1.id)) is None
assert not self.is_timeout
def test_per_message_warning_is_only_shown_once(self, recwarn):
ConversationHandler(
entry_points=self.entry_points,
states={
self.THIRSTY: [CommandHandler('pourCoffee', self.drink)],
self.BREWING: [CommandHandler('startCoding', self.code)]
},
fallbacks=self.fallbacks,
per_message=True
)
assert len(recwarn) == 1
assert str(recwarn[0].message) == (
"If 'per_message=True', all entry points and state handlers"
" must be 'CallbackQueryHandler', since no other handlers"
" have a message context."
)
def test_per_message_false_warning_is_only_shown_once(self, recwarn):
ConversationHandler(
entry_points=self.entry_points,
states={
self.THIRSTY: [CallbackQueryHandler(self.drink)],
self.BREWING: [CallbackQueryHandler(self.code)],
},
fallbacks=self.fallbacks,
per_message=False
)
assert len(recwarn) == 1
assert str(recwarn[0].message) == (
"If 'per_message=False', 'CallbackQueryHandler' will not be "
"tracked for every message."
)
def test_warnings_per_chat_is_only_shown_once(self, recwarn):
def hello(bot, update):
return self.BREWING
def bye(bot, update):
return ConversationHandler.END
ConversationHandler(
entry_points=self.entry_points,
states={
self.THIRSTY: [InlineQueryHandler(hello)],
self.BREWING: [InlineQueryHandler(bye)]
},
fallbacks=self.fallbacks,
per_chat=True
)
assert len(recwarn) == 1
assert str(recwarn[0].message) == (
"If 'per_chat=True', 'InlineQueryHandler' can not be used,"
" since inline queries have no chat context."
)
+80 -7
View File
@@ -16,15 +16,17 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import sys
from queue import Queue
from threading import current_thread
from time import sleep
import pytest
from telegram import TelegramError, Message, User, Chat, Update
from telegram.ext import MessageHandler, Filters, CommandHandler
from telegram import TelegramError, Message, User, Chat, Update, Bot, MessageEntity
from telegram.ext import MessageHandler, Filters, CommandHandler, CallbackContext, JobQueue
from telegram.ext.dispatcher import run_async, Dispatcher, DispatcherHandlerStop
from telegram.utils.deprecate import TelegramDeprecationWarning
from tests.conftest import create_dp
@@ -40,7 +42,10 @@ class TestDispatcher(object):
received = None
count = 0
@pytest.fixture(autouse=True)
@pytest.fixture(autouse=True, name='reset')
def reset_fixture(self):
self.reset()
def reset(self):
self.received = None
self.count = 0
@@ -67,6 +72,34 @@ class TestDispatcher(object):
if update_queue is not None:
self.received = update.message
def callback_context(self, update, context):
if (isinstance(context, CallbackContext)
and isinstance(context.bot, Bot)
and isinstance(context.update_queue, Queue)
and isinstance(context.job_queue, JobQueue)
and isinstance(context.error, TelegramError)):
self.received = context.error.message
def test_one_context_per_update(self, cdp):
def one(update, context):
if update.message.text == 'test':
context.my_flag = True
def two(update, context):
if update.message.text == 'test':
if not hasattr(context, 'my_flag'):
pytest.fail()
else:
if hasattr(context, 'my_flag'):
pytest.fail()
cdp.add_handler(MessageHandler(Filters.regex('test'), one), group=1)
cdp.add_handler(MessageHandler(None, two), group=2)
u = Update(1, Message(1, None, None, None, text='test'))
cdp.process_update(u)
u.message.text = 'something'
cdp.process_update(u)
def test_error_handler(self, dp):
dp.add_error_handler(self.error_handler)
error = TelegramError('Unauthorized.')
@@ -82,6 +115,16 @@ class TestDispatcher(object):
sleep(.1)
assert self.received is None
def test_construction_with_bad_persistence(self, caplog, bot):
class my_per:
def __init__(self):
self.store_user_data = False
self.store_chat_data = False
with pytest.raises(TypeError,
match='persistence should be based on telegram.ext.BasePersistence'):
Dispatcher(bot, None, persistence=my_per())
def test_error_handler_that_raises_errors(self, dp):
"""
Make sure that errors raised in error handlers don't break the main loop of the dispatcher
@@ -217,7 +260,11 @@ class TestDispatcher(object):
passed.append('error')
passed.append(e)
update = Update(1, message=Message(1, None, None, None, text='/start', bot=bot))
update = Update(1, message=Message(1, None, None, None, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0,
length=len('/start'))],
bot=bot))
# If Stop raised handlers in other groups should not be called.
passed = []
@@ -244,7 +291,11 @@ class TestDispatcher(object):
passed.append('error')
passed.append(e)
update = Update(1, message=Message(1, None, None, None, text='/start', bot=bot))
update = Update(1, message=Message(1, None, None, None, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0,
length=len('/start'))],
bot=bot))
# If an unhandled exception was caught, no further handlers from the same group should be
# called.
@@ -274,7 +325,11 @@ class TestDispatcher(object):
passed.append('error')
passed.append(e)
update = Update(1, message=Message(1, None, None, None, text='/start', bot=bot))
update = Update(1, message=Message(1, None, None, None, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0,
length=len('/start'))],
bot=bot))
# If a TelegramException was caught, an error handler should be called and no further
# handlers from the same group should be called.
@@ -305,7 +360,11 @@ class TestDispatcher(object):
passed.append(e)
raise DispatcherHandlerStop
update = Update(1, message=Message(1, None, None, None, text='/start', bot=bot))
update = Update(1, message=Message(1, None, None, None, text='/start',
entities=[MessageEntity(type=MessageEntity.BOT_COMMAND,
offset=0,
length=len('/start'))],
bot=bot))
# If a TelegramException was caught, an error handler should be called and no further
# handlers from the same group should be called.
@@ -316,3 +375,17 @@ class TestDispatcher(object):
dp.process_update(update)
assert passed == ['start1', 'error', err]
assert passed[2] is err
def test_error_handler_context(self, cdp):
cdp.add_error_handler(self.callback_context)
error = TelegramError('Unauthorized.')
cdp.update_queue.put(error)
sleep(.1)
assert self.received == 'Unauthorized.'
@pytest.mark.skipif(sys.version_info < (3, 0), reason='pytest fails this for no reason')
def test_non_context_deprecation(self, dp):
with pytest.warns(TelegramDeprecationWarning):
Dispatcher(dp.bot, dp.update_queue, job_queue=dp.job_queue, workers=0,
use_context=False)
+6 -6
View File
@@ -43,14 +43,14 @@ class TestDocument(object):
file_size = 12948
mime_type = 'image/png'
file_name = 'telegram.png'
thumb_file_size = 2364
thumb_width = 90
thumb_height = 90
thumb_file_size = 8090
thumb_width = 300
thumb_height = 300
def test_creation(self, document):
assert isinstance(document, Document)
assert isinstance(document.file_id, str)
assert document.file_id is not ''
assert document.file_id != ''
def test_expected_values(self, document):
assert document.file_size == self.file_size
@@ -75,8 +75,8 @@ class TestDocument(object):
assert message.document.mime_type == document.mime_type
assert message.document.file_size == document.file_size
assert message.caption == self.caption.replace('*', '')
assert message.document.thumb.width == 50
assert message.document.thumb.height == 50
assert message.document.thumb.width == self.thumb_width
assert message.document.thumb.height == self.thumb_height
@flaky(3, 1)
@pytest.mark.timeout(10)

Some files were not shown because too many files have changed in this diff Show More