Compare commits

...

76 Commits

Author SHA1 Message Date
Hinrich Mahler bf68942c91 Bump to v13.0 2020-10-07 21:23:55 +02:00
Hinrich Mahler 5fd7606084 Type Hinting (#1920) 2020-10-07 20:30:41 +02:00
Bibo-Joshi 103b115486 Defaults.tzinfo (#2042) 2020-10-07 20:30:41 +02:00
Hinrich Mahler b07e42ef33 Make context-based callbacks the default setting (#2050) 2020-10-07 20:30:41 +02:00
Bibo-Joshi 3842846b2d Refactor Handling of Message VS Update Filters (#2032)
* Refactor handling of message vs update filters

* address review
2020-10-07 20:30:41 +02:00
Bibo-Joshi 7daddfb54d Refactor handling of default_quote (#1965)
* Refactor handling of `default_quote`

* Make it a breaking change

* Pickle a bots defaults

* Temporarily enable tests for the v13 branch

* Temporarily enable tests for the v13 branch

* Refactor handling of kwargs in Bot methods (#1924)

* Unify kwargs handling in Bot methods

* Remove Request.get, make api_kwargs an explicit argument, move note to head of Bot class

* Fix test_official

* Update get_file methods

* Refactor JobQueue (#1981)

* First go on refactoring JobQueue

* Temporarily enable tests for the v13 branch

* Work on tests

* Temporarily enable tests for the v13 branch

* Increase coverage

* Remove JobQueue.tick()

* Address review

* Temporarily enable tests for the v13 branch

* Address review

* Dispatch errors

* Fix handling of job_kwargs

* Remove possibility to pass a Bot to JobQueue

* Refactor persistence of Bot instances (#1994)

* Refactor persistence of bots

* User BP.set_bot in Dispatcher

* Temporarily enable tests for the v13 branch

* Add documentation

* Add warning to Updater for passing both defaults and bot

* Address review

* Fix test
2020-10-07 20:30:41 +02:00
Bibo-Joshi 2d4d48b89d Extend rich comparison of objects (#1724)
* Make most objects comparable

* ID attrs for PollAnswer

* fix test_game

* fix test_userprofilephotos

* update for API 4.7

* Warn on meaningless comparisons

* Update for API 4.8

* Address review

* Get started on docs, update Message._id_attrs

* Change PollOption & InputLocation

* Some more changes

* Even more changes
2020-10-07 20:30:41 +02:00
Bibo-Joshi 2381724b7c Refactor persistence of Bot instances (#1994)
* Refactor persistence of bots

* Use BP.set_bot in Dispatcher

* Add documentation
2020-10-07 20:30:41 +02:00
Bibo-Joshi 19a4f9e53a Refactor JobQueue (#1981)
* First go on refactoring JobQueue

* Temporarily enable tests for the v13 branch

* Work on tests

* Temporarily enable tests for the v13 branch

* Increase coverage

* Remove JobQueue.tick()

* Address review

* Temporarily enable tests for the v13 branch

* Address review

* Dispatch errors

* Fix handling of job_kwargs

* Remove possibility to pass a Bot to JobQueue
2020-10-07 20:30:41 +02:00
Bibo-Joshi 3930072659 Refactor handling of kwargs in Bot methods (#1924)
* Unify kwargs handling in Bot methods

* Remove Request.get, make api_kwargs an explicit argument, move note to head of Bot class

* Fix test_official

* Update get_file methods
2020-10-07 20:30:41 +02:00
Bibo-Joshi 5555582b72 Doc Fixes/Additions (#2094)
* Add notes on thumbs being ignored for small video files

* Fix some cross refs

* Add not to DictPersistence about it not actually writing to file

* Fix reply_to_message docs of Message
2020-10-07 17:12:05 +02:00
Delgan e67b995e64 Make Errors picklable (#2106)
* Fix TypeError while unpickling TelegramError (and children)

* Add more extensive unit tests for errors pickling

* Move error pickling tests back to "test_error.py"

* Add test making sure that new errors are covered by tests

* Make meta test independent of sorting

Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2020-10-04 18:15:36 +02:00
Bibo-Joshi 0d419ed6b4 Refactor Dispatcher.run_async (#2051) 2020-10-04 17:20:33 +02:00
Bibo-Joshi 97adcdf538 Automatic Pagination for answer_inline_query (#2072)
* Auto Pagination

* Fix test_official

* Get things to actually work

* Fine tune

* Tweak tests

* Address review

* Add warning to answer_inline_query
2020-09-27 14:11:49 +02:00
Poolitzer 2989108e95 Fix setting thumbs with send_media_group (#2093)
* fix: uploading thumb in media group

* add test

Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2020-09-19 16:50:34 +02:00
Bibo-Joshi 897a20d758 Link Ask-Right Wiki Page in Question Template (#2090)
* Link ask-right wiki page in question template

* typo
2020-09-16 20:53:48 +02:00
Bibo-Joshi ed3a9b64e2 Add Note on PyPy to Readme (#2089) 2020-09-16 20:53:19 +02:00
Bibo-Joshi 49c0c9e4d1 Make MessageHandler filter for Filters.update first (#2085)
* Tweak MessageHandlers filters

* Improve test
2020-09-14 17:55:01 +02:00
Bibo-Joshi bb34c79909 Fix Webhook not working on Windows with Python 3.8+ (#2067)
* Fix start_webhook NotImplementedError on windows with python3.8

* Fine-tune and add tests.

* Minor fixes

* typos

* Make Codacy happy

Co-authored-by: n5y <41209360+n5y@users.noreply.github.com>
2020-08-25 22:21:24 +02:00
Bibo-Joshi a0720b9ac6 Documentation Improvements (#2008)
* Minor doc updates, following official API docs

* Fix spelling in Defaults docstrings

* Clarify Changelog of v12.7 about aware dates

* Fix typo in CHANGES.rst (#2024)

* Fix PicklePersistence.flush() with only bot_data (#2017)

* Update pylint in pre-commit to fix CI (#2018)

* Add Filters.via_bot (#2009)

* feat: via_bot filter

also fixing a small mistake in the empty parameter of the user filter and improve docs slightly

* fix: forgot to set via_bot to None

* fix: redoing subclassing to copy paste solution

* Cosmetic changes

Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>

* Update CHANGES.rst

Fixed Typo

Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>
Co-authored-by: Poolitzer <25934244+Poolitzer@users.noreply.github.com>

* Update downloads badge, add info on IRC Channel to Getting Help section

* Remove RegexHandler from ConversationHandlers Docs (#1973)

Replaced RegexHandler with MessageHandler, since the former is deprecated

* Fix Filters.via_bot docstrings

* Add notes on Markdown v1 being legacy mode

* Fixed typo in the Regex doc.. (#2036)

* Typo: Spelling

* Minor cleanup from #2043

* Document CommandHandler ignoring channel posts

* Doc fixes for a few telegram.ext classes

* Doc fixes for most `telegram` classes.

* pep-8

forgot the hard wrap is at 99 chars, not 100!
fixed a few spelling mistakes too.

* Address review and made rendering of booleans consistent

True, False, None are now rendered with ``bool`` wherever they weren't in telegram and telegram.ext classes.

* Few doc fixes for inline* classes

As usual, docs were cross-checked with official tg api docs.

* Doc fixes for telegram/files classes

As usual, docs were cross-checked with official tg api docs.

* Doc fixes for telegram.Game

Mostly just added hyperlinks. And fixed message length doc.

As usual, docs were cross-checked with official tg api docs.

* Very minor doc fix for passportfile.py and passportelementerrors.py

Didn't bother changing too much since this seems to be a custom implementation.

* Doc fixes for telegram.payments

As usual, cross-checked with official bot api docs.

* Address review 2

Few tiny other fixes too.

* Changed from ``True/False/None`` to :obj:`True/False/None` project-wide.

Few tiny other doc fixes too.

Co-authored-by: Robert Geislinger <mitachundkrach@gmail.com>
Co-authored-by: Poolitzer <25934244+Poolitzer@users.noreply.github.com>
Co-authored-by: GauthamramRavichandran <30320759+GauthamramRavichandran@users.noreply.github.com>
Co-authored-by: Mahesh19 <maheshvagicherla99438@gmail.com>
Co-authored-by: hoppingturtles <ilovebhagwan@gmail.com>
2020-08-24 19:35:57 +02:00
Bibo-Joshi faa93fbf75 Refine Codecov Settings (#2061) 2020-08-21 23:37:07 +02:00
Bibo-Joshi da452df07d Allow DispatcherHandlerStop in ConversationHandler (#2059)
* First go

* Fix bug with nested convs
2020-08-21 23:20:28 +02:00
Bibo-Joshi 3304cc5c90 Fix Lock-Bot configuration (#2053)
* Run lock bot twice a day and don't give a reason for locking

* Fix indentation

* Fix invalid time
2020-08-18 20:52:43 +02:00
Bibo-Joshi 9105d83d37 Refine Lock-Bot (#2052)
* Run lock bot twice a day and don't give a reason for locking

* Fix indentation
2020-08-18 20:43:27 +02:00
Bibo-Joshi b6b42b2043 Switch from Lock-Bot to GH Actions (#2049) 2020-08-16 10:43:30 +02:00
Bibo-Joshi f857e1c23b Add Lock-Bot (#2048) 2020-08-14 12:57:18 +02:00
Bibo-Joshi fc5844c13d Add missing shortcuts (#2043)
* Add all the methods

* Add tests and fix some typos

* A few more minor changes

* Address review
2020-08-13 13:39:43 +02:00
Leonardo Rezende dea24bcb7c Refine pollbot.py example (#2047)
* pollbot.py example was sending the poll to the effective_user and not effective_chat... well, it's a poll to be answered by multiple uses.

* Use User.mention_html() shortcut

Co-authored-by: Leonardo <leonardo.rezende@trt19.jus.br>
Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2020-08-13 13:38:23 +02:00
Bibo-Joshi 2789fd2bff Refine Filters in examples (#2027) 2020-07-18 12:17:42 +02:00
Bibo-Joshi d6f8077a50 Rename echobot examples (#2025) 2020-07-16 19:17:57 +02:00
Poolitzer 0189442525 Add Filters.via_bot (#2009)
* feat: via_bot filter

also fixing a small mistake in the empty parameter of the user filter and improve docs slightly

* fix: forgot to set via_bot to None

* fix: redoing subclassing to copy paste solution

* Cosmetic changes

Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2020-07-14 21:51:36 +02:00
Bibo-Joshi fd0325fbe5 Update pylint in pre-commit to fix CI (#2018) 2020-07-10 12:57:27 +02:00
Bibo-Joshi ff4bb15fef Fix PicklePersistence.flush() with only bot_data (#2017) 2020-07-10 11:10:53 +02:00
ikkemaniac 9288e4f2e4 Fix docstring of Message.reply_media_group (#2005) 2020-06-25 07:54:23 +02:00
ikkemaniac e60318166e Add test for clean argument of Updater.start_polling/webhook (#2002)
* added test for 'clean' argument passed to 'start_polling()'

* remove TODO

* prettify

* remove bool from func name

* improve name-ing of fake update func

* cleanup class and nameing

* replace while for for

* swap valueerror for runtimeerror

* remove all other code to reduce testing

* add comments

* don't raise error, complete cycle and assert

* remove inf loop protection

* Revert "remove all other code to reduce testing"

This reverts commit 4566a1debd.

* remove error parametrization

* remove comment

* remove pass from class

* rename update_id to offset as the original get_updates() takes argument offset (which is the update_id)

* rename test func to match original func

* fix comment

* shorten for loop

* mock get_updates() behavior when 'offset' is passed. Assert with get_updates()

* remove other functions to reduce testing

* replicate original get_updates()

* move fakeupdate class and list creation outside get_updates and store in var

* loop from 0 to make update_id consistant w array key, just easier to debug

* update comments

* Revert "remove other functions to reduce testing"

This reverts commit 1fb498a6cc.

* fix typo

* Revert "fix typo"

This reverts commit ade9fec609.

* Revert "Revert "remove other functions to reduce testing""

This reverts commit 734de1371c.

* Revert "update comments"

This reverts commit f3a032e75e.

* Revert "loop from 0 to make update_id consistant w array key, just easier to debug"

This reverts commit 0c6881d8a1.

* Revert "move fakeupdate class and list creation outside get_updates and store in var"

This reverts commit 71de999300.

* Revert "replicate original get_updates()"

This reverts commit 5d0710ac3a.

* Revert "remove other functions to reduce testing"

This reverts commit 1fb498a6cc.

* Revert "mock get_updates() behavior when 'offset' is passed. Assert with get_updates()"

This reverts commit 8c727ba1e8.

* loop from 0 to make update_id consistant w array key, for consitency

Co-authored-by: ikkemaniac <ikkemaniac@localhost>
2020-06-24 00:25:58 +02:00
Hinrich Mahler 15268acb27 Bump to v12.8 2020-06-22 20:20:00 +02:00
Poolitzer 927502e588 API 4.9 (#1980)
* Add Basketball Dice

Added Basketball Dice Variation

* Update dice.py

* Update dice.py

* Update telegram/dice.py

Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>

* Update bot.py

* Update filters.py

* Update test_filters.py

* Fixed whitespaces

* Update dice.py

* Fix line length

* adding dice values to docstring

* adding via_bot to message and thumb_mime_type to iqresults

* feat: updating docs

* feat: improving message attribute test

* Fix flake8

Co-authored-by: Jannik <32801117+code1mountain@users.noreply.github.com>
Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>
2020-06-22 20:09:52 +02:00
Yan 0af5cc2db8 Don't override builtin method help() in examples (#1997)
* Do not override builtin method help()

* Rename inlinebot and inlinekeyboard /help function to not conflict with builtin
2020-06-16 17:07:05 +02:00
Bibo-Joshi 6005861f46 Stabilize CI (#2000) 2020-06-15 18:45:38 +02:00
Nils K 8406889179 Remove Python 2 Support (#1715)
* Remove usages of python-future lib

* Remove python2 datetime.timezone replacement

* Remove python2 workaround in InputFile.__init__

* Remove import of str necessary for python2

* Remove urllib2 import necessary for python2

* Remove a mention of python 2 in doc

* Remove python 2 from travis config file

* Remove python 2 from appveyor config

* Remove python2 from debian build rules

* Remove unnecessarry aliasing of time.perf_counter

* Remove python 2 from github workflow

* Remove mention of python 2 in descriptions/readme

* Remove version check for queue import

* Remove version checks in tests

* Adjust docs to correctly mention supported version

* Fix indentation

* Remove unused 'sys' imports

* Fix indentation

* Remove references to mq.curtime in tests

* Replace super calls by argumentsless version

* Remove future dependency

* Fix error in de_json declaration

* Use python3 metaclass syntax

* Use implicit inheriting from object

* Remove accidentally committed .vscode folder

* Use nameless f-string and raw string

* Fix regex string literal syntax

* Remove old style classes

* Run pyupgrade

* Fix leftover from automatic merge

* Fix lint errors

* Update telegram/files/sticker.py

Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>
2020-06-15 18:20:51 +02:00
n5y a4e78f6183 Add standalone example on error handlers (#1983)
* Remove error handlers from examples

Most examples use the same error handler, that error handler logs
update.to_dict but doesn't log error traceback. Hiding error traceback
is quite bad, removing the error handler entirely causes PTB to use
default error logging which does include error traceback.

* adding error handling example

* Change error handler example

Including:
- Change the telegram message to include usual python error message.
- HTML-escape the strings used to build the telegram message.
- Capitalize comments and add more empty lines to hopefully unify the
  style with other examples, at least a bit.
- Reorder imports.

* Add an error-rising command to the error handler example

* Slightly change example error handler docstring and comments

* Make telegram message sent by the error handler example more readable

* Rename error_handler.py to errorhandlerbot.py and add a start command

* Change error handler example to work without developer chat id

* Revert "Change error handler example to work without developer chat id"

This reverts commit c4efea6f

* Make bot token a module level constant in the error handler example

Otherwise the example will require two edits 40 lines apart to run.

* Show chat id in start command of the error handler example

The example requires you to set developer chat id, this change will
make things easier for users that don't know how to see their chat id.

* Add errorhandlerbot.py to the examples folder readme

Co-authored-by: poolitzer <25934244+poolitzer@users.noreply.github.com>
Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>
2020-06-12 18:50:12 +02:00
Bibo-Joshi 8c6cb44a85 Update examples Readme (#1995) 2020-06-12 13:58:20 +02:00
n5y ac7cc7fe5e Ignore private attributes in TelegramObject.to_dict() (#1989) 2020-06-10 22:21:25 +02:00
Bibo-Joshi a42b68933c Add User.send_poll (#1968) 2020-05-27 21:59:49 +02:00
ѕнιναм c2d91c752f Typo-Fix (#1962) 2020-05-22 15:32:03 +02:00
Bibo-Joshi 5057825586 Doc fixes (#1940)
* Update notes on editing messages

* Update thumb and InputMedia* doc strings

* Fix attribute docstring for Updater.user_sig_handler

* Improve rendering for CCs attributes

* fix doc str for InputMedia*.media attribute

* Minor fix
2020-05-15 15:59:41 +02:00
Bibo-Joshi 613175b2c4 Allow updating ids/usernames of Filters.{user, chat} (#1757)
* Make Filters.user attrs mutable

* Add test_filters_user_empty_args

* Add test_filters_user_empty_args

* fix locks

* Make codecov happy

* Make user_ids and usernames sets

* Correct doc string

* Address review

* Review Vol. II

* Apply suggestions from code review

Co-authored-by: Noam Meltzer <tsnoam@gmail.com>

* Review Vol III.

* propery setter is now only a wrapper to a private method + more cleanups

pylint complained on some extra stuff, so cleaned them as well

* Review Vol. IV

* Review Vol. V

* Apply changes to Filters.chat

Co-authored-by: Noam Meltzer <tsnoam@gmail.com>
2020-05-10 12:15:11 +02:00
Bibo-Joshi 1330696259 Improve readability of nested conversation example (#1943) 2020-05-04 16:59:51 +02:00
n5y 5898e1fe7a Remove NullHandlers (#1913) 2020-05-03 10:28:03 +02:00
Bibo-Joshi b6dec118c1 Update contribution guide and stale bot (#1937)
* * Remove developers Mail from contrib guide
* stricter settings for stale bot

* Remove link, too ...

* Link to TG Group instead
2020-05-02 22:24:12 +02:00
Hinrich Mahler 186fd1b418 Bump version to v12.7 2020-05-02 12:14:38 +02:00
Hinrich Mahler 284786fdb8 Fix doc string of run_monthly 2020-05-02 12:14:01 +02:00
Bibo-Joshi c7c56ad24e Api 4.8 (#1917)
* API 4.8

* Elaborate docs

* Address review

* Fix Message.to_json/dict() test

* More coverage

* Update telegram/bot.py

Co-authored-by: Noam Meltzer <tsnoam@gmail.com>

Co-authored-by: Noam Meltzer <tsnoam@gmail.com>
2020-05-02 11:56:52 +02:00
D David Livingston ae17ce977e Add JobQueue.run_monthly() (#1705)
* added monthly job

* removed fold argument

* addressed pr comments

* addressed pr comments

* made changes from pr review

* updated comments

* clean up code

* Update .pre-commit-config.yaml

* Minor cleanup

* Update according to #1685, minor robustness changes

Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2020-05-02 08:59:50 +02:00
Bibo-Joshi 7e231183c4 Add tzinfo kwarg to from_timestamp() (#1621)
* Add tz kwarg to from_timestamp()

* Correct handling of tzinfo=None

* Small Improvements

* None-tz yields naive dto

* Remove legacey compatibility of UTC stuff

* Update telegram/utils/helpers.py

Co-authored-by: Noam Meltzer <tsnoam@gmail.com>
2020-05-01 22:55:13 +02:00
Bibo-Joshi 8427346a0d Add supegroup for each test bot (#1919) 2020-05-01 21:29:18 +03:00
Bibo-Joshi 632b989d90 Use @abstractmethod instead of raising NotImplementedError (#1905) 2020-05-01 21:27:34 +03:00
Bibo-Joshi 76567ba635 Stabilize CI (#1931) 2020-05-01 13:27:46 +02:00
Bibo-Joshi 2bd3f2a65a Render Notes correctly (#1914)
* Renders Notes in JobQueues docs correctly

* Notes: -> Note:
2020-04-25 12:34:13 +02:00
Bibo-Joshi 26a5006bf1 Update question template (#1910)
* Update formulation in question template

* grammar
2020-04-20 18:11:48 +02:00
Andrej730 110e2df443 Job.next_t (#1685)
* next_t property is added to Job class

Added new property to Job class - next_t, it will show the datetime when the job will be executed next time.
The property is updated during JobQueue._put method, right after job is added to queue.
Related to #1676

* Fixed newline and trailing whitespace

* Fixed PR issues, added test

1. Added setter for next_t - now JobQueue doesn't access protected Job._next_t.
2. Fixed Job class docstring.
3. Added test for next_t property.
4. Set next_t to None for run_once jobs that already ran.

* Fixed Flake8 issues

* Added next_t setter for datetime, added test

1. next_t setter now can accept datetime type.
2. added test for setting datetime to next_t and added some asserts that check tests results.
3. Also noticed Job.days setter raises ValueError when it's more appropriate to raise TypeError.

* Fixed test_warnings, added Number type to next_t setter

1. Changed type of error raised by interval setter from ValueError to TypeError..
2. Fixed test_warning after changing type of errors in Job.days and Job.interval.
3. Added Number type to next_t setter - now it can accept int too.

* Python 2 compatibility for test_job_next_t_property

Added _UTC and _UtcOffsetTimezone for python 2 compatibility

* Fixed PR issues

1. Replaced "datetime.replace tzinfo" with "datetime.astimezone"
2. Moved testing next_t setter to separate test.
3. Changed test_job_next_t_setter so it now uses non UTC timezone.

* Defining tzinfo from run_once, run_repeating

1. Added option to define Job.tzinfo from run_once (by when.tzinfo) and run_repeating (first.tzinfo)
2. Added test to check that tzinfo is always passed correctly.

* address review

Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2020-04-18 15:08:16 +02:00
Bibo-Joshi 57546795c5 Notes on Filters.text accepting command messages (#1902) 2020-04-18 12:16:14 +02:00
Hinrich Mahler 314f87ec44 Bump version to v12.6.1 2020-04-11 09:53:29 +02:00
Bibo-Joshi 4bbcd51ef5 Fix serialization of reply_markups (#1889) 2020-04-11 09:44:40 +02:00
Hinrich Mahler 38a33581b1 Bump version to v12.6 2020-04-10 23:52:08 +02:00
Hinrich Mahler fe821c08e6 Doc Fixes 2020-04-10 23:43:58 +02:00
Harshil 0a9f4bfbdd Doc fixes (#1884)
* Bot.py doc fixes

All docs obtained from official Bot API docs

* made flake8 happy

* address review

Also improved consistency of `returns:` in docs

Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2020-04-10 20:05:01 +02:00
Bibo-Joshi c4364c7166 GitHub Actions: Use checkout@v2 (#1887) 2020-04-10 19:57:52 +02:00
Bibo-Joshi d63e710784 API 4.7 (#1858)
* Pure API changes

* Address review

* set Bot.commands on successfull call of set_my_commands

* Get started on tests

* More tests!

* More Coverage!

* Reset changes in utils.request

* Filters.dice, Filters.dice.text

* more coverage

* Address review

* Address review

* Test stop_poll with reply_markup

* Test stop_poll also without reply_markup

* Rephrase note on 'dice'

* Fix grammar in note on Filters.dice

* update api version readme

* address review
2020-04-10 19:22:45 +02:00
Bibo-Joshi f379f54d5a Tweak persistence handling (#1827)
* Unify persistence updates in dispatcher

* Ensure user/chat_data is not None when updating it

* Update persistence after job runs

* Increase coverage
2020-04-10 13:23:13 +02:00
Bibo-Joshi bdf0cb91f3 Pass last valid context to TIMEOUT handlers (#1826) 2020-04-10 13:18:43 +02:00
Bibo-Joshi 3101ea8432 Favor concrete types over "Iterable" (#1882)
* Use concrete types instead of 'iterable'

* Fix overlooked docstring

* address review
2020-04-08 22:49:01 +02:00
Harshil beb8ba3db0 Doc Fixes (#1874)
* doc fixes

* Update AUTHORS.rst

* More doc fixes

All docs were obtained from official Bot API docs.

* Shortened line length

Did this so it passes codacy check

* Revert id docstring changes

* typo

Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2020-04-07 17:25:17 +02:00
Bibo-Joshi f0b1aeb6fd Customize issue template chooser (#1880)
* Customize issue template chooser

* Improve wording
2020-04-07 15:45:17 +02:00
Bibo-Joshi d65558888e Add note on UTC to run_{repeating, once} (#1854) 2020-03-31 00:05:08 +02:00
Bibo-Joshi 61a66a32c8 Add tests for empty string as switch_inline_query(_current_chat) (#1635) 2020-03-31 00:03:45 +02:00
281 changed files with 12297 additions and 5861 deletions
+8 -4
View File
@@ -41,9 +41,9 @@ If you already know what you'd like to work on, you can skip this section.
If you have an idea for something to do, first check if it's already been filed on the `issue tracker`_. If so, add a comment to the issue saying you'd like to work on it, and we'll help you get started! Otherwise, please file a new issue and assign yourself to it.
Another great way to start contributing is by writing tests. Tests are really important because they help prevent developers from accidentally breaking existing code, allowing them to build cool things faster. If you're interested in helping out, let the development team know by posting to the `developers' mailing list`_, and we'll help you get started.
Another great way to start contributing is by writing tests. Tests are really important because they help prevent developers from accidentally breaking existing code, allowing them to build cool things faster. If you're interested in helping out, let the development team know by posting to the `Telegram group`_ (use `@admins` to mention the maintainers), and we'll help you get started.
That being said, we want to mention that we are very hesistant about adding new requirements to our projects. If you intend to do this, please state this in an issue and get a verification from one of the maintainers.
That being said, we want to mention that we are very hesitant about adding new requirements to our projects. If you intend to do this, please state this in an issue and get a verification from one of the maintainers.
Instructions for making a code change
#####################################
@@ -68,7 +68,9 @@ Here's how to make a one-off code change.
- You can refer to relevant issues in the commit message by writing, e.g., "#105".
- Your code should adhere to the `PEP 8 Style Guide`_, with the exception that we have a maximum line length of 99.
- Provide static typing with signature annotations. The documentation of `MyPy`_ will be a good start, the cheat sheet is `here`_. We also have some custom type aliases in ``telegram.utils.helpers.typing``.
- Document your code. This project uses `sphinx`_ to generate static HTML docs. To build them, first make sure you have the required dependencies:
.. code-block:: bash
@@ -245,9 +247,11 @@ break the API classes. For example:
.. _`Code of Conduct`: https://www.python.org/psf/codeofconduct/
.. _`issue tracker`: https://github.com/python-telegram-bot/python-telegram-bot/issues
.. _`developers' mailing list`: mailto:devs@python-telegram-bot.org
.. _`Telegram group`: https://telegram.me/pythontelegrambotgroup
.. _`PEP 8 Style Guide`: https://www.python.org/dev/peps/pep-0008/
.. _`sphinx`: http://sphinx-doc.org
.. _`Google Python Style Guide`: http://google.github.io/styleguide/pyguide.html
.. _`Google Python Style Docstrings`: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html
.. _AUTHORS.rst: ../AUTHORS.rst
.. _`MyPy`: https://mypy.readthedocs.io/en/stable/index.html
.. _`here`: https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Telegram Group
url: https://telegram.me/pythontelegrambotgroup
about: Questions asked on the group usually get answered faster.
- name: IRC Channel
url: https://webchat.freenode.net/?channels=##python-telegram-bot
about: In case you are unable to join our group due to Telegram restrictions, you can use our IRC channel
+1 -1
View File
@@ -10,7 +10,7 @@ assignees: ''
<!--
Hey there, you have a question? We are happy to answer. Please make sure no similar question was opened already.
The following template is a suggestion how you can report an issue you run into whilst using our library. If you just want to ask a question, feel free to delete everything; just make sure you have a describing title :)
To make it easier for us to help you, please read this article https://git.io/JURJO and try to follow the template below as closely as possible.
Please mind that there is also a users' Telegram group at https://t.me/pythontelegrambotgroup for questions about the library. Questions asked there might be answered quicker than here. In case you are unable to join our group due to Telegram restrictions, you can use our IRC channel at https://webchat.freenode.net/?channels=##python-telegram-bot to participate in the group.
-->
+1 -1
View File
@@ -1,5 +1,5 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 5
daysUntilStale: 3
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 2
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
+18
View File
@@ -0,0 +1,18 @@
name: 'Lock Closed Threads'
on:
schedule:
- cron: '8 4 * * *'
- cron: '42 17 * * *'
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2.0.1
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: '1'
issue-lock-reason: ''
pr-lock-inactive-days: '1'
pr-lock-reason: ''
+5 -5
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ${{matrix.os}}
strategy:
matrix:
python-version: [3.5, 3.6, 3.7, 3.8]
python-version: [3.6, 3.7, 3.8]
os: [ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
@@ -26,7 +26,7 @@ jobs:
test-build: True
fail-fast: False
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Initialize vendored libs
run:
git submodule update --init --recursive
@@ -51,7 +51,7 @@ jobs:
exit ${global_exit}
env:
JOB_INDEX: ${{ strategy.job-index }}
BOTS: W3sidG9rZW4iOiAiNjk2MTg4NzMyOkFBR1Z3RUtmSEhsTmpzY3hFRE5LQXdraEdzdFpfa28xbUMwIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WldGaU1UUmxNbVF5TnpNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMi43IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzI3X2JvdCJ9LCB7InRva2VuIjogIjY3MTQ2ODg4NjpBQUdQR2ZjaVJJQlVORmU4MjR1SVZkcTdKZTNfWW5BVE5HdyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpHWXdPVGxrTXpNeE4yWTIiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNCIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zNF9ib3QifSwgeyJ0b2tlbiI6ICI2MjkzMjY1Mzg6QUFGUnJaSnJCN29CM211ekdzR0pYVXZHRTVDUXpNNUNVNG8iLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpNbU01WVdKaFl6a3hNMlUxIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgQ1B5dGhvbiAzLjUiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX2NweXRob25fMzVfYm90In0sIHsidG9rZW4iOiAiNjQwMjA4OTQzOkFBRmhCalFwOXFtM1JUeFN6VXBZekJRakNsZS1Kano1aGNrIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WXpoa1pUZzFOamMxWXpWbCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMy42IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzM2X2JvdCJ9LCB7InRva2VuIjogIjY5NTEwNDA4ODpBQUhmenlsSU9qU0lJUy1lT25JMjB5MkUyMEhvZEhzZnotMCIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk9HUTFNRGd3WmpJd1pqRmwiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNyIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zN19ib3QifSwgeyJ0b2tlbiI6ICI2OTE0MjM1NTQ6QUFGOFdrakNaYm5IcVBfaTZHaFRZaXJGRWxackdhWU9oWDAiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpZamM1TlRoaU1tUXlNV1ZoIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgUHlQeSAyLjciLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX3B5cHlfMjdfYm90In0sIHsidG9rZW4iOiAiNjg0MzM5OTg0OkFBRk1nRUVqcDAxcjVyQjAwN3lDZFZOc2c4QWxOc2FVLWNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TVRBek1UWTNNR1V5TmpnMCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIFB5UHkgMy41IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19weXB5XzM1X2JvdCJ9LCB7InRva2VuIjogIjY5MDA5MTM0NzpBQUZMbVI1cEFCNVljcGVfbU9oN3pNNEpGQk9oMHozVDBUbyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpEaGxOekU1TURrd1lXSmkiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIEFwcFZleW9yIHVzaW5nIENQeXRob24gMy40IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX2FwcHZleW9yX2NweXRob25fMzRfYm90In0sIHsidG9rZW4iOiAiNjk0MzA4MDUyOkFBRUIyX3NvbkNrNTVMWTlCRzlBTy1IOGp4aVBTNTVvb0JBIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WW1aaVlXWm1NakpoWkdNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gQXBwVmV5b3IgdXNpbmcgQ1B5dGhvbiAyLjciLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfYXBwdmV5b3JfY3B5dGhvbl8yN19ib3QifSwgeyJ0b2tlbiI6ICIxMDU1Mzk3NDcxOkFBRzE4bkJfUzJXQXd1SjNnN29oS0JWZ1hYY2VNbklPeVNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpBd056QXpZalZpTkdOayIsICJuYW1lIjogIlBUQiB0ZXN0cyBbMF0iLCAidXNlcm5hbWUiOiAicHRiXzBfYm90In0sIHsidG9rZW4iOiAiMTA0NzMyNjc3MTpBQUY4bk90ODFGcFg4bGJidno4VWV3UVF2UmZUYkZmQnZ1SSIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOllUVTFOVEk0WkdSallqbGkiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzFdIiwgInVzZXJuYW1lIjogInB0Yl8xX2JvdCJ9LCB7InRva2VuIjogIjk3MTk5Mjc0NTpBQUdPa09hVzBOSGpnSXY1LTlqUWJPajR2R3FkaFNGLVV1cyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk5XWmtNV1ZoWWpsallqVTUiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzJdIiwgInVzZXJuYW1lIjogInB0Yl8yX2JvdCJ9XQ==
BOTS: W3sidG9rZW4iOiAiNjk2MTg4NzMyOkFBR1Z3RUtmSEhsTmpzY3hFRE5LQXdraEdzdFpfa28xbUMwIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WldGaU1UUmxNbVF5TnpNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMi43IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMzkwOTgzOTk3IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzI3X2JvdCJ9LCB7InRva2VuIjogIjY3MTQ2ODg4NjpBQUdQR2ZjaVJJQlVORmU4MjR1SVZkcTdKZTNfWW5BVE5HdyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpHWXdPVGxrTXpNeE4yWTIiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ0NjAyMjUyMiIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zNF9ib3QifSwgeyJ0b2tlbiI6ICI2MjkzMjY1Mzg6QUFGUnJaSnJCN29CM211ekdzR0pYVXZHRTVDUXpNNUNVNG8iLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpNbU01WVdKaFl6a3hNMlUxIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgQ1B5dGhvbiAzLjUiLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDE0OTY5MTc3NTAiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX2NweXRob25fMzVfYm90In0sIHsidG9rZW4iOiAiNjQwMjA4OTQzOkFBRmhCalFwOXFtM1JUeFN6VXBZekJRakNsZS1Kano1aGNrIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WXpoa1pUZzFOamMxWXpWbCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIENQeXRob24gMy42IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMzMzODcxNDYxIiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19jcHl0aG9uXzM2X2JvdCJ9LCB7InRva2VuIjogIjY5NTEwNDA4ODpBQUhmenlsSU9qU0lJUy1lT25JMjB5MkUyMEhvZEhzZnotMCIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk9HUTFNRGd3WmpJd1pqRmwiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIFRyYXZpcyB1c2luZyBDUHl0aG9uIDMuNyIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTQ3ODI5MzcxNCIsICJib3RfdXNlcm5hbWUiOiAiQHB0Yl90cmF2aXNfY3B5dGhvbl8zN19ib3QifSwgeyJ0b2tlbiI6ICI2OTE0MjM1NTQ6QUFGOFdrakNaYm5IcVBfaTZHaFRZaXJGRWxackdhWU9oWDAiLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpZamM1TlRoaU1tUXlNV1ZoIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBvbiBUcmF2aXMgdXNpbmcgUHlQeSAyLjciLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEzNjM5MzI1NzMiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfdHJhdmlzX3B5cHlfMjdfYm90In0sIHsidG9rZW4iOiAiNjg0MzM5OTg0OkFBRk1nRUVqcDAxcjVyQjAwN3lDZFZOc2c4QWxOc2FVLWNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TVRBek1UWTNNR1V5TmpnMCIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gVHJhdmlzIHVzaW5nIFB5UHkgMy41IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDA3ODM2NjA1IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX3RyYXZpc19weXB5XzM1X2JvdCJ9LCB7InRva2VuIjogIjY5MDA5MTM0NzpBQUZMbVI1cEFCNVljcGVfbU9oN3pNNEpGQk9oMHozVDBUbyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOlpEaGxOekU1TURrd1lXSmkiLCAiYm90X25hbWUiOiAiUFRCIHRlc3RzIG9uIEFwcFZleW9yIHVzaW5nIENQeXRob24gMy40IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMjc5NjAwMDI2IiwgImJvdF91c2VybmFtZSI6ICJAcHRiX2FwcHZleW9yX2NweXRob25fMzRfYm90In0sIHsidG9rZW4iOiAiNjk0MzA4MDUyOkFBRUIyX3NvbkNrNTVMWTlCRzlBTy1IOGp4aVBTNTVvb0JBIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WW1aaVlXWm1NakpoWkdNeSIsICJib3RfbmFtZSI6ICJQVEIgdGVzdHMgb24gQXBwVmV5b3IgdXNpbmcgQ1B5dGhvbiAyLjciLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDEyOTMwNzkxNjUiLCAiYm90X3VzZXJuYW1lIjogIkBwdGJfYXBwdmV5b3JfY3B5dGhvbl8yN19ib3QifSwgeyJ0b2tlbiI6ICIxMDU1Mzk3NDcxOkFBRzE4bkJfUzJXQXd1SjNnN29oS0JWZ1hYY2VNbklPeVNjIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpBd056QXpZalZpTkdOayIsICJuYW1lIjogIlBUQiB0ZXN0cyBbMF0iLCAic3VwZXJfZ3JvdXBfaWQiOiAiLTEwMDExODU1MDk2MzYiLCAidXNlcm5hbWUiOiAicHRiXzBfYm90In0sIHsidG9rZW4iOiAiMTA0NzMyNjc3MTpBQUY4bk90ODFGcFg4bGJidno4VWV3UVF2UmZUYkZmQnZ1SSIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOllUVTFOVEk0WkdSallqbGkiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzFdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDg0Nzk3NjEyIiwgInVzZXJuYW1lIjogInB0Yl8xX2JvdCJ9LCB7InRva2VuIjogIjk3MTk5Mjc0NTpBQUdPa09hVzBOSGpnSXY1LTlqUWJPajR2R3FkaFNGLVV1cyIsICJwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY4NTA2MzpURVNUOk5XWmtNV1ZoWWpsallqVTUiLCAibmFtZSI6ICJQVEIgdGVzdHMgWzJdIiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxNDAyMjU1MDcwIiwgInVzZXJuYW1lIjogInB0Yl8yX2JvdCJ9XQ==
TEST_BUILD: ${{ matrix.test-build }}
TEST_PRE_COMMIT: ${{ matrix.test-pre-commit }}
shell: bash --noprofile --norc {0}
@@ -73,7 +73,7 @@ jobs:
os: [ubuntu-latest]
fail-fast: False
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Initialize vendored libs
run:
git submodule update --init --recursive
@@ -102,7 +102,7 @@ jobs:
os: [ubuntu-latest]
fail-fast: False
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Initialize vendored libs
run:
git submodule update --init --recursive
+1
View File
@@ -46,6 +46,7 @@ htmlcov/
.coverage.*
.cache
.pytest_cache
.mypy_cache
nosetests.xml
coverage.xml
*,cover
+7 -2
View File
@@ -7,14 +7,19 @@ repos:
args:
- --diff
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.1
rev: 3.8.1
hooks:
- id: flake8
- repo: git://github.com/pre-commit/mirrors-pylint
rev: 9d8dcbc2b86c796275680f239c1e90dcd50bd398
rev: v2.5.3
hooks:
- id: pylint
files: ^telegram/.*\.py$
args:
- --errors-only
- --disable=import-error
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v0.770'
hooks:
- id: mypy
files: ^telegram/.*\.py$
+4
View File
@@ -18,6 +18,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Alateas <https://github.com/alateas>`_
- `Ales Dokshanin <https://github.com/alesdokshanin>`_
- `Ambro17 <https://github.com/Ambro17>`_
- `Andrej Zhilenkov <https://github.com/Andrej730>`_
- `Anton Tagunov <https://github.com/anton-tagunov>`_
- `Avanatiker <https://github.com/Avanatiker>`_
- `Balduro <https://github.com/Balduro>`_
@@ -26,6 +27,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `d-qoi <https://github.com/d-qoi>`_
- `daimajia <https://github.com/daimajia>`_
- `Daniel Reed <https://github.com/nmlorg>`_
- `D David Livingston <https://github.com/daviddl9>`_
- `Eana Hufwe <https://github.com/blueset>`_
- `Ehsan Online <https://github.com/ehsanonline>`_
- `Eli Gao <https://github.com/eligao>`_
@@ -37,6 +39,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `evgfilim1 <https://github.com/evgfilim1>`_
- `franciscod <https://github.com/franciscod>`_
- `gamgi <https://github.com/gamgi>`_
- `Harshil <https://github.com/harshil21>`_
- `Hugo Damer <https://github.com/HakimusGIT>`_
- `ihoru <https://github.com/ihoru>`_
- `Jasmin Bom <https://github.com/jsmnbom>`_
@@ -53,6 +56,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Kjwon15 <https://github.com/kjwon15>`_
- `Li-aung Yip <https://github.com/LiaungYip>`_
- `Loo Zheng Yuan <https://github.com/loozhengyuan>`_
- `LRezende <https://github.com/lrezende>`_
- `macrojames <https://github.com/macrojames>`_
- `Michael Elovskikh <https://github.com/wronglink>`_
- `Mischa Krüger <https://github.com/Makman2>`_
+181
View File
@@ -2,6 +2,187 @@
Changelog
=========
Version 13.0
============
*Released 2020-10-07*
**For a detailed guide on how to migrate from v12 to v13, see this** `wiki page <https://github.com/python-telegram-bot/python-telegram-bot/wiki/Transition-guide-to-Version-13.0>`_.
**Major Changes:**
- Deprecate old-style callbacks, i.e. set ``use_context=True`` by default (`#2050`_)
- Refactor Handling of Message VS Update Filters (`#2032`_)
- Deprecate ``Message.default_quote`` (`#1965`_)
- Refactor persistence of Bot instances (`#1994`_)
- Refactor ``JobQueue`` (`#1981`_)
- Refactor handling of kwargs in Bot methods (`#1924`_)
- Refactor ``Dispatcher.run_async``, deprecating the ``@run_async`` decorator (`#2051`_)
**New Features:**
- Type Hinting (`#1920`_)
- Automatic Pagination for ``answer_inline_query`` (`#2072`_)
- ``Defaults.tzinfo`` (`#2042`_)
- Extend rich comparison of objects (`#1724`_)
- Add ``Filters.via_bot`` (`#2009`_)
- Add missing shortcuts (`#2043`_)
- Allow ``DispatcherHandlerStop`` in ``ConversationHandler`` (`#2059`_)
- Make Errors picklable (`#2106`_)
**Minor changes, CI improvements, doc fixes or bug fixes:**
- Fix Webhook not working on Windows with Python 3.8+ (`#2067`_)
- Fix setting thumbs with ``send_media_group`` (`#2093`_)
- Make ``MessageHandler`` filter for ``Filters.update`` first (`#2085`_)
- Fix ``PicklePersistence.flush()`` with only ``bot_data`` (`#2017`_)
- Add test for clean argument of ``Updater.start_polling/webhook`` (`#2002`_)
- Doc fixes, refinements and additions (`#2005`_, `#2008`_, `#2089`_, `#2094`_, `#2090`_)
- CI fixes (`#2018`_, `#2061`_)
- Refine ``pollbot.py`` example (`#2047`_)
- Refine Filters in examples (`#2027`_)
- Rename ``echobot`` examples (`#2025`_)
- Use Lock-Bot to lock old threads (`#2048`_, `#2052`_, `#2049`_, `#2053`_)
.. _`#2050`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2050
.. _`#2032`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2032
.. _`#1965`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1965
.. _`#1994`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1994
.. _`#1981`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1981
.. _`#1924`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1924
.. _`#2051`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2051
.. _`#1920`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1920
.. _`#2072`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2072
.. _`#2042`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2042
.. _`#1724`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1724
.. _`#2009`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2009
.. _`#2043`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2043
.. _`#2059`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2059
.. _`#2106`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2106
.. _`#2067`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2067
.. _`#2093`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2093
.. _`#2085`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2085
.. _`#2017`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2017
.. _`#2002`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2002
.. _`#2005`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2005
.. _`#2008`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2008
.. _`#2089`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2089
.. _`#2094`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2094
.. _`#2090`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2090
.. _`#2018`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2018
.. _`#2061`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2061
.. _`#2047`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2047
.. _`#2027`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2027
.. _`#2025`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2025
.. _`#2048`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2048
.. _`#2052`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2052
.. _`#2049`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2049
.. _`#2053`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2053
Version 12.8
============
*Released 2020-06-22*
**Major Changes:**
- Remove Python 2 support (`#1715`_)
- Bot API 4.9 support (`#1980`_)
- IDs/Usernames of ``Filters.user`` and ``Filters.chat`` can now be updated (`#1757`_)
**Minor changes, CI improvements, doc fixes or bug fixes:**
- Update contribution guide and stale bot (`#1937`_)
- Remove ``NullHandlers`` (`#1913`_)
- Improve and expand examples (`#1943`_, `#1995`_, `#1983`_, `#1997`_)
- Doc fixes (`#1940`_, `#1962`_)
- Add ``User.send_poll()`` shortcut (`#1968`_)
- Ignore private attributes en ``TelegramObject.to_dict()`` (`#1989`_)
- Stabilize CI (`#2000`_)
.. _`#1937`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1937
.. _`#1913`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1913
.. _`#1943`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1943
.. _`#1757`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1757
.. _`#1940`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1940
.. _`#1962`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1962
.. _`#1968`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1968
.. _`#1989`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1989
.. _`#1995`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1995
.. _`#1983`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1983
.. _`#1715`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1715
.. _`#2000`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2000
.. _`#1997`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1997
.. _`#1980`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1980
Version 12.7
============
*Released 2020-05-02*
**Major Changes:**
- Bot API 4.8 support. **Note:** The ``Dice`` object now has a second positional argument ``emoji``. This is relevant, if you instantiate ``Dice`` objects manually. (`#1917`_)
- Added ``tzinfo`` argument to ``helpers.from_timestamp``. It now returns an timezone aware object. This is relevant for ``Message.{date,forward_date,edit_date}``, ``Poll.close_date`` and ``ChatMember.until_date`` (`#1621`_)
**New Features:**
- New method ``run_monthly`` for the ``JobQueue`` (`#1705`_)
- ``Job.next_t`` now gives the datetime of the jobs next execution (`#1685`_)
**Minor changes, CI improvements, doc fixes or bug fixes:**
- Stabalize CI (`#1919`_, `#1931`_)
- Use ABCs ``@abstractmethod`` instead of raising ``NotImplementedError`` for ``Handler``, ``BasePersistence`` and ``BaseFilter`` (`#1905`_)
- Doc fixes (`#1914`_, `#1902`_, `#1910`_)
.. _`#1902`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1902
.. _`#1685`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1685
.. _`#1910`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1910
.. _`#1914`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1914
.. _`#1931`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1931
.. _`#1905`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1905
.. _`#1919`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1919
.. _`#1621`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1621
.. _`#1705`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1705
.. _`#1917`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1917
Version 12.6.1
==============
*Released 2020-04-11*
**Bug fixes:**
- Fix serialization of ``reply_markup`` in media messages (`#1889`_)
.. _`#1889`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1889
Version 12.6
============
*Released 2020-04-10*
**Major Changes:**
- Bot API 4.7 support. **Note:** In ``Bot.create_new_sticker_set`` and ``Bot.add_sticker_to_set``, the order of the parameters had be changed, as the ``png_sticker`` parameter is now optional. (`#1858`_)
**Minor changes, CI improvements or bug fixes:**
- Add tests for ``swtich_inline_query(_current_chat)`` with empty string (`#1635`_)
- Doc fixes (`#1854`_, `#1874`_, `#1884`_)
- Update issue templates (`#1880`_)
- Favor concrete types over "Iterable" (`#1882`_)
- Pass last valid ``CallbackContext`` to ``TIMEOUT`` handlers of ``ConversationHandler`` (`#1826`_)
- Tweak handling of persistence and update persistence after job calls (`#1827`_)
- Use checkout@v2 for GitHub actions (`#1887`_)
.. _`#1858`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1858
.. _`#1635`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1635
.. _`#1854`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1854
.. _`#1874`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1874
.. _`#1884`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1884
.. _`#1880`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1880
.. _`#1882`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1882
.. _`#1826`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1826
.. _`#1827`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1827
.. _`#1887`: https://github.com/python-telegram-bot/python-telegram-bot/pull/1887
Version 12.5.1
==============
*Released 2020-03-30*
+6
View File
@@ -6,6 +6,7 @@ PYTEST := pytest
PEP257 := pep257
PEP8 := flake8
YAPF := yapf
MYPY := mypy
PIP := pip
clean:
@@ -28,6 +29,9 @@ yapf:
lint:
$(PYLINT) -E telegram --disable=no-name-in-module,import-error
mypy:
$(MYPY) -p telegram
test:
$(PYTEST) -v
@@ -41,6 +45,7 @@ help:
@echo "- pep8 Check style with flake8"
@echo "- lint Check style with pylint"
@echo "- yapf Check style with yapf"
@echo "- mypy Check type hinting with mypy"
@echo "- test Run tests using pytest"
@echo
@echo "Available variables:"
@@ -49,4 +54,5 @@ help:
@echo "- PEP257 default: $(PEP257)"
@echo "- PEP8 default: $(PEP8)"
@echo "- YAPF default: $(YAPF)"
@echo "- MYPY default: $(MYPY)"
@echo "- PIP default: $(PIP)"
+9 -7
View File
@@ -17,7 +17,7 @@ We have a vibrant community of developers helping each other in our `Telegram gr
:target: https://pypi.org/project/python-telegram-bot/
:alt: Supported Python versions
.. image:: https://www.cpu.re/static/python-telegram-bot/downloads.svg
.. image:: https://cpu.re/static/python-telegram-bot/downloads.svg
:target: https://www.cpu.re/static/python-telegram-bot/downloads-by-python-version.txt
:alt: PyPi Package Monthly Download
@@ -83,7 +83,7 @@ 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 3.5+ and `PyPy <http://pypy.org/>`_.
It's compatible with Python versions 3.6+. PTB might also work on `PyPy <http://pypy.org/>`_, though there have been a lot of issues before. Hence, PyPy is not officially supported.
In addition to the pure API implementation, this library features a number of high-level classes to
make the development of bots easy and straightforward. These classes are contained in the
@@ -93,7 +93,7 @@ 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.6** are supported.
All types and methods of the Telegram Bot API **4.8** are supported.
==========
Installing
@@ -139,7 +139,7 @@ Learning by example
We believe that the best way to learn this package is by example. Here
are some examples for you to review. Even if it is not your approach for learning, please take a
look at ``echobot2``, it is the de facto base for most of the bots out there. Best of all,
look at ``echobot.py``, it is the de facto base for most of the bots out there. Best of all,
the code for these examples are released to the public domain, so you can start by grabbing the
code and building on top of it.
@@ -187,11 +187,13 @@ You can get help in several ways:
1. We have a vibrant community of developers helping each other in our `Telegram group <https://telegram.me/pythontelegrambotgroup>`_. Join us!
2. Report bugs, request new features or ask questions by `creating an issue <https://github.com/python-telegram-bot/python-telegram-bot/issues/new/choose>`_.
2. In case you are unable to join our group due to Telegram restrictions, you can use our `IRC channel <https://webchat.freenode.net/?channels=##python-telegram-bot>`_.
3. Our `Wiki pages <https://github.com/python-telegram-bot/python-telegram-bot/wiki/>`_ offer a growing amount of resources.
3. Report bugs, request new features or ask questions by `creating an issue <https://github.com/python-telegram-bot/python-telegram-bot/issues/new/choose>`_.
4. You can even ask for help on Stack Overflow using the `python-telegram-bot tag <https://stackoverflow.com/questions/tagged/python-telegram-bot>`_.
4. Our `Wiki pages <https://github.com/python-telegram-bot/python-telegram-bot/wiki/>`_ offer a growing amount of resources.
5. You can even ask for help on Stack Overflow using the `python-telegram-bot tag <https://stackoverflow.com/questions/tagged/python-telegram-bot>`_.
+9
View File
@@ -1 +1,10 @@
comment: false
coverage:
status:
project:
default:
# We allow small coverage decreases in the project because we don't retry
# on hitting flood limits, which adds noise to the coverage
target: auto
threshold: 0.1%
+1 -1
View File
@@ -15,7 +15,7 @@ Depends: ${python3:Depends}, ${misc:Depends}
Description: We have made you a wrapper you can't refuse!
The Python Telegram bot (Python 3)
This library provides a pure Python interface for the Telegram Bot API.
It's compatible with Python versions 2.7, 3.3+ and PyPy.
It's compatible with Python versions 3.5+ and PyPy.
.
In addition to the pure API implementation, this library features
a number of high-level
+1 -1
View File
@@ -6,7 +6,7 @@
export PYBUILD_NAME=telegram
%:
DEB_BUILD_OPTIONS=nocheck dh $@ --with python2,python3 --buildsystem=pybuild
DEB_BUILD_OPTIONS=nocheck dh $@ --with python3 --buildsystem=pybuild
# If you need to rebuild the Sphinx documentation
+2 -2
View File
@@ -58,9 +58,9 @@ author = u'Leandro Toledo'
# built documents.
#
# The short X.Y version.
version = '12.5' # telegram.__version__[:3]
version = '13.0' # telegram.__version__[:3]
# The full version, including alpha/beta/rc tags.
release = '12.5.1' # telegram.__version__
release = '13.0' # telegram.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
+6
View File
@@ -0,0 +1,6 @@
telegram.BotCommand
===================
.. autoclass:: telegram.BotCommand
:members:
:show-inheritance:
+6
View File
@@ -0,0 +1,6 @@
telegram.Dice
=============
.. autoclass:: telegram.Dice
:members:
:show-inheritance:
+2
View File
@@ -9,6 +9,7 @@ telegram package
telegram.animation
telegram.audio
telegram.bot
telegram.botcommand
telegram.callbackquery
telegram.chat
telegram.chataction
@@ -17,6 +18,7 @@ telegram package
telegram.chatphoto
telegram.constants
telegram.contact
telegram.dice
telegram.document
telegram.error
telegram.file
+1
View File
@@ -6,3 +6,4 @@ telegram.utils package
telegram.utils.helpers
telegram.utils.promise
telegram.utils.request
telegram.utils.types
+6
View File
@@ -0,0 +1,6 @@
telegram.utils.types Module
===========================
.. automodule:: telegram.utils.types
:members:
:show-inheritance:
+17 -5
View File
@@ -1,10 +1,10 @@
# Examples
In this folder are small examples to show what a bot written with `python-telegram-bot` looks like. Some bots focus on one specific aspect of the Telegram Bot API while others focus on one of the mechanics of this library. Except for the [`echobot.py`](#pure-api) example, they all use the high-level framework this library provides with the [`telegram.ext`](https://python-telegram-bot.readthedocs.io/en/latest/telegram.ext.html) submodule.
In this folder are small examples to show what a bot written with `python-telegram-bot` looks like. Some bots focus on one specific aspect of the Telegram Bot API while others focus on one of the mechanics of this library. Except for the [`rawapibot.py`](#pure-api) example, they all use the high-level framework this library provides with the [`telegram.ext`](https://python-telegram-bot.readthedocs.io/en/latest/telegram.ext.html) submodule.
All examples are licensed under the [CC0 License](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/LICENSE.txt) and are therefore fully dedicated to the public domain. You can use them as the base for your own bots without worrying about copyrights.
### [`echobot2.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot2.py)
### [`echobot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot.py)
This is probably the base for most of the bots made with `python-telegram-bot`. It simply replies to each text message with a message that contains the same text.
### [`timerbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/timerbot.py)
@@ -19,20 +19,32 @@ A more complex example of a bot that uses the `ConversationHandler`. It is also
### [`nestedconversationbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/nestedconversationbot.py)
A even more complex example of a bot that uses the nested `ConversationHandler`s. While it's certainly not that complex that you couldn't built it without nested `ConversationHanldler`s, it gives a good impression on how to work with them. Of course, there is a [fancy state diagram](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/nestedconversationbot.png) for this example, too!
### [`persistentconversationbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/persistentconversationbot.py)
A basic example of a bot store conversation state and user_data over multiple restarts.
### [`inlinekeyboard.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/inlinekeyboard.py)
This example sheds some light on inline keyboards, callback queries and message editing.
### [`inlinekeyboard2.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/inlinekeyboard2.py)
A more complex example about inline keyboards, callback queries and message editing. This example showcases how an interactive menu could be build using inline keyboards.
### [`deeplinking.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/deeplinking.py)
A basic example on how to use deeplinking with inline keyboards.
### [`inlinebot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/inlinebot.py)
A basic example of an [inline bot](https://core.telegram.org/bots/inline). Don't forget to enable inline mode with [@BotFather](https://telegram.me/BotFather).
### [`pollbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/pollbot.py)
This example sheds some light on polls, poll answers and the corresponding handlers.
### [`passportbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/passportbot.py)
A basic example of a bot that can accept passports. Use in combination with [`passportbot.html`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/passportbot.html). Don't forget to enable and configure payments with [@BotFather](https://telegram.me/BotFather). Check out this [guide](https://git.io/fAvYd) on Telegram passports in PTB.
### [`paymentbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/paymentbot.py)
A basic example of a bot that can accept payments. Don't forget to enable and configure payments with [@BotFather](https://telegram.me/BotFather).
### [`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.
### [`errorhandlerbot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/errorhandlerbot.py)
A basic example on how to set up a custom error handler.
## Pure API
The [`echobot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/echobot.py) example uses only the pure, "bare-metal" API wrapper.
The [`rawapibot.py`](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples/rawapibot.py) example uses only the pure, "bare-metal" API wrapper.
+1 -9
View File
@@ -108,11 +108,6 @@ def cancel(update, context):
return ConversationHandler.END
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, context.error)
def main():
# Create the Updater and pass it your bot's token.
# Make sure to set use_context=True to use the new context based callbacks
@@ -135,7 +130,7 @@ def main():
LOCATION: [MessageHandler(Filters.location, location),
CommandHandler('skip', skip_location)],
BIO: [MessageHandler(Filters.text, bio)]
BIO: [MessageHandler(Filters.text & ~Filters.command, bio)]
},
fallbacks=[CommandHandler('cancel', cancel)]
@@ -143,9 +138,6 @@ def main():
dp.add_handler(conv_handler)
# log all errors
dp.add_error_handler(error)
# Start the Bot
updater.start_polling()
+6 -14
View File
@@ -96,11 +96,6 @@ def done(update, context):
return ConversationHandler.END
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, context.error)
def main():
# Create the Updater and pass it your bot's token.
# Make sure to set use_context=True to use the new context based callbacks
@@ -121,13 +116,13 @@ def main():
custom_choice)
],
TYPING_CHOICE: [MessageHandler(Filters.text,
regular_choice)
],
TYPING_CHOICE: [
MessageHandler(Filters.text & ~(Filters.command | Filters.regex('^Done$')),
regular_choice)],
TYPING_REPLY: [MessageHandler(Filters.text,
received_information),
],
TYPING_REPLY: [
MessageHandler(Filters.text & ~(Filters.command | Filters.regex('^Done$')),
received_information)],
},
fallbacks=[MessageHandler(Filters.regex('^Done$'), done)]
@@ -135,9 +130,6 @@ def main():
dp.add_handler(conv_handler)
# log all errors
dp.add_error_handler(error)
# Start the Bot
updater.start_polling()
+2 -10
View File
@@ -61,7 +61,7 @@ def deep_linked_level_2(update, context):
bot = context.bot
url = helpers.create_deep_linked_url(bot.get_me().username, USING_ENTITIES)
text = "You can also mask the deep-linked URLs as links: " \
"[▶️ CLICK HERE]({0}).".format(url)
"[▶️ CLICK HERE]({}).".format(url)
update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True)
@@ -69,12 +69,7 @@ def deep_linked_level_3(update, context):
"""Reached through the USING_ENTITIES payload"""
payload = context.args
update.message.reply_text("Congratulations! This is as deep as it gets 👏🏻\n\n"
"The payload was: {0}".format(payload))
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, context.error)
"The payload was: {}".format(payload))
def main():
@@ -103,9 +98,6 @@ def main():
# Make sure the deep-linking handlers occur *before* the normal /start handler.
dp.add_handler(CommandHandler("start", start))
# log all errors
dp.add_error_handler(error)
# Start the Bot
updater.start_polling()
+54 -37
View File
@@ -1,55 +1,72 @@
#!/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 is built on the API wrapper, see echobot2.py to see the same example built
on the telegram.ext bot framework.
This program is dedicated to the public domain under the CC0 license.
"""
Simple Bot to reply to Telegram messages.
First, a few handler functions are defined. Then, those functions are passed to
the Dispatcher and registered at their respective places.
Then, the bot is started and runs until we press Ctrl-C on the command line.
Usage:
Basic Echobot example, repeats messages.
Press Ctrl-C on the command line or send a signal to the process to stop the
bot.
"""
import logging
import telegram
from telegram.error import NetworkError, Unauthorized
from time import sleep
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logger = logging.getLogger(__name__)
update_id = None
# Define a few command handlers. These usually take the two arguments update and
# context. Error handlers also receive the raised TelegramError object in error.
def start(update, context):
"""Send a message when the command /start is issued."""
update.message.reply_text('Hi!')
def help_command(update, context):
"""Send a message when the command /help is issued."""
update.message.reply_text('Help!')
def echo(update, context):
"""Echo the user message."""
update.message.reply_text(update.message.text)
def main():
"""Run the bot."""
global update_id
# Telegram Bot Authorization Token
bot = telegram.Bot('TOKEN')
"""Start the bot."""
# Create the Updater and pass it your bot's token.
# Make sure to set use_context=True to use the new context based callbacks
# Post version 12 this will no longer be necessary
updater = Updater("TOKEN", use_context=True)
# get the first pending update_id, this is so we can skip over it in case
# we get an "Unauthorized" exception.
try:
update_id = bot.get_updates()[0].update_id
except IndexError:
update_id = None
# Get the dispatcher to register handlers
dp = updater.dispatcher
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# on different commands - answer in Telegram
dp.add_handler(CommandHandler("start", start))
dp.add_handler(CommandHandler("help", help_command))
while True:
try:
echo(bot)
except NetworkError:
sleep(1)
except Unauthorized:
# The user has removed or blocked the bot.
update_id += 1
# on noncommand i.e message - echo the message on Telegram
dp.add_handler(MessageHandler(Filters.text & ~Filters.command, echo))
# Start the Bot
updater.start_polling()
def echo(bot):
"""Echo the message the user sent."""
global update_id
# Request updates after the last update_id
for update in bot.get_updates(offset=update_id, timeout=10):
update_id = update.update_id + 1
if update.message: # your bot can receive updates without messages
# Reply to the message
update.message.reply_text(update.message.text)
# 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__':
-81
View File
@@ -1,81 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This program is dedicated to the public domain under the CC0 license.
"""
Simple Bot to reply to Telegram messages.
First, a few handler functions are defined. Then, those functions are passed to
the Dispatcher and registered at their respective places.
Then, the bot is started and runs until we press Ctrl-C on the command line.
Usage:
Basic Echobot example, repeats messages.
Press Ctrl-C on the command line or send a signal to the process to stop the
bot.
"""
import logging
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logger = logging.getLogger(__name__)
# Define a few command handlers. These usually take the two arguments update and
# context. Error handlers also receive the raised TelegramError object in error.
def start(update, context):
"""Send a message when the command /start is issued."""
update.message.reply_text('Hi!')
def help(update, context):
"""Send a message when the command /help is issued."""
update.message.reply_text('Help!')
def echo(update, context):
"""Echo the user message."""
update.message.reply_text(update.message.text)
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, context.error)
def main():
"""Start the bot."""
# Create the Updater and pass it your bot's token.
# Make sure to set use_context=True to use the new context based callbacks
# Post version 12 this will no longer be necessary
updater = Updater("TOKEN", use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
# on different commands - answer in Telegram
dp.add_handler(CommandHandler("start", start))
dp.add_handler(CommandHandler("help", help))
# on noncommand i.e message - echo the message on Telegram
dp.add_handler(MessageHandler(Filters.text, echo))
# 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()
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This program is dedicated to the public domain under the CC0 license.
"""
This is a very simple example on how one could implement a custom error handler
"""
import html
import json
import logging
import traceback
from telegram import Update, ParseMode
from telegram.ext import Updater, CallbackContext, CommandHandler
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logger = logging.getLogger(__name__)
# The token you got from @botfather when you created the bot
BOT_TOKEN = "TOKEN"
# This can be your own ID, or one for a developer group/channel.
# You can use the /start command of this bot to see your chat id.
DEVELOPER_CHAT_ID = 123456789
def error_handler(update: Update, context: CallbackContext):
"""Log the error and send a telegram message to notify the developer."""
# Log the error before we do anything else, so we can see it even if something breaks.
logger.error(msg="Exception while handling an update:", exc_info=context.error)
# traceback.format_exception returns the usual python message about an exception, but as a
# list of strings rather than a single string, so we have to join them together.
tb_list = traceback.format_exception(None, context.error, context.error.__traceback__)
tb = ''.join(tb_list)
# Build the message with some markup and additional information about what happened.
# You might need to add some logic to deal with messages longer than the 4096 character limit.
message = (
'An exception was raised while handling an update\n'
'<pre>update = {}</pre>\n\n'
'<pre>context.chat_data = {}</pre>\n\n'
'<pre>context.user_data = {}</pre>\n\n'
'<pre>{}</pre>'
).format(
html.escape(json.dumps(update.to_dict(), indent=2, ensure_ascii=False)),
html.escape(str(context.chat_data)),
html.escape(str(context.user_data)),
html.escape(tb)
)
# Finally, send the message
context.bot.send_message(chat_id=DEVELOPER_CHAT_ID, text=message, parse_mode=ParseMode.HTML)
def bad_command(update: Update, context: CallbackContext):
"""Raise an error to trigger the error handler."""
context.bot.wrong_method_name()
def start(update: Update, context: CallbackContext):
update.effective_message.reply_html('Use /bad_command to cause an error.\n'
'Your chat id is <code>{}</code>.'
.format(update.effective_chat.id))
def main():
# Create the Updater and pass it your bot's token.
# Make sure to set use_context=True to use the new context based callbacks
# Post version 12 this will no longer be necessary
updater = Updater(BOT_TOKEN, use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
# Register the commands...
dp.add_handler(CommandHandler('start', start))
dp.add_handler(CommandHandler('bad_command', bad_command))
# ...and the error handler
dp.add_error_handler(error_handler)
# Start the Bot
updater.start_polling()
# Run the bot until you press Ctrl-C or the process receives SIGINT,
# SIGTERM or SIGABRT. This should be used most of the time, since
# start_polling() is non-blocking and will stop the bot gracefully.
updater.idle()
if __name__ == '__main__':
main()
+2 -10
View File
@@ -34,7 +34,7 @@ def start(update, context):
update.message.reply_text('Hi!')
def help(update, context):
def help_command(update, context):
"""Send a message when the command /help is issued."""
update.message.reply_text('Help!')
@@ -64,11 +64,6 @@ def inlinequery(update, context):
update.inline_query.answer(results)
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, context.error)
def main():
# Create the Updater and pass it your bot's token.
# Make sure to set use_context=True to use the new context based callbacks
@@ -80,14 +75,11 @@ def main():
# on different commands - answer in Telegram
dp.add_handler(CommandHandler("start", start))
dp.add_handler(CommandHandler("help", help))
dp.add_handler(CommandHandler("help", help_command))
# on noncommand i.e message - echo the message on Telegram
dp.add_handler(InlineQueryHandler(inlinequery))
# log all errors
dp.add_error_handler(error)
# Start the Bot
updater.start_polling()
+2 -8
View File
@@ -36,15 +36,10 @@ def button(update, context):
query.edit_message_text(text="Selected option: {}".format(query.data))
def help(update, context):
def help_command(update, context):
update.message.reply_text("Use /start to test this bot.")
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, context.error)
def main():
# Create the Updater and pass it your bot's token.
# Make sure to set use_context=True to use the new context based callbacks
@@ -53,8 +48,7 @@ def main():
updater.dispatcher.add_handler(CommandHandler('start', start))
updater.dispatcher.add_handler(CallbackQueryHandler(button))
updater.dispatcher.add_handler(CommandHandler('help', help))
updater.dispatcher.add_error_handler(error)
updater.dispatcher.add_handler(CommandHandler('help', help_command))
# Start the Bot
updater.start_polling()
-8
View File
@@ -149,11 +149,6 @@ def end(update, context):
return ConversationHandler.END
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, context.error)
def main():
# Create the Updater and pass it your bot's token.
updater = Updater("TOKEN", use_context=True)
@@ -184,9 +179,6 @@ def main():
# updates
dp.add_handler(conv_handler)
# log all errors
dp.add_error_handler(error)
# Start the Bot
updater.start_polling()
+17 -25
View File
@@ -100,14 +100,14 @@ def show_data(update, context):
text = ''
if level == SELF:
for person in user_data[level]:
text += '\nName: {0}, Age: {1}'.format(person.get(NAME, '-'), person.get(AGE, '-'))
text += '\nName: {}, Age: {}'.format(person.get(NAME, '-'), person.get(AGE, '-'))
else:
male, female = _name_switcher(level)
for person in user_data[level]:
gender = female if person[GENDER] == FEMALE else male
text += '\n{0}: Name: {1}, Age: {2}'.format(gender, person.get(NAME, '-'),
person.get(AGE, '-'))
text += '\n{}: Name: {}, Age: {}'.format(gender, person.get(NAME, '-'),
person.get(AGE, '-'))
return text
ud = context.user_data
@@ -267,12 +267,6 @@ def stop_nested(update, context):
return STOPPING
# Error handler
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, context.error)
def main():
# Create the Updater and pass it your bot's token.
# Make sure to set use_context=True to use the new context based callbacks
@@ -290,7 +284,7 @@ def main():
states={
SELECTING_FEATURE: [CallbackQueryHandler(ask_for_input,
pattern='^(?!' + str(END) + ').*$')],
TYPING: [MessageHandler(Filters.text, save_input)],
TYPING: [MessageHandler(Filters.text & ~Filters.command, save_input)],
},
fallbacks=[
@@ -313,8 +307,8 @@ def main():
states={
SELECTING_LEVEL: [CallbackQueryHandler(select_gender,
pattern='^{0}$|^{1}$'.format(str(PARENTS),
str(CHILDREN)))],
pattern='^{}$|^{}$'.format(str(PARENTS),
str(CHILDREN)))],
SELECTING_GENDER: [description_conv]
},
@@ -335,32 +329,30 @@ def main():
)
# Set up top level ConversationHandler (selecting action)
# Because the states of the third level conversation map to the ones of the econd level
# conversation, we need to make sure the top level conversation can also handle them
selection_handlers = [
add_member_conv,
CallbackQueryHandler(show_data, pattern='^' + str(SHOWING) + '$'),
CallbackQueryHandler(adding_self, pattern='^' + str(ADDING_SELF) + '$'),
CallbackQueryHandler(end, pattern='^' + str(END) + '$'),
]
conv_handler = ConversationHandler(
entry_points=[CommandHandler('start', start)],
states={
SHOWING: [CallbackQueryHandler(start, pattern='^' + str(END) + '$')],
SELECTING_ACTION: [
add_member_conv,
CallbackQueryHandler(show_data, pattern='^' + str(SHOWING) + '$'),
CallbackQueryHandler(adding_self, pattern='^' + str(ADDING_SELF) + '$'),
CallbackQueryHandler(end, pattern='^' + str(END) + '$'),
],
SELECTING_ACTION: selection_handlers,
SELECTING_LEVEL: selection_handlers,
DESCRIBING_SELF: [description_conv],
STOPPING: [CommandHandler('start', start)],
},
fallbacks=[CommandHandler('stop', stop)],
)
# Because the states of the third level conversation map to the ones of the
# second level conversation, we need to be a bit hacky about that:
conv_handler.states[SELECTING_LEVEL] = conv_handler.states[SELECTING_ACTION]
conv_handler.states[STOPPING] = conv_handler.entry_points
dp.add_handler(conv_handler)
# log all errors
dp.add_error_handler(error)
# Start the Bot
updater.start_polling()
-8
View File
@@ -77,11 +77,6 @@ def msg(update, context):
actual_file.download()
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, context.error)
def main():
"""Start the bot."""
# Create the Updater and pass it your token and private key
@@ -93,9 +88,6 @@ def main():
# On messages that include passport data call msg
dp.add_handler(MessageHandler(Filters.passport_data, msg))
# log all errors
dp.add_error_handler(error)
# Start the Bot
updater.start_polling()
-8
View File
@@ -19,11 +19,6 @@ logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s
logger = logging.getLogger(__name__)
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, context.error)
def start_callback(update, context):
msg = "Use /shipping to get an invoice for shipping-payment, "
msg += "or /noshipping for an invoice without shipping."
@@ -134,9 +129,6 @@ def main():
# Success! Notify your user!
dp.add_handler(MessageHandler(Filters.successful_payment, successful_payment_callback))
# log all errors
dp.add_error_handler(error)
# Start the Bot
updater.start_polling()
+6 -13
View File
@@ -107,11 +107,6 @@ def done(update, context):
return ConversationHandler.END
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, context.error)
def main():
# Create the Updater and pass it your bot's token.
pp = PicklePersistence(filename='conversationbot')
@@ -131,13 +126,13 @@ def main():
custom_choice),
],
TYPING_CHOICE: [MessageHandler(Filters.text,
regular_choice),
],
TYPING_CHOICE: [
MessageHandler(Filters.text & ~(Filters.command | Filters.regex('^Done$')),
regular_choice)],
TYPING_REPLY: [MessageHandler(Filters.text,
received_information),
],
TYPING_REPLY: [
MessageHandler(Filters.text & ~(Filters.command | Filters.regex('^Done$')),
received_information)],
},
fallbacks=[MessageHandler(Filters.regex('^Done$'), done)],
@@ -149,8 +144,6 @@ def main():
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()
+3 -4
View File
@@ -13,7 +13,6 @@ from telegram import (Poll, ParseMode, KeyboardButton, KeyboardButtonPollType,
ReplyKeyboardMarkup, ReplyKeyboardRemove)
from telegram.ext import (Updater, CommandHandler, PollAnswerHandler, PollHandler, MessageHandler,
Filters)
from telegram.utils.helpers import mention_html
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
@@ -29,7 +28,7 @@ def start(update, context):
def poll(update, context):
"""Sends a predefined poll"""
questions = ["Good", "Really good", "Fantastic", "Great"]
message = context.bot.send_poll(update.effective_user.id, "How are you?", questions,
message = context.bot.send_poll(update.effective_chat.id, "How are you?", questions,
is_anonymous=False, allows_multiple_answers=True)
# Save some info about the poll the bot_data for later use in receive_poll_answer
payload = {message.poll.id: {"questions": questions, "message_id": message.message_id,
@@ -53,9 +52,9 @@ def receive_poll_answer(update, context):
answer_string += questions[question_id] + " and "
else:
answer_string += questions[question_id]
user_mention = mention_html(update.effective_user.id, update.effective_user.full_name)
context.bot.send_message(context.bot_data[poll_id]["chat_id"],
"{} feels {}!".format(user_mention, answer_string),
"{} feels {}!".format(update.effective_user.mention_html(),
answer_string),
parse_mode=ParseMode.HTML)
context.bot_data[poll_id]["answers"] += 1
# Close poll after three participants voted
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Simple Bot to reply to Telegram messages.
This is built on the API wrapper, see rawapibot.py to see the same example built
on the telegram.ext bot framework.
This program is dedicated to the public domain under the CC0 license.
"""
import logging
import telegram
from telegram.error import NetworkError, Unauthorized
from time import sleep
update_id = None
def main():
"""Run the bot."""
global update_id
# Telegram Bot Authorization Token
bot = telegram.Bot('TOKEN')
# get the first pending update_id, this is so we can skip over it in case
# we get an "Unauthorized" exception.
try:
update_id = bot.get_updates()[0].update_id
except IndexError:
update_id = None
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
while True:
try:
echo(bot)
except NetworkError:
sleep(1)
except Unauthorized:
# The user has removed or blocked the bot.
update_id += 1
def echo(bot):
"""Echo the message the user sent."""
global update_id
# Request updates after the last update_id
for update in bot.get_updates(offset=update_id, timeout=10):
update_id = update.update_id + 1
if update.message: # your bot can receive updates without messages
# Reply to the message
update.message.reply_text(update.message.text)
if __name__ == '__main__':
main()
-8
View File
@@ -77,11 +77,6 @@ def unset(update, context):
update.message.reply_text('Timer successfully unset!')
def error(update, context):
"""Log Errors caused by Updates."""
logger.warning('Update "%s" caused error "%s"', update, context.error)
def main():
"""Run bot."""
# Create the Updater and pass it your bot's token.
@@ -101,9 +96,6 @@ def main():
pass_chat_data=True))
dp.add_handler(CommandHandler("unset", unset, pass_chat_data=True))
# log all errors
dp.add_error_handler(error)
# Start the Bot
updater.start_polling()
+1
View File
@@ -3,6 +3,7 @@ pep257
pylint
flaky
yapf
mypy==0.770
pre-commit
beautifulsoup4
pytest==4.2.0
+1 -1
View File
@@ -1,5 +1,5 @@
future>=0.16.0
certifi
tornado>=5.1
cryptography
decorator>=4.4.0
APScheduler==3.6.3
+19
View File
@@ -40,3 +40,22 @@ omit =
telegram/__main__.py
telegram/vendor/*
[coverage:report]
exclude_lines =
if TYPE_CHECKING:
[mypy]
warn_unused_ignores = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
disallow_untyped_decorators = True
show_error_codes = True
[mypy-telegram.vendor.*]
ignore_errors = True
# Disable strict optional for telegram objects with class methods
# We don't want to clutter the code with 'if self.bot is None: raise RuntimeError()'
[mypy-telegram.callbackquery,telegram.chat,telegram.message,telegram.user,telegram.files.*,telegram.inline.inlinequery,telegram.payment.precheckoutquery,telegram.payment.shippingquery,telegram.passport.passportdata,telegram.passport.credentials,telegram.passport.passportfile,telegram.ext.filters]
strict_optional = False
-1
View File
@@ -61,7 +61,6 @@ with codecs.open('README.rst', 'r', 'utf-8') as fd:
'Topic :: Internet',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
+5 -2
View File
@@ -19,6 +19,7 @@
"""A library that provides a Python interface to the Telegram Bot API"""
from .base import TelegramObject
from .botcommand import BotCommand
from .user import User
from .files.chatphoto import ChatPhoto
from .chat import Chat
@@ -36,6 +37,7 @@ from .files.location import Location
from .files.venue import Venue
from .files.videonote import VideoNote
from .chataction import ChatAction
from .dice import Dice
from .userprofilephotos import UserProfilePhotos
from .keyboardbutton import KeyboardButton
from .keyboardbuttonpolltype import KeyboardButtonPollType
@@ -102,7 +104,6 @@ from .games.gamehighscore import GameHighScore
from .update import Update
from .files.inputmedia import (InputMedia, InputMediaVideo, InputMediaPhoto, InputMediaAnimation,
InputMediaAudio, InputMediaDocument)
from .bot import Bot
from .constants import (MAX_MESSAGE_LENGTH, MAX_CAPTION_LENGTH, SUPPORTED_WEBHOOK_PORTS,
MAX_FILESIZE_DOWNLOAD, MAX_FILESIZE_UPLOAD,
MAX_MESSAGES_PER_SECOND_PER_CHAT, MAX_MESSAGES_PER_SECOND,
@@ -122,6 +123,7 @@ from .passport.credentials import (Credentials,
SecureData,
FileCredentials,
TelegramDecryptionError)
from .bot import Bot
from .version import __version__ # noqa: F401
__author__ = 'devs@python-telegram-bot.org'
@@ -157,5 +159,6 @@ __all__ = [
'InputMediaAudio', 'InputMediaDocument', 'TelegramDecryptionError',
'PassportElementErrorSelfie', 'PassportElementErrorTranslationFile',
'PassportElementErrorTranslationFiles', 'PassportElementErrorUnspecified', 'Poll',
'PollOption', 'PollAnswer', 'LoginUrl', 'KeyboardButton', 'KeyboardButtonPollType',
'PollOption', 'PollAnswer', 'LoginUrl', 'KeyboardButton', 'KeyboardButtonPollType', 'Dice',
'BotCommand'
]
+8 -9
View File
@@ -20,13 +20,13 @@ import sys
import subprocess
import certifi
import future
from typing import Optional
from . import __version__ as telegram_ver
def _git_revision():
def _git_revision() -> Optional[str]:
try:
output = subprocess.check_output(["git", "describe", "--long", "--tags"],
stderr=subprocess.STDOUT)
@@ -35,16 +35,15 @@ def _git_revision():
return output.decode().strip()
def print_ver_info():
def print_ver_info() -> None:
git_revision = _git_revision()
print('python-telegram-bot {0}'.format(telegram_ver) + (' ({0})'.format(git_revision)
if git_revision else ''))
print('certifi {0}'.format(certifi.__version__))
print('future {0}'.format(future.__version__))
print('Python {0}'.format(sys.version.replace('\n', ' ')))
print('python-telegram-bot {}'.format(telegram_ver) + (' ({})'.format(git_revision)
if git_revision else ''))
print('certifi {}'.format(certifi.__version__)) # type: ignore[attr-defined]
print('Python {}'.format(sys.version.replace('\n', ' ')))
def main():
def main() -> None:
print_ver_info()
+51 -23
View File
@@ -17,37 +17,64 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""Base class for Telegram Objects."""
try:
import ujson as json
except ImportError:
import json
import json # type: ignore[no-redef]
from abc import ABCMeta
import warnings
from telegram.utils.types import JSONDict
from typing import Tuple, Any, Optional, Type, TypeVar, TYPE_CHECKING, List
if TYPE_CHECKING:
from telegram import Bot
TO = TypeVar('TO', bound='TelegramObject', covariant=True)
class TelegramObject(object):
class TelegramObject:
"""Base class for most telegram objects."""
__metaclass__ = ABCMeta
_id_attrs = ()
# def __init__(self, *args: Any, **kwargs: Any):
# pass
def __str__(self):
_id_attrs: Tuple[Any, ...] = ()
def __str__(self) -> str:
return str(self.to_dict())
def __getitem__(self, item):
def __getitem__(self, item: str) -> Any:
return self.__dict__[item]
@staticmethod
def parse_data(data: Optional[JSONDict]) -> Optional[JSONDict]:
if not data:
return None
return data.copy()
@classmethod
def de_json(cls, data, bot):
def de_json(cls: Type[TO], data: Optional[JSONDict], bot: 'Bot') -> Optional[TO]:
data = cls.parse_data(data)
if not data:
return None
data = data.copy()
if cls == TelegramObject:
return cls()
else:
return cls(bot=bot, **data) # type: ignore[call-arg]
return data
@classmethod
def de_list(cls: Type[TO],
data: Optional[List[JSONDict]],
bot: 'Bot') -> List[Optional[TO]]:
if not data:
return []
def to_json(self):
return [cls.de_json(d, bot) for d in data]
def to_json(self) -> str:
"""
Returns:
:obj:`str`
@@ -56,16 +83,11 @@ class TelegramObject(object):
return json.dumps(self.to_dict())
def to_dict(self):
def to_dict(self) -> JSONDict:
data = dict()
for key in iter(self.__dict__):
if key in ('bot',
'_id_attrs',
'_credentials',
'_decrypted_credentials',
'_decrypted_data',
'_decrypted_secret'):
if key == 'bot' or key.startswith('_'):
continue
value = self.__dict__[key]
@@ -79,12 +101,18 @@ class TelegramObject(object):
data['from'] = data.pop('from_user', None)
return data
def __eq__(self, other):
def __eq__(self, other: object) -> bool:
if isinstance(other, self.__class__):
if self._id_attrs == ():
warnings.warn("Objects of type {} can not be meaningfully tested for "
"equivalence.".format(self.__class__.__name__))
if other._id_attrs == ():
warnings.warn("Objects of type {} can not be meaningfully tested for "
"equivalence.".format(other.__class__.__name__))
return self._id_attrs == other._id_attrs
return super(TelegramObject, self).__eq__(other) # pylint: disable=no-member
return super().__eq__(other) # pylint: disable=no-member
def __hash__(self):
def __hash__(self) -> int:
if self._id_attrs:
return hash((self.__class__, self._id_attrs)) # pylint: disable=no-member
return super(TelegramObject, self).__hash__()
return super().__hash__()
+1556 -1114
View File
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python
# pylint: disable=R0903
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2020
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Bot Command."""
from telegram import TelegramObject
from typing import Any
class BotCommand(TelegramObject):
"""
This object represents a bot command.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`command` and :attr:`description` are equal.
Attributes:
command (:obj:`str`): Text of the command.
description (:obj:`str`): Description of the command.
Args:
command (:obj:`str`): Text of the command, 1-32 characters. Can contain only lowercase
English letters, digits and underscores.
description (:obj:`str`): Description of the command, 3-256 characters.
"""
def __init__(self, command: str, description: str, **kwargs: Any):
self.command = command
self.description = description
self._id_attrs = (self.command, self.description)
+175 -30
View File
@@ -17,9 +17,14 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram CallbackQuery"""
from telegram import TelegramObject, Message, User
from telegram.utils.types import JSONDict
from typing import Optional, Any, Union, TYPE_CHECKING, List
if TYPE_CHECKING:
from telegram import Bot, InlineKeyboardMarkup, GameHighScore
class CallbackQuery(TelegramObject):
"""
@@ -29,6 +34,9 @@ class CallbackQuery(TelegramObject):
:attr:`message` will be present. If the button was attached to a message sent via the bot (in
inline mode), the field :attr:`inline_message_id` will be present.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`id` is equal.
Note:
* In Python `from` is a reserved word, use `from_user` instead.
* Exactly one of the fields :attr:`data` or :attr:`game_short_name` will be present.
@@ -71,15 +79,15 @@ class CallbackQuery(TelegramObject):
"""
def __init__(self,
id,
from_user,
chat_instance,
message=None,
data=None,
inline_message_id=None,
game_short_name=None,
bot=None,
**kwargs):
id: str,
from_user: User,
chat_instance: str,
message: Message = None,
data: str = None,
inline_message_id: str = None,
game_short_name: str = None,
bot: 'Bot' = None,
**kwargs: Any):
# Required
self.id = id
self.from_user = from_user
@@ -95,32 +103,29 @@ class CallbackQuery(TelegramObject):
self._id_attrs = (self.id,)
@classmethod
def de_json(cls, data, bot):
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['CallbackQuery']:
data = cls.parse_data(data)
if not data:
return None
data = super(CallbackQuery, cls).de_json(data, bot)
data['from_user'] = User.de_json(data.get('from'), bot)
message = data.get('message')
if message:
message['default_quote'] = data.get('default_quote')
data['message'] = Message.de_json(message, bot)
data['message'] = Message.de_json(data.get('message'), bot)
return cls(bot=bot, **data)
def answer(self, *args, **kwargs):
def answer(self, *args: Any, **kwargs: Any) -> bool:
"""Shortcut for::
bot.answer_callback_query(update.callback_query.id, *args, **kwargs)
Returns:
:obj:`bool`: On success, ``True`` is returned.
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.answerCallbackQuery(self.id, *args, **kwargs)
return self.bot.answer_callback_query(self.id, *args, **kwargs)
def edit_message_text(self, text, *args, **kwargs):
def edit_message_text(self, text: str, *args: Any, **kwargs: Any) -> Union[Message, bool]:
"""Shortcut for either::
bot.edit_message_text(text, chat_id=update.callback_query.message.chat_id,
@@ -134,7 +139,7 @@ class CallbackQuery(TelegramObject):
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
edited Message is returned, otherwise ``True`` is returned.
edited Message is returned, otherwise :obj:`True` is returned.
"""
if self.inline_message_id:
@@ -144,7 +149,8 @@ class CallbackQuery(TelegramObject):
return self.bot.edit_message_text(text, chat_id=self.message.chat_id,
message_id=self.message.message_id, *args, **kwargs)
def edit_message_caption(self, caption, *args, **kwargs):
def edit_message_caption(self, caption: str, *args: Any,
**kwargs: Any) -> Union[Message, bool]:
"""Shortcut for either::
bot.edit_message_caption(caption=caption,
@@ -160,7 +166,7 @@ class CallbackQuery(TelegramObject):
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
edited Message is returned, otherwise ``True`` is returned.
edited Message is returned, otherwise :obj:`True` is returned.
"""
if self.inline_message_id:
@@ -172,13 +178,14 @@ class CallbackQuery(TelegramObject):
message_id=self.message.message_id,
*args, **kwargs)
def edit_message_reply_markup(self, reply_markup, *args, **kwargs):
def edit_message_reply_markup(self, reply_markup: 'InlineKeyboardMarkup', *args: Any,
**kwargs: Any) -> Union[Message, bool]:
"""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)
bot.edit_message_reply_markup(chat_id=update.callback_query.message.chat_id,
message_id=update.callback_query.message.message_id,
reply_markup=reply_markup,
*args, **kwargs)
or::
@@ -188,7 +195,7 @@ class CallbackQuery(TelegramObject):
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
edited Message is returned, otherwise ``True`` is returned.
edited Message is returned, otherwise :obj:`True` is returned.
"""
if self.inline_message_id:
@@ -200,3 +207,141 @@ class CallbackQuery(TelegramObject):
chat_id=self.message.chat_id,
message_id=self.message.message_id,
*args, **kwargs)
def edit_message_media(self, *args: Any, **kwargs: Any) -> Union[Message, bool]:
"""Shortcut for either::
bot.edit_message_media(chat_id=update.callback_query.message.chat_id,
message_id=update.callback_query.message.message_id,
media=media,
*args, **kwargs)
or::
bot.edit_message_media(inline_message_id=update.callback_query.inline_message_id,
media=media,
*args, **kwargs)
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
edited Message is returned, otherwise :obj:`True` is returned.
"""
if self.inline_message_id:
return self.bot.edit_message_media(inline_message_id=self.inline_message_id,
*args, **kwargs)
else:
return self.bot.edit_message_media(chat_id=self.message.chat_id,
message_id=self.message.message_id,
*args, **kwargs)
def edit_message_live_location(self, *args: Any, **kwargs: Any) -> Union[Message, bool]:
"""Shortcut for either::
bot.edit_message_live_location(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_live_location(
inline_message_id=update.callback_query.inline_message_id,
reply_markup=reply_markup,
*args, **kwargs
)
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
edited Message is returned, otherwise :obj:`True` is returned.
"""
if self.inline_message_id:
return self.bot.edit_message_live_location(inline_message_id=self.inline_message_id,
*args, **kwargs)
else:
return self.bot.edit_message_live_location(chat_id=self.message.chat_id,
message_id=self.message.message_id,
*args, **kwargs)
def stop_message_live_location(self, *args: Any, **kwargs: Any) -> Union[Message, bool]:
"""Shortcut for either::
bot.stop_message_live_location(chat_id=update.callback_query.message.chat_id,
message_id=update.callback_query.message.message_id,
reply_markup=reply_markup,
*args, **kwargs)
or::
bot.stop_message_live_location(
inline_message_id=update.callback_query.inline_message_id,
reply_markup=reply_markup,
*args, **kwargs
)
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
edited Message is returned, otherwise :obj:`True` is returned.
"""
if self.inline_message_id:
return self.bot.stop_message_live_location(inline_message_id=self.inline_message_id,
*args, **kwargs)
else:
return self.bot.stop_message_live_location(chat_id=self.message.chat_id,
message_id=self.message.message_id,
*args, **kwargs)
def set_game_score(self, *args: Any, **kwargs: Any) -> Union[Message, bool]:
"""Shortcut for either::
bot.set_game_score(chat_id=update.callback_query.message.chat_id,
message_id=update.callback_query.message.message_id,
reply_markup=reply_markup,
*args, **kwargs)
or::
bot.set_game_score(inline_message_id=update.callback_query.inline_message_id,
reply_markup=reply_markup,
*args, **kwargs)
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
edited Message is returned, otherwise :obj:`True` is returned.
"""
if self.inline_message_id:
return self.bot.set_game_score(inline_message_id=self.inline_message_id,
*args, **kwargs)
else:
return self.bot.set_game_score(chat_id=self.message.chat_id,
message_id=self.message.message_id,
*args, **kwargs)
def get_game_high_scores(self, *args: Any, **kwargs: Any) -> List['GameHighScore']:
"""Shortcut for either::
bot.get_game_high_scores(chat_id=update.callback_query.message.chat_id,
message_id=update.callback_query.message.message_id,
reply_markup=reply_markup,
*args, **kwargs)
or::
bot.get_game_high_scores(inline_message_id=update.callback_query.inline_message_id,
reply_markup=reply_markup,
*args, **kwargs)
Returns:
List[:class:`telegram.GameHighScore`]
"""
if self.inline_message_id:
return self.bot.get_game_high_scores(inline_message_id=self.inline_message_id,
*args, **kwargs)
else:
return self.bot.get_game_high_scores(chat_id=self.message.chat_id,
message_id=self.message.message_id,
*args, **kwargs)
+182 -111
View File
@@ -22,10 +22,18 @@
from telegram import TelegramObject, ChatPhoto
from .chatpermissions import ChatPermissions
from telegram.utils.types import JSONDict
from typing import Any, Optional, List, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot, Message, ChatMember
class Chat(TelegramObject):
"""This object represents a chat.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`id` is equal.
Attributes:
id (:obj:`int`): Unique identifier for this chat.
type (:obj:`str`): Type of chat.
@@ -37,13 +45,14 @@ class Chat(TelegramObject):
description (:obj:`str`): Optional. Description, for groups, supergroups and channel chats.
invite_link (:obj:`str`): Optional. Chat invite link, for supergroups and channel chats.
pinned_message (:class:`telegram.Message`): Optional. Pinned message, for supergroups.
Returned only in get_chat.
permissions (:class:`telegram.ChatPermission`): Optional. Default chat member permissions,
for groups and supergroups. Returned only in getChat.
Returned only in :meth:`telegram.Bot.get_chat`.
permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions,
for groups and supergroups. Returned only in :meth:`telegram.Bot.get_chat`.
slow_mode_delay (:obj:`int`): Optional. For supergroups, the minimum allowed delay between
consecutive messages sent by each unpriviledged user. Returned only in getChat.
consecutive messages sent by each unprivileged user. Returned only in
:meth:`telegram.Bot.get_chat`.
sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set.
can_set_sticker_set (:obj:`bool`): Optional. ``True``, if the bot can change group the
can_set_sticker_set (:obj:`bool`): Optional. :obj:`True`, if the bot can change group the
sticker set.
Args:
@@ -58,52 +67,56 @@ class Chat(TelegramObject):
available.
first_name(:obj:`str`, optional): First name of the other party in a private chat.
last_name(:obj:`str`, optional): Last name of the other party in a private chat.
photo (:class:`telegram.ChatPhoto`, optional): Chat photo. Returned only in getChat.
photo (:class:`telegram.ChatPhoto`, optional): Chat photo.
Returned only in :meth:`telegram.Bot.get_chat`.
description (:obj:`str`, optional): Description, for groups, supergroups and channel chats.
Returned only in get_chat.
invite_link (:obj:`str`, optional): Chat invite link, for supergroups and channel chats.
Returned only in get_chat.
pinned_message (:class:`telegram.Message`, optional): Pinned message, for supergroups.
Returned only in get_chat.
permissions (:class:`telegram.ChatPermission`): Optional. Default chat member permissions,
for groups and supergroups. Returned only in getChat.
Returned only in :meth:`telegram.Bot.get_chat`.
invite_link (:obj:`str`, optional): Chat invite link, for groups, supergroups and channel
chats. Each administrator in a chat generates their own invite links, so the bot must
first generate the link using ``export_chat_invite_link()``. Returned only
in :meth:`telegram.Bot.get_chat`.
pinned_message (:class:`telegram.Message`, optional): Pinned message, for groups,
supergroups and channels. Returned only in :meth:`telegram.Bot.get_chat`.
permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions,
for groups and supergroups. Returned only in :meth:`telegram.Bot.get_chat`.
slow_mode_delay (:obj:`int`, optional): For supergroups, the minimum allowed delay between
consecutive messages sent by each unpriviledged user. Returned only in getChat.
consecutive messages sent by each unprivileged user.
Returned only in :meth:`telegram.Bot.get_chat`.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
sticker_set_name (:obj:`str`, optional): For supergroups, name of Group sticker set.
Returned only in get_chat.
can_set_sticker_set (:obj:`bool`, optional): ``True``, if the bot can change group the
sticker set. Returned only in get_chat.
sticker_set_name (:obj:`str`, optional): For supergroups, name of group sticker set.
Returned only in :meth:`telegram.Bot.get_chat`.
can_set_sticker_set (:obj:`bool`, optional): :obj:`True`, if the bot can change group the
sticker set. Returned only in :meth:`telegram.Bot.get_chat`.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
PRIVATE = 'private'
PRIVATE: str = 'private'
""":obj:`str`: 'private'"""
GROUP = 'group'
GROUP: str = 'group'
""":obj:`str`: 'group'"""
SUPERGROUP = 'supergroup'
SUPERGROUP: str = 'supergroup'
""":obj:`str`: 'supergroup'"""
CHANNEL = 'channel'
CHANNEL: str = 'channel'
""":obj:`str`: 'channel'"""
def __init__(self,
id,
type,
title=None,
username=None,
first_name=None,
last_name=None,
bot=None,
photo=None,
description=None,
invite_link=None,
pinned_message=None,
permissions=None,
sticker_set_name=None,
can_set_sticker_set=None,
slow_mode_delay=None,
**kwargs):
id: int,
type: str,
title: str = None,
username: str = None,
first_name: str = None,
last_name: str = None,
bot: 'Bot' = None,
photo: ChatPhoto = None,
description: str = None,
invite_link: str = None,
pinned_message: 'Message' = None,
permissions: ChatPermissions = None,
sticker_set_name: str = None,
can_set_sticker_set: bool = None,
slow_mode_delay: int = None,
**kwargs: Any):
# Required
self.id = int(id)
self.type = type
@@ -127,7 +140,7 @@ class Chat(TelegramObject):
self._id_attrs = (self.id,)
@property
def link(self):
def link(self) -> Optional[str]:
""":obj:`str`: Convenience property. If the chat has a :attr:`username`, returns a t.me
link of the chat."""
if self.username:
@@ -135,36 +148,23 @@ class Chat(TelegramObject):
return None
@classmethod
def de_json(cls, data, bot):
def de_json(cls, data: JSONDict, bot: 'Bot') -> Optional['Chat']:
data = cls.parse_data(data)
if not data:
return None
data['photo'] = ChatPhoto.de_json(data.get('photo'), bot)
from telegram import Message
pinned_message = data.get('pinned_message')
if pinned_message:
pinned_message['default_quote'] = data.get('default_quote')
data['pinned_message'] = Message.de_json(pinned_message, bot)
data['pinned_message'] = Message.de_json(data.get('pinned_message'), bot)
data['permissions'] = ChatPermissions.de_json(data.get('permissions'), bot)
return cls(bot=bot, **data)
def send_action(self, *args, **kwargs):
def leave(self, *args: Any, **kwargs: Any) -> bool:
"""Shortcut for::
bot.send_chat_action(update.message.chat.id, *args, **kwargs)
Returns:
:obj:`bool`: If the action was sent successfully.
"""
return self.bot.send_chat_action(self.id, *args, **kwargs)
def leave(self, *args, **kwargs):
"""Shortcut for::
bot.leave_chat(update.message.chat.id, *args, **kwargs)
bot.leave_chat(update.effective_chat.id, *args, **kwargs)
Returns:
:obj:`bool` If the action was sent successfully.
@@ -172,24 +172,24 @@ class Chat(TelegramObject):
"""
return self.bot.leave_chat(self.id, *args, **kwargs)
def get_administrators(self, *args, **kwargs):
def get_administrators(self, *args: Any, **kwargs: Any) -> List['ChatMember']:
"""Shortcut for::
bot.get_chat_administrators(update.message.chat.id, *args, **kwargs)
bot.get_chat_administrators(update.effective_chat.id, *args, **kwargs)
Returns:
List[:class:`telegram.ChatMember`]: A list of administrators in a chat. An Array of
:class:`telegram.ChatMember` objects that contains information about all
chat administrators except other bots. If the chat is a group or a supergroup
and no administrators were appointed, only the creator will be returned
and no administrators were appointed, only the creator will be returned.
"""
return self.bot.get_chat_administrators(self.id, *args, **kwargs)
def get_members_count(self, *args, **kwargs):
def get_members_count(self, *args: Any, **kwargs: Any) -> int:
"""Shortcut for::
bot.get_chat_members_count(update.message.chat.id, *args, **kwargs)
bot.get_chat_members_count(update.effective_chat.id, *args, **kwargs)
Returns:
:obj:`int`
@@ -197,10 +197,10 @@ class Chat(TelegramObject):
"""
return self.bot.get_chat_members_count(self.id, *args, **kwargs)
def get_member(self, *args, **kwargs):
def get_member(self, *args: Any, **kwargs: Any) -> 'ChatMember':
"""Shortcut for::
bot.get_chat_member(update.message.chat.id, *args, **kwargs)
bot.get_chat_member(update.effective_chat.id, *args, **kwargs)
Returns:
:class:`telegram.ChatMember`
@@ -208,13 +208,13 @@ class Chat(TelegramObject):
"""
return self.bot.get_chat_member(self.id, *args, **kwargs)
def kick_member(self, *args, **kwargs):
def kick_member(self, *args: Any, **kwargs: Any) -> bool:
"""Shortcut for::
bot.kick_chat_member(update.message.chat.id, *args, **kwargs)
bot.kick_chat_member(update.effective_chat.id, *args, **kwargs)
Returns:
:obj:`bool`: If the action was sent succesfully.
:obj:`bool`: If the action was sent successfully.
Note:
This method will only work if the `All Members Are Admins` setting is off in the
@@ -224,10 +224,10 @@ class Chat(TelegramObject):
"""
return self.bot.kick_chat_member(self.id, *args, **kwargs)
def unban_member(self, *args, **kwargs):
def unban_member(self, *args: Any, **kwargs: Any) -> bool:
"""Shortcut for::
bot.unban_chat_member(update.message.chat.id, *args, **kwargs)
bot.unban_chat_member(update.effective_chat.id, *args, **kwargs)
Returns:
:obj:`bool`: If the action was sent successfully.
@@ -235,21 +235,21 @@ class Chat(TelegramObject):
"""
return self.bot.unban_chat_member(self.id, *args, **kwargs)
def set_permissions(self, *args, **kwargs):
def set_permissions(self, *args: Any, **kwargs: Any) -> bool:
"""Shortcut for::
bot.set_chat_permissions(update.message.chat.id, *args, **kwargs)
bot.set_chat_permissions(update.effective_chat.id, *args, **kwargs)
Returns:
:obj:`bool`: If the action was sent successfully.
:obj:`bool`: If the action was sent successfully.
"""
return self.bot.set_chat_permissions(self.id, *args, **kwargs)
def set_administrator_custom_title(self, *args, **kwargs):
def set_administrator_custom_title(self, *args: Any, **kwargs: Any) -> bool:
"""Shortcut for::
bot.set_chat_administrator_custom_title(update.message.chat.id, *args, **kwargs)
bot.set_chat_administrator_custom_title(update.effective_chat.id, *args, **kwargs)
Returns:
:obj:`bool`: If the action was sent successfully.
@@ -257,12 +257,10 @@ class Chat(TelegramObject):
"""
return self.bot.set_chat_administrator_custom_title(self.id, *args, **kwargs)
def send_message(self, *args, **kwargs):
def send_message(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
bot.send_message(Chat.id, *args, **kwargs)
Where Chat is the current instance.
bot.send_message(update.effective_chat.id, *args, **kwargs)
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
@@ -270,12 +268,35 @@ class Chat(TelegramObject):
"""
return self.bot.send_message(self.id, *args, **kwargs)
def send_photo(self, *args, **kwargs):
def send_media_group(self, *args: Any, **kwargs: Any) -> List['Message']:
"""Shortcut for::
bot.send_photo(Chat.id, *args, **kwargs)
bot.send_media_group(update.effective_chat.id, *args, **kwargs)
Where Chat is the current instance.
Returns:
List[:class:`telegram.Message`:] On success, instance representing the message posted.
"""
return self.bot.send_media_group(self.id, *args, **kwargs)
def send_chat_action(self, *args: Any, **kwargs: Any) -> bool:
"""Shortcut for::
bot.send_chat_action(update.effective_chat.id, *args, **kwargs)
Returns:
:obj:`True`: On success.
"""
return self.bot.send_chat_action(self.id, *args, **kwargs)
send_action = send_chat_action
"""Alias for :attr:`send_chat_action`"""
def send_photo(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
bot.send_photo(update.effective_chat.id, *args, **kwargs)
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
@@ -283,12 +304,21 @@ class Chat(TelegramObject):
"""
return self.bot.send_photo(self.id, *args, **kwargs)
def send_audio(self, *args, **kwargs):
def send_contact(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
bot.send_audio(Chat.id, *args, **kwargs)
bot.send_contact(update.effective_chat.id, *args, **kwargs)
Where Chat is the current instance.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
return self.bot.send_contact(self.id, *args, **kwargs)
def send_audio(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
bot.send_audio(update.effective_chat.id, *args, **kwargs)
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
@@ -296,12 +326,10 @@ class Chat(TelegramObject):
"""
return self.bot.send_audio(self.id, *args, **kwargs)
def send_document(self, *args, **kwargs):
def send_document(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
bot.send_document(Chat.id, *args, **kwargs)
Where Chat is the current instance.
bot.send_document(update.effective_chat.id, *args, **kwargs)
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
@@ -309,12 +337,54 @@ class Chat(TelegramObject):
"""
return self.bot.send_document(self.id, *args, **kwargs)
def send_animation(self, *args, **kwargs):
def send_dice(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
bot.send_animation(Chat.id, *args, **kwargs)
bot.send_dice(update.effective_chat.id, *args, **kwargs)
Where Chat is the current instance.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
return self.bot.send_dice(self.id, *args, **kwargs)
def send_game(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
bot.send_game(update.effective_chat.id, *args, **kwargs)
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
return self.bot.send_game(self.id, *args, **kwargs)
def send_invoice(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
bot.send_invoice(update.effective_chat.id, *args, **kwargs)
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
return self.bot.send_invoice(self.id, *args, **kwargs)
def send_location(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
bot.send_location(update.effective_chat.id, *args, **kwargs)
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
return self.bot.send_location(self.id, *args, **kwargs)
def send_animation(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
bot.send_animation(update.effective_chat.id, *args, **kwargs)
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
@@ -322,12 +392,10 @@ class Chat(TelegramObject):
"""
return self.bot.send_animation(self.id, *args, **kwargs)
def send_sticker(self, *args, **kwargs):
def send_sticker(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
bot.send_sticker(Chat.id, *args, **kwargs)
Where Chat is the current instance.
bot.send_sticker(update.effective_chat.id, *args, **kwargs)
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
@@ -335,12 +403,21 @@ class Chat(TelegramObject):
"""
return self.bot.send_sticker(self.id, *args, **kwargs)
def send_video(self, *args, **kwargs):
def send_venue(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
bot.send_video(Chat.id, *args, **kwargs)
bot.send_venue(update.effective_chat.id, *args, **kwargs)
Where Chat is the current instance.
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
return self.bot.send_venue(self.id, *args, **kwargs)
def send_video(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
bot.send_video(update.effective_chat.id, *args, **kwargs)
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
@@ -348,12 +425,10 @@ class Chat(TelegramObject):
"""
return self.bot.send_video(self.id, *args, **kwargs)
def send_video_note(self, *args, **kwargs):
def send_video_note(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
bot.send_video_note(Chat.id, *args, **kwargs)
Where Chat is the current instance.
bot.send_video_note(update.effective_chat.id, *args, **kwargs)
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
@@ -361,12 +436,10 @@ class Chat(TelegramObject):
"""
return self.bot.send_video_note(self.id, *args, **kwargs)
def send_voice(self, *args, **kwargs):
def send_voice(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
bot.send_voice(Chat.id, *args, **kwargs)
Where Chat is the current instance.
bot.send_voice(update.effective_chat.id, *args, **kwargs)
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
@@ -374,12 +447,10 @@ class Chat(TelegramObject):
"""
return self.bot.send_voice(self.id, *args, **kwargs)
def send_poll(self, *args, **kwargs):
def send_poll(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
bot.send_poll(Chat.id, *args, **kwargs)
Where Chat is the current instance.
bot.send_poll(update.effective_chat.id, *args, **kwargs)
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
+12 -12
View File
@@ -20,26 +20,26 @@
"""This module contains an object that represents a Telegram ChatAction."""
class ChatAction(object):
"""Helper class to provide constants for different chatactions."""
class ChatAction:
"""Helper class to provide constants for different chat actions."""
FIND_LOCATION = 'find_location'
FIND_LOCATION: str = 'find_location'
""":obj:`str`: 'find_location'"""
RECORD_AUDIO = 'record_audio'
RECORD_AUDIO: str = 'record_audio'
""":obj:`str`: 'record_audio'"""
RECORD_VIDEO = 'record_video'
RECORD_VIDEO: str = 'record_video'
""":obj:`str`: 'record_video'"""
RECORD_VIDEO_NOTE = 'record_video_note'
RECORD_VIDEO_NOTE: str = 'record_video_note'
""":obj:`str`: 'record_video_note'"""
TYPING = 'typing'
TYPING: str = 'typing'
""":obj:`str`: 'typing'"""
UPLOAD_AUDIO = 'upload_audio'
UPLOAD_AUDIO: str = 'upload_audio'
""":obj:`str`: 'upload_audio'"""
UPLOAD_DOCUMENT = 'upload_document'
UPLOAD_DOCUMENT: str = 'upload_document'
""":obj:`str`: 'upload_document'"""
UPLOAD_PHOTO = 'upload_photo'
UPLOAD_PHOTO: str = 'upload_photo'
""":obj:`str`: 'upload_photo'"""
UPLOAD_VIDEO = 'upload_video'
UPLOAD_VIDEO: str = 'upload_video'
""":obj:`str`: 'upload_video'"""
UPLOAD_VIDEO_NOTE = 'upload_video_note'
UPLOAD_VIDEO_NOTE: str = 'upload_video_note'
""":obj:`str`: 'upload_video_note'"""
+73 -51
View File
@@ -17,13 +17,22 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ChatMember."""
import datetime
from telegram import User, TelegramObject
from telegram.utils.helpers import to_timestamp, from_timestamp
from telegram.utils.types import JSONDict
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot
class ChatMember(TelegramObject):
"""This object contains information about one member of the chat.
"""This object contains information about one member of a chat.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`user` and :attr:`status` are equal.
Attributes:
user (:class:`telegram.User`): Information about the user.
@@ -46,13 +55,13 @@ class ChatMember(TelegramObject):
can_pin_messages (:obj:`bool`): Optional. If the user can pin messages.
can_promote_members (:obj:`bool`): Optional. If the administrator can add new
administrators.
is_member (:obj:`bool`): Optional. Restricted only. True, if the user is a member of the
chat at the moment of the request.
is_member (:obj:`bool`): Optional. Restricted only. :obj:`True`, if the user is a member of
the chat at the moment of the request.
can_send_messages (:obj:`bool`): Optional. If the user can send text messages, contacts,
locations and venues.
can_send_media_messages (:obj:`bool`): Optional. If the user can send media messages,
implies can_send_messages.
can_send_polls (:obj:`bool`): Optional. True, if the user is allowed to
can_send_polls (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to
send polls.
can_send_other_messages (:obj:`bool`): Optional. If the user can send animations, games,
stickers and use inline bots, implies can_send_media_messages.
@@ -67,61 +76,74 @@ class ChatMember(TelegramObject):
Custom title for this user.
until_date (:class:`datetime.datetime`, optional): Restricted and kicked only. Date when
restrictions will be lifted for this user.
can_be_edited (:obj:`bool`, optional): Administrators only. True, if the bot is allowed to
edit administrator privileges of that user.
can_change_info (:obj:`bool`, optional): Administrators and restricted only. True, if the
user can change the chat title, photo and other settings.
can_post_messages (:obj:`bool`, optional): Administrators only. True, if the administrator
can post in the channel, channels only.
can_edit_messages (:obj:`bool`, optional): Administrators only. True, if the administrator
can edit messages of other users, channels only.
can_delete_messages (:obj:`bool`, optional): Administrators only. True, if the
administrator can delete messages of other user.
can_invite_users (:obj:`bool`, optional): Administrators and restricted only. True, if the
user can invite new users to the chat.
can_restrict_members (:obj:`bool`, optional): Administrators only. True, if the
can_be_edited (:obj:`bool`, optional): Administrators only. :obj:`True`, if the bot is
allowed to edit administrator privileges of that user.
can_change_info (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`,
if the user can change the chat title, photo and other settings.
can_post_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
administrator can post in the channel, channels only.
can_edit_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
administrator can edit messages of other users and can pin messages; channels only.
can_delete_messages (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
administrator can delete messages of other users.
can_invite_users (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`,
if the user can invite new users to the chat.
can_restrict_members (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
administrator can restrict, ban or unban chat members.
can_pin_messages (:obj:`bool`, optional): Administrators and restricted only. True, if the
user can pin messages, supergroups only.
can_promote_members (:obj:`bool`, optional): Administrators only. True, if the
can_pin_messages (:obj:`bool`, optional): Administrators and restricted only. :obj:`True`,
if the user can pin messages, groups and supergroups only.
can_promote_members (:obj:`bool`, optional): Administrators only. :obj:`True`, if the
administrator can add new administrators with a subset of his own privileges or demote
administrators that he has promoted, directly or indirectly (promoted by administrators
that were appointed by the user).
is_member (:obj:`bool`, optional): Restricted only. True, if the user is a member of the
chat at the moment of the request.
can_send_messages (:obj:`bool`, optional): Restricted only. True, if the user can send text
messages, contacts, locations and venues.
can_send_media_messages (:obj:`bool`, optional): Restricted only. True, if the user can
send audios, documents, photos, videos, video notes and voice notes, implies
can_send_messages.
can_send_polls (:obj:`bool`, optional): Restricted only. True, if the user is allowed to
send polls.
can_send_other_messages (:obj:`bool`, optional): Restricted only. True, if the user can
send animations, games, stickers and use inline bots, implies can_send_media_messages.
can_add_web_page_previews (:obj:`bool`, optional): Restricted only. True, if user may add
web page previews to his messages, implies can_send_media_messages.
is_member (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user is a member of
the chat at the moment of the request.
can_send_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user can
send text messages, contacts, locations and venues.
can_send_media_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user
can send audios, documents, photos, videos, video notes and voice notes.
can_send_polls (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user is
allowed to send polls.
can_send_other_messages (:obj:`bool`, optional): Restricted only. :obj:`True`, if the user
can send animations, games, stickers and use inline bots.
can_add_web_page_previews (:obj:`bool`, optional): Restricted only. :obj:`True`, if user
may add web page previews to his messages.
"""
ADMINISTRATOR = 'administrator'
ADMINISTRATOR: str = 'administrator'
""":obj:`str`: 'administrator'"""
CREATOR = 'creator'
CREATOR: str = 'creator'
""":obj:`str`: 'creator'"""
KICKED = 'kicked'
KICKED: str = 'kicked'
""":obj:`str`: 'kicked'"""
LEFT = 'left'
LEFT: str = 'left'
""":obj:`str`: 'left'"""
MEMBER = 'member'
MEMBER: str = 'member'
""":obj:`str`: 'member'"""
RESTRICTED = 'restricted'
RESTRICTED: str = 'restricted'
""":obj:`str`: 'restricted'"""
def __init__(self, user, status, until_date=None, can_be_edited=None,
can_change_info=None, can_post_messages=None, can_edit_messages=None,
can_delete_messages=None, can_invite_users=None,
can_restrict_members=None, can_pin_messages=None,
can_promote_members=None, can_send_messages=None,
can_send_media_messages=None, can_send_polls=None, can_send_other_messages=None,
can_add_web_page_previews=None, is_member=None, custom_title=None, **kwargs):
def __init__(self,
user: User,
status: str,
until_date: datetime.datetime = None,
can_be_edited: bool = None,
can_change_info: bool = None,
can_post_messages: bool = None,
can_edit_messages: bool = None,
can_delete_messages: bool = None,
can_invite_users: bool = None,
can_restrict_members: bool = None,
can_pin_messages: bool = None,
can_promote_members: bool = None,
can_send_messages: bool = None,
can_send_media_messages: bool = None,
can_send_polls: bool = None,
can_send_other_messages: bool = None,
can_add_web_page_previews: bool = None,
is_member: bool = None,
custom_title: str = None,
**kwargs: Any):
# Required
self.user = user
self.status = status
@@ -146,19 +168,19 @@ class ChatMember(TelegramObject):
self._id_attrs = (self.user, self.status)
@classmethod
def de_json(cls, data, bot):
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatMember']:
data = cls.parse_data(data)
if not data:
return None
data = super(ChatMember, cls).de_json(data, bot)
data['user'] = User.de_json(data.get('user'), bot)
data['until_date'] = from_timestamp(data.get('until_date', None))
return cls(**data)
def to_dict(self):
data = super(ChatMember, self).to_dict()
def to_dict(self) -> JSONDict:
data = super().to_dict()
data['until_date'] = to_timestamp(self.until_date)
+59 -42
View File
@@ -19,61 +19,74 @@
"""This module contains an object that represents a Telegram ChatPermission."""
from telegram import TelegramObject
from typing import Any
class ChatPermissions(TelegramObject):
"""Describes actions that a non-administrator user is allowed to take in a chat.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`can_send_messages`, :attr:`can_send_media_messages`,
:attr:`can_send_polls`, :attr:`can_send_other_messages`, :attr:`can_add_web_page_previews`,
:attr:`can_change_info`, :attr:`can_invite_users` and :attr:`can_pin_message` are equal.
Note:
Though not stated explicitly in the offical docs, Telegram changes not only the permissions
that are set, but also sets all the others to :obj:`False`. However, since not documented,
this behaviour may change unbeknown to PTB.
Though not stated explicitly in the official docs, Telegram changes not only the
permissions that are set, but also sets all the others to :obj:`False`. However, since not
documented, this behaviour may change unbeknown to PTB.
Attributes:
can_send_messages (:obj:`bool`): Optional. True, if the user is allowed to send text
can_send_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send text
messages, contacts, locations and venues.
can_send_media_messages (:obj:`bool`): Optional. True, if the user is allowed to send
audios, documents, photos, videos, video notes and voice notes, implies
can_send_media_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to
send audios, documents, photos, videos, video notes and voice notes, implies
:attr:`can_send_messages`.
can_send_polls (:obj:`bool`): Optional. True, if the user is allowed to send polls, implies
:attr:`can_send_messages`.
can_send_other_messages (:obj:`bool`): Optional. True, if the user is allowed to send
animations, games, stickers and use inline bots, implies
can_send_polls (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send polls,
implies :attr:`can_send_messages`.
can_send_other_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to
send animations, games, stickers and use inline bots, implies
:attr:`can_send_media_messages`.
can_add_web_page_previews (:obj:`bool`): Optional. True, if the user is allowed to add web
page previews to their messages, implies :attr:`can_send_media_messages`.
can_change_info (:obj:`bool`): Optional. True, if the user is allowed to change the chat
title, photo and other settings. Ignored in public supergroups.
can_invite_users (:obj:`bool`): Optional. True, if the user is allowed to invite new users
to the chat.
can_pin_messages (:obj:`bool`): Optional. True, if the user is allowed to pin messages.
Ignored in public supergroups.
can_add_web_page_previews (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to
add web page previews to their messages, implies :attr:`can_send_media_messages`.
can_change_info (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to change the
chat title, photo and other settings. Ignored in public supergroups.
can_invite_users (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to invite
new users to the chat.
can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin
messages. Ignored in public supergroups.
Args:
can_send_messages (:obj:`bool`, optional): True, if the user is allowed to send text
can_send_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to send text
messages, contacts, locations and venues.
can_send_media_messages (:obj:`bool`, optional): True, if the user is allowed to send
audios, documents, photos, videos, video notes and voice notes, implies
can_send_media_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to
send audios, documents, photos, videos, video notes and voice notes, implies
:attr:`can_send_messages`.
can_send_polls (:obj:`bool`, optional): True, if the user is allowed to send polls, implies
:attr:`can_send_messages`.
can_send_other_messages (:obj:`bool`, optional): True, if the user is allowed to send
animations, games, stickers and use inline bots, implies
can_send_polls (:obj:`bool`, optional): :obj:`True`, if the user is allowed to send polls,
implies :attr:`can_send_messages`.
can_send_other_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to
send animations, games, stickers and use inline bots, implies
:attr:`can_send_media_messages`.
can_add_web_page_previews (:obj:`bool`, optional): True, if the user is allowed to add web
page previews to their messages, implies :attr:`can_send_media_messages`.
can_change_info (:obj:`bool`, optional): True, if the user is allowed to change the chat
title, photo and other settings. Ignored in public supergroups.
can_invite_users (:obj:`bool`, optional): True, if the user is allowed to invite new users
to the chat.
can_pin_messages (:obj:`bool`, optional): True, if the user is allowed to pin messages.
Ignored in public supergroups.
can_add_web_page_previews (:obj:`bool`, optional): :obj:`True`, if the user is allowed to
add web page previews to their messages, implies :attr:`can_send_media_messages`.
can_change_info (:obj:`bool`, optional): :obj:`True`, if the user is allowed to change the
chat title, photo and other settings. Ignored in public supergroups.
can_invite_users (:obj:`bool`, optional): :obj:`True`, if the user is allowed to invite new
users to the chat.
can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin
messages. Ignored in public supergroups.
"""
def __init__(self, can_send_messages=None, can_send_media_messages=None, can_send_polls=None,
can_send_other_messages=None, can_add_web_page_previews=None,
can_change_info=None, can_invite_users=None, can_pin_messages=None, **kwargs):
def __init__(self,
can_send_messages: bool = None,
can_send_media_messages: bool = None,
can_send_polls: bool = None,
can_send_other_messages: bool = None,
can_add_web_page_previews: bool = None,
can_change_info: bool = None,
can_invite_users: bool = None,
can_pin_messages: bool = None,
**kwargs: Any):
# Required
self.can_send_messages = can_send_messages
self.can_send_media_messages = can_send_media_messages
@@ -84,9 +97,13 @@ class ChatPermissions(TelegramObject):
self.can_invite_users = can_invite_users
self.can_pin_messages = can_pin_messages
@classmethod
def de_json(cls, data, bot):
if not data:
return None
return cls(**data)
self._id_attrs = (
self.can_send_messages,
self.can_send_media_messages,
self.can_send_polls,
self.can_send_other_messages,
self.can_add_web_page_previews,
self.can_change_info,
self.can_invite_users,
self.can_pin_messages
)
+20 -8
View File
@@ -20,6 +20,10 @@
"""This module contains an object that represents a Telegram ChosenInlineResult."""
from telegram import TelegramObject, User, Location
from telegram.utils.types import JSONDict
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot
class ChosenInlineResult(TelegramObject):
@@ -27,6 +31,9 @@ class ChosenInlineResult(TelegramObject):
Represents a result of an inline query that was chosen by the user and sent to their chat
partner.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`result_id` is equal.
Note:
In Python `from` is a reserved word, use `from_user` instead.
@@ -48,15 +55,19 @@ class ChosenInlineResult(TelegramObject):
query (:obj:`str`): The query that was used to obtain the result.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
Note:
It is necessary to enable inline feedback via `@Botfather <https://t.me/BotFather>`_ in
order to receive these objects in updates.
"""
def __init__(self,
result_id,
from_user,
query,
location=None,
inline_message_id=None,
**kwargs):
result_id: str,
from_user: User,
query: str,
location: Location = None,
inline_message_id: str = None,
**kwargs: Any):
# Required
self.result_id = result_id
self.from_user = from_user
@@ -68,11 +79,12 @@ class ChosenInlineResult(TelegramObject):
self._id_attrs = (self.result_id,)
@classmethod
def de_json(cls, data, bot):
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChosenInlineResult']:
data = cls.parse_data(data)
if not data:
return None
data = super(ChosenInlineResult, cls).de_json(data, bot)
# Required
data['from_user'] = User.de_json(data.pop('from'), bot)
# Optionals
+12 -11
View File
@@ -40,18 +40,19 @@ Attributes:
formatting styles)
"""
from typing import List
MAX_MESSAGE_LENGTH = 4096
MAX_CAPTION_LENGTH = 1024
MAX_MESSAGE_LENGTH: int = 4096
MAX_CAPTION_LENGTH: int = 1024
# constants above this line are tested
SUPPORTED_WEBHOOK_PORTS = [443, 80, 88, 8443]
MAX_FILESIZE_DOWNLOAD = int(20E6) # (20MB)
MAX_FILESIZE_UPLOAD = int(50E6) # (50MB)
MAX_PHOTOSIZE_UPLOAD = int(10E6) # (10MB)
MAX_MESSAGES_PER_SECOND_PER_CHAT = 1
MAX_MESSAGES_PER_SECOND = 30
MAX_MESSAGES_PER_MINUTE_PER_GROUP = 20
MAX_MESSAGE_ENTITIES = 100
MAX_INLINE_QUERY_RESULTS = 50
SUPPORTED_WEBHOOK_PORTS: List[int] = [443, 80, 88, 8443]
MAX_FILESIZE_DOWNLOAD: int = int(20E6) # (20MB)
MAX_FILESIZE_UPLOAD: int = int(50E6) # (50MB)
MAX_PHOTOSIZE_UPLOAD: int = int(10E6) # (10MB)
MAX_MESSAGES_PER_SECOND_PER_CHAT: int = 1
MAX_MESSAGES_PER_SECOND: int = 30
MAX_MESSAGES_PER_MINUTE_PER_GROUP: int = 20
MAX_MESSAGE_ENTITIES: int = 100
MAX_INLINE_QUERY_RESULTS: int = 50
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python
# pylint: disable=R0903
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2020
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Dice."""
from telegram import TelegramObject
from typing import Any, List
class Dice(TelegramObject):
"""
This object represents an animated emoji with a random value for currently supported base
emoji. (The singular form of "dice" is "die". However, PTB mimics the Telegram API, which uses
the term "dice".)
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`value` and :attr:`emoji` are equal.
Note:
If :attr:`emoji` is "🎯", a value of 6 currently represents a bullseye, while a value of 1
indicates that the dartboard was missed. However, this behaviour is undocumented and might
be changed by Telegram.
If :attr:`emoji` is "🏀", a value of 4 or 5 currently score a basket, while a value of 1 to
3 indicates that the basket was missed. However, this behaviour is undocumented and might
be changed by Telegram.
Attributes:
value (:obj:`int`): Value of the dice.
emoji (:obj:`str`): Emoji on which the dice throw animation is based.
Args:
value (:obj:`int`): Value of the dice. 1-6 for dice and darts, 1-5 for basketball.
emoji (:obj:`str`): Emoji on which the dice throw animation is based.
"""
def __init__(self, value: int, emoji: str, **kwargs: Any):
self.value = value
self.emoji = emoji
self._id_attrs = (self.value, self.emoji)
DICE: str = '🎲'
""":obj:`str`: '🎲'"""
DARTS: str = '🎯'
""":obj:`str`: '🎯'"""
BASKETBALL = '🏀'
""":obj:`str`: '🏀'"""
ALL_EMOJI: List[str] = [DICE, DARTS, BASKETBALL]
"""List[:obj:`str`]: List of all supported base emoji. Currently :attr:`DICE`,
:attr:`DARTS` and :attr:`BASKETBALL`."""
+37 -20
View File
@@ -17,16 +17,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/].
"""This module contains an object that represents Telegram errors."""
from typing import Tuple
def _lstrip_str(in_s, lstr):
def _lstrip_str(in_s: str, lstr: str) -> str:
"""
Args:
in_s (:obj:`str`): in string
lstr (:obj:`str`): substr to strip from left side
Returns:
str:
:obj:`str`: The stripped string.
"""
if in_s.startswith(lstr):
@@ -37,8 +38,8 @@ def _lstrip_str(in_s, lstr):
class TelegramError(Exception):
def __init__(self, message):
super(TelegramError, self).__init__()
def __init__(self, message: str):
super().__init__()
msg = _lstrip_str(message, 'Error: ')
msg = _lstrip_str(msg, '[Error]: ')
@@ -48,8 +49,11 @@ class TelegramError(Exception):
msg = msg.capitalize()
self.message = msg
def __str__(self):
return '%s' % (self.message)
def __str__(self) -> str:
return '%s' % self.message
def __reduce__(self) -> Tuple[type, Tuple[str]]:
return self.__class__, (self.message,)
class Unauthorized(TelegramError):
@@ -57,8 +61,11 @@ class Unauthorized(TelegramError):
class InvalidToken(TelegramError):
def __init__(self):
super(InvalidToken, self).__init__('Invalid token')
def __init__(self) -> None:
super().__init__('Invalid token')
def __reduce__(self) -> Tuple[type, Tuple]: # type: ignore[override]
return self.__class__, ()
class NetworkError(TelegramError):
@@ -70,35 +77,42 @@ class BadRequest(NetworkError):
class TimedOut(NetworkError):
def __init__(self):
super(TimedOut, self).__init__('Timed out')
def __init__(self) -> None:
super().__init__('Timed out')
def __reduce__(self) -> Tuple[type, Tuple]: # type: ignore[override]
return self.__class__, ()
class ChatMigrated(TelegramError):
"""
Args:
new_chat_id (:obj:`int`):
new_chat_id (:obj:`int`): The new chat id of the group.
"""
def __init__(self, new_chat_id):
super(ChatMigrated,
self).__init__('Group migrated to supergroup. New chat id: {}'.format(new_chat_id))
def __init__(self, new_chat_id: int):
super().__init__('Group migrated to supergroup. New chat id: {}'.format(new_chat_id))
self.new_chat_id = new_chat_id
def __reduce__(self) -> Tuple[type, Tuple[int]]: # type: ignore[override]
return self.__class__, (self.new_chat_id,)
class RetryAfter(TelegramError):
"""
Args:
retry_after (:obj:`int`):
retry_after (:obj:`int`): Time in seconds, after which the bot can retry the request.
"""
def __init__(self, retry_after):
super(RetryAfter,
self).__init__('Flood control exceeded. Retry in {} seconds'.format(retry_after))
def __init__(self, retry_after: int):
super().__init__('Flood control exceeded. Retry in {} seconds'.format(float(retry_after)))
self.retry_after = float(retry_after)
def __reduce__(self) -> Tuple[type, Tuple[float]]: # type: ignore[override]
return self.__class__, (self.retry_after,)
class Conflict(TelegramError):
"""
@@ -109,5 +123,8 @@ class Conflict(TelegramError):
"""
def __init__(self, msg):
super(Conflict, self).__init__(msg)
def __init__(self, msg: str):
super().__init__(msg)
def __reduce__(self) -> Tuple[type, Tuple[str]]:
return self.__class__, (self.message,)
+7 -7
View File
@@ -29,7 +29,7 @@ from .updater import Updater
from .callbackqueryhandler import CallbackQueryHandler
from .choseninlineresulthandler import ChosenInlineResultHandler
from .inlinequeryhandler import InlineQueryHandler
from .filters import BaseFilter, Filters
from .filters import BaseFilter, MessageFilter, UpdateFilter, Filters
from .messagehandler import MessageHandler
from .commandhandler import CommandHandler, PrefixHandler
from .regexhandler import RegexHandler
@@ -47,9 +47,9 @@ from .defaults import Defaults
__all__ = ('Dispatcher', 'JobQueue', 'Job', 'Updater', 'CallbackQueryHandler',
'ChosenInlineResultHandler', 'CommandHandler', 'Handler', 'InlineQueryHandler',
'MessageHandler', 'BaseFilter', 'Filters', 'RegexHandler', 'StringCommandHandler',
'StringRegexHandler', 'TypeHandler', 'ConversationHandler',
'PreCheckoutQueryHandler', 'ShippingQueryHandler', 'MessageQueue', 'DelayQueue',
'DispatcherHandlerStop', 'run_async', 'CallbackContext', 'BasePersistence',
'PicklePersistence', 'DictPersistence', 'PrefixHandler', 'PollAnswerHandler',
'PollHandler', 'Defaults')
'MessageHandler', 'BaseFilter', 'MessageFilter', 'UpdateFilter', 'Filters',
'RegexHandler', 'StringCommandHandler', 'StringRegexHandler', 'TypeHandler',
'ConversationHandler', 'PreCheckoutQueryHandler', 'ShippingQueryHandler',
'MessageQueue', 'DelayQueue', 'DispatcherHandlerStop', 'run_async', 'CallbackContext',
'BasePersistence', 'PicklePersistence', 'DictPersistence', 'PrefixHandler',
'PollAnswerHandler', 'PollHandler', 'Defaults')
+180 -31
View File
@@ -18,23 +18,44 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the BasePersistence class."""
from abc import ABC, abstractmethod
from collections import defaultdict
from copy import copy
class BasePersistence(object):
from telegram import Bot
from typing import DefaultDict, Dict, Any, Tuple, Optional, cast
from telegram.utils.types import ConversationDict
class BasePersistence(ABC):
"""Interface class for adding persistence to your bot.
Subclass this object for different implementations of a persistent bot.
All relevant methods must be overwritten. This means:
* If :attr:`store_bot_data` is ``True`` you must overwrite :meth:`get_bot_data` and
* If :attr:`store_bot_data` is :obj:`True` you must overwrite :meth:`get_bot_data` and
:meth:`update_bot_data`.
* If :attr:`store_chat_data` is ``True`` you must overwrite :meth:`get_chat_data` and
* If :attr:`store_chat_data` is :obj:`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
* If :attr:`store_user_data` is :obj:`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.
Warning:
Persistence will try to replace :class:`telegram.Bot` instances by :attr:`REPLACED_BOT` and
insert the bot set with :meth:`set_bot` upon loading of the data. This is to ensure that
changes to the bot apply to the saved objects, too. If you change the bots token, this may
lead to e.g. ``Chat not found`` errors. For the limitations on replacing bots see
:meth:`replace_bot` and :meth:`insert_bot`.
Note:
:meth:`replace_bot` and :meth:`insert_bot` are used *independently* of the implementation
of the :meth:`update/get_*` methods, i.e. you don't need to worry about it while
implementing a custom persistence subclass.
Attributes:
store_user_data (:obj:`bool`): Optional, Whether user_data should be saved by this
persistence class.
@@ -45,19 +66,143 @@ class BasePersistence(object):
Args:
store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this
persistence class. Default is ``True``.
persistence class. Default is :obj:`True`.
store_chat_data (:obj:`bool`, optional): Whether chat_data should be saved by this
persistence class. Default is ``True`` .
persistence class. Default is :obj:`True` .
store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this
persistence class. Default is ``True`` .
persistence class. Default is :obj:`True` .
"""
def __init__(self, store_user_data=True, store_chat_data=True, store_bot_data=True):
def __new__(cls, *args: Any, **kwargs: Any) -> 'BasePersistence':
instance = super().__new__(cls)
get_user_data = instance.get_user_data
get_chat_data = instance.get_chat_data
get_bot_data = instance.get_bot_data
update_user_data = instance.update_user_data
update_chat_data = instance.update_chat_data
update_bot_data = instance.update_bot_data
def get_user_data_insert_bot() -> DefaultDict[int, Dict[Any, Any]]:
return instance.insert_bot(get_user_data())
def get_chat_data_insert_bot() -> DefaultDict[int, Dict[Any, Any]]:
return instance.insert_bot(get_chat_data())
def get_bot_data_insert_bot() -> Dict[Any, Any]:
return instance.insert_bot(get_bot_data())
def update_user_data_replace_bot(user_id: int, data: Dict) -> None:
return update_user_data(user_id, instance.replace_bot(data))
def update_chat_data_replace_bot(chat_id: int, data: Dict) -> None:
return update_chat_data(chat_id, instance.replace_bot(data))
def update_bot_data_replace_bot(data: Dict) -> None:
return update_bot_data(instance.replace_bot(data))
instance.get_user_data = get_user_data_insert_bot
instance.get_chat_data = get_chat_data_insert_bot
instance.get_bot_data = get_bot_data_insert_bot
instance.update_user_data = update_user_data_replace_bot
instance.update_chat_data = update_chat_data_replace_bot
instance.update_bot_data = update_bot_data_replace_bot
return instance
def __init__(self,
store_user_data: bool = True,
store_chat_data: bool = True,
store_bot_data: bool = True):
self.store_user_data = store_user_data
self.store_chat_data = store_chat_data
self.store_bot_data = store_bot_data
self.bot: Bot = None # type: ignore[assignment]
def get_user_data(self):
def set_bot(self, bot: Bot) -> None:
"""Set the Bot to be used by this persistence instance.
Args:
bot (:class:`telegram.Bot`): The bot.
"""
self.bot = bot
@classmethod
def replace_bot(cls, obj: object) -> object:
"""
Replaces all instances of :class:`telegram.Bot` that occur within the passed object with
:attr:`REPLACED_BOT`. Currently, this handles objects of type ``list``, ``tuple``, ``set``,
``frozenset``, ``dict``, ``defaultdict`` and objects that have a ``__dict__`` or
``__slot__`` attribute.
Args:
obj (:obj:`object`): The object
Returns:
:obj:`obj`: Copy of the object with Bot instances replaced.
"""
if isinstance(obj, Bot):
return cls.REPLACED_BOT
if isinstance(obj, (list, tuple, set, frozenset)):
return obj.__class__(cls.replace_bot(item) for item in obj)
new_obj = copy(obj)
if isinstance(obj, (dict, defaultdict)):
new_obj = cast(dict, new_obj)
new_obj.clear()
for k, v in obj.items():
new_obj[cls.replace_bot(k)] = cls.replace_bot(v)
return new_obj
if hasattr(obj, '__dict__'):
for attr_name, attr in new_obj.__dict__.items():
setattr(new_obj, attr_name, cls.replace_bot(attr))
return new_obj
if hasattr(obj, '__slots__'):
for attr_name in new_obj.__slots__:
setattr(new_obj, attr_name,
cls.replace_bot(cls.replace_bot(getattr(new_obj, attr_name))))
return new_obj
return obj
def insert_bot(self, obj: object) -> object:
"""
Replaces all instances of :attr:`REPLACED_BOT` that occur within the passed object with
:attr:`bot`. Currently, this handles objects of type ``list``, ``tuple``, ``set``,
``frozenset``, ``dict``, ``defaultdict`` and objects that have a ``__dict__`` or
``__slot__`` attribute.
Args:
obj (:obj:`object`): The object
Returns:
:obj:`obj`: Copy of the object with Bot instances inserted.
"""
if isinstance(obj, Bot):
return self.bot
if obj == self.REPLACED_BOT:
return self.bot
if isinstance(obj, (list, tuple, set, frozenset)):
return obj.__class__(self.insert_bot(item) for item in obj)
new_obj = copy(obj)
if isinstance(obj, (dict, defaultdict)):
new_obj = cast(dict, new_obj)
new_obj.clear()
for k, v in obj.items():
new_obj[self.insert_bot(k)] = self.insert_bot(v)
return new_obj
if hasattr(obj, '__dict__'):
for attr_name, attr in new_obj.__dict__.items():
setattr(new_obj, attr_name, self.insert_bot(attr))
return new_obj
if hasattr(obj, '__slots__'):
for attr_name in obj.__slots__:
setattr(new_obj, attr_name,
self.insert_bot(self.insert_bot(getattr(new_obj, attr_name))))
return new_obj
return obj
@abstractmethod
def get_user_data(self) -> DefaultDict[int, Dict[Any, Any]]:
""""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)``.
@@ -65,9 +210,9 @@ class BasePersistence(object):
Returns:
:obj:`defaultdict`: The restored user data.
"""
raise NotImplementedError
def get_chat_data(self):
@abstractmethod
def get_chat_data(self) -> DefaultDict[int, Dict[Any, Any]]:
""""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)``.
@@ -75,23 +220,23 @@ class BasePersistence(object):
Returns:
:obj:`defaultdict`: The restored chat data.
"""
raise NotImplementedError
def get_bot_data(self):
@abstractmethod
def get_bot_data(self) -> Dict[Any, Any]:
""""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
persistence object. It should return the bot_data if stored, or an empty
``dict``.
:obj:`dict`.
Returns:
:obj:`defaultdict`: The restored bot data.
:obj:`dict`: The restored bot data.
"""
raise NotImplementedError
def get_conversations(self, name):
@abstractmethod
def get_conversations(self, name: str) -> ConversationDict:
""""Will be called by :class:`telegram.ext.Dispatcher` when a
:class:`telegram.ext.ConversationHandler` is added if
:attr:`telegram.ext.ConversationHandler.persistent` is ``True``.
It should return the conversations for the handler with `name` or an empty ``dict``
:attr:`telegram.ext.ConversationHandler.persistent` is :obj:`True`.
It should return the conversations for the handler with `name` or an empty :obj:`dict`
Args:
name (:obj:`str`): The handlers name.
@@ -99,20 +244,22 @@ class BasePersistence(object):
Returns:
:obj:`dict`: The restored conversations for the handler.
"""
raise NotImplementedError
def update_conversation(self, name, key, new_state):
@abstractmethod
def update_conversation(self,
name: str, key: Tuple[int, ...],
new_state: Optional[object]) -> None:
"""Will be called when a :attr:`telegram.ext.ConversationHandler.update_state`
is called. this allows the storeage of the new state in the persistence.
is called. This allows the storage of the new state in the persistence.
Args:
name (:obj:`str`): The handlers name.
name (:obj:`str`): The handler's name.
key (:obj:`tuple`): The key the state is changed for.
new_state (:obj:`tuple` | :obj:`any`): The new state for the given key.
"""
raise NotImplementedError
def update_user_data(self, user_id, data):
@abstractmethod
def update_user_data(self, user_id: int, data: Dict) -> None:
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
handled an update.
@@ -120,9 +267,9 @@ class BasePersistence(object):
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):
@abstractmethod
def update_chat_data(self, chat_id: int, data: Dict) -> None:
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
handled an update.
@@ -130,20 +277,22 @@ class BasePersistence(object):
chat_id (:obj:`int`): The chat the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data` [chat_id].
"""
raise NotImplementedError
def update_bot_data(self, data):
@abstractmethod
def update_bot_data(self, data: Dict) -> None:
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
handled an update.
Args:
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.bot_data` .
"""
raise NotImplementedError
def flush(self):
def flush(self) -> None:
"""Will be called by :class:`telegram.ext.Updater` upon receiving a stop signal. Gives the
persistence a chance to finish up saving or close a database connection gracefully. If this
is not of any importance just pass will be sufficient.
"""
pass
REPLACED_BOT = 'bot_instance_replaced_by_ptb_persistence'
""":obj:`str`: Placeholder for :class:`telegram.Bot` instances replaced in saved data."""
+53 -32
View File
@@ -17,11 +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/].
"""This module contains the CallbackContext class."""
from queue import Queue
from typing import Dict, Any, Tuple, TYPE_CHECKING, Optional, Match, List, NoReturn, Union
from telegram import Update
if TYPE_CHECKING:
from telegram import Bot
from telegram.ext import Dispatcher, Job, JobQueue
class CallbackContext(object):
class CallbackContext:
"""
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
@@ -38,14 +43,15 @@ class CallbackContext(object):
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
Do not combine custom attributes and ``@run_async``/
:meth:`telegram.ext.Disptacher.run_async`. Due to how ``run_async`` works, it will
almost certainly execute the callbacks for an update out of order, and the attributes
that you think you added will not be present.
Attributes:
bot_data (:obj:`dict`, optional): A dict that can be used to keep any data in. For each
bot_data (:obj:`dict`): Optional. A dict that can be used to keep any data in. For each
update it will be the same ``dict``.
chat_data (:obj:`dict`, optional): A dict that can be used to keep any data in. For each
chat_data (:obj:`dict`): Optional. A dict that can be used to keep any data in. For each
update from the same chat id it will be the same ``dict``.
Warning:
@@ -54,26 +60,32 @@ class CallbackContext(object):
<https://github.com/python-telegram-bot/python-telegram-bot/wiki/
Storing-user--and-chat-related-data#chat-migration>`_.
user_data (:obj:`dict`, optional): A dict that can be used to keep any data in. For each
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
matches (List[:obj:`re match object`]): Optional. If the associated update originated from
a regex-supported handler or had a :class:`Filters.regex`, this will contain a list of
match objects for every pattern where ``re.search(pattern, string)`` returned a match.
Note that filters short circuit, so combined regex filters will not always
be evaluated.
args (List[:obj:`str`], optional): Arguments passed to a command if the associated update
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.
error (:class:`telegram.TelegramError`): Optional. The 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.
async_args (List[:obj:`object`]): Optional. Positional arguments of the function that
raised the error. Only present when the raising function was run asynchronously using
:meth:`telegram.ext.Dispatcher.run_async`.
async_kwargs (Dict[:obj:`str`, :obj:`object`]): Optional. Keyword arguments of the function
that raised the error. Only present when the raising function was run asynchronously
using :meth:`telegram.ext.Dispatcher.run_async`.
job (:class:`telegram.ext.Job`): Optional. The job which originated this callback.
Only present when passed to the callback of :class:`telegram.ext.Job`.
"""
def __init__(self, dispatcher):
def __init__(self, dispatcher: 'Dispatcher'):
"""
Args:
dispatcher (:class:`telegram.ext.Dispatcher`):
@@ -83,53 +95,62 @@ class CallbackContext(object):
'dispatcher!')
self._dispatcher = dispatcher
self._bot_data = dispatcher.bot_data
self._chat_data = None
self._user_data = None
self.args = None
self.matches = None
self.error = None
self.job = None
self._chat_data: Optional[Dict[Any, Any]] = None
self._user_data: Optional[Dict[Any, Any]] = None
self.args: Optional[List[str]] = None
self.matches: Optional[List[Match]] = None
self.error: Optional[Exception] = None
self.job: Optional['Job'] = None
self.async_args: Optional[Union[List, Tuple]] = None
self.async_kwargs: Optional[Dict[str, Any]] = None
@property
def dispatcher(self):
def dispatcher(self) -> 'Dispatcher':
""":class:`telegram.ext.Dispatcher`: The dispatcher associated with this context."""
return self._dispatcher
@property
def bot_data(self):
def bot_data(self) -> Dict:
return self._bot_data
@bot_data.setter
def bot_data(self, value):
def bot_data(self, value: Any) -> NoReturn:
raise AttributeError("You can not assign a new value to bot_data, see "
"https://git.io/fjxKe")
@property
def chat_data(self):
def chat_data(self) -> Optional[Dict]:
return self._chat_data
@chat_data.setter
def chat_data(self, value):
def chat_data(self, value: Any) -> NoReturn:
raise AttributeError("You can not assign a new value to chat_data, see "
"https://git.io/fjxKe")
@property
def user_data(self):
def user_data(self) -> Optional[Dict]:
return self._user_data
@user_data.setter
def user_data(self, value):
def user_data(self, value: Any) -> NoReturn:
raise AttributeError("You can not assign a new value to user_data, see "
"https://git.io/fjxKe")
@classmethod
def from_error(cls, update, error, dispatcher):
def from_error(cls,
update: object,
error: Exception,
dispatcher: 'Dispatcher',
async_args: Union[List, Tuple] = None,
async_kwargs: Dict[str, Any] = None) -> 'CallbackContext':
self = cls.from_update(update, dispatcher)
self.error = error
self.async_args = async_args
self.async_kwargs = async_kwargs
return self
@classmethod
def from_update(cls, update, dispatcher):
def from_update(cls, update: object, dispatcher: 'Dispatcher') -> 'CallbackContext':
self = cls(dispatcher)
if update is not None and isinstance(update, Update):
@@ -143,21 +164,21 @@ class CallbackContext(object):
return self
@classmethod
def from_job(cls, job, dispatcher):
def from_job(cls, job: 'Job', dispatcher: 'Dispatcher') -> 'CallbackContext':
self = cls(dispatcher)
self.job = job
return self
def update(self, data):
def update(self, data: Dict[str, Any]) -> None:
self.__dict__.update(data)
@property
def bot(self):
def bot(self) -> 'Bot':
""":class:`telegram.Bot`: The bot associated with this context."""
return self._dispatcher.bot
@property
def job_queue(self):
def job_queue(self) -> Optional['JobQueue']:
"""
:class:`telegram.ext.JobQueue`: The ``JobQueue`` used by the
:class:`telegram.ext.Dispatcher` and (usually) the :class:`telegram.ext.Updater`
@@ -167,7 +188,7 @@ class CallbackContext(object):
return self._dispatcher.job_queue
@property
def update_queue(self):
def update_queue(self) -> Queue:
"""
:class:`queue.Queue`: The ``Queue`` instance used by the
:class:`telegram.ext.Dispatcher` and (usually) the :class:`telegram.ext.Updater`
@@ -177,13 +198,13 @@ class CallbackContext(object):
return self._dispatcher.update_queue
@property
def match(self):
def match(self) -> Optional[Match[str]]:
"""
`Regex match type`: The first match from :attr:`matches`.
Useful if you are only filtering using a single regex filter.
Returns `None` if :attr:`matches` is empty.
"""
try:
return self.matches[0] # pylint: disable=unsubscriptable-object
return self.matches[0] # type: ignore[index] # pylint: disable=unsubscriptable-object
except (IndexError, TypeError):
return None
+54 -30
View File
@@ -20,11 +20,18 @@
import re
from future.utils import string_types
from telegram import Update
from .handler import Handler
from telegram.utils.types import HandlerArg
from typing import Callable, TYPE_CHECKING, Any, Optional, Union, TypeVar, Pattern, Match, Dict, \
cast
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
RT = TypeVar('RT')
class CallbackQueryHandler(Handler):
"""Handler class to handle Telegram callback queries. Optionally based on a regex.
@@ -47,6 +54,7 @@ class CallbackQueryHandler(Handler):
the callback function.
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
@@ -57,6 +65,10 @@ class CallbackQueryHandler(Handler):
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Warning:
When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
@@ -66,60 +78,64 @@ class CallbackQueryHandler(Handler):
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
pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
that contains new updates which can be used to insert updates. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
which can be used to schedule new jobs. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pattern (:obj:`str` | `Pattern`, optional): Regex pattern. If not ``None``, ``re.match``
pattern (:obj:`str` | `Pattern`, optional): Regex pattern. If not :obj:`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``
Default is :obj:`False`
DEPRECATED: Please switch to context based callbacks.
pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``.
Default is ``False``
Default is :obj:`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``.
pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``user_data`` will be passed to the callback function. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``chat_data`` will be passed to the callback function. Default is ``False``.
pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``chat_data`` will be passed to the callback function. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Defaults to :obj:`False`.
"""
def __init__(self,
callback,
pass_update_queue=False,
pass_job_queue=False,
pattern=None,
pass_groups=False,
pass_groupdict=False,
pass_user_data=False,
pass_chat_data=False):
super(CallbackQueryHandler, self).__init__(
callback: Callable[[HandlerArg, 'CallbackContext'], RT],
pass_update_queue: bool = False,
pass_job_queue: bool = False,
pattern: Union[str, Pattern] = None,
pass_groups: bool = False,
pass_groupdict: bool = False,
pass_user_data: bool = False,
pass_chat_data: bool = False,
run_async: bool = False):
super().__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
pass_chat_data=pass_chat_data)
pass_chat_data=pass_chat_data,
run_async=run_async)
if isinstance(pattern, string_types):
if isinstance(pattern, str):
pattern = re.compile(pattern)
self.pattern = pattern
self.pass_groups = pass_groups
self.pass_groupdict = pass_groupdict
def check_update(self, update):
def check_update(self, update: HandlerArg) -> Optional[Union[bool, object]]:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
@@ -137,18 +153,26 @@ class CallbackQueryHandler(Handler):
return match
else:
return True
return None
def collect_optional_args(self, dispatcher, update=None, check_result=None):
optional_args = super(CallbackQueryHandler, self).collect_optional_args(dispatcher,
update,
check_result)
def collect_optional_args(self,
dispatcher: 'Dispatcher',
update: HandlerArg = None,
check_result: Union[bool, Match] = None) -> Dict[str, Any]:
optional_args = super().collect_optional_args(dispatcher, update, check_result)
if self.pattern:
check_result = cast(Match, check_result)
if self.pass_groups:
optional_args['groups'] = check_result.groups()
if self.pass_groupdict:
optional_args['groupdict'] = check_result.groupdict()
return optional_args
def collect_additional_context(self, context, update, dispatcher, check_result):
def collect_additional_context(self,
context: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Union[bool, Match]) -> None:
if self.pattern:
check_result = cast(Match, check_result)
context.matches = [check_result]
+20 -9
View File
@@ -21,6 +21,10 @@
from telegram import Update
from .handler import Handler
from telegram.utils.types import HandlerArg
from typing import Optional, Union, TypeVar
RT = TypeVar('RT')
class ChosenInlineResultHandler(Handler):
"""Handler class to handle Telegram updates that contain a chosen inline result.
@@ -35,6 +39,7 @@ class ChosenInlineResultHandler(Handler):
the callback function.
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
@@ -45,6 +50,10 @@ class ChosenInlineResultHandler(Handler):
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Warning:
When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
@@ -54,26 +63,28 @@ class ChosenInlineResultHandler(Handler):
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
pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
that contains new updates which can be used to insert updates. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
which can be used to schedule new jobs. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``user_data`` will be passed to the callback function. Default is :obj:`False`.
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``.
pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``chat_data`` will be passed to the callback function. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Defaults to :obj:`False`.
"""
def check_update(self, update):
def check_update(self, update: HandlerArg) -> Optional[Union[bool, object]]:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
+131 -82
View File
@@ -20,14 +20,19 @@
import re
import warnings
from future.utils import string_types
from telegram.ext import Filters
from telegram.ext import Filters, BaseFilter
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram import Update, MessageEntity
from .handler import Handler
from telegram.utils.types import HandlerArg
from typing import Callable, TYPE_CHECKING, Any, Optional, Union, TypeVar, Dict, List, Tuple
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
RT = TypeVar('RT')
class CommandHandler(Handler):
"""Handler class to handle Telegram commands.
@@ -40,6 +45,9 @@ class CommandHandler(Handler):
By default the handler listens to messages as well as edited messages. To change this behavior
use ``~Filters.update.edited_message`` in the filter argument.
Note:
:class:`telegram.ext.CommandHandler` does *not* handle (edited) channel posts.
Attributes:
command (:obj:`str` | List[:obj:`str`]): The command or list of commands this handler
should listen for. Limitations are the same as described here
@@ -47,7 +55,7 @@ class CommandHandler(Handler):
callback (:obj:`callable`): The callback function for this handler.
filters (:class:`telegram.ext.BaseFilter`): Optional. Only allow updates with these
Filters.
allow_edited (:obj:`bool`): Determines Whether the handler should also accept
allow_edited (:obj:`bool`): Determines whether the handler should also accept
edited messages.
pass_args (:obj:`bool`): Determines whether the handler should be passed
``args``.
@@ -59,16 +67,21 @@ class CommandHandler(Handler):
the callback function.
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a :obj:`dict` you
can use to keep any data in will be sent to the :attr:`callback` function. Related to
either the user or the chat that the update was sent in. For each update from the same user
or in the same chat, it will be the same ``dict``.
or in the same chat, it will be the same :obj:`dict`.
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Warning:
When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
command (:obj:`str` | List[:obj:`str`]): The command or list of commands this handler
should listen for. Limitations are the same as described here
@@ -86,53 +99,57 @@ class CommandHandler(Handler):
: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``.
edited messages. Default is :obj:`False`.
DEPRECATED: Edited is allowed by default. To change this behavior use
``~Filters.update.edited_message``.
pass_args (:obj:`bool`, optional): Determines whether the handler should be passed the
arguments passed to the command as a keyword argument called ``args``. It will contain
a list of strings, which is the text following the command split on single or
consecutive whitespace characters. Default is ``False``
consecutive whitespace characters. Default is :obj:`False`
DEPRECATED: Please switch to context based callbacks.
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
that contains new updates which can be used to insert updates. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
which can be used to schedule new jobs. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``user_data`` will be passed to the callback function. Default is :obj:`False`.
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``.
pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``chat_data`` will be passed to the callback function. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Defaults to :obj:`False`.
Raises:
ValueError - when command is too long or has illegal chars.
"""
def __init__(self,
command,
callback,
filters=None,
allow_edited=None,
pass_args=False,
pass_update_queue=False,
pass_job_queue=False,
pass_user_data=False,
pass_chat_data=False):
super(CommandHandler, self).__init__(
command: Union[str, List[str]],
callback: Callable[[HandlerArg, 'CallbackContext'], RT],
filters: BaseFilter = None,
allow_edited: bool = None,
pass_args: bool = False,
pass_update_queue: bool = False,
pass_job_queue: bool = False,
pass_user_data: bool = False,
pass_chat_data: bool = False,
run_async: bool = False):
super().__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
pass_chat_data=pass_chat_data)
pass_chat_data=pass_chat_data,
run_async=run_async)
if isinstance(command, string_types):
if isinstance(command, str):
self.command = [command.lower()]
else:
self.command = [x.lower() for x in command]
@@ -153,28 +170,31 @@ class CommandHandler(Handler):
self.filters &= ~Filters.update.edited_message
self.pass_args = pass_args
def check_update(self, update):
def check_update(
self,
update: HandlerArg) -> Optional[Union[bool, Tuple[List[str],
Optional[Union[bool, Dict]]]]]:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
update (:class:`telegram.Update`): Incoming telegram update.
Returns:
:obj:`list`: The list of args for the handler
:obj:`list`: The list of args for the handler.
"""
if isinstance(update, Update) and update.effective_message:
message = update.effective_message
if (message.entities and message.entities[0].type == MessageEntity.BOT_COMMAND
and message.entities[0].offset == 0):
and message.entities[0].offset == 0 and message.text and message.bot):
command = message.text[1:message.entities[0].length]
args = message.text.split()[1:]
command = command.split('@')
command.append(message.bot.username)
command_parts = command.split('@')
command_parts.append(message.bot.username)
if not (command[0].lower() in self.command
and command[1].lower() == message.bot.username.lower()):
if not (command_parts[0].lower() in self.command
and command_parts[1].lower() == message.bot.username.lower()):
return None
filter_result = self.filters(update)
@@ -182,17 +202,29 @@ class CommandHandler(Handler):
return args, filter_result
else:
return False
return None
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:
def collect_optional_args(
self,
dispatcher: 'Dispatcher',
update: HandlerArg = None,
check_result: Optional[Union[bool, Tuple[List[str],
Optional[bool]]]] = None) -> Dict[str, Any]:
optional_args = super().collect_optional_args(dispatcher, update)
if self.pass_args and isinstance(check_result, tuple):
optional_args['args'] = check_result[0]
return optional_args
def collect_additional_context(self, context, update, dispatcher, check_result):
context.args = check_result[0]
if isinstance(check_result[1], dict):
context.update(check_result[1])
def collect_additional_context(
self,
context: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]]) -> None:
if isinstance(check_result, tuple):
context.args = check_result[0]
if isinstance(check_result[1], dict):
context.update(check_result[1])
class PrefixHandler(CommandHandler):
@@ -215,7 +247,7 @@ class PrefixHandler(CommandHandler):
PrefixHandler(['!', '#'], 'test', callback) will respond to '!test' and
'#test'.
Miltiple prefixes and commands:
Multiple prefixes and commands:
PrefixHandler(['!', '#'], ['test', 'help`], callback) will respond to '!test',
'#test', '!help' and '#help'.
@@ -241,6 +273,7 @@ class PrefixHandler(CommandHandler):
the callback function.
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
@@ -251,6 +284,10 @@ class PrefixHandler(CommandHandler):
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Warning:
When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
prefix (: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
@@ -270,88 +307,93 @@ class PrefixHandler(CommandHandler):
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``
consecutive whitespace characters. Default is :obj:`False`
DEPRECATED: Please switch to context based callbacks.
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
that contains new updates which can be used to insert updates. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
which can be used to schedule new jobs. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``user_data`` will be passed to the callback function. Default is :obj:`False`.
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``.
pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``chat_data`` will be passed to the callback function. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Defaults to :obj:`False`.
"""
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):
prefix: Union[str, List[str]],
command: Union[str, List[str]],
callback: Callable[[HandlerArg, 'CallbackContext'], RT],
filters: BaseFilter = None,
pass_args: bool = False,
pass_update_queue: bool = False,
pass_job_queue: bool = False,
pass_user_data: bool = False,
pass_chat_data: bool = False,
run_async: bool = False):
self._prefix = list()
self._command = list()
self._commands = list()
self._prefix: List[str] = list()
self._command: List[str] = list()
self._commands: List[str] = list()
super(PrefixHandler, self).__init__(
super().__init__(
'nocommand', callback, filters=filters, allow_edited=None, pass_args=pass_args,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
pass_chat_data=pass_chat_data)
pass_chat_data=pass_chat_data,
run_async=run_async)
self.prefix = prefix
self.command = command
self.prefix = prefix # type: ignore[assignment]
self.command = command # type: ignore[assignment]
self._build_commands()
@property
def prefix(self):
def prefix(self) -> List[str]:
return self._prefix
@prefix.setter
def prefix(self, prefix):
if isinstance(prefix, string_types):
def prefix(self, prefix: Union[str, List[str]]) -> None:
if isinstance(prefix, str):
self._prefix = [prefix.lower()]
else:
self._prefix = prefix
self._build_commands()
@property
def command(self):
@property # type: ignore[override]
def command(self) -> List[str]: # type: ignore[override]
return self._command
@command.setter
def command(self, command):
if isinstance(command, string_types):
def command(self, command: Union[str, List[str]]) -> None:
if isinstance(command, str):
self._command = [command.lower()]
else:
self._command = command
self._build_commands()
def _build_commands(self):
def _build_commands(self) -> None:
self._commands = [x.lower() + y.lower() for x in self.prefix for y in self.command]
def check_update(self, update):
def check_update(self, update: HandlerArg) -> Optional[Union[bool, Tuple[List[str],
Optional[Union[bool, Dict]]]]]:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
update (:class:`telegram.Update`): Incoming telegram update.
Returns:
:obj:`list`: The list of args for the handler
:obj:`list`: The list of args for the handler.
"""
if isinstance(update, Update) and update.effective_message:
@@ -366,8 +408,15 @@ class PrefixHandler(CommandHandler):
return text_list[1:], filter_result
else:
return False
return None
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 collect_additional_context(
self,
context: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]]) -> None:
if isinstance(check_result, tuple):
context.args = check_result[0]
if isinstance(check_result[1], dict):
context.update(check_result[1])
+135 -90
View File
@@ -24,15 +24,28 @@ from threading import Lock
from telegram import Update
from telegram.ext import (Handler, CallbackQueryHandler, InlineQueryHandler,
ChosenInlineResultHandler, CallbackContext)
ChosenInlineResultHandler, CallbackContext, BasePersistence,
DispatcherHandlerStop)
from telegram.utils.promise import Promise
from telegram.utils.types import ConversationDict, HandlerArg
from typing import Dict, Any, List, Optional, Tuple, TYPE_CHECKING, cast, NoReturn
class _ConversationTimeoutContext(object):
def __init__(self, conversation_key, update, dispatcher):
if TYPE_CHECKING:
from telegram.ext import Dispatcher, Job
CheckUpdateType = Optional[Tuple[Tuple[int, ...], Handler, object]]
class _ConversationTimeoutContext:
def __init__(self,
conversation_key: Tuple[int, ...],
update: Update,
dispatcher: 'Dispatcher',
callback_context: Optional[CallbackContext]):
self.conversation_key = conversation_key
self.update = update
self.dispatcher = dispatcher
self.callback_context = callback_context
class ConversationHandler(Handler):
@@ -42,7 +55,7 @@ class ConversationHandler(Handler):
The first collection, a ``list`` named :attr:`entry_points`, is used to initiate the
conversation, for example with a :class:`telegram.ext.CommandHandler` or
:class:`telegram.ext.RegexHandler`.
:class:`telegram.ext.MessageHandler`.
The second collection, a ``dict`` named :attr:`states`, contains the different conversation
steps and one or more associated handlers that should be used if the user sends a message when
@@ -58,8 +71,8 @@ class ConversationHandler(Handler):
user know their message was not recognized.
To change the state of conversation, the callback function of a handler must return the new
state after responding to the user. If it does not return anything (returning ``None`` by
default), the state will not change. If an entry point callback function returns None,
state after responding to the user. If it does not return anything (returning :obj:`None` by
default), the state will not change. If an entry point callback function returns :obj:`None`,
the conversation ends immediately after the execution of this callback function.
To end the conversation, the callback function must return :attr:`END` or ``-1``. To
handle the conversation timeout, use handler :attr:`TIMEOUT` or ``-2``.
@@ -86,7 +99,7 @@ class ConversationHandler(Handler):
associated ``Handler`` objects that should be used in that state.
fallbacks (List[:class:`telegram.ext.Handler`]): A list of handlers that might be used if
the user is in a conversation, but every handler for their current state returned
``False`` on :attr:`check_update`.
:obj:`False` on :attr:`check_update`.
allow_reentry (:obj:`bool`): Determines if a user can restart a conversation with
an entry point.
per_chat (:obj:`bool`): If the conversationkey should contain the Chat's ID.
@@ -96,8 +109,9 @@ class ConversationHandler(Handler):
conversation_timeout (:obj:`float` | :obj:`datetime.timedelta`): Optional. When this
handler is inactive more than this timeout (in seconds), it will be automatically
ended. If this value is 0 (default), there will be no timeout. When it's triggered, the
last received update will be handled by ALL the handler's who's `check_update` method
returns True that are in the state :attr:`ConversationHandler.TIMEOUT`.
last received update and the corresponding ``context`` will be handled by ALL the
handler's who's :attr:`check_update` method returns :obj:`True` that are in the state
:attr:`ConversationHandler.TIMEOUT`.
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
@@ -109,31 +123,33 @@ class ConversationHandler(Handler):
Args:
entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
trigger the start of the conversation. The first handler which :attr:`check_update`
method returns ``True`` will be used. If all return ``False``, the update is not
method returns :obj:`True` will be used. If all return :obj:`False`, the update is not
handled.
states (Dict[:obj:`object`, List[:class:`telegram.ext.Handler`]]): A :obj:`dict` that
defines the different states of conversation a user can be in and one or more
associated ``Handler`` objects that should be used in that state. The first handler
which :attr:`check_update` method returns ``True`` will be used.
which :attr:`check_update` method returns :obj:`True` will be used.
fallbacks (List[:class:`telegram.ext.Handler`]): A list of handlers that might be used if
the user is in a conversation, but every handler for their current state returned
``False`` on :attr:`check_update`. The first handler which :attr:`check_update` method
returns ``True`` will be used. If all return ``False``, the update is not handled.
allow_reentry (:obj:`bool`, optional): If set to ``True``, a user that is currently in a
:obj:`False` on :attr:`check_update`. The first handler which :attr:`check_update`
method returns :obj:`True` will be used. If all return :obj:`False`, the update is not
handled.
allow_reentry (:obj:`bool`, optional): If set to :obj:`True`, a user that is currently in a
conversation can restart the conversation by triggering one of the entry points.
per_chat (:obj:`bool`, optional): If the conversationkey should contain the Chat's ID.
Default is ``True``.
Default is :obj:`True`.
per_user (:obj:`bool`, optional): If the conversationkey should contain the User's ID.
Default is ``True``.
Default is :obj:`True`.
per_message (:obj:`bool`, optional): If the conversationkey should contain the Message's
ID. Default is ``False``.
ID. Default is :obj:`False`.
conversation_timeout (:obj:`float` | :obj:`datetime.timedelta`, optional): When this
handler is inactive more than this timeout (in seconds), it will be automatically
ended. If this value is 0 or 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`.
ended. If this value is 0 or :obj:`None` (default), there will be no timeout. The last
received update and the corresponding ``context`` will be handled by ALL the handler's
who's :attr:`check_update` method returns :obj:`True` that are in the state
:attr:`ConversationHandler.TIMEOUT`.
name (:obj:`str`, optional): The name for this conversationhandler. Required for
persistence
persistence.
persistent (:obj:`bool`, optional): If the conversations dict for this handler should be
saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater`
map_to_parent (Dict[:obj:`object`, :obj:`object`], optional): A :obj:`dict` that can be
@@ -153,17 +169,18 @@ class ConversationHandler(Handler):
previous ``@run_sync`` decorated running handler to finish."""
def __init__(self,
entry_points,
states,
fallbacks,
allow_reentry=False,
per_chat=True,
per_user=True,
per_message=False,
conversation_timeout=None,
name=None,
persistent=False,
map_to_parent=None):
entry_points: List[Handler],
states: Dict[object, List[Handler]],
fallbacks: List[Handler],
allow_reentry: bool = False,
per_chat: bool = True,
per_user: bool = True,
per_message: bool = False,
conversation_timeout: int = None,
name: str = None,
persistent: bool = False,
map_to_parent: Dict[object, object] = None):
self.run_async = False
self._entry_points = entry_points
self._states = states
@@ -177,15 +194,15 @@ class ConversationHandler(Handler):
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.
self.persistent: bool = persistent
self._persistence: Optional[BasePersistence] = None
""":obj:`telegram.ext.BasePersistence`: The persistence used to store conversations.
Set by dispatcher"""
self._map_to_parent = map_to_parent
self.timeout_jobs = dict()
self.timeout_jobs: Dict[Tuple[int, ...], 'Job'] = dict()
self._timeout_jobs_lock = Lock()
self._conversations = dict()
self._conversations: ConversationDict = dict()
self._conversations_lock = Lock()
self.logger = logging.getLogger(__name__)
@@ -226,92 +243,92 @@ class ConversationHandler(Handler):
break
@property
def entry_points(self):
def entry_points(self) -> List[Handler]:
return self._entry_points
@entry_points.setter
def entry_points(self, value):
def entry_points(self, value: Any) -> NoReturn:
raise ValueError('You can not assign a new value to entry_points after initialization.')
@property
def states(self):
def states(self) -> Dict[object, List[Handler]]:
return self._states
@states.setter
def states(self, value):
def states(self, value: Any) -> NoReturn:
raise ValueError('You can not assign a new value to states after initialization.')
@property
def fallbacks(self):
def fallbacks(self) -> List[Handler]:
return self._fallbacks
@fallbacks.setter
def fallbacks(self, value):
def fallbacks(self, value: Any) -> NoReturn:
raise ValueError('You can not assign a new value to fallbacks after initialization.')
@property
def allow_reentry(self):
def allow_reentry(self) -> bool:
return self._allow_reentry
@allow_reentry.setter
def allow_reentry(self, value):
def allow_reentry(self, value: Any) -> NoReturn:
raise ValueError('You can not assign a new value to allow_reentry after initialization.')
@property
def per_user(self):
def per_user(self) -> bool:
return self._per_user
@per_user.setter
def per_user(self, value):
def per_user(self, value: Any) -> NoReturn:
raise ValueError('You can not assign a new value to per_user after initialization.')
@property
def per_chat(self):
def per_chat(self) -> bool:
return self._per_chat
@per_chat.setter
def per_chat(self, value):
def per_chat(self, value: Any) -> NoReturn:
raise ValueError('You can not assign a new value to per_chat after initialization.')
@property
def per_message(self):
def per_message(self) -> bool:
return self._per_message
@per_message.setter
def per_message(self, value):
def per_message(self, value: Any) -> NoReturn:
raise ValueError('You can not assign a new value to per_message after initialization.')
@property
def conversation_timeout(self):
def conversation_timeout(self) -> Optional[int]:
return self._conversation_timeout
@conversation_timeout.setter
def conversation_timeout(self, value):
def conversation_timeout(self, value: Any) -> NoReturn:
raise ValueError('You can not assign a new value to conversation_timeout after '
'initialization.')
@property
def name(self):
def name(self) -> Optional[str]:
return self._name
@name.setter
def name(self, value):
def name(self, value: Any) -> NoReturn:
raise ValueError('You can not assign a new value to name after initialization.')
@property
def map_to_parent(self):
def map_to_parent(self) -> Optional[Dict[object, object]]:
return self._map_to_parent
@map_to_parent.setter
def map_to_parent(self, value):
def map_to_parent(self, value: Any) -> NoReturn:
raise ValueError('You can not assign a new value to map_to_parent after initialization.')
@property
def persistence(self):
def persistence(self) -> Optional[BasePersistence]:
return self._persistence
@persistence.setter
def persistence(self, persistence):
def persistence(self, persistence: BasePersistence) -> None:
self._persistence = persistence
# Set persistence for nested conversations
for handlers in self.states.values():
@@ -320,37 +337,37 @@ class ConversationHandler(Handler):
handler.persistence = self.persistence
@property
def conversations(self):
def conversations(self) -> ConversationDict:
return self._conversations
@conversations.setter
def conversations(self, value):
def conversations(self, value: ConversationDict) -> None:
self._conversations = value
# Set conversations for nested conversations
for handlers in self.states.values():
for handler in handlers:
if isinstance(handler, ConversationHandler):
if isinstance(handler, ConversationHandler) and self.persistence and handler.name:
handler.conversations = self.persistence.get_conversations(handler.name)
def _get_key(self, update):
def _get_key(self, update: Update) -> Tuple[int, ...]:
chat = update.effective_chat
user = update.effective_user
key = list()
if self.per_chat:
key.append(chat.id)
key.append(chat.id) # type: ignore[union-attr]
if self.per_user and user is not None:
key.append(user.id)
if self.per_message:
key.append(update.callback_query.inline_message_id
or update.callback_query.message.message_id)
key.append(update.callback_query.inline_message_id # type: ignore[union-attr]
or update.callback_query.message.message_id) # type: ignore[union-attr]
return tuple(key)
def check_update(self, update):
def check_update(self, update: HandlerArg) -> CheckUpdateType:
"""
Determines whether an update should be handled by this conversationhandler, and if so in
which state the conversation currently is.
@@ -394,14 +411,14 @@ class ConversationHandler(Handler):
with self._conversations_lock:
state = self.conversations.get(key)
else:
handlers = self.states.get(self.WAITING, [])
for handler in handlers:
check = handler.check_update(update)
hdlrs = self.states.get(self.WAITING, [])
for hdlr in hdlrs:
check = hdlr.check_update(update)
if check is not None and check is not False:
return key, handler, check
return key, hdlr, check
return None
self.logger.debug('selecting conversation %s with state %s' % (str(key), str(state)))
self.logger.debug('selecting conversation {} with state {}'.format(str(key), str(state)))
handler = None
@@ -438,9 +455,13 @@ class ConversationHandler(Handler):
else:
return None
return key, handler, check
return key, handler, check # type: ignore[return-value]
def handle_update(self, update, dispatcher, check_result, context=None):
def handle_update(self, # type: ignore[override]
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: CheckUpdateType,
context: CallbackContext = None) -> Optional[object]:
"""Send the update to the callback for the current state and Handler
Args:
@@ -448,9 +469,13 @@ class ConversationHandler(Handler):
handler, and the handler's check result.
update (:class:`telegram.Update`): Incoming telegram update.
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
context (:class:`telegram.ext.CallbackContext`, optional): The context as provided by
the dispatcher.
"""
conversation_key, handler, check_result = check_result
update = cast(Update, update) # for mypy
conversation_key, handler, check_result = check_result # type: ignore[assignment,misc]
raise_dp_handler_stop = False
with self._timeout_jobs_lock:
# Remove the old timeout job (if present)
@@ -458,54 +483,69 @@ class ConversationHandler(Handler):
if timeout_job is not None:
timeout_job.schedule_removal()
new_state = handler.handle_update(update, dispatcher, check_result, context)
try:
new_state = handler.handle_update(update, dispatcher, check_result, context)
except DispatcherHandlerStop as e:
new_state = e.state
raise_dp_handler_stop = True
with self._timeout_jobs_lock:
if self.conversation_timeout and new_state != self.END:
if self.conversation_timeout and new_state != self.END and dispatcher.job_queue:
# Add the new timeout job
self.timeout_jobs[conversation_key] = dispatcher.job_queue.run_once(
self._trigger_timeout, self.conversation_timeout,
context=_ConversationTimeoutContext(conversation_key, update, dispatcher))
self._trigger_timeout, self.conversation_timeout, # type: ignore[arg-type]
context=_ConversationTimeoutContext(conversation_key, update,
dispatcher, context))
if isinstance(self.map_to_parent, dict) and new_state in self.map_to_parent:
self.update_state(self.END, conversation_key)
return self.map_to_parent.get(new_state)
if raise_dp_handler_stop:
raise DispatcherHandlerStop(self.map_to_parent.get(new_state))
else:
return self.map_to_parent.get(new_state)
else:
self.update_state(new_state, conversation_key)
if raise_dp_handler_stop:
# Don't pass the new state here. If we're in a nested conversation, the parent is
# expecting None as return value.
raise DispatcherHandlerStop()
return None
def update_state(self, new_state, key):
def update_state(self,
new_state: object,
key: Tuple[int, ...]) -> None:
if new_state == self.END:
with self._conversations_lock:
if key in self.conversations:
# If there is no key in conversations, nothing is done.
del self.conversations[key]
if self.persistent:
if self.persistent and self.persistence and self.name:
self.persistence.update_conversation(self.name, key, None)
elif isinstance(new_state, Promise):
with self._conversations_lock:
self.conversations[key] = (self.conversations.get(key), new_state)
if self.persistent:
if self.persistent and self.persistence and self.name:
self.persistence.update_conversation(self.name, key,
(self.conversations.get(key), new_state))
elif new_state is not None:
with self._conversations_lock:
self.conversations[key] = new_state
if self.persistent:
if self.persistent and self.persistence and self.name:
self.persistence.update_conversation(self.name, key, new_state)
def _trigger_timeout(self, context, job=None):
def _trigger_timeout(self,
context: _ConversationTimeoutContext,
job: 'Job' = None) -> None:
self.logger.debug('conversation timeout was triggered!')
# Backward compatibility with bots that do not use CallbackContext
callback_context = None
if isinstance(context, CallbackContext):
job = context.job
callback_context = context
context = job.context
context = job.context # type:ignore[union-attr,assignment]
callback_context = context.callback_context
with self._timeout_jobs_lock:
found_job = self.timeout_jobs[context.conversation_key]
@@ -518,5 +558,10 @@ class ConversationHandler(Handler):
for handler in handlers:
check = handler.check_update(context.update)
if check is not None and check is not False:
handler.handle_update(context.update, context.dispatcher, check, callback_context)
try:
handler.handle_update(context.update, context.dispatcher, check,
callback_context)
except DispatcherHandlerStop:
self.logger.warning('DispatcherHandlerStop in TIMEOUT state of '
'ConversationHandler has no effect. Ignoring.')
self.update_state(self.END, context.conversation_key)
+48 -28
View File
@@ -17,8 +17,10 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the class Defaults, which allows to pass default values to Updater."""
import pytz
from typing import Union, Optional, Any, NoReturn
from telegram.utils.helpers import DEFAULT_NONE
from telegram.utils.helpers import DEFAULT_NONE, DefaultValue
class Defaults:
@@ -26,7 +28,7 @@ class Defaults:
Attributes:
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width toxt or URLs in your bot's message.
bold, italic, fixed-width text or URLs in your bot's message.
disable_notification (:obj:`bool`): Optional. Sends the message silently. Users will
receive a notification with no sound.
disable_web_page_preview (:obj:`bool`): Optional. Disables link previews for links in this
@@ -34,13 +36,15 @@ class Defaults:
timeout (:obj:`int` | :obj:`float`): Optional. If this value is specified, use it as the
read timeout from the server (instead of the one specified during creation of the
connection pool).
quote (:obj:`bool`): Optional. If set to ``True``, the reply is sent as an actual reply to
the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will
be ignored. Default: ``True`` in group chats and ``False`` in private chats.
quote (:obj:`bool`): Optional. If set to :obj:`True`, the reply is sent as an actual reply
to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will
be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats.
tzinfo (:obj:`tzinfo`): A timezone to be used for all date(time) objects appearing
throughout PTB.
Parameters:
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width toxt or URLs in your bot's message.
bold, italic, fixed-width text or URLs in your bot's message.
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
receive a notification with no sound.
disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in this
@@ -48,80 +52,96 @@ class Defaults:
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as the
read timeout from the server (instead of the one specified during creation of the
connection pool).
quote (:obj:`bool`, opitonal): If set to ``True``, the reply is sent as an actual reply to
the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will
be ignored. Default: ``True`` in group chats and ``False`` in private chats.
quote (:obj:`bool`, optional): If set to :obj:`True`, the reply is sent as an actual reply
to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will
be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats.
tzinfo (:obj:`tzinfo`, optional): A timezone to be used for all date(time) inputs
appearing throughout PTB, i.e. if a timezone naive date(time) object is passed
somewhere, it will be assumed to be in ``tzinfo``. Must be a timezone provided by the
``pytz`` module. Defaults to UTC.
"""
def __init__(self,
parse_mode=None,
disable_notification=None,
disable_web_page_preview=None,
parse_mode: str = None,
disable_notification: bool = None,
disable_web_page_preview: bool = None,
# Timeout needs special treatment, since the bot methods have two different
# default values for timeout (None and 20s)
timeout=DEFAULT_NONE,
quote=None):
timeout: Union[float, DefaultValue] = DEFAULT_NONE,
quote: bool = None,
tzinfo: pytz.BaseTzInfo = pytz.utc):
self._parse_mode = parse_mode
self._disable_notification = disable_notification
self._disable_web_page_preview = disable_web_page_preview
self._timeout = timeout
self._quote = quote
self._tzinfo = tzinfo
@property
def parse_mode(self):
def parse_mode(self) -> Optional[str]:
return self._parse_mode
@parse_mode.setter
def parse_mode(self, value):
def parse_mode(self, value: Any) -> NoReturn:
raise AttributeError("You can not assign a new value to defaults after because it would "
"not have any effect.")
@property
def disable_notification(self):
def disable_notification(self) -> Optional[bool]:
return self._disable_notification
@disable_notification.setter
def disable_notification(self, value):
def disable_notification(self, value: Any) -> NoReturn:
raise AttributeError("You can not assign a new value to defaults after because it would "
"not have any effect.")
@property
def disable_web_page_preview(self):
def disable_web_page_preview(self) -> Optional[bool]:
return self._disable_web_page_preview
@disable_web_page_preview.setter
def disable_web_page_preview(self, value):
def disable_web_page_preview(self, value: Any) -> NoReturn:
raise AttributeError("You can not assign a new value to defaults after because it would "
"not have any effect.")
@property
def timeout(self):
def timeout(self) -> Union[float, DefaultValue]:
return self._timeout
@timeout.setter
def timeout(self, value):
def timeout(self, value: Any) -> NoReturn:
raise AttributeError("You can not assign a new value to defaults after because it would "
"not have any effect.")
@property
def quote(self):
def quote(self) -> Optional[bool]:
return self._quote
@quote.setter
def quote(self, value):
def quote(self, value: Any) -> NoReturn:
raise AttributeError("You can not assign a new value to defaults after because it would "
"not have any effect.")
def __hash__(self):
@property
def tzinfo(self) -> pytz.BaseTzInfo:
return self._tzinfo
@tzinfo.setter
def tzinfo(self, value: Any) -> NoReturn:
raise AttributeError("You can not assign a new value to defaults after because it would "
"not have any effect.")
def __hash__(self) -> int:
return hash((self._parse_mode,
self._disable_notification,
self._disable_web_page_preview,
self._timeout,
self._quote))
self._quote,
self._tzinfo))
def __eq__(self, other):
def __eq__(self, other: object) -> bool:
if isinstance(other, Defaults):
return self.__dict__ == other.__dict__
return False
def __ne__(self, other):
def __ne__(self, other: object) -> bool:
return not self == other
+75 -47
View File
@@ -25,14 +25,32 @@ from telegram.utils.helpers import decode_user_chat_data_from_json,\
try:
import ujson as json
except ImportError:
import json
import json # type: ignore[no-redef]
from collections import defaultdict
from telegram.ext import BasePersistence
from typing import DefaultDict, Dict, Any, Tuple, Optional
from telegram.utils.types import ConversationDict
class DictPersistence(BasePersistence):
"""Using python's dicts and json for making your bot persistent.
Note:
This class does *not* implement a :meth:`flush` method, meaning that data managed by
``DictPersistence`` is in-memory only and will be lost when the bot shuts down. This is,
because ``DictPersistence`` is mainly intended as starting point for custom persistence
classes that need to JSON-serialize the stored data before writing them to file/database.
Warning:
:class:`DictPersistence` will try to replace :class:`telegram.Bot` instances by
:attr:`REPLACED_BOT` and insert the bot set with
:meth:`telegram.ext.BasePersistence.set_bot` upon loading of the data. This is to ensure
that changes to the bot apply to the saved objects, too. If you change the bots token, this
may lead to e.g. ``Chat not found`` errors. For the limitations on replacing bots see
:meth:`telegram.ext.BasePersistence.replace_bot` and
:meth:`telegram.ext.BasePersistence.insert_bot`.
Attributes:
store_user_data (:obj:`bool`): Whether user_data should be saved by this
persistence class.
@@ -43,11 +61,11 @@ class DictPersistence(BasePersistence):
Args:
store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this
persistence class. Default is ``True``.
persistence class. Default is :obj:`True`.
store_chat_data (:obj:`bool`, optional): Whether user_data should be saved by this
persistence class. Default is ``True``.
persistence class. Default is :obj:`True`.
store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this
persistence class. Default is ``True`` .
persistence class. Default is :obj:`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
@@ -59,16 +77,16 @@ class DictPersistence(BasePersistence):
"""
def __init__(self,
store_user_data=True,
store_chat_data=True,
store_bot_data=True,
user_data_json='',
chat_data_json='',
bot_data_json='',
conversations_json=''):
super(DictPersistence, self).__init__(store_user_data=store_user_data,
store_chat_data=store_chat_data,
store_bot_data=store_bot_data)
store_user_data: bool = True,
store_chat_data: bool = True,
store_bot_data: bool = True,
user_data_json: str = '',
chat_data_json: str = '',
bot_data_json: str = '',
conversations_json: str = ''):
super().__init__(store_user_data=store_user_data,
store_chat_data=store_chat_data,
store_bot_data=store_bot_data)
self._user_data = None
self._chat_data = None
self._bot_data = None
@@ -106,12 +124,12 @@ class DictPersistence(BasePersistence):
raise TypeError("Unable to deserialize conversations_json. Not valid JSON")
@property
def user_data(self):
""":obj:`dict`: The user_data as a dict"""
def user_data(self) -> Optional[DefaultDict[int, Dict]]:
""":obj:`dict`: The user_data as a dict."""
return self._user_data
@property
def user_data_json(self):
def user_data_json(self) -> str:
""":obj:`str`: The user_data serialized as a JSON-string."""
if self._user_data_json:
return self._user_data_json
@@ -119,12 +137,12 @@ class DictPersistence(BasePersistence):
return json.dumps(self.user_data)
@property
def chat_data(self):
""":obj:`dict`: The chat_data as a dict"""
def chat_data(self) -> Optional[DefaultDict[int, Dict]]:
""":obj:`dict`: The chat_data as a dict."""
return self._chat_data
@property
def chat_data_json(self):
def chat_data_json(self) -> str:
""":obj:`str`: The chat_data serialized as a JSON-string."""
if self._chat_data_json:
return self._chat_data_json
@@ -132,12 +150,12 @@ class DictPersistence(BasePersistence):
return json.dumps(self.chat_data)
@property
def bot_data(self):
""":obj:`dict`: The bot_data as a dict"""
def bot_data(self) -> Optional[Dict]:
""":obj:`dict`: The bot_data as a dict."""
return self._bot_data
@property
def bot_data_json(self):
def bot_data_json(self) -> str:
""":obj:`str`: The bot_data serialized as a JSON-string."""
if self._bot_data_json:
return self._bot_data_json
@@ -145,20 +163,21 @@ class DictPersistence(BasePersistence):
return json.dumps(self.bot_data)
@property
def conversations(self):
""":obj:`dict`: The conversations as a dict"""
def conversations(self) -> Optional[Dict[str, Dict[Tuple, Any]]]:
""":obj:`dict`: The conversations as a dict."""
return self._conversations
@property
def conversations_json(self):
def conversations_json(self) -> str:
""":obj:`str`: The conversations serialized as a JSON-string."""
if self._conversations_json:
return self._conversations_json
else:
return encode_conversations_to_json(self.conversations)
return encode_conversations_to_json(self.conversations) # type: ignore[arg-type]
def get_user_data(self):
"""Returns the user_data created from the ``user_data_json`` or an empty defaultdict.
def get_user_data(self) -> DefaultDict[int, Dict[Any, Any]]:
"""Returns the user_data created from the ``user_data_json`` or an empty
:obj:`defaultdict`.
Returns:
:obj:`defaultdict`: The restored user data.
@@ -167,83 +186,92 @@ class DictPersistence(BasePersistence):
pass
else:
self._user_data = defaultdict(dict)
return deepcopy(self.user_data)
return deepcopy(self.user_data) # type: ignore[arg-type]
def get_chat_data(self):
"""Returns the chat_data created from the ``chat_data_json`` or an empty defaultdict.
def get_chat_data(self) -> DefaultDict[int, Dict[Any, Any]]:
"""Returns the chat_data created from the ``chat_data_json`` or an empty
:obj:`defaultdict`.
Returns:
:obj:`defaultdict`: The restored user data.
:obj:`defaultdict`: The restored chat data.
"""
if self.chat_data:
pass
else:
self._chat_data = defaultdict(dict)
return deepcopy(self.chat_data)
return deepcopy(self.chat_data) # type: ignore[arg-type]
def get_bot_data(self):
"""Returns the bot_data created from the ``bot_data_json`` or an empty dict.
def get_bot_data(self) -> Dict[Any, Any]:
"""Returns the bot_data created from the ``bot_data_json`` or an empty :obj:`dict`.
Returns:
:obj:`defaultdict`: The restored user data.
:obj:`dict`: The restored bot data.
"""
if self.bot_data:
pass
else:
self._bot_data = {}
return deepcopy(self.bot_data)
return deepcopy(self.bot_data) # type: ignore[arg-type]
def get_conversations(self, name):
def get_conversations(self, name: str) -> ConversationDict:
"""Returns the conversations created from the ``conversations_json`` or an empty
defaultdict.
:obj:`dict`.
Returns:
:obj:`defaultdict`: The restored user data.
:obj:`dict`: The restored conversations data.
"""
if self.conversations:
pass
else:
self._conversations = {}
return self.conversations.get(name, {}).copy()
return self.conversations.get(name, {}).copy() # type: ignore[union-attr]
def update_conversation(self, name, key, new_state):
def update_conversation(self,
name: str, key: Tuple[int, ...],
new_state: Optional[object]) -> None:
"""Will update the conversations for the given handler.
Args:
name (:obj:`str`): The handlers name.
name (:obj:`str`): The handler's name.
key (:obj:`tuple`): The key the state is changed for.
new_state (:obj:`tuple` | :obj:`any`): The new state for the given key.
"""
if not self._conversations:
self._conversations = {}
if self._conversations.setdefault(name, {}).get(key) == new_state:
return
self._conversations[name][key] = new_state
self._conversations_json = None
def update_user_data(self, user_id, data):
def update_user_data(self, user_id: int, data: Dict) -> None:
"""Will update the user_data (if changed).
Args:
user_id (:obj:`int`): The user the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data` [user_id].
"""
if self._user_data is None:
self._user_data = defaultdict(dict)
if self._user_data.get(user_id) == data:
return
self._user_data[user_id] = data
self._user_data_json = None
def update_chat_data(self, chat_id, data):
def update_chat_data(self, chat_id: int, data: Dict) -> None:
"""Will update the chat_data (if changed).
Args:
chat_id (:obj:`int`): The chat the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data` [chat_id].
"""
if self._chat_data is None:
self._chat_data = defaultdict(dict)
if self._chat_data.get(chat_id) == data:
return
self._chat_data[chat_id] = data
self._chat_data_json = None
def update_bot_data(self, data):
def update_bot_data(self, data: Dict) -> None:
"""Will update the bot_data (if changed).
Args:
+250 -137
View File
@@ -29,8 +29,6 @@ from collections import defaultdict
from queue import Queue, Empty
from future.builtins import range
from telegram import TelegramError, Update
from telegram.ext.handler import Handler
from telegram.ext.callbackcontext import CallbackContext
@@ -38,11 +36,19 @@ from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.utils.promise import Promise
from telegram.ext import BasePersistence
logging.getLogger(__name__).addHandler(logging.NullHandler())
from typing import Any, Callable, TYPE_CHECKING, Optional, Union, DefaultDict, Dict, List, Set
from telegram.utils.types import HandlerArg
if TYPE_CHECKING:
from telegram import Bot
from telegram.ext import JobQueue
DEFAULT_GROUP = 0
def run_async(func):
def run_async(func: Callable[[Update, CallbackContext],
Any]) -> Callable[[Update, CallbackContext], Any]:
"""
Function decorator that will run the function in a new thread.
@@ -50,24 +56,52 @@ def run_async(func):
Using this decorator is only possible when only a single Dispatcher exist in the system.
Note:
DEPRECATED. Use :attr:`telegram.ext.Dispatcher.run_async` directly instead or the
:attr:`Handler.run_async` parameter.
Warning:
If you're using @run_async you cannot rely on adding custom attributes to
If you're using ``@run_async`` you cannot rely on adding custom attributes to
:class:`telegram.ext.CallbackContext`. See its docs for more info.
"""
@wraps(func)
def async_func(*args, **kwargs):
return Dispatcher.get_instance().run_async(func, *args, **kwargs)
def async_func(*args: Any, **kwargs: Any) -> Any:
warnings.warn('The @run_async decorator is deprecated. Use the `run_async` parameter of'
'`Dispatcher.add_handler` or `Dispatcher.run_async` instead.',
TelegramDeprecationWarning,
stacklevel=2)
return Dispatcher.get_instance()._run_async(func, *args, update=None, error_handling=False,
**kwargs)
return async_func
class DispatcherHandlerStop(Exception):
"""Raise this in handler to prevent execution any other handler (even in different group)."""
pass
"""
Raise this in handler to prevent execution any other handler (even in different group).
In order to use this exception in a :class:`telegram.ext.ConversationHandler`, pass the
optional ``state`` parameter instead of returning the next state:
.. code-block:: python
def callback(update, context):
...
raise DispatcherHandlerStop(next_state)
Attributes:
state (:obj:`object`): Optional. The next state of the conversation.
Args:
state (:obj:`object`, optional): The next state of the conversation.
"""
def __init__(self, state: object = None) -> None:
super().__init__()
self.state = state
class Dispatcher(object):
class Dispatcher:
"""This class dispatches all kinds of updates to its registered handlers.
Attributes:
@@ -75,13 +109,13 @@ class Dispatcher(object):
update_queue (:obj:`Queue`): The synchronized queue that will contain the updates.
job_queue (:class:`telegram.ext.JobQueue`): Optional. The :class:`telegram.ext.JobQueue`
instance to pass onto handler callbacks.
workers (:obj:`int`): Number of maximum concurrent worker threads for the ``@run_async``
decorator.
workers (:obj:`int`, optional): Number of maximum concurrent worker threads for the
``@run_async`` decorator and :meth:`run_async`.
user_data (:obj:`defaultdict`): A dictionary handlers can use to store data for the user.
chat_data (:obj:`defaultdict`): A dictionary handlers can use to store data for the chat.
bot_data (:obj:`dict`): A dictionary handlers can use to store data for the bot.
persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to
store data that should be persistent over restarts
store data that should be persistent over restarts.
Args:
bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers.
@@ -89,12 +123,12 @@ class Dispatcher(object):
job_queue (:class:`telegram.ext.JobQueue`, optional): The :class:`telegram.ext.JobQueue`
instance to pass onto handler callbacks.
workers (:obj:`int`, optional): Number of maximum concurrent worker threads for the
``@run_async`` decorator. defaults to 4.
``@run_async`` decorator and :meth:`run_async`. Defaults to 4.
persistence (:class:`telegram.ext.BasePersistence`, optional): The persistence class to
store data that should be persistent over restarts
use_context (:obj:`bool`, optional): If set to ``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``.
store data that should be persistent over restarts.
use_context (:obj:`bool`, optional): If set to :obj:`True` uses the context based callback
API (ignored if `dispatcher` argument is used). Defaults to :obj:`True`.
**New users**: set this to :obj:`True`.
"""
@@ -104,13 +138,13 @@ class Dispatcher(object):
logger = logging.getLogger(__name__)
def __init__(self,
bot,
update_queue,
workers=4,
exception_event=None,
job_queue=None,
persistence=None,
use_context=False):
bot: 'Bot',
update_queue: Queue,
workers: int = 4,
exception_event: Event = None,
job_queue: 'JobQueue' = None,
persistence: BasePersistence = None,
use_context: bool = True):
self.bot = bot
self.update_queue = update_queue
self.job_queue = job_queue
@@ -121,13 +155,16 @@ class Dispatcher(object):
warnings.warn('Old Handler API is deprecated - see https://git.io/fxJuV for details',
TelegramDeprecationWarning, stacklevel=3)
self.user_data = defaultdict(dict)
self.chat_data = defaultdict(dict)
self.user_data: DefaultDict[int, Dict[Any, Any]] = defaultdict(dict)
self.chat_data: DefaultDict[int, Dict[Any, Any]] = defaultdict(dict)
self.bot_data = {}
self.persistence: Optional[BasePersistence] = None
self._update_persistence_lock = Lock()
if persistence:
if not isinstance(persistence, BasePersistence):
raise TypeError("persistence should be based on telegram.ext.BasePersistence")
raise TypeError("persistence must be based on telegram.ext.BasePersistence")
self.persistence = persistence
self.persistence.set_bot(self.bot)
if self.persistence.store_user_data:
self.user_data = self.persistence.get_user_data()
if not isinstance(self.user_data, defaultdict):
@@ -143,33 +180,34 @@ class Dispatcher(object):
else:
self.persistence = None
self.handlers = {}
self.handlers: Dict[int, List[Handler]] = {}
"""Dict[:obj:`int`, List[:class:`telegram.ext.Handler`]]: Holds the handlers per group."""
self.groups = []
self.groups: List[int] = []
"""List[:obj:`int`]: A list with all groups."""
self.error_handlers = []
"""List[:obj:`callable`]: A list of errorHandlers."""
self.error_handlers: Dict[Callable, bool] = {}
"""Dict[:obj:`callable`, :obj:`bool`]: A dict, where the keys are error handlers and the
values indicate whether they are to be run asynchronously."""
self.running = False
""":obj:`bool`: Indicates if this dispatcher is running."""
self.__stop_event = Event()
self.__exception_event = exception_event or Event()
self.__async_queue = Queue()
self.__async_threads = set()
self.__async_queue: Queue = Queue()
self.__async_threads: Set[Thread] = set()
# For backward compatibility, we allow a "singleton" mode for the dispatcher. When there's
# only one instance of Dispatcher, it will be possible to use the `run_async` decorator.
with self.__singleton_lock:
if self.__singleton_semaphore.acquire(blocking=0):
if self.__singleton_semaphore.acquire(blocking=False):
self._set_singleton(self)
else:
self._set_singleton(None)
@property
def exception_event(self):
def exception_event(self) -> Event:
return self.__exception_event
def _init_async_threads(self, base_name, workers):
def _init_async_threads(self, base_name: str, workers: int) -> None:
base_name = '{}_'.format(base_name) if base_name else ''
for i in range(workers):
@@ -179,12 +217,12 @@ class Dispatcher(object):
thread.start()
@classmethod
def _set_singleton(cls, val):
def _set_singleton(cls, val: Optional['Dispatcher']) -> None:
cls.logger.debug('Setting singleton dispatcher as %s', val)
cls.__singleton = weakref.ref(val) if val else None
@classmethod
def get_instance(cls):
def get_instance(cls) -> 'Dispatcher':
"""Get the singleton instance of this class.
Returns:
@@ -195,12 +233,12 @@ class Dispatcher(object):
"""
if cls.__singleton is not None:
return cls.__singleton() # pylint: disable=not-callable
return cls.__singleton() # type: ignore[return-value] # pylint: disable=not-callable
else:
raise RuntimeError('{} not initialized or multiple instances exist'.format(
cls.__name__))
def _pooled(self):
def _pooled(self) -> None:
thr_name = current_thread().getName()
while 1:
promise = self.__async_queue.get()
@@ -212,34 +250,78 @@ class Dispatcher(object):
break
promise.run()
if not promise.exception:
self.update_persistence(update=promise.update)
continue
if isinstance(promise.exception, DispatcherHandlerStop):
self.logger.warning(
'DispatcherHandlerStop is not supported with async functions; func: %s',
promise.pooled_function.__name__)
continue
def run_async(self, func, *args, **kwargs):
"""Queue a function (with given args/kwargs) to be run asynchronously.
# Avoid infinite recursion of error handlers.
if promise.pooled_function in self.error_handlers:
self.logger.error('An uncaught error was raised while handling the error.')
continue
# Don't perform error handling for a `Promise` with deactivated error handling. This
# should happen only via the deprecated `@run_async` decorator or `Promises` created
# within error handlers
if not promise.error_handling:
self.logger.error('A promise with deactivated error handling raised an error.')
continue
# If we arrive here, an exception happened in the promise and was neither
# DispatcherHandlerStop nor raised by an error handler. So we can and must handle it
try:
self.dispatch_error(promise.update, promise.exception, promise=promise)
except Exception:
self.logger.exception('An uncaught error was raised while handling the error.')
def run_async(self,
func: Callable[..., Any],
*args: Any,
update: HandlerArg = None,
**kwargs: Any) -> Promise:
"""
Queue a function (with given args/kwargs) to be run asynchronously. Exceptions raised
by the function will be handled by the error handlers registered with
:meth:`add_error_handler`.
Warning:
If you're using @run_async you cannot rely on adding custom attributes to
:class:`telegram.ext.CallbackContext`. See its docs for more info.
* If you're using ``@run_async``/:meth:`run_async` you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
* Calling a function through :meth:`run_async` from within an error handler can lead to
an infinite error handling loop.
Args:
func (:obj:`callable`): The function to run in the thread.
*args (:obj:`tuple`, optional): Arguments to `func`.
**kwargs (:obj:`dict`, optional): Keyword arguments to `func`.
*args (:obj:`tuple`, optional): Arguments to ``func``.
update (:class:`telegram.Update`, optional): The update associated with the functions
call. If passed, it will be available in the error handlers, in case an exception
is raised by :attr:`func`.
**kwargs (:obj:`dict`, optional): Keyword arguments to ``func``.
Returns:
Promise
"""
# TODO: handle exception in async threads
# set a threading.Event to notify caller thread
promise = Promise(func, args, kwargs)
return self._run_async(func, *args, update=update, error_handling=True, **kwargs)
def _run_async(self,
func: Callable[..., Any],
*args: Any,
update: HandlerArg = None,
error_handling: bool = True,
**kwargs: Any) -> Promise:
# TODO: Remove error_handling parameter once we drop the @run_async decorator
promise = Promise(func, args, kwargs, update=update, error_handling=error_handling)
self.__async_queue.put(promise)
return promise
def start(self, ready=None):
def start(self, ready: Event = None) -> None:
"""Thread target of thread 'dispatcher'.
Runs in background and processes the update queue.
@@ -260,7 +342,7 @@ class Dispatcher(object):
self.logger.error(msg)
raise TelegramError(msg)
self._init_async_threads(uuid4(), self.workers)
self._init_async_threads(str(uuid4()), self.workers)
self.running = True
self.logger.debug('Dispatcher started')
@@ -287,7 +369,7 @@ class Dispatcher(object):
self.running = False
self.logger.debug('Dispatcher thread stopped')
def stop(self):
def stop(self) -> None:
"""Stops the thread."""
if self.running:
self.__stop_event.set()
@@ -305,16 +387,16 @@ class Dispatcher(object):
self.__async_queue.put(None)
for i, thr in enumerate(threads):
self.logger.debug('Waiting for async thread {0}/{1} to end'.format(i + 1, total))
self.logger.debug('Waiting for async thread {}/{} to end'.format(i + 1, total))
thr.join()
self.__async_threads.remove(thr)
self.logger.debug('async thread {0}/{1} has ended'.format(i + 1, total))
self.logger.debug('async thread {}/{} has ended'.format(i + 1, total))
@property
def has_running_threads(self):
def has_running_threads(self) -> bool:
return self.running or bool(self.__async_threads)
def process_update(self, update):
def process_update(self, update: Union[str, Update, TelegramError]) -> None:
"""Processes a single update.
Args:
@@ -323,59 +405,12 @@ class Dispatcher(object):
"""
def persist_update(update):
"""Persist a single update.
Args:
update (:class:`telegram.Update`):
The update to process.
"""
if self.persistence and isinstance(update, Update):
if self.persistence.store_bot_data:
try:
self.persistence.update_bot_data(self.bot_data)
except Exception as e:
try:
self.dispatch_error(update, e)
except Exception:
message = 'Saving bot data raised an error and an ' \
'uncaught error was raised while handling ' \
'the error with an error_handler'
self.logger.exception(message)
if self.persistence.store_chat_data and update.effective_chat:
chat_id = update.effective_chat.id
try:
self.persistence.update_chat_data(chat_id,
self.chat_data[chat_id])
except Exception as e:
try:
self.dispatch_error(update, e)
except Exception:
message = 'Saving chat data raised an error and an ' \
'uncaught error was raised while handling ' \
'the error with an error_handler'
self.logger.exception(message)
if self.persistence.store_user_data and update.effective_user:
user_id = update.effective_user.id
try:
self.persistence.update_user_data(user_id,
self.user_data[user_id])
except Exception as e:
try:
self.dispatch_error(update, e)
except Exception:
message = 'Saving user data raised an error and an ' \
'uncaught error was raised while handling ' \
'the error with an error_handler'
self.logger.exception(message)
# An error happened while polling
if isinstance(update, TelegramError):
try:
self.dispatch_error(None, update)
except Exception:
self.logger.exception('An uncaught error was raised while handling the error')
self.logger.exception('An uncaught error was raised while handling the error.')
return
context = None
@@ -388,13 +423,16 @@ class Dispatcher(object):
if not context and self.use_context:
context = CallbackContext.from_update(update, self)
handler.handle_update(update, self, check, context)
persist_update(update)
# If handler runs async updating immediately doesn't make sense
if not handler.run_async:
self.update_persistence(update=update)
break
# Stop processing with any other handler.
except DispatcherHandlerStop:
self.logger.debug('Stopping further handlers due to DispatcherHandlerStop')
persist_update(update)
self.update_persistence(update=update)
break
# Dispatch any error.
@@ -406,11 +444,9 @@ class Dispatcher(object):
break
# Errors should not stop the thread.
except Exception:
self.logger.exception('An error was raised while processing the update and an '
'uncaught error was raised while handling the error '
'with an error_handler')
self.logger.exception('An uncaught error was raised while handling the error.')
def add_handler(self, handler, group=DEFAULT_GROUP):
def add_handler(self, handler: Handler, group: int = DEFAULT_GROUP) -> None:
"""Register a handler.
TL;DR: Order and priority counts. 0 or 1 handlers per group will be used. End handling of
@@ -439,13 +475,13 @@ class Dispatcher(object):
from .conversationhandler import ConversationHandler
if not isinstance(handler, Handler):
raise TypeError('handler is not an instance of {0}'.format(Handler.__name__))
raise TypeError('handler is not an instance of {}'.format(Handler.__name__))
if not isinstance(group, int):
raise TypeError('group is not int')
if isinstance(handler, ConversationHandler) and handler.persistent:
if isinstance(handler, ConversationHandler) and handler.persistent and handler.name:
if not self.persistence:
raise ValueError(
"Conversationhandler {} can not be persistent if dispatcher has no "
"ConversationHandler {} can not be persistent if dispatcher has no "
"persistence".format(handler.name))
handler.persistence = self.persistence
handler.conversations = self.persistence.get_conversations(handler.name)
@@ -457,7 +493,7 @@ class Dispatcher(object):
self.handlers[group].append(handler)
def remove_handler(self, handler, group=DEFAULT_GROUP):
def remove_handler(self, handler: Handler, group: int = DEFAULT_GROUP) -> None:
"""Remove a handler from the specified group.
Args:
@@ -471,25 +507,81 @@ class Dispatcher(object):
del self.handlers[group]
self.groups.remove(group)
def update_persistence(self):
def update_persistence(self, update: HandlerArg = None) -> None:
"""Update :attr:`user_data`, :attr:`chat_data` and :attr:`bot_data` in :attr:`persistence`.
"""
if self.persistence:
if self.persistence.store_bot_data:
self.persistence.update_bot_data(self.bot_data)
if self.persistence.store_chat_data:
for chat_id in self.chat_data:
self.persistence.update_chat_data(chat_id, self.chat_data[chat_id])
if self.persistence.store_user_data:
for user_id in self.user_data:
self.persistence.update_user_data(user_id, self.user_data[user_id])
def add_error_handler(self, callback):
Args:
update (:class:`telegram.Update`, optional): The update to process. If passed, only the
corresponding ``user_data`` and ``chat_data`` will be updated.
"""
with self._update_persistence_lock:
self.__update_persistence(update)
def __update_persistence(self, update: HandlerArg = None) -> None:
if self.persistence:
# We use list() here in order to decouple chat_ids from self.chat_data, as dict view
# objects will change, when the dict does and we want to loop over chat_ids
chat_ids = list(self.chat_data.keys())
user_ids = list(self.user_data.keys())
if isinstance(update, Update):
if update.effective_chat:
chat_ids = [update.effective_chat.id]
else:
chat_ids = []
if update.effective_user:
user_ids = [update.effective_user.id]
else:
user_ids = []
if self.persistence.store_bot_data:
try:
self.persistence.update_bot_data(self.bot_data)
except Exception as e:
try:
self.dispatch_error(update, e)
except Exception:
message = 'Saving bot data raised an error and an ' \
'uncaught error was raised while handling ' \
'the error with an error_handler'
self.logger.exception(message)
if self.persistence.store_chat_data:
for chat_id in chat_ids:
try:
self.persistence.update_chat_data(chat_id, self.chat_data[chat_id])
except Exception as e:
try:
self.dispatch_error(update, e)
except Exception:
message = 'Saving chat data raised an error and an ' \
'uncaught error was raised while handling ' \
'the error with an error_handler'
self.logger.exception(message)
if self.persistence.store_user_data:
for user_id in user_ids:
try:
self.persistence.update_user_data(user_id, self.user_data[user_id])
except Exception as e:
try:
self.dispatch_error(update, e)
except Exception:
message = 'Saving user data raised an error and an ' \
'uncaught error was raised while handling ' \
'the error with an error_handler'
self.logger.exception(message)
def add_error_handler(self,
callback: Callable[[Any, CallbackContext], None],
run_async: bool = False) -> None:
"""Registers an error handler in the Dispatcher. This handler will receive every error
which happens in your bot.
Warning: The errors handled within these handlers won't show up in the logger, so you
need to make sure that you reraise the error.
Note:
Attempts to add the same callback multiple times will be ignored.
Warning:
The errors handled within these handlers won't show up in the logger, so you
need to make sure that you reraise the error.
Args:
callback (:obj:`callable`): The callback function for this error handler. Will be
@@ -498,36 +590,57 @@ class Dispatcher(object):
``def callback(update: Update, context: CallbackContext)``
The error that happened will be present in context.error.
run_async (:obj:`bool`, optional): Whether this handlers callback should be run
asynchronously using :meth:`run_async`. Defaults to :obj:`False`.
Note:
See https://git.io/fxJuV for more info about switching to context based API.
"""
self.error_handlers.append(callback)
if callback in self.error_handlers:
self.logger.debug('The callback is already registered as an error handler. Ignoring.')
return
self.error_handlers[callback] = run_async
def remove_error_handler(self, callback):
def remove_error_handler(self, callback: Callable[[Any, CallbackContext], None]) -> None:
"""Removes an error handler.
Args:
callback (:obj:`callable`): The error handler to remove.
"""
if callback in self.error_handlers:
self.error_handlers.remove(callback)
self.error_handlers.pop(callback, None)
def dispatch_error(self, update, error):
def dispatch_error(self,
update: Optional[HandlerArg],
error: Exception,
promise: Promise = None) -> None:
"""Dispatches an error.
Args:
update (:obj:`str` | :class:`telegram.Update` | None): The update that caused the error
error (:obj:`Exception`): The error that was raised.
promise (:class:`telegram.utils.Promise`, optional): The promise whose pooled function
raised the error.
"""
async_args = None if not promise else promise.args
async_kwargs = None if not promise else promise.kwargs
if self.error_handlers:
for callback in self.error_handlers:
for callback, run_async in self.error_handlers.items():
if self.use_context:
callback(update, CallbackContext.from_error(update, error, self))
context = CallbackContext.from_error(update, error, self,
async_args=async_args,
async_kwargs=async_kwargs)
if run_async:
self.run_async(callback, update, context, update=update)
else:
callback(update, context)
else:
callback(self.bot, update, error)
if run_async:
self.run_async(callback, self.bot, update, error, update=update)
else:
callback(self.bot, update, error)
else:
self.logger.exception(
+819 -259
View File
File diff suppressed because it is too large Load Diff
+75 -34
View File
@@ -18,8 +18,19 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the base class for handlers as used by the Dispatcher."""
from abc import ABC, abstractmethod
class Handler(object):
from telegram.utils.promise import Promise
from telegram.utils.types import HandlerArg
from telegram import Update
from typing import Callable, TYPE_CHECKING, Any, Optional, Union, TypeVar, Dict
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
RT = TypeVar('RT')
class Handler(ABC):
"""The base class for all update handlers. Create custom handlers by inheriting from it.
Attributes:
@@ -32,6 +43,7 @@ class Handler(object):
the callback function.
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
@@ -42,6 +54,10 @@ class Handler(object):
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Warning:
When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
@@ -51,38 +67,42 @@ class Handler(object):
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
pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
that contains new updates which can be used to insert updates. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
which can be used to schedule new jobs. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``user_data`` will be passed to the callback function. Default is :obj:`False`.
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``.
pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``chat_data`` will be passed to the callback function. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Defaults to :obj:`False`.
"""
def __init__(self,
callback,
pass_update_queue=False,
pass_job_queue=False,
pass_user_data=False,
pass_chat_data=False):
self.callback = callback
callback: Callable[[HandlerArg, 'CallbackContext'], RT],
pass_update_queue: bool = False,
pass_job_queue: bool = False,
pass_user_data: bool = False,
pass_chat_data: bool = False,
run_async: bool = False):
self.callback: Callable[[HandlerArg, 'CallbackContext'], RT] = callback
self.pass_update_queue = pass_update_queue
self.pass_job_queue = pass_job_queue
self.pass_user_data = pass_user_data
self.pass_chat_data = pass_chat_data
self.run_async = run_async
def check_update(self, update):
@abstractmethod
def check_update(self, update: HandlerArg) -> Optional[Union[bool, object]]:
"""
This method is called to determine if an update should be handled by
this handler instance. It should always be overridden.
@@ -91,35 +111,51 @@ class Handler(object):
update (:obj:`str` | :class:`telegram.Update`): The update to be tested.
Returns:
Either ``None`` or ``False`` if the update should not be handled. Otherwise an object
that will be passed to :attr:`handle_update` and :attr:`collect_additional_context`
when the update gets handled.
Either :obj:`None` or :obj:`False` if the update should not be handled. Otherwise an
object that will be passed to :meth:`handle_update` and
:meth:`collect_additional_context` when the update gets handled.
"""
raise NotImplementedError
def handle_update(self, update, dispatcher, check_result, context=None):
def handle_update(self,
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: object,
context: 'CallbackContext' = None) -> Union[RT, Promise]:
"""
This method is called if it was determined that an update should indeed
be handled by this instance. Calls :attr:`self.callback` along with its respectful
be handled by this instance. Calls :attr:`callback` along with its respectful
arguments. To work with the :class:`telegram.ext.ConversationHandler`, this method
returns the value returned from ``self.callback``.
returns the value returned from :attr:`callback`.
Note that it can be overridden if needed by the subclassing handler.
Args:
update (:obj:`str` | :class:`telegram.Update`): The update to be handled.
dispatcher (:class:`telegram.ext.Dispatcher`): The calling dispatcher.
check_result: The result from :attr:`check_update`.
check_result (:obj:`obj`): The result from :attr:`check_update`.
context (:class:`telegram.ext.CallbackContext`, optional): The context as provided by
the dispatcher.
"""
if context:
self.collect_additional_context(context, update, dispatcher, check_result)
return self.callback(update, context)
if self.run_async:
return dispatcher.run_async(self.callback, update, context, update=update)
else:
return self.callback(update, context)
else:
optional_args = self.collect_optional_args(dispatcher, update, check_result)
return self.callback(dispatcher.bot, update, **optional_args)
if self.run_async:
return dispatcher.run_async(self.callback, dispatcher.bot, update, update=update,
**optional_args)
else:
return self.callback(dispatcher.bot, update, **optional_args) # type: ignore
def collect_additional_context(self, context, update, dispatcher, check_result):
def collect_additional_context(self,
context: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Any) -> None:
"""Prepares additional arguments for the context. Override if needed.
Args:
@@ -131,7 +167,10 @@ class Handler(object):
"""
pass
def collect_optional_args(self, dispatcher, update=None, check_result=None):
def collect_optional_args(self,
dispatcher: 'Dispatcher',
update: HandlerArg = None,
check_result: Any = None) -> Dict[str, Any]:
"""
Prepares the optional arguments. If the handler has additional optional args,
it should subclass this method, but remember to call this super method.
@@ -145,17 +184,19 @@ class Handler(object):
check_result: The result from check_update
"""
optional_args = dict()
optional_args: Dict[str, Any] = dict()
if self.pass_update_queue:
optional_args['update_queue'] = dispatcher.update_queue
if self.pass_job_queue:
optional_args['job_queue'] = dispatcher.job_queue
if self.pass_user_data:
if self.pass_user_data and isinstance(update, Update):
user = update.effective_user
optional_args['user_data'] = dispatcher.user_data[user.id if user else None]
if self.pass_chat_data:
optional_args['user_data'] = dispatcher.user_data[
user.id if user else None] # type: ignore[index]
if self.pass_chat_data and isinstance(update, Update):
chat = update.effective_chat
optional_args['chat_data'] = dispatcher.chat_data[chat.id if chat else None]
optional_args['chat_data'] = dispatcher.chat_data[
chat.id if chat else None] # type: ignore[index]
return optional_args
+55 -29
View File
@@ -19,11 +19,19 @@
""" This module contains the InlineQueryHandler class """
import re
from future.utils import string_types
from telegram import Update
from .handler import Handler
from telegram.utils.types import HandlerArg
from typing import Callable, TYPE_CHECKING, Any, Optional, Union, TypeVar, Dict, Pattern, Match, \
cast
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
RT = TypeVar('RT')
class InlineQueryHandler(Handler):
"""
@@ -46,6 +54,7 @@ class InlineQueryHandler(Handler):
the callback function.
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
@@ -56,6 +65,10 @@ class InlineQueryHandler(Handler):
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Warning:
When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
@@ -65,60 +78,64 @@ class InlineQueryHandler(Handler):
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
pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
that contains new updates which can be used to insert updates. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
which can be used to schedule new jobs. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pattern (:obj:`str` | :obj:`Pattern`, optional): Regex pattern. If not ``None``,
pattern (:obj:`str` | :obj:`Pattern`, optional): Regex pattern. If not :obj:`None`,
``re.match`` is used on :attr:`telegram.InlineQuery.query` to determine if an update
should be handled by this handler.
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``
Default is :obj:`False`
DEPRECATED: Please switch to context based callbacks.
pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``.
Default is ``False``
Default is :obj:`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``.
pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``user_data`` will be passed to the callback function. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``chat_data`` will be passed to the callback function. Default is ``False``.
pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``chat_data`` will be passed to the callback function. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Defaults to :obj:`False`.
"""
def __init__(self,
callback,
pass_update_queue=False,
pass_job_queue=False,
pattern=None,
pass_groups=False,
pass_groupdict=False,
pass_user_data=False,
pass_chat_data=False):
super(InlineQueryHandler, self).__init__(
callback: Callable[[HandlerArg, 'CallbackContext'], RT],
pass_update_queue: bool = False,
pass_job_queue: bool = False,
pattern: Union[str, Pattern] = None,
pass_groups: bool = False,
pass_groupdict: bool = False,
pass_user_data: bool = False,
pass_chat_data: bool = False,
run_async: bool = False):
super().__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
pass_chat_data=pass_chat_data)
pass_chat_data=pass_chat_data,
run_async=run_async)
if isinstance(pattern, string_types):
if isinstance(pattern, str):
pattern = re.compile(pattern)
self.pattern = pattern
self.pass_groups = pass_groups
self.pass_groupdict = pass_groupdict
def check_update(self, update):
def check_update(self, update: HandlerArg) -> Optional[Union[bool, Match]]:
"""
Determines whether an update should be passed to this handlers :attr:`callback`.
@@ -138,17 +155,26 @@ class InlineQueryHandler(Handler):
return match
else:
return True
return None
def collect_optional_args(self, dispatcher, update=None, check_result=None):
optional_args = super(InlineQueryHandler, self).collect_optional_args(dispatcher,
update, check_result)
def collect_optional_args(self,
dispatcher: 'Dispatcher',
update: HandlerArg = None,
check_result: Optional[Union[bool, Match]] = None) -> Dict[str, Any]:
optional_args = super().collect_optional_args(dispatcher, update, check_result)
if self.pattern:
check_result = cast(Match, check_result)
if self.pass_groups:
optional_args['groups'] = check_result.groups()
if self.pass_groupdict:
optional_args['groupdict'] = check_result.groupdict()
return optional_args
def collect_additional_context(self, context, update, dispatcher, check_result):
def collect_additional_context(self,
context: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Optional[Union[bool, Match]]) -> None:
if self.pattern:
check_result = cast(Match, check_result)
context.matches = [check_result]
+395 -319
View File
@@ -20,56 +20,105 @@
import datetime
import logging
import time
import warnings
import weakref
from numbers import Number
from queue import PriorityQueue, Empty
from threading import Thread, Lock, Event
import pytz
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.combining import OrTrigger
from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR, JobEvent
from telegram.ext.callbackcontext import CallbackContext
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.utils.helpers import to_float_timestamp, _UTC
from typing import TYPE_CHECKING, Union, Callable, Tuple, Optional, List, Any, cast, overload
from telegram.utils.types import JSONDict
if TYPE_CHECKING:
from telegram.ext import Dispatcher
from telegram import Bot
class Days(object):
class Days:
MON, TUE, WED, THU, FRI, SAT, SUN = range(7)
EVERY_DAY = tuple(range(7))
class JobQueue(object):
"""This class allows you to periodically perform tasks with the bot.
class JobQueue:
"""This class allows you to periodically perform tasks with the bot. It is a convenience
wrapper for the APScheduler library.
Attributes:
_queue (:obj:`PriorityQueue`): The queue that holds the Jobs.
scheduler (:class:`apscheduler.schedulers.background.BackgroundScheduler`): The APScheduler
bot (:class:`telegram.Bot`): The bot instance that should be passed to the jobs.
DEPRECATED: Use :attr:`set_dispatcher` instead.
"""
def __init__(self, bot=None):
self._queue = PriorityQueue()
if bot:
warnings.warn("Passing bot to jobqueue is deprecated. Please use set_dispatcher "
"instead!", TelegramDeprecationWarning, stacklevel=2)
class MockDispatcher(object):
def __init__(self):
self.bot = bot
self.use_context = False
self._dispatcher = MockDispatcher()
else:
self._dispatcher = None
def __init__(self) -> None:
self._dispatcher: 'Dispatcher' = None # type: ignore[assignment]
self.logger = logging.getLogger(self.__class__.__name__)
self.__start_lock = Lock()
self.__next_peek_lock = Lock() # to protect self._next_peek & self.__tick
self.__tick = Event()
self.__thread = None
self._next_peek = None
self._running = False
self.scheduler = BackgroundScheduler(timezone=pytz.utc)
self.scheduler.add_listener(self._update_persistence,
mask=EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
def set_dispatcher(self, dispatcher):
# Dispatch errors and don't log them in the APS logger
def aps_log_filter(record): # type: ignore
return 'raised an exception' not in record.msg
logging.getLogger('apscheduler.executors.default').addFilter(aps_log_filter)
self.scheduler.add_listener(self._dispatch_error, EVENT_JOB_ERROR)
def _build_args(self, job: 'Job') -> List[Union[CallbackContext, 'Bot', 'Job']]:
if self._dispatcher.use_context:
return [CallbackContext.from_job(job, self._dispatcher)]
return [self._dispatcher.bot, job]
def _tz_now(self) -> datetime.datetime:
return datetime.datetime.now(self.scheduler.timezone)
def _update_persistence(self, event: JobEvent) -> None:
self._dispatcher.update_persistence()
def _dispatch_error(self, event: JobEvent) -> None:
try:
self._dispatcher.dispatch_error(None, event.exception)
# Errors should not stop the thread.
except Exception:
self.logger.exception('An error was raised while processing the job and an '
'uncaught error was raised while handling the error '
'with an error_handler.')
@overload
def _parse_time_input(self, time: None, shift_day: bool = False) -> None:
...
@overload
def _parse_time_input(self,
time: Union[float, int, datetime.timedelta, datetime.datetime,
datetime.time],
shift_day: bool = False) -> datetime.datetime:
...
def _parse_time_input(self,
time: Union[float, int, datetime.timedelta, datetime.datetime,
datetime.time, None],
shift_day: bool = False) -> Optional[datetime.datetime]:
if time is None:
return None
if isinstance(time, (int, float)):
return self._tz_now() + datetime.timedelta(seconds=time)
if isinstance(time, datetime.timedelta):
return self._tz_now() + time
if isinstance(time, datetime.time):
dt = datetime.datetime.combine(
datetime.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time)
if dt.tzinfo is None:
dt = self.scheduler.timezone.localize(dt)
if shift_day and dt <= datetime.datetime.now(pytz.utc):
dt += datetime.timedelta(days=1)
return dt
# isinstance(time, datetime.datetime):
return time
def set_dispatcher(self, dispatcher: 'Dispatcher') -> None:
"""Set the dispatcher to be used by this JobQueue. Use this instead of passing a
:class:`telegram.Bot` to the JobQueue, which is deprecated.
@@ -78,37 +127,16 @@ class JobQueue(object):
"""
self._dispatcher = dispatcher
if dispatcher.bot.defaults:
if dispatcher.bot.defaults:
self.scheduler.configure(timezone=dispatcher.bot.defaults.tzinfo or pytz.utc)
def _put(self, job, time_spec=None, previous_t=None):
"""
Enqueues the job, scheduling its next run at the correct time.
Args:
job (telegram.ext.Job): job to enqueue
time_spec (optional):
Specification of the time for which the job should be scheduled. The precise
semantics of this parameter depend on its type (see
:func:`telegram.ext.JobQueue.run_repeating` for details).
Defaults to now + ``job.interval``.
previous_t (optional):
Time at which the job last ran (``None`` if it hasn't run yet).
"""
# get time at which to run:
if time_spec is None:
time_spec = job.interval
if time_spec is None:
raise ValueError("no time specification given for scheduling non-repeating job")
next_t = to_float_timestamp(time_spec, reference_timestamp=previous_t)
# enqueue:
self.logger.debug('Putting job %s with t=%s', job.name, time_spec)
self._queue.put((next_t, job))
# Wake up the loop if this job should be executed next
self._set_next_peek(next_t)
def run_once(self, callback, when, context=None, name=None):
def run_once(self,
callback: Callable[['CallbackContext'], None],
when: Union[float, datetime.timedelta, datetime.datetime, datetime.time],
context: object = None,
name: str = None,
job_kwargs: JSONDict = None) -> 'Job':
"""Creates a new ``Job`` that runs once and adds it to the queue.
Args:
@@ -129,26 +157,53 @@ class JobQueue(object):
* :obj:`datetime.timedelta` will be interpreted as "time from now" in which the
job should run.
* :obj:`datetime.datetime` will be interpreted as a specific date and time at
which the job should run.
which the job should run. If the timezone (``datetime.tzinfo``) is :obj:`None`,
the default timezone of the bot will be used.
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
job should run. This could be either today or, if the time has already passed,
tomorrow.
tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, the
default timezone of the bot will be used.
context (:obj:`object`, optional): Additional data needed for the callback function.
Can be accessed through ``job.context`` in the callback. Defaults to ``None``.
Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`.
name (:obj:`str`, optional): The name of the new job. Defaults to
``callback.__name__``.
job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
``scheduler.add_job()``.
Returns:
:class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job
queue.
"""
job = Job(callback, repeat=False, context=context, name=name, job_queue=self)
self._put(job, time_spec=when)
if not job_kwargs:
job_kwargs = {}
name = name or callback.__name__
job = Job(callback, context, name, self)
dt = self._parse_time_input(when, shift_day=True)
j = self.scheduler.add_job(callback,
name=name,
trigger='date',
run_date=dt,
args=self._build_args(job),
timezone=dt.tzinfo or self.scheduler.timezone,
**job_kwargs)
job.job = j
return job
def run_repeating(self, callback, interval, first=None, context=None, name=None):
def run_repeating(self,
callback: Callable[['CallbackContext'], None],
interval: Union[float, datetime.timedelta],
first: Union[float, datetime.timedelta, datetime.datetime,
datetime.time] = None,
last: Union[float, datetime.timedelta, datetime.datetime,
datetime.time] = None,
context: object = None,
name: str = None,
job_kwargs: JSONDict = None) -> 'Job':
"""Creates a new ``Job`` that runs at specified intervals and adds it to the queue.
Args:
@@ -172,37 +227,150 @@ class JobQueue(object):
* :obj:`datetime.timedelta` will be interpreted as "time from now" in which the
job should run.
* :obj:`datetime.datetime` will be interpreted as a specific date and time at
which the job should run.
which the job should run. If the timezone (``datetime.tzinfo``) is :obj:`None`,
the default timezone of the bot will be used.
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
job should run. This could be either today or, if the time has already passed,
tomorrow.
tomorrow. If the timezone (``time.tzinfo``) is :obj:`None`, the
default timezone of the bot will be used.
Defaults to ``interval``
last (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
:obj:`datetime.datetime` | :obj:`datetime.time`, optional):
Latest possible time for the job to run. This parameter will be interpreted
depending on its type. See ``first`` for details.
If ``last`` is :obj:`datetime.datetime` or :obj:`datetime.time` type
and ``last.tzinfo`` is :obj:`None`, the default timezone of the bot will be
assumed.
Defaults to :obj:`None`.
context (:obj:`object`, optional): Additional data needed for the callback function.
Can be accessed through ``job.context`` in the callback. Defaults to ``None``.
Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`.
name (:obj:`str`, optional): The name of the new job. Defaults to
``callback.__name__``.
job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
``scheduler.add_job()``.
Returns:
:class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job
queue.
Notes:
Note:
`interval` is always respected "as-is". That means that if DST changes during that
interval, the job might not run at the time one would expect. It is always recommended
to pin servers to UTC time, then time related behaviour can always be expected.
"""
job = Job(callback,
interval=interval,
repeat=True,
context=context,
name=name,
job_queue=self)
self._put(job, time_spec=first)
if not job_kwargs:
job_kwargs = {}
name = name or callback.__name__
job = Job(callback, context, name, self)
dt_first = self._parse_time_input(first)
dt_last = self._parse_time_input(last)
if dt_last and dt_first and dt_last < dt_first:
raise ValueError("'last' must not be before 'first'!")
if isinstance(interval, datetime.timedelta):
interval = interval.total_seconds()
j = self.scheduler.add_job(callback,
trigger='interval',
args=self._build_args(job),
start_date=dt_first,
end_date=dt_last,
seconds=interval,
name=name,
**job_kwargs)
job.job = j
return job
def run_daily(self, callback, time, days=Days.EVERY_DAY, context=None, name=None):
def run_monthly(self,
callback: Callable[['CallbackContext'], None],
when: datetime.time,
day: int,
context: object = None,
name: str = None,
day_is_strict: bool = True,
job_kwargs: JSONDict = None) -> 'Job':
"""Creates a new ``Job`` that runs on a monthly basis and adds it to the queue.
Args:
callback (:obj:`callable`): The callback function that should be executed by the new
job. Callback signature for context based API:
``def callback(CallbackContext)``
``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
its ``job.context`` or change it to a repeating job.
when (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
(``when.tzinfo``) is :obj:`None`, the default timezone of the bot will be used.
day (:obj:`int`): Defines the day of the month whereby the job would run. It should
be within the range of 1 and 31, inclusive.
context (:obj:`object`, optional): Additional data needed for the callback function.
Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`.
name (:obj:`str`, optional): The name of the new job. Defaults to
``callback.__name__``.
day_is_strict (:obj:`bool`, optional): If :obj:`False` and day > month.days, will pick
the last day in the month. Defaults to :obj:`True`.
job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
``scheduler.add_job()``.
Returns:
:class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job
queue.
"""
if not job_kwargs:
job_kwargs = {}
name = name or callback.__name__
job = Job(callback, context, name, self)
if day_is_strict:
j = self.scheduler.add_job(callback,
trigger='cron',
args=self._build_args(job),
name=name,
day=day,
hour=when.hour,
minute=when.minute,
second=when.second,
timezone=when.tzinfo or self.scheduler.timezone,
**job_kwargs)
else:
trigger = OrTrigger([CronTrigger(day=day,
hour=when.hour,
minute=when.minute,
second=when.second,
timezone=when.tzinfo,
**job_kwargs),
CronTrigger(day='last',
hour=when.hour,
minute=when.minute,
second=when.second,
timezone=when.tzinfo or self.scheduler.timezone,
**job_kwargs)])
j = self.scheduler.add_job(callback,
trigger=trigger,
args=self._build_args(job),
name=name,
**job_kwargs)
job.job = j
return job
def run_daily(self,
callback: Callable[['CallbackContext'], None],
time: datetime.time,
days: Tuple[int, ...] = Days.EVERY_DAY,
context: object = None,
name: str = None,
job_kwargs: JSONDict = None) -> 'Job':
"""Creates a new ``Job`` that runs on a daily basis and adds it to the queue.
Args:
@@ -214,9 +382,64 @@ class JobQueue(object):
``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
its ``job.context`` or change it to a repeating job.
time (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
(``time.tzinfo``) is ``None``, UTC will be assumed.
(``time.tzinfo``) is :obj:`None`, the default timezone of the bot will be used.
days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should
run. Defaults to ``EVERY_DAY``
context (:obj:`object`, optional): Additional data needed for the callback function.
Can be accessed through ``job.context`` in the callback. Defaults to :obj:`None`.
name (:obj:`str`, optional): The name of the new job. Defaults to
``callback.__name__``.
job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
``scheduler.add_job()``.
Returns:
:class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job
queue.
Note:
For a note about DST, please see the documentation of `APScheduler`_.
.. _`APScheduler`: https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html
#daylight-saving-time-behavior
"""
if not job_kwargs:
job_kwargs = {}
name = name or callback.__name__
job = Job(callback, context, name, self)
j = self.scheduler.add_job(callback,
name=name,
args=self._build_args(job),
trigger='cron',
day_of_week=','.join([str(d) for d in days]),
hour=time.hour,
minute=time.minute,
second=time.second,
timezone=time.tzinfo or self.scheduler.timezone,
**job_kwargs)
job.job = j
return job
def run_custom(self,
callback: Callable[['CallbackContext'], None],
job_kwargs: JSONDict,
context: object = None,
name: str = None) -> 'Job':
"""Creates a new customly defined ``Job``.
Args:
callback (:obj:`callable`): The callback function that should be executed by the new
job. Callback signature for context based API:
``def callback(CallbackContext)``
``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
its ``job.context`` or change it to a repeating job.
job_kwargs (:obj:`dict`): Arbitrary keyword arguments. Used as arguments for
``scheduler.add_job``.
context (:obj:`object`, optional): Additional data needed for the callback function.
Can be accessed through ``job.context`` in the callback. Defaults to ``None``.
name (:obj:`str`, optional): The name of the new job. Defaults to
@@ -226,144 +449,56 @@ class JobQueue(object):
:class:`telegram.ext.Job`: The new ``Job`` instance that has been added to the job
queue.
Notes:
Daily is just an alias for "24 Hours". That means that if DST changes during that
interval, the job might not run at the time one would expect. It is always recommended
to pin servers to UTC time, then time related behaviour can always be expected.
"""
job = Job(callback,
interval=datetime.timedelta(days=1),
repeat=True,
days=days,
tzinfo=time.tzinfo,
context=context,
name=name,
job_queue=self)
self._put(job, time_spec=time)
name = name or callback.__name__
job = Job(callback, context, name, self)
j = self.scheduler.add_job(callback,
args=self._build_args(job),
name=name,
**job_kwargs)
job.job = j
return job
def _set_next_peek(self, t):
# """
# Set next peek if not defined or `t` is before next peek.
# In case the next peek was set, also trigger the `self.__tick` event.
# """
with self.__next_peek_lock:
if not self._next_peek or self._next_peek > t:
self._next_peek = t
self.__tick.set()
def tick(self):
"""Run all jobs that are due and re-enqueue them with their interval."""
now = time.time()
self.logger.debug('Ticking jobs with t=%f', now)
while True:
try:
t, job = self._queue.get(False)
except Empty:
break
self.logger.debug('Peeked at %s with t=%f', job.name, t)
if t > now:
# We can get here in two conditions:
# 1. At the second or later pass of the while loop, after we've already
# processed the job(s) we were supposed to at this time.
# 2. At the first iteration of the loop only if `self.put()` had triggered
# `self.__tick` because `self._next_peek` wasn't set
self.logger.debug("Next task isn't due yet. Finished!")
self._queue.put((t, job))
self._set_next_peek(t)
break
if job.removed:
self.logger.debug('Removing job %s', job.name)
continue
if job.enabled:
try:
current_week_day = datetime.datetime.now(job.tzinfo).date().weekday()
if current_week_day in job.days:
self.logger.debug('Running job %s', job.name)
job.run(self._dispatcher)
except Exception:
self.logger.exception('An uncaught error was raised while executing job %s',
job.name)
else:
self.logger.debug('Skipping disabled job %s', job.name)
if job.repeat and not job.removed:
self._put(job, previous_t=t)
else:
self.logger.debug('Dropping non-repeating or removed job %s', job.name)
def start(self):
def start(self) -> None:
"""Starts the job_queue thread."""
self.__start_lock.acquire()
if not self.scheduler.running:
self.scheduler.start()
if not self._running:
self._running = True
self.__start_lock.release()
self.__thread = Thread(target=self._main_loop,
name="Bot:{}:job_queue".format(self._dispatcher.bot.id))
self.__thread.start()
self.logger.debug('%s thread started', self.__class__.__name__)
else:
self.__start_lock.release()
def _main_loop(self):
"""
Thread target of thread ``job_queue``. Runs in background and performs ticks on the job
queue.
"""
while self._running:
# self._next_peek may be (re)scheduled during self.tick() or self.put()
with self.__next_peek_lock:
tmout = self._next_peek - time.time() if self._next_peek else None
self._next_peek = None
self.__tick.clear()
self.__tick.wait(tmout)
# If we were woken up by self.stop(), just bail out
if not self._running:
break
self.tick()
self.logger.debug('%s thread stopped', self.__class__.__name__)
def stop(self):
def stop(self) -> None:
"""Stops the thread."""
with self.__start_lock:
self._running = False
if self.scheduler.running:
self.scheduler.shutdown()
self.__tick.set()
if self.__thread is not None:
self.__thread.join()
def jobs(self):
def jobs(self) -> Tuple['Job', ...]:
"""Returns a tuple of all jobs that are currently in the ``JobQueue``."""
with self._queue.mutex:
return tuple(job[1] for job in self._queue.queue if job)
return tuple(Job.from_aps_job(job, self) for job in self.scheduler.get_jobs())
def get_jobs_by_name(self, name):
def get_jobs_by_name(self, name: str) -> Tuple['Job', ...]:
"""Returns a tuple of jobs with the given name that are currently in the ``JobQueue``"""
with self._queue.mutex:
return tuple(job[1] for job in self._queue.queue if job and job[1].name == name)
return tuple(job for job in self.jobs() if job.name == name)
class Job(object):
"""This class encapsulates a Job.
class Job:
"""This class is a convenience wrapper for the jobs held in a :class:`telegram.ext.JobQueue`.
With the current backend APScheduler, :attr:`job` holds a :class:`apscheduler.job.Job`
instance.
Note:
* All attributes and instance methods of :attr:`job` are also directly available as
attributes/methods of the corresponding :class:`telegram.ext.Job` object.
* Two instances of :class:`telegram.ext.Job` are considered equal, if their corresponding
``job`` attributes have the same ``id``.
* If :attr:`job` isn't passed on initialization, it must be set manually afterwards for
this :class:`telegram.ext.Job` to be useful.
Attributes:
callback (:obj:`callable`): The callback function that should be executed by the new job.
context (:obj:`object`): Optional. Additional data needed for the callback function.
name (:obj:`str`): Optional. The name of the new job.
job_queue (:class:`telegram.ext.JobQueue`): Optional. The ``JobQueue`` this job belongs to.
job (:class:`apscheduler.job.Job`): Optional. The APS Job this job is a wrapper for.
Args:
callback (:obj:`callable`): The callback function that should be executed by the new job.
@@ -373,157 +508,98 @@ class Job(object):
a ``context.job`` is the :class:`telegram.ext.Job` instance. It can be used to access
its ``job.context`` or change it to a repeating job.
interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`, optional): The time
interval between executions of the job. If it is an :obj:`int` or a :obj:`float`,
it will be interpreted as seconds. If you don't set this value, you must set
:attr:`repeat` to ``False`` and specify :attr:`time_spec` when you put the job into
the job queue.
repeat (:obj:`bool`, optional): If this job should be periodically execute its callback
function (``True``) or only once (``False``). Defaults to ``True``.
context (:obj:`object`, optional): Additional data needed for the callback function. Can be
accessed through ``job.context`` in the callback. Defaults to ``None``.
accessed through ``job.context`` in the callback. Defaults to :obj:`None`.
name (:obj:`str`, optional): The name of the new job. Defaults to ``callback.__name__``.
days (Tuple[:obj:`int`], optional): Defines on which days of the week the job should run.
Defaults to ``Days.EVERY_DAY``
job_queue (:class:`telegram.ext.JobQueue`, optional): The ``JobQueue`` this job belongs to.
Only optional for backward compatibility with ``JobQueue.put()``.
tzinfo (:obj:`datetime.tzinfo`, optional): timezone associated to this job. Used when
checking the day of the week to determine whether a job should run (only relevant when
``days is not Days.EVERY_DAY``). Defaults to UTC.
job (:class:`apscheduler.job.Job`, optional): The APS Job this job is a wrapper for.
"""
def __init__(self,
callback,
interval=None,
repeat=True,
context=None,
days=Days.EVERY_DAY,
name=None,
job_queue=None,
tzinfo=None):
callback: Callable[['CallbackContext'], None],
context: object = None,
name: str = None,
job_queue: JobQueue = None,
job: 'Job' = None):
self.callback = callback
self.context = context
self.name = name or callback.__name__
self.job_queue = job_queue
self._repeat = None
self._interval = None
self.interval = interval
self.repeat = repeat
self._removed = False
self._enabled = False
self._days = None
self.days = days
self.tzinfo = tzinfo or _UTC
self.job = cast('Job', job)
self._job_queue = weakref.proxy(job_queue) if job_queue is not None else None
def run(self, dispatcher: 'Dispatcher') -> None:
"""Executes the callback function independently of the jobs schedule."""
try:
if dispatcher.use_context:
self.callback(CallbackContext.from_job(self, dispatcher))
else:
self.callback(dispatcher.bot, self) # type: ignore[arg-type,call-arg]
except Exception as e:
try:
dispatcher.dispatch_error(None, e)
# Errors should not stop the thread.
except Exception:
dispatcher.logger.exception('An error was raised while processing the job and an '
'uncaught error was raised while handling the error '
'with an error_handler.')
self._remove = Event()
self._enabled = Event()
self._enabled.set()
def run(self, dispatcher):
"""Executes the callback function."""
if dispatcher.use_context:
self.callback(CallbackContext.from_job(self, dispatcher))
else:
self.callback(dispatcher.bot, self)
def schedule_removal(self):
def schedule_removal(self) -> None:
"""
Schedules this job for removal from the ``JobQueue``. It will be removed without executing
its callback function again.
"""
self._remove.set()
self.job.remove()
self._removed = True
@property
def removed(self):
def removed(self) -> bool:
""":obj:`bool`: Whether this job is due to be removed."""
return self._remove.is_set()
return self._removed
@property
def enabled(self):
def enabled(self) -> bool:
""":obj:`bool`: Whether this job is enabled."""
return self._enabled.is_set()
return self._enabled
@enabled.setter
def enabled(self, status):
def enabled(self, status: bool) -> None:
if status:
self._enabled.set()
self.job.resume()
else:
self._enabled.clear()
self.job.pause()
self._enabled = status
@property
def interval(self):
def next_t(self) -> Optional[datetime.datetime]:
"""
:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`: Optional. The interval in which the
job will run.
:obj:`datetime.datetime`: Datetime for the next job execution.
Datetime is localized according to :attr:`tzinfo`.
If job is removed or already ran it equals to :obj:`None`.
"""
return self._interval
return self.job.next_run_time
@interval.setter
def interval(self, interval):
if interval is None and self.repeat:
raise ValueError("The 'interval' can not be 'None' when 'repeat' is set to 'True'")
if not (interval is None or isinstance(interval, (Number, datetime.timedelta))):
raise ValueError("The 'interval' must be of type 'datetime.timedelta',"
" 'int' or 'float'")
self._interval = interval
@property
def interval_seconds(self):
""":obj:`int`: The interval for this job in seconds."""
interval = self.interval
if isinstance(interval, datetime.timedelta):
return interval.total_seconds()
@classmethod
def from_aps_job(cls, job: 'Job', job_queue: JobQueue) -> 'Job':
# context based callbacks
if len(job.args) == 1:
context = job.args[0].job.context
else:
return interval
context = job.args[1].context
return cls(job.func, context=context, name=job.name, job_queue=job_queue, job=job)
@property
def repeat(self):
""":obj:`bool`: Optional. If this job should periodically execute its callback function."""
return self._repeat
def __getattr__(self, item: str) -> Any:
return getattr(self.job, item)
@repeat.setter
def repeat(self, repeat):
if self.interval is None and repeat:
raise ValueError("'repeat' can not be set to 'True' when no 'interval' is set")
self._repeat = repeat
@property
def days(self):
"""Tuple[:obj:`int`]: Optional. Defines on which days of the week the job should run."""
return self._days
@days.setter
def days(self, days):
if not isinstance(days, tuple):
raise ValueError("The 'days' argument should be of type 'tuple'")
if not all(isinstance(day, int) for day in days):
raise ValueError("The elements of the 'days' argument should be of type 'int'")
if not all(0 <= day <= 6 for day in days):
raise ValueError("The elements of the 'days' argument should be from 0 up to and "
"including 6")
self._days = days
@property
def job_queue(self):
""":class:`telegram.ext.JobQueue`: Optional. The ``JobQueue`` this job belongs to."""
return self._job_queue
@job_queue.setter
def job_queue(self, job_queue):
# Property setter for backward compatibility with JobQueue.put()
if not self._job_queue:
self._job_queue = weakref.proxy(job_queue)
else:
raise RuntimeError("The 'job_queue' attribute can only be set once.")
def __lt__(self, other):
def __lt__(self, other: object) -> bool:
return False
def __eq__(self, other: object) -> bool:
if isinstance(other, self.__class__):
return self.id == other.id
return False
+51 -31
View File
@@ -23,9 +23,16 @@ import warnings
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram import Update
from telegram.ext import Filters
from telegram.ext import Filters, BaseFilter
from .handler import Handler
from telegram.utils.types import HandlerArg
from typing import Callable, TYPE_CHECKING, Any, Optional, Union, TypeVar, Dict
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
RT = TypeVar('RT')
class MessageHandler(Handler):
"""Handler class to handle telegram messages. They might contain text, media or status updates.
@@ -43,11 +50,12 @@ class MessageHandler(Handler):
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
message_updates (:obj:`bool`): Should "normal" message updates be handled?
Default is ``None``.
Default is :obj:`None`.
channel_post_updates (:obj:`bool`): Should channel posts updates be handled?
Default is ``None``.
Default is :obj:`None`.
edited_updates (:obj:`bool`): Should "edited" message updates be handled?
Default is ``None``.
Default is :obj:`None`.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
@@ -58,6 +66,10 @@ class MessageHandler(Handler):
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Warning:
When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
filters (:class:`telegram.ext.BaseFilter`, optional): A filter inheriting from
:class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in
@@ -75,31 +87,33 @@ class MessageHandler(Handler):
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
pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
that contains new updates which can be used to insert updates. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
which can be used to schedule new jobs. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``user_data`` will be passed to the callback function. Default is :obj:`False`.
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``.
pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``chat_data`` will be passed to the callback function. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
message_updates (:obj:`bool`, optional): Should "normal" message updates be handled?
Default is ``None``.
Default is :obj:`None`.
DEPRECATED: Please switch to filters for update filtering.
channel_post_updates (:obj:`bool`, optional): Should channel posts updates be handled?
Default is ``None``.
Default is :obj:`None`.
DEPRECATED: Please switch to filters for update filtering.
edited_updates (:obj:`bool`, optional): Should "edited" message updates be handled? Default
is ``None``.
is :obj:`None`.
DEPRECATED: Please switch to filters for update filtering.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Defaults to :obj:`False`.
Raises:
ValueError
@@ -107,28 +121,29 @@ class MessageHandler(Handler):
"""
def __init__(self,
filters,
callback,
pass_update_queue=False,
pass_job_queue=False,
pass_user_data=False,
pass_chat_data=False,
message_updates=None,
channel_post_updates=None,
edited_updates=None):
filters: BaseFilter,
callback: Callable[[HandlerArg, 'CallbackContext'], RT],
pass_update_queue: bool = False,
pass_job_queue: bool = False,
pass_user_data: bool = False,
pass_chat_data: bool = False,
message_updates: bool = None,
channel_post_updates: bool = None,
edited_updates: bool = None,
run_async: bool = False):
super(MessageHandler, self).__init__(
super().__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
pass_chat_data=pass_chat_data)
pass_chat_data=pass_chat_data,
run_async=run_async)
if message_updates is False and channel_post_updates is False and edited_updates is False:
raise ValueError(
'message_updates, channel_post_updates and edited_updates are all False')
self.filters = filters
if self.filters is not None:
self.filters &= Filters.update
if filters is not None:
self.filters = Filters.update & filters
else:
self.filters = Filters.update
if message_updates is not None:
@@ -154,7 +169,7 @@ class MessageHandler(Handler):
self.filters &= ~(Filters.update.edited_message
| Filters.update.edited_channel_post)
def check_update(self, update):
def check_update(self, update: HandlerArg) -> Optional[Union[bool, Dict[str, Any]]]:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
@@ -166,7 +181,12 @@ class MessageHandler(Handler):
"""
if isinstance(update, Update) and update.effective_message:
return self.filters(update)
return None
def collect_additional_context(self, context, update, dispatcher, check_result):
def collect_additional_context(self,
context: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Optional[Union[bool, Dict[str, Any]]]) -> None:
if isinstance(check_result, dict):
context.update(check_result)
+59 -63
View File
@@ -23,24 +23,17 @@
from telegram.utils import promise
import functools
import sys
import time
import threading
if sys.version_info.major > 2:
import queue as q
else:
import Queue as q
import queue as q
from typing import Callable, Any, TYPE_CHECKING, List, NoReturn
if TYPE_CHECKING:
from telegram import Bot
# We need to count < 1s intervals, so the most accurate timer is needed
# Starting from Python 3.3 we have time.perf_counter which is the clock
# with the highest resolution available to the system, so let's use it there.
# In Python 2.7, there's no perf_counter yet, so fallback on what we have:
# on Windows, the best available is time.clock while time.time is on
# another platforms (M. Lutz, "Learning Python," 4ed, p.630-634)
if sys.version_info.major == 3 and sys.version_info.minor >= 3:
curtime = time.perf_counter # pylint: disable=E1101
else:
curtime = time.clock if sys.platform[:3] == 'win' else time.time
curtime = time.perf_counter
class DelayQueueError(RuntimeError):
@@ -72,8 +65,9 @@ class DelayQueue(threading.Thread):
route exceptions from processor thread to main thread; is called on `Exception`
subclass exceptions. If not provided, exceptions are routed through dummy handler,
which re-raises them.
autostart (:obj:`bool`, optional): If True, processor is started immediately after object's
creation; if ``False``, should be started manually by `start` method. Defaults to True.
autostart (:obj:`bool`, optional): If :obj:`True`, processor is started immediately after
object's creation; if :obj:`False`, should be started manually by `start` method.
Defaults to :obj:`True`.
name (:obj:`str`, optional): Thread's name. Defaults to ``'DelayQueue-N'``, where N is
sequential number of object created.
@@ -82,12 +76,12 @@ class DelayQueue(threading.Thread):
_instcnt = 0 # instance counter
def __init__(self,
queue=None,
burst_limit=30,
time_limit_ms=1000,
exc_route=None,
autostart=True,
name=None):
queue: q.Queue = None,
burst_limit: int = 30,
time_limit_ms: int = 1000,
exc_route: Callable[[Exception], None] = None,
autostart: bool = True,
name: str = None):
self._queue = queue if queue is not None else q.Queue()
self.burst_limit = burst_limit
self.time_limit = time_limit_ms / 1000
@@ -95,26 +89,26 @@ class DelayQueue(threading.Thread):
self.__exit_req = False # flag to gently exit thread
self.__class__._instcnt += 1
if name is None:
name = '%s-%s' % (self.__class__.__name__, self.__class__._instcnt)
super(DelayQueue, self).__init__(name=name)
name = '{}-{}'.format(self.__class__.__name__, self.__class__._instcnt)
super().__init__(name=name)
self.daemon = False
if autostart: # immediately start processing
super(DelayQueue, self).start()
super().start()
def run(self):
def run(self) -> None:
"""
Do not use the method except for unthreaded testing purposes, the method normally is
automatically called by autostart argument.
"""
times = [] # used to store each callable processing time
times: List[float] = [] # used to store each callable processing time
while True:
item = self._queue.get()
if self.__exit_req:
return # shutdown thread
# delay routine
now = curtime()
now = time.perf_counter()
t_delta = now - self.time_limit # calculate early to improve perf.
if times and t_delta > times[-1]:
# if last call was before the limit time-window
@@ -133,23 +127,24 @@ class DelayQueue(threading.Thread):
except Exception as exc: # re-route any exceptions
self.exc_route(exc) # to prevent thread exit
def stop(self, timeout=None):
def stop(self, timeout: float = None) -> None:
"""Used to gently stop processor and shutdown its thread.
Args:
timeout (:obj:`float`): Indicates maximum time to wait for processor to stop and its
thread to exit. If timeout exceeds and processor has not stopped, method silently
returns. :attr:`is_alive` could be used afterwards to check the actual status.
``timeout`` set to None, blocks until processor is shut down. Defaults to None.
``timeout`` set to :obj:`None`, blocks until processor is shut down.
Defaults to :obj:`None`.
"""
self.__exit_req = True # gently request
self._queue.put(None) # put something to unfreeze if frozen
super(DelayQueue, self).join(timeout=timeout)
super().join(timeout=timeout)
@staticmethod
def _default_exception_handler(exc):
def _default_exception_handler(exc: Exception) -> NoReturn:
"""
Dummy exception handler which re-raises exception in thread. Could be possibly overwritten
by subclasses.
@@ -158,7 +153,7 @@ class DelayQueue(threading.Thread):
raise exc
def __call__(self, func, *args, **kwargs):
def __call__(self, func: Callable, *args: Any, **kwargs: Any) -> None:
"""Used to process callbacks in throughput-limiting thread through queue.
Args:
@@ -174,13 +169,13 @@ class DelayQueue(threading.Thread):
self._queue.put((func, args, kwargs))
# The most straightforward way to implement this is to use 2 sequenital delay
# The most straightforward way to implement this is to use 2 sequential delay
# queues, like on classic delay chain schematics in electronics.
# So, message path is:
# msg --> group delay if group msg, else no delay --> normal msg delay --> out
# This way OS threading scheduler cares of timings accuracy.
# (see time.time, time.clock, time.perf_counter, time.sleep @ docs.python.org)
class MessageQueue(object):
class MessageQueue:
"""
Implements callback processing with proper delays to avoid hitting Telegram's message limits.
Contains two ``DelayQueue``, for group and for all messages, interconnected in delay chain.
@@ -200,20 +195,20 @@ class MessageQueue(object):
to route exceptions from processor threads to main thread; is called on ``Exception``
subclass exceptions. If not provided, exceptions are routed through dummy handler,
which re-raises them.
autostart (:obj:`bool`, optional): If True, processors are started immediately after
object's creation; if ``False``, should be started manually by :attr:`start` method.
Defaults to ``True``.
autostart (:obj:`bool`, optional): If :obj:`True`, processors are started immediately after
object's creation; if :obj:`False`, should be started manually by :attr:`start` method.
Defaults to :obj:`True`.
"""
def __init__(self,
all_burst_limit=30,
all_time_limit_ms=1000,
group_burst_limit=20,
group_time_limit_ms=60000,
exc_route=None,
autostart=True):
# create accoring delay queues, use composition
all_burst_limit: int = 30,
all_time_limit_ms: int = 1000,
group_burst_limit: int = 20,
group_time_limit_ms: int = 60000,
exc_route: Callable[[Exception], None] = None,
autostart: bool = True):
# create according delay queues, use composition
self._all_delayq = DelayQueue(
burst_limit=all_burst_limit,
time_limit_ms=all_time_limit_ms,
@@ -225,31 +220,31 @@ class MessageQueue(object):
exc_route=exc_route,
autostart=autostart)
def start(self):
def start(self) -> None:
"""Method is used to manually start the ``MessageQueue`` processing."""
self._all_delayq.start()
self._group_delayq.start()
def stop(self, timeout=None):
def stop(self, timeout: float = None) -> None:
self._group_delayq.stop(timeout=timeout)
self._all_delayq.stop(timeout=timeout)
stop.__doc__ = DelayQueue.stop.__doc__ or '' # reuse docsting if any
stop.__doc__ = DelayQueue.stop.__doc__ or '' # reuse docstring if any
def __call__(self, promise, is_group_msg=False):
def __call__(self, promise: Callable, is_group_msg: bool = False) -> Callable:
"""
Processes callables in troughput-limiting queues to avoid hitting limits (specified with
Processes callables in throughput-limiting queues to avoid hitting limits (specified with
:attr:`burst_limit` and :attr:`time_limit`.
Args:
promise (:obj:`callable`): Mainly the ``telegram.utils.promise.Promise`` (see Notes for
other callables), that is processed in delay queues.
is_group_msg (:obj:`bool`, optional): Defines whether ``promise`` would be processed in
group*+*all* ``DelayQueue``s (if set to ``True``), or only through *all*
``DelayQueue`` (if set to ``False``), resulting in needed delays to avoid
hitting specified limits. Defaults to ``False``.
group*+*all* ``DelayQueue``s (if set to :obj:`True`), or only through *all*
``DelayQueue`` (if set to :obj:`False`), resulting in needed delays to avoid
hitting specified limits. Defaults to :obj:`False`.
Notes:
Note:
Method is designed to accept ``telegram.utils.promise.Promise`` as ``promise``
argument, but other callables could be used too. For example, lambdas or simple
functions could be used to wrap original func to be called with needed args. In that
@@ -268,7 +263,7 @@ class MessageQueue(object):
return promise
def queuedmessage(method):
def queuedmessage(method: Callable) -> Callable:
"""A decorator to be used with :attr:`telegram.Bot` send* methods.
Note:
@@ -287,12 +282,12 @@ def queuedmessage(method):
Wrapped method starts accepting the next kwargs:
Args:
queued (:obj:`bool`, optional): If set to ``True``, the ``MessageQueue`` is used to process
output messages. Defaults to `self._is_queued_out`.
isgroup (:obj:`bool`, optional): If set to ``True``, the message is meant to be group-type
(as there's no obvious way to determine its type in other way at the moment).
queued (:obj:`bool`, optional): If set to :obj:`True`, the ``MessageQueue`` is used to
process output messages. Defaults to `self._is_queued_out`.
isgroup (:obj:`bool`, optional): If set to :obj:`True`, the message is meant to be
group-type(as there's no obvious way to determine its type in other way at the moment).
Group-type messages could have additional processing delay according to limits set
in `self._out_queue`. Defaults to ``False``.
in `self._out_queue`. Defaults to :obj:`False`.
Returns:
``telegram.utils.promise.Promise``: In case call is queued or original method's return
@@ -301,12 +296,13 @@ def queuedmessage(method):
"""
@functools.wraps(method)
def wrapped(self, *args, **kwargs):
queued = kwargs.pop('queued', self._is_messages_queued_default)
def wrapped(self: 'Bot', *args: Any, **kwargs: Any) -> Any:
queued = kwargs.pop('queued',
self._is_messages_queued_default) # type: ignore[attr-defined]
isgroup = kwargs.pop('isgroup', False)
if queued:
prom = promise.Promise(method, (self, ) + args, kwargs)
return self._msg_queue(prom, isgroup)
return self._msg_queue(prom, isgroup) # type: ignore[attr-defined]
return method(self, *args, **kwargs)
return wrapped
+79 -59
View File
@@ -23,61 +23,76 @@ from copy import deepcopy
from telegram.ext import BasePersistence
from typing import DefaultDict, Dict, Any, Tuple, Optional
from telegram.utils.types import ConversationDict
class PicklePersistence(BasePersistence):
"""Using python's builtin pickle for making you bot persistent.
Warning:
:class:`PicklePersistence` will try to replace :class:`telegram.Bot` instances by
:attr:`REPLACED_BOT` and insert the bot set with
:meth:`telegram.ext.BasePersistence.set_bot` upon loading of the data. This is to ensure
that changes to the bot apply to the saved objects, too. If you change the bots token, this
may lead to e.g. ``Chat not found`` errors. For the limitations on replacing bots see
:meth:`telegram.ext.BasePersistence.replace_bot` and
:meth:`telegram.ext.BasePersistence.insert_bot`.
Attributes:
filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file`
is false this will be used as a prefix.
is :obj:`False` this will be used as a prefix.
store_user_data (:obj:`bool`): Optional. Whether user_data should be saved by this
persistence class.
store_chat_data (:obj:`bool`): Optional. Whether user_data should be saved by this
persistence class.
store_bot_data (:obj:`bool`): Optional. Whether bot_data should be saved by this
persistence class.
single_file (:obj:`bool`): Optional. When ``False`` will store 3 sperate files of
single_file (:obj:`bool`): Optional. When :obj:`False` will store 3 separate 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``.
:obj:`True`.
on_flush (:obj:`bool`, optional): When :obj:`True` will only save to file when
:meth:`flush` is called and keep data in memory until that happens. When
:obj:`False` will store data on any transaction *and* on call to :meth:`flush`.
Default is :obj:`False`.
Args:
filename (:obj:`str`): The filename for storing the pickle files. When :attr:`single_file`
is false this will be used as a prefix.
is :obj:`False` this will be used as a prefix.
store_user_data (:obj:`bool`, optional): Whether user_data should be saved by this
persistence class. Default is ``True``.
persistence class. Default is :obj:`True`.
store_chat_data (:obj:`bool`, optional): Whether user_data should be saved by this
persistence class. Default is ``True``.
persistence class. Default is :obj:`True`.
store_bot_data (:obj:`bool`, optional): Whether bot_data should be saved by this
persistence class. Default is ``True`` .
single_file (:obj:`bool`, optional): When ``False`` will store 3 sperate files of
persistence class. Default is :obj:`True` .
single_file (:obj:`bool`, optional): When :obj:`False` will store 3 separate 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``.
:obj:`True`.
on_flush (:obj:`bool`, optional): When :obj:`True` will only save to file when
:meth:`flush` is called and keep data in memory until that happens. When
:obj:`False` will store data on any transaction *and* on call to :meth:`flush`.
Default is :obj:`False`.
"""
def __init__(self, filename,
store_user_data=True,
store_chat_data=True,
store_bot_data=True,
single_file=True,
on_flush=False):
super(PicklePersistence, self).__init__(store_user_data=store_user_data,
store_chat_data=store_chat_data,
store_bot_data=store_bot_data)
def __init__(self,
filename: str,
store_user_data: bool = True,
store_chat_data: bool = True,
store_bot_data: bool = True,
single_file: bool = True,
on_flush: bool = False):
super().__init__(store_user_data=store_user_data,
store_chat_data=store_chat_data,
store_bot_data=store_bot_data)
self.filename = filename
self.single_file = single_file
self.on_flush = on_flush
self.user_data = None
self.chat_data = None
self.bot_data = None
self.conversations = None
self.user_data: Optional[DefaultDict[int, Dict]] = None
self.chat_data: Optional[DefaultDict[int, Dict]] = None
self.bot_data: Optional[Dict] = None
self.conversations: Optional[Dict[str, Dict[Tuple, Any]]] = None
def load_singlefile(self):
def load_singlefile(self) -> None:
try:
filename = self.filename
with open(self.filename, "rb") as f:
@@ -88,7 +103,7 @@ class PicklePersistence(BasePersistence):
self.bot_data = data.get('bot_data', {})
self.conversations = data['conversations']
except IOError:
self.conversations = {}
self.conversations = dict()
self.user_data = defaultdict(dict)
self.chat_data = defaultdict(dict)
self.bot_data = {}
@@ -97,7 +112,7 @@ class PicklePersistence(BasePersistence):
except Exception:
raise TypeError("Something went wrong unpickling {}".format(filename))
def load_file(self, filename):
def load_file(self, filename: str) -> Any:
try:
with open(filename, "rb") as f:
return pickle.load(f)
@@ -108,18 +123,18 @@ class PicklePersistence(BasePersistence):
except Exception:
raise TypeError("Something went wrong unpickling {}".format(filename))
def dump_singlefile(self):
def dump_singlefile(self) -> None:
with open(self.filename, "wb") as f:
data = {'conversations': self.conversations, 'user_data': self.user_data,
'chat_data': self.chat_data, 'bot_data': self.bot_data}
pickle.dump(data, f)
def dump_file(self, filename, data):
def dump_file(self, filename: str, data: Any) -> None:
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.
def get_user_data(self) -> DefaultDict[int, Dict[Any, Any]]:
"""Returns the user_data from the pickle file if it exists or an empty :obj:`defaultdict`.
Returns:
:obj:`defaultdict`: The restored user data.
@@ -136,10 +151,10 @@ class PicklePersistence(BasePersistence):
self.user_data = data
else:
self.load_singlefile()
return deepcopy(self.user_data)
return deepcopy(self.user_data) # type: ignore[arg-type]
def get_chat_data(self):
"""Returns the chat_data from the pickle file if it exsists or an empty defaultdict.
def get_chat_data(self) -> DefaultDict[int, Dict[Any, Any]]:
"""Returns the chat_data from the pickle file if it exists or an empty :obj:`defaultdict`.
Returns:
:obj:`defaultdict`: The restored chat data.
@@ -156,13 +171,13 @@ class PicklePersistence(BasePersistence):
self.chat_data = data
else:
self.load_singlefile()
return deepcopy(self.chat_data)
return deepcopy(self.chat_data) # type: ignore[arg-type]
def get_bot_data(self):
"""Returns the bot_data from the pickle file if it exsists or an empty dict.
def get_bot_data(self) -> Dict[Any, Any]:
"""Returns the bot_data from the pickle file if it exists or an empty :obj:`dict`.
Returns:
:obj:`defaultdict`: The restored bot data.
:obj:`dict`: The restored bot data.
"""
if self.bot_data:
pass
@@ -174,10 +189,10 @@ class PicklePersistence(BasePersistence):
self.bot_data = data
else:
self.load_singlefile()
return deepcopy(self.bot_data)
return deepcopy(self.bot_data) # type: ignore[arg-type]
def get_conversations(self, name):
"""Returns the conversations from the pickle file if it exsists or an empty defaultdict.
def get_conversations(self, name: str) -> ConversationDict:
"""Returns the conversations from the pickle file if it exsists or an empty dict.
Args:
name (:obj:`str`): The handlers name.
@@ -195,17 +210,21 @@ class PicklePersistence(BasePersistence):
self.conversations = data
else:
self.load_singlefile()
return self.conversations.get(name, {}).copy()
return self.conversations.get(name, {}).copy() # type: ignore[union-attr]
def update_conversation(self, name, key, new_state):
def update_conversation(self,
name: str, key: Tuple[int, ...],
new_state: Optional[object]) -> None:
"""Will update the conversations for the given handler and depending on :attr:`on_flush`
save the pickle file.
Args:
name (:obj:`str`): The handlers name.
name (:obj:`str`): The handler's name.
key (:obj:`tuple`): The key the state is changed for.
new_state (:obj:`tuple` | :obj:`any`): The new state for the given key.
"""
if not self.conversations:
self.conversations = dict()
if self.conversations.setdefault(name, {}).get(key) == new_state:
return
self.conversations[name][key] = new_state
@@ -216,14 +235,15 @@ class PicklePersistence(BasePersistence):
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.
def update_user_data(self, user_id: int, data: Dict) -> None:
"""Will update the user_data and depending on :attr:`on_flush` save the pickle file.
Args:
user_id (:obj:`int`): The user the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.user_data` [user_id].
"""
if self.user_data is None:
self.user_data = defaultdict(dict)
if self.user_data.get(user_id) == data:
return
self.user_data[user_id] = data
@@ -234,14 +254,15 @@ class PicklePersistence(BasePersistence):
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.
def update_chat_data(self, chat_id: int, data: Dict) -> None:
"""Will update the chat_data and depending on :attr:`on_flush` save the pickle file.
Args:
chat_id (:obj:`int`): The chat the data might have been changed for.
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.chat_data` [chat_id].
"""
if self.chat_data is None:
self.chat_data = defaultdict(dict)
if self.chat_data.get(chat_id) == data:
return
self.chat_data[chat_id] = data
@@ -252,9 +273,8 @@ class PicklePersistence(BasePersistence):
else:
self.dump_singlefile()
def update_bot_data(self, data):
"""Will update the bot_data (if changed) and depending on :attr:`on_flush` save the
pickle file.
def update_bot_data(self, data: Dict) -> None:
"""Will update the bot_data and depending on :attr:`on_flush` save the pickle file.
Args:
data (:obj:`dict`): The :attr:`telegram.ext.dispatcher.bot_data`.
@@ -269,11 +289,11 @@ class PicklePersistence(BasePersistence):
else:
self.dump_singlefile()
def flush(self):
def flush(self) -> None:
""" Will save all data in memory to pickle file(s).
"""
if self.single_file:
if self.user_data or self.chat_data or self.conversations:
if self.user_data or self.chat_data or self.bot_data or self.conversations:
self.dump_singlefile()
else:
if self.user_data:
+19 -10
View File
@@ -20,6 +20,8 @@
from telegram import Update
from .handler import Handler
from telegram.utils.types import HandlerArg
class PollAnswerHandler(Handler):
"""Handler class to handle Telegram updates that contain a poll answer.
@@ -34,6 +36,7 @@ class PollAnswerHandler(Handler):
the callback function.
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
@@ -44,6 +47,10 @@ class PollAnswerHandler(Handler):
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Warning:
When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
@@ -53,26 +60,28 @@ class PollAnswerHandler(Handler):
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
pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
that contains new updates which can be used to insert updates. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
which can be used to schedule new jobs. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``user_data`` will be passed to the callback function. Default is :obj:`False`.
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``.
pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``chat_data`` will be passed to the callback function. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Defaults to :obj:`False`.
"""
def check_update(self, update):
def check_update(self, update: HandlerArg) -> bool:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
@@ -82,4 +91,4 @@ class PollAnswerHandler(Handler):
:obj:`bool`
"""
return isinstance(update, Update) and update.poll_answer
return isinstance(update, Update) and bool(update.poll_answer)
+19 -10
View File
@@ -20,6 +20,8 @@
from telegram import Update
from .handler import Handler
from telegram.utils.types import HandlerArg
class PollHandler(Handler):
"""Handler class to handle Telegram updates that contain a poll.
@@ -34,6 +36,7 @@ class PollHandler(Handler):
the callback function.
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
@@ -44,6 +47,10 @@ class PollHandler(Handler):
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Warning:
When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
@@ -53,26 +60,28 @@ class PollHandler(Handler):
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
pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
that contains new updates which can be used to insert updates. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
which can be used to schedule new jobs. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``user_data`` will be passed to the callback function. Default is :obj:`False`.
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``.
pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``chat_data`` will be passed to the callback function. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Defaults to :obj:`False`.
"""
def check_update(self, update):
def check_update(self, update: HandlerArg) -> bool:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
@@ -82,4 +91,4 @@ class PollHandler(Handler):
:obj:`bool`
"""
return isinstance(update, Update) and update.poll
return isinstance(update, Update) and bool(update.poll)
+19 -10
View File
@@ -21,6 +21,8 @@
from telegram import Update
from .handler import Handler
from telegram.utils.types import HandlerArg
class PreCheckoutQueryHandler(Handler):
"""Handler class to handle Telegram PreCheckout callback queries.
@@ -35,6 +37,7 @@ class PreCheckoutQueryHandler(Handler):
the callback function.
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
@@ -45,6 +48,10 @@ class PreCheckoutQueryHandler(Handler):
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Warning:
When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
@@ -54,26 +61,28 @@ class PreCheckoutQueryHandler(Handler):
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
pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
DEPRECATED: Please switch to context based callbacks.
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
that contains new updates which can be used to insert updates. Default is :obj:`False`.
pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
which can be used to schedule new jobs. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``user_data`` will be passed to the callback function. Default is :obj:`False`.
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``.
pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``chat_data`` will be passed to the callback function. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Defaults to :obj:`False`.
"""
def check_update(self, update):
def check_update(self, update: HandlerArg) -> bool:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
@@ -83,4 +92,4 @@ class PreCheckoutQueryHandler(Handler):
:obj:`bool`
"""
return isinstance(update, Update) and update.pre_checkout_query
return isinstance(update, Update) and bool(update.pre_checkout_query)
+62 -42
View File
@@ -25,6 +25,13 @@ from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.ext import MessageHandler, Filters
from telegram.utils.types import HandlerArg
from typing import Callable, TYPE_CHECKING, Any, Optional, Union, TypeVar, Dict, Pattern
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
RT = TypeVar('RT')
class RegexHandler(MessageHandler):
"""Handler class to handle Telegram updates based on a regex.
@@ -48,11 +55,16 @@ class RegexHandler(MessageHandler):
the callback function.
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Note:
This handler is being deprecated. For the same usecase use:
This handler is being deprecated. For the same use case use:
``MessageHandler(Filters.regex(r'pattern'), callback)``
Warning:
When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
@@ -66,28 +78,30 @@ class RegexHandler(MessageHandler):
: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``
Default is :obj:`False`
pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``.
Default is ``False``
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
Default is :obj:`False`
pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
that contains new updates which can be used to insert updates. Default is :obj:`False`.
pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
pass_chat_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``chat_data`` will be passed to the callback function. Default is ``False``.
which can be used to schedule new jobs. Default is :obj:`False`.
pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``user_data`` will be passed to the callback function. Default is :obj:`False`.
pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``chat_data`` will be passed to the callback function. Default is :obj:`False`.
message_updates (:obj:`bool`, optional): Should "normal" message updates be handled?
Default is ``True``.
Default is :obj:`True`.
channel_post_updates (:obj:`bool`, optional): Should channel posts updates be handled?
Default is ``True``.
Default is :obj:`True`.
edited_updates (:obj:`bool`, optional): Should "edited" message updates be handled? Default
is ``False``.
is :obj:`False`.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Defaults to :obj:`False`.
Raises:
ValueError
@@ -95,38 +109,44 @@ class RegexHandler(MessageHandler):
"""
def __init__(self,
pattern,
callback,
pass_groups=False,
pass_groupdict=False,
pass_update_queue=False,
pass_job_queue=False,
pass_user_data=False,
pass_chat_data=False,
allow_edited=False,
message_updates=True,
channel_post_updates=False,
edited_updates=False):
pattern: Union[str, Pattern],
callback: Callable[[HandlerArg, 'CallbackContext'], RT],
pass_groups: bool = False,
pass_groupdict: bool = False,
pass_update_queue: bool = False,
pass_job_queue: bool = False,
pass_user_data: bool = False,
pass_chat_data: bool = False,
allow_edited: bool = False,
message_updates: bool = True,
channel_post_updates: bool = False,
edited_updates: bool = False,
run_async: bool = 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)
super().__init__(Filters.regex(pattern),
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
pass_chat_data=pass_chat_data,
message_updates=message_updates,
channel_post_updates=channel_post_updates,
edited_updates=edited_updates,
run_async=run_async)
self.pass_groups = pass_groups
self.pass_groupdict = pass_groupdict
def collect_optional_args(self, dispatcher, update=None, check_result=None):
optional_args = super(RegexHandler, self).collect_optional_args(dispatcher, update,
check_result)
if self.pass_groups:
optional_args['groups'] = check_result['matches'][0].groups()
if self.pass_groupdict:
optional_args['groupdict'] = check_result['matches'][0].groupdict()
def collect_optional_args(
self,
dispatcher: 'Dispatcher',
update: HandlerArg = None,
check_result: Optional[Union[bool, Dict[str, Any]]] = None) -> Dict[str, Any]:
optional_args = super().collect_optional_args(dispatcher, update, check_result)
if isinstance(check_result, dict):
if self.pass_groups:
optional_args['groups'] = check_result['matches'][0].groups()
if self.pass_groupdict:
optional_args['groupdict'] = check_result['matches'][0].groupdict()
return optional_args
+19 -10
View File
@@ -21,6 +21,8 @@
from telegram import Update
from .handler import Handler
from telegram.utils.types import HandlerArg
class ShippingQueryHandler(Handler):
"""Handler class to handle Telegram shipping callback queries.
@@ -35,6 +37,7 @@ class ShippingQueryHandler(Handler):
the callback function.
pass_chat_data (:obj:`bool`): Determines whether ``chat_data`` will be passed to
the callback function.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Note:
:attr:`pass_user_data` and :attr:`pass_chat_data` determine whether a ``dict`` you
@@ -45,6 +48,10 @@ class ShippingQueryHandler(Handler):
Note that this is DEPRECATED, and you should use context based callbacks. See
https://git.io/fxJuV for more info.
Warning:
When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
@@ -54,26 +61,28 @@ class ShippingQueryHandler(Handler):
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
pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
that contains new updates which can be used to insert updates. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
which can be used to schedule new jobs. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_user_data (:obj:`bool`, optional): If set to ``True``, a keyword argument called
``user_data`` will be passed to the callback function. Default is ``False``.
pass_user_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``user_data`` will be passed to the callback function. Default is :obj:`False`.
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``.
pass_chat_data (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``chat_data`` will be passed to the callback function. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Defaults to :obj:`False`.
"""
def check_update(self, update):
def check_update(self, update: HandlerArg) -> bool:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
@@ -83,4 +92,4 @@ class ShippingQueryHandler(Handler):
:obj:`bool`
"""
return isinstance(update, Update) and update.shipping_query
return isinstance(update, Update) and bool(update.shipping_query)
+42 -21
View File
@@ -18,10 +18,15 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the StringCommandHandler class."""
from future.utils import string_types
from .handler import Handler
from telegram.utils.types import HandlerArg
from typing import Callable, TYPE_CHECKING, Any, Optional, TypeVar, Dict, List
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
RT = TypeVar('RT')
class StringCommandHandler(Handler):
"""Handler class to handle string commands. Commands are string updates that start with ``/``.
@@ -30,6 +35,10 @@ class StringCommandHandler(Handler):
This handler is not used to handle Telegram :attr:`telegram.Update`, but strings manually
put in the queue. For example to send messages with the bot using command line or API.
Warning:
When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Attributes:
command (:obj:`str`): The command this handler should listen for.
callback (:obj:`callable`): The callback function for this handler.
@@ -39,8 +48,10 @@ class StringCommandHandler(Handler):
passed to the callback function.
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Args:
command (:obj:`str`): The command this handler should listen for.
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
Callback signature for context based API:
@@ -52,35 +63,39 @@ class StringCommandHandler(Handler):
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``
consecutive whitespace characters. Default is :obj:`False`
DEPRECATED: Please switch to context based callbacks.
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
that contains new updates which can be used to insert updates. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
which can be used to schedule new jobs. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Defaults to :obj:`False`.
"""
def __init__(self,
command,
callback,
pass_args=False,
pass_update_queue=False,
pass_job_queue=False):
super(StringCommandHandler, self).__init__(
command: str,
callback: Callable[[HandlerArg, 'CallbackContext'], RT],
pass_args: bool = False,
pass_update_queue: bool = False,
pass_job_queue: bool = False,
run_async: bool = False):
super().__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue)
pass_job_queue=pass_job_queue,
run_async=run_async)
self.command = command
self.pass_args = pass_args
def check_update(self, update):
def check_update(self, update: HandlerArg) -> Optional[List[str]]:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
@@ -90,18 +105,24 @@ class StringCommandHandler(Handler):
:obj:`bool`
"""
if isinstance(update, string_types) and update.startswith('/'):
if isinstance(update, str) and update.startswith('/'):
args = update[1:].split(' ')
if args[0] == self.command:
return args[1:]
return None
def collect_optional_args(self, dispatcher, update=None, check_result=None):
optional_args = super(StringCommandHandler, self).collect_optional_args(dispatcher,
update,
check_result)
def collect_optional_args(self,
dispatcher: 'Dispatcher',
update: HandlerArg = None,
check_result: Optional[List[str]] = None) -> Dict[str, Any]:
optional_args = super().collect_optional_args(dispatcher, update, check_result)
if self.pass_args:
optional_args['args'] = check_result
return optional_args
def collect_additional_context(self, context, update, dispatcher, check_result):
def collect_additional_context(self,
context: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Optional[List[str]]) -> None:
context.args = check_result
+47 -26
View File
@@ -20,10 +20,15 @@
import re
from future.utils import string_types
from .handler import Handler
from typing import Callable, TYPE_CHECKING, Optional, TypeVar, Match, Dict, Any, Union, Pattern
from telegram.utils.types import HandlerArg
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
RT = TypeVar('RT')
class StringRegexHandler(Handler):
"""Handler class to handle string updates based on a regex which checks the update content.
@@ -35,6 +40,10 @@ class StringRegexHandler(Handler):
This handler is not used to handle Telegram :attr:`telegram.Update`, but strings manually
put in the queue. For example to send messages with the bot using command line or API.
Warning:
When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Attributes:
pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
callback (:obj:`callable`): The callback function for this handler.
@@ -46,6 +55,7 @@ class StringRegexHandler(Handler):
passed to the callback function.
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Args:
pattern (:obj:`str` | :obj:`Pattern`): The regex pattern.
@@ -59,45 +69,49 @@ class StringRegexHandler(Handler):
: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``
Default is :obj:`False`
DEPRECATED: Please switch to context based callbacks.
pass_groupdict (:obj:`bool`, optional): If the callback should be passed the result of
``re.match(pattern, data).groupdict()`` as a keyword argument called ``groupdict``.
Default is ``False``
Default is :obj:`False`
DEPRECATED: Please switch to context based callbacks.
pass_update_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
that contains new updates which can be used to insert updates. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
which can be used to schedule new jobs. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Defaults to :obj:`False`.
"""
def __init__(self,
pattern,
callback,
pass_groups=False,
pass_groupdict=False,
pass_update_queue=False,
pass_job_queue=False):
super(StringRegexHandler, self).__init__(
pattern: Union[str, Pattern],
callback: Callable[[HandlerArg, 'CallbackContext'], RT],
pass_groups: bool = False,
pass_groupdict: bool = False,
pass_update_queue: bool = False,
pass_job_queue: bool = False,
run_async: bool = False):
super().__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue)
pass_job_queue=pass_job_queue,
run_async=run_async)
if isinstance(pattern, string_types):
if isinstance(pattern, str):
pattern = re.compile(pattern)
self.pattern = pattern
self.pass_groups = pass_groups
self.pass_groupdict = pass_groupdict
def check_update(self, update):
def check_update(self, update: HandlerArg) -> Optional[Match]:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
@@ -107,21 +121,28 @@ class StringRegexHandler(Handler):
:obj:`bool`
"""
if isinstance(update, string_types):
if isinstance(update, str):
match = re.match(self.pattern, update)
if match:
return match
return None
def collect_optional_args(self, dispatcher, update=None, check_result=None):
optional_args = super(StringRegexHandler, self).collect_optional_args(dispatcher,
update, check_result)
def collect_optional_args(self,
dispatcher: 'Dispatcher',
update: HandlerArg = None,
check_result: Optional[Match] = None) -> Dict[str, Any]:
optional_args = super().collect_optional_args(dispatcher, update, check_result)
if self.pattern:
if self.pass_groups:
if self.pass_groups and check_result:
optional_args['groups'] = check_result.groups()
if self.pass_groupdict:
if self.pass_groupdict and check_result:
optional_args['groupdict'] = check_result.groupdict()
return optional_args
def collect_additional_context(self, context, update, dispatcher, check_result):
if self.pattern:
def collect_additional_context(self,
context: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Optional[Match]) -> None:
if self.pattern and check_result:
context.matches = [check_result]
+31 -14
View File
@@ -21,17 +21,30 @@
from .handler import Handler
from typing import Callable, TYPE_CHECKING, TypeVar, Type, Any
if TYPE_CHECKING:
from telegram.ext import CallbackContext
RT = TypeVar('RT')
class TypeHandler(Handler):
"""Handler class to handle updates of custom types.
Attributes:
type (:obj:`type`): The ``type`` of updates this handler should process.
callback (:obj:`callable`): The callback function for this handler.
strict (:obj:`bool`): Use ``type`` instead of ``isinstance``. Default is ``False``.
strict (:obj:`bool`): Use ``type`` instead of ``isinstance``. Default is :obj:`False`.
pass_update_queue (:obj:`bool`): Determines whether ``update_queue`` will be
passed to the callback function.
pass_job_queue (:obj:`bool`): Determines whether ``job_queue`` will be passed to
the callback function.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Warning:
When setting ``run_async`` to :obj:`True`, you cannot rely on adding custom
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
Args:
type (:obj:`type`): The ``type`` of updates this handler should process, as
@@ -45,34 +58,38 @@ class TypeHandler(Handler):
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
Default is :obj:`False`
pass_update_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``update_queue`` will be passed to the callback function. It will be the ``Queue``
instance used by the :class:`telegram.ext.Updater` and :class:`telegram.ext.Dispatcher`
that contains new updates which can be used to insert updates. Default is ``False``.
that contains new updates which can be used to insert updates. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
pass_job_queue (:obj:`bool`, optional): If set to ``True``, a keyword argument called
pass_job_queue (:obj:`bool`, optional): If set to :obj:`True`, a keyword argument called
``job_queue`` will be passed to the callback function. It will be a
:class:`telegram.ext.JobQueue` instance created by the :class:`telegram.ext.Updater`
which can be used to schedule new jobs. Default is ``False``.
which can be used to schedule new jobs. Default is :obj:`False`.
DEPRECATED: Please switch to context based callbacks.
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
Defaults to :obj:`False`.
"""
def __init__(self,
type,
callback,
strict=False,
pass_update_queue=False,
pass_job_queue=False):
super(TypeHandler, self).__init__(
type: Type,
callback: Callable[[Any, 'CallbackContext'], RT],
strict: bool = False,
pass_update_queue: bool = False,
pass_job_queue: bool = False,
run_async: bool = False):
super().__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue)
pass_job_queue=pass_job_queue,
run_async=run_async)
self.type = type
self.strict = strict
def check_update(self, update):
def check_update(self, update: Any) -> bool:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
Args:
+117 -73
View File
@@ -20,6 +20,7 @@
import logging
import ssl
import warnings
from threading import Thread, Lock, current_thread, Event
from time import sleep
from signal import signal, SIGINT, SIGTERM, SIGABRT
@@ -28,14 +29,18 @@ from queue import Queue
from telegram import Bot, TelegramError
from telegram.ext import Dispatcher, JobQueue
from telegram.error import Unauthorized, InvalidToken, RetryAfter, TimedOut
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.utils.helpers import get_signal_name
from telegram.utils.request import Request
from telegram.utils.webhookhandler import (WebhookServer, WebhookAppClass)
logging.getLogger(__name__).addHandler(logging.NullHandler())
from typing import Callable, Dict, TYPE_CHECKING, Any, List, Union, Tuple, no_type_check, Optional
if TYPE_CHECKING:
from telegram.ext import BasePersistence, Defaults
class Updater(object):
class Updater:
"""
This class, which employs the :class:`telegram.ext.Dispatcher`, provides a frontend to
:class:`telegram.Bot` to the programmer, so they can focus on coding the bot. Its purpose is to
@@ -49,7 +54,8 @@ class Updater(object):
Attributes:
bot (:class:`telegram.Bot`): The bot used with this Updater.
user_sig_handler (:obj:`signal`): signals the updater will respond to.
user_sig_handler (:obj:`function`): Optional. Function to be called when a signal is
received.
update_queue (:obj:`Queue`): Queue for the updates.
job_queue (:class:`telegram.ext.JobQueue`): Jobqueue for the updater.
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that handles the updates and
@@ -57,7 +63,7 @@ class Updater(object):
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.
use_context (:obj:`bool`): Optional. :obj:`True` if using context based callbacks.
Args:
token (:obj:`str`, optional): The bot's token given by the @BotFather.
@@ -76,14 +82,14 @@ class Updater(object):
private_key_password (:obj:`bytes`, optional): Password for above private key.
user_sig_handler (:obj:`function`, optional): Takes ``signum, frame`` as positional
arguments. This will be called when a signal is received, defaults are (SIGINT,
SIGTERM, SIGABRT) setable with :attr:`idle`.
SIGTERM, SIGABRT) settable with :attr:`idle`.
request_kwargs (:obj:`dict`, optional): Keyword args to control the creation of a
`telegram.utils.request.Request` object (ignored if `bot` or `dispatcher` argument is
used). The request_kwargs are very useful for the advanced users who would like to
control the default timeouts and/or control the proxy used for http communication.
use_context (:obj:`bool`, optional): If set to ``True`` Use the context based callback API
(ignored if `dispatcher` argument is used). During the deprecation period of the old
API the default is ``False``. **New users**: set this to ``True``.
use_context (:obj:`bool`, optional): If set to :obj:`True` uses the context based callback
API (ignored if `dispatcher` argument is used). Defaults to :obj:`True`.
**New users**: set this to :obj:`True`.
persistence (:class:`telegram.ext.BasePersistence`, optional): The persistence class to
store data that should be persistent over restarts (ignored if `dispatcher` argument is
used).
@@ -103,19 +109,25 @@ class Updater(object):
_request = None
def __init__(self,
token=None,
base_url=None,
workers=4,
bot=None,
private_key=None,
private_key_password=None,
user_sig_handler=None,
request_kwargs=None,
persistence=None,
defaults=None,
use_context=False,
dispatcher=None,
base_file_url=None):
token: str = None,
base_url: str = None,
workers: int = 4,
bot: Bot = None,
private_key: bytes = None,
private_key_password: bytes = None,
user_sig_handler: Callable = None,
request_kwargs: Dict[str, Any] = None,
persistence: 'BasePersistence' = None,
defaults: 'Defaults' = None,
use_context: bool = True,
dispatcher: Dispatcher = None,
base_file_url: str = None):
if defaults and bot:
warnings.warn('Passing defaults to an Updater has no effect when a Bot is passed '
'as well. Pass them to the Bot instead.',
TelegramDeprecationWarning,
stacklevel=2)
if dispatcher is None:
if (token is None) and (bot is None):
@@ -157,14 +169,14 @@ class Updater(object):
if 'con_pool_size' not in request_kwargs:
request_kwargs['con_pool_size'] = con_pool_size
self._request = Request(**request_kwargs)
self.bot = Bot(token,
self.bot = Bot(token, # type: ignore[arg-type]
base_url,
base_file_url=base_file_url,
request=self._request,
private_key=private_key,
private_key_password=private_key_password,
defaults=defaults)
self.update_queue = Queue()
self.update_queue: Queue = Queue()
self.job_queue = JobQueue()
self.__exception_event = Event()
self.persistence = persistence
@@ -196,12 +208,9 @@ class Updater(object):
self.is_idle = False
self.httpd = None
self.__lock = Lock()
self.__threads = []
self.__threads: List[Thread] = []
# Just for passing to WebhookAppClass
self._default_quote = defaults.quote if defaults else None
def _init_thread(self, target, name, *args, **kwargs):
def _init_thread(self, target: Callable, name: str, *args: Any, **kwargs: Any) -> None:
thr = Thread(target=self._thread_wrapper,
name="Bot:{}:{}".format(self.bot.id, name),
args=(target,) + args,
@@ -209,24 +218,24 @@ class Updater(object):
thr.start()
self.__threads.append(thr)
def _thread_wrapper(self, target, *args, **kwargs):
def _thread_wrapper(self, target: Callable, *args: Any, **kwargs: Any) -> None:
thr_name = current_thread().name
self.logger.debug('{0} - started'.format(thr_name))
self.logger.debug('{} - started'.format(thr_name))
try:
target(*args, **kwargs)
except Exception:
self.__exception_event.set()
self.logger.exception('unhandled exception in %s', thr_name)
raise
self.logger.debug('{0} - ended'.format(thr_name))
self.logger.debug('{} - ended'.format(thr_name))
def start_polling(self,
poll_interval=0.0,
timeout=10,
clean=False,
bootstrap_retries=-1,
read_latency=2.,
allowed_updates=None):
poll_interval: float = 0.0,
timeout: float = 10,
clean: bool = False,
bootstrap_retries: int = -1,
read_latency: float = 2.,
allowed_updates: List[str] = None) -> Optional[Queue]:
"""Starts polling updates from Telegram.
Args:
@@ -234,7 +243,7 @@ class Updater(object):
Telegram in seconds. Default is 0.0.
timeout (:obj:`float`, optional): Passed to :attr:`telegram.Bot.get_updates`.
clean (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers
before actually starting to poll. Default is False.
before actually starting to poll. Default is :obj:`False`.
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
`Updater` will retry on failures on the Telegram server.
@@ -259,25 +268,31 @@ class Updater(object):
# Create & start threads
self.job_queue.start()
dispatcher_ready = Event()
polling_ready = Event()
self._init_thread(self.dispatcher.start, "dispatcher", ready=dispatcher_ready)
self._init_thread(self._start_polling, "updater", poll_interval, timeout,
read_latency, bootstrap_retries, clean, allowed_updates)
read_latency, bootstrap_retries, clean, allowed_updates,
ready=polling_ready)
self.logger.debug('Waiting for Dispatcher and polling to start')
dispatcher_ready.wait()
polling_ready.wait()
# Return the update queue so the main thread can insert updates
return self.update_queue
return None
def start_webhook(self,
listen='127.0.0.1',
port=80,
url_path='',
cert=None,
key=None,
clean=False,
bootstrap_retries=0,
webhook_url=None,
allowed_updates=None):
listen: str = '127.0.0.1',
port: int = 80,
url_path: str = '',
cert: str = None,
key: str = None,
clean: bool = False,
bootstrap_retries: int = 0,
webhook_url: str = None,
allowed_updates: List[str] = None,
force_event_loop: bool = False) -> Optional[Queue]:
"""
Starts a small http server to listen for updates via webhook. If cert
and key are not provided, the webhook will be started directly on
@@ -285,6 +300,15 @@ class Updater(object):
application. Else, the webhook will be started on
https://listen:port/url_path
Note:
Due to an incompatibility of the Tornado library PTB uses for the webhook with Python
3.8+ on Windows machines, PTB will attempt to set the event loop to
:attr:`asyncio.SelectorEventLoop` and raise an exception, if an incompatible event loop
has already been specified. See this `thread`_ for more details. To suppress the
exception, set :attr:`force_event_loop` to :obj:`True`.
.. _thread: https://github.com/tornadoweb/tornado/issues/2608
Args:
listen (:obj:`str`, optional): IP-Address to listen on. Default ``127.0.0.1``.
port (:obj:`int`, optional): Port the bot should be listening on. Default ``80``.
@@ -292,7 +316,7 @@ class Updater(object):
cert (:obj:`str`, optional): Path to the SSL certificate file.
key (:obj:`str`, optional): Path to the SSL key file.
clean (:obj:`bool`, optional): Whether to clean any pending updates on Telegram servers
before actually starting the webhook. Default is ``False``.
before actually starting the webhook. Default is :obj:`False`.
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
`Updater` will retry on failures on the Telegram server.
@@ -304,6 +328,8 @@ class Updater(object):
NAT, reverse proxy, etc. Default is derived from `listen`, `port` & `url_path`.
allowed_updates (List[:obj:`str`], optional): Passed to
:attr:`telegram.Bot.set_webhook`.
force_event_loop (:obj:`bool`, optional): Force using the current event loop. See above
note for details. Defaults to :obj:`False`
Returns:
:obj:`Queue`: The update queue that can be filled from the main thread.
@@ -314,16 +340,25 @@ class Updater(object):
self.running = True
# Create & start threads
webhook_ready = Event()
dispatcher_ready = Event()
self.job_queue.start()
self._init_thread(self.dispatcher.start, "dispatcher"),
self._init_thread(self.dispatcher.start, "dispatcher", dispatcher_ready)
self._init_thread(self._start_webhook, "updater", listen, port, url_path, cert,
key, bootstrap_retries, clean, webhook_url, allowed_updates)
key, bootstrap_retries, clean, webhook_url, allowed_updates,
ready=webhook_ready, force_event_loop=force_event_loop)
self.logger.debug('Waiting for Dispatcher and Webhook to start')
webhook_ready.wait()
dispatcher_ready.wait()
# Return the update queue so the main thread can insert updates
return self.update_queue
return None
@no_type_check
def _start_polling(self, poll_interval, timeout, read_latency, bootstrap_retries, clean,
allowed_updates): # pragma: no cover
allowed_updates, ready=None): # pragma: no cover
# Thread target of thread 'updater'. Runs in background, pulls
# updates from Telegram and inserts them in the update queue of the
# Dispatcher.
@@ -355,14 +390,18 @@ class Updater(object):
# broadcast it
self.update_queue.put(exc)
if ready is not None:
ready.set()
self._network_loop_retry(polling_action_cb, polling_onerr_cb, 'getting Updates',
poll_interval)
@no_type_check
def _network_loop_retry(self, action_cb, onerr_cb, description, interval):
"""Perform a loop calling `action_cb`, retrying after network errors.
Stop condition for loop: `self.running` evaluates False or return value of `action_cb`
evaluates False.
Stop condition for loop: `self.running` evaluates :obj:`False` or return value of
`action_cb` evaluates :obj:`False`.
Args:
action_cb (:obj:`callable`): Network oriented callback function to call.
@@ -400,7 +439,7 @@ class Updater(object):
sleep(cur_interval)
@staticmethod
def _increase_poll_interval(current_interval):
def _increase_poll_interval(current_interval: float) -> float:
# increase waiting times on subsequent errors up to 30secs
if current_interval == 0:
current_interval = 1
@@ -410,16 +449,16 @@ class Updater(object):
current_interval = 30
return current_interval
@no_type_check
def _start_webhook(self, listen, port, url_path, cert, key, bootstrap_retries, clean,
webhook_url, allowed_updates):
webhook_url, allowed_updates, ready=None, force_event_loop=False):
self.logger.debug('Updater thread started (webhook)')
use_ssl = cert is not None and key is not None
if not url_path.startswith('/'):
url_path = '/{0}'.format(url_path)
url_path = '/{}'.format(url_path)
# Create Tornado app instance
app = WebhookAppClass(url_path, self.bot, self.update_queue,
default_quote=self._default_quote)
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
@@ -449,12 +488,13 @@ class Updater(object):
self.logger.warning("cleaning updates is not supported if "
"SSL-termination happens elsewhere; skipping")
self.httpd.serve_forever()
self.httpd.serve_forever(force_event_loop=force_event_loop, ready=ready)
@staticmethod
def _gen_webhook_url(listen, port, url_path):
def _gen_webhook_url(listen: str, port: int, url_path: str) -> str:
return 'https://{listen}:{port}{path}'.format(listen=listen, port=port, path=url_path)
@no_type_check
def _bootstrap(self,
max_retries,
clean,
@@ -512,7 +552,7 @@ class Updater(object):
self._network_loop_retry(bootstrap_set_webhook, bootstrap_onerr_cb,
'bootstrap set webhook', bootstrap_interval)
def stop(self):
def stop(self) -> None:
"""Stops the polling/webhook thread, the dispatcher and the job queue."""
self.job_queue.stop()
@@ -530,7 +570,8 @@ class Updater(object):
if self._request:
self._request.stop()
def _stop_httpd(self):
@no_type_check
def _stop_httpd(self) -> None:
if self.httpd:
self.logger.debug('Waiting for current webhook connection to be '
'closed... Send a Telegram message to the bot to exit '
@@ -538,24 +579,27 @@ class Updater(object):
self.httpd.shutdown()
self.httpd = None
def _stop_dispatcher(self):
@no_type_check
def _stop_dispatcher(self) -> None:
self.logger.debug('Requesting Dispatcher to stop...')
self.dispatcher.stop()
def _join_threads(self):
@no_type_check
def _join_threads(self) -> None:
for thr in self.__threads:
self.logger.debug('Waiting for {0} thread to end'.format(thr.name))
self.logger.debug('Waiting for {} thread to end'.format(thr.name))
thr.join()
self.logger.debug('{0} thread has ended'.format(thr.name))
self.logger.debug('{} thread has ended'.format(thr.name))
self.__threads = []
def signal_handler(self, signum, frame):
@no_type_check
def signal_handler(self, signum, frame) -> None:
self.is_idle = False
if self.running:
self.logger.info('Received signal {} ({}), stopping...'.format(
signum, get_signal_name(signum)))
if self.persistence:
# Update user_data and chat_data before flushing
# Update user_data, chat_data and bot_data before flushing
self.dispatcher.update_persistence()
self.persistence.flush()
self.stop()
@@ -566,13 +610,13 @@ class Updater(object):
import os
os._exit(1)
def idle(self, stop_signals=(SIGINT, SIGTERM, SIGABRT)):
def idle(self, stop_signals: Union[List, Tuple] = (SIGINT, SIGTERM, SIGABRT)) -> None:
"""Blocks until one of the signals are received and stops the updater.
Args:
stop_signals (:obj:`iterable`): Iterable containing signals from the signal module that
should be subscribed to. Updater.stop() will be called on receiving one of those
signals. Defaults to (``SIGINT``, ``SIGTERM``, ``SIGABRT``).
stop_signals (:obj:`list` | :obj:`tuple`): List containing signals from the signal
module that should be subscribed to. Updater.stop() will be called on receiving one
of those signals. Defaults to (``SIGINT``, ``SIGTERM``, ``SIGABRT``).
"""
for sig in stop_signals:
+31 -22
View File
@@ -20,9 +20,17 @@
from telegram import PhotoSize
from telegram import TelegramObject
from telegram.utils.types import JSONDict
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot, File
class Animation(TelegramObject):
"""This object represents an animation file to be displayed in the message containing a game.
"""This object represents an animation file (GIF or H.264/MPEG-4 AVC video without sound).
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`file_unique_id` is equal.
Attributes:
file_id (:obj:`str`): File identifier.
@@ -32,8 +40,7 @@ class Animation(TelegramObject):
width (:obj:`int`): Video width as defined by sender.
height (:obj:`int`): Video height as defined by sender.
duration (:obj:`int`): Duration of the video in seconds as defined by sender.
thumb (:class:`telegram.PhotoSize`): Optional. Animation thumbnail as defined
by sender.
thumb (:class:`telegram.PhotoSize`): Optional. Animation thumbnail as defined by sender.
file_name (:obj:`str`): Optional. Original animation filename as defined by sender.
mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender.
file_size (:obj:`int`): Optional. File size.
@@ -42,8 +49,9 @@ class Animation(TelegramObject):
Args:
file_id (:obj:`str`): Identifier for this file, which can be used to download
or reuse the file.
file_unique_id (:obj:`str`): Unique and the same over time and
for different bots file identifier.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
width (:obj:`int`): Video width as defined by sender.
height (:obj:`int`): Video height as defined by sender.
duration (:obj:`int`): Duration of the video in seconds as defined by sender.
@@ -57,17 +65,17 @@ class Animation(TelegramObject):
"""
def __init__(self,
file_id,
file_unique_id,
width,
height,
duration,
thumb=None,
file_name=None,
mime_type=None,
file_size=None,
bot=None,
**kwargs):
file_id: str,
file_unique_id: str,
width: int,
height: int,
duration: int,
thumb: PhotoSize = None,
file_name: str = None,
mime_type: str = None,
file_size: int = None,
bot: 'Bot' = None,
**kwargs: Any):
# Required
self.file_id = str(file_id)
self.file_unique_id = str(file_unique_id)
@@ -84,24 +92,25 @@ class Animation(TelegramObject):
self._id_attrs = (self.file_unique_id,)
@classmethod
def de_json(cls, data, bot):
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Animation']:
data = cls.parse_data(data)
if not data:
return None
data = super(Animation, cls).de_json(data, bot)
data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
return cls(bot=bot, **data)
def get_file(self, timeout=None, **kwargs):
def get_file(self, timeout: int = None, api_kwargs: JSONDict = None) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
Args:
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API.
Returns:
:class:`telegram.File`
@@ -110,4 +119,4 @@ class Animation(TelegramObject):
:class:`telegram.TelegramError`
"""
return self.bot.get_file(self.file_id, timeout=timeout, **kwargs)
return self.bot.get_file(self.file_id, timeout=timeout, api_kwargs=api_kwargs)
+30 -19
View File
@@ -20,12 +20,20 @@
from telegram import TelegramObject, PhotoSize
from telegram.utils.types import JSONDict
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot, File
class Audio(TelegramObject):
"""This object represents an audio file to be treated as music by the Telegram clients.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`file_unique_id` is equal.
Attributes:
file_id (:obj:`str`): Unique identifier for this file.
file_id (:obj:`str`): Identifier for this file.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
@@ -36,14 +44,14 @@ class Audio(TelegramObject):
mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender.
file_size (:obj:`int`): Optional. File size.
thumb (:class:`telegram.PhotoSize`): Optional. Thumbnail of the album cover to
which the music file belongs
which the music file belongs.
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
Args:
file_id (:obj:`str`): Identifier for this file, which can be used to download
or reuse the file.
file_unique_id (:obj:`str`): Unique and the same over time and
for different bots file identifier.
file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be
the same over time and for different bots. Can't be used to download or reuse the file.
duration (:obj:`int`): Duration of the audio in seconds as defined by sender.
performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio
tags.
@@ -51,23 +59,23 @@ class Audio(TelegramObject):
mime_type (:obj:`str`, optional): MIME type of the file as defined by sender.
file_size (:obj:`int`, optional): File size.
thumb (:class:`telegram.PhotoSize`, optional): Thumbnail of the album cover to
which the music file belongs
which the music file belongs.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
def __init__(self,
file_id,
file_unique_id,
duration,
performer=None,
title=None,
mime_type=None,
file_size=None,
thumb=None,
bot=None,
**kwargs):
file_id: str,
file_unique_id: str,
duration: int,
performer: str = None,
title: str = None,
mime_type: str = None,
file_size: int = None,
thumb: PhotoSize = None,
bot: 'Bot' = None,
**kwargs: Any):
# Required
self.file_id = str(file_id)
self.file_unique_id = str(file_unique_id)
@@ -83,7 +91,9 @@ class Audio(TelegramObject):
self._id_attrs = (self.file_unique_id,)
@classmethod
def de_json(cls, data, bot):
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Audio']:
data = cls.parse_data(data)
if not data:
return None
@@ -91,14 +101,15 @@ class Audio(TelegramObject):
return cls(bot=bot, **data)
def get_file(self, timeout=None, **kwargs):
def get_file(self, timeout: int = None, api_kwargs: JSONDict = None) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
Args:
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API.
Returns:
:class:`telegram.File`
@@ -107,4 +118,4 @@ class Audio(TelegramObject):
:class:`telegram.TelegramError`
"""
return self.bot.get_file(self.file_id, timeout=timeout, **kwargs)
return self.bot.get_file(self.file_id, timeout=timeout, api_kwargs=api_kwargs)
+20 -16
View File
@@ -19,10 +19,18 @@
"""This module contains an object that represents a Telegram ChatPhoto."""
from telegram import TelegramObject
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot, File
class ChatPhoto(TelegramObject):
"""This object represents a chat photo.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`small_file_unique_id` and :attr:`big_file_unique_id` are
equal.
Attributes:
small_file_id (:obj:`str`): File identifier of small (160x160) chat photo.
This file_id can be used only for photo download and only for as long
@@ -54,11 +62,12 @@ class ChatPhoto(TelegramObject):
"""
def __init__(self,
small_file_id,
small_file_unique_id,
big_file_id,
big_file_unique_id,
bot=None, **kwargs):
small_file_id: str,
small_file_unique_id: str,
big_file_id: str,
big_file_unique_id: str,
bot: 'Bot' = None,
**kwargs: Any):
self.small_file_id = small_file_id
self.small_file_unique_id = small_file_unique_id
self.big_file_id = big_file_id
@@ -68,14 +77,7 @@ class ChatPhoto(TelegramObject):
self._id_attrs = (self.small_file_unique_id, self.big_file_unique_id,)
@classmethod
def de_json(cls, data, bot):
if not data:
return None
return cls(bot=bot, **data)
def get_small_file(self, timeout=None, **kwargs):
def get_small_file(self, timeout: int = None, **kwargs: Any) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the
small (160x160) chat photo
@@ -83,7 +85,8 @@ class ChatPhoto(TelegramObject):
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API.
Returns:
:class:`telegram.File`
@@ -94,7 +97,7 @@ class ChatPhoto(TelegramObject):
"""
return self.bot.get_file(self.small_file_id, timeout=timeout, **kwargs)
def get_big_file(self, timeout=None, **kwargs):
def get_big_file(self, timeout: int = None, **kwargs: Any) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the
big (640x640) chat photo
@@ -102,7 +105,8 @@ class ChatPhoto(TelegramObject):
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API.
Returns:
:class:`telegram.File`
+11 -9
View File
@@ -19,11 +19,15 @@
"""This module contains an object that represents a Telegram Contact."""
from telegram import TelegramObject
from typing import Any
class Contact(TelegramObject):
"""This object represents a phone contact.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`phone_number` is equal.
Attributes:
phone_number (:obj:`str`): Contact's phone number.
first_name (:obj:`str`): Contact's first name.
@@ -41,8 +45,13 @@ class Contact(TelegramObject):
"""
def __init__(self, phone_number, first_name, last_name=None, user_id=None, vcard=None,
**kwargs):
def __init__(self,
phone_number: str,
first_name: str,
last_name: str = None,
user_id: int = None,
vcard: str = None,
**kwargs: Any):
# Required
self.phone_number = str(phone_number)
self.first_name = first_name
@@ -52,10 +61,3 @@ class Contact(TelegramObject):
self.vcard = vcard
self._id_attrs = (self.phone_number,)
@classmethod
def de_json(cls, data, bot):
if not data:
return None
return cls(**data)
+28 -18
View File
@@ -20,12 +20,21 @@
from telegram import PhotoSize, TelegramObject
from telegram.utils.types import JSONDict
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot, File
class Document(TelegramObject):
"""This object represents a general file (as opposed to photos, voice messages and audio files).
"""This object represents a general file
(as opposed to photos, voice messages and audio files).
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`file_unique_id` is equal.
Attributes:
file_id (:obj:`str`): Unique file identifier.
file_id (:obj:`str`): File identifier.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
@@ -38,8 +47,8 @@ class Document(TelegramObject):
Args:
file_id (:obj:`str`): Identifier for this file, which can be used to download
or reuse the file.
file_unique_id (:obj:`str`): Unique and the same over time and
for different bots file identifier.
file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be
the same over time and for different bots. Can't be used to download or reuse the file.
thumb (:class:`telegram.PhotoSize`, optional): Document thumbnail as defined by sender.
file_name (:obj:`str`, optional): Original filename as defined by sender.
mime_type (:obj:`str`, optional): MIME type of the file as defined by sender.
@@ -51,14 +60,14 @@ class Document(TelegramObject):
_id_keys = ('file_id',)
def __init__(self,
file_id,
file_unique_id,
thumb=None,
file_name=None,
mime_type=None,
file_size=None,
bot=None,
**kwargs):
file_id: str,
file_unique_id: str,
thumb: PhotoSize = None,
file_name: str = None,
mime_type: str = None,
file_size: int = None,
bot: 'Bot' = None,
**kwargs: Any):
# Required
self.file_id = str(file_id)
self.file_unique_id = str(file_unique_id)
@@ -72,24 +81,25 @@ class Document(TelegramObject):
self._id_attrs = (self.file_unique_id,)
@classmethod
def de_json(cls, data, bot):
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Document']:
data = cls.parse_data(data)
if not data:
return None
data = super(Document, cls).de_json(data, bot)
data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
return cls(bot=bot, **data)
def get_file(self, timeout=None, **kwargs):
def get_file(self, timeout: int = None, api_kwargs: JSONDict = None) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
Args:
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API.
Returns:
:class:`telegram.File`
@@ -98,4 +108,4 @@ class Document(TelegramObject):
:class:`telegram.TelegramError`
"""
return self.bot.get_file(self.file_id, timeout=timeout, **kwargs)
return self.bot.get_file(self.file_id, timeout=timeout, api_kwargs=api_kwargs)
+28 -24
View File
@@ -21,23 +21,30 @@ from base64 import b64decode
from os.path import basename
import os
from future.backports.urllib import parse as urllib_parse
import urllib.parse as urllib_parse
from telegram import TelegramObject
from telegram.passport.credentials import decrypt
from typing import Any, Optional, IO, Union, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot, FileCredentials
class File(TelegramObject):
"""
This object represents a file ready to be downloaded. The file can be downloaded with
:attr:`download`. It is guaranteed that the link will be valid for at least 1 hour. When the
link expires, a new one can be requested by calling getFile.
link expires, a new one can be requested by calling :meth:`telegram.Bot.get_file`.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`file_unique_id` is equal.
Note:
Maximum file size to download is 20 MB
Maximum file size to download is 20 MB.
Attributes:
file_id (:obj:`str`): Unique identifier for this file.
file_id (:obj:`str`): Identifier for this file.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
@@ -47,8 +54,9 @@ class File(TelegramObject):
Args:
file_id (:obj:`str`): Identifier for this file, which can be used to download
or reuse the file.
file_unique_id (:obj:`str`): Unique and the same over time and
for different bots file identifier.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
file_size (:obj:`int`, optional): Optional. File size, if known.
file_path (:obj:`str`, optional): File path. Use :attr:`download` to get the file.
bot (:obj:`telegram.Bot`, optional): Bot to use with shortcut method.
@@ -61,12 +69,12 @@ class File(TelegramObject):
"""
def __init__(self,
file_id,
file_unique_id,
bot=None,
file_size=None,
file_path=None,
**kwargs):
file_id: str,
file_unique_id: str,
bot: 'Bot' = None,
file_size: int = None,
file_path: str = None,
**kwargs: Any):
# Required
self.file_id = str(file_id)
self.file_unique_id = str(file_unique_id)
@@ -74,18 +82,14 @@ class File(TelegramObject):
self.file_size = file_size
self.file_path = file_path
self.bot = bot
self._credentials = None
self._credentials: Optional['FileCredentials'] = None
self._id_attrs = (self.file_unique_id,)
@classmethod
def de_json(cls, data, bot):
if not data:
return None
return cls(bot=bot, **data)
def download(self, custom_path=None, out=None, timeout=None):
def download(self,
custom_path: str = None,
out: IO = None,
timeout: int = None) -> Union[str, IO]:
"""
Download this file. By default, the file is saved in the current working directory with its
original filename as reported by Telegram. If the file has no filename, it the file ID will
@@ -143,13 +147,13 @@ class File(TelegramObject):
fobj.write(buf)
return filename
def _get_encoded_url(self):
def _get_encoded_url(self) -> str:
"""Convert any UTF-8 char in :obj:`File.file_path` into a url encoded ASCII string."""
sres = urllib_parse.urlsplit(self.file_path)
return urllib_parse.urlunsplit(urllib_parse.SplitResult(
sres.scheme, sres.netloc, urllib_parse.quote(sres.path), sres.query, sres.fragment))
def download_as_bytearray(self, buf=None):
def download_as_bytearray(self, buf: bytearray = None) -> bytes:
"""Download this file and return it as a bytearray.
Args:
@@ -166,5 +170,5 @@ class File(TelegramObject):
buf.extend(self.bot.request.retrieve(self._get_encoded_url()))
return buf
def set_credentials(self, credentials):
def set_credentials(self, credentials: 'FileCredentials') -> None:
self._credentials = credentials
+14 -15
View File
@@ -26,16 +26,18 @@ from uuid import uuid4
from telegram import TelegramError
from typing import IO, Tuple, Optional
DEFAULT_MIME_TYPE = 'application/octet-stream'
class InputFile(object):
class InputFile:
"""This object represents a Telegram InputFile.
Attributes:
input_file_content (:obj:`bytes`): The binaray content of the file to send.
filename (:obj:`str`): Optional, Filename for the file to be sent.
attach (:obj:`str`): Optional, attach id for sending multiple files.
input_file_content (:obj:`bytes`): The binary content of the file to send.
filename (:obj:`str`): Optional. Filename for the file to be sent.
attach (:obj:`str`): Optional. Attach id for sending multiple files.
Args:
obj (:obj:`File handler`): An open file descriptor.
@@ -48,18 +50,14 @@ class InputFile(object):
"""
def __init__(self, obj, filename=None, attach=None):
def __init__(self, obj: IO, filename: str = None, attach: bool = None):
self.filename = None
self.input_file_content = obj.read()
self.attach = 'attached' + uuid4().hex if attach else None
if filename:
self.filename = filename
elif (hasattr(obj, 'name')
and not isinstance(obj.name, int) # py3
and obj.name != '<fdopen>'): # py2
# on py2.7, pylint fails to understand this properly
# pylint: disable=E1101
elif (hasattr(obj, 'name') and not isinstance(obj.name, int)):
self.filename = os.path.basename(obj.name)
try:
@@ -74,15 +72,15 @@ class InputFile(object):
self.filename = self.mimetype.replace('/', '.')
@property
def field_tuple(self):
def field_tuple(self) -> Tuple[str, bytes, str]:
return self.filename, self.input_file_content, self.mimetype
@staticmethod
def is_image(stream):
def is_image(stream: bytes) -> str:
"""Check if the content file is an image by analyzing its headers.
Args:
stream (:obj:`str`): A str representing the content of a file.
stream (:obj:`bytes`): A byte stream representing the content of a file.
Returns:
:obj:`str`: The str mime-type of an image.
@@ -95,9 +93,10 @@ class InputFile(object):
raise TelegramError('Could not parse file content')
@staticmethod
def is_file(obj):
def is_file(obj: object) -> bool:
return hasattr(obj, 'read')
def to_dict(self):
def to_dict(self) -> Optional[str]:
if self.attach:
return 'attach://' + self.attach
return None
+142 -140
View File
@@ -19,7 +19,11 @@
"""Base class for Telegram InputMedia Objects."""
from telegram import TelegramObject, InputFile, PhotoSize, Animation, Video, Audio, Document
from telegram.utils.helpers import DEFAULT_NONE
from telegram.utils.helpers import DEFAULT_NONE, DefaultValue
from typing import Union, IO, cast
from telegram.utils.types import FileLike
class InputMedia(TelegramObject):
@@ -38,33 +42,25 @@ class InputMediaAnimation(InputMedia):
Attributes:
type (:obj:`str`): ``animation``.
media (:obj:`str` | `filelike object` | :class:`telegram.Animation`): Animation to
send. Pass a file_id as String to send an animation that exists on the Telegram
servers (recommended), pass an HTTP URL as a String for Telegram to get an
animation from the Internet, or upload a new animation using multipart/form-data.
Lastly you can pass an existing :class:`telegram.Animation` object to send.
thumb (`filelike object`): Optional. Thumbnail of the
file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
A thumbnail's width and height should not exceed 320. Ignored if the file is not
is passed as a string or file_id.
caption (:obj:`str`): Optional. Caption of the animation to be sent, 0-1024 characters
after entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
media (:obj:`str` | :class:`telegram.InputFile`): Animation to send.
caption (:obj:`str`): Optional. Caption of the document to be sent.
parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting.
thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send.
width (:obj:`int`): Optional. Animation width.
height (:obj:`int`): Optional. Animation height.
duration (:obj:`int`): Optional. Animation duration.
Args:
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
Lastly you can pass an existing :class:`telegram.Animation` object to send.
thumb (`filelike object`, optional): Thumbnail of the
file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
A thumbnail's width and height should not exceed 320. Ignored if the file is not
is passed as a string or file_id.
media (:obj:`str` | `filelike object` | :class:`telegram.Animation`): File to send. Pass a
file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
URL for Telegram to get a file from the Internet. Lastly you can pass an existing
:class:`telegram.Animation` object to send.
thumb (`filelike object`, optional): Thumbnail of the file sent; can be ignored if
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than 200 kB in size. A thumbnail's width and height should
not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.
caption (:obj:`str`, optional): Caption of the animation to be sent, 0-1024 characters
after entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
@@ -81,29 +77,32 @@ class InputMediaAnimation(InputMedia):
"""
def __init__(self,
media,
thumb=None,
caption=None,
parse_mode=DEFAULT_NONE,
width=None,
height=None,
duration=None):
media: Union[str, FileLike, Animation],
thumb: FileLike = None,
caption: str = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE,
width: int = None,
height: int = None,
duration: int = None):
self.type = 'animation'
if isinstance(media, Animation):
self.media = media.file_id
self.media: Union[str, InputFile] = media.file_id
self.width = media.width
self.height = media.height
self.duration = media.duration
elif InputFile.is_file(media):
media = cast(IO, media)
self.media = InputFile(media, attach=True)
else:
self.media = media
self.media = media # type: ignore[assignment]
if thumb:
self.thumb = thumb
if InputFile.is_file(self.thumb):
self.thumb = InputFile(self.thumb, attach=True)
if InputFile.is_file(thumb):
thumb = cast(IO, thumb)
self.thumb = InputFile(thumb, attach=True)
else:
self.thumb = thumb # type: ignore[assignment]
if caption:
self.caption = caption
@@ -121,21 +120,15 @@ class InputMediaPhoto(InputMedia):
Attributes:
type (:obj:`str`): ``photo``.
media (:obj:`str` | `filelike object` | :class:`telegram.PhotoSize`): Photo to send.
Pass a file_id as String to send a photo that exists on the Telegram servers
(recommended), pass an HTTP URL as a String for Telegram to get a photo from the
Internet, or upload a new photo using multipart/form-data. Lastly you can pass
an existing :class:`telegram.PhotoSize` object to send.
caption (:obj:`str`): Optional. Caption of the photo to be sent, 0-1024 characters after
entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
media (:obj:`str` | :class:`telegram.InputFile`): Photo to send.
caption (:obj:`str`): Optional. Caption of the document to be sent.
parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting.
Args:
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the
Telegram servers (recommended), pass an HTTP URL for Telegram to get a file from the
Internet. Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
media (:obj:`str` | `filelike object` | :class:`telegram.PhotoSize`): File to send. Pass a
file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
URL for Telegram to get a file from the Internet. Lastly you can pass an existing
:class:`telegram.PhotoSize` object to send.
caption (:obj:`str`, optional ): Caption of the photo to be sent, 0-1024 characters after
entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
@@ -143,15 +136,19 @@ class InputMediaPhoto(InputMedia):
in :class:`telegram.ParseMode` for the available modes.
"""
def __init__(self, media, caption=None, parse_mode=DEFAULT_NONE):
def __init__(self,
media: Union[str, FileLike, PhotoSize],
caption: str = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE):
self.type = 'photo'
if isinstance(media, PhotoSize):
self.media = media.file_id
self.media: Union[str, InputFile] = media.file_id
elif InputFile.is_file(media):
media = cast(IO, media)
self.media = InputFile(media, attach=True)
else:
self.media = media
self.media = media # type: ignore[assignment]
if caption:
self.caption = caption
@@ -163,30 +160,21 @@ class InputMediaVideo(InputMedia):
Attributes:
type (:obj:`str`): ``video``.
media (:obj:`str` | `filelike object` | :class:`telegram.Video`): Video file to send.
Pass a file_id as String to send an video file that exists on the Telegram servers
(recommended), pass an HTTP URL as a String for Telegram to get an video file from
the Internet, or upload a new one using multipart/form-data. Lastly you can pass
an existing :class:`telegram.Video` object to send.
caption (:obj:`str`): Optional. Caption of the video to be sent, 0-1024 characters after
entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
media (:obj:`str` | :class:`telegram.InputFile`): Video file to send.
caption (:obj:`str`): Optional. Caption of the document to be sent.
parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting.
width (:obj:`int`): Optional. Video width.
height (:obj:`int`): Optional. Video height.
duration (:obj:`int`): Optional. Video duration.
supports_streaming (:obj:`bool`): Optional. Pass True, if the uploaded video is suitable
for streaming.
thumb (`filelike object`): Optional. Thumbnail of the
file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
A thumbnail's width and height should not exceed 320. Ignored if the file is not
is passed as a string or file_id.
supports_streaming (:obj:`bool`): Optional. Pass :obj:`True`, if the uploaded video is
suitable for streaming.
thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send.
Args:
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
Lastly you can pass an existing :class:`telegram.Video` object to send.
media (:obj:`str` | `filelike object` | :class:`telegram.Video`): File to send. Pass a
file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
URL for Telegram to get a file from the Internet. Lastly you can pass an existing
:class:`telegram.Video` object to send.
caption (:obj:`str`, optional): Caption of the video to be sent, 0-1024 characters after
entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
@@ -195,37 +183,51 @@ class InputMediaVideo(InputMedia):
width (:obj:`int`, optional): Video width.
height (:obj:`int`, optional): Video height.
duration (:obj:`int`, optional): Video duration.
supports_streaming (:obj:`bool`, optional): Pass True, if the uploaded video is suitable
for streaming.
thumb (`filelike object`, optional): Thumbnail of the
file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
A thumbnail's width and height should not exceed 320. Ignored if the file is not
is passed as a string or file_id.
supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is
suitable for streaming.
thumb (`filelike object`, optional): Thumbnail of the file sent; can be ignored if
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than 200 kB in size. A thumbnail's width and height should
not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.
Note:
When using a :class:`telegram.Video` for the :attr:`media` attribute. It will take the
width, height and duration from that video, unless otherwise specified with the optional
arguments.
* When using a :class:`telegram.Video` for the :attr:`media` attribute. It will take the
width, height and duration from that video, unless otherwise specified with the optional
arguments.
* ``thumb`` will be ignored for small video files, for which Telegram can easily
generate thumb nails. However, this behaviour is undocumented and might be changed
by Telegram.
"""
def __init__(self, media, caption=None, width=None, height=None, duration=None,
supports_streaming=None, parse_mode=DEFAULT_NONE, thumb=None):
def __init__(self,
media: Union[str, FileLike, Video],
caption: str = None,
width: int = None,
height: int = None,
duration: int = None,
supports_streaming: bool = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE,
thumb: FileLike = None):
self.type = 'video'
if isinstance(media, Video):
self.media = media.file_id
self.media: Union[str, InputFile] = media.file_id
self.width = media.width
self.height = media.height
self.duration = media.duration
elif InputFile.is_file(media):
media = cast(IO, media)
self.media = InputFile(media, attach=True)
else:
self.media = media
self.media = media # type: ignore[assignment]
if thumb:
self.thumb = thumb
if InputFile.is_file(self.thumb):
self.thumb = InputFile(self.thumb, attach=True)
if InputFile.is_file(thumb):
thumb = cast(IO, thumb)
self.thumb = InputFile(thumb, attach=True)
else:
self.thumb = thumb # type: ignore[assignment]
if caption:
self.caption = caption
@@ -245,29 +247,20 @@ class InputMediaAudio(InputMedia):
Attributes:
type (:obj:`str`): ``audio``.
media (:obj:`str` | `filelike object` | :class:`telegram.Audio`): Audio file to send.
Pass a file_id as String to send an audio file that exists on the Telegram servers
(recommended), pass an HTTP URL as a String for Telegram to get an audio file from
the Internet, or upload a new one using multipart/form-data. Lastly you can pass
an existing :class:`telegram.Audio` object to send.
caption (:obj:`str`): Optional. Caption of the audio to be sent, 0-1024 characters after
entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
media (:obj:`str` | :class:`telegram.InputFile`): Audio file to send.
caption (:obj:`str`): Optional. Caption of the document to be sent.
parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting.
duration (:obj:`int`): Duration of the audio in seconds.
performer (:obj:`str`): Optional. Performer of the audio as defined by sender or by audio
tags.
title (:obj:`str`): Optional. Title of the audio as defined by sender or by audio tags.
thumb (`filelike object`): Optional. Thumbnail of the
file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
A thumbnail's width and height should not exceed 320. Ignored if the file is not
is passed as a string or file_id.
thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send.
Args:
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
Lastly you can pass an existing :class:`telegram.Document` object to send.
media (:obj:`str` | `filelike object` | :class:`telegram.Audio`): File to send. Pass a
file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
URL for Telegram to get a file from the Internet. Lastly you can pass an existing
:class:`telegram.Audio` object to send.
caption (:obj:`str`, optional): Caption of the audio to be sent, 0-1024 characters after
entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
@@ -277,10 +270,11 @@ class InputMediaAudio(InputMedia):
performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio
tags.
title (:obj:`str`, optional): Title of the audio as defined by sender or by audio tags.
thumb (`filelike object`, optional): Thumbnail of the
file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
A thumbnail's width and height should not exceed 320. Ignored if the file is not
is passed as a string or file_id.
thumb (`filelike object`, optional): Thumbnail of the file sent; can be ignored if
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than 200 kB in size. A thumbnail's width and height should
not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.
Note:
When using a :class:`telegram.Audio` for the :attr:`media` attribute. It will take the
@@ -288,24 +282,33 @@ class InputMediaAudio(InputMedia):
optional arguments.
"""
def __init__(self, media, thumb=None, caption=None, parse_mode=DEFAULT_NONE,
duration=None, performer=None, title=None):
def __init__(self,
media: Union[str, FileLike, Audio],
thumb: FileLike = None,
caption: str = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE,
duration: int = None,
performer: str = None,
title: str = None):
self.type = 'audio'
if isinstance(media, Audio):
self.media = media.file_id
self.media: Union[str, InputFile] = media.file_id
self.duration = media.duration
self.performer = media.performer
self.title = media.title
elif InputFile.is_file(media):
media = cast(IO, media)
self.media = InputFile(media, attach=True)
else:
self.media = media
self.media = media # type: ignore[assignment]
if thumb:
self.thumb = thumb
if InputFile.is_file(self.thumb):
self.thumb = InputFile(self.thumb, attach=True)
if InputFile.is_file(thumb):
thumb = cast(IO, thumb)
self.thumb = InputFile(thumb, attach=True)
else:
self.thumb = thumb # type: ignore[assignment]
if caption:
self.caption = caption
@@ -323,50 +326,49 @@ class InputMediaDocument(InputMedia):
Attributes:
type (:obj:`str`): ``document``.
media (:obj:`str` | `filelike object` | :class:`telegram.Document`): File to send.
Pass a file_id as String to send a file that exists on the Telegram servers
(recommended), pass an HTTP URL as a String for Telegram to get a file from the
Internet, or upload a new one using multipart/form-data. Lastly you can pass
an existing :class:`telegram.Document` object to send.
caption (:obj:`str`): Optional. Caption of the document to be sent, 0-1024 characters after
entities parsing.
parse_mode (:obj:`str`): Optional. Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
thumb (`filelike object`): Optional. Thumbnail of the
file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
A thumbnail's width and height should not exceed 320. Ignored if the file is not
is passed as a string or file_id.
media (:obj:`str` | :class:`telegram.InputFile`): File to send.
caption (:obj:`str`): Optional. Caption of the document to be sent.
parse_mode (:obj:`str`): Optional. The parse mode to use for text formatting.
thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send.
Args:
media (:obj:`str`): File to send. Pass a file_id to send a file that exists on the Telegram
servers (recommended), pass an HTTP URL for Telegram to get a file from the Internet.
Lastly you can pass an existing :class:`telegram.Document` object to send.
media (:obj:`str` | `filelike object` | :class:`telegram.Document`): File to send. Pass a
file_id to send a file that exists on the Telegram servers (recommended), pass an HTTP
URL for Telegram to get a file from the Internet. Lastly you can pass an existing
:class:`telegram.Document` object to send.
caption (:obj:`str`, optional): Caption of the document to be sent, 0-1024 characters after
entities parsing.
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
in :class:`telegram.ParseMode` for the available modes.
thumb (`filelike object`, optional): Thumbnail of the
file sent. The thumbnail should be in JPEG format and less than 200 kB in size.
A thumbnail's width and height should not exceed 320. Ignored if the file is not
is passed as a string or file_id.
thumb (`filelike object`, optional): Thumbnail of the file sent; can be ignored if
thumbnail generation for the file is supported server-side. The thumbnail should be
in JPEG format and less than 200 kB in size. A thumbnail's width and height should
not exceed 320. Ignored if the file is not uploaded using multipart/form-data.
Thumbnails can't be reused and can be only uploaded as a new file.
"""
def __init__(self, media, thumb=None, caption=None, parse_mode=DEFAULT_NONE):
def __init__(self,
media: Union[str, FileLike, Document],
thumb: FileLike = None,
caption: str = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE):
self.type = 'document'
if isinstance(media, Document):
self.media = media.file_id
self.media: Union[str, InputFile] = media.file_id
elif InputFile.is_file(media):
media = cast(IO, media)
self.media = InputFile(media, attach=True)
else:
self.media = media
self.media = media # type: ignore[assignment]
if thumb:
self.thumb = thumb
if InputFile.is_file(self.thumb):
self.thumb = InputFile(self.thumb, attach=True)
if InputFile.is_file(thumb):
thumb = cast(IO, thumb)
self.thumb = InputFile(thumb, attach=True)
else:
self.thumb = thumb # type: ignore[assignment]
if caption:
self.caption = caption
+5 -8
View File
@@ -19,11 +19,15 @@
"""This module contains an object that represents a Telegram Location."""
from telegram import TelegramObject
from typing import Any
class Location(TelegramObject):
"""This object represents a point on the map.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`longitute` and :attr:`latitude` are equal.
Attributes:
longitude (:obj:`float`): Longitude as defined by sender.
latitude (:obj:`float`): Latitude as defined by sender.
@@ -35,16 +39,9 @@ class Location(TelegramObject):
"""
def __init__(self, longitude, latitude, **kwargs):
def __init__(self, longitude: float, latitude: float, **kwargs: Any):
# Required
self.longitude = float(longitude)
self.latitude = float(latitude)
self._id_attrs = (self.longitude, self.latitude)
@classmethod
def de_json(cls, data, bot):
if not data:
return None
return cls(**data)
+22 -31
View File
@@ -19,13 +19,20 @@
"""This module contains an object that represents a Telegram PhotoSize."""
from telegram import TelegramObject
from telegram.utils.types import JSONDict
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot, File
class PhotoSize(TelegramObject):
"""This object represents one size of a photo or a file/sticker thumbnail.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`file_unique_id` is equal.
Attributes:
file_id (:obj:`str`): Unique identifier for this file.
file_id (:obj:`str`): Identifier for this file.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
@@ -37,8 +44,9 @@ class PhotoSize(TelegramObject):
Args:
file_id (:obj:`str`): Identifier for this file, which can be used to download
or reuse the file.
file_unique_id (:obj:`str`): Unique and the same over time and
for different bots file identifier.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
width (:obj:`int`): Photo width.
height (:obj:`int`): Photo height.
file_size (:obj:`int`, optional): File size.
@@ -48,13 +56,13 @@ class PhotoSize(TelegramObject):
"""
def __init__(self,
file_id,
file_unique_id,
width,
height,
file_size=None,
bot=None,
**kwargs):
file_id: str,
file_unique_id: str,
width: int,
height: int,
file_size: int = None,
bot: 'Bot' = None,
**kwargs: Any):
# Required
self.file_id = str(file_id)
self.file_unique_id = str(file_unique_id)
@@ -66,32 +74,15 @@ class PhotoSize(TelegramObject):
self._id_attrs = (self.file_unique_id,)
@classmethod
def de_json(cls, data, bot):
if not data:
return None
return cls(bot=bot, **data)
@classmethod
def de_list(cls, data, bot):
if not data:
return []
photos = list()
for photo in data:
photos.append(cls.de_json(photo, bot))
return photos
def get_file(self, timeout=None, **kwargs):
def get_file(self, timeout: int = None, api_kwargs: JSONDict = None) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
Args:
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API.
Returns:
:class:`telegram.File`
@@ -100,4 +91,4 @@ class PhotoSize(TelegramObject):
:class:`telegram.TelegramError`
"""
return self.bot.get_file(self.file_id, timeout=timeout, **kwargs)
return self.bot.get_file(self.file_id, timeout=timeout, api_kwargs=api_kwargs)
+81 -52
View File
@@ -19,19 +19,26 @@
"""This module contains objects that represents stickers."""
from telegram import PhotoSize, TelegramObject
from telegram.utils.types import JSONDict
from typing import Any, Optional, List, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot, File
class Sticker(TelegramObject):
"""This object represents a sticker.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`file_unique_id` is equal.
Attributes:
file_id (:obj:`str`): Unique identifier for this file.
file_id (:obj:`str`): Identifier for this file.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
width (:obj:`int`): Sticker width.
height (:obj:`int`): Sticker height.
is_animated (:obj:`bool`): True, if the sticker is animated.
is_animated (:obj:`bool`): :obj:`True`, if the sticker is animated.
thumb (:class:`telegram.PhotoSize`): Optional. Sticker thumbnail in the .webp or .jpg
format.
emoji (:obj:`str`): Optional. Emoji associated with the sticker.
@@ -44,12 +51,13 @@ class Sticker(TelegramObject):
Args:
file_id (:obj:`str`): Identifier for this file, which can be used to download
or reuse the file.
file_unique_id (:obj:`str`): Unique and the same over time and
for different bots file identifier.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
width (:obj:`int`): Sticker width.
height (:obj:`int`): Sticker height.
is_animated (:obj:`bool`): True, if the sticker is animated.
thumb (:class:`telegram.PhotoSize`, optional): Sticker thumbnail in the .webp or .jpg
is_animated (:obj:`bool`): :obj:`True`, if the sticker is animated.
thumb (:class:`telegram.PhotoSize`, optional): Sticker thumbnail in the .WEBP or .JPG
format.
emoji (:obj:`str`, optional): Emoji associated with the sticker
set_name (:obj:`str`, optional): Name of the sticker set to which the sticker
@@ -57,24 +65,25 @@ class Sticker(TelegramObject):
mask_position (:class:`telegram.MaskPosition`, optional): For mask stickers, the
position where the mask should be placed.
file_size (:obj:`int`, optional): File size.
**kwargs (obj:`dict`): Arbitrary keyword arguments.7
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
**kwargs (obj:`dict`): Arbitrary keyword arguments.
"""
def __init__(self,
file_id,
file_unique_id,
width,
height,
is_animated,
thumb=None,
emoji=None,
file_size=None,
set_name=None,
mask_position=None,
bot=None,
**kwargs):
file_id: str,
file_unique_id: str,
width: int,
height: int,
is_animated: bool,
thumb: PhotoSize = None,
emoji: str = None,
file_size: int = None,
set_name: str = None,
mask_position: 'MaskPosition' = None,
bot: 'Bot' = None,
**kwargs: Any):
# Required
self.file_id = str(file_id)
self.file_unique_id = str(file_unique_id)
@@ -92,32 +101,26 @@ class Sticker(TelegramObject):
self._id_attrs = (self.file_unique_id,)
@classmethod
def de_json(cls, data, bot):
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Sticker']:
data = cls.parse_data(data)
if not data:
return None
data = super(Sticker, cls).de_json(data, bot)
data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
data['mask_position'] = MaskPosition.de_json(data.get('mask_position'), bot)
return cls(bot=bot, **data)
@classmethod
def de_list(cls, data, bot):
if not data:
return list()
return [cls.de_json(d, bot) for d in data]
def get_file(self, timeout=None, **kwargs):
def get_file(self, timeout: str = None, api_kwargs: JSONDict = None) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
Args:
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API.
Returns:
:class:`telegram.File`
@@ -126,50 +129,66 @@ class Sticker(TelegramObject):
:class:`telegram.TelegramError`
"""
return self.bot.get_file(self.file_id, timeout=timeout, **kwargs)
return self.bot.get_file(self.file_id, timeout=timeout, api_kwargs=api_kwargs)
class StickerSet(TelegramObject):
"""This object represents a sticker set.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`name` is equal.
Attributes:
name (:obj:`str`): Sticker set name.
title (:obj:`str`): Sticker set title.
is_animated (:obj:`bool`): True, if the sticker set contains animated stickers.
contains_masks (:obj:`bool`): True, if the sticker set contains masks.
is_animated (:obj:`bool`): :obj:`True`, if the sticker set contains animated stickers.
contains_masks (:obj:`bool`): :obj:`True`, if the sticker set contains masks.
stickers (List[:class:`telegram.Sticker`]): List of all set stickers.
thumb (:class:`telegram.PhotoSize`): Optional. Sticker set thumbnail in the .WEBP or .TGS
format.
Args:
name (:obj:`str`): Sticker set name.
title (:obj:`str`): Sticker set title.
is_animated (:obj:`bool`): True, if the sticker set contains animated stickers.
contains_masks (:obj:`bool`): True, if the sticker set contains masks.
is_animated (:obj:`bool`): :obj:`True`, if the sticker set contains animated stickers.
contains_masks (:obj:`bool`): :obj:`True`, if the sticker set contains masks.
stickers (List[:class:`telegram.Sticker`]): List of all set stickers.
thumb (:class:`telegram.PhotoSize`, optional): Sticker set thumbnail in the .WEBP or .TGS
format.
"""
def __init__(self, name, title, is_animated, contains_masks, stickers, bot=None, **kwargs):
def __init__(self,
name: str,
title: str,
is_animated: bool,
contains_masks: bool,
stickers: List[Sticker],
bot: 'Bot' = None,
thumb: PhotoSize = None,
**kwargs: Any):
self.name = name
self.title = title
self.is_animated = is_animated
self.contains_masks = contains_masks
self.stickers = stickers
# Optionals
self.thumb = thumb
self._id_attrs = (self.name,)
@staticmethod
def de_json(data, bot):
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['StickerSet']:
if not data:
return None
data = super(StickerSet, StickerSet).de_json(data, bot)
data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
data['stickers'] = Sticker.de_list(data.get('stickers'), bot)
return StickerSet(bot=bot, **data)
return cls(bot=bot, **data)
def to_dict(self):
data = super(StickerSet, self).to_dict()
def to_dict(self) -> JSONDict:
data = super().to_dict()
data['stickers'] = [s.to_dict() for s in data.get('stickers')]
@@ -179,20 +198,26 @@ class StickerSet(TelegramObject):
class MaskPosition(TelegramObject):
"""This object describes the position on faces where a mask should be placed by default.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`point`, :attr:`x_shift`, :attr:`y_shift` and, :attr:`scale`
are equal.
Attributes:
point (:obj:`str`): The part of the face relative to which the mask should be placed.
One of ``'forehead'``, ``'eyes'``, ``'mouth'``, or ``'chin'``.
x_shift (:obj:`float`): Shift by X-axis measured in widths of the mask scaled to the face
size, from left to right.
y_shift (:obj:`float`): Shift by Y-axis measured in heights of the mask scaled to the face
size, from top to bottom.
scale (:obj:`float`): Mask scaling coefficient. For example, 2.0 means double size.
Notes:
Note:
:attr:`type` should be one of the following: `forehead`, `eyes`, `mouth` or `chin`. You can
use the classconstants for those.
use the class constants for those.
Args:
point (:obj:`str`): The part of the face relative to which the mask should be placed.
One of ``'forehead'``, ``'eyes'``, ``'mouth'``, or ``'chin'``.
x_shift (:obj:`float`): Shift by X-axis measured in widths of the mask scaled to the face
size, from left to right. For example, choosing -1.0 will place mask just to the left
of the default mask position.
@@ -202,23 +227,27 @@ class MaskPosition(TelegramObject):
scale (:obj:`float`): Mask scaling coefficient. For example, 2.0 means double size.
"""
FOREHEAD = 'forehead'
FOREHEAD: str = 'forehead'
""":obj:`str`: 'forehead'"""
EYES = 'eyes'
EYES: str = 'eyes'
""":obj:`str`: 'eyes'"""
MOUTH = 'mouth'
MOUTH: str = 'mouth'
""":obj:`str`: 'mouth'"""
CHIN = 'chin'
CHIN: str = 'chin'
""":obj:`str`: 'chin'"""
def __init__(self, point, x_shift, y_shift, scale, **kwargs):
def __init__(self, point: str, x_shift: float, y_shift: float, scale: float, **kwargs: Any):
self.point = point
self.x_shift = x_shift
self.y_shift = y_shift
self.scale = scale
self._id_attrs = (self.point, self.x_shift, self.y_shift, self.scale)
@classmethod
def de_json(cls, data, bot):
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['MaskPosition']:
data = cls.parse_data(data)
if data is None:
return None
+16 -4
View File
@@ -19,11 +19,18 @@
"""This module contains an object that represents a Telegram Venue."""
from telegram import TelegramObject, Location
from telegram.utils.types import JSONDict
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot
class Venue(TelegramObject):
"""This object represents a venue.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`location` and :attr:`title` are equal.
Attributes:
location (:class:`telegram.Location`): Venue location.
title (:obj:`str`): Name of the venue.
@@ -43,8 +50,13 @@ class Venue(TelegramObject):
"""
def __init__(self, location, title, address, foursquare_id=None, foursquare_type=None,
**kwargs):
def __init__(self,
location: Location,
title: str,
address: str,
foursquare_id: str = None,
foursquare_type: str = None,
**kwargs: Any):
# Required
self.location = location
self.title = title
@@ -56,8 +68,8 @@ class Venue(TelegramObject):
self._id_attrs = (self.location, self.title)
@classmethod
def de_json(cls, data, bot):
data = super(Venue, cls).de_json(data, bot)
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Venue']:
data = cls.parse_data(data)
if not data:
return None
+28 -19
View File
@@ -19,13 +19,20 @@
"""This module contains an object that represents a Telegram Video."""
from telegram import PhotoSize, TelegramObject
from telegram.utils.types import JSONDict
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot, File
class Video(TelegramObject):
"""This object represents a video file.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`file_unique_id` is equal.
Attributes:
file_id (:obj:`str`): Unique identifier for this file.
file_id (:obj:`str`): Identifier for this file.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
@@ -40,8 +47,9 @@ class Video(TelegramObject):
Args:
file_id (:obj:`str`): Identifier for this file, which can be used to download
or reuse the file.
file_unique_id (:obj:`str`): Unique and the same over time and
for different bots file identifier.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
width (:obj:`int`): Video width as defined by sender.
height (:obj:`int`): Video height as defined by sender.
duration (:obj:`int`): Duration of the video in seconds as defined by sender.
@@ -54,16 +62,16 @@ class Video(TelegramObject):
"""
def __init__(self,
file_id,
file_unique_id,
width,
height,
duration,
thumb=None,
mime_type=None,
file_size=None,
bot=None,
**kwargs):
file_id: str,
file_unique_id: str,
width: int,
height: int,
duration: int,
thumb: PhotoSize = None,
mime_type: str = None,
file_size: int = None,
bot: 'Bot' = None,
**kwargs: Any):
# Required
self.file_id = str(file_id)
self.file_unique_id = str(file_unique_id)
@@ -79,24 +87,25 @@ class Video(TelegramObject):
self._id_attrs = (self.file_unique_id,)
@classmethod
def de_json(cls, data, bot):
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Video']:
data = cls.parse_data(data)
if not data:
return None
data = super(Video, cls).de_json(data, bot)
data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
return cls(bot=bot, **data)
def get_file(self, timeout=None, **kwargs):
def get_file(self, timeout: int = None, api_kwargs: JSONDict = None) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
Args:
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API.
Returns:
:class:`telegram.File`
@@ -105,4 +114,4 @@ class Video(TelegramObject):
:class:`telegram.TelegramError`
"""
return self.bot.get_file(self.file_id, timeout=timeout, **kwargs)
return self.bot.get_file(self.file_id, timeout=timeout, api_kwargs=api_kwargs)
+28 -18
View File
@@ -19,13 +19,20 @@
"""This module contains an object that represents a Telegram VideoNote."""
from telegram import PhotoSize, TelegramObject
from telegram.utils.types import JSONDict
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot, File
class VideoNote(TelegramObject):
"""This object represents a video message (available in Telegram apps as of v.4.0).
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`file_unique_id` is equal.
Attributes:
file_id (:obj:`str`): Unique identifier for this file.
file_id (:obj:`str`): Identifier for this file.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
@@ -38,9 +45,11 @@ class VideoNote(TelegramObject):
Args:
file_id (:obj:`str`): Identifier for this file, which can be used to download
or reuse the file.
file_unique_id (:obj:`str`): Unique and the same over time and
for different bots file identifier.
length (:obj:`int`): Video width and height as defined by sender.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
length (:obj:`int`): Video width and height (diameter of the video message) as defined
by sender.
duration (:obj:`int`): Duration of the video in seconds as defined by sender.
thumb (:class:`telegram.PhotoSize`, optional): Video thumbnail.
file_size (:obj:`int`, optional): File size.
@@ -50,14 +59,14 @@ class VideoNote(TelegramObject):
"""
def __init__(self,
file_id,
file_unique_id,
length,
duration,
thumb=None,
file_size=None,
bot=None,
**kwargs):
file_id: str,
file_unique_id: str,
length: int,
duration: int,
thumb: PhotoSize = None,
file_size: int = None,
bot: 'Bot' = None,
**kwargs: Any):
# Required
self.file_id = str(file_id)
self.file_unique_id = str(file_unique_id)
@@ -71,24 +80,25 @@ class VideoNote(TelegramObject):
self._id_attrs = (self.file_unique_id,)
@classmethod
def de_json(cls, data, bot):
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['VideoNote']:
data = cls.parse_data(data)
if not data:
return None
data = super(VideoNote, cls).de_json(data, bot)
data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot)
return cls(bot=bot, **data)
def get_file(self, timeout=None, **kwargs):
def get_file(self, timeout: int = None, api_kwargs: JSONDict = None) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
Args:
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API.
Returns:
:class:`telegram.File`
@@ -97,4 +107,4 @@ class VideoNote(TelegramObject):
:class:`telegram.TelegramError`
"""
return self.bot.get_file(self.file_id, timeout=timeout, **kwargs)
return self.bot.get_file(self.file_id, timeout=timeout, api_kwargs=api_kwargs)
+22 -22
View File
@@ -19,13 +19,20 @@
"""This module contains an object that represents a Telegram Voice."""
from telegram import TelegramObject
from telegram.utils.types import JSONDict
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot, File
class Voice(TelegramObject):
"""This object represents a voice note.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`file_unique_id` is equal.
Attributes:
file_id (:obj:`str`): Unique identifier for this file.
file_id (:obj:`str`): Identifier for this file.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
@@ -37,8 +44,9 @@ class Voice(TelegramObject):
Args:
file_id (:obj:`str`): Identifier for this file, which can be used to download
or reuse the file.
file_unique_id (:obj:`str`): Unique and the same over time and
for different bots file identifier.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
duration (:obj:`int`, optional): Duration of the audio in seconds as defined by sender.
mime_type (:obj:`str`, optional): MIME type of the file as defined by sender.
file_size (:obj:`int`, optional): File size.
@@ -48,13 +56,13 @@ class Voice(TelegramObject):
"""
def __init__(self,
file_id,
file_unique_id,
duration,
mime_type=None,
file_size=None,
bot=None,
**kwargs):
file_id: str,
file_unique_id: str,
duration: int,
mime_type: str = None,
file_size: int = None,
bot: 'Bot' = None,
**kwargs: Any):
# Required
self.file_id = str(file_id)
self.file_unique_id = str(file_unique_id)
@@ -66,23 +74,15 @@ class Voice(TelegramObject):
self._id_attrs = (self.file_unique_id,)
@classmethod
def de_json(cls, data, bot):
if not data:
return None
data = super(Voice, cls).de_json(data, bot)
return cls(bot=bot, **data)
def get_file(self, timeout=None, **kwargs):
def get_file(self, timeout: int = None, api_kwargs: JSONDict = None) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
Args:
timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as
the read timeout from the server (instead of the one specified during creation of
the connection pool).
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
api_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to be passed to the
Telegram API.
Returns:
:class:`telegram.File`
@@ -91,4 +91,4 @@ class Voice(TelegramObject):
:class:`telegram.TelegramError`
"""
return self.bot.get_file(self.file_id, timeout=timeout, **kwargs)
return self.bot.get_file(self.file_id, timeout=timeout, api_kwargs=api_kwargs)
+11 -4
View File
@@ -19,6 +19,7 @@
"""This module contains an object that represents a Telegram ForceReply."""
from telegram import ReplyMarkup
from typing import Any
class ForceReply(ReplyMarkup):
@@ -28,24 +29,30 @@ class ForceReply(ReplyMarkup):
extremely useful if you want to create user-friendly step-by-step interfaces without having
to sacrifice privacy mode.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`selective` is equal.
Attributes:
force_reply (:obj:`True`): Shows reply interface to the user.
force_reply (:obj:`True`): Shows reply interface to the user, as if they manually selected
the bot's message and tapped 'Reply'.
selective (:obj:`bool`): Optional. Force reply from specific users only.
Args:
selective (:obj:`bool`, optional): Use this parameter if you want to force reply from
specific users only. Targets:
1) users that are @mentioned in the text of the Message object
2) if the bot's message is a reply (has reply_to_message_id), sender of the
1) Users that are @mentioned in the text of the Message object.
2) If the bot's message is a reply (has reply_to_message_id), sender of the
original message.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
def __init__(self, force_reply=True, selective=False, **kwargs):
def __init__(self, force_reply: bool = True, selective: bool = False, **kwargs: Any):
# Required
self.force_reply = bool(force_reply)
# Optionals
self.selective = bool(selective)
self._id_attrs = (self.selective,)
+44 -22
View File
@@ -21,12 +21,19 @@
import sys
from telegram import MessageEntity, TelegramObject, Animation, PhotoSize
from telegram.utils.types import JSONDict
from typing import List, Any, Dict, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot
class Game(TelegramObject):
"""
This object represents a game. Use BotFather to create and edit games, their short names will
act as unique identifiers.
This object represents a game. Use `BotFather <https://t.me/BotFather>`_ to create and edit
games, their short names will act as unique identifiers.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`title`, :attr:`description` and :attr:`photo` are equal.
Attributes:
title (:obj:`str`): Title of the game.
@@ -35,11 +42,12 @@ class Game(TelegramObject):
in chats.
text (:obj:`str`): Optional. Brief description of the game or high scores included in the
game message. Can be automatically edited to include current high scores for the game
when the bot calls set_game_score, or manually edited using edit_message_text.
when the bot calls :meth:`telegram.Bot.set_game_score`, or manually edited
using :meth:`telegram.Bot.edit_message_text`.
text_entities (List[:class:`telegram.MessageEntity`]): Optional. Special entities that
appear in text, such as usernames, URLs, bot commands, etc.
animation (:class:`telegram.Animation`): Optional. Animation that will be displayed in the
game message in chats. Upload via BotFather.
game message in chats. Upload via `BotFather <https://t.me/BotFather>`_.
Args:
title (:obj:`str`): Title of the game.
@@ -48,45 +56,50 @@ class Game(TelegramObject):
in chats.
text (:obj:`str`, optional): Brief description of the game or high scores included in the
game message. Can be automatically edited to include current high scores for the game
when the bot calls set_game_score, or manually edited using edit_message_text.
0-4096 characters. Also found as ``telegram.constants.MAX_MESSAGE_LENGTH``.
when the bot calls :meth:`telegram.Bot.set_game_score`, or manually edited
using :meth:`telegram.Bot.edit_message_text`.
1-4096 characters. Also found as ``telegram.constants.MAX_MESSAGE_LENGTH``.
text_entities (List[:class:`telegram.MessageEntity`], optional): Special entities that
appear in text, such as usernames, URLs, bot commands, etc.
animation (:class:`telegram.Animation`, optional): Animation that will be displayed in the
game message in chats. Upload via BotFather.
game message in chats. Upload via `BotFather <https://t.me/BotFather>`_.
"""
def __init__(self,
title,
description,
photo,
text=None,
text_entities=None,
animation=None,
**kwargs):
title: str,
description: str,
photo: List[PhotoSize],
text: str = None,
text_entities: List[MessageEntity] = None,
animation: Animation = None,
**kwargs: Any):
# Required
self.title = title
self.description = description
self.photo = photo
# Optionals
self.text = text
self.text_entities = text_entities or list()
self.animation = animation
self._id_attrs = (self.title, self.description, self.photo)
@classmethod
def de_json(cls, data, bot):
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['Game']:
data = cls.parse_data(data)
if not data:
return None
data = super(Game, cls).de_json(data, bot)
data['photo'] = PhotoSize.de_list(data.get('photo'), bot)
data['text_entities'] = MessageEntity.de_list(data.get('text_entities'), bot)
data['animation'] = Animation.de_json(data.get('animation'), bot)
return cls(**data)
def to_dict(self):
data = super(Game, self).to_dict()
def to_dict(self) -> JSONDict:
data = super().to_dict()
data['photo'] = [p.to_dict() for p in self.photo]
if self.text_entities:
@@ -94,7 +107,7 @@ class Game(TelegramObject):
return data
def parse_text_entity(self, entity):
def parse_text_entity(self, entity: MessageEntity) -> str:
"""Returns the text from a given :class:`telegram.MessageEntity`.
Note:
@@ -109,7 +122,13 @@ class Game(TelegramObject):
Returns:
:obj:`str`: The text of the given entity.
Raises:
RuntimeError: If this game has no text.
"""
if not self.text:
raise RuntimeError("This Game has no 'text'.")
# Is it a narrow build, if so we don't need to convert
if sys.maxunicode == 0xffff:
return self.text[entity.offset:entity.offset + entity.length]
@@ -119,7 +138,7 @@ class Game(TelegramObject):
return entity_text.decode('utf-16-le')
def parse_text_entities(self, types=None):
def parse_text_entities(self, types: List[str] = None) -> Dict[MessageEntity, str]:
"""
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
It contains entities from this message filtered by their ``type`` attribute as the key, and
@@ -145,5 +164,8 @@ class Game(TelegramObject):
return {
entity: self.parse_text_entity(entity)
for entity in self.text_entities if entity.type in types
for entity in (self.text_entities or []) if entity.type in types
}
def __hash__(self) -> int:
return hash((self.title, self.description, tuple(p for p in self.photo)))
+13 -4
View File
@@ -19,11 +19,18 @@
"""This module contains an object that represents a Telegram GameHighScore."""
from telegram import TelegramObject, User
from telegram.utils.types import JSONDict
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot
class GameHighScore(TelegramObject):
"""This object represents one row of the high scores table for a game.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`position`, :attr:`user` and :attr:`score` are equal.
Attributes:
position (:obj:`int`): Position in high score table for the game.
user (:class:`telegram.User`): User.
@@ -36,18 +43,20 @@ class GameHighScore(TelegramObject):
"""
def __init__(self, position, user, score):
def __init__(self, position: int, user: User, score: int):
self.position = position
self.user = user
self.score = score
self._id_attrs = (self.position, self.user, self.score)
@classmethod
def de_json(cls, data, bot):
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['GameHighScore']:
data = cls.parse_data(data)
if not data:
return None
data = super(GameHighScore, cls).de_json(data, bot)
data['user'] = User.de_json(data.get('user'), bot)
return cls(**data)

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