Compare commits

..

31 Commits

Author SHA1 Message Date
Hinrich Mahler 73b0e29a30 Bump version to v13.1 2020-11-29 17:01:03 +01:00
Bibo-Joshi d27d1ea4d5 Correct Some Type Hints (#2204)
* Correct some reply_markup hints

* Fix type hints of effective_message_type

* fixup
2020-11-29 16:32:38 +01:00
Bibo-Joshi ca04daf782 Doc Fixes & Extensions (#2201)
* Add note on dispatcherhandlerstop to conversationhandler

* Fine tune @run_async deprecation warning

* Refine docs of JobQueue.jobs/get_jobs_by_name
2020-11-29 16:25:47 +01:00
Bibo-Joshi ae9ce60b55 API 5.0 (#2181)
Co-authored-by: poolitzer <25934244+Poolitzer@users.noreply.github.com>
Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
2020-11-29 16:20:46 +01:00
Bibo-Joshi 1cd3a0a156 Handle Non-Binary File Input (#2202)
* Don't fail if stream is not bytes

* move logger

* Refactor InputFile.is_image

* Use f-strings

* some clean up
2020-11-24 20:31:34 +01:00
Bibo-Joshi 58b9882021 Use F-Strings Where Possible (#2222) 2020-11-23 22:09:29 +01:00
Bibo-Joshi df6d5f0840 Fix bugs in replace/insert_bot (#2218)
* Fix bugs in replace/insert_bot

* Some tweaks
2020-11-22 11:08:46 +01:00
Stɑrry Shivɑm 425716f966 Add Defaults.run_async (#2210)
* Add Defaults.run_async support

Signed-off-by: starry69 <starry369126@outlook.com>

* Address some requested changes.

Signed-off-by: starry69 <starry369126@outlook.com>

* Add tests for defaults.run_async

Signed-off-by: starry69 <starry369126@outlook.com>

* Fix tests logic & add default value support for dp.add_error_handler

Signed-off-by: starry69 <starry369126@outlook.com>

* Fix tests, with requested changes

Signed-off-by: starry69 <starry369126@outlook.com>

* Add tests for error_handler

Signed-off-by: starry69 <starry369126@outlook.com>

* try to fix pre-commit

Signed-off-by: starry69 <starry369126@outlook.com>

* Enhance tests & address suggested changes

Signed-off-by: starry69 <starry369126@outlook.com>

* Improve docs

Signed-off-by: starry69 <starry369126@outlook.com>
2020-11-17 21:31:01 +01:00
Bibo-Joshi 8d9bb26cca Improve Handling of Custom Objects in BasePersistence.insert/replace_bot (#2151)
* Handle unpickable objects

* Improve coverage

* Add user warning

* make comparison to REPLACED_BOT safe

* make pre-commit happy

* Shorten warning
2020-11-14 03:08:18 +01:00
Bibo-Joshi d1438a9b23 Add XOR Filters and make Filters.name a Property (#2179)
* XOR Filters and make Filters.name a property

* add XORFilter to __all__

* Change example
2020-11-07 08:44:45 +01:00
Evgeny Denisov 27b03edc59 Expand Type Hints to Tuples (#2167)
Co-authored-by: Pranjalya <pranjalyawarrior@gmail.com>
2020-11-07 08:26:32 +01:00
Evgeny Denisov ac449deb5d Add Filters.document.file_extension (#2169)
Co-authored-by: Matheus Lemos <matheuslemosf@protonmail.com>
Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>
2020-11-06 18:41:54 +01:00
Bibo-Joshi 3b9187ed5a Rename kwargs to _kwargs where possible (#2182) 2020-11-05 18:12:01 +01:00
Bibo-Joshi 9831458e22 Improve and Expand CallbackQuery Shortcuts (#2172)
* CallbackQuery.delete_message()

* Improve internals of CQ shortcuts
2020-11-05 17:11:35 +01:00
Marco Fincato a0cd6e8fef Add Filters.caption_regex (#2163)
* Check caption in Filters.regex

Added regex matching for message caption in Filters.regex.

* Moved caption check to Filters.caption_regex

* Added caption_regex tests

The same as for regex, with only the content of the message changed, that is now inside caption.

* Fixed pre-commit tests

Lines too long and an additional blank line

* Moved line break to comply

* Reformatted code with black

* Added docstrings
2020-11-04 20:54:24 +01:00
Bibo-Joshi 8e7c0d6976 Comply with PEP561 (#2168)
* Comply with PEP561

* Try harder

* third time's (hopefully) a charme
2020-11-01 19:33:01 +01:00
Bibo-Joshi 92b9370c23 Improve Code Quality (#2131)
* Make pre-commit more strict

* Get pylint to read setup.cfg

* Make pylint & mypy happy aka ignore all the things

* use LogRecord.getMessage() in tests

* Make noam happy

* Update both pylint & mypy while we're at it

* Bring reqs-dev and makefile up to speed

* try making pre-commit happy

* fix jobqueue tests on the fly
2020-10-31 16:33:34 +01:00
GauthamramRavichandran 237e73bfb4 Add Filters.chat_type (#2128)
* add supergroup filter

* add chat_type filter

* re-implemented ChatType

* Add deprecations, improve tests

* Fix some docs

* Fix black

Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2020-10-29 19:42:08 +01:00
Bibo-Joshi ff3fd34f08 Update Credits (#2161) 2020-10-27 17:43:23 +01:00
Bibo-Joshi 83791d34e7 Fix Regex in Configuration of Black Formatter (#2159) 2020-10-23 14:46:56 +02:00
Bibo-Joshi 02cd7b642f Fix Configuration of Black Formatter (#2158)
* Fix Black in pre-commit

* Fix black some more
2020-10-23 13:40:02 +02:00
NikitaPirate 165a24e13d Add Convenience Properties for Service Chats and Anonymous Admins (#2147)
Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>
2020-10-18 16:15:56 +02:00
Michael K 88440079e3 Update Wheel Settings (#2142)
Wheels are only universal if they support both Python 2 and 3.
2020-10-15 21:50:25 +02:00
Bibo-Joshi 9be4c7563b Improve Type Hinting for Class Variables (#2136) 2020-10-15 18:50:47 +02:00
Timur Kushukov b554f1a85d Update timerbot.py to v13.0 (#2149)
* Update timerbot example (#2144)

* update timerbot example (suggestions from review)

Co-authored-by: Bibo-Joshi <hinrich.mahler@freenet.de>
2020-10-15 18:48:12 +02:00
NikitaPirate 3b4559dd95 Overhaul Constants (#2137)
* Move all constants to constants.py and documentation refactor.

* Move all constants to constants.py and documentation refactor.

* Overhaul constants

* Overhaul constants

* Minor docstring change

Co-authored-by: Hinrich Mahler <hinrich.mahler@freenet.de>
2020-10-13 17:58:36 +02:00
Bibo-Joshi 9ae48fecfe Add Python 3.9 to Test Matrix (#2132)
* Add Py 3.9 to tests

* update setup-python action
2020-10-11 11:47:19 +02:00
Bibo-Joshi 6af6648509 Switch Codecov to GitHub Action (#2127) 2020-10-10 10:50:19 +02:00
Bibo-Joshi 264b2c9c72 Switch Code Formatting to Black (#2122)
* Swtich code formatting to Black

* Update docs

* Fix tests

* TRy fixing pre-commit
2020-10-09 17:22:07 +02:00
Nano 8efb05290a Specify Required pytz Version (#2121) 2020-10-09 09:15:34 +02:00
Harshil 83a8874bb5 Correct Some Type Hints (#2118)
* Add string type hint

* fixed type hint in send_message()

* change type hint of send_chat_action to str

* make flaky happy

* fixed another type hint in edit_message_text
2020-10-09 08:22:44 +02:00
276 changed files with 16110 additions and 7633 deletions
+5 -6
View File
@@ -91,12 +91,14 @@ Here's how to make a one-off code change.
Once the process terminates, you can view the built documentation by opening ``docs/build/html/index.html`` with a browser.
- For consistency, please conform to `Google Python Style Guide`_ and `Google Python Style Docstrings`_. In addition, code should be formatted consistently with other code around it.
- For consistency, please conform to `Google Python Style Guide`_ and `Google Python Style Docstrings`_.
- The following exceptions to the above (Google's) style guides applies:
- Documenting types of global variables and complex types of class members can be done using the Sphinx docstring convention.
- In addition, PTB uses the `Black`_ coder formatting. Plugins for Black exist for some `popular editors`_. You can use those instead of manually formatting everything.
- Please ensure that the code you write is well-tested.
- Dont break backward compatibility.
@@ -189,11 +191,6 @@ Here's how to make a one-off code change.
Style commandments
------------------
Specific commandments
#####################
- Avoid using "double quotes" where you can reasonably use 'single quotes'.
Assert comparison order
#######################
@@ -255,3 +252,5 @@ break the API classes. For example:
.. _AUTHORS.rst: ../AUTHORS.rst
.. _`MyPy`: https://mypy.readthedocs.io/en/stable/index.html
.. _`here`: https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html
.. _`Black`: https://black.readthedocs.io/en/stable/index.html
.. _`popular editors`: https://black.readthedocs.io/en/stable/editor_integration.html
+7 -9
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ${{matrix.os}}
strategy:
matrix:
python-version: [3.6, 3.7, 3.8]
python-version: [3.6, 3.7, 3.8, 3.9]
os: [ubuntu-latest, windows-latest]
include:
- os: ubuntu-latest
@@ -31,7 +31,7 @@ jobs:
run:
git submodule update --init --recursive
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
@@ -57,13 +57,11 @@ jobs:
shell: bash --noprofile --norc {0}
- name: Submit coverage
run: |
if [ "$CODECOV_TOKEN" != "" ]; then
codecov -F github -t $CODECOV_TOKEN --name "${{ matrix.os }}-${{ matrix.python-version }}"
fi
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
shell: bash
uses: codecov/codecov-action@v1.0.13
with:
env_vars: OS,PYTHON
name: ${{ matrix.os }}-${{ matrix.python-version }}
fail_ci_if_error: true
test_official:
name: test-official
runs-on: ${{matrix.os}}
+14 -12
View File
@@ -1,25 +1,27 @@
# Make sure that
# * the revs specified here match requirements-dev.txt
# * the makefile checks the same files as pre-commit
repos:
- repo: git://github.com/python-telegram-bot/mirrors-yapf
rev: 5769e088ef6e0a0d1eb63bd6d0c1fe9f3606d6c8
- repo: https://github.com/psf/black
rev: 20.8b1
hooks:
- id: yapf
files: ^(telegram|tests)/.*\.py$
- id: black
args:
- --diff
- --check
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.1
rev: 3.8.4
hooks:
- id: flake8
- repo: git://github.com/pre-commit/mirrors-pylint
rev: v2.5.3
- repo: https://github.com/PyCQA/pylint
rev: pylint-2.6.0
hooks:
- id: pylint
files: ^telegram/.*\.py$
files: ^(telegram|examples)/.*\.py$
args:
- --errors-only
- --disable=import-error
- --rcfile=setup.cfg
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v0.770'
rev: v0.790
hooks:
- id: mypy
files: ^telegram/.*\.py$
files: ^(telegram|examples)/.*\.py$
+11 -2
View File
@@ -2,9 +2,12 @@ Credits
=======
``python-telegram-bot`` was originally created by
`Leandro Toledo <https://github.com/leandrotoledo>`_ and is now maintained by
`Leandro Toledo <https://github.com/leandrotoledo>`_ and is now maintained by `Hinrich Mahler <https://github.com/Bibo-Joshi>`_.
Emeritus maintainers include
`Jannes Höke <https://github.com/jh0ker>`_ (`@jh0ker <https://t.me/jh0ker>`_ on Telegram),
`Noam Meltzer <https://github.com/tsnoam>`_, `Pieter Schutz <https://github.com/eldinnie>`_, `Jasmin Bom <https://github.com/jsmnbom>`_ and `Hinrich Mahler <https://github.com/Bibo-Joshi>`_.
`Noam Meltzer <https://github.com/tsnoam>`_, `Pieter Schutz <https://github.com/eldinnie>`_ and `Jasmin Bom <https://github.com/jsmnbom>`_.
The maintainers are actively supported by `Poolitzer <https://github.com/Poolitzer>`_ in terms of development and community liaison.
We're vendoring urllib3 as part of ``python-telegram-bot`` which is distributed under the MIT
license. For more info, full credits & license terms, the sources can be found here:
@@ -36,9 +39,11 @@ The following wonderful people contributed directly or indirectly to this projec
- `Eugene Lisitsky <https://github.com/lisitsky>`_
- `Eugenio Panadero <https://github.com/azogue>`_
- `Evan Haberecht <https://github.com/habereet>`_
- `Evgeny Denisov <https://github.com/eIGato>`_
- `evgfilim1 <https://github.com/evgfilim1>`_
- `franciscod <https://github.com/franciscod>`_
- `gamgi <https://github.com/gamgi>`_
- `Gauthamram Ravichandran <https://github.com/GauthamramRavichandran>`_
- `Harshil <https://github.com/harshil21>`_
- `Hugo Damer <https://github.com/HakimusGIT>`_
- `ihoru <https://github.com/ihoru>`_
@@ -58,10 +63,12 @@ The following wonderful people contributed directly or indirectly to this projec
- `Loo Zheng Yuan <https://github.com/loozhengyuan>`_
- `LRezende <https://github.com/lrezende>`_
- `macrojames <https://github.com/macrojames>`_
- `Matheus Lemos <https://github.com/mlemosf>`_
- `Michael Elovskikh <https://github.com/wronglink>`_
- `Mischa Krüger <https://github.com/Makman2>`_
- `naveenvhegde <https://github.com/naveenvhegde>`_
- `neurrone <https://github.com/neurrone>`_
- `NikitaPirate <https://github.com/NikitaPirate>`_
- `njittam <https://github.com/njittam>`_
- `Noam Meltzer <https://github.com/tsnoam>`_
- `Oleg Shlyazhko <https://github.com/ollmer>`_
@@ -72,6 +79,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Paul Larsen <https://github.com/PaulSonOfLars>`_
- `Pieter Schutz <https://github.com/eldinnie>`_
- `Poolitzer <https://github.com/Poolitzer>`_
- `Pranjalya Tiwari <https://github.com/Pranjalya>`_
- `Rahiel Kasim <https://github.com/rahiel>`_
- `Riko Naka <https://github.com/rikonaka>`_
- `Rizlas <https://github.com/rizlas>`_
@@ -82,6 +90,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `sooyhwang <https://github.com/sooyhwang>`_
- `syntx <https://github.com/syntx>`_
- `thodnev <https://github.com/thodnev>`_
- `Timur Kushukov <https://github.com/timqsh>`_
- `Trainer Jono <https://github.com/Tr-Jono>`_
- `Valentijn <https://github.com/Faalentijn>`_
- `voider1 <https://github.com/voider1>`_
+85
View File
@@ -2,6 +2,91 @@
Changelog
=========
Version 13.1
============
*Released 2020-11-29*
**Major Changes:**
- Full support of Bot API 5.0 (`#2181`_, `#2186`_, `#2190`_, `#2189`_, `#2183`_, `#2184`_, `#2188`_, `#2185`_, `#2192`_, `#2196`_, `#2193`_, `#2223`_, `#2199`_, `#2187`_, `#2147`_, `#2205`_)
**New Features:**
- Add ``Defaults.run_async`` (`#2210`_)
- Improve and Expand ``CallbackQuery`` Shortcuts (`#2172`_)
- Add XOR Filters and make ``Filters.name`` a Property (`#2179`_)
- Add ``Filters.document.file_extension`` (`#2169`_)
- Add ``Filters.caption_regex`` (`#2163`_)
- Add ``Filters.chat_type`` (`#2128`_)
- Handle Non-Binary File Input (`#2202`_)
**Bug Fixes:**
- Improve Handling of Custom Objects in ``BasePersistence.insert``/``replace_bot`` (`#2151`_)
- Fix bugs in ``replace/insert_bot`` (`#2218`_)
**Minor changes, CI improvements, doc fixes and type hinting:**
- Improve Type hinting (`#2204`_, `#2118`_, `#2167`_, `#2136`_)
- Doc Fixes & Extensions (`#2201`_, `#2161`_)
- Use F-Strings Where Possible (`#2222`_)
- Rename kwargs to _kwargs where possible (`#2182`_)
- Comply with PEP561 (`#2168`_)
- Improve Code Quality (`#2131`_)
- Switch Code Formatting to Black (`#2122`_, `#2159`_, `#2158`_)
- Update Wheel Settings (`#2142`_)
- Update ``timerbot.py`` to ``v13.0`` (`#2149`_)
- Overhaul Constants (`#2137`_)
- Add Python 3.9 to Test Matrix (`#2132`_)
- Switch Codecov to ``GitHub`` Action (`#2127`_)
- Specify Required pytz Version (`#2121`_)
.. _`#2181`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2181
.. _`#2186`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2186
.. _`#2190`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2190
.. _`#2189`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2189
.. _`#2183`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2183
.. _`#2184`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2184
.. _`#2188`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2188
.. _`#2185`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2185
.. _`#2192`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2192
.. _`#2196`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2196
.. _`#2193`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2193
.. _`#2223`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2223
.. _`#2199`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2199
.. _`#2187`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2187
.. _`#2147`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2147
.. _`#2205`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2205
.. _`#2210`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2210
.. _`#2172`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2172
.. _`#2179`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2179
.. _`#2169`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2169
.. _`#2163`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2163
.. _`#2128`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2128
.. _`#2202`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2202
.. _`#2151`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2151
.. _`#2218`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2218
.. _`#2204`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2204
.. _`#2118`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2118
.. _`#2167`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2167
.. _`#2136`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2136
.. _`#2201`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2201
.. _`#2161`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2161
.. _`#2222`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2222
.. _`#2182`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2182
.. _`#2168`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2168
.. _`#2131`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2131
.. _`#2122`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2122
.. _`#2159`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2159
.. _`#2158`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2158
.. _`#2142`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2142
.. _`#2149`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2149
.. _`#2137`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2137
.. _`#2132`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2132
.. _`#2127`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2127
.. _`#2121`: https://github.com/python-telegram-bot/python-telegram-bot/pull/2121
Version 13.0
============
*Released 2020-10-07*
+1 -1
View File
@@ -1 +1 @@
include LICENSE LICENSE.lesser Makefile requirements.txt
include LICENSE LICENSE.lesser Makefile requirements.txt py.typed
+10 -15
View File
@@ -1,11 +1,10 @@
.DEFAULT_GOAL := help
.PHONY: clean pep257 pep8 yapf lint test install
.PHONY: clean pep8 black lint test install
PYLINT := pylint
PYTEST := pytest
PEP257 := pep257
PEP8 := flake8
YAPF := yapf
BLACK := black
MYPY := mypy
PIP := pip
@@ -15,22 +14,20 @@ clean:
find . -name '*.pyc' -exec rm -f {} \;
find . -name '*.pyo' -exec rm -f {} \;
find . -name '*~' -exec rm -f {} \;
find . -regex "./telegram.\(mp3\|mp4\|ogg\|png\|webp\)" -exec rm {} \;
pep257:
$(PEP257) telegram
find . -regex "./telegram[0-9]*.\(jpg\|mp3\|mp4\|ogg\|png\|webp\)" -exec rm {} \;
pep8:
$(PEP8) telegram
$(PEP8) telegram tests examples
yapf:
$(YAPF) -r telegram
black:
$(BLACK) .
lint:
$(PYLINT) -E telegram --disable=no-name-in-module,import-error
$(PYLINT) --rcfile=setup.cfg telegram examples
mypy:
$(MYPY) -p telegram
$(MYPY) examples
test:
$(PYTEST) -v
@@ -41,18 +38,16 @@ install:
help:
@echo "Available targets:"
@echo "- clean Clean up the source directory"
@echo "- pep257 Check docstring style with pep257"
@echo "- pep8 Check style with flake8"
@echo "- lint Check style with pylint"
@echo "- yapf Check style with yapf"
@echo "- black Check style with black"
@echo "- mypy Check type hinting with mypy"
@echo "- test Run tests using pytest"
@echo
@echo "Available variables:"
@echo "- PYLINT default: $(PYLINT)"
@echo "- PYTEST default: $(PYTEST)"
@echo "- PEP257 default: $(PEP257)"
@echo "- PEP8 default: $(PEP8)"
@echo "- YAPF default: $(YAPF)"
@echo "- BLACK default: $(BLACK)"
@echo "- MYPY default: $(MYPY)"
@echo "- PIP default: $(PIP)"
+3
View File
@@ -45,6 +45,9 @@ We have a vibrant community of developers helping each other in our `Telegram gr
:target: https://www.codacy.com/app/python-telegram-bot/python-telegram-bot?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=python-telegram-bot/python-telegram-bot&amp;utm_campaign=Badge_Grade
:alt: Code quality
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
.. image:: https://img.shields.io/badge/Telegram-Group-blue.svg
:target: https://telegram.me/pythontelegrambotgroup
:alt: Telegram Group
+2 -2
View File
@@ -58,9 +58,9 @@ author = u'Leandro Toledo'
# built documents.
#
# The short X.Y version.
version = '13.0' # telegram.__version__[:3]
version = '13.1' # telegram.__version__[:3]
# The full version, including alpha/beta/rc tags.
release = '13.0' # telegram.__version__
release = '13.1' # 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.ChatLocation
=====================
.. autoclass:: telegram.ChatLocation
:members:
:show-inheritance:
+6
View File
@@ -0,0 +1,6 @@
telegram.MessageId
==================
.. autoclass:: telegram.MessageId
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
telegram.ProximityAlertTriggered
================================
.. autoclass:: telegram.ProximityAlertTriggered
:members:
:show-inheritance:
+3
View File
@@ -13,6 +13,7 @@ telegram package
telegram.callbackquery
telegram.chat
telegram.chataction
telegram.chatlocation
telegram.chatmember
telegram.chatpermissions
telegram.chatphoto
@@ -37,12 +38,14 @@ telegram package
telegram.location
telegram.loginurl
telegram.message
telegram.messageid
telegram.messageentity
telegram.parsemode
telegram.photosize
telegram.poll
telegram.pollanswer
telegram.polloption
telegram.proximityalerttriggered
telegram.replykeyboardremove
telegram.replykeyboardmarkup
telegram.replymarkup
+57 -43
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=W0613, C0116
# type: ignore[union-attr]
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -16,82 +18,97 @@ bot.
import logging
from telegram import (ReplyKeyboardMarkup, ReplyKeyboardRemove)
from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters,
ConversationHandler)
from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
from telegram.ext import (
Updater,
CommandHandler,
MessageHandler,
Filters,
ConversationHandler,
CallbackContext,
)
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
GENDER, PHOTO, LOCATION, BIO = range(4)
def start(update, context):
def start(update: Update, context: CallbackContext) -> int:
reply_keyboard = [['Boy', 'Girl', 'Other']]
update.message.reply_text(
'Hi! My name is Professor Bot. I will hold a conversation with you. '
'Send /cancel to stop talking to me.\n\n'
'Are you a boy or a girl?',
reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True))
reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True),
)
return GENDER
def gender(update, context):
def gender(update: Update, context: CallbackContext) -> int:
user = update.message.from_user
logger.info("Gender of %s: %s", user.first_name, update.message.text)
update.message.reply_text('I see! Please send me a photo of yourself, '
'so I know what you look like, or send /skip if you don\'t want to.',
reply_markup=ReplyKeyboardRemove())
update.message.reply_text(
'I see! Please send me a photo of yourself, '
'so I know what you look like, or send /skip if you don\'t want to.',
reply_markup=ReplyKeyboardRemove(),
)
return PHOTO
def photo(update, context):
def photo(update: Update, context: CallbackContext) -> int:
user = update.message.from_user
photo_file = update.message.photo[-1].get_file()
photo_file.download('user_photo.jpg')
logger.info("Photo of %s: %s", user.first_name, 'user_photo.jpg')
update.message.reply_text('Gorgeous! Now, send me your location please, '
'or send /skip if you don\'t want to.')
update.message.reply_text(
'Gorgeous! Now, send me your location please, ' 'or send /skip if you don\'t want to.'
)
return LOCATION
def skip_photo(update, context):
def skip_photo(update: Update, context: CallbackContext) -> int:
user = update.message.from_user
logger.info("User %s did not send a photo.", user.first_name)
update.message.reply_text('I bet you look great! Now, send me your location please, '
'or send /skip.')
update.message.reply_text(
'I bet you look great! Now, send me your location please, ' 'or send /skip.'
)
return LOCATION
def location(update, context):
def location(update: Update, context: CallbackContext) -> int:
user = update.message.from_user
user_location = update.message.location
logger.info("Location of %s: %f / %f", user.first_name, user_location.latitude,
user_location.longitude)
update.message.reply_text('Maybe I can visit you sometime! '
'At last, tell me something about yourself.')
logger.info(
"Location of %s: %f / %f", user.first_name, user_location.latitude, user_location.longitude
)
update.message.reply_text(
'Maybe I can visit you sometime! ' 'At last, tell me something about yourself.'
)
return BIO
def skip_location(update, context):
def skip_location(update: Update, context: CallbackContext) -> int:
user = update.message.from_user
logger.info("User %s did not send a location.", user.first_name)
update.message.reply_text('You seem a bit paranoid! '
'At last, tell me something about yourself.')
update.message.reply_text(
'You seem a bit paranoid! ' 'At last, tell me something about yourself.'
)
return BIO
def bio(update, context):
def bio(update: Update, context: CallbackContext) -> int:
user = update.message.from_user
logger.info("Bio of %s: %s", user.first_name, update.message.text)
update.message.reply_text('Thank you! I hope we can talk again some day.')
@@ -99,44 +116,41 @@ def bio(update, context):
return ConversationHandler.END
def cancel(update, context):
def cancel(update: Update, context: CallbackContext) -> int:
user = update.message.from_user
logger.info("User %s canceled the conversation.", user.first_name)
update.message.reply_text('Bye! I hope we can talk again some day.',
reply_markup=ReplyKeyboardRemove())
update.message.reply_text(
'Bye! I hope we can talk again some day.', reply_markup=ReplyKeyboardRemove()
)
return ConversationHandler.END
def main():
def main() -> None:
# 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
dispatcher = updater.dispatcher
# Add conversation handler with the states GENDER, PHOTO, LOCATION and BIO
conv_handler = ConversationHandler(
entry_points=[CommandHandler('start', start)],
states={
GENDER: [MessageHandler(Filters.regex('^(Boy|Girl|Other)$'), gender)],
PHOTO: [MessageHandler(Filters.photo, photo),
CommandHandler('skip', skip_photo)],
LOCATION: [MessageHandler(Filters.location, location),
CommandHandler('skip', skip_location)],
BIO: [MessageHandler(Filters.text & ~Filters.command, bio)]
PHOTO: [MessageHandler(Filters.photo, photo), CommandHandler('skip', skip_photo)],
LOCATION: [
MessageHandler(Filters.location, location),
CommandHandler('skip', skip_location),
],
BIO: [MessageHandler(Filters.text & ~Filters.command, bio)],
},
fallbacks=[CommandHandler('cancel', cancel)]
fallbacks=[CommandHandler('cancel', cancel)],
)
dp.add_handler(conv_handler)
dispatcher.add_handler(conv_handler)
# Start the Bot
updater.start_polling()
+61 -44
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=W0613, C0116
# type: ignore[union-attr]
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -15,120 +17,135 @@ bot.
"""
import logging
from typing import Dict
from telegram import ReplyKeyboardMarkup
from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters,
ConversationHandler)
from telegram import ReplyKeyboardMarkup, Update
from telegram.ext import (
Updater,
CommandHandler,
MessageHandler,
Filters,
ConversationHandler,
CallbackContext,
)
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
CHOOSING, TYPING_REPLY, TYPING_CHOICE = range(3)
reply_keyboard = [['Age', 'Favourite colour'],
['Number of siblings', 'Something else...'],
['Done']]
reply_keyboard = [
['Age', 'Favourite colour'],
['Number of siblings', 'Something else...'],
['Done'],
]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
def facts_to_str(user_data):
def facts_to_str(user_data: Dict[str, str]) -> str:
facts = list()
for key, value in user_data.items():
facts.append('{} - {}'.format(key, value))
facts.append(f'{key} - {value}')
return "\n".join(facts).join(['\n', '\n'])
def start(update, context):
def start(update: Update, context: CallbackContext) -> int:
update.message.reply_text(
"Hi! My name is Doctor Botter. I will hold a more complex conversation with you. "
"Why don't you tell me something about yourself?",
reply_markup=markup)
reply_markup=markup,
)
return CHOOSING
def regular_choice(update, context):
def regular_choice(update: Update, context: CallbackContext) -> int:
text = update.message.text
context.user_data['choice'] = text
update.message.reply_text(
'Your {}? Yes, I would love to hear about that!'.format(text.lower()))
update.message.reply_text(f'Your {text.lower()}? Yes, I would love to hear about that!')
return TYPING_REPLY
def custom_choice(update, context):
update.message.reply_text('Alright, please send me the category first, '
'for example "Most impressive skill"')
def custom_choice(update: Update, context: CallbackContext) -> int:
update.message.reply_text(
'Alright, please send me the category first, ' 'for example "Most impressive skill"'
)
return TYPING_CHOICE
def received_information(update, context):
def received_information(update: Update, context: CallbackContext) -> int:
user_data = context.user_data
text = update.message.text
category = user_data['choice']
user_data[category] = text
del user_data['choice']
update.message.reply_text("Neat! Just so you know, this is what you already told me:"
"{} You can tell me more, or change your opinion"
" on something.".format(facts_to_str(user_data)),
reply_markup=markup)
update.message.reply_text(
"Neat! Just so you know, this is what you already told me:"
f"{facts_to_str(user_data)} You can tell me more, or change your opinion"
" on something.",
reply_markup=markup,
)
return CHOOSING
def done(update, context):
def done(update: Update, context: CallbackContext) -> int:
user_data = context.user_data
if 'choice' in user_data:
del user_data['choice']
update.message.reply_text("I learned these facts about you:"
"{}"
"Until next time!".format(facts_to_str(user_data)))
update.message.reply_text(
f"I learned these facts about you: {facts_to_str(user_data)} Until next time!"
)
user_data.clear()
return ConversationHandler.END
def main():
def main() -> None:
# 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
dispatcher = updater.dispatcher
# Add conversation handler with the states CHOOSING, TYPING_CHOICE and TYPING_REPLY
conv_handler = ConversationHandler(
entry_points=[CommandHandler('start', start)],
states={
CHOOSING: [MessageHandler(Filters.regex('^(Age|Favourite colour|Number of siblings)$'),
regular_choice),
MessageHandler(Filters.regex('^Something else...$'),
custom_choice)
],
CHOOSING: [
MessageHandler(
Filters.regex('^(Age|Favourite colour|Number of siblings)$'), regular_choice
),
MessageHandler(Filters.regex('^Something else...$'), custom_choice),
],
TYPING_CHOICE: [
MessageHandler(Filters.text & ~(Filters.command | Filters.regex('^Done$')),
regular_choice)],
MessageHandler(
Filters.text & ~(Filters.command | Filters.regex('^Done$')), regular_choice
)
],
TYPING_REPLY: [
MessageHandler(Filters.text & ~(Filters.command | Filters.regex('^Done$')),
received_information)],
MessageHandler(
Filters.text & ~(Filters.command | Filters.regex('^Done$')),
received_information,
)
],
},
fallbacks=[MessageHandler(Filters.regex('^Done$'), done)]
fallbacks=[MessageHandler(Filters.regex('^Done$'), done)],
)
dp.add_handler(conv_handler)
dispatcher.add_handler(conv_handler)
# Start the Bot
updater.start_polling()
+29 -22
View File
@@ -1,5 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=W0613, C0116
# type: ignore[union-attr]
# This program is dedicated to the public domain under the CC0 license.
"""Bot that explains Telegram's "Deep Linking Parameters" functionality.
@@ -19,14 +22,15 @@ bot.
import logging
from telegram import ParseMode, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import Updater, CommandHandler, Filters
from telegram import ParseMode, InlineKeyboardMarkup, InlineKeyboardButton, Update
from telegram.ext import Updater, CommandHandler, Filters, CallbackContext
# Enable logging
from telegram.utils import helpers
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
@@ -36,7 +40,7 @@ USING_ENTITIES = 'using-entities-here'
SO_COOL = 'so-cool'
def start(update, context):
def start(update: Update, context: CallbackContext) -> None:
"""Send a deep-linked URL when the command /start is issued."""
bot = context.bot
url = helpers.create_deep_linked_url(bot.get_me().username, CHECK_THIS_OUT, group=True)
@@ -44,32 +48,34 @@ def start(update, context):
update.message.reply_text(text)
def deep_linked_level_1(update, context):
def deep_linked_level_1(update: Update, context: CallbackContext) -> None:
"""Reached through the CHECK_THIS_OUT payload"""
bot = context.bot
url = helpers.create_deep_linked_url(bot.get_me().username, SO_COOL)
text = "Awesome, you just accessed hidden functionality! " \
" Now let's get back to the private chat."
text = (
"Awesome, you just accessed hidden functionality! "
" Now let's get back to the private chat."
)
keyboard = InlineKeyboardMarkup.from_button(
InlineKeyboardButton(text='Continue here!', url=url)
)
update.message.reply_text(text, reply_markup=keyboard)
def deep_linked_level_2(update, context):
def deep_linked_level_2(update: Update, context: CallbackContext) -> None:
"""Reached through the SO_COOL payload"""
bot = context.bot
url = helpers.create_deep_linked_url(bot.get_me().username, USING_ENTITIES)
text = "You can also mask the deep-linked URLs as links: " \
"[▶️ CLICK HERE]({}).".format(url)
text = f"You can also mask the deep-linked URLs as links: [▶️ CLICK HERE]({url})."
update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True)
def deep_linked_level_3(update, context):
def deep_linked_level_3(update: Update, context: CallbackContext) -> None:
"""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: {}".format(payload))
update.message.reply_text(
f"Congratulations! This is as deep as it gets 👏🏻\n\nThe payload was: {payload}"
)
def main():
@@ -78,25 +84,26 @@ def main():
updater = Updater("TOKEN", use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
dispatcher = updater.dispatcher
# More info on what deep linking actually is (read this first if it's unclear to you):
# https://core.telegram.org/bots#deep-linking
# Register a deep-linking handler
dp.add_handler(CommandHandler("start", deep_linked_level_1, Filters.regex(CHECK_THIS_OUT)))
dispatcher.add_handler(
CommandHandler("start", deep_linked_level_1, Filters.regex(CHECK_THIS_OUT))
)
# This one works with a textual link instead of an URL
dp.add_handler(CommandHandler("start", deep_linked_level_2, Filters.regex(SO_COOL)))
dispatcher.add_handler(CommandHandler("start", deep_linked_level_2, Filters.regex(SO_COOL)))
# We can also pass on the deep-linking payload
dp.add_handler(CommandHandler("start",
deep_linked_level_3,
Filters.regex(USING_ENTITIES),
pass_args=True))
dispatcher.add_handler(
CommandHandler("start", deep_linked_level_3, Filters.regex(USING_ENTITIES), pass_args=True)
)
# Make sure the deep-linking handlers occur *before* the normal /start handler.
dp.add_handler(CommandHandler("start", start))
dispatcher.add_handler(CommandHandler("start", start))
# Start the Bot
updater.start_polling()
+14 -10
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=W0613, C0116
# type: ignore[union-attr]
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -17,28 +19,30 @@ bot.
import logging
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters
from telegram import Update
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, CallbackContext
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
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):
def start(update: Update, context: CallbackContext) -> None:
"""Send a message when the command /start is issued."""
update.message.reply_text('Hi!')
def help_command(update, context):
def help_command(update: Update, context: CallbackContext) -> None:
"""Send a message when the command /help is issued."""
update.message.reply_text('Help!')
def echo(update, context):
def echo(update: Update, context: CallbackContext) -> None:
"""Echo the user message."""
update.message.reply_text(update.message.text)
@@ -51,14 +55,14 @@ def main():
updater = Updater("TOKEN", use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
dispatcher = updater.dispatcher
# on different commands - answer in Telegram
dp.add_handler(CommandHandler("start", start))
dp.add_handler(CommandHandler("help", help_command))
dispatcher.add_handler(CommandHandler("start", start))
dispatcher.add_handler(CommandHandler("help", help_command))
# on noncommand i.e message - echo the message on Telegram
dp.add_handler(MessageHandler(Filters.text & ~Filters.command, echo))
dispatcher.add_handler(MessageHandler(Filters.text & ~Filters.command, echo))
# Start the Bot
updater.start_polling()
+23 -23
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=W0613, C0116
# type: ignore[union-attr]
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -13,8 +15,9 @@ 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)
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
@@ -26,7 +29,7 @@ BOT_TOKEN = "TOKEN"
DEVELOPER_CHAT_ID = 123456789
def error_handler(update: Update, context: CallbackContext):
def error_handler(update: Update, context: CallbackContext) -> None:
"""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)
@@ -34,36 +37,33 @@ def error_handler(update: Update, context: CallbackContext):
# 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)
tb_string = ''.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)
f'An exception was raised while handling an update\n'
f'<pre>update = {html.escape(json.dumps(update.to_dict(), indent=2, ensure_ascii=False))}'
'</pre>\n\n'
f'<pre>context.chat_data = {html.escape(str(context.chat_data))}</pre>\n\n'
f'<pre>context.user_data = {html.escape(str(context.user_data))}</pre>\n\n'
f'<pre>{html.escape(tb_string)}</pre>'
)
# 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):
def bad_command(update: Update, context: CallbackContext) -> None:
"""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 start(update: Update, context: CallbackContext) -> None:
update.effective_message.reply_html(
'Use /bad_command to cause an error.\n'
f'Your chat id is <code>{update.effective_chat.id}</code>.'
)
def main():
@@ -73,14 +73,14 @@ def main():
updater = Updater(BOT_TOKEN, use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
dispatcher = updater.dispatcher
# Register the commands...
dp.add_handler(CommandHandler('start', start))
dp.add_handler(CommandHandler('bad_command', bad_command))
dispatcher.add_handler(CommandHandler('start', start))
dispatcher.add_handler(CommandHandler('bad_command', bad_command))
# ...and the error handler
dp.add_error_handler(error_handler)
dispatcher.add_error_handler(error_handler)
# Start the Bot
updater.start_polling()
+24 -21
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=W0613, C0116
# type: ignore[union-attr]
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -15,70 +17,71 @@ bot.
import logging
from uuid import uuid4
from telegram import InlineQueryResultArticle, ParseMode, \
InputTextMessageContent
from telegram.ext import Updater, InlineQueryHandler, CommandHandler
from telegram import InlineQueryResultArticle, ParseMode, InputTextMessageContent, Update
from telegram.ext import Updater, InlineQueryHandler, CommandHandler, CallbackContext
from telegram.utils.helpers import escape_markdown
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
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):
def start(update: Update, context: CallbackContext) -> None:
"""Send a message when the command /start is issued."""
update.message.reply_text('Hi!')
def help_command(update, context):
def help_command(update: Update, context: CallbackContext) -> None:
"""Send a message when the command /help is issued."""
update.message.reply_text('Help!')
def inlinequery(update, context):
def inlinequery(update: Update, context: CallbackContext) -> None:
"""Handle the inline query."""
query = update.inline_query.query
results = [
InlineQueryResultArticle(
id=uuid4(),
title="Caps",
input_message_content=InputTextMessageContent(
query.upper())),
id=uuid4(), title="Caps", input_message_content=InputTextMessageContent(query.upper())
),
InlineQueryResultArticle(
id=uuid4(),
title="Bold",
input_message_content=InputTextMessageContent(
"*{}*".format(escape_markdown(query)),
parse_mode=ParseMode.MARKDOWN)),
f"*{escape_markdown(query)}*", parse_mode=ParseMode.MARKDOWN
),
),
InlineQueryResultArticle(
id=uuid4(),
title="Italic",
input_message_content=InputTextMessageContent(
"_{}_".format(escape_markdown(query)),
parse_mode=ParseMode.MARKDOWN))]
f"_{escape_markdown(query)}_", parse_mode=ParseMode.MARKDOWN
),
),
]
update.inline_query.answer(results)
def main():
def main() -> None:
# 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
dispatcher = updater.dispatcher
# on different commands - answer in Telegram
dp.add_handler(CommandHandler("start", start))
dp.add_handler(CommandHandler("help", help_command))
dispatcher.add_handler(CommandHandler("start", start))
dispatcher.add_handler(CommandHandler("help", help_command))
# on noncommand i.e message - echo the message on Telegram
dp.add_handler(InlineQueryHandler(inlinequery))
dispatcher.add_handler(InlineQueryHandler(inlinequery))
# Start the Bot
updater.start_polling()
+18 -12
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=W0613, C0116
# type: ignore[union-attr]
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -7,36 +9,40 @@ Basic example for a bot that uses inline keyboards.
"""
import logging
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, CallbackContext
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
def start(update, context):
keyboard = [[InlineKeyboardButton("Option 1", callback_data='1'),
InlineKeyboardButton("Option 2", callback_data='2')],
[InlineKeyboardButton("Option 3", callback_data='3')]]
def start(update: Update, context: CallbackContext) -> None:
keyboard = [
[
InlineKeyboardButton("Option 1", callback_data='1'),
InlineKeyboardButton("Option 2", callback_data='2'),
],
[InlineKeyboardButton("Option 3", callback_data='3')],
]
reply_markup = InlineKeyboardMarkup(keyboard)
update.message.reply_text('Please choose:', reply_markup=reply_markup)
def button(update, context):
def button(update: Update, context: CallbackContext) -> None:
query = update.callback_query
# CallbackQueries need to be answered, even if no notification to the user is needed
# Some clients may have trouble otherwise. See https://core.telegram.org/bots/api#callbackquery
query.answer()
query.edit_message_text(text="Selected option: {}".format(query.data))
query.edit_message_text(text=f"Selected option: {query.data}")
def help_command(update, context):
def help_command(update: Update, context: CallbackContext) -> None:
update.message.reply_text("Use /start to test this bot.")
+66 -51
View File
@@ -1,5 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=W0613, C0116
# type: ignore[union-attr]
# This program is dedicated to the public domain under the CC0 license.
"""Simple inline keyboard bot with multiple CallbackQueryHandlers.
This Bot uses the Updater class to handle the bot.
@@ -12,13 +16,20 @@ ConversationHandler.
Send /start to initiate the conversation.
Press Ctrl-C on the command line to stop the bot.
"""
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, ConversationHandler
import logging
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (
Updater,
CommandHandler,
CallbackQueryHandler,
ConversationHandler,
CallbackContext,
)
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
@@ -28,7 +39,7 @@ FIRST, SECOND = range(2)
ONE, TWO, THREE, FOUR = range(4)
def start(update, context):
def start(update: Update, context: CallbackContext) -> None:
"""Send message on `/start`."""
# Get user that sent /start and log his name
user = update.message.from_user
@@ -38,20 +49,19 @@ def start(update, context):
# The keyboard is a list of button rows, where each row is in turn
# a list (hence `[[...]]`).
keyboard = [
[InlineKeyboardButton("1", callback_data=str(ONE)),
InlineKeyboardButton("2", callback_data=str(TWO))]
[
InlineKeyboardButton("1", callback_data=str(ONE)),
InlineKeyboardButton("2", callback_data=str(TWO)),
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
# Send message with text and appended InlineKeyboard
update.message.reply_text(
"Start handler, Choose a route",
reply_markup=reply_markup
)
update.message.reply_text("Start handler, Choose a route", reply_markup=reply_markup)
# Tell ConversationHandler that we're in state `FIRST` now
return FIRST
def start_over(update, context):
def start_over(update: Update, context: CallbackContext) -> None:
"""Prompt same text & keyboard as `start` does but not as new message"""
# Get CallbackQuery from Update
query = update.callback_query
@@ -59,93 +69,94 @@ def start_over(update, context):
# Some clients may have trouble otherwise. See https://core.telegram.org/bots/api#callbackquery
query.answer()
keyboard = [
[InlineKeyboardButton("1", callback_data=str(ONE)),
InlineKeyboardButton("2", callback_data=str(TWO))]
[
InlineKeyboardButton("1", callback_data=str(ONE)),
InlineKeyboardButton("2", callback_data=str(TWO)),
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
# Instead of sending a new message, edit the message that
# originated the CallbackQuery. This gives the feeling of an
# interactive menu.
query.edit_message_text(
text="Start handler, Choose a route",
reply_markup=reply_markup
)
query.edit_message_text(text="Start handler, Choose a route", reply_markup=reply_markup)
return FIRST
def one(update, context):
def one(update: Update, context: CallbackContext) -> None:
"""Show new choice of buttons"""
query = update.callback_query
query.answer()
keyboard = [
[InlineKeyboardButton("3", callback_data=str(THREE)),
InlineKeyboardButton("4", callback_data=str(FOUR))]
[
InlineKeyboardButton("3", callback_data=str(THREE)),
InlineKeyboardButton("4", callback_data=str(FOUR)),
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
query.edit_message_text(
text="First CallbackQueryHandler, Choose a route",
reply_markup=reply_markup
text="First CallbackQueryHandler, Choose a route", reply_markup=reply_markup
)
return FIRST
def two(update, context):
def two(update: Update, context: CallbackContext) -> None:
"""Show new choice of buttons"""
query = update.callback_query
query.answer()
keyboard = [
[InlineKeyboardButton("1", callback_data=str(ONE)),
InlineKeyboardButton("3", callback_data=str(THREE))]
[
InlineKeyboardButton("1", callback_data=str(ONE)),
InlineKeyboardButton("3", callback_data=str(THREE)),
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
query.edit_message_text(
text="Second CallbackQueryHandler, Choose a route",
reply_markup=reply_markup
text="Second CallbackQueryHandler, Choose a route", reply_markup=reply_markup
)
return FIRST
def three(update, context):
def three(update: Update, context: CallbackContext) -> None:
"""Show new choice of buttons"""
query = update.callback_query
query.answer()
keyboard = [
[InlineKeyboardButton("Yes, let's do it again!", callback_data=str(ONE)),
InlineKeyboardButton("Nah, I've had enough ...", callback_data=str(TWO))]
[
InlineKeyboardButton("Yes, let's do it again!", callback_data=str(ONE)),
InlineKeyboardButton("Nah, I've had enough ...", callback_data=str(TWO)),
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
query.edit_message_text(
text="Third CallbackQueryHandler. Do want to start over?",
reply_markup=reply_markup
text="Third CallbackQueryHandler. Do want to start over?", reply_markup=reply_markup
)
# Transfer to conversation state `SECOND`
return SECOND
def four(update, context):
def four(update: Update, context: CallbackContext) -> None:
"""Show new choice of buttons"""
query = update.callback_query
query.answer()
keyboard = [
[InlineKeyboardButton("2", callback_data=str(TWO)),
InlineKeyboardButton("4", callback_data=str(FOUR))]
[
InlineKeyboardButton("2", callback_data=str(TWO)),
InlineKeyboardButton("4", callback_data=str(FOUR)),
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
query.edit_message_text(
text="Fourth CallbackQueryHandler, Choose a route",
reply_markup=reply_markup
text="Fourth CallbackQueryHandler, Choose a route", reply_markup=reply_markup
)
return FIRST
def end(update, context):
def end(update: Update, context: CallbackContext) -> None:
"""Returns `ConversationHandler.END`, which tells the
ConversationHandler that the conversation is over"""
query = update.callback_query
query.answer()
query.edit_message_text(
text="See you next time!"
)
query.edit_message_text(text="See you next time!")
return ConversationHandler.END
@@ -154,7 +165,7 @@ def main():
updater = Updater("TOKEN", use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
dispatcher = updater.dispatcher
# Setup conversation handler with the states FIRST and SECOND
# Use the pattern parameter to pass CallbackQueries with specific
@@ -165,19 +176,23 @@ def main():
conv_handler = ConversationHandler(
entry_points=[CommandHandler('start', start)],
states={
FIRST: [CallbackQueryHandler(one, pattern='^' + str(ONE) + '$'),
CallbackQueryHandler(two, pattern='^' + str(TWO) + '$'),
CallbackQueryHandler(three, pattern='^' + str(THREE) + '$'),
CallbackQueryHandler(four, pattern='^' + str(FOUR) + '$')],
SECOND: [CallbackQueryHandler(start_over, pattern='^' + str(ONE) + '$'),
CallbackQueryHandler(end, pattern='^' + str(TWO) + '$')]
FIRST: [
CallbackQueryHandler(one, pattern='^' + str(ONE) + '$'),
CallbackQueryHandler(two, pattern='^' + str(TWO) + '$'),
CallbackQueryHandler(three, pattern='^' + str(THREE) + '$'),
CallbackQueryHandler(four, pattern='^' + str(FOUR) + '$'),
],
SECOND: [
CallbackQueryHandler(start_over, pattern='^' + str(ONE) + '$'),
CallbackQueryHandler(end, pattern='^' + str(TWO) + '$'),
],
},
fallbacks=[CommandHandler('start', start)]
fallbacks=[CommandHandler('start', start)],
)
# Add ConversationHandler to dispatcher that will be used for handling
# updates
dp.add_handler(conv_handler)
dispatcher.add_handler(conv_handler)
# Start the Bot
updater.start_polling()
+125 -97
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=W0613, C0116
# type: ignore[union-attr]
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -16,13 +18,21 @@ bot.
import logging
from telegram import (InlineKeyboardMarkup, InlineKeyboardButton)
from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters,
ConversationHandler, CallbackQueryHandler)
from telegram import InlineKeyboardMarkup, InlineKeyboardButton, Update
from telegram.ext import (
Updater,
CommandHandler,
MessageHandler,
Filters,
ConversationHandler,
CallbackQueryHandler,
CallbackContext,
)
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
@@ -38,30 +48,46 @@ STOPPING, SHOWING = map(chr, range(8, 10))
END = ConversationHandler.END
# Different constants for this example
(PARENTS, CHILDREN, SELF, GENDER, MALE, FEMALE, AGE, NAME, START_OVER, FEATURES,
CURRENT_FEATURE, CURRENT_LEVEL) = map(chr, range(10, 22))
(
PARENTS,
CHILDREN,
SELF,
GENDER,
MALE,
FEMALE,
AGE,
NAME,
START_OVER,
FEATURES,
CURRENT_FEATURE,
CURRENT_LEVEL,
) = map(chr, range(10, 22))
# Helper
def _name_switcher(level):
if level == PARENTS:
return ('Father', 'Mother')
elif level == CHILDREN:
return ('Brother', 'Sister')
return 'Father', 'Mother'
return 'Brother', 'Sister'
# Top level conversation callbacks
def start(update, context):
def start(update: Update, context: CallbackContext) -> None:
"""Select an action: Adding parent/child or show data."""
text = 'You may add a familiy member, yourself show the gathered data or end the ' \
'conversation. To abort, simply type /stop.'
buttons = [[
InlineKeyboardButton(text='Add family member', callback_data=str(ADDING_MEMBER)),
InlineKeyboardButton(text='Add yourself', callback_data=str(ADDING_SELF))
], [
InlineKeyboardButton(text='Show data', callback_data=str(SHOWING)),
InlineKeyboardButton(text='Done', callback_data=str(END))
]]
text = (
'You may add a familiy member, yourself show the gathered data or end the '
'conversation. To abort, simply type /stop.'
)
buttons = [
[
InlineKeyboardButton(text='Add family member', callback_data=str(ADDING_MEMBER)),
InlineKeyboardButton(text='Add yourself', callback_data=str(ADDING_SELF)),
],
[
InlineKeyboardButton(text='Show data', callback_data=str(SHOWING)),
InlineKeyboardButton(text='Done', callback_data=str(END)),
],
]
keyboard = InlineKeyboardMarkup(buttons)
# If we're starting over we don't need do send a new message
@@ -69,15 +95,16 @@ def start(update, context):
update.callback_query.answer()
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
else:
update.message.reply_text('Hi, I\'m FamiliyBot and here to help you gather information'
'about your family.')
update.message.reply_text(
'Hi, I\'m FamiliyBot and here to help you gather information' 'about your family.'
)
update.message.reply_text(text=text, reply_markup=keyboard)
context.user_data[START_OVER] = False
return SELECTING_ACTION
def adding_self(update, context):
def adding_self(update: Update, context: CallbackContext) -> None:
"""Add information about youself."""
context.user_data[CURRENT_LEVEL] = SELF
text = 'Okay, please tell me about yourself.'
@@ -90,8 +117,9 @@ def adding_self(update, context):
return DESCRIBING_SELF
def show_data(update, context):
def show_data(update: Update, context: CallbackContext) -> None:
"""Pretty print gathered data."""
def prettyprint(user_data, level):
people = user_data.get(level)
if not people:
@@ -100,41 +128,38 @@ def show_data(update, context):
text = ''
if level == SELF:
for person in user_data[level]:
text += '\nName: {}, Age: {}'.format(person.get(NAME, '-'), person.get(AGE, '-'))
text += f"\nName: {person.get(NAME, '-')}, Age: {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{}: Name: {}, Age: {}'.format(gender, person.get(NAME, '-'),
person.get(AGE, '-'))
text += f"\n{gender}: Name: {person.get(NAME, '-')}, Age: {person.get(AGE, '-')}"
return text
ud = context.user_data
text = 'Yourself:' + prettyprint(ud, SELF)
text += '\n\nParents:' + prettyprint(ud, PARENTS)
text += '\n\nChildren:' + prettyprint(ud, CHILDREN)
user_data = context.user_data
text = 'Yourself:' + prettyprint(user_data, SELF)
text += '\n\nParents:' + prettyprint(user_data, PARENTS)
text += '\n\nChildren:' + prettyprint(user_data, CHILDREN)
buttons = [[
InlineKeyboardButton(text='Back', callback_data=str(END))
]]
buttons = [[InlineKeyboardButton(text='Back', callback_data=str(END))]]
keyboard = InlineKeyboardMarkup(buttons)
update.callback_query.answer()
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
ud[START_OVER] = True
user_data[START_OVER] = True
return SHOWING
def stop(update, context):
def stop(update: Update, context: CallbackContext) -> None:
"""End Conversation by command."""
update.message.reply_text('Okay, bye.')
return END
def end(update, context):
def end(update: Update, context: CallbackContext) -> None:
"""End conversation from InlineKeyboardButton."""
update.callback_query.answer()
@@ -145,16 +170,19 @@ def end(update, context):
# Second level conversation callbacks
def select_level(update, context):
def select_level(update: Update, context: CallbackContext) -> None:
"""Choose to add a parent or a child."""
text = 'You may add a parent or a child. Also you can show the gathered data or go back.'
buttons = [[
InlineKeyboardButton(text='Add parent', callback_data=str(PARENTS)),
InlineKeyboardButton(text='Add child', callback_data=str(CHILDREN))
], [
InlineKeyboardButton(text='Show data', callback_data=str(SHOWING)),
InlineKeyboardButton(text='Back', callback_data=str(END))
]]
buttons = [
[
InlineKeyboardButton(text='Add parent', callback_data=str(PARENTS)),
InlineKeyboardButton(text='Add child', callback_data=str(CHILDREN)),
],
[
InlineKeyboardButton(text='Show data', callback_data=str(SHOWING)),
InlineKeyboardButton(text='Back', callback_data=str(END)),
],
]
keyboard = InlineKeyboardMarkup(buttons)
update.callback_query.answer()
@@ -163,7 +191,7 @@ def select_level(update, context):
return SELECTING_LEVEL
def select_gender(update, context):
def select_gender(update: Update, context: CallbackContext) -> None:
"""Choose to add mother or father."""
level = update.callback_query.data
context.user_data[CURRENT_LEVEL] = level
@@ -172,13 +200,16 @@ def select_gender(update, context):
male, female = _name_switcher(level)
buttons = [[
InlineKeyboardButton(text='Add ' + male, callback_data=str(MALE)),
InlineKeyboardButton(text='Add ' + female, callback_data=str(FEMALE))
], [
InlineKeyboardButton(text='Show data', callback_data=str(SHOWING)),
InlineKeyboardButton(text='Back', callback_data=str(END))
]]
buttons = [
[
InlineKeyboardButton(text='Add ' + male, callback_data=str(MALE)),
InlineKeyboardButton(text='Add ' + female, callback_data=str(FEMALE)),
],
[
InlineKeyboardButton(text='Show data', callback_data=str(SHOWING)),
InlineKeyboardButton(text='Back', callback_data=str(END)),
],
]
keyboard = InlineKeyboardMarkup(buttons)
update.callback_query.answer()
@@ -187,7 +218,7 @@ def select_gender(update, context):
return SELECTING_GENDER
def end_second_level(update, context):
def end_second_level(update: Update, context: CallbackContext) -> None:
"""Return to top level conversation."""
context.user_data[START_OVER] = True
start(update, context)
@@ -196,13 +227,15 @@ def end_second_level(update, context):
# Third level callbacks
def select_feature(update, context):
def select_feature(update: Update, context: CallbackContext) -> None:
"""Select a feature to update for the person."""
buttons = [[
InlineKeyboardButton(text='Name', callback_data=str(NAME)),
InlineKeyboardButton(text='Age', callback_data=str(AGE)),
InlineKeyboardButton(text='Done', callback_data=str(END)),
]]
buttons = [
[
InlineKeyboardButton(text='Name', callback_data=str(NAME)),
InlineKeyboardButton(text='Age', callback_data=str(AGE)),
InlineKeyboardButton(text='Done', callback_data=str(END)),
]
]
keyboard = InlineKeyboardMarkup(buttons)
# If we collect features for a new person, clear the cache and save the gender
@@ -221,7 +254,7 @@ def select_feature(update, context):
return SELECTING_FEATURE
def ask_for_input(update, context):
def ask_for_input(update: Update, context: CallbackContext) -> None:
"""Prompt user to input data for selected feature."""
context.user_data[CURRENT_FEATURE] = update.callback_query.data
text = 'Okay, tell me.'
@@ -232,27 +265,27 @@ def ask_for_input(update, context):
return TYPING
def save_input(update, context):
def save_input(update: Update, context: CallbackContext) -> None:
"""Save input for feature and return to feature selection."""
ud = context.user_data
ud[FEATURES][ud[CURRENT_FEATURE]] = update.message.text
user_data = context.user_data
user_data[FEATURES][user_data[CURRENT_FEATURE]] = update.message.text
ud[START_OVER] = True
user_data[START_OVER] = True
return select_feature(update, context)
def end_describing(update, context):
def end_describing(update: Update, context: CallbackContext) -> None:
"""End gathering of features and return to parent conversation."""
ud = context.user_data
level = ud[CURRENT_LEVEL]
if not ud.get(level):
ud[level] = []
ud[level].append(ud[FEATURES])
user_data = context.user_data
level = user_data[CURRENT_LEVEL]
if not user_data.get(level):
user_data[level] = []
user_data[level].append(user_data[FEATURES])
# Print upper level menu
if level == SELF:
ud[START_OVER] = True
user_data[START_OVER] = True
start(update, context)
else:
select_level(update, context)
@@ -260,7 +293,7 @@ def end_describing(update, context):
return END
def stop_nested(update, context):
def stop_nested(update: Update, context: CallbackContext) -> None:
"""Completely end conversation from within nested conversation."""
update.message.reply_text('Okay, bye.')
@@ -274,50 +307,47 @@ def main():
updater = Updater("TOKEN", use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
dispatcher = updater.dispatcher
# Set up third level ConversationHandler (collecting features)
description_conv = ConversationHandler(
entry_points=[CallbackQueryHandler(select_feature,
pattern='^' + str(MALE) + '$|^' + str(FEMALE) + '$')],
entry_points=[
CallbackQueryHandler(
select_feature, pattern='^' + str(MALE) + '$|^' + str(FEMALE) + '$'
)
],
states={
SELECTING_FEATURE: [CallbackQueryHandler(ask_for_input,
pattern='^(?!' + str(END) + ').*$')],
SELECTING_FEATURE: [
CallbackQueryHandler(ask_for_input, pattern='^(?!' + str(END) + ').*$')
],
TYPING: [MessageHandler(Filters.text & ~Filters.command, save_input)],
},
fallbacks=[
CallbackQueryHandler(end_describing, pattern='^' + str(END) + '$'),
CommandHandler('stop', stop_nested)
CommandHandler('stop', stop_nested),
],
map_to_parent={
# Return to second level menu
END: SELECTING_LEVEL,
# End conversation alltogether
STOPPING: STOPPING,
}
},
)
# Set up second level ConversationHandler (adding a person)
add_member_conv = ConversationHandler(
entry_points=[CallbackQueryHandler(select_level,
pattern='^' + str(ADDING_MEMBER) + '$')],
entry_points=[CallbackQueryHandler(select_level, pattern='^' + str(ADDING_MEMBER) + '$')],
states={
SELECTING_LEVEL: [CallbackQueryHandler(select_gender,
pattern='^{}$|^{}$'.format(str(PARENTS),
str(CHILDREN)))],
SELECTING_GENDER: [description_conv]
SELECTING_LEVEL: [
CallbackQueryHandler(select_gender, pattern=f'^{PARENTS}$|^{CHILDREN}$')
],
SELECTING_GENDER: [description_conv],
},
fallbacks=[
CallbackQueryHandler(show_data, pattern='^' + str(SHOWING) + '$'),
CallbackQueryHandler(end_second_level, pattern='^' + str(END) + '$'),
CommandHandler('stop', stop_nested)
CommandHandler('stop', stop_nested),
],
map_to_parent={
# After showing data return to top level menu
SHOWING: SHOWING,
@@ -325,7 +355,7 @@ def main():
END: SELECTING_ACTION,
# End conversation alltogether
STOPPING: END,
}
},
)
# Set up top level ConversationHandler (selecting action)
@@ -339,7 +369,6 @@ def main():
]
conv_handler = ConversationHandler(
entry_points=[CommandHandler('start', start)],
states={
SHOWING: [CallbackQueryHandler(start, pattern='^' + str(END) + '$')],
SELECTING_ACTION: selection_handlers,
@@ -347,11 +376,10 @@ def main():
DESCRIBING_SELF: [description_conv],
STOPPING: [CommandHandler('start', start)],
},
fallbacks=[CommandHandler('stop', stop)],
)
dp.add_handler(conv_handler)
dispatcher.add_handler(conv_handler)
# Start the Bot
updater.start_polling()
+38 -18
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=W0613, C0116
# type: ignore[union-attr]
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -12,16 +14,18 @@ See https://git.io/fAvYd for how to use Telegram Passport properly with python-t
"""
import logging
from telegram.ext import Updater, MessageHandler, Filters
from telegram import Update
from telegram.ext import Updater, MessageHandler, Filters, CallbackContext
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.DEBUG)
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.DEBUG
)
logger = logging.getLogger(__name__)
def msg(update, context):
def msg(update: Update, context: CallbackContext) -> None:
# If we received any passport data
passport_data = update.message.passport_data
if passport_data:
@@ -39,18 +43,28 @@ def msg(update, context):
print('Phone: ', data.phone_number)
elif data.type == 'email':
print('Email: ', data.email)
if data.type in ('personal_details', 'passport', 'driver_license', 'identity_card',
'internal_passport', 'address'):
if data.type in (
'personal_details',
'passport',
'driver_license',
'identity_card',
'internal_passport',
'address',
):
print(data.type, data.data)
if data.type in ('utility_bill', 'bank_statement', 'rental_agreement',
'passport_registration', 'temporary_registration'):
if data.type in (
'utility_bill',
'bank_statement',
'rental_agreement',
'passport_registration',
'temporary_registration',
):
print(data.type, len(data.files), 'files')
for file in data.files:
actual_file = file.get_file()
print(actual_file)
actual_file.download()
if data.type in ('passport', 'driver_license', 'identity_card',
'internal_passport'):
if data.type in ('passport', 'driver_license', 'identity_card', 'internal_passport'):
if data.front_side:
file = data.front_side.get_file()
print(data.type, file)
@@ -60,16 +74,22 @@ def msg(update, context):
file = data.reverse_side.get_file()
print(data.type, file)
file.download()
if data.type in ('passport', 'driver_license', 'identity_card',
'internal_passport'):
if data.type in ('passport', 'driver_license', 'identity_card', 'internal_passport'):
if data.selfie:
file = data.selfie.get_file()
print(data.type, file)
file.download()
if data.type in ('passport', 'driver_license', 'identity_card',
'internal_passport', 'utility_bill', 'bank_statement',
'rental_agreement', 'passport_registration',
'temporary_registration'):
if data.type in (
'passport',
'driver_license',
'identity_card',
'internal_passport',
'utility_bill',
'bank_statement',
'rental_agreement',
'passport_registration',
'temporary_registration',
):
print(data.type, len(data.translation), 'translation')
for file in data.translation:
actual_file = file.get_file()
@@ -83,10 +103,10 @@ def main():
updater = Updater("TOKEN", private_key=open('private.key', 'rb').read())
# Get the dispatcher to register handlers
dp = updater.dispatcher
dispatcher = updater.dispatcher
# On messages that include passport data call msg
dp.add_handler(MessageHandler(Filters.passport_data, msg))
dispatcher.add_handler(MessageHandler(Filters.passport_data, msg))
# Start the Bot
updater.start_polling()
+54 -32
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=W0613, C0116
# type: ignore[union-attr]
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -8,24 +10,32 @@ Basic example for a bot that can receive payment from user.
import logging
from telegram import (LabeledPrice, ShippingOption)
from telegram.ext import (Updater, CommandHandler, MessageHandler,
Filters, PreCheckoutQueryHandler, ShippingQueryHandler)
from telegram import LabeledPrice, ShippingOption, Update
from telegram.ext import (
Updater,
CommandHandler,
MessageHandler,
Filters,
PreCheckoutQueryHandler,
ShippingQueryHandler,
CallbackContext,
)
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
def start_callback(update, context):
def start_callback(update: Update, context: CallbackContext) -> None:
msg = "Use /shipping to get an invoice for shipping-payment, "
msg += "or /noshipping for an invoice without shipping."
update.message.reply_text(msg)
def start_with_shipping_callback(update, context):
def start_with_shipping_callback(update: Update, context: CallbackContext) -> None:
chat_id = update.message.chat_id
title = "Payment Example"
description = "Payment Example using python-telegram-bot"
@@ -43,13 +53,24 @@ def start_with_shipping_callback(update, context):
# optionally pass need_name=True, need_phone_number=True,
# need_email=True, need_shipping_address=True, is_flexible=True
context.bot.send_invoice(chat_id, title, description, payload,
provider_token, start_parameter, currency, prices,
need_name=True, need_phone_number=True,
need_email=True, need_shipping_address=True, is_flexible=True)
context.bot.send_invoice(
chat_id,
title,
description,
payload,
provider_token,
start_parameter,
currency,
prices,
need_name=True,
need_phone_number=True,
need_email=True,
need_shipping_address=True,
is_flexible=True,
)
def start_without_shipping_callback(update, context):
def start_without_shipping_callback(update: Update, context: CallbackContext) -> None:
chat_id = update.message.chat_id
title = "Payment Example"
description = "Payment Example using python-telegram-bot"
@@ -66,29 +87,30 @@ def start_without_shipping_callback(update, context):
# optionally pass need_name=True, need_phone_number=True,
# need_email=True, need_shipping_address=True, is_flexible=True
context.bot.send_invoice(chat_id, title, description, payload,
provider_token, start_parameter, currency, prices)
context.bot.send_invoice(
chat_id, title, description, payload, provider_token, start_parameter, currency, prices
)
def shipping_callback(update, context):
def shipping_callback(update: Update, context: CallbackContext) -> None:
query = update.shipping_query
# check the payload, is this from your bot?
if query.invoice_payload != 'Custom-Payload':
# answer False pre_checkout_query
query.answer(ok=False, error_message="Something went wrong...")
return
else:
options = list()
# a single LabeledPrice
options.append(ShippingOption('1', 'Shipping Option A', [LabeledPrice('A', 100)]))
# an array of LabeledPrice objects
price_list = [LabeledPrice('B1', 150), LabeledPrice('B2', 200)]
options.append(ShippingOption('2', 'Shipping Option B', price_list))
query.answer(ok=True, shipping_options=options)
options = list()
# a single LabeledPrice
options.append(ShippingOption('1', 'Shipping Option A', [LabeledPrice('A', 100)]))
# an array of LabeledPrice objects
price_list = [LabeledPrice('B1', 150), LabeledPrice('B2', 200)]
options.append(ShippingOption('2', 'Shipping Option B', price_list))
query.answer(ok=True, shipping_options=options)
# after (optional) shipping, it's the pre-checkout
def precheckout_callback(update, context):
def precheckout_callback(update: Update, context: CallbackContext) -> None:
query = update.pre_checkout_query
# check the payload, is this from your bot?
if query.invoice_payload != 'Custom-Payload':
@@ -99,7 +121,7 @@ def precheckout_callback(update, context):
# finally, after contacting the payment provider...
def successful_payment_callback(update, context):
def successful_payment_callback(update: Update, context: CallbackContext) -> None:
# do something after successfully receiving payment?
update.message.reply_text("Thank you for your payment!")
@@ -111,23 +133,23 @@ def main():
updater = Updater("TOKEN", use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
dispatcher = updater.dispatcher
# simple start function
dp.add_handler(CommandHandler("start", start_callback))
dispatcher.add_handler(CommandHandler("start", start_callback))
# Add command handler to start the payment invoice
dp.add_handler(CommandHandler("shipping", start_with_shipping_callback))
dp.add_handler(CommandHandler("noshipping", start_without_shipping_callback))
dispatcher.add_handler(CommandHandler("shipping", start_with_shipping_callback))
dispatcher.add_handler(CommandHandler("noshipping", start_without_shipping_callback))
# Optional handler if your product requires shipping
dp.add_handler(ShippingQueryHandler(shipping_callback))
dispatcher.add_handler(ShippingQueryHandler(shipping_callback))
# Pre-checkout handler to final check
dp.add_handler(PreCheckoutQueryHandler(precheckout_callback))
dispatcher.add_handler(PreCheckoutQueryHandler(precheckout_callback))
# Success! Notify your user!
dp.add_handler(MessageHandler(Filters.successful_payment, successful_payment_callback))
dispatcher.add_handler(MessageHandler(Filters.successful_payment, successful_payment_callback))
# Start the Bot
updater.start_polling()
+73 -50
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=W0613, C0116, C0103
# type: ignore[union-attr]
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -14,23 +16,34 @@ Press Ctrl-C on the command line or send a signal to the process to stop the
bot.
"""
from telegram import ReplyKeyboardMarkup
from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters,
ConversationHandler, PicklePersistence)
import logging
from telegram import ReplyKeyboardMarkup, Update
from telegram.ext import (
Updater,
CommandHandler,
MessageHandler,
Filters,
ConversationHandler,
PicklePersistence,
CallbackContext,
)
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
CHOOSING, TYPING_REPLY, TYPING_CHOICE = range(3)
reply_keyboard = [['Age', 'Favourite colour'],
['Number of siblings', 'Something else...'],
['Done']]
reply_keyboard = [
['Age', 'Favourite colour'],
['Number of siblings', 'Something else...'],
['Done'],
]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
@@ -38,72 +51,80 @@ def facts_to_str(user_data):
facts = list()
for key, value in user_data.items():
facts.append('{} - {}'.format(key, value))
facts.append(f'{key} - {value}')
return "\n".join(facts).join(['\n', '\n'])
def start(update, context):
def start(update: Update, context: CallbackContext) -> None:
reply_text = "Hi! My name is Doctor Botter."
if context.user_data:
reply_text += " You already told me your {}. Why don't you tell me something more " \
"about yourself? Or change anything I " \
"already know.".format(", ".join(context.user_data.keys()))
reply_text += (
f" You already told me your {', '.join(context.user_data.keys())}. Why don't you "
f"tell me something more about yourself? Or change anything I already know."
)
else:
reply_text += " I will hold a more complex conversation with you. Why don't you tell me " \
"something about yourself?"
reply_text += (
" I will hold a more complex conversation with you. Why don't you tell me "
"something about yourself?"
)
update.message.reply_text(reply_text, reply_markup=markup)
return CHOOSING
def regular_choice(update, context):
def regular_choice(update: Update, context: CallbackContext) -> None:
text = update.message.text.lower()
context.user_data['choice'] = text
if context.user_data.get(text):
reply_text = 'Your {}, I already know the following ' \
'about that: {}'.format(text, context.user_data[text])
reply_text = (
f'Your {text}, I already know the following about that: {context.user_data[text]}'
)
else:
reply_text = 'Your {}? Yes, I would love to hear about that!'.format(text)
reply_text = f'Your {text}? Yes, I would love to hear about that!'
update.message.reply_text(reply_text)
return TYPING_REPLY
def custom_choice(update, context):
update.message.reply_text('Alright, please send me the category first, '
'for example "Most impressive skill"')
def custom_choice(update: Update, context: CallbackContext) -> None:
update.message.reply_text(
'Alright, please send me the category first, ' 'for example "Most impressive skill"'
)
return TYPING_CHOICE
def received_information(update, context):
def received_information(update: Update, context: CallbackContext) -> None:
text = update.message.text
category = context.user_data['choice']
context.user_data[category] = text.lower()
del context.user_data['choice']
update.message.reply_text("Neat! Just so you know, this is what you already told me:"
"{}"
"You can tell me more, or change your opinion on "
"something.".format(facts_to_str(context.user_data)),
reply_markup=markup)
update.message.reply_text(
"Neat! Just so you know, this is what you already told me:"
f"{facts_to_str(context.user_data)}"
"You can tell me more, or change your opinion on "
"something.",
reply_markup=markup,
)
return CHOOSING
def show_data(update, context):
update.message.reply_text("This is what you already told me:"
"{}".format(facts_to_str(context.user_data)))
def show_data(update: Update, context: CallbackContext) -> None:
update.message.reply_text(
f"This is what you already told me: {facts_to_str(context.user_data)}"
)
def done(update, context):
def done(update: Update, context: CallbackContext) -> None:
if 'choice' in context.user_data:
del context.user_data['choice']
update.message.reply_text("I learned these facts about you:"
"{}"
"Until next time!".format(facts_to_str(context.user_data)))
update.message.reply_text(
"I learned these facts about you:" f"{facts_to_str(context.user_data)}" "Until next time!"
)
return ConversationHandler.END
@@ -118,26 +139,28 @@ def main():
# Add conversation handler with the states CHOOSING, TYPING_CHOICE and TYPING_REPLY
conv_handler = ConversationHandler(
entry_points=[CommandHandler('start', start)],
states={
CHOOSING: [MessageHandler(Filters.regex('^(Age|Favourite colour|Number of siblings)$'),
regular_choice),
MessageHandler(Filters.regex('^Something else...$'),
custom_choice),
],
CHOOSING: [
MessageHandler(
Filters.regex('^(Age|Favourite colour|Number of siblings)$'), regular_choice
),
MessageHandler(Filters.regex('^Something else...$'), custom_choice),
],
TYPING_CHOICE: [
MessageHandler(Filters.text & ~(Filters.command | Filters.regex('^Done$')),
regular_choice)],
MessageHandler(
Filters.text & ~(Filters.command | Filters.regex('^Done$')), regular_choice
)
],
TYPING_REPLY: [
MessageHandler(Filters.text & ~(Filters.command | Filters.regex('^Done$')),
received_information)],
MessageHandler(
Filters.text & ~(Filters.command | Filters.regex('^Done$')),
received_information,
)
],
},
fallbacks=[MessageHandler(Filters.regex('^Done$'), done)],
name="my_conversation",
persistent=True
persistent=True,
)
dp.add_handler(conv_handler)
+79 -46
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=W0613, C0116
# type: ignore[union-attr]
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -9,34 +11,62 @@ one the user sends the bot
"""
import logging
from telegram import (Poll, ParseMode, KeyboardButton, KeyboardButtonPollType,
ReplyKeyboardMarkup, ReplyKeyboardRemove)
from telegram.ext import (Updater, CommandHandler, PollAnswerHandler, PollHandler, MessageHandler,
Filters)
from telegram import (
Poll,
ParseMode,
KeyboardButton,
KeyboardButtonPollType,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
Update,
)
from telegram.ext import (
Updater,
CommandHandler,
PollAnswerHandler,
PollHandler,
MessageHandler,
Filters,
CallbackContext,
)
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)
def start(update, context):
def start(update: Update, context: CallbackContext) -> None:
"""Inform user about what this bot can do"""
update.message.reply_text('Please select /poll to get a Poll, /quiz to get a Quiz or /preview'
' to generate a preview for your poll')
update.message.reply_text(
'Please select /poll to get a Poll, /quiz to get a Quiz or /preview'
' to generate a preview for your poll'
)
def poll(update, context):
def poll(update: Update, context: CallbackContext) -> None:
"""Sends a predefined poll"""
questions = ["Good", "Really good", "Fantastic", "Great"]
message = context.bot.send_poll(update.effective_chat.id, "How are you?", questions,
is_anonymous=False, allows_multiple_answers=True)
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,
"chat_id": update.effective_chat.id, "answers": 0}}
payload = {
message.poll.id: {
"questions": questions,
"message_id": message.message_id,
"chat_id": update.effective_chat.id,
"answers": 0,
}
}
context.bot_data.update(payload)
def receive_poll_answer(update, context):
def receive_poll_answer(update: Update, context: CallbackContext) -> None:
"""Summarize a users poll vote"""
answer = update.poll_answer
poll_id = answer.poll_id
@@ -52,29 +82,33 @@ def receive_poll_answer(update, context):
answer_string += questions[question_id] + " and "
else:
answer_string += questions[question_id]
context.bot.send_message(context.bot_data[poll_id]["chat_id"],
"{} feels {}!".format(update.effective_user.mention_html(),
answer_string),
parse_mode=ParseMode.HTML)
context.bot.send_message(
context.bot_data[poll_id]["chat_id"],
f"{update.effective_user.mention_html()} feels {answer_string}!",
parse_mode=ParseMode.HTML,
)
context.bot_data[poll_id]["answers"] += 1
# Close poll after three participants voted
if context.bot_data[poll_id]["answers"] == 3:
context.bot.stop_poll(context.bot_data[poll_id]["chat_id"],
context.bot_data[poll_id]["message_id"])
context.bot.stop_poll(
context.bot_data[poll_id]["chat_id"], context.bot_data[poll_id]["message_id"]
)
def quiz(update, context):
def quiz(update: Update, context: CallbackContext) -> None:
"""Send a predefined poll"""
questions = ["1", "2", "4", "20"]
message = update.effective_message.reply_poll("How many eggs do you need for a cake?",
questions, type=Poll.QUIZ, correct_option_id=2)
message = update.effective_message.reply_poll(
"How many eggs do you need for a cake?", questions, type=Poll.QUIZ, correct_option_id=2
)
# Save some info about the poll the bot_data for later use in receive_quiz_answer
payload = {message.poll.id: {"chat_id": update.effective_chat.id,
"message_id": message.message_id}}
payload = {
message.poll.id: {"chat_id": update.effective_chat.id, "message_id": message.message_id}
}
context.bot_data.update(payload)
def receive_quiz_answer(update, context):
def receive_quiz_answer(update: Update, context: CallbackContext) -> None:
"""Close quiz after three participants took it"""
# the bot can receive closed poll updates we don't care about
if update.poll.is_closed:
@@ -88,18 +122,18 @@ def receive_quiz_answer(update, context):
context.bot.stop_poll(quiz_data["chat_id"], quiz_data["message_id"])
def preview(update, context):
def preview(update: Update, context: CallbackContext) -> None:
"""Ask user to create a poll and display a preview of it"""
# using this without a type lets the user chooses what he wants (quiz or poll)
button = [[KeyboardButton("Press me!", request_poll=KeyboardButtonPollType())]]
message = "Press the button to let the bot generate a preview for your poll"
# using one_time_keyboard to hide the keyboard
update.effective_message.reply_text(message,
reply_markup=ReplyKeyboardMarkup(button,
one_time_keyboard=True))
update.effective_message.reply_text(
message, reply_markup=ReplyKeyboardMarkup(button, one_time_keyboard=True)
)
def receive_poll(update, context):
def receive_poll(update: Update, context: CallbackContext) -> None:
"""On receiving polls, reply to it by a closed poll copying the received poll"""
actual_poll = update.effective_message.poll
# Only need to set the question and options, since all other parameters don't matter for
@@ -109,30 +143,29 @@ def receive_poll(update, context):
options=[o.text for o in actual_poll.options],
# with is_closed true, the poll/quiz is immediately closed
is_closed=True,
reply_markup=ReplyKeyboardRemove()
reply_markup=ReplyKeyboardRemove(),
)
def help_handler(update, context):
def help_handler(update: Update, context: CallbackContext) -> None:
"""Display a help message"""
update.message.reply_text("Use /quiz, /poll or /preview to test this "
"bot.")
update.message.reply_text("Use /quiz, /poll or /preview to test this " "bot.")
def main():
def main() -> None:
# 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)
dp = updater.dispatcher
dp.add_handler(CommandHandler('start', start))
dp.add_handler(CommandHandler('poll', poll))
dp.add_handler(PollAnswerHandler(receive_poll_answer))
dp.add_handler(CommandHandler('quiz', quiz))
dp.add_handler(PollHandler(receive_quiz_answer))
dp.add_handler(CommandHandler('preview', preview))
dp.add_handler(MessageHandler(Filters.poll, receive_poll))
dp.add_handler(CommandHandler('help', help_handler))
dispatcher = updater.dispatcher
dispatcher.add_handler(CommandHandler('start', start))
dispatcher.add_handler(CommandHandler('poll', poll))
dispatcher.add_handler(PollAnswerHandler(receive_poll_answer))
dispatcher.add_handler(CommandHandler('quiz', quiz))
dispatcher.add_handler(PollHandler(receive_quiz_answer))
dispatcher.add_handler(CommandHandler('preview', preview))
dispatcher.add_handler(MessageHandler(Filters.poll, receive_poll))
dispatcher.add_handler(CommandHandler('help', help_handler))
# Start the Bot
updater.start_polling()
+16 -13
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=W0603
"""Simple Bot to reply to Telegram messages.
This is built on the API wrapper, see rawapibot.py to see the same example built
@@ -7,26 +8,28 @@ 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 typing import NoReturn
from time import sleep
update_id = None
import telegram
from telegram.error import NetworkError, Unauthorized
def main():
UPDATE_ID = None
def main() -> NoReturn:
"""Run the bot."""
global update_id
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
UPDATE_ID = bot.get_updates()[0].update_id
except IndexError:
update_id = None
UPDATE_ID = None
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
@@ -37,15 +40,15 @@ def main():
sleep(1)
except Unauthorized:
# The user has removed or blocked the bot.
update_id += 1
UPDATE_ID += 1 # type: ignore[operator]
def echo(bot):
def echo(bot: telegram.Bot) -> None:
"""Echo the message the user sent."""
global update_id
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
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
+35 -30
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pylint: disable=W0613, C0116
# type: ignore[union-attr]
# This program is dedicated to the public domain under the CC0 license.
"""
@@ -20,18 +22,20 @@ bot.
import logging
from telegram.ext import Updater, CommandHandler
from telegram import Update
from telegram.ext import Updater, CommandHandler, CallbackContext
# Enable logging
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
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):
def start(update: Update, context: CallbackContext) -> None:
update.message.reply_text('Hi! Use /set <seconds> to set a timer')
@@ -41,7 +45,17 @@ def alarm(context):
context.bot.send_message(job.context, text='Beep!')
def set_timer(update, context):
def remove_job_if_exists(name, context):
"""Remove job with given name. Returns whether job was removed."""
current_jobs = context.job_queue.get_jobs_by_name(name)
if not current_jobs:
return False
for job in current_jobs:
job.schedule_removal()
return True
def set_timer(update: Update, context: CallbackContext) -> None:
"""Add a job to the queue."""
chat_id = update.message.chat_id
try:
@@ -51,30 +65,24 @@ def set_timer(update, context):
update.message.reply_text('Sorry we can not go back to future!')
return
# Add job to queue and stop current one if there is a timer already
if 'job' in context.chat_data:
old_job = context.chat_data['job']
old_job.schedule_removal()
new_job = context.job_queue.run_once(alarm, due, context=chat_id)
context.chat_data['job'] = new_job
job_removed = remove_job_if_exists(str(chat_id), context)
context.job_queue.run_once(alarm, due, context=chat_id, name=str(chat_id))
update.message.reply_text('Timer successfully set!')
text = 'Timer successfully set!'
if job_removed:
text += ' Old one was removed.'
update.message.reply_text(text)
except (IndexError, ValueError):
update.message.reply_text('Usage: /set <seconds>')
def unset(update, context):
def unset(update: Update, context: CallbackContext) -> None:
"""Remove the job if the user changed their mind."""
if 'job' not in context.chat_data:
update.message.reply_text('You have no active timer')
return
job = context.chat_data['job']
job.schedule_removal()
del context.chat_data['job']
update.message.reply_text('Timer successfully unset!')
chat_id = update.message.chat_id
job_removed = remove_job_if_exists(str(chat_id), context)
text = 'Timer successfully cancelled!' if job_removed else 'You have no active timer.'
update.message.reply_text(text)
def main():
@@ -85,16 +93,13 @@ def main():
updater = Updater("TOKEN", use_context=True)
# Get the dispatcher to register handlers
dp = updater.dispatcher
dispatcher = updater.dispatcher
# on different commands - answer in Telegram
dp.add_handler(CommandHandler("start", start))
dp.add_handler(CommandHandler("help", start))
dp.add_handler(CommandHandler("set", set_timer,
pass_args=True,
pass_job_queue=True,
pass_chat_data=True))
dp.add_handler(CommandHandler("unset", unset, pass_chat_data=True))
dispatcher.add_handler(CommandHandler("start", start))
dispatcher.add_handler(CommandHandler("help", start))
dispatcher.add_handler(CommandHandler("set", set_timer))
dispatcher.add_handler(CommandHandler("unset", unset))
# Start the Bot
updater.start_polling()
+11
View File
@@ -0,0 +1,11 @@
[tool.black]
line-length = 99
target-version = ['py36']
skip-string-normalization = true
# We need to force-exclude the negated include pattern
# so that pre-commit run --all-files does the correct thing
# see https://github.com/psf/black/issues/1778
force-exclude = '^(?!/(telegram|examples|tests)/).*\.py$'
include = '(telegram|examples|tests)/.*\.py$'
exclude = 'telegram/vendor'
+11 -8
View File
@@ -1,12 +1,15 @@
flake8
pep257
pylint
flaky
yapf
mypy==0.770
pre-commit
beautifulsoup4
# Make sure that the versions specified here match the pre-commit settings
black==20.8b1
flake8==3.8.4
pylint==2.6.0
mypy==0.790
pytest==4.2.0
# Need older attrs version for pytest 4.2.0
attrs==19.1.0
flaky
beautifulsoup4
pytest-timeout
wheel
attrs==19.1.0
+1
View File
@@ -3,3 +3,4 @@ tornado>=5.1
cryptography
decorator>=4.4.0
APScheduler==3.6.3
pytz>=2018.6
+13 -8
View File
@@ -1,6 +1,3 @@
[wheel]
universal = 1
[metadata]
license_file = LICENSE.dual
@@ -15,12 +12,14 @@ upload-dir = docs/build/html
[flake8]
max-line-length = 99
ignore = W503, W605
exclude = setup.py, docs/source/conf.py
extend-ignore = E203
exclude = setup.py, docs/source/conf.py, telegram/vendor
[yapf]
based_on_style = google
split_before_logical_operator = True
column_limit = 99
[pylint]
ignore=vendor
[pylint.message-control]
disable = C0330,R0801,R0913,R0904,R0903,R0902,W0511,C0116,C0115,W0703,R0914,R0914,C0302,R0912,R0915,R0401
[tool:pytest]
testpaths = tests
@@ -59,3 +58,9 @@ ignore_errors = True
# 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
[mypy-urllib3.*]
ignore_missing_imports = True
[mypy-apscheduler.*]
ignore_missing_imports = True
+2
View File
@@ -45,6 +45,7 @@ with codecs.open('README.rst', 'r', 'utf-8') as fd:
description="We have made you a wrapper you can't refuse",
long_description=fd.read(),
packages=packages,
package_data={'telegram': ['py.typed']},
install_requires=requirements,
extras_require={
'json': 'ujson',
@@ -64,4 +65,5 @@ with codecs.open('README.rst', 'r', 'utf-8') as fd:
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
],)
+166 -53
View File
@@ -23,6 +23,7 @@ from .botcommand import BotCommand
from .user import User
from .files.chatphoto import ChatPhoto
from .chat import Chat
from .chatlocation import ChatLocation
from .chatmember import ChatMember
from .chatpermissions import ChatPermissions
from .files.photosize import PhotoSize
@@ -50,9 +51,11 @@ from .files.inputfile import InputFile
from .files.file import File
from .parsemode import ParseMode
from .messageentity import MessageEntity
from .messageid import MessageId
from .games.game import Game
from .poll import Poll, PollOption, PollAnswer
from .loginurl import LoginUrl
from .proximityalerttriggered import ProximityAlertTriggered
from .games.callbackgame import CallbackGame
from .payment.shippingaddress import ShippingAddress
from .payment.orderinfo import OrderInfo
@@ -102,63 +105,173 @@ from .payment.shippingquery import ShippingQuery
from .webhookinfo import WebhookInfo
from .games.gamehighscore import GameHighScore
from .update import Update
from .files.inputmedia import (InputMedia, InputMediaVideo, InputMediaPhoto, InputMediaAnimation,
InputMediaAudio, InputMediaDocument)
from .constants import (MAX_MESSAGE_LENGTH, MAX_CAPTION_LENGTH, SUPPORTED_WEBHOOK_PORTS,
MAX_FILESIZE_DOWNLOAD, MAX_FILESIZE_UPLOAD,
MAX_MESSAGES_PER_SECOND_PER_CHAT, MAX_MESSAGES_PER_SECOND,
MAX_MESSAGES_PER_MINUTE_PER_GROUP)
from .passport.passportelementerrors import (PassportElementError,
PassportElementErrorDataField,
PassportElementErrorFile,
PassportElementErrorFiles,
PassportElementErrorFrontSide,
PassportElementErrorReverseSide,
PassportElementErrorSelfie,
PassportElementErrorTranslationFile,
PassportElementErrorTranslationFiles,
PassportElementErrorUnspecified)
from .passport.credentials import (Credentials,
DataCredentials,
SecureData,
FileCredentials,
TelegramDecryptionError)
from .files.inputmedia import (
InputMedia,
InputMediaVideo,
InputMediaPhoto,
InputMediaAnimation,
InputMediaAudio,
InputMediaDocument,
)
from .constants import (
MAX_MESSAGE_LENGTH,
MAX_CAPTION_LENGTH,
SUPPORTED_WEBHOOK_PORTS,
MAX_FILESIZE_DOWNLOAD,
MAX_FILESIZE_UPLOAD,
MAX_MESSAGES_PER_SECOND_PER_CHAT,
MAX_MESSAGES_PER_SECOND,
MAX_MESSAGES_PER_MINUTE_PER_GROUP,
)
from .passport.passportelementerrors import (
PassportElementError,
PassportElementErrorDataField,
PassportElementErrorFile,
PassportElementErrorFiles,
PassportElementErrorFrontSide,
PassportElementErrorReverseSide,
PassportElementErrorSelfie,
PassportElementErrorTranslationFile,
PassportElementErrorTranslationFiles,
PassportElementErrorUnspecified,
)
from .passport.credentials import (
Credentials,
DataCredentials,
SecureData,
FileCredentials,
TelegramDecryptionError,
)
from .bot import Bot
from .version import __version__ # noqa: F401
__author__ = 'devs@python-telegram-bot.org'
__all__ = [
'Audio', 'Bot', 'Chat', 'ChatMember', 'ChatPermissions', 'ChatAction', 'ChosenInlineResult',
'CallbackQuery', 'Contact', 'Document', 'File', 'ForceReply', 'InlineKeyboardButton',
'InlineKeyboardMarkup', 'InlineQuery', 'InlineQueryResult', 'InlineQueryResult',
'InlineQueryResultArticle', 'InlineQueryResultAudio', 'InlineQueryResultCachedAudio',
'InlineQueryResultCachedDocument', 'InlineQueryResultCachedGif',
'InlineQueryResultCachedMpeg4Gif', 'InlineQueryResultCachedPhoto',
'InlineQueryResultCachedSticker', 'InlineQueryResultCachedVideo',
'InlineQueryResultCachedVoice', 'InlineQueryResultContact', 'InlineQueryResultDocument',
'InlineQueryResultGif', 'InlineQueryResultLocation', 'InlineQueryResultMpeg4Gif',
'InlineQueryResultPhoto', 'InlineQueryResultVenue', 'InlineQueryResultVideo',
'InlineQueryResultVoice', 'InlineQueryResultGame', 'InputContactMessageContent', 'InputFile',
'InputLocationMessageContent', 'InputMessageContent', 'InputTextMessageContent',
'InputVenueMessageContent', 'Location', 'EncryptedCredentials',
'PassportFile', 'EncryptedPassportElement', 'PassportData', 'Message', 'MessageEntity',
'ParseMode', 'PhotoSize', 'ReplyKeyboardRemove', 'ReplyKeyboardMarkup', 'ReplyMarkup',
'Sticker', 'TelegramError', 'TelegramObject', 'Update', 'User', 'UserProfilePhotos', 'Venue',
'Video', 'Voice', 'MAX_MESSAGE_LENGTH', 'MAX_CAPTION_LENGTH', 'SUPPORTED_WEBHOOK_PORTS',
'MAX_FILESIZE_DOWNLOAD', 'MAX_FILESIZE_UPLOAD', 'MAX_MESSAGES_PER_SECOND_PER_CHAT',
'MAX_MESSAGES_PER_SECOND', 'MAX_MESSAGES_PER_MINUTE_PER_GROUP', 'WebhookInfo', 'Animation',
'Game', 'GameHighScore', 'VideoNote', 'LabeledPrice', 'SuccessfulPayment', 'ShippingOption',
'ShippingAddress', 'PreCheckoutQuery', 'OrderInfo', 'Invoice', 'ShippingQuery', 'ChatPhoto',
'StickerSet', 'MaskPosition', 'CallbackGame', 'InputMedia', 'InputMediaPhoto',
'InputMediaVideo', 'PassportElementError', 'PassportElementErrorFile',
'PassportElementErrorReverseSide', 'PassportElementErrorFrontSide',
'PassportElementErrorFiles', 'PassportElementErrorDataField', 'PassportElementErrorFile',
'Credentials', 'DataCredentials', 'SecureData', 'FileCredentials', 'IdDocumentData',
'PersonalDetails', 'ResidentialAddress', 'InputMediaVideo', 'InputMediaAnimation',
'InputMediaAudio', 'InputMediaDocument', 'TelegramDecryptionError',
'PassportElementErrorSelfie', 'PassportElementErrorTranslationFile',
'PassportElementErrorTranslationFiles', 'PassportElementErrorUnspecified', 'Poll',
'PollOption', 'PollAnswer', 'LoginUrl', 'KeyboardButton', 'KeyboardButtonPollType', 'Dice',
'BotCommand'
'Audio',
'Bot',
'Chat',
'ChatMember',
'ChatPermissions',
'ChatAction',
'ChosenInlineResult',
'CallbackQuery',
'Contact',
'Document',
'File',
'ForceReply',
'InlineKeyboardButton',
'InlineKeyboardMarkup',
'InlineQuery',
'InlineQueryResult',
'InlineQueryResult',
'InlineQueryResultArticle',
'InlineQueryResultAudio',
'InlineQueryResultCachedAudio',
'InlineQueryResultCachedDocument',
'InlineQueryResultCachedGif',
'InlineQueryResultCachedMpeg4Gif',
'InlineQueryResultCachedPhoto',
'InlineQueryResultCachedSticker',
'InlineQueryResultCachedVideo',
'InlineQueryResultCachedVoice',
'InlineQueryResultContact',
'InlineQueryResultDocument',
'InlineQueryResultGif',
'InlineQueryResultLocation',
'InlineQueryResultMpeg4Gif',
'InlineQueryResultPhoto',
'InlineQueryResultVenue',
'InlineQueryResultVideo',
'InlineQueryResultVoice',
'InlineQueryResultGame',
'InputContactMessageContent',
'InputFile',
'InputLocationMessageContent',
'InputMessageContent',
'InputTextMessageContent',
'InputVenueMessageContent',
'Location',
'ChatLocation',
'ProximityAlertTriggered',
'EncryptedCredentials',
'PassportFile',
'EncryptedPassportElement',
'PassportData',
'Message',
'MessageEntity',
'ParseMode',
'PhotoSize',
'ReplyKeyboardRemove',
'ReplyKeyboardMarkup',
'ReplyMarkup',
'Sticker',
'TelegramError',
'TelegramObject',
'Update',
'User',
'UserProfilePhotos',
'Venue',
'Video',
'Voice',
'MAX_MESSAGE_LENGTH',
'MAX_CAPTION_LENGTH',
'SUPPORTED_WEBHOOK_PORTS',
'MAX_FILESIZE_DOWNLOAD',
'MAX_FILESIZE_UPLOAD',
'MAX_MESSAGES_PER_SECOND_PER_CHAT',
'MAX_MESSAGES_PER_SECOND',
'MAX_MESSAGES_PER_MINUTE_PER_GROUP',
'WebhookInfo',
'Animation',
'Game',
'GameHighScore',
'VideoNote',
'LabeledPrice',
'SuccessfulPayment',
'ShippingOption',
'ShippingAddress',
'PreCheckoutQuery',
'OrderInfo',
'Invoice',
'ShippingQuery',
'ChatPhoto',
'StickerSet',
'MaskPosition',
'CallbackGame',
'InputMedia',
'InputMediaPhoto',
'InputMediaVideo',
'PassportElementError',
'PassportElementErrorFile',
'PassportElementErrorReverseSide',
'PassportElementErrorFrontSide',
'PassportElementErrorFiles',
'PassportElementErrorDataField',
'PassportElementErrorFile',
'Credentials',
'DataCredentials',
'SecureData',
'FileCredentials',
'IdDocumentData',
'PersonalDetails',
'ResidentialAddress',
'InputMediaVideo',
'InputMediaAnimation',
'InputMediaAudio',
'InputMediaDocument',
'TelegramDecryptionError',
'PassportElementErrorSelfie',
'PassportElementErrorTranslationFile',
'PassportElementErrorTranslationFiles',
'PassportElementErrorUnspecified',
'Poll',
'PollOption',
'PollAnswer',
'LoginUrl',
'KeyboardButton',
'KeyboardButtonPollType',
'Dice',
'BotCommand',
'MessageId',
]
+10 -9
View File
@@ -16,20 +16,21 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import sys
# pylint: disable=E0401, C0114
import subprocess
import sys
from typing import Optional
import certifi
from typing import Optional
from . import __version__ as telegram_ver
def _git_revision() -> Optional[str]:
try:
output = subprocess.check_output(["git", "describe", "--long", "--tags"],
stderr=subprocess.STDOUT)
output = subprocess.check_output(
["git", "describe", "--long", "--tags"], stderr=subprocess.STDOUT
)
except (subprocess.SubprocessError, OSError):
return None
return output.decode().strip()
@@ -37,10 +38,10 @@ def _git_revision() -> Optional[str]:
def print_ver_info() -> None:
git_revision = _git_revision()
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', ' ')))
print(f'python-telegram-bot {telegram_ver}' + (f' ({git_revision})' if git_revision else ''))
print(f'certifi {certifi.__version__}') # type: ignore[attr-defined]
sys_version = sys.version.replace('\n', ' ')
print(f'Python {sys_version}')
def main() -> None:
+12 -11
View File
@@ -23,9 +23,9 @@ except ImportError:
import json # type: ignore[no-redef]
import warnings
from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Type, TypeVar
from telegram.utils.types import JSONDict
from typing import Tuple, Any, Optional, Type, TypeVar, TYPE_CHECKING, List
if TYPE_CHECKING:
from telegram import Bot
@@ -36,7 +36,7 @@ TO = TypeVar('TO', bound='TelegramObject', covariant=True)
class TelegramObject:
"""Base class for most telegram objects."""
# def __init__(self, *args: Any, **kwargs: Any):
# def __init__(self, *args: Any, **_kwargs: Any):
# pass
_id_attrs: Tuple[Any, ...] = ()
@@ -62,13 +62,10 @@ class TelegramObject:
if cls == TelegramObject:
return cls()
else:
return cls(bot=bot, **data) # type: ignore[call-arg]
return cls(bot=bot, **data) # type: ignore[call-arg]
@classmethod
def de_list(cls: Type[TO],
data: Optional[List[JSONDict]],
bot: 'Bot') -> List[Optional[TO]]:
def de_list(cls: Type[TO], data: Optional[List[JSONDict]], bot: 'Bot') -> List[Optional[TO]]:
if not data:
return []
@@ -104,11 +101,15 @@ class TelegramObject:
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__))
warnings.warn(
f"Objects of type {self.__class__.__name__} can not be meaningfully tested for"
" equivalence."
)
if other._id_attrs == ():
warnings.warn("Objects of type {} can not be meaningfully tested for "
"equivalence.".format(other.__class__.__name__))
warnings.warn(
f"Objects of type {other.__class__.__name__} can not be meaningfully tested"
" for equivalence."
)
return self._id_attrs == other._id_attrs
return super().__eq__(other) # pylint: disable=no-member
+1526 -899
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -18,9 +18,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 an object that represents a Telegram Bot Command."""
from telegram import TelegramObject
from typing import Any
from telegram import TelegramObject
class BotCommand(TelegramObject):
"""
@@ -38,7 +39,8 @@ class BotCommand(TelegramObject):
English letters, digits and underscores.
description (:obj:`str`): Description of the command, 3-256 characters.
"""
def __init__(self, command: str, description: str, **kwargs: Any):
def __init__(self, command: str, description: str, **_kwargs: Any):
self.command = command
self.description = description
+131 -107
View File
@@ -16,14 +16,15 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
# pylint: disable=W0622
"""This module contains an object that represents a Telegram CallbackQuery"""
from telegram import TelegramObject, Message, User
from typing import TYPE_CHECKING, Any, List, Optional, Union
from telegram import Message, TelegramObject, 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
from telegram import Bot, GameHighScore, InlineKeyboardMarkup, MessageId
class CallbackQuery(TelegramObject):
@@ -78,18 +79,20 @@ class CallbackQuery(TelegramObject):
"""
def __init__(self,
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):
def __init__(
self,
id: str, # pylint: disable=W0622
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.id = id # pylint: disable=C0103
self.from_user = from_user
self.chat_instance = chat_instance
# Optionals
@@ -128,9 +131,7 @@ class CallbackQuery(TelegramObject):
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,
message_id=update.callback_query.message.message_id,
*args, **kwargs)
update.callback_query.message.edit_text(text, *args, **kwargs)
or::
@@ -143,20 +144,17 @@ class CallbackQuery(TelegramObject):
"""
if self.inline_message_id:
return self.bot.edit_message_text(text, inline_message_id=self.inline_message_id,
*args, **kwargs)
else:
return self.bot.edit_message_text(text, chat_id=self.message.chat_id,
message_id=self.message.message_id, *args, **kwargs)
return self.bot.edit_message_text(
text, inline_message_id=self.inline_message_id, *args, **kwargs
)
return self.message.edit_text(text, *args, **kwargs)
def edit_message_caption(self, caption: str, *args: Any,
**kwargs: Any) -> Union[Message, bool]:
def edit_message_caption(
self, caption: str, *args: Any, **kwargs: Any
) -> Union[Message, bool]:
"""Shortcut for either::
bot.edit_message_caption(caption=caption,
chat_id=update.callback_query.message.chat_id,
message_id=update.callback_query.message.message_id,
*args, **kwargs)
update.callback_query.message.edit_caption(caption, *args, **kwargs)
or::
@@ -170,28 +168,30 @@ class CallbackQuery(TelegramObject):
"""
if self.inline_message_id:
return self.bot.edit_message_caption(caption=caption,
inline_message_id=self.inline_message_id,
*args, **kwargs)
else:
return self.bot.edit_message_caption(caption=caption, chat_id=self.message.chat_id,
message_id=self.message.message_id,
*args, **kwargs)
return self.bot.edit_message_caption(
caption=caption, inline_message_id=self.inline_message_id, *args, **kwargs
)
return self.message.edit_caption(caption=caption, *args, **kwargs)
def edit_message_reply_markup(self, reply_markup: 'InlineKeyboardMarkup', *args: Any,
**kwargs: Any) -> Union[Message, bool]:
def edit_message_reply_markup(
self, reply_markup: 'InlineKeyboardMarkup', *args: Any, **kwargs: Any
) -> Union[Message, bool]:
"""Shortcut for either::
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)
update.callback_query.message.edit_reply_markup(
reply_markup=reply_markup,
*args,
**kwargs
)
or::
bot.edit_message_reply_markup(inline_message_id=update.callback_query.inline_message_id,
reply_markup=reply_markup,
*args, **kwargs)
bot.edit_message_reply_markup
inline_message_id=update.callback_query.inline_message_id,
reply_markup=reply_markup,
*args,
**kwargs
)
Returns:
:class:`telegram.Message`: On success, if edited message is sent by the bot, the
@@ -199,27 +199,22 @@ class CallbackQuery(TelegramObject):
"""
if self.inline_message_id:
return self.bot.edit_message_reply_markup(reply_markup=reply_markup,
inline_message_id=self.inline_message_id,
*args, **kwargs)
else:
return self.bot.edit_message_reply_markup(reply_markup=reply_markup,
chat_id=self.message.chat_id,
message_id=self.message.message_id,
*args, **kwargs)
return self.bot.edit_message_reply_markup(
reply_markup=reply_markup,
inline_message_id=self.inline_message_id,
*args,
**kwargs,
)
return self.message.edit_reply_markup(reply_markup=reply_markup, *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)
update.callback_query.message.edit_media(*args, **kwargs)
or::
bot.edit_message_media(inline_message_id=update.callback_query.inline_message_id,
media=media,
*args, **kwargs)
Returns:
@@ -228,26 +223,20 @@ class CallbackQuery(TelegramObject):
"""
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)
return self.bot.edit_message_media(
inline_message_id=self.inline_message_id, *args, **kwargs
)
return self.message.edit_media(*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)
update.callback_query.message.edit_live_location(*args, **kwargs)
or::
bot.edit_message_live_location(
inline_message_id=update.callback_query.inline_message_id,
reply_markup=reply_markup,
*args, **kwargs
)
@@ -257,26 +246,20 @@ class CallbackQuery(TelegramObject):
"""
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)
return self.bot.edit_message_live_location(
inline_message_id=self.inline_message_id, *args, **kwargs
)
return self.message.edit_live_location(*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)
update.callback_query.message.stop_live_location(*args, **kwargs)
or::
bot.stop_message_live_location(
inline_message_id=update.callback_query.inline_message_id,
reply_markup=reply_markup,
*args, **kwargs
)
@@ -286,25 +269,19 @@ class CallbackQuery(TelegramObject):
"""
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)
return self.bot.stop_message_live_location(
inline_message_id=self.inline_message_id, *args, **kwargs
)
return self.message.stop_live_location(*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)
update.callback_query.message.set_game_score(*args, **kwargs)
or::
bot.set_game_score(inline_message_id=update.callback_query.inline_message_id,
reply_markup=reply_markup,
*args, **kwargs)
Returns:
@@ -313,25 +290,19 @@ class CallbackQuery(TelegramObject):
"""
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)
return self.bot.set_game_score(
inline_message_id=self.inline_message_id, *args, **kwargs
)
return self.message.set_game_score(*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)
update.callback_query.message.get_game_high_score(*args, **kwargs)
or::
bot.get_game_high_scores(inline_message_id=update.callback_query.inline_message_id,
reply_markup=reply_markup,
*args, **kwargs)
Returns:
@@ -339,9 +310,62 @@ class CallbackQuery(TelegramObject):
"""
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)
return self.bot.get_game_high_scores(
inline_message_id=self.inline_message_id, *args, **kwargs
)
return self.message.get_game_high_scores(*args, **kwargs)
def delete_message(self, *args: Any, **kwargs: Any) -> Union[Message, bool]:
"""Shortcut for::
update.callback_query.message.delete(*args, **kwargs)
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.message.delete(*args, **kwargs)
def pin_message(self, *args: Any, **kwargs: Any) -> bool:
"""Shortcut for::
bot.pin_chat_message(chat_id=message.chat_id,
message_id=message.message_id,
*args,
**kwargs)
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.message.pin(*args, **kwargs)
def unpin_message(self, *args: Any, **kwargs: Any) -> bool:
"""Shortcut for::
bot.unpin_chat_message(chat_id=message.chat_id,
message_id=message.message_id,
*args,
**kwargs)
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.message.unpin(*args, **kwargs)
def copy_message(self, chat_id: int, *args: Any, **kwargs: Any) -> 'MessageId':
"""Shortcut for::
update.callback_query.message.copy(
chat_id,
from_chat_id=update.message.chat_id,
message_id=update.message.message_id,
*args,
**kwargs)
Returns:
:class:`telegram.MessageId`: On success, returns the MessageId of the sent message.
"""
return self.message.copy(chat_id, *args, **kwargs)
+126 -38
View File
@@ -19,13 +19,16 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Chat."""
from telegram import TelegramObject, ChatPhoto
from .chatpermissions import ChatPermissions
from typing import TYPE_CHECKING, Any, List, Optional, ClassVar
from telegram import ChatPhoto, TelegramObject, constants
from telegram.utils.types import JSONDict
from typing import Any, Optional, List, TYPE_CHECKING
from .chatpermissions import ChatPermissions
from .chatlocation import ChatLocation
if TYPE_CHECKING:
from telegram import Bot, Message, ChatMember
from telegram import Bot, ChatMember, Message, MessageId
class Chat(TelegramObject):
@@ -42,10 +45,12 @@ class Chat(TelegramObject):
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.
bio (:obj:`str`): Optional. Bio of the other party in a private chat. Returned only in
:meth:`telegram.Bot.get_chat`.
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 :meth:`telegram.Bot.get_chat`.
pinned_message (:class:`telegram.Message`): Optional. The most recent pinned message
(by sending date). 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
@@ -54,6 +59,11 @@ class Chat(TelegramObject):
sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set.
can_set_sticker_set (:obj:`bool`): Optional. :obj:`True`, if the bot can change group the
sticker set.
linked_chat_id (:obj:`int`): Optional. Unique identifier for the linked chat, i.e. the
discussion group identifier for a channel and vice versa; for supergroups and channel
chats. Returned only in :meth:`telegram.Bot.get_chat`.
location (:class:`telegram.ChatLocation`): Optional. For supergroups, the location to which
the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`.
Args:
id (:obj:`int`): Unique identifier for this chat. This number may be greater than 32 bits
@@ -69,14 +79,16 @@ class Chat(TelegramObject):
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 :meth:`telegram.Bot.get_chat`.
bio (:obj:`str`, optional): Bio of the other party in a private chat. Returned only in
:meth:`telegram.Bot.get_chat`.
description (:obj:`str`, optional): Description, for groups, supergroups and channel chats.
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`.
pinned_message (:class:`telegram.Message`, optional): The most recent pinned message
(by sending date). 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
@@ -87,36 +99,46 @@ class Chat(TelegramObject):
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`.
linked_chat_id (:obj:`int`, optional): Unique identifier for the linked chat, i.e. the
discussion group identifier for a channel and vice versa; for supergroups and channel
chats. Returned only in :meth:`telegram.Bot.get_chat`.
location (:class:`telegram.ChatLocation`, optional): For supergroups, the location to which
the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
PRIVATE: str = 'private'
""":obj:`str`: 'private'"""
GROUP: str = 'group'
""":obj:`str`: 'group'"""
SUPERGROUP: str = 'supergroup'
""":obj:`str`: 'supergroup'"""
CHANNEL: str = 'channel'
""":obj:`str`: 'channel'"""
PRIVATE: ClassVar[str] = constants.CHAT_PRIVATE
""":const:`telegram.constants.CHAT_PRIVATE`"""
GROUP: ClassVar[str] = constants.CHAT_GROUP
""":const:`telegram.constants.CHAT_GROUP`"""
SUPERGROUP: ClassVar[str] = constants.CHAT_SUPERGROUP
""":const:`telegram.constants.CHAT_SUPERGROUP`"""
CHANNEL: ClassVar[str] = constants.CHAT_CHANNEL
""":const:`telegram.constants.CHAT_CHANNEL`"""
def __init__(self,
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):
def __init__(
self,
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,
bio: str = None,
linked_chat_id: int = None,
location: ChatLocation = None,
**_kwargs: Any,
):
# Required
self.id = int(id)
self.type = type
@@ -126,8 +148,9 @@ class Chat(TelegramObject):
self.first_name = first_name
self.last_name = last_name
# TODO: Remove (also from tests), when Telegram drops this completely
self.all_members_are_administrators = kwargs.get('all_members_are_administrators')
self.all_members_are_administrators = _kwargs.get('all_members_are_administrators')
self.photo = photo
self.bio = bio
self.description = description
self.invite_link = invite_link
self.pinned_message = pinned_message
@@ -135,6 +158,8 @@ class Chat(TelegramObject):
self.slow_mode_delay = slow_mode_delay
self.sticker_set_name = sticker_set_name
self.can_set_sticker_set = can_set_sticker_set
self.linked_chat_id = linked_chat_id
self.location = location
self.bot = bot
self._id_attrs = (self.id,)
@@ -144,7 +169,7 @@ class Chat(TelegramObject):
""":obj:`str`: Convenience property. If the chat has a :attr:`username`, returns a t.me
link of the chat."""
if self.username:
return "https://t.me/{}".format(self.username)
return f"https://t.me/{self.username}"
return None
@classmethod
@@ -155,9 +180,11 @@ class Chat(TelegramObject):
return None
data['photo'] = ChatPhoto.de_json(data.get('photo'), bot)
from telegram import Message
from telegram import Message # pylint: disable=C0415
data['pinned_message'] = Message.de_json(data.get('pinned_message'), bot)
data['permissions'] = ChatPermissions.de_json(data.get('permissions'), bot)
data['location'] = ChatLocation.de_json(data.get('location'), bot)
return cls(bot=bot, **data)
@@ -243,7 +270,7 @@ class Chat(TelegramObject):
Returns:
: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: Any, **kwargs: Any) -> bool:
@@ -254,9 +281,48 @@ class Chat(TelegramObject):
Returns:
:obj:`bool`: If the action was sent successfully.
"""
"""
return self.bot.set_chat_administrator_custom_title(self.id, *args, **kwargs)
def pin_message(self, *args: Any, **kwargs: Any) -> bool:
"""Shortcut for::
bot.pin_chat_message(chat_id=update.effective_chat.id,
*args,
**kwargs)
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.pin_chat_message(self.id, *args, **kwargs)
def unpin_message(self, *args: Any, **kwargs: Any) -> bool:
"""Shortcut for::
bot.unpin_chat_message(chat_id=update.effective_chat.id,
*args,
**kwargs)
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.unpin_chat_message(self.id, *args, **kwargs)
def unpin_all_messages(self, *args: Any, **kwargs: Any) -> bool:
"""Shortcut for::
bot.unpin_all_chat_messages(chat_id=update.effective_chat.id,
*args,
**kwargs)
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return self.bot.unpin_all_chat_messages(chat_id=self.id, *args, **kwargs)
def send_message(self, *args: Any, **kwargs: Any) -> 'Message':
"""Shortcut for::
@@ -457,3 +523,25 @@ class Chat(TelegramObject):
"""
return self.bot.send_poll(self.id, *args, **kwargs)
def send_copy(self, *args: Any, **kwargs: Any) -> 'MessageId':
"""Shortcut for::
bot.copy_message(chat_id=update.effective_chat.id, *args, **kwargs)
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
return self.bot.copy_message(chat_id=self.id, *args, **kwargs)
def copy_message(self, *args: Any, **kwargs: Any) -> 'MessageId':
"""Shortcut for::
bot.copy_message(from_chat_id=update.effective_chat.id, *args, **kwargs)
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
return self.bot.copy_message(from_chat_id=self.id, *args, **kwargs)
+22 -20
View File
@@ -18,28 +18,30 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ChatAction."""
from typing import ClassVar
from telegram import constants
class ChatAction:
"""Helper class to provide constants for different chat actions."""
FIND_LOCATION: str = 'find_location'
""":obj:`str`: 'find_location'"""
RECORD_AUDIO: str = 'record_audio'
""":obj:`str`: 'record_audio'"""
RECORD_VIDEO: str = 'record_video'
""":obj:`str`: 'record_video'"""
RECORD_VIDEO_NOTE: str = 'record_video_note'
""":obj:`str`: 'record_video_note'"""
TYPING: str = 'typing'
""":obj:`str`: 'typing'"""
UPLOAD_AUDIO: str = 'upload_audio'
""":obj:`str`: 'upload_audio'"""
UPLOAD_DOCUMENT: str = 'upload_document'
""":obj:`str`: 'upload_document'"""
UPLOAD_PHOTO: str = 'upload_photo'
""":obj:`str`: 'upload_photo'"""
UPLOAD_VIDEO: str = 'upload_video'
""":obj:`str`: 'upload_video'"""
UPLOAD_VIDEO_NOTE: str = 'upload_video_note'
""":obj:`str`: 'upload_video_note'"""
FIND_LOCATION: ClassVar[str] = constants.CHATACTION_FIND_LOCATION
""":const:`telegram.constants.CHATACTION_FIND_LOCATION`"""
RECORD_AUDIO: ClassVar[str] = constants.CHATACTION_RECORD_AUDIO
""":const:`telegram.constants.CHATACTION_RECORD_AUDIO`"""
RECORD_VIDEO: ClassVar[str] = constants.CHATACTION_RECORD_VIDEO
""":const:`telegram.constants.CHATACTION_RECORD_VIDEO`"""
RECORD_VIDEO_NOTE: ClassVar[str] = constants.CHATACTION_RECORD_VIDEO_NOTE
""":const:`telegram.constants.CHATACTION_RECORD_VIDEO_NOTE`"""
TYPING: ClassVar[str] = constants.CHATACTION_TYPING
""":const:`telegram.constants.CHATACTION_TYPING`"""
UPLOAD_AUDIO: ClassVar[str] = constants.CHATACTION_UPLOAD_AUDIO
""":const:`telegram.constants.CHATACTION_UPLOAD_AUDIO`"""
UPLOAD_DOCUMENT: ClassVar[str] = constants.CHATACTION_UPLOAD_DOCUMENT
""":const:`telegram.constants.CHATACTION_UPLOAD_DOCUMENT`"""
UPLOAD_PHOTO: ClassVar[str] = constants.CHATACTION_UPLOAD_PHOTO
""":const:`telegram.constants.CHATACTION_UPLOAD_PHOTO`"""
UPLOAD_VIDEO: ClassVar[str] = constants.CHATACTION_UPLOAD_VIDEO
""":const:`telegram.constants.CHATACTION_UPLOAD_VIDEO`"""
UPLOAD_VIDEO_NOTE: ClassVar[str] = constants.CHATACTION_UPLOAD_VIDEO_NOTE
""":const:`telegram.constants.CHATACTION_UPLOAD_VIDEO_NOTE`"""
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python
#
# 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 location to which a chat is connected."""
from typing import TYPE_CHECKING, Any, Optional
from telegram import TelegramObject
from telegram.utils.types import JSONDict
from .files.location import Location
if TYPE_CHECKING:
from telegram import Bot
class ChatLocation(TelegramObject):
"""This object represents a location to which a chat is connected.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`location` is equal.
Attributes:
location (:class:`telegram.Location`): The location to which the supergroup is connected.
address (:obj:`str`): Location address, as defined by the chat owner
Args:
location (:class:`telegram.Location`): The location to which the supergroup is connected.
Can't be a live location.
address (:obj:`str`): Location address; 1-64 characters, as defined by the chat owner
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
def __init__(
self,
location: Location,
address: str,
**_kwargs: Any,
):
self.location = location
self.address = address
self._id_attrs = (self.location,)
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['ChatLocation']:
data = cls.parse_data(data)
if not data:
return None
data['location'] = Location.de_json(data.get('location'), bot)
return cls(bot=bot, **data)
+48 -37
View File
@@ -18,12 +18,12 @@
# 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 typing import TYPE_CHECKING, Any, Optional, ClassVar
from telegram import User, TelegramObject
from telegram.utils.helpers import to_timestamp, from_timestamp
from telegram import TelegramObject, User, constants
from telegram.utils.helpers import from_timestamp, to_timestamp
from telegram.utils.types import JSONDict
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot
@@ -38,6 +38,8 @@ class ChatMember(TelegramObject):
user (:class:`telegram.User`): Information about the user.
status (:obj:`str`): The member's status in the chat.
custom_title (:obj:`str`): Optional. Custom title for owner and administrators.
is_anonymous (:obj:`bool`): Optional. :obj:`True`, if the user's presence in the chat is
hidden.
until_date (:class:`datetime.datetime`): Optional. Date when restrictions will be lifted
for this user.
can_be_edited (:obj:`bool`): Optional. If the bot is allowed to edit administrator
@@ -74,6 +76,8 @@ class ChatMember(TelegramObject):
'member', 'restricted', 'left' or 'kicked'.
custom_title (:obj:`str`, optional): Owner and administrators only.
Custom title for this user.
is_anonymous (:obj:`bool`, optional): Owner and administrators only. :obj:`True`, if the
user's presence in the chat is hidden.
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. :obj:`True`, if the bot is
@@ -110,44 +114,51 @@ class ChatMember(TelegramObject):
may add web page previews to his messages.
"""
ADMINISTRATOR: str = 'administrator'
""":obj:`str`: 'administrator'"""
CREATOR: str = 'creator'
""":obj:`str`: 'creator'"""
KICKED: str = 'kicked'
""":obj:`str`: 'kicked'"""
LEFT: str = 'left'
""":obj:`str`: 'left'"""
MEMBER: str = 'member'
""":obj:`str`: 'member'"""
RESTRICTED: str = 'restricted'
""":obj:`str`: 'restricted'"""
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):
ADMINISTRATOR: ClassVar[str] = constants.CHATMEMBER_ADMINISTRATOR
""":const:`telegram.constants.CHATMEMBER_ADMINISTRATOR`"""
CREATOR: ClassVar[str] = constants.CHATMEMBER_CREATOR
""":const:`telegram.constants.CHATMEMBER_CREATOR`"""
KICKED: ClassVar[str] = constants.CHATMEMBER_KICKED
""":const:`telegram.constants.CHATMEMBER_KICKED`"""
LEFT: ClassVar[str] = constants.CHATMEMBER_LEFT
""":const:`telegram.constants.CHATMEMBER_LEFT`"""
MEMBER: ClassVar[str] = constants.CHATMEMBER_MEMBER
""":const:`telegram.constants.CHATMEMBER_MEMBER`"""
RESTRICTED: ClassVar[str] = constants.CHATMEMBER_RESTRICTED
""":const:`telegram.constants.CHATMEMBER_RESTRICTED`"""
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,
is_anonymous: bool = None,
**_kwargs: Any,
):
# Required
self.user = user
self.status = status
# Optionals
self.custom_title = custom_title
self.is_anonymous = is_anonymous
self.until_date = until_date
self.can_be_edited = can_be_edited
self.can_change_info = can_change_info
+15 -12
View File
@@ -18,9 +18,10 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ChatPermission."""
from telegram import TelegramObject
from typing import Any
from telegram import TelegramObject
class ChatPermissions(TelegramObject):
"""Describes actions that a non-administrator user is allowed to take in a chat.
@@ -77,16 +78,18 @@ class ChatPermissions(TelegramObject):
"""
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):
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
@@ -105,5 +108,5 @@ class ChatPermissions(TelegramObject):
self.can_add_web_page_previews,
self.can_change_info,
self.can_invite_users,
self.can_pin_messages
self.can_pin_messages,
)
+14 -10
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# pylint: disable=R0902,R0912,R0913
# pylint: disable=R0902,R0913
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2020
@@ -19,9 +19,11 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ChosenInlineResult."""
from telegram import TelegramObject, User, Location
from typing import TYPE_CHECKING, Any, Optional
from telegram import Location, TelegramObject, User
from telegram.utils.types import JSONDict
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot
@@ -61,13 +63,15 @@ class ChosenInlineResult(TelegramObject):
"""
def __init__(self,
result_id: str,
from_user: User,
query: str,
location: Location = None,
inline_message_id: str = None,
**kwargs: Any):
def __init__(
self,
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
+179 -3
View File
@@ -38,21 +38,197 @@ The following constant have been found by experimentation:
Attributes:
MAX_MESSAGE_ENTITIES (:obj:`int`): 100 (Beyond this cap telegram will simply ignore further
formatting styles)
ANONYMOUS_ADMIN_ID (:obj:`int`): ``1087968824`` (User id in groups for anonymous admin)
SERVICE_CHAT_ID (:obj:`int`): ``777000`` (Telegram service chat, that also acts as sender of
channel posts forwarded to discussion groups)
The following constants are related to specific classes and are also available
as attributes of those classes:
:class:`telegram.Chat`:
Attributes:
CHAT_PRIVATE (:obj:`str`): 'private'
CHAT_GROUP (:obj:`str`): 'group'
CHAT_SUPERGROUP (:obj:`str`): 'supergroup'
CHAT_CHANNEL (:obj:`str`): 'channel'
:class:`telegram.ChatAction`:
Attributes:
CHATACTION_FIND_LOCATION (:obj:`str`): 'find_location'
CHATACTION_RECORD_AUDIO (:obj:`str`): 'record_audio'
CHATACTION_RECORD_VIDEO (:obj:`str`): 'record_video'
CHATACTION_RECORD_VIDEO_NOTE (:obj:`str`): 'record_video_note'
CHATACTION_TYPING (:obj:`str`): 'typing'
CHATACTION_UPLOAD_AUDIO (:obj:`str`): 'upload_audio'
CHATACTION_UPLOAD_DOCUMENT (:obj:`str`): 'upload_document'
CHATACTION_UPLOAD_PHOTO (:obj:`str`): 'upload_photo'
CHATACTION_UPLOAD_VIDEO (:obj:`str`): 'upload_video'
CHATACTION_UPLOAD_VIDEO_NOTE (:obj:`str`): 'upload_video_note'
:class:`telegram.ChatMember`:
Attributes:
CHATMEMBER_ADMINISTRATOR (:obj:`str`): 'administrator'
CHATMEMBER_CREATOR (:obj:`str`): 'creator'
CHATMEMBER_KICKED (:obj:`str`): 'kicked'
CHATMEMBER_LEFT (:obj:`str`): 'left'
CHATMEMBER_MEMBER (:obj:`str`): 'member'
CHATMEMBER_RESTRICTED (:obj:`str`): 'restricted'
:class:`telegram.Dice`:
Attributes:
DICE_DICE (:obj:`str`): '🎲'
DICE_DARTS (:obj:`str`): '🎯'
DICE_BASKETBALL (:obj:`str`): '🏀'
DICE_FOOTBALL (:obj:`str`): ''
DICE_SLOT_MACHINE (:obj:`str`): '🎰'
DICE_ALL_EMOJI (List[:obj:`str`]): List of all supported base emoji.
:class:`telegram.MessageEntity`:
Attributes:
MESSAGEENTITY_MENTION (:obj:`str`): 'mention'
MESSAGEENTITY_HASHTAG (:obj:`str`): 'hashtag'
MESSAGEENTITY_CASHTAG (:obj:`str`): 'cashtag'
MESSAGEENTITY_PHONE_NUMBER (:obj:`str`): 'phone_number'
MESSAGEENTITY_BOT_COMMAND (:obj:`str`): 'bot_command'
MESSAGEENTITY_URL (:obj:`str`): 'url'
MESSAGEENTITY_EMAIL (:obj:`str`): 'email'
MESSAGEENTITY_BOLD (:obj:`str`): 'bold'
MESSAGEENTITY_ITALIC (:obj:`str`): 'italic'
MESSAGEENTITY_CODE (:obj:`str`): 'code'
MESSAGEENTITY_PRE (:obj:`str`): 'pre'
MESSAGEENTITY_TEXT_LINK (:obj:`str`): 'text_link'
MESSAGEENTITY_TEXT_MENTION (:obj:`str`): 'text_mention'
MESSAGEENTITY_UNDERLINE (:obj:`str`): 'underline'
MESSAGEENTITY_STRIKETHROUGH (:obj:`str`): 'strikethrough'
MESSAGEENTITY_ALL_TYPES (List[:obj:`str`]): List of all the types of message entity.
:class:`telegram.ParseMode`:
Attributes:
PARSEMODE_MARKDOWN (:obj:`str`): 'Markdown'
PARSEMODE_MARKDOWN_V2 (:obj:`str`): 'MarkdownV2'
PARSEMODE_HTML (:obj:`str`): 'HTML'
:class:`telegram.Poll`:
Attributes:
POLL_REGULAR (:obj:`str`): 'regular'
POLL_QUIZ (:obj:`str`): 'quiz'
MAX_POLL_QUESTION_LENGTH (:obj:`int`): 300
MAX_POLL_OPTION_LENGTH (:obj:`int`): 100
:class:`telegram.files.MaskPosition`:
Attributes:
STICKER_FOREHEAD (:obj:`str`): 'forehead'
STICKER_EYES (:obj:`str`): 'eyes'
STICKER_MOUTH (:obj:`str`): 'mouth'
STICKER_CHIN (:obj:`str`): 'chin'
"""
from typing import List
MAX_MESSAGE_LENGTH: int = 4096
MAX_CAPTION_LENGTH: int = 1024
ANONYMOUS_ADMIN_ID: int = 1087968824
SERVICE_CHAT_ID: int = 777000
# constants above this line are tested
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_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
CHAT_PRIVATE: str = 'private'
CHAT_GROUP: str = 'group'
CHAT_SUPERGROUP: str = 'supergroup'
CHAT_CHANNEL: str = 'channel'
CHATACTION_FIND_LOCATION: str = 'find_location'
CHATACTION_RECORD_AUDIO: str = 'record_audio'
CHATACTION_RECORD_VIDEO: str = 'record_video'
CHATACTION_RECORD_VIDEO_NOTE: str = 'record_video_note'
CHATACTION_TYPING: str = 'typing'
CHATACTION_UPLOAD_AUDIO: str = 'upload_audio'
CHATACTION_UPLOAD_DOCUMENT: str = 'upload_document'
CHATACTION_UPLOAD_PHOTO: str = 'upload_photo'
CHATACTION_UPLOAD_VIDEO: str = 'upload_video'
CHATACTION_UPLOAD_VIDEO_NOTE: str = 'upload_video_note'
CHATMEMBER_ADMINISTRATOR: str = 'administrator'
CHATMEMBER_CREATOR: str = 'creator'
CHATMEMBER_KICKED: str = 'kicked'
CHATMEMBER_LEFT: str = 'left'
CHATMEMBER_MEMBER: str = 'member'
CHATMEMBER_RESTRICTED: str = 'restricted'
DICE_DICE: str = '🎲'
DICE_DARTS: str = '🎯'
DICE_BASKETBALL: str = '🏀'
DICE_FOOTBALL: str = ''
DICE_SLOT_MACHINE: str = '🎰'
DICE_ALL_EMOJI: List[str] = [
DICE_DICE,
DICE_DARTS,
DICE_BASKETBALL,
DICE_FOOTBALL,
DICE_SLOT_MACHINE,
]
MESSAGEENTITY_MENTION: str = 'mention'
MESSAGEENTITY_HASHTAG: str = 'hashtag'
MESSAGEENTITY_CASHTAG: str = 'cashtag'
MESSAGEENTITY_PHONE_NUMBER: str = 'phone_number'
MESSAGEENTITY_BOT_COMMAND: str = 'bot_command'
MESSAGEENTITY_URL: str = 'url'
MESSAGEENTITY_EMAIL: str = 'email'
MESSAGEENTITY_BOLD: str = 'bold'
MESSAGEENTITY_ITALIC: str = 'italic'
MESSAGEENTITY_CODE: str = 'code'
MESSAGEENTITY_PRE: str = 'pre'
MESSAGEENTITY_TEXT_LINK: str = 'text_link'
MESSAGEENTITY_TEXT_MENTION: str = 'text_mention'
MESSAGEENTITY_UNDERLINE: str = 'underline'
MESSAGEENTITY_STRIKETHROUGH: str = 'strikethrough'
MESSAGEENTITY_ALL_TYPES: List[str] = [
MESSAGEENTITY_MENTION,
MESSAGEENTITY_HASHTAG,
MESSAGEENTITY_CASHTAG,
MESSAGEENTITY_PHONE_NUMBER,
MESSAGEENTITY_BOT_COMMAND,
MESSAGEENTITY_URL,
MESSAGEENTITY_EMAIL,
MESSAGEENTITY_BOLD,
MESSAGEENTITY_ITALIC,
MESSAGEENTITY_CODE,
MESSAGEENTITY_PRE,
MESSAGEENTITY_TEXT_LINK,
MESSAGEENTITY_TEXT_MENTION,
MESSAGEENTITY_UNDERLINE,
MESSAGEENTITY_STRIKETHROUGH,
]
PARSEMODE_MARKDOWN: str = 'Markdown'
PARSEMODE_MARKDOWN_V2: str = 'MarkdownV2'
PARSEMODE_HTML: str = 'HTML'
POLL_REGULAR: str = 'regular'
POLL_QUIZ: str = 'quiz'
MAX_POLL_QUESTION_LENGTH: int = 300
MAX_POLL_OPTION_LENGTH: int = 100
STICKER_FOREHEAD: str = 'forehead'
STICKER_EYES: str = 'eyes'
STICKER_MOUTH: str = 'mouth'
STICKER_CHIN: str = 'chin'
+27 -13
View File
@@ -18,8 +18,9 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Dice."""
from telegram import TelegramObject
from typing import Any, List
from typing import Any, List, ClassVar
from telegram import TelegramObject, constants
class Dice(TelegramObject):
@@ -40,26 +41,39 @@ class Dice(TelegramObject):
3 indicates that the basket was missed. However, this behaviour is undocumented and might
be changed by Telegram.
If :attr:`emoji` is "", a value of 3 to 5 currently scores a goal, while a value of 1 to
3 indicates that the goal was missed. However, this behaviour is undocumented and might
be changed by Telegram.
If :attr:`emoji` is "🎰", each value corresponds to a unique combination of symbols, which
can be found at our `wiki <https://git.io/JkeC6>`_. 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.
value (:obj:`int`): Value of the dice. 1-6 for dice and darts, 1-5 for basketball and
football/soccer ball, 1-64 for slot machine.
emoji (:obj:`str`): Emoji on which the dice throw animation is based.
"""
def __init__(self, value: int, emoji: str, **kwargs: Any):
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`."""
DICE: ClassVar[str] = constants.DICE_DICE
""":const:`telegram.constants.DICE_DICE`"""
DARTS: ClassVar[str] = constants.DICE_DARTS
""":const:`telegram.constants.DICE_DARTS`"""
BASKETBALL: ClassVar[str] = constants.DICE_BASKETBALL
""":const:`telegram.constants.DICE_BASKETBALL`"""
FOOTBALL: ClassVar[str] = constants.DICE_FOOTBALL
""":const:`telegram.constants.DICE_FOOTBALL`"""
SLOT_MACHINE: ClassVar[str] = constants.DICE_SLOT_MACHINE
""":const:`telegram.constants.DICE_SLOT_MACHINE`"""
ALL_EMOJI: ClassVar[List[str]] = constants.DICE_ALL_EMOJI
""":const:`telegram.constants.DICE_ALL_EMOJI`"""
+7 -9
View File
@@ -16,6 +16,7 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
# pylint: disable=C0115
"""This module contains an object that represents Telegram errors."""
from typing import Tuple
@@ -31,7 +32,7 @@ def _lstrip_str(in_s: str, lstr: str) -> str:
"""
if in_s.startswith(lstr):
res = in_s[len(lstr):]
res = in_s[len(lstr) :]
else:
res = in_s
return res
@@ -92,7 +93,7 @@ class ChatMigrated(TelegramError):
"""
def __init__(self, new_chat_id: int):
super().__init__('Group migrated to supergroup. New chat id: {}'.format(new_chat_id))
super().__init__(f'Group migrated to supergroup. New chat id: {new_chat_id}')
self.new_chat_id = new_chat_id
def __reduce__(self) -> Tuple[type, Tuple[int]]: # type: ignore[override]
@@ -107,7 +108,7 @@ class RetryAfter(TelegramError):
"""
def __init__(self, retry_after: int):
super().__init__('Flood control exceeded. Retry in {} seconds'.format(float(retry_after)))
super().__init__(f'Flood control exceeded. Retry in {float(retry_after)} seconds')
self.retry_after = float(retry_after)
def __reduce__(self) -> Tuple[type, Tuple[float]]: # type: ignore[override]
@@ -116,15 +117,12 @@ class RetryAfter(TelegramError):
class Conflict(TelegramError):
"""
Raised when a long poll or webhook conflicts with another one.
Raised when a long poll or webhook conflicts with another one.
Args:
msg (:obj:`str`): The message from telegrams server.
Args:
msg (:obj:`str`): The message from telegrams server.
"""
def __init__(self, msg: str):
super().__init__(msg)
def __reduce__(self) -> Tuple[type, Tuple[str]]:
return self.__class__, (self.message,)
+35 -8
View File
@@ -45,11 +45,38 @@ from .pollanswerhandler import PollAnswerHandler
from .pollhandler import PollHandler
from .defaults import Defaults
__all__ = ('Dispatcher', 'JobQueue', 'Job', 'Updater', 'CallbackQueryHandler',
'ChosenInlineResultHandler', 'CommandHandler', 'Handler', 'InlineQueryHandler',
'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')
__all__ = (
'Dispatcher',
'JobQueue',
'Job',
'Updater',
'CallbackQueryHandler',
'ChosenInlineResultHandler',
'CommandHandler',
'Handler',
'InlineQueryHandler',
'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',
)
+125 -43
View File
@@ -17,14 +17,13 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the BasePersistence class."""
import warnings
from abc import ABC, abstractmethod
from collections import defaultdict
from copy import copy
from typing import Any, DefaultDict, Dict, Optional, Tuple, cast, ClassVar
from telegram import Bot
from typing import DefaultDict, Dict, Any, Tuple, Optional, cast
from telegram.utils.types import ConversationDict
@@ -73,7 +72,7 @@ class BasePersistence(ABC):
persistence class. Default is :obj:`True` .
"""
def __new__(cls, *args: Any, **kwargs: Any) -> 'BasePersistence':
def __new__(cls, *args: Any, **kwargs: Any) -> 'BasePersistence': # pylint: disable=W0613
instance = super().__new__(cls)
get_user_data = instance.get_user_data
get_chat_data = instance.get_chat_data
@@ -108,10 +107,12 @@ class BasePersistence(ABC):
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):
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
@@ -131,7 +132,7 @@ class BasePersistence(ABC):
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.
``__slot__`` attribute, excluding objects that can't be copied with `copy.copy`.
Args:
obj (:obj:`object`): The object
@@ -139,26 +140,67 @@ class BasePersistence(ABC):
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)
return cls._replace_bot(obj, {})
new_obj = copy(obj)
if isinstance(obj, (dict, defaultdict)):
@classmethod
def _replace_bot(cls, obj: object, memo: Dict[int, Any]) -> object: # pylint: disable=R0911
obj_id = id(obj)
if obj_id in memo:
return memo[obj_id]
if isinstance(obj, Bot):
memo[obj_id] = cls.REPLACED_BOT
return cls.REPLACED_BOT
if isinstance(obj, (list, set)):
# We copy the iterable here for thread safety, i.e. make sure the object we iterate
# over doesn't change its length during the iteration
temp_iterable = obj.copy()
new_iterable = obj.__class__(cls._replace_bot(item, memo) for item in temp_iterable)
memo[obj_id] = new_iterable
return new_iterable
if isinstance(obj, (tuple, frozenset)):
# tuples and frozensets are immutable so we don't need to worry about thread safety
new_immutable = obj.__class__(cls._replace_bot(item, memo) for item in obj)
memo[obj_id] = new_immutable
return new_immutable
try:
new_obj = copy(obj)
memo[obj_id] = new_obj
except Exception:
warnings.warn(
'BasePersistence.replace_bot does not handle objects that can not be copied. See '
'the docs of BasePersistence.replace_bot for more information.',
RuntimeWarning,
)
memo[obj_id] = obj
return obj
if isinstance(obj, dict):
# We handle dicts via copy(obj) so we don't have to make a
# difference between dict and defaultdict
new_obj = cast(dict, new_obj)
# We can't iterate over obj.items() due to thread safety, i.e. the dicts length may
# change during the iteration
temp_dict = new_obj.copy()
new_obj.clear()
for k, v in obj.items():
new_obj[cls.replace_bot(k)] = cls.replace_bot(v)
for k, val in temp_dict.items():
new_obj[cls._replace_bot(k, memo)] = cls._replace_bot(val, memo)
memo[obj_id] = new_obj
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))
setattr(new_obj, attr_name, cls._replace_bot(attr, memo))
memo[obj_id] = new_obj
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))))
setattr(
new_obj,
attr_name,
cls._replace_bot(cls._replace_bot(getattr(new_obj, attr_name), memo), memo),
)
memo[obj_id] = new_obj
return new_obj
return obj
@@ -168,7 +210,7 @@ class BasePersistence(ABC):
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.
``__slot__`` attribute, excluding objects that can't be copied with `copy.copy`.
Args:
obj (:obj:`object`): The object
@@ -176,34 +218,75 @@ class BasePersistence(ABC):
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)
return self._insert_bot(obj, {})
new_obj = copy(obj)
if isinstance(obj, (dict, defaultdict)):
def _insert_bot(self, obj: object, memo: Dict[int, Any]) -> object: # pylint: disable=R0911
obj_id = id(obj)
if obj_id in memo:
return memo[obj_id]
if isinstance(obj, Bot):
memo[obj_id] = self.bot
return self.bot
if isinstance(obj, str) and obj == self.REPLACED_BOT:
memo[obj_id] = self.bot
return self.bot
if isinstance(obj, (list, set)):
# We copy the iterable here for thread safety, i.e. make sure the object we iterate
# over doesn't change its length during the iteration
temp_iterable = obj.copy()
new_iterable = obj.__class__(self._insert_bot(item, memo) for item in temp_iterable)
memo[obj_id] = new_iterable
return new_iterable
if isinstance(obj, (tuple, frozenset)):
# tuples and frozensets are immutable so we don't need to worry about thread safety
new_immutable = obj.__class__(self._insert_bot(item, memo) for item in obj)
memo[obj_id] = new_immutable
return new_immutable
try:
new_obj = copy(obj)
except Exception:
warnings.warn(
'BasePersistence.insert_bot does not handle objects that can not be copied. See '
'the docs of BasePersistence.insert_bot for more information.',
RuntimeWarning,
)
memo[obj_id] = obj
return obj
if isinstance(obj, dict):
# We handle dicts via copy(obj) so we don't have to make a
# difference between dict and defaultdict
new_obj = cast(dict, new_obj)
# We can't iterate over obj.items() due to thread safety, i.e. the dicts length may
# change during the iteration
temp_dict = new_obj.copy()
new_obj.clear()
for k, v in obj.items():
new_obj[self.insert_bot(k)] = self.insert_bot(v)
for k, val in temp_dict.items():
new_obj[self._insert_bot(k, memo)] = self._insert_bot(val, memo)
memo[obj_id] = new_obj
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))
setattr(new_obj, attr_name, self._insert_bot(attr, memo))
memo[obj_id] = new_obj
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))))
setattr(
new_obj,
attr_name,
self._insert_bot(self._insert_bot(getattr(new_obj, attr_name), memo), memo),
)
memo[obj_id] = new_obj
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
""" "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)``.
@@ -213,7 +296,7 @@ class BasePersistence(ABC):
@abstractmethod
def get_chat_data(self) -> DefaultDict[int, Dict[Any, Any]]:
""""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
""" "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)``.
@@ -223,7 +306,7 @@ class BasePersistence(ABC):
@abstractmethod
def get_bot_data(self) -> Dict[Any, Any]:
""""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
""" "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
:obj:`dict`.
@@ -233,7 +316,7 @@ class BasePersistence(ABC):
@abstractmethod
def get_conversations(self, name: str) -> ConversationDict:
""""Will be called by :class:`telegram.ext.Dispatcher` when a
""" "Will be called by :class:`telegram.ext.Dispatcher` when a
:class:`telegram.ext.ConversationHandler` is added if
:attr:`telegram.ext.ConversationHandler.persistent` is :obj:`True`.
It should return the conversations for the handler with `name` or an empty :obj:`dict`
@@ -246,9 +329,9 @@ class BasePersistence(ABC):
"""
@abstractmethod
def update_conversation(self,
name: str, key: Tuple[int, ...],
new_state: Optional[object]) -> None:
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 storage of the new state in the persistence.
@@ -292,7 +375,6 @@ class BasePersistence(ABC):
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'
REPLACED_BOT: ClassVar[str] = 'bot_instance_replaced_by_ptb_persistence'
""":obj:`str`: Placeholder for :class:`telegram.Bot` instances replaced in saved data."""
+25 -17
View File
@@ -16,11 +16,13 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
# pylint: disable=R0201
"""This module contains the CallbackContext class."""
from queue import Queue
from typing import Dict, Any, Tuple, TYPE_CHECKING, Optional, Match, List, NoReturn, Union
from typing import TYPE_CHECKING, Any, Dict, List, Match, NoReturn, Optional, Tuple, Union
from telegram import Update
if TYPE_CHECKING:
from telegram import Bot
from telegram.ext import Dispatcher, Job, JobQueue
@@ -91,8 +93,9 @@ class CallbackContext:
dispatcher (:class:`telegram.ext.Dispatcher`):
"""
if not dispatcher.use_context:
raise ValueError('CallbackContext should not be used with a non context aware '
'dispatcher!')
raise ValueError(
'CallbackContext should not be used with a non context aware ' 'dispatcher!'
)
self._dispatcher = dispatcher
self._bot_data = dispatcher.bot_data
self._chat_data: Optional[Dict[Any, Any]] = None
@@ -115,8 +118,9 @@ class CallbackContext:
@bot_data.setter
def bot_data(self, value: Any) -> NoReturn:
raise AttributeError("You can not assign a new value to bot_data, see "
"https://git.io/fjxKe")
raise AttributeError(
"You can not assign a new value to bot_data, see " "https://git.io/fjxKe"
)
@property
def chat_data(self) -> Optional[Dict]:
@@ -124,8 +128,9 @@ class CallbackContext:
@chat_data.setter
def chat_data(self, value: Any) -> NoReturn:
raise AttributeError("You can not assign a new value to chat_data, see "
"https://git.io/fjxKe")
raise AttributeError(
"You can not assign a new value to chat_data, see " "https://git.io/fjxKe"
)
@property
def user_data(self) -> Optional[Dict]:
@@ -133,16 +138,19 @@ class CallbackContext:
@user_data.setter
def user_data(self, value: Any) -> NoReturn:
raise AttributeError("You can not assign a new value to user_data, see "
"https://git.io/fjxKe")
raise AttributeError(
"You can not assign a new value to user_data, see " "https://git.io/fjxKe"
)
@classmethod
def from_error(cls,
update: object,
error: Exception,
dispatcher: 'Dispatcher',
async_args: Union[List, Tuple] = None,
async_kwargs: Dict[str, Any] = None) -> 'CallbackContext':
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
@@ -158,9 +166,9 @@ class CallbackContext:
user = update.effective_user
if chat:
self._chat_data = dispatcher.chat_data[chat.id]
self._chat_data = dispatcher.chat_data[chat.id] # pylint: disable=W0212
if user:
self._user_data = dispatcher.user_data[user.id]
self._user_data = dispatcher.user_data[user.id] # pylint: disable=W0212
return self
@classmethod
+42 -24
View File
@@ -19,13 +19,24 @@
"""This module contains the CallbackQueryHandler class."""
import re
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Match,
Optional,
Pattern,
TypeVar,
Union,
cast,
)
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
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
from .handler import Handler
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
@@ -110,23 +121,26 @@ class CallbackQueryHandler(Handler):
"""
def __init__(self,
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):
def __init__(
self,
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: Union[bool, DefaultValue] = DEFAULT_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,
run_async=run_async)
run_async=run_async,
)
if isinstance(pattern, str):
pattern = re.compile(pattern)
@@ -155,10 +169,12 @@ class CallbackQueryHandler(Handler):
return True
return None
def collect_optional_args(self,
dispatcher: 'Dispatcher',
update: HandlerArg = None,
check_result: Union[bool, Match] = None) -> Dict[str, Any]:
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)
@@ -168,11 +184,13 @@ class CallbackQueryHandler(Handler):
optional_args['groupdict'] = check_result.groupdict()
return optional_args
def collect_additional_context(self,
context: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Union[bool, Match]) -> None:
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]
+4 -2
View File
@@ -18,11 +18,13 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the ChosenInlineResultHandler class."""
from typing import Optional, TypeVar, Union
from telegram import Update
from telegram.utils.types import HandlerArg
from .handler import Handler
from telegram.utils.types import HandlerArg
from typing import Optional, Union, TypeVar
RT = TypeVar('RT')
+103 -73
View File
@@ -19,15 +19,16 @@
"""This module contains the CommandHandler and PrefixHandler classes."""
import re
import warnings
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union
from telegram.ext import Filters, BaseFilter
from telegram import MessageEntity, Update
from telegram.ext import BaseFilter, Filters
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.utils.types import HandlerArg, SLT
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
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
@@ -49,9 +50,9 @@ class CommandHandler(Handler):
: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
https://core.telegram.org/bots#commands
command (:class:`telegram.utils.types.SLT[str]`):
The command or list of commands this handler should listen for.
Limitations are the same as described here https://core.telegram.org/bots#commands
callback (:obj:`callable`): The callback function for this handler.
filters (:class:`telegram.ext.BaseFilter`): Optional. Only allow updates with these
Filters.
@@ -83,9 +84,9 @@ class CommandHandler(Handler):
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
https://core.telegram.org/bots#commands
command (:class:`telegram.utils.types.SLT[str]`):
The command or list of commands this handler should listen for.
Limitations are the same as described here https://core.telegram.org/bots#commands
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
Callback signature for context based API:
@@ -130,24 +131,27 @@ class CommandHandler(Handler):
ValueError - when command is too long or has illegal chars.
"""
def __init__(self,
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):
def __init__(
self,
command: SLT[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: Union[bool, DefaultValue] = DEFAULT_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,
run_async=run_async)
run_async=run_async,
)
if isinstance(command, str):
self.command = [command.lower()]
@@ -163,17 +167,18 @@ class CommandHandler(Handler):
self.filters = Filters.update.messages
if allow_edited is not None:
warnings.warn('allow_edited is deprecated. See https://git.io/fxJuV for more info',
TelegramDeprecationWarning,
stacklevel=2)
warnings.warn(
'allow_edited is deprecated. See https://git.io/fxJuV for more info',
TelegramDeprecationWarning,
stacklevel=2,
)
if not allow_edited:
self.filters &= ~Filters.update.edited_message
self.pass_args = pass_args
def check_update(
self,
update: HandlerArg) -> Optional[Union[bool, Tuple[List[str],
Optional[Union[bool, Dict]]]]]:
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:
@@ -186,41 +191,48 @@ class CommandHandler(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.text and message.bot):
command = message.text[1:message.entities[0].length]
if (
message.entities
and message.entities[0].type == MessageEntity.BOT_COMMAND
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_parts = command.split('@')
command_parts.append(message.bot.username)
if not (command_parts[0].lower() in self.command
and command_parts[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)
if filter_result:
return args, filter_result
else:
return False
return False
return None
def collect_optional_args(
self,
dispatcher: 'Dispatcher',
update: HandlerArg = None,
check_result: Optional[Union[bool, Tuple[List[str],
Optional[bool]]]] = None) -> Dict[str, Any]:
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: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]]) -> None:
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):
@@ -257,9 +269,6 @@ class PrefixHandler(CommandHandler):
use ~``Filters.update.edited_message``.
Attributes:
prefix (:obj:`str` | List[:obj:`str`]): The prefix(es) that will precede :attr:`command`.
command (:obj:`str` | List[:obj:`str`]): The command or list of commands this handler
should listen for.
callback (:obj:`callable`): The callback function for this handler.
filters (:class:`telegram.ext.BaseFilter`): Optional. Only allow updates with these
Filters.
@@ -289,9 +298,10 @@ class PrefixHandler(CommandHandler):
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
should listen for.
prefix (:class:`telegram.utils.types.SLT[str]`):
The prefix(es) that will precede :attr:`command`.
command (:class:`telegram.utils.types.SLT[str]`):
The command or list of commands this handler should listen for.
callback (:obj:`callable`): The callback function for this handler. Will be called when
:attr:`check_update` has determined that an update should be processed by this handler.
Callback signature for context based API:
@@ -330,29 +340,36 @@ class PrefixHandler(CommandHandler):
"""
def __init__(self,
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):
def __init__(
self,
prefix: SLT[str],
command: SLT[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: Union[bool, DefaultValue] = DEFAULT_FALSE,
):
self._prefix: List[str] = list()
self._command: List[str] = list()
self._commands: List[str] = list()
super().__init__(
'nocommand', callback, filters=filters, allow_edited=None, pass_args=pass_args,
'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,
run_async=run_async)
run_async=run_async,
)
self.prefix = prefix # type: ignore[assignment]
self.command = command # type: ignore[assignment]
@@ -360,6 +377,12 @@ class PrefixHandler(CommandHandler):
@property
def prefix(self) -> List[str]:
"""
The prefixes that will precede :attr:`command`.
Returns:
List[:obj:`str`]
"""
return self._prefix
@prefix.setter
@@ -372,6 +395,12 @@ class PrefixHandler(CommandHandler):
@property # type: ignore[override]
def command(self) -> List[str]: # type: ignore[override]
"""
The list of commands this handler should listen for.
Returns:
List[:obj:`str`]
"""
return self._command
@command.setter
@@ -385,8 +414,9 @@ class PrefixHandler(CommandHandler):
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: HandlerArg) -> Optional[Union[bool, Tuple[List[str],
Optional[Union[bool, Dict]]]]]:
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:
@@ -406,16 +436,16 @@ class PrefixHandler(CommandHandler):
filter_result = self.filters(update)
if filter_result:
return text_list[1:], filter_result
else:
return False
return False
return None
def collect_additional_context(
self,
context: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]]) -> None:
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):
+110 -78
View File
@@ -16,20 +16,26 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
# pylint: disable=R0201
"""This module contains the ConversationHandler."""
import logging
import warnings
from threading import Lock
from typing import TYPE_CHECKING, Any, Dict, List, NoReturn, Optional, Tuple, cast, ClassVar
from telegram import Update
from telegram.ext import (Handler, CallbackQueryHandler, InlineQueryHandler,
ChosenInlineResultHandler, CallbackContext, BasePersistence,
DispatcherHandlerStop)
from telegram.ext import (
BasePersistence,
CallbackContext,
CallbackQueryHandler,
ChosenInlineResultHandler,
DispatcherHandlerStop,
Handler,
InlineQueryHandler,
)
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
if TYPE_CHECKING:
from telegram.ext import Dispatcher, Job
@@ -37,11 +43,13 @@ CheckUpdateType = Optional[Tuple[Tuple[int, ...], Handler, object]]
class _ConversationTimeoutContext:
def __init__(self,
conversation_key: Tuple[int, ...],
update: Update,
dispatcher: 'Dispatcher',
callback_context: Optional[CallbackContext]):
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
@@ -76,6 +84,8 @@ class ConversationHandler(Handler):
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``.
Finally, :class:`telegram.ext.DispatcherHandlerStop` can be used in conversations as described
in the corresponding documentation.
Note:
In each of the described collections of handlers, a handler may in turn be a
@@ -160,26 +170,29 @@ class ConversationHandler(Handler):
ValueError
"""
END = -1
END: ClassVar[int] = -1
""":obj:`int`: Used as a constant to return when a conversation is ended."""
TIMEOUT = -2
TIMEOUT: ClassVar[int] = -2
""":obj:`int`: Used as a constant to handle state when a conversation is timed out."""
WAITING = -3
WAITING: ClassVar[int] = -3
""":obj:`int`: Used as a constant to handle state when a conversation is still waiting on the
previous ``@run_sync`` decorated running handler to finish."""
def __init__(self,
entry_points: 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):
# pylint: disable=W0231
def __init__(
self,
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
@@ -211,8 +224,10 @@ class ConversationHandler(Handler):
raise ValueError("'per_user', 'per_chat' and 'per_message' can't all be 'False'")
if self.per_message and not self.per_chat:
warnings.warn("If 'per_message=True' is used, 'per_chat=True' should also be used, "
"since message IDs are not globally unique.")
warnings.warn(
"If 'per_message=True' is used, 'per_chat=True' should also be used, "
"since message IDs are not globally unique."
)
all_handlers = list()
all_handlers.extend(entry_points)
@@ -224,22 +239,28 @@ class ConversationHandler(Handler):
if self.per_message:
for handler in all_handlers:
if not isinstance(handler, CallbackQueryHandler):
warnings.warn("If 'per_message=True', all entry points and state handlers"
" must be 'CallbackQueryHandler', since no other handlers "
"have a message context.")
warnings.warn(
"If 'per_message=True', all entry points and state handlers"
" must be 'CallbackQueryHandler', since no other handlers "
"have a message context."
)
break
else:
for handler in all_handlers:
if isinstance(handler, CallbackQueryHandler):
warnings.warn("If 'per_message=False', 'CallbackQueryHandler' will not be "
"tracked for every message.")
warnings.warn(
"If 'per_message=False', 'CallbackQueryHandler' will not be "
"tracked for every message."
)
break
if self.per_chat:
for handler in all_handlers:
if isinstance(handler, (InlineQueryHandler, ChosenInlineResultHandler)):
warnings.warn("If 'per_chat=True', 'InlineQueryHandler' can not be used, "
"since inline queries have no chat context.")
warnings.warn(
"If 'per_chat=True', 'InlineQueryHandler' can not be used, "
"since inline queries have no chat context."
)
break
@property
@@ -304,8 +325,9 @@ class ConversationHandler(Handler):
@conversation_timeout.setter
def conversation_timeout(self, value: Any) -> NoReturn:
raise ValueError('You can not assign a new value to conversation_timeout after '
'initialization.')
raise ValueError(
'You can not assign a new value to conversation_timeout after ' 'initialization.'
)
@property
def name(self) -> Optional[str]:
@@ -362,12 +384,14 @@ class ConversationHandler(Handler):
key.append(user.id)
if self.per_message:
key.append(update.callback_query.inline_message_id # type: ignore[union-attr]
or update.callback_query.message.message_id) # type: ignore[union-attr]
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: HandlerArg) -> CheckUpdateType:
def check_update(self, update: HandlerArg) -> CheckUpdateType: # pylint: disable=R0911
"""
Determines whether an update should be handled by this conversationhandler, and if so in
which state the conversation currently is.
@@ -379,12 +403,16 @@ class ConversationHandler(Handler):
:obj:`bool`
"""
if not isinstance(update, Update):
return None
# Ignore messages in channels
if (not isinstance(update, Update)
or update.channel_post
or self.per_chat and not update.effective_chat
or self.per_message and not update.callback_query
or update.callback_query and self.per_chat and not update.callback_query.message):
if update.channel_post:
return None
if self.per_chat and not update.effective_chat:
return None
if self.per_message and not update.callback_query:
return None
if update.callback_query and self.per_chat and not update.callback_query.message:
return None
key = self._get_key(update)
@@ -402,7 +430,7 @@ class ConversationHandler(Handler):
res = res if res is not None else old_state
except Exception as exc:
self.logger.exception("Promise function raised exception")
self.logger.exception("{}".format(exc))
self.logger.exception("%s", exc)
res = old_state
finally:
if res is None and old_state is None:
@@ -418,7 +446,7 @@ class ConversationHandler(Handler):
return key, hdlr, check
return None
self.logger.debug('selecting conversation {} with state {}'.format(str(key), str(state)))
self.logger.debug('selecting conversation %s with state %s', str(key), str(state))
handler = None
@@ -438,7 +466,7 @@ class ConversationHandler(Handler):
if state is not None and not handler:
handlers = self.states.get(state)
for candidate in (handlers or []):
for candidate in handlers or []:
check = candidate.check_update(update)
if check is not None and check is not False:
handler = candidate
@@ -457,11 +485,13 @@ class ConversationHandler(Handler):
return key, handler, check # type: ignore[return-value]
def handle_update(self, # type: ignore[override]
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: CheckUpdateType,
context: CallbackContext = None) -> Optional[object]:
def handle_update( # type: ignore[override]
self,
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:
@@ -485,34 +515,34 @@ class ConversationHandler(Handler):
timeout_job.schedule_removal()
try:
new_state = handler.handle_update(update, dispatcher, check_result, context)
except DispatcherHandlerStop as e:
new_state = e.state
except DispatcherHandlerStop as exception:
new_state = exception.state
raise_dp_handler_stop = True
with self._timeout_jobs_lock:
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, # type: ignore[arg-type]
context=_ConversationTimeoutContext(conversation_key, update,
dispatcher, context))
self._trigger_timeout, # type: ignore[arg-type]
self.conversation_timeout,
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)
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 self.map_to_parent.get(new_state)
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: object,
key: Tuple[int, ...]) -> None:
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:
@@ -525,8 +555,9 @@ class ConversationHandler(Handler):
with self._conversations_lock:
self.conversations[key] = (self.conversations.get(key), new_state)
if self.persistent and self.persistence and self.name:
self.persistence.update_conversation(self.name, key,
(self.conversations.get(key), new_state))
self.persistence.update_conversation(
self.name, key, (self.conversations.get(key), new_state)
)
elif new_state is not None:
with self._conversations_lock:
@@ -534,9 +565,7 @@ class ConversationHandler(Handler):
if self.persistent and self.persistence and self.name:
self.persistence.update_conversation(self.name, key, new_state)
def _trigger_timeout(self,
context: _ConversationTimeoutContext,
job: 'Job' = None) -> 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
@@ -559,9 +588,12 @@ class ConversationHandler(Handler):
check = handler.check_update(context.update)
if check is not None and check is not False:
try:
handler.handle_update(context.update, context.dispatcher, check,
callback_context)
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.logger.warning(
'DispatcherHandlerStop in TIMEOUT state of '
'ConversationHandler has no effect. Ignoring.'
)
self.update_state(self.END, context.conversation_key)
+87 -28
View File
@@ -16,9 +16,11 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
# pylint: disable=R0201, E0401
"""This module contains the class Defaults, which allows to pass default values to Updater."""
from typing import Any, NoReturn, Optional, Union
import pytz
from typing import Union, Optional, Any, NoReturn
from telegram.utils.helpers import DEFAULT_NONE, DefaultValue
@@ -33,6 +35,8 @@ class Defaults:
receive a notification with no sound.
disable_web_page_preview (:obj:`bool`): Optional. Disables link previews for links in this
message.
allow_sending_without_reply (:obj:`bool`): Optional. Pass :obj:`True`, if the message
should be sent even if the specified replied-to message is not found.
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).
@@ -41,6 +45,9 @@ class Defaults:
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.
run_async (:obj:`bool`): Optional. Default setting for the ``run_async`` parameter of
handlers and error handlers registered through :meth:`Dispatcher.add_handler` and
:meth:`Dispatcher.add_error_handler`.
Parameters:
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
@@ -49,6 +56,8 @@ class Defaults:
receive a notification with no sound.
disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in this
message.
allow_sending_without_reply (:obj:`bool`, optional): Pass :obj:`True`, if the message
should be sent even if the specified replied-to message is not found.
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).
@@ -59,22 +68,32 @@ class Defaults:
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.
run_async (:obj:`bool`, optional): Default setting for the ``run_async`` parameter of
handlers and error handlers registered through :meth:`Dispatcher.add_handler` and
:meth:`Dispatcher.add_error_handler`. Defaults to :obj:`False`.
"""
def __init__(self,
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: Union[float, DefaultValue] = DEFAULT_NONE,
quote: bool = None,
tzinfo: pytz.BaseTzInfo = pytz.utc):
def __init__(
self,
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: Union[float, DefaultValue] = DEFAULT_NONE,
quote: bool = None,
tzinfo: pytz.BaseTzInfo = pytz.utc,
run_async: bool = False,
allow_sending_without_reply: bool = None,
):
self._parse_mode = parse_mode
self._disable_notification = disable_notification
self._disable_web_page_preview = disable_web_page_preview
self._allow_sending_without_reply = allow_sending_without_reply
self._timeout = timeout
self._quote = quote
self._tzinfo = tzinfo
self._run_async = run_async
@property
def parse_mode(self) -> Optional[str]:
@@ -82,8 +101,10 @@ class Defaults:
@parse_mode.setter
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.")
raise AttributeError(
"You can not assign a new value to defaults after because it would "
"not have any effect."
)
@property
def disable_notification(self) -> Optional[bool]:
@@ -91,8 +112,10 @@ class Defaults:
@disable_notification.setter
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.")
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) -> Optional[bool]:
@@ -100,8 +123,21 @@ class Defaults:
@disable_web_page_preview.setter
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.")
raise AttributeError(
"You can not assign a new value to defaults after because it would "
"not have any effect."
)
@property
def allow_sending_without_reply(self) -> Optional[bool]:
return self._allow_sending_without_reply
@allow_sending_without_reply.setter
def allow_sending_without_reply(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) -> Union[float, DefaultValue]:
@@ -109,8 +145,10 @@ class Defaults:
@timeout.setter
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.")
raise AttributeError(
"You can not assign a new value to defaults after because it would "
"not have any effect."
)
@property
def quote(self) -> Optional[bool]:
@@ -118,8 +156,10 @@ class Defaults:
@quote.setter
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.")
raise AttributeError(
"You can not assign a new value to defaults after because it would "
"not have any effect."
)
@property
def tzinfo(self) -> pytz.BaseTzInfo:
@@ -127,16 +167,35 @@ class Defaults:
@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.")
raise AttributeError(
"You can not assign a new value to defaults after because it would "
"not have any effect."
)
@property
def run_async(self) -> Optional[bool]:
return self._run_async
@run_async.setter
def run_async(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._tzinfo))
return hash(
(
self._parse_mode,
self._disable_notification,
self._disable_web_page_preview,
self._allow_sending_without_reply,
self._timeout,
self._quote,
self._tzinfo,
self._run_async,
)
)
def __eq__(self, other: object) -> bool:
if isinstance(other, Defaults):
+42 -37
View File
@@ -19,18 +19,21 @@
"""This module contains the DictPersistence class."""
from copy import deepcopy
from telegram.utils.helpers import decode_user_chat_data_from_json,\
decode_conversations_from_json, encode_conversations_to_json
from typing import Any, DefaultDict, Dict, Optional, Tuple
from collections import defaultdict
from telegram.utils.helpers import (
decode_conversations_from_json,
decode_user_chat_data_from_json,
encode_conversations_to_json,
)
from telegram.ext import BasePersistence
from telegram.utils.types import ConversationDict
try:
import ujson as json
except ImportError:
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):
@@ -76,17 +79,21 @@ class DictPersistence(BasePersistence):
conversation on creating this persistence. Default is ``""``.
"""
def __init__(self,
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)
def __init__(
self,
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
@@ -99,20 +106,20 @@ class DictPersistence(BasePersistence):
try:
self._user_data = decode_user_chat_data_from_json(user_data_json)
self._user_data_json = user_data_json
except (ValueError, AttributeError):
raise TypeError("Unable to deserialize user_data_json. Not valid JSON")
except (ValueError, AttributeError) as exc:
raise TypeError("Unable to deserialize user_data_json. Not valid JSON") from exc
if chat_data_json:
try:
self._chat_data = decode_user_chat_data_from_json(chat_data_json)
self._chat_data_json = chat_data_json
except (ValueError, AttributeError):
raise TypeError("Unable to deserialize chat_data_json. Not valid JSON")
except (ValueError, AttributeError) as exc:
raise TypeError("Unable to deserialize chat_data_json. Not valid JSON") from exc
if bot_data_json:
try:
self._bot_data = json.loads(bot_data_json)
self._bot_data_json = bot_data_json
except (ValueError, AttributeError):
raise TypeError("Unable to deserialize bot_data_json. Not valid JSON")
except (ValueError, AttributeError) as exc:
raise TypeError("Unable to deserialize bot_data_json. Not valid JSON") from exc
if not isinstance(self._bot_data, dict):
raise TypeError("bot_data_json must be serialized dict")
@@ -120,8 +127,10 @@ class DictPersistence(BasePersistence):
try:
self._conversations = decode_conversations_from_json(conversations_json)
self._conversations_json = conversations_json
except (ValueError, AttributeError):
raise TypeError("Unable to deserialize conversations_json. Not valid JSON")
except (ValueError, AttributeError) as exc:
raise TypeError(
"Unable to deserialize conversations_json. Not valid JSON"
) from exc
@property
def user_data(self) -> Optional[DefaultDict[int, Dict]]:
@@ -133,8 +142,7 @@ class DictPersistence(BasePersistence):
""":obj:`str`: The user_data serialized as a JSON-string."""
if self._user_data_json:
return self._user_data_json
else:
return json.dumps(self.user_data)
return json.dumps(self.user_data)
@property
def chat_data(self) -> Optional[DefaultDict[int, Dict]]:
@@ -146,8 +154,7 @@ class DictPersistence(BasePersistence):
""":obj:`str`: The chat_data serialized as a JSON-string."""
if self._chat_data_json:
return self._chat_data_json
else:
return json.dumps(self.chat_data)
return json.dumps(self.chat_data)
@property
def bot_data(self) -> Optional[Dict]:
@@ -159,8 +166,7 @@ class DictPersistence(BasePersistence):
""":obj:`str`: The bot_data serialized as a JSON-string."""
if self._bot_data_json:
return self._bot_data_json
else:
return json.dumps(self.bot_data)
return json.dumps(self.bot_data)
@property
def conversations(self) -> Optional[Dict[str, Dict[Tuple, Any]]]:
@@ -172,8 +178,7 @@ class DictPersistence(BasePersistence):
""":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) # type: ignore[arg-type]
return encode_conversations_to_json(self.conversations) # type: ignore[arg-type]
def get_user_data(self) -> DefaultDict[int, Dict[Any, Any]]:
"""Returns the user_data created from the ``user_data_json`` or an empty
@@ -226,9 +231,9 @@ class DictPersistence(BasePersistence):
self._conversations = {}
return self.conversations.get(name, {}).copy() # type: ignore[union-attr]
def update_conversation(self,
name: str, key: Tuple[int, ...],
new_state: Optional[object]) -> None:
def update_conversation(
self, name: str, key: Tuple[int, ...], new_state: Optional[object]
) -> None:
"""Will update the conversations for the given handler.
Args:
+109 -88
View File
@@ -21,34 +21,33 @@
import logging
import warnings
import weakref
from functools import wraps
from threading import Thread, Lock, Event, current_thread, BoundedSemaphore
from time import sleep
from uuid import uuid4
from collections import defaultdict
from queue import Queue, Empty
from functools import wraps
from queue import Empty, Queue
from threading import BoundedSemaphore, Event, Lock, Thread, current_thread
from time import sleep
from typing import TYPE_CHECKING, Any, Callable, DefaultDict, Dict, List, Optional, Set, Union
from uuid import uuid4
from telegram import TelegramError, Update
from telegram.ext.handler import Handler
from telegram.ext import BasePersistence
from telegram.ext.callbackcontext import CallbackContext
from telegram.ext.handler import Handler
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.utils.promise import Promise
from telegram.ext import BasePersistence
from typing import Any, Callable, TYPE_CHECKING, Optional, Union, DefaultDict, Dict, List, Set
from telegram.utils.types import HandlerArg
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
if TYPE_CHECKING:
from telegram import Bot
from telegram.ext import JobQueue
DEFAULT_GROUP = 0
DEFAULT_GROUP: int = 0
def run_async(func: Callable[[Update, CallbackContext],
Any]) -> Callable[[Update, CallbackContext], Any]:
def run_async(
func: Callable[[Update, CallbackContext], Any]
) -> Callable[[Update, CallbackContext], Any]:
"""
Function decorator that will run the function in a new thread.
@@ -67,12 +66,15 @@ def run_async(func: Callable[[Update, CallbackContext],
@wraps(func)
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)
warnings.warn(
'The @run_async decorator is deprecated. Use the `run_async` parameter of '
'your Handler or `Dispatcher.run_async` instead.',
TelegramDeprecationWarning,
stacklevel=2,
)
return Dispatcher.get_instance()._run_async( # pylint: disable=W0212
func, *args, update=None, error_handling=False, **kwargs
)
return async_func
@@ -96,6 +98,7 @@ class DispatcherHandlerStop(Exception):
Args:
state (:obj:`object`, optional): The next state of the conversation.
"""
def __init__(self, state: object = None) -> None:
super().__init__()
self.state = state
@@ -137,14 +140,16 @@ class Dispatcher:
__singleton = None
logger = logging.getLogger(__name__)
def __init__(self,
bot: 'Bot',
update_queue: Queue,
workers: int = 4,
exception_event: Event = None,
job_queue: 'JobQueue' = None,
persistence: BasePersistence = None,
use_context: bool = True):
def __init__(
self,
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
@@ -152,8 +157,11 @@ class Dispatcher:
self.use_context = use_context
if not use_context:
warnings.warn('Old Handler API is deprecated - see https://git.io/fxJuV for details',
TelegramDeprecationWarning, stacklevel=3)
warnings.warn(
'Old Handler API is deprecated - see https://git.io/fxJuV for details',
TelegramDeprecationWarning,
stacklevel=3,
)
self.user_data: DefaultDict[int, Dict[Any, Any]] = defaultdict(dict)
self.chat_data: DefaultDict[int, Dict[Any, Any]] = defaultdict(dict)
@@ -184,7 +192,7 @@ class Dispatcher:
"""Dict[:obj:`int`, List[:class:`telegram.ext.Handler`]]: Holds the handlers per group."""
self.groups: List[int] = []
"""List[:obj:`int`]: A list with all groups."""
self.error_handlers: Dict[Callable, bool] = {}
self.error_handlers: Dict[Callable, Union[bool, DefaultValue]] = {}
"""Dict[:obj:`callable`, :obj:`bool`]: A dict, where the keys are error handlers and the
values indicate whether they are to be run asynchronously."""
@@ -208,11 +216,10 @@ class Dispatcher:
return self.__exception_event
def _init_async_threads(self, base_name: str, workers: int) -> None:
base_name = '{}_'.format(base_name) if base_name else ''
base_name = f'{base_name}_' if base_name else ''
for i in range(workers):
thread = Thread(target=self._pooled, name='Bot:{}:worker:{}{}'.format(self.bot.id,
base_name, i))
thread = Thread(target=self._pooled, name=f'Bot:{self.bot.id}:worker:{base_name}{i}')
self.__async_threads.add(thread)
thread.start()
@@ -234,9 +241,7 @@ class Dispatcher:
"""
if cls.__singleton is not None:
return cls.__singleton() # type: ignore[return-value] # pylint: disable=not-callable
else:
raise RuntimeError('{} not initialized or multiple instances exist'.format(
cls.__name__))
raise RuntimeError(f'{cls.__name__} not initialized or multiple instances exist')
def _pooled(self) -> None:
thr_name = current_thread().getName()
@@ -245,8 +250,9 @@ class Dispatcher:
# If unpacking fails, the thread pool is being closed from Updater._join_async_threads
if not isinstance(promise, Promise):
self.logger.debug("Closing run_async thread %s/%d", thr_name,
len(self.__async_threads))
self.logger.debug(
"Closing run_async thread %s/%d", thr_name, len(self.__async_threads)
)
break
promise.run()
@@ -258,7 +264,8 @@ class Dispatcher:
if isinstance(promise.exception, DispatcherHandlerStop):
self.logger.warning(
'DispatcherHandlerStop is not supported with async functions; func: %s',
promise.pooled_function.__name__)
promise.pooled_function.__name__,
)
continue
# Avoid infinite recursion of error handlers.
@@ -280,11 +287,9 @@ class Dispatcher:
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:
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
@@ -310,12 +315,14 @@ class Dispatcher:
"""
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:
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)
@@ -357,12 +364,12 @@ class Dispatcher:
if self.__stop_event.is_set():
self.logger.debug('orderly stopping')
break
elif self.__exception_event.is_set():
if self.__exception_event.is_set():
self.logger.critical('stopping due to exception in another thread')
break
continue
self.logger.debug('Processing Update: %s' % update)
self.logger.debug('Processing Update: %s', update)
self.process_update(update)
self.update_queue.task_done()
@@ -387,10 +394,10 @@ class Dispatcher:
self.__async_queue.put(None)
for i, thr in enumerate(threads):
self.logger.debug('Waiting for async thread {}/{} to end'.format(i + 1, total))
self.logger.debug('Waiting for async thread %s/%s to end', i + 1, total)
thr.join()
self.__async_threads.remove(thr)
self.logger.debug('async thread {}/{} has ended'.format(i + 1, total))
self.logger.debug('async thread %s/%s has ended', i + 1, total)
@property
def has_running_threads(self) -> bool:
@@ -436,9 +443,9 @@ class Dispatcher:
break
# Dispatch any error.
except Exception as e:
except Exception as exc:
try:
self.dispatch_error(update, e)
self.dispatch_error(update, exc)
except DispatcherHandlerStop:
self.logger.debug('Error handler stopped further handlers')
break
@@ -472,17 +479,18 @@ class Dispatcher:
"""
# Unfortunately due to circular imports this has to be here
from .conversationhandler import ConversationHandler
from .conversationhandler import ConversationHandler # pylint: disable=C0415
if not isinstance(handler, Handler):
raise TypeError('handler is not an instance of {}'.format(Handler.__name__))
raise TypeError(f'handler is not an instance of {Handler.__name__}')
if not isinstance(group, int):
raise TypeError('group is not int')
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 "
"persistence".format(handler.name))
f"ConversationHandler {handler.name} can not be persistent if dispatcher has "
f"no persistence"
)
handler.persistence = self.persistence
handler.conversations = self.persistence.get_conversations(handler.name)
@@ -537,42 +545,50 @@ class Dispatcher:
if self.persistence.store_bot_data:
try:
self.persistence.update_bot_data(self.bot_data)
except Exception as e:
except Exception as exc:
try:
self.dispatch_error(update, e)
self.dispatch_error(update, exc)
except Exception:
message = 'Saving bot data raised an error and an ' \
'uncaught error was raised while handling ' \
'the error with an error_handler'
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:
except Exception as exc:
try:
self.dispatch_error(update, e)
self.dispatch_error(update, exc)
except Exception:
message = 'Saving chat data raised an error and an ' \
'uncaught error was raised while handling ' \
'the error with an error_handler'
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:
except Exception as exc:
try:
self.dispatch_error(update, e)
self.dispatch_error(update, exc)
except Exception:
message = 'Saving user data raised an error and an ' \
'uncaught error was raised while handling ' \
'the error with an error_handler'
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:
def add_error_handler(
self,
callback: Callable[[Any, CallbackContext], None],
run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, # pylint: disable=W0621
) -> None:
"""Registers an error handler in the Dispatcher. This handler will receive every error
which happens in your bot.
@@ -599,6 +615,11 @@ class Dispatcher:
if callback in self.error_handlers:
self.logger.debug('The callback is already registered as an error handler. Ignoring.')
return
if run_async is DEFAULT_FALSE and self.bot.defaults:
if self.bot.defaults.run_async:
run_async = True
self.error_handlers[callback] = run_async
def remove_error_handler(self, callback: Callable[[Any, CallbackContext], None]) -> None:
@@ -610,10 +631,9 @@ class Dispatcher:
"""
self.error_handlers.pop(callback, None)
def dispatch_error(self,
update: Optional[HandlerArg],
error: Exception,
promise: Promise = None) -> None:
def dispatch_error(
self, update: Optional[HandlerArg], error: Exception, promise: Promise = None
) -> None:
"""Dispatches an error.
Args:
@@ -627,11 +647,11 @@ class Dispatcher:
async_kwargs = None if not promise else promise.kwargs
if self.error_handlers:
for callback, run_async in self.error_handlers.items():
for callback, run_async in self.error_handlers.items(): # pylint: disable=W0621
if self.use_context:
context = CallbackContext.from_error(update, error, self,
async_args=async_args,
async_kwargs=async_kwargs)
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:
@@ -644,4 +664,5 @@ class Dispatcher:
else:
self.logger.exception(
'No error handlers are registered, logging exception.', exc_info=error)
'No error handlers are registered, logging exception.', exc_info=error
)
+763 -422
View File
File diff suppressed because it is too large Load Diff
+52 -36
View File
@@ -19,11 +19,13 @@
"""This module contains the base class for handlers as used by the Dispatcher."""
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, TypeVar, Union
from telegram import Update
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
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
@@ -87,13 +89,16 @@ class Handler(ABC):
Defaults to :obj:`False`.
"""
def __init__(self,
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):
def __init__(
self,
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: Union[bool, DefaultValue] = DEFAULT_FALSE,
):
self.callback: Callable[[HandlerArg, 'CallbackContext'], RT] = callback
self.pass_update_queue = pass_update_queue
self.pass_job_queue = pass_job_queue
@@ -117,11 +122,13 @@ class Handler(ABC):
"""
def handle_update(self,
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: object,
context: 'CallbackContext' = None) -> Union[RT, Promise]:
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:`callback` along with its respectful
@@ -137,25 +144,31 @@ class Handler(ABC):
the dispatcher.
"""
run_async = self.run_async
if self.run_async is DEFAULT_FALSE and dispatcher.bot.defaults:
if dispatcher.bot.defaults.run_async:
run_async = True
if context:
self.collect_additional_context(context, update, dispatcher, check_result)
if self.run_async:
if 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)
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
return self.callback(update, context)
def collect_additional_context(self,
context: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Any) -> None:
optional_args = self.collect_optional_args(dispatcher, update, check_result)
if run_async:
return dispatcher.run_async(
self.callback, dispatcher.bot, update, update=update, **optional_args
)
return self.callback(dispatcher.bot, update, **optional_args) # type: ignore
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:
@@ -165,12 +178,13 @@ class Handler(ABC):
check_result: The result (return value) from :attr:`check_update`.
"""
pass
def collect_optional_args(self,
dispatcher: 'Dispatcher',
update: HandlerArg = None,
check_result: Any = None) -> Dict[str, Any]:
def collect_optional_args(
self,
dispatcher: 'Dispatcher',
update: HandlerArg = None,
check_result: Any = None, # pylint: disable=W0613
) -> 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.
@@ -193,10 +207,12 @@ class Handler(ABC):
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] # type: ignore[index]
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] # type: ignore[index]
chat.id if chat else None # type: ignore[index]
]
return optional_args
+41 -24
View File
@@ -18,15 +18,25 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
""" This module contains the InlineQueryHandler class """
import re
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Match,
Optional,
Pattern,
TypeVar,
Union,
cast,
)
from telegram import Update
from telegram.utils.types import HandlerArg
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
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
@@ -110,23 +120,26 @@ class InlineQueryHandler(Handler):
"""
def __init__(self,
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):
def __init__(
self,
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: Union[bool, DefaultValue] = DEFAULT_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,
run_async=run_async)
run_async=run_async,
)
if isinstance(pattern, str):
pattern = re.compile(pattern)
@@ -157,10 +170,12 @@ class InlineQueryHandler(Handler):
return True
return None
def collect_optional_args(self,
dispatcher: 'Dispatcher',
update: HandlerArg = None,
check_result: Optional[Union[bool, Match]] = None) -> Dict[str, Any]:
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)
@@ -170,11 +185,13 @@ class InlineQueryHandler(Handler):
optional_args['groupdict'] = check_result.groupdict()
return optional_args
def collect_additional_context(self,
context: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Optional[Union[bool, Match]]) -> None:
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]
+166 -133
View File
@@ -16,24 +16,25 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
# pylint: disable=E0401
"""This module contains the classes JobQueue and Job."""
import datetime
import logging
import pytz
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Union, cast, overload
import pytz
from apscheduler.events import EVENT_JOB_ERROR, EVENT_JOB_EXECUTED, JobEvent
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 apscheduler.triggers.cron import CronTrigger
from telegram.ext.callbackcontext import CallbackContext
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
from telegram.ext import Dispatcher
class Days:
@@ -56,8 +57,9 @@ class JobQueue:
self._dispatcher: 'Dispatcher' = None # type: ignore[assignment]
self.logger = logging.getLogger(self.__class__.__name__)
self.scheduler = BackgroundScheduler(timezone=pytz.utc)
self.scheduler.add_listener(self._update_persistence,
mask=EVENT_JOB_EXECUTED | EVENT_JOB_ERROR)
self.scheduler.add_listener(
self._update_persistence, mask=EVENT_JOB_EXECUTED | EVENT_JOB_ERROR
)
# Dispatch errors and don't log them in the APS logger
def aps_log_filter(record): # type: ignore
@@ -74,7 +76,7 @@ class JobQueue:
def _tz_now(self) -> datetime.datetime:
return datetime.datetime.now(self.scheduler.timezone)
def _update_persistence(self, event: JobEvent) -> None:
def _update_persistence(self, event: JobEvent) -> None: # pylint: disable=W0613
self._dispatcher.update_persistence()
def _dispatch_error(self, event: JobEvent) -> None:
@@ -82,25 +84,29 @@ class JobQueue:
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.')
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],
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]:
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)):
@@ -108,13 +114,14 @@ class JobQueue:
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
date_time = datetime.datetime.combine(
datetime.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time
)
if date_time.tzinfo is None:
date_time = self.scheduler.timezone.localize(date_time)
if shift_day and date_time <= datetime.datetime.now(pytz.utc):
date_time += datetime.timedelta(days=1)
return date_time
# isinstance(time, datetime.datetime):
return time
@@ -131,12 +138,14 @@ class JobQueue:
if dispatcher.bot.defaults:
self.scheduler.configure(timezone=dispatcher.bot.defaults.tzinfo or pytz.utc)
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':
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:
@@ -181,29 +190,31 @@ class JobQueue:
name = name or callback.__name__
job = Job(callback, context, name, self)
dt = self._parse_time_input(when, shift_day=True)
date_time = 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)
j = self.scheduler.add_job(
callback,
name=name,
trigger='date',
run_date=date_time,
args=self._build_args(job),
timezone=date_time.tzinfo or self.scheduler.timezone,
**job_kwargs,
)
job.job = j
return job
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':
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:
@@ -277,26 +288,30 @@ class JobQueue:
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)
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_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':
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:
@@ -332,45 +347,55 @@ class JobQueue:
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)
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)
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':
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:
@@ -409,25 +434,29 @@ class JobQueue:
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)
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':
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:
@@ -453,10 +482,7 @@ class JobQueue:
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)
j = self.scheduler.add_job(callback, args=self._build_args(job), name=name, **job_kwargs)
job.job = j
return job
@@ -472,11 +498,14 @@ class JobQueue:
self.scheduler.shutdown()
def jobs(self) -> Tuple['Job', ...]:
"""Returns a tuple of all jobs that are currently in the ``JobQueue``."""
"""
Returns a tuple of all *pending/scheduled* jobs that are currently in the ``JobQueue``.
"""
return tuple(Job.from_aps_job(job, self) for job in self.scheduler.get_jobs())
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``"""
"""Returns a tuple of all *pending/scheduled* jobs with the given name that are currently
in the ``JobQueue``"""
return tuple(job for job in self.jobs() if job.name == name)
@@ -516,12 +545,14 @@ class Job:
job (:class:`apscheduler.job.Job`, optional): The APS Job this job is a wrapper for.
"""
def __init__(self,
callback: Callable[['CallbackContext'], None],
context: object = None,
name: str = None,
job_queue: JobQueue = None,
job: 'Job' = None):
def __init__(
self,
callback: Callable[['CallbackContext'], None],
context: object = None,
name: str = None,
job_queue: JobQueue = None,
job: 'Job' = None,
):
self.callback = callback
self.context = context
@@ -540,14 +571,16 @@ class Job:
self.callback(CallbackContext.from_job(self, dispatcher))
else:
self.callback(dispatcher.bot, self) # type: ignore[arg-type,call-arg]
except Exception as e:
except Exception as exc:
try:
dispatcher.dispatch_error(None, e)
dispatcher.dispatch_error(None, exc)
# 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.')
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.'
)
def schedule_removal(self) -> None:
"""
+48 -35
View File
@@ -19,15 +19,16 @@
# TODO: Remove allow_edited
"""This module contains the MessageHandler class."""
import warnings
from telegram.utils.deprecate import TelegramDeprecationWarning
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, TypeVar, Union
from telegram import Update
from telegram.ext import Filters, BaseFilter
from telegram.ext import BaseFilter, Filters
from telegram.utils.deprecate import TelegramDeprecationWarning
from telegram.utils.types import HandlerArg
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
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
@@ -120,17 +121,19 @@ class MessageHandler(Handler):
"""
def __init__(self,
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):
def __init__(
self,
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: Union[bool, DefaultValue] = DEFAULT_FALSE,
):
super().__init__(
callback,
@@ -138,36 +141,44 @@ class MessageHandler(Handler):
pass_job_queue=pass_job_queue,
pass_user_data=pass_user_data,
pass_chat_data=pass_chat_data,
run_async=run_async)
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')
'message_updates, channel_post_updates and edited_updates are all False'
)
if filters is not None:
self.filters = Filters.update & filters
else:
self.filters = Filters.update
if message_updates is not None:
warnings.warn('message_updates is deprecated. See https://git.io/fxJuV for more info',
TelegramDeprecationWarning,
stacklevel=2)
warnings.warn(
'message_updates is deprecated. See https://git.io/fxJuV for more info',
TelegramDeprecationWarning,
stacklevel=2,
)
if message_updates is False:
self.filters &= ~Filters.update.message
if channel_post_updates is not None:
warnings.warn('channel_post_updates is deprecated. See https://git.io/fxJuV '
'for more info',
TelegramDeprecationWarning,
stacklevel=2)
warnings.warn(
'channel_post_updates is deprecated. See https://git.io/fxJuV ' 'for more info',
TelegramDeprecationWarning,
stacklevel=2,
)
if channel_post_updates is False:
self.filters &= ~Filters.update.channel_post
if edited_updates is not None:
warnings.warn('edited_updates is deprecated. See https://git.io/fxJuV for more info',
TelegramDeprecationWarning,
stacklevel=2)
warnings.warn(
'edited_updates is deprecated. See https://git.io/fxJuV for more info',
TelegramDeprecationWarning,
stacklevel=2,
)
if edited_updates is False:
self.filters &= ~(Filters.update.edited_message
| Filters.update.edited_channel_post)
self.filters &= ~(
Filters.update.edited_message | Filters.update.edited_channel_post
)
def check_update(self, update: HandlerArg) -> Optional[Union[bool, Dict[str, Any]]]:
"""Determines whether an update should be passed to this handlers :attr:`callback`.
@@ -183,10 +194,12 @@ class MessageHandler(Handler):
return self.filters(update)
return None
def collect_additional_context(self,
context: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Optional[Union[bool, Dict[str, Any]]]) -> None:
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)
+33 -27
View File
@@ -20,14 +20,13 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/]
"""A throughput-limiting message processor for Telegram bots."""
from telegram.utils import promise
import functools
import time
import threading
import queue as q
import threading
import time
from typing import TYPE_CHECKING, Any, Callable, List, NoReturn
from typing import Callable, Any, TYPE_CHECKING, List, NoReturn
from telegram.utils.promise import Promise
if TYPE_CHECKING:
from telegram import Bot
@@ -38,7 +37,6 @@ curtime = time.perf_counter
class DelayQueueError(RuntimeError):
"""Indicates processing errors."""
pass
class DelayQueue(threading.Thread):
@@ -75,21 +73,23 @@ class DelayQueue(threading.Thread):
_instcnt = 0 # instance counter
def __init__(self,
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):
def __init__(
self,
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
self.exc_route = (exc_route if exc_route is not None else self._default_exception_handler)
self.exc_route = exc_route if exc_route is not None else self._default_exception_handler
self.__exit_req = False # flag to gently exit thread
self.__class__._instcnt += 1
if name is None:
name = '{}-{}'.format(self.__class__.__name__, self.__class__._instcnt)
name = f'{self.__class__.__name__}-{self.__class__._instcnt}'
super().__init__(name=name)
self.daemon = False
if autostart: # immediately start processing
@@ -201,24 +201,28 @@ class MessageQueue:
"""
def __init__(self,
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):
def __init__(
self,
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,
exc_route=exc_route,
autostart=autostart)
autostart=autostart,
)
self._group_delayq = DelayQueue(
burst_limit=group_burst_limit,
time_limit_ms=group_time_limit_ms,
exc_route=exc_route,
autostart=autostart)
autostart=autostart,
)
def start(self) -> None:
"""Method is used to manually start the ``MessageQueue`` processing."""
@@ -297,11 +301,13 @@ def queuedmessage(method: Callable) -> Callable:
@functools.wraps(method)
def wrapped(self: 'Bot', *args: Any, **kwargs: Any) -> Any:
queued = kwargs.pop('queued',
self._is_messages_queued_default) # type: ignore[attr-defined]
# pylint: disable=W0212
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)
prom = Promise(method, (self,) + args, kwargs)
return self._msg_queue(prom, isgroup) # type: ignore[attr-defined]
return method(self, *args, **kwargs)
+57 -49
View File
@@ -20,10 +20,9 @@
import pickle
from collections import defaultdict
from copy import deepcopy
from typing import Any, DefaultDict, Dict, Optional, Tuple
from telegram.ext import BasePersistence
from typing import DefaultDict, Dict, Any, Tuple, Optional
from telegram.utils.types import ConversationDict
@@ -74,16 +73,20 @@ class PicklePersistence(BasePersistence):
Default is :obj:`False`.
"""
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)
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
@@ -95,8 +98,8 @@ class PicklePersistence(BasePersistence):
def load_singlefile(self) -> None:
try:
filename = self.filename
with open(self.filename, "rb") as f:
data = pickle.load(f)
with open(self.filename, "rb") as file:
data = pickle.load(file)
self.user_data = defaultdict(dict, data['user_data'])
self.chat_data = defaultdict(dict, data['chat_data'])
# For backwards compatibility with files not containing bot data
@@ -107,31 +110,37 @@ class PicklePersistence(BasePersistence):
self.user_data = defaultdict(dict)
self.chat_data = defaultdict(dict)
self.bot_data = {}
except pickle.UnpicklingError:
raise TypeError("File {} does not contain valid pickle data".format(filename))
except Exception:
raise TypeError("Something went wrong unpickling {}".format(filename))
except pickle.UnpicklingError as exc:
raise TypeError(f"File {filename} does not contain valid pickle data") from exc
except Exception as exc:
raise TypeError(f"Something went wrong unpickling {filename}") from exc
def load_file(self, filename: str) -> Any:
@staticmethod
def load_file(filename: str) -> Any:
try:
with open(filename, "rb") as f:
return pickle.load(f)
with open(filename, "rb") as file:
return pickle.load(file)
except IOError:
return None
except pickle.UnpicklingError:
raise TypeError("File {} does not contain valid pickle data".format(filename))
except Exception:
raise TypeError("Something went wrong unpickling {}".format(filename))
except pickle.UnpicklingError as exc:
raise TypeError(f"File {filename} does not contain valid pickle data") from exc
except Exception as exc:
raise TypeError(f"Something went wrong unpickling {filename}") from exc
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)
with open(self.filename, "wb") as file:
data = {
'conversations': self.conversations,
'user_data': self.user_data,
'chat_data': self.chat_data,
'bot_data': self.bot_data,
}
pickle.dump(data, file)
def dump_file(self, filename: str, data: Any) -> None:
with open(filename, "wb") as f:
pickle.dump(data, f)
@staticmethod
def dump_file(filename: str, data: Any) -> None:
with open(filename, "wb") as file:
pickle.dump(data, file)
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`.
@@ -142,7 +151,7 @@ class PicklePersistence(BasePersistence):
if self.user_data:
pass
elif not self.single_file:
filename = "{}_user_data".format(self.filename)
filename = f"{self.filename}_user_data"
data = self.load_file(filename)
if not data:
data = defaultdict(dict)
@@ -162,7 +171,7 @@ class PicklePersistence(BasePersistence):
if self.chat_data:
pass
elif not self.single_file:
filename = "{}_chat_data".format(self.filename)
filename = f"{self.filename}_chat_data"
data = self.load_file(filename)
if not data:
data = defaultdict(dict)
@@ -182,7 +191,7 @@ class PicklePersistence(BasePersistence):
if self.bot_data:
pass
elif not self.single_file:
filename = "{}_bot_data".format(self.filename)
filename = f"{self.filename}_bot_data"
data = self.load_file(filename)
if not data:
data = {}
@@ -203,7 +212,7 @@ class PicklePersistence(BasePersistence):
if self.conversations:
pass
elif not self.single_file:
filename = "{}_conversations".format(self.filename)
filename = f"{self.filename}_conversations"
data = self.load_file(filename)
if not data:
data = {name: {}}
@@ -212,9 +221,9 @@ class PicklePersistence(BasePersistence):
self.load_singlefile()
return self.conversations.get(name, {}).copy() # type: ignore[union-attr]
def update_conversation(self,
name: str, key: Tuple[int, ...],
new_state: Optional[object]) -> None:
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.
@@ -230,7 +239,7 @@ class PicklePersistence(BasePersistence):
self.conversations[name][key] = new_state
if not self.on_flush:
if not self.single_file:
filename = "{}_conversations".format(self.filename)
filename = f"{self.filename}_conversations"
self.dump_file(filename, self.conversations)
else:
self.dump_singlefile()
@@ -249,7 +258,7 @@ class PicklePersistence(BasePersistence):
self.user_data[user_id] = data
if not self.on_flush:
if not self.single_file:
filename = "{}_user_data".format(self.filename)
filename = f"{self.filename}_user_data"
self.dump_file(filename, self.user_data)
else:
self.dump_singlefile()
@@ -268,7 +277,7 @@ class PicklePersistence(BasePersistence):
self.chat_data[chat_id] = data
if not self.on_flush:
if not self.single_file:
filename = "{}_chat_data".format(self.filename)
filename = f"{self.filename}_chat_data"
self.dump_file(filename, self.chat_data)
else:
self.dump_singlefile()
@@ -284,23 +293,22 @@ class PicklePersistence(BasePersistence):
self.bot_data = data.copy()
if not self.on_flush:
if not self.single_file:
filename = "{}_bot_data".format(self.filename)
filename = f"{self.filename}_bot_data"
self.dump_file(filename, self.bot_data)
else:
self.dump_singlefile()
def flush(self) -> None:
""" Will save all data in memory to pickle file(s).
"""
"""Will save all data in memory to pickle file(s)."""
if self.single_file:
if self.user_data or self.chat_data or self.bot_data or self.conversations:
self.dump_singlefile()
else:
if self.user_data:
self.dump_file("{}_user_data".format(self.filename), self.user_data)
self.dump_file(f"{self.filename}_user_data", self.user_data)
if self.chat_data:
self.dump_file("{}_chat_data".format(self.filename), self.chat_data)
self.dump_file(f"{self.filename}_chat_data", self.chat_data)
if self.bot_data:
self.dump_file("{}_bot_data".format(self.filename), self.bot_data)
self.dump_file(f"{self.filename}_bot_data", self.bot_data)
if self.conversations:
self.dump_file("{}_conversations".format(self.filename), self.conversations)
self.dump_file(f"{self.filename}_conversations", self.conversations)
+3 -2
View File
@@ -16,12 +16,13 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the PollAnswerHandler class."""
from telegram import Update
from .handler import Handler
from telegram.utils.types import HandlerArg
from .handler import Handler
class PollAnswerHandler(Handler):
"""Handler class to handle Telegram updates that contain a poll answer.
+3 -2
View File
@@ -16,12 +16,13 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the PollHandler classes."""
from telegram import Update
from .handler import Handler
from telegram.utils.types import HandlerArg
from .handler import Handler
class PollHandler(Handler):
"""Handler class to handle Telegram updates that contain a poll.
+2 -2
View File
@@ -19,10 +19,10 @@
"""This module contains the PreCheckoutQueryHandler class."""
from telegram import Update
from .handler import Handler
from telegram.utils.types import HandlerArg
from .handler import Handler
class PreCheckoutQueryHandler(Handler):
"""Handler class to handle Telegram PreCheckout callback queries.
+42 -35
View File
@@ -20,13 +20,13 @@
"""This module contains the RegexHandler class."""
import warnings
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Pattern, TypeVar, Union
from telegram.ext import Filters, MessageHandler
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
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
@@ -108,41 +108,48 @@ class RegexHandler(MessageHandler):
"""
def __init__(self,
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().__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)
def __init__(
self,
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, # pylint: disable=W0613
message_updates: bool = True,
channel_post_updates: bool = False,
edited_updates: bool = False,
run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
):
warnings.warn(
'RegexHandler is deprecated. See https://git.io/fxJuV for more info',
TelegramDeprecationWarning,
stacklevel=2,
)
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: 'Dispatcher',
update: HandlerArg = None,
check_result: Optional[Union[bool, Dict[str, Any]]] = None) -> Dict[str, Any]:
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:
+2 -2
View File
@@ -19,10 +19,10 @@
"""This module contains the ShippingQueryHandler class."""
from telegram import Update
from .handler import Handler
from telegram.utils.types import HandlerArg
from .handler import Handler
class ShippingQueryHandler(Handler):
"""Handler class to handle Telegram shipping callback queries.
+29 -19
View File
@@ -18,10 +18,13 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the StringCommandHandler class."""
from .handler import Handler
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, TypeVar, Union
from telegram.utils.types import HandlerArg
from typing import Callable, TYPE_CHECKING, Any, Optional, TypeVar, Dict, List
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
from .handler import Handler
if TYPE_CHECKING:
from telegram.ext import CallbackContext, Dispatcher
@@ -80,18 +83,21 @@ class StringCommandHandler(Handler):
"""
def __init__(self,
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):
def __init__(
self,
command: str,
callback: Callable[[HandlerArg, 'CallbackContext'], RT],
pass_args: bool = False,
pass_update_queue: bool = False,
pass_job_queue: bool = False,
run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
):
super().__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
run_async=run_async)
run_async=run_async,
)
self.command = command
self.pass_args = pass_args
@@ -111,18 +117,22 @@ class StringCommandHandler(Handler):
return args[1:]
return None
def collect_optional_args(self,
dispatcher: 'Dispatcher',
update: HandlerArg = None,
check_result: Optional[List[str]] = None) -> Dict[str, Any]:
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: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Optional[List[str]]) -> None:
def collect_additional_context(
self,
context: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Optional[List[str]],
) -> None:
context.args = check_result
+29 -20
View File
@@ -19,11 +19,13 @@
"""This module contains the StringRegexHandler class."""
import re
from typing import TYPE_CHECKING, Any, Callable, Dict, Match, Optional, Pattern, TypeVar, Union
from telegram.utils.types import HandlerArg
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
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
@@ -90,19 +92,22 @@ class StringRegexHandler(Handler):
"""
def __init__(self,
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):
def __init__(
self,
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: Union[bool, DefaultValue] = DEFAULT_FALSE,
):
super().__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
run_async=run_async)
run_async=run_async,
)
if isinstance(pattern, str):
pattern = re.compile(pattern)
@@ -127,10 +132,12 @@ class StringRegexHandler(Handler):
return match
return None
def collect_optional_args(self,
dispatcher: 'Dispatcher',
update: HandlerArg = None,
check_result: Optional[Match] = None) -> Dict[str, Any]:
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 and check_result:
@@ -139,10 +146,12 @@ class StringRegexHandler(Handler):
optional_args['groupdict'] = check_result.groupdict()
return optional_args
def collect_additional_context(self,
context: 'CallbackContext',
update: HandlerArg,
dispatcher: 'Dispatcher',
check_result: Optional[Match]) -> None:
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]
+15 -13
View File
@@ -18,11 +18,11 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the TypeHandler class."""
from typing import TYPE_CHECKING, Any, Callable, Type, TypeVar, Union
from telegram.utils.helpers import DefaultValue, DEFAULT_FALSE
from .handler import Handler
from typing import Callable, TYPE_CHECKING, TypeVar, Type, Any
if TYPE_CHECKING:
from telegram.ext import CallbackContext
@@ -74,18 +74,21 @@ class TypeHandler(Handler):
"""
def __init__(self,
type: Type,
callback: Callable[[Any, 'CallbackContext'], RT],
strict: bool = False,
pass_update_queue: bool = False,
pass_job_queue: bool = False,
run_async: bool = False):
def __init__(
self,
type: Type, # pylint: disable=W0622
callback: Callable[[Any, 'CallbackContext'], RT],
strict: bool = False,
pass_update_queue: bool = False,
pass_job_queue: bool = False,
run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
):
super().__init__(
callback,
pass_update_queue=pass_update_queue,
pass_job_queue=pass_job_queue,
run_async=run_async)
run_async=run_async,
)
self.type = type
self.strict = strict
@@ -101,5 +104,4 @@ class TypeHandler(Handler):
"""
if not self.strict:
return isinstance(update, self.type)
else:
return type(update) is self.type
return type(update) is self.type # pylint: disable=C0123
+199 -123
View File
@@ -21,20 +21,19 @@
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
from queue import Queue
from signal import SIGABRT, SIGINT, SIGTERM, signal
from threading import Event, Lock, Thread, current_thread
from time import sleep
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union, no_type_check
from telegram import Bot, TelegramError
from telegram.error import InvalidToken, RetryAfter, TimedOut, Unauthorized
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)
from typing import Callable, Dict, TYPE_CHECKING, Any, List, Union, Tuple, no_type_check, Optional
from telegram.utils.webhookhandler import WebhookAppClass, WebhookServer
if TYPE_CHECKING:
from telegram.ext import BasePersistence, Defaults
@@ -108,26 +107,30 @@ class Updater:
_request = None
def __init__(self,
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):
def __init__(
self,
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)
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):
@@ -156,7 +159,8 @@ class Updater:
if bot.request.con_pool_size < con_pool_size:
self.logger.warning(
'Connection pool of Request object is smaller than optimal value (%s)',
con_pool_size)
con_pool_size,
)
else:
# we need a connection pool the size of:
# * for each of the workers
@@ -169,24 +173,28 @@ class Updater:
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, # 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.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 = Queue()
self.job_queue = JobQueue()
self.__exception_event = Event()
self.persistence = persistence
self.dispatcher = Dispatcher(self.bot,
self.update_queue,
job_queue=self.job_queue,
workers=workers,
exception_event=self.__exception_event,
persistence=persistence,
use_context=use_context)
self.dispatcher = Dispatcher(
self.bot,
self.update_queue,
job_queue=self.job_queue,
workers=workers,
exception_event=self.__exception_event,
persistence=persistence,
use_context=use_context,
)
self.job_queue.set_dispatcher(self.dispatcher)
else:
con_pool_size = dispatcher.workers + 4
@@ -195,7 +203,8 @@ class Updater:
if self.bot.request.con_pool_size < con_pool_size:
self.logger.warning(
'Connection pool of Request object is smaller than optimal value (%s)',
con_pool_size)
con_pool_size,
)
self.update_queue = dispatcher.update_queue
self.__exception_event = dispatcher.exception_event
self.persistence = dispatcher.persistence
@@ -211,31 +220,35 @@ class Updater:
self.__threads: List[Thread] = []
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,
kwargs=kwargs)
thr = Thread(
target=self._thread_wrapper,
name=f"Bot:{self.bot.id}:{name}",
args=(target,) + args,
kwargs=kwargs,
)
thr.start()
self.__threads.append(thr)
def _thread_wrapper(self, target: Callable, *args: Any, **kwargs: Any) -> None:
thr_name = current_thread().name
self.logger.debug('{} - started'.format(thr_name))
self.logger.debug('%s - started', 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('{} - ended'.format(thr_name))
self.logger.debug('%s - ended', thr_name)
def start_polling(self,
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]:
def start_polling(
self,
poll_interval: float = 0.0,
timeout: float = 10,
clean: bool = False,
bootstrap_retries: int = -1,
read_latency: float = 2.0,
allowed_updates: List[str] = None,
) -> Optional[Queue]:
"""Starts polling updates from Telegram.
Args:
@@ -270,9 +283,17 @@ class Updater:
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,
ready=polling_ready)
self._init_thread(
self._start_polling,
"updater",
poll_interval,
timeout,
read_latency,
bootstrap_retries,
clean,
allowed_updates,
ready=polling_ready,
)
self.logger.debug('Waiting for Dispatcher and polling to start')
dispatcher_ready.wait()
@@ -282,17 +303,19 @@ class Updater:
return self.update_queue
return None
def start_webhook(self,
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]:
def start_webhook(
self,
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
@@ -344,9 +367,21 @@ class Updater:
dispatcher_ready = Event()
self.job_queue.start()
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,
ready=webhook_ready, force_event_loop=force_event_loop)
self._init_thread(
self._start_webhook,
"updater",
listen,
port,
url_path,
cert,
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()
@@ -357,8 +392,16 @@ class Updater:
return None
@no_type_check
def _start_polling(self, poll_interval, timeout, read_latency, bootstrap_retries, clean,
allowed_updates, ready=None): # pragma: no cover
def _start_polling(
self,
poll_interval,
timeout,
read_latency,
bootstrap_retries,
clean,
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.
@@ -370,10 +413,12 @@ class Updater:
self.logger.debug('Bootstrap done')
def polling_action_cb():
updates = self.bot.get_updates(self.last_update_id,
timeout=timeout,
read_latency=read_latency,
allowed_updates=allowed_updates)
updates = self.bot.get_updates(
self.last_update_id,
timeout=timeout,
read_latency=read_latency,
allowed_updates=allowed_updates,
)
if updates:
if not self.running:
@@ -393,8 +438,9 @@ class Updater:
if ready is not None:
ready.set()
self._network_loop_retry(polling_action_cb, polling_onerr_cb, 'getting Updates',
poll_interval)
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):
@@ -418,9 +464,9 @@ class Updater:
try:
if not action_cb():
break
except RetryAfter as e:
self.logger.info('%s', e)
cur_interval = 0.5 + e.retry_after
except RetryAfter as exc:
self.logger.info('%s', exc)
cur_interval = 0.5 + exc.retry_after
except TimedOut as toe:
self.logger.debug('Timed out %s: %s', description, toe)
# If failure is due to timeout, we should retry asap.
@@ -428,9 +474,9 @@ class Updater:
except InvalidToken as pex:
self.logger.error('Invalid token; aborting')
raise pex
except TelegramError as te:
self.logger.error('Error while %s: %s', description, te)
onerr_cb(te)
except TelegramError as telegram_exc:
self.logger.error('Error while %s: %s', description, telegram_exc)
onerr_cb(telegram_exc)
cur_interval = self._increase_poll_interval(cur_interval)
else:
cur_interval = interval
@@ -450,12 +496,24 @@ class Updater:
return current_interval
@no_type_check
def _start_webhook(self, listen, port, url_path, cert, key, bootstrap_retries, clean,
webhook_url, allowed_updates, ready=None, force_event_loop=False):
def _start_webhook(
self,
listen,
port,
url_path,
cert,
key,
bootstrap_retries,
clean,
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 = '/{}'.format(url_path)
url_path = f'/{url_path}'
# Create Tornado app instance
app = WebhookAppClass(url_path, self.bot, self.update_queue)
@@ -466,8 +524,8 @@ class Updater:
try:
ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_ctx.load_cert_chain(cert, key)
except ssl.SSLError:
raise TelegramError('Invalid SSL Certificate')
except ssl.SSLError as exc:
raise TelegramError('Invalid SSL Certificate') from exc
else:
ssl_ctx = None
@@ -479,29 +537,29 @@ class Updater:
if not webhook_url:
webhook_url = self._gen_webhook_url(listen, port, url_path)
self._bootstrap(max_retries=bootstrap_retries,
clean=clean,
webhook_url=webhook_url,
cert=open(cert, 'rb'),
allowed_updates=allowed_updates)
self._bootstrap(
max_retries=bootstrap_retries,
clean=clean,
webhook_url=webhook_url,
cert=open(cert, 'rb'),
allowed_updates=allowed_updates,
)
elif clean:
self.logger.warning("cleaning updates is not supported if "
"SSL-termination happens elsewhere; skipping")
self.logger.warning(
"cleaning updates is not supported if "
"SSL-termination happens elsewhere; skipping"
)
self.httpd.serve_forever(force_event_loop=force_event_loop, ready=ready)
@staticmethod
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)
return f'https://{listen}:{port}{url_path}'
@no_type_check
def _bootstrap(self,
max_retries,
clean,
webhook_url,
allowed_updates,
cert=None,
bootstrap_interval=5):
def _bootstrap(
self, max_retries, clean, webhook_url, allowed_updates, cert=None, bootstrap_interval=5
):
retries = [0]
def bootstrap_del_webhook():
@@ -516,16 +574,17 @@ class Updater:
return False
def bootstrap_set_webhook():
self.bot.set_webhook(url=webhook_url,
certificate=cert,
allowed_updates=allowed_updates)
self.bot.set_webhook(
url=webhook_url, certificate=cert, allowed_updates=allowed_updates
)
return False
def bootstrap_onerr_cb(exc):
if not isinstance(exc, Unauthorized) and (max_retries < 0 or retries[0] < max_retries):
retries[0] += 1
self.logger.warning('Failed bootstrap phase; try=%s max_retries=%s', retries[0],
max_retries)
self.logger.warning(
'Failed bootstrap phase; try=%s max_retries=%s', retries[0], max_retries
)
else:
self.logger.error('Failed bootstrap phase after %s retries (%s)', retries[0], exc)
raise exc
@@ -535,22 +594,34 @@ class Updater:
# We also take this chance to delete pre-configured webhook if this is a polling Updater.
# NOTE: We don't know ahead if a webhook is configured, so we just delete.
if clean or not webhook_url:
self._network_loop_retry(bootstrap_del_webhook, bootstrap_onerr_cb,
'bootstrap del webhook', bootstrap_interval)
self._network_loop_retry(
bootstrap_del_webhook,
bootstrap_onerr_cb,
'bootstrap del webhook',
bootstrap_interval,
)
retries[0] = 0
# Clean pending messages, if requested.
if clean:
self._network_loop_retry(bootstrap_clean_updates, bootstrap_onerr_cb,
'bootstrap clean updates', bootstrap_interval)
self._network_loop_retry(
bootstrap_clean_updates,
bootstrap_onerr_cb,
'bootstrap clean updates',
bootstrap_interval,
)
retries[0] = 0
sleep(1)
# Restore/set webhook settings, if needed. Again, we don't know ahead if a webhook is set,
# so we set it anyhow.
if webhook_url:
self._network_loop_retry(bootstrap_set_webhook, bootstrap_onerr_cb,
'bootstrap set webhook', bootstrap_interval)
self._network_loop_retry(
bootstrap_set_webhook,
bootstrap_onerr_cb,
'bootstrap set webhook',
bootstrap_interval,
)
def stop(self) -> None:
"""Stops the polling/webhook thread, the dispatcher and the job queue."""
@@ -573,9 +644,11 @@ class Updater:
@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 '
'immediately.')
self.logger.debug(
'Waiting for current webhook connection to be '
'closed... Send a Telegram message to the bot to exit '
'immediately.'
)
self.httpd.shutdown()
self.httpd = None
@@ -587,17 +660,18 @@ class Updater:
@no_type_check
def _join_threads(self) -> None:
for thr in self.__threads:
self.logger.debug('Waiting for {} thread to end'.format(thr.name))
self.logger.debug('Waiting for %s thread to end', thr.name)
thr.join()
self.logger.debug('{} thread has ended'.format(thr.name))
self.logger.debug('%s thread has ended', thr.name)
self.__threads = []
@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)))
self.logger.info(
'Received signal %s (%s), stopping...', signum, get_signal_name(signum)
)
if self.persistence:
# Update user_data, chat_data and bot_data before flushing
self.dispatcher.update_persistence()
@@ -607,7 +681,9 @@ class Updater:
self.user_sig_handler(signum, frame)
else:
self.logger.warning('Exiting immediately!')
# pylint: disable=C0415,W0212
import os
os._exit(1)
def idle(self, stop_signals: Union[List, Tuple] = (SIGINT, SIGTERM, SIGABRT)) -> None:
+17 -15
View File
@@ -17,11 +17,11 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Animation."""
from telegram import PhotoSize
from telegram import TelegramObject
from typing import TYPE_CHECKING, Any, Optional
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
@@ -64,18 +64,20 @@ class Animation(TelegramObject):
"""
def __init__(self,
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):
def __init__(
self,
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)
+20 -13
View File
@@ -18,10 +18,11 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Audio."""
from telegram import TelegramObject, PhotoSize
from typing import TYPE_CHECKING, Any, Optional
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
@@ -41,6 +42,7 @@ class Audio(TelegramObject):
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.
file_name (:obj:`str`): Optional. Original filename as defined by sender.
mime_type (:obj:`str`): Optional. MIME type of the file as defined by sender.
file_size (:obj:`int`): Optional. File size.
thumb (:class:`telegram.PhotoSize`): Optional. Thumbnail of the album cover to
@@ -56,6 +58,7 @@ class Audio(TelegramObject):
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.
file_name (:obj:`str`, optional): Original filename as defined by sender.
mime_type (:obj:`str`, optional): MIME type of the file as defined by sender.
file_size (:obj:`int`, optional): File size.
thumb (:class:`telegram.PhotoSize`, optional): Thumbnail of the album cover to
@@ -65,17 +68,20 @@ class Audio(TelegramObject):
"""
def __init__(self,
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):
def __init__(
self,
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,
file_name: str = None,
**_kwargs: Any,
):
# Required
self.file_id = str(file_id)
self.file_unique_id = str(file_unique_id)
@@ -83,6 +89,7 @@ class Audio(TelegramObject):
# Optionals
self.performer = performer
self.title = title
self.file_name = file_name
self.mime_type = mime_type
self.file_size = file_size
self.thumb = thumb
+15 -9
View File
@@ -17,9 +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 an object that represents a Telegram ChatPhoto."""
from typing import TYPE_CHECKING, Any
from telegram import TelegramObject
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot, File
@@ -61,13 +62,15 @@ class ChatPhoto(TelegramObject):
"""
def __init__(self,
small_file_id: str,
small_file_unique_id: str,
big_file_id: str,
big_file_unique_id: str,
bot: 'Bot' = None,
**kwargs: Any):
def __init__(
self,
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
@@ -75,7 +78,10 @@ class ChatPhoto(TelegramObject):
self.bot = bot
self._id_attrs = (self.small_file_unique_id, self.big_file_unique_id,)
self._id_attrs = (
self.small_file_unique_id,
self.big_file_unique_id,
)
def get_small_file(self, timeout: int = None, **kwargs: Any) -> 'File':
"""Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the
+11 -8
View File
@@ -18,9 +18,10 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Contact."""
from telegram import TelegramObject
from typing import Any
from telegram import TelegramObject
class Contact(TelegramObject):
"""This object represents a phone contact.
@@ -45,13 +46,15 @@ class Contact(TelegramObject):
"""
def __init__(self,
phone_number: str,
first_name: str,
last_name: str = None,
user_id: int = None,
vcard: str = None,
**kwargs: Any):
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
+15 -11
View File
@@ -18,10 +18,11 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Document."""
from telegram import PhotoSize, TelegramObject
from typing import TYPE_CHECKING, Any, Optional
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
@@ -57,17 +58,20 @@ class Document(TelegramObject):
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
_id_keys = ('file_id',)
def __init__(self,
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):
def __init__(
self,
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)
+69 -43
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 a Telegram File."""
import os
import shutil
import urllib.parse as urllib_parse
from base64 import b64decode
from os.path import basename
import os
import urllib.parse as urllib_parse
from typing import IO, TYPE_CHECKING, Any, Optional, Union
from telegram import TelegramObject
from telegram.passport.credentials import decrypt
from telegram.utils.helpers import is_local_file
from typing import Any, Optional, IO, Union, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot, FileCredentials
@@ -68,13 +69,15 @@ class File(TelegramObject):
"""
def __init__(self,
file_id: str,
file_unique_id: str,
bot: 'Bot' = None,
file_size: int = None,
file_path: str = None,
**kwargs: Any):
def __init__(
self,
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)
@@ -86,10 +89,9 @@ class File(TelegramObject):
self._id_attrs = (self.file_unique_id,)
def download(self,
custom_path: str = None,
out: IO = None,
timeout: int = None) -> Union[str, IO]:
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
@@ -98,7 +100,10 @@ class File(TelegramObject):
the ``out.write`` method.
Note:
:attr:`custom_path` and :attr:`out` are mutually exclusive.
* :attr:`custom_path` and :attr:`out` are mutually exclusive.
* If neither :attr:`custom_path` nor :attr:`out` is provided and :attr:`file_path` is
the path of a local file (which is the case when a Bot API Server is running in
local mode), this method will just return the path.
Args:
custom_path (:obj:`str`, optional): Custom path.
@@ -110,7 +115,7 @@ class File(TelegramObject):
Returns:
:obj:`str` | :obj:`io.BufferedWriter`: The same object as :attr:`out` if specified.
Otherwise, returns the filename downloaded to.
Otherwise, returns the filename downloaded to or the file path of the local file.
Raises:
ValueError: If both :attr:`custom_path` and :attr:`out` are passed.
@@ -119,39 +124,57 @@ class File(TelegramObject):
if custom_path is not None and out is not None:
raise ValueError('custom_path and out are mutually exclusive')
# Convert any UTF-8 char into a url encoded ASCII string.
url = self._get_encoded_url()
local_file = is_local_file(self.file_path)
if local_file:
url = self.file_path
else:
# Convert any UTF-8 char into a url encoded ASCII string.
url = self._get_encoded_url()
if out:
buf = self.bot.request.retrieve(url)
if self._credentials:
buf = decrypt(b64decode(self._credentials.secret),
b64decode(self._credentials.hash),
buf)
if local_file:
with open(url, 'rb') as file:
buf = file.read()
else:
buf = self.bot.request.retrieve(url)
if self._credentials:
buf = decrypt(
b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf
)
out.write(buf)
return out
else:
if custom_path:
filename = custom_path
elif self.file_path:
filename = basename(self.file_path)
else:
filename = os.path.join(os.getcwd(), self.file_id)
buf = self.bot.request.retrieve(url, timeout=timeout)
if self._credentials:
buf = decrypt(b64decode(self._credentials.secret),
b64decode(self._credentials.hash),
buf)
with open(filename, 'wb') as fobj:
fobj.write(buf)
return filename
if custom_path and local_file:
shutil.copyfile(self.file_path, custom_path)
return custom_path
if custom_path:
filename = custom_path
elif local_file:
return self.file_path
elif self.file_path:
filename = basename(self.file_path)
else:
filename = os.path.join(os.getcwd(), self.file_id)
buf = self.bot.request.retrieve(url, timeout=timeout)
if self._credentials:
buf = decrypt(
b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf
)
with open(filename, 'wb') as fobj:
fobj.write(buf)
return filename
def _get_encoded_url(self) -> 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))
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: bytearray = None) -> bytes:
"""Download this file and return it as a bytearray.
@@ -166,8 +189,11 @@ class File(TelegramObject):
"""
if buf is None:
buf = bytearray()
buf.extend(self.bot.request.retrieve(self._get_encoded_url()))
if is_local_file(self.file_path):
with open(self.file_path, "rb") as file:
buf.extend(file.read())
else:
buf.extend(self.bot.request.retrieve(self._get_encoded_url()))
return buf
def set_credentials(self, credentials: 'FileCredentials') -> None:
+25 -20
View File
@@ -20,15 +20,14 @@
"""This module contains an object that represents a Telegram InputFile."""
import imghdr
import logging
import mimetypes
import os
from typing import IO, Optional, Tuple
from uuid import uuid4
from telegram import TelegramError
from typing import IO, Tuple, Optional
DEFAULT_MIME_TYPE = 'application/octet-stream'
logger = logging.getLogger(__name__)
class InputFile:
@@ -57,17 +56,17 @@ class InputFile:
if filename:
self.filename = filename
elif (hasattr(obj, 'name') and not isinstance(obj.name, int)):
elif hasattr(obj, 'name') and not isinstance(obj.name, int):
self.filename = os.path.basename(obj.name)
try:
self.mimetype = self.is_image(self.input_file_content)
except TelegramError:
if self.filename:
self.mimetype = mimetypes.guess_type(
self.filename)[0] or DEFAULT_MIME_TYPE
else:
self.mimetype = DEFAULT_MIME_TYPE
image_mime_type = self.is_image(self.input_file_content)
if image_mime_type:
self.mimetype = image_mime_type
elif self.filename:
self.mimetype = mimetypes.guess_type(self.filename)[0] or DEFAULT_MIME_TYPE
else:
self.mimetype = DEFAULT_MIME_TYPE
if not self.filename:
self.filename = self.mimetype.replace('/', '.')
@@ -76,21 +75,27 @@ class InputFile:
return self.filename, self.input_file_content, self.mimetype
@staticmethod
def is_image(stream: bytes) -> str:
def is_image(stream: bytes) -> Optional[str]:
"""Check if the content file is an image by analyzing its headers.
Args:
stream (:obj:`bytes`): A byte stream representing the content of a file.
Returns:
:obj:`str`: The str mime-type of an image.
:obj:`str` | :obj:`None`: The mime-type of an image, if the input is an image, or
:obj:`None` else.
"""
image = imghdr.what(None, stream)
if image:
return 'image/%s' % image
raise TelegramError('Could not parse file content')
try:
image = imghdr.what(None, stream)
if image:
return f'image/{image}'
return None
except Exception:
logger.debug(
"Could not parse file content. Assuming that file is not an image.", exc_info=True
)
return None
@staticmethod
def is_file(obj: object) -> bool:
+134 -97
View File
@@ -18,12 +18,20 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""Base class for Telegram InputMedia Objects."""
from telegram import TelegramObject, InputFile, PhotoSize, Animation, Video, Audio, Document
from telegram.utils.helpers import DEFAULT_NONE, DefaultValue
from typing import Union, List, Tuple
from typing import Union, IO, cast
from telegram.utils.types import FileLike
from telegram import (
Animation,
Audio,
Document,
InputFile,
PhotoSize,
TelegramObject,
Video,
MessageEntity,
)
from telegram.utils.helpers import DEFAULT_NONE, DefaultValue, parse_file_input
from telegram.utils.types import FileInput, JSONDict
class InputMedia(TelegramObject):
@@ -34,7 +42,18 @@ class InputMedia(TelegramObject):
:class:`telegram.InputMediaVideo` for detailed use.
"""
pass
caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...], None] = None
def to_dict(self) -> JSONDict:
data = super().to_dict()
if self.caption_entities:
data['caption_entities'] = [
ce.to_dict() for ce in self.caption_entities # pylint: disable=E1133
]
return data
class InputMediaAnimation(InputMedia):
@@ -45,6 +64,8 @@ class InputMediaAnimation(InputMedia):
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.
caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
entities that appear in the caption.
thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send.
width (:obj:`int`): Optional. Animation width.
height (:obj:`int`): Optional. Animation height.
@@ -52,11 +73,13 @@ class InputMediaAnimation(InputMedia):
Args:
media (:obj:`str` | `filelike object` | :class:`telegram.Animation`): File to send. Pass a
media (:obj:`str` | `filelike object` | :class:`pathlib.Path` | \
: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
thumb (`filelike object` | :class:`pathlib.Path`, 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.
@@ -66,6 +89,8 @@ class InputMediaAnimation(InputMedia):
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.
caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
entities that appear in the caption, which can be specified instead of parse_mode.
width (:obj:`int`, optional): Animation width.
height (:obj:`int`, optional): Animation height.
duration (:obj:`int`, optional): Animation duration.
@@ -76,14 +101,17 @@ class InputMediaAnimation(InputMedia):
arguments.
"""
def __init__(self,
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):
def __init__(
self,
media: Union[FileInput, Animation],
thumb: FileInput = None,
caption: str = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE,
width: int = None,
height: int = None,
duration: int = None,
caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
):
self.type = 'animation'
if isinstance(media, Animation):
@@ -91,22 +119,16 @@ class InputMediaAnimation(InputMedia):
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 # type: ignore[assignment]
self.media = parse_file_input(media, attach=True)
if thumb:
if InputFile.is_file(thumb):
thumb = cast(IO, thumb)
self.thumb = InputFile(thumb, attach=True)
else:
self.thumb = thumb # type: ignore[assignment]
self.thumb = parse_file_input(thumb, attach=True)
if caption:
self.caption = caption
self.parse_mode = parse_mode
self.caption_entities = caption_entities
if width:
self.width = width
if height:
@@ -123,9 +145,12 @@ class InputMediaPhoto(InputMedia):
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.
caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
entities that appear in the caption.
Args:
media (:obj:`str` | `filelike object` | :class:`telegram.PhotoSize`): File to send. Pass a
media (:obj:`str` | `filelike object` | :class:`pathlib.Path` | \
: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.
@@ -134,25 +159,24 @@ class InputMediaPhoto(InputMedia):
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.
caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
entities that appear in the caption, which can be specified instead of parse_mode.
"""
def __init__(self,
media: Union[str, FileLike, PhotoSize],
caption: str = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE):
def __init__(
self,
media: Union[FileInput, PhotoSize],
caption: str = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE,
caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
):
self.type = 'photo'
if isinstance(media, PhotoSize):
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 # type: ignore[assignment]
self.media = parse_file_input(media, PhotoSize, attach=True)
if caption:
self.caption = caption
self.parse_mode = parse_mode
self.caption_entities = caption_entities
class InputMediaVideo(InputMedia):
@@ -163,6 +187,8 @@ class InputMediaVideo(InputMedia):
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.
caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
entities that appear in the caption.
width (:obj:`int`): Optional. Video width.
height (:obj:`int`): Optional. Video height.
duration (:obj:`int`): Optional. Video duration.
@@ -171,7 +197,8 @@ class InputMediaVideo(InputMedia):
thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send.
Args:
media (:obj:`str` | `filelike object` | :class:`telegram.Video`): File to send. Pass a
media (:obj:`str` | `filelike object` | :class:`pathlib.Path` | :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.
@@ -180,12 +207,15 @@ class InputMediaVideo(InputMedia):
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.
caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
entities that appear in the caption, which can be specified instead of parse_mode.
width (:obj:`int`, optional): Video width.
height (:obj:`int`, optional): Video height.
duration (:obj:`int`, optional): Video duration.
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
thumb (`filelike object` | :class:`pathlib.Path`, 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.
@@ -200,15 +230,18 @@ class InputMediaVideo(InputMedia):
by Telegram.
"""
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):
def __init__(
self,
media: Union[FileInput, 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: FileInput = None,
caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
):
self.type = 'video'
if isinstance(media, Video):
@@ -216,22 +249,16 @@ class InputMediaVideo(InputMedia):
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 # type: ignore[assignment]
self.media = parse_file_input(media, attach=True)
if thumb:
if InputFile.is_file(thumb):
thumb = cast(IO, thumb)
self.thumb = InputFile(thumb, attach=True)
else:
self.thumb = thumb # type: ignore[assignment]
self.thumb = parse_file_input(thumb, attach=True)
if caption:
self.caption = caption
self.parse_mode = parse_mode
self.caption_entities = caption_entities
if width:
self.width = width
if height:
@@ -250,6 +277,8 @@ class InputMediaAudio(InputMedia):
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.
caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
entities that appear in the caption.
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.
@@ -257,7 +286,8 @@ class InputMediaAudio(InputMedia):
thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send.
Args:
media (:obj:`str` | `filelike object` | :class:`telegram.Audio`): File to send. Pass a
media (:obj:`str` | `filelike object` | :class:`pathlib.Path` | :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.
@@ -266,11 +296,14 @@ class InputMediaAudio(InputMedia):
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.
caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
entities that appear in the caption, which can be specified instead of parse_mode.
duration (:obj:`int`): Duration of the audio in seconds as defined by sender.
performer (:obj:`str`, optional): Performer of the audio as defined by sender or by audio
tags.
title (:obj:`str`, optional): Title of the audio as defined by sender or by audio tags.
thumb (`filelike object`, optional): Thumbnail of the file sent; can be ignored if
thumb (`filelike object` | :class:`pathlib.Path`, 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.
@@ -282,14 +315,17 @@ class InputMediaAudio(InputMedia):
optional arguments.
"""
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):
def __init__(
self,
media: Union[FileInput, Audio],
thumb: FileInput = None,
caption: str = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE,
duration: int = None,
performer: str = None,
title: str = None,
caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
):
self.type = 'audio'
if isinstance(media, Audio):
@@ -297,22 +333,16 @@ class InputMediaAudio(InputMedia):
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 # type: ignore[assignment]
self.media = parse_file_input(media, attach=True)
if thumb:
if InputFile.is_file(thumb):
thumb = cast(IO, thumb)
self.thumb = InputFile(thumb, attach=True)
else:
self.thumb = thumb # type: ignore[assignment]
self.thumb = parse_file_input(thumb, attach=True)
if caption:
self.caption = caption
self.parse_mode = parse_mode
self.caption_entities = caption_entities
if duration:
self.duration = duration
if performer:
@@ -329,10 +359,16 @@ class InputMediaDocument(InputMedia):
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.
caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
entities that appear in the caption.
thumb (:class:`telegram.InputFile`): Optional. Thumbnail of the file to send.
disable_content_type_detection (:obj:`bool`): Optional. Disables automatic server-side
content type detection for files uploaded using multipart/form-data. Always true, if
the document is sent as part of an album.
Args:
media (:obj:`str` | `filelike object` | :class:`telegram.Document`): File to send. Pass a
media (:obj:`str` | `filelike object` | :class:`pathlib.Path` | \
: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.
@@ -341,35 +377,36 @@ class InputMediaDocument(InputMedia):
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; can be ignored if
caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
entities that appear in the caption, which can be specified instead of parse_mode.
thumb (`filelike object` | :class:`pathlib.Path`, 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.
disable_content_type_detection (:obj:`bool`, optional): Disables automatic server-side
content type detection for files uploaded using multipart/form-data. Always true, if
the document is sent as part of an album.
"""
def __init__(self,
media: Union[str, FileLike, Document],
thumb: FileLike = None,
caption: str = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE):
def __init__(
self,
media: Union[FileInput, Document],
thumb: FileInput = None,
caption: str = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE,
disable_content_type_detection: bool = None,
caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
):
self.type = 'document'
if isinstance(media, Document):
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 # type: ignore[assignment]
self.media = parse_file_input(media, Document, attach=True)
if thumb:
if InputFile.is_file(thumb):
thumb = cast(IO, thumb)
self.thumb = InputFile(thumb, attach=True)
else:
self.thumb = thumb # type: ignore[assignment]
self.thumb = parse_file_input(thumb, attach=True)
if caption:
self.caption = caption
self.parse_mode = parse_mode
self.caption_entities = caption_entities
self.disable_content_type_detection = disable_content_type_detection
+36 -2
View File
@@ -18,9 +18,10 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Location."""
from telegram import TelegramObject
from typing import Any
from telegram import TelegramObject
class Location(TelegramObject):
"""This object represents a point on the map.
@@ -31,17 +32,50 @@ class Location(TelegramObject):
Attributes:
longitude (:obj:`float`): Longitude as defined by sender.
latitude (:obj:`float`): Latitude as defined by sender.
horizontal_accuracy (:obj:`float`): Optional. The radius of uncertainty for the location,
measured in meters.
live_period (:obj:`int`): Optional. Time relative to the message sending date, during which
the location can be updated, in seconds. For active live locations only.
heading (:obj:`int`): Optional. The direction in which user is moving, in degrees.
For active live locations only.
proximity_alert_radius (:obj:`int`): Optional. Maximum distance for proximity alerts about
approaching another chat member, in meters. For sent live locations only.
Args:
longitude (:obj:`float`): Longitude as defined by sender.
latitude (:obj:`float`): Latitude as defined by sender.
horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location,
measured in meters; 0-1500.
live_period (:obj:`int`, optional): Time relative to the message sending date, during which
the location can be updated, in seconds. For active live locations only.
heading (:obj:`int`, optional): The direction in which user is moving, in degrees; 1-360.
For active live locations only.
proximity_alert_radius (:obj:`int`, optional): Maximum distance for proximity alerts about
approaching another chat member, in meters. For sent live locations only.
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
def __init__(self, longitude: float, latitude: float, **kwargs: Any):
def __init__(
self,
longitude: float,
latitude: float,
horizontal_accuracy: float = None,
live_period: int = None,
heading: int = None,
proximity_alert_radius: int = None,
**_kwargs: Any,
):
# Required
self.longitude = float(longitude)
self.latitude = float(latitude)
# Optionals
self.horizontal_accuracy = float(horizontal_accuracy) if horizontal_accuracy else None
self.live_period = int(live_period) if live_period else None
self.heading = int(heading) if heading else None
self.proximity_alert_radius = (
int(proximity_alert_radius) if proximity_alert_radius else None
)
self._id_attrs = (self.longitude, self.latitude)
+13 -9
View File
@@ -18,9 +18,11 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram PhotoSize."""
from typing import TYPE_CHECKING, Any
from telegram import TelegramObject
from telegram.utils.types import JSONDict
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot, File
@@ -55,14 +57,16 @@ class PhotoSize(TelegramObject):
"""
def __init__(self,
file_id: str,
file_unique_id: str,
width: int,
height: int,
file_size: int = None,
bot: 'Bot' = None,
**kwargs: Any):
def __init__(
self,
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)
+39 -33
View File
@@ -18,9 +18,11 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains objects that represents stickers."""
from telegram import PhotoSize, TelegramObject
from typing import TYPE_CHECKING, Any, List, Optional, ClassVar
from telegram import PhotoSize, TelegramObject, constants
from telegram.utils.types import JSONDict
from typing import Any, Optional, List, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot, File
@@ -71,19 +73,21 @@ class Sticker(TelegramObject):
"""
def __init__(self,
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):
def __init__(
self,
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)
@@ -158,15 +162,16 @@ class StickerSet(TelegramObject):
"""
def __init__(self,
name: str,
title: str,
is_animated: bool,
contains_masks: bool,
stickers: List[Sticker],
bot: 'Bot' = None,
thumb: PhotoSize = None,
**kwargs: Any):
def __init__(
self,
name: str,
title: str,
is_animated: bool,
contains_masks: bool,
stickers: List[Sticker],
thumb: PhotoSize = None,
**_kwargs: Any,
):
self.name = name
self.title = title
self.is_animated = is_animated
@@ -227,16 +232,17 @@ class MaskPosition(TelegramObject):
scale (:obj:`float`): Mask scaling coefficient. For example, 2.0 means double size.
"""
FOREHEAD: str = 'forehead'
""":obj:`str`: 'forehead'"""
EYES: str = 'eyes'
""":obj:`str`: 'eyes'"""
MOUTH: str = 'mouth'
""":obj:`str`: 'mouth'"""
CHIN: str = 'chin'
""":obj:`str`: 'chin'"""
def __init__(self, point: str, x_shift: float, y_shift: float, scale: float, **kwargs: Any):
FOREHEAD: ClassVar[str] = constants.STICKER_FOREHEAD
""":const:`telegram.constants.STICKER_FOREHEAD`"""
EYES: ClassVar[str] = constants.STICKER_EYES
""":const:`telegram.constants.STICKER_EYES`"""
MOUTH: ClassVar[str] = constants.STICKER_MOUTH
""":const:`telegram.constants.STICKER_MOUTH`"""
CHIN: ClassVar[str] = constants.STICKER_CHIN
""":const:`telegram.constants.STICKER_CHIN`"""
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
+27 -11
View File
@@ -18,9 +18,11 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Venue."""
from telegram import TelegramObject, Location
from typing import TYPE_CHECKING, Any, Optional
from telegram import Location, TelegramObject
from telegram.utils.types import JSONDict
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot
@@ -31,13 +33,18 @@ class Venue(TelegramObject):
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.
Note:
Foursquare details and Google Pace details are mutually exclusive. However, this
behaviour is undocumented and might be changed by Telegram.
Attributes:
location (:class:`telegram.Location`): Venue location.
title (:obj:`str`): Name of the venue.
address (:obj:`str`): Address of the venue.
foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue.
foursquare_type (:obj:`str`): Optional. Foursquare type of the venue. (For example,
"arts_entertainment/default", "arts_entertainment/aquarium" or "food/icecream".)
foursquare_type (:obj:`str`): Optional. Foursquare type of the venue.
google_place_id (:obj:`str`): Optional. Google Places identifier of the venue.
google_place_type (:obj:`str`): Optional. Google Places type of the venue.
Args:
location (:class:`telegram.Location`): Venue location.
@@ -46,17 +53,24 @@ class Venue(TelegramObject):
foursquare_id (:obj:`str`, optional): Foursquare identifier of the venue.
foursquare_type (:obj:`str`, optional): Foursquare type of the venue. (For example,
"arts_entertainment/default", "arts_entertainment/aquarium" or "food/icecream".)
google_place_id (:obj:`str`, optional): Google Places identifier of the venue.
google_place_type (:obj:`str`, optional): Google Places type of the venue. (See
`supported types <https://developers.google.com/places/web-service/supported_types>`_.)
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
"""
def __init__(self,
location: Location,
title: str,
address: str,
foursquare_id: str = None,
foursquare_type: str = None,
**kwargs: Any):
def __init__(
self,
location: Location,
title: str,
address: str,
foursquare_id: str = None,
foursquare_type: str = None,
google_place_id: str = None,
google_place_type: str = None,
**_kwargs: Any,
):
# Required
self.location = location
self.title = title
@@ -64,6 +78,8 @@ class Venue(TelegramObject):
# Optionals
self.foursquare_id = foursquare_id
self.foursquare_type = foursquare_type
self.google_place_id = google_place_id
self.google_place_type = google_place_type
self._id_attrs = (self.location, self.title)
+20 -12
View File
@@ -18,9 +18,11 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Video."""
from typing import TYPE_CHECKING, Any, Optional
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
@@ -40,6 +42,7 @@ class Video(TelegramObject):
height (:obj:`int`): Video height as defined by sender.
duration (:obj:`int`): Duration of the video in seconds as defined by sender.
thumb (:class:`telegram.PhotoSize`): Optional. Video thumbnail.
file_name (:obj:`str`): Optional. Original filename as defined by sender.
mime_type (:obj:`str`): Optional. Mime type of a file as defined by sender.
file_size (:obj:`int`): Optional. File size.
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
@@ -54,6 +57,7 @@ class Video(TelegramObject):
height (:obj:`int`): Video height as defined by sender.
duration (:obj:`int`): Duration of the video in seconds as defined by sender.
thumb (:class:`telegram.PhotoSize`, optional): Video thumbnail.
file_name (:obj:`str`, optional): Original filename as defined by sender.
mime_type (:obj:`str`, optional): Mime type of a file as defined by sender.
file_size (:obj:`int`, optional): File size.
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
@@ -61,17 +65,20 @@ class Video(TelegramObject):
"""
def __init__(self,
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):
def __init__(
self,
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,
file_name: str = None,
**_kwargs: Any,
):
# Required
self.file_id = str(file_id)
self.file_unique_id = str(file_unique_id)
@@ -80,6 +87,7 @@ class Video(TelegramObject):
self.duration = int(duration)
# Optionals
self.thumb = thumb
self.file_name = file_name
self.mime_type = mime_type
self.file_size = file_size
self.bot = bot
+14 -10
View File
@@ -18,9 +18,11 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram VideoNote."""
from typing import TYPE_CHECKING, Any, Optional
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
@@ -58,15 +60,17 @@ class VideoNote(TelegramObject):
"""
def __init__(self,
file_id: str,
file_unique_id: str,
length: int,
duration: int,
thumb: PhotoSize = None,
file_size: int = None,
bot: 'Bot' = None,
**kwargs: Any):
def __init__(
self,
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)
+13 -9
View File
@@ -18,9 +18,11 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Voice."""
from typing import TYPE_CHECKING, Any
from telegram import TelegramObject
from telegram.utils.types import JSONDict
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot, File
@@ -55,14 +57,16 @@ class Voice(TelegramObject):
"""
def __init__(self,
file_id: str,
file_unique_id: str,
duration: int,
mime_type: str = None,
file_size: int = None,
bot: 'Bot' = None,
**kwargs: Any):
def __init__(
self,
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)
+3 -2
View File
@@ -18,9 +18,10 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram ForceReply."""
from telegram import ReplyMarkup
from typing import Any
from telegram import ReplyMarkup
class ForceReply(ReplyMarkup):
"""
@@ -49,7 +50,7 @@ class ForceReply(ReplyMarkup):
"""
def __init__(self, force_reply: bool = True, selective: bool = False, **kwargs: Any):
def __init__(self, force_reply: bool = True, selective: bool = False, **_kwargs: Any):
# Required
self.force_reply = bool(force_reply)
# Optionals
+19 -16
View File
@@ -19,10 +19,11 @@
"""This module contains an object that represents a Telegram Game."""
import sys
from typing import TYPE_CHECKING, Any, Dict, List, Optional
from telegram import MessageEntity, TelegramObject, Animation, PhotoSize
from telegram import Animation, MessageEntity, PhotoSize, TelegramObject
from telegram.utils.types import JSONDict
from typing import List, Any, Dict, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot
@@ -66,14 +67,16 @@ class Game(TelegramObject):
"""
def __init__(self,
title: str,
description: str,
photo: List[PhotoSize],
text: str = None,
text_entities: List[MessageEntity] = None,
animation: Animation = None,
**kwargs: Any):
def __init__(
self,
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
@@ -130,11 +133,10 @@ class Game(TelegramObject):
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]
else:
entity_text = self.text.encode('utf-16-le')
entity_text = entity_text[entity.offset * 2:(entity.offset + entity.length) * 2]
if sys.maxunicode == 0xFFFF:
return self.text[entity.offset : entity.offset + entity.length]
entity_text = self.text.encode('utf-16-le')
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
return entity_text.decode('utf-16-le')
@@ -164,7 +166,8 @@ class Game(TelegramObject):
return {
entity: self.parse_text_entity(entity)
for entity in (self.text_entities or []) if entity.type in types
for entity in (self.text_entities or [])
if entity.type in types
}
def __hash__(self) -> int:
+3 -1
View File
@@ -18,9 +18,11 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram GameHighScore."""
from typing import TYPE_CHECKING, Optional
from telegram import TelegramObject, User
from telegram.utils.types import JSONDict
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot
+15 -11
View File
@@ -18,8 +18,10 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram InlineKeyboardButton."""
from typing import TYPE_CHECKING, Any
from telegram import TelegramObject
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import CallbackGame, LoginUrl
@@ -81,16 +83,18 @@ class InlineKeyboardButton(TelegramObject):
"""
def __init__(self,
text: str,
url: str = None,
callback_data: str = None,
switch_inline_query: str = None,
switch_inline_query_current_chat: str = None,
callback_game: 'CallbackGame' = None,
pay: bool = None,
login_url: 'LoginUrl' = None,
**kwargs: Any):
def __init__(
self,
text: str,
url: str = None,
callback_data: str = None,
switch_inline_query: str = None,
switch_inline_query_current_chat: str = None,
callback_game: 'CallbackGame' = None,
pay: bool = None,
login_url: 'LoginUrl' = None,
**_kwargs: Any,
):
# Required
self.text = text
+13 -10
View File
@@ -18,9 +18,11 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram InlineKeyboardMarkup."""
from telegram import ReplyMarkup, InlineKeyboardButton
from typing import TYPE_CHECKING, Any, List, Optional
from telegram import InlineKeyboardButton, ReplyMarkup
from telegram.utils.types import JSONDict
from typing import Any, List, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot
@@ -43,7 +45,7 @@ class InlineKeyboardMarkup(ReplyMarkup):
"""
def __init__(self, inline_keyboard: List[List[InlineKeyboardButton]], **kwargs: Any):
def __init__(self, inline_keyboard: List[List[InlineKeyboardButton]], **_kwargs: Any):
# Required
self.inline_keyboard = inline_keyboard
@@ -57,8 +59,7 @@ class InlineKeyboardMarkup(ReplyMarkup):
return data
@classmethod
def de_json(cls, data: Optional[JSONDict],
bot: 'Bot') -> Optional['InlineKeyboardMarkup']:
def de_json(cls, data: Optional[JSONDict], bot: 'Bot') -> Optional['InlineKeyboardMarkup']:
data = cls.parse_data(data)
if not data:
@@ -91,8 +92,9 @@ class InlineKeyboardMarkup(ReplyMarkup):
return cls([[button]], **kwargs)
@classmethod
def from_row(cls, button_row: List[InlineKeyboardButton],
**kwargs: Any) -> 'InlineKeyboardMarkup':
def from_row(
cls, button_row: List[InlineKeyboardButton], **kwargs: Any
) -> 'InlineKeyboardMarkup':
"""Shortcut for::
InlineKeyboardMarkup([button_row], **kwargs)
@@ -108,8 +110,9 @@ class InlineKeyboardMarkup(ReplyMarkup):
return cls([button_row], **kwargs)
@classmethod
def from_column(cls, button_column: List[InlineKeyboardButton],
**kwargs: Any) -> 'InlineKeyboardMarkup':
def from_column(
cls, button_column: List[InlineKeyboardButton], **kwargs: Any
) -> 'InlineKeyboardMarkup':
"""Shortcut for::
InlineKeyboardMarkup([[button] for button in button_column], **kwargs)
@@ -136,7 +139,7 @@ class InlineKeyboardMarkup(ReplyMarkup):
if button != other.inline_keyboard[idx][jdx]:
return False
return True
return super(InlineKeyboardMarkup, self).__eq__(other) # pylint: disable=no-member
return super().__eq__(other)
def __hash__(self) -> int:
return hash(tuple(tuple(button for button in row) for row in self.inline_keyboard))
+17 -16
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python
# pylint: disable=R0902,R0912,R0913
# pylint: disable=R0902,R0913
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2020
@@ -19,9 +19,11 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram InlineQuery."""
from telegram import TelegramObject, User, Location
from typing import TYPE_CHECKING, Any, Optional
from telegram import Location, TelegramObject, User
from telegram.utils.types import JSONDict
from typing import Any, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import Bot
@@ -57,16 +59,18 @@ class InlineQuery(TelegramObject):
"""
def __init__(self,
id: str,
from_user: User,
query: str,
offset: str,
location: Location = None,
bot: 'Bot' = None,
**kwargs: Any):
def __init__(
self,
id: str, # pylint: disable=W0622
from_user: User,
query: str,
offset: str,
location: Location = None,
bot: 'Bot' = None,
**_kwargs: Any,
):
# Required
self.id = id
self.id = id # pylint: disable=C0103
self.from_user = from_user
self.query = query
self.offset = offset
@@ -125,8 +129,5 @@ class InlineQuery(TelegramObject):
"""
return self.bot.answer_inline_query(
self.id,
*args,
current_offset=self.offset if auto_pagination else None,
**kwargs
self.id, *args, current_offset=self.offset if auto_pagination else None, **kwargs
)
+20 -3
View File
@@ -16,11 +16,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/].
# pylint: disable=W0622
"""This module contains the classes that represent Telegram InlineQueryResult."""
from telegram import TelegramObject
from typing import Any
from telegram import TelegramObject
from telegram.utils.types import JSONDict
class InlineQueryResult(TelegramObject):
"""Baseclass for the InlineQueryResult* classes.
@@ -39,10 +42,10 @@ class InlineQueryResult(TelegramObject):
"""
def __init__(self, type: str, id: str, **kwargs: Any):
def __init__(self, type: str, id: str, **_kwargs: Any):
# Required
self.type = str(type)
self.id = str(id)
self.id = str(id) # pylint: disable=C0103
self._id_attrs = (self.id,)
@@ -53,3 +56,17 @@ class InlineQueryResult(TelegramObject):
@property
def _has_input_message_content(self) -> bool:
return hasattr(self, 'input_message_content')
def to_dict(self) -> JSONDict:
data = super().to_dict()
# pylint: disable=E1101
if (
hasattr(self, 'caption_entities')
and self.caption_entities # type: ignore[attr-defined]
):
data['caption_entities'] = [
ce.to_dict() for ce in self.caption_entities # type: ignore[attr-defined]
]
return data
+17 -13
View File
@@ -18,8 +18,10 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultArticle."""
from typing import TYPE_CHECKING, Any
from telegram import InlineQueryResult
from typing import Any, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import InputMessageContent, ReplyMarkup
@@ -61,18 +63,20 @@ class InlineQueryResultArticle(InlineQueryResult):
"""
def __init__(self,
id: str,
title: str,
input_message_content: 'InputMessageContent',
reply_markup: 'ReplyMarkup' = None,
url: str = None,
hide_url: bool = None,
description: str = None,
thumb_url: str = None,
thumb_width: int = None,
thumb_height: int = None,
**kwargs: Any):
def __init__(
self,
id: str, # pylint: disable=W0622
title: str,
input_message_content: 'InputMessageContent',
reply_markup: 'ReplyMarkup' = None,
url: str = None,
hide_url: bool = None,
description: str = None,
thumb_url: str = None,
thumb_width: int = None,
thumb_height: int = None,
**_kwargs: Any,
):
# Required
super().__init__('article', id)
+25 -13
View File
@@ -18,9 +18,11 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultAudio."""
from telegram import InlineQueryResult
from typing import TYPE_CHECKING, Any, Union, Tuple, List
from telegram import InlineQueryResult, MessageEntity
from telegram.utils.helpers import DEFAULT_NONE, DefaultValue
from typing import Any, Union, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import InputMessageContent, ReplyMarkup
@@ -42,6 +44,9 @@ class InlineQueryResultAudio(InlineQueryResult):
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.
caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
entities that appear in the caption, which can be specified instead of
:attr:`parse_mode`.
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
@@ -57,6 +62,9 @@ class InlineQueryResultAudio(InlineQueryResult):
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.
caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
entities that appear in the caption, which can be specified instead of
:attr:`parse_mode`.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
@@ -65,17 +73,20 @@ class InlineQueryResultAudio(InlineQueryResult):
"""
def __init__(self,
id: str,
audio_url: str,
title: str,
performer: str = None,
audio_duration: int = None,
caption: str = None,
reply_markup: 'ReplyMarkup' = None,
input_message_content: 'InputMessageContent' = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE,
**kwargs: Any):
def __init__(
self,
id: str, # pylint: disable=W0622
audio_url: str,
title: str,
performer: str = None,
audio_duration: int = None,
caption: str = None,
reply_markup: 'ReplyMarkup' = None,
input_message_content: 'InputMessageContent' = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE,
caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
**_kwargs: Any,
):
# Required
super().__init__('audio', id)
@@ -87,5 +98,6 @@ class InlineQueryResultAudio(InlineQueryResult):
self.audio_duration = audio_duration
self.caption = caption
self.parse_mode = parse_mode
self.caption_entities = caption_entities
self.reply_markup = reply_markup
self.input_message_content = input_message_content
+22 -10
View File
@@ -18,9 +18,11 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains the classes that represent Telegram InlineQueryResultCachedAudio."""
from telegram import InlineQueryResult
from typing import TYPE_CHECKING, Any, Union, Tuple, List
from telegram import InlineQueryResult, MessageEntity
from telegram.utils.helpers import DEFAULT_NONE, DefaultValue
from typing import Any, Union, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import InputMessageContent, ReplyMarkup
@@ -39,6 +41,9 @@ class InlineQueryResultCachedAudio(InlineQueryResult):
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.
caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
entities that appear in the caption, which can be specified instead of
:attr:`parse_mode`.
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
@@ -51,6 +56,9 @@ class InlineQueryResultCachedAudio(InlineQueryResult):
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.
caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
entities that appear in the caption, which can be specified instead of
:attr:`parse_mode`.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
@@ -59,14 +67,17 @@ class InlineQueryResultCachedAudio(InlineQueryResult):
"""
def __init__(self,
id: str,
audio_file_id: str,
caption: str = None,
reply_markup: 'ReplyMarkup' = None,
input_message_content: 'InputMessageContent' = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE,
**kwargs: Any):
def __init__(
self,
id: str, # pylint: disable=W0622
audio_file_id: str,
caption: str = None,
reply_markup: 'ReplyMarkup' = None,
input_message_content: 'InputMessageContent' = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE,
caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
**_kwargs: Any,
):
# Required
super().__init__('audio', id)
self.audio_file_id = audio_file_id
@@ -74,5 +85,6 @@ class InlineQueryResultCachedAudio(InlineQueryResult):
# Optionals
self.caption = caption
self.parse_mode = parse_mode
self.caption_entities = caption_entities
self.reply_markup = reply_markup
self.input_message_content = input_message_content
@@ -16,11 +16,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/].
# pylint: disable=W0622
"""This module contains the classes that represent Telegram InlineQueryResultCachedDocument."""
from telegram import InlineQueryResult
from typing import TYPE_CHECKING, Any, Union, Tuple, List
from telegram import InlineQueryResult, MessageEntity
from telegram.utils.helpers import DEFAULT_NONE, DefaultValue
from typing import Any, Union, TYPE_CHECKING
if TYPE_CHECKING:
from telegram import InputMessageContent, ReplyMarkup
@@ -42,6 +45,9 @@ class InlineQueryResultCachedDocument(InlineQueryResult):
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.
caption_entities (List[:class:`telegram.MessageEntity`]): Optional. List of special
entities that appear in the caption, which can be specified instead of
:attr:`parse_mode`.
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
@@ -57,6 +63,9 @@ class InlineQueryResultCachedDocument(InlineQueryResult):
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.
caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
entities that appear in the caption, which can be specified instead of
:attr:`parse_mode`.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message.
input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
@@ -65,16 +74,19 @@ class InlineQueryResultCachedDocument(InlineQueryResult):
"""
def __init__(self,
id: str,
title: str,
document_file_id: str,
description: str = None,
caption: str = None,
reply_markup: 'ReplyMarkup' = None,
input_message_content: 'InputMessageContent' = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE,
**kwargs: Any):
def __init__(
self,
id: str, # pylint: disable=W0622
title: str,
document_file_id: str,
description: str = None,
caption: str = None,
reply_markup: 'ReplyMarkup' = None,
input_message_content: 'InputMessageContent' = None,
parse_mode: Union[str, DefaultValue] = DEFAULT_NONE,
caption_entities: Union[Tuple[MessageEntity, ...], List[MessageEntity]] = None,
**_kwargs: Any,
):
# Required
super().__init__('document', id)
self.title = title
@@ -84,5 +96,6 @@ class InlineQueryResultCachedDocument(InlineQueryResult):
self.description = description
self.caption = caption
self.parse_mode = parse_mode
self.caption_entities = caption_entities
self.reply_markup = reply_markup
self.input_message_content = input_message_content

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