mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2024-12-22 06:25:12 +01:00
Switch to asyncio
(#2731)
Co-authored-by: tsnoam <tsnoam@gmail.com> Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com> Co-authored-by: Poolitzer <25934244+Poolitzer@users.noreply.github.com>
This commit is contained in:
parent
a743726b08
commit
42eaa67fd5
203 changed files with 22819 additions and 15229 deletions
|
@ -5,7 +5,6 @@ test_patterns = ["tests/**"]
|
|||
exclude_patterns = [
|
||||
"tests/**",
|
||||
"docs/**",
|
||||
"telegram/vendor/**",
|
||||
"setup.py",
|
||||
"setup-raw.py"
|
||||
]
|
||||
|
|
11
.github/CONTRIBUTING.rst
vendored
11
.github/CONTRIBUTING.rst
vendored
|
@ -153,12 +153,6 @@ Here's how to make a one-off code change.
|
|||
$ git commit -a
|
||||
$ git push origin your-branch-name
|
||||
|
||||
- If after merging you see local modified files in ``telegram/vendor/`` directory, that you didn't actually touch, that means you need to update submodules with this command:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ git submodule update --init --recursive
|
||||
|
||||
- At the end, the reviewer will merge the pull request.
|
||||
|
||||
6. **Tidy up!** Delete the feature branch from both your local clone and the GitHub repository:
|
||||
|
@ -260,11 +254,12 @@ break the API classes. For example:
|
|||
|
||||
# GOOD
|
||||
def __init__(self, id, name, last_name=None, **kwargs):
|
||||
self.last_name = last_name
|
||||
self.last_name = last_name
|
||||
|
||||
|
||||
# BAD
|
||||
def __init__(self, id, name, last_name=None):
|
||||
self.last_name = last_name
|
||||
self.last_name = last_name
|
||||
|
||||
|
||||
.. _`Code of Conduct`: https://www.python.org/psf/codeofconduct/
|
||||
|
|
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
|
@ -8,7 +8,7 @@ Hey! You're PRing? Cool! Please have a look at the below checklist. It's here to
|
|||
- [ ] Created new or adapted existing unit tests
|
||||
- [ ] Documented code changes according to the [CSI standard](https://standards.mousepawmedia.com/en/stable/csi.html)
|
||||
- [ ] Added myself alphabetically to `AUTHORS.rst` (optional)
|
||||
- [ ] Added new classes & modules to the docs
|
||||
- [ ] Added new classes & modules to the docs and all suitable `__all__` s
|
||||
|
||||
|
||||
### If the PR contains API changes (otherwise, you can delete this passage)
|
||||
|
|
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
|
@ -18,9 +18,6 @@ jobs:
|
|||
fail-fast: False
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Initialize vendored libs
|
||||
run:
|
||||
git submodule update --init --recursive
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
|
@ -75,9 +72,6 @@ jobs:
|
|||
fail-fast: False
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Initialize vendored libs
|
||||
run:
|
||||
git submodule update --init --recursive
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
|
@ -104,9 +98,6 @@ jobs:
|
|||
fail-fast: False
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Initialize vendored libs
|
||||
run:
|
||||
git submodule update --init --recursive
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
|
|
4
.gitmodules
vendored
4
.gitmodules
vendored
|
@ -1,4 +0,0 @@
|
|||
[submodule "telegram/vendor/urllib3"]
|
||||
path = telegram/vendor/ptb_urllib3
|
||||
url = https://github.com/python-telegram-bot/urllib3.git
|
||||
branch = ptb
|
|
@ -9,6 +9,8 @@ repos:
|
|||
args:
|
||||
- --diff
|
||||
- --check
|
||||
additional_dependencies:
|
||||
- click==8.0.2
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 4.0.1
|
||||
hooks:
|
||||
|
@ -23,7 +25,7 @@ repos:
|
|||
# run pylint across multiple cpu cores to speed it up-
|
||||
- --jobs=0 # See https://pylint.pycqa.org/en/latest/user_guide/run.html?#parallel-execution to know more
|
||||
additional_dependencies:
|
||||
- certifi
|
||||
- httpx >= 0.20.0,<1.0
|
||||
- tornado>=6.1
|
||||
- APScheduler==3.6.3
|
||||
- cachetools==4.2.2
|
||||
|
@ -38,25 +40,24 @@ repos:
|
|||
- types-ujson
|
||||
- types-pytz
|
||||
- types-cryptography
|
||||
- types-certifi
|
||||
- types-cachetools
|
||||
- certifi
|
||||
- tornado>=6.1
|
||||
- APScheduler==3.6.3
|
||||
- cachetools==4.2.2
|
||||
- . # this basically does `pip install -e .`
|
||||
- id: mypy
|
||||
name: mypy-examples
|
||||
files: ^examples/.*\.py$
|
||||
args:
|
||||
- --no-strict-optional
|
||||
- --follow-imports=silent
|
||||
additional_dependencies:
|
||||
- certifi
|
||||
- httpx >= 0.20.0,<1.0
|
||||
- tornado>=6.1
|
||||
- APScheduler==3.6.3
|
||||
- cachetools==4.2.2
|
||||
- . # this basically does `pip install -e .`
|
||||
- id: mypy
|
||||
name: mypy-examples
|
||||
files: ^examples/.*\.py$
|
||||
args:
|
||||
- --no-strict-optional
|
||||
- --follow-imports=silent
|
||||
additional_dependencies:
|
||||
- certifi
|
||||
- tornado>=6.1
|
||||
- APScheduler==3.6.3
|
||||
- cachetools==4.2.2
|
||||
- . # this basically does `pip install -e .`
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.29.0
|
||||
hooks:
|
||||
|
|
|
@ -14,13 +14,6 @@ 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>`_ and `Jasmin Bom <https://github.com/jsmnbom>`_.
|
||||
|
||||
Vendored packages
|
||||
-----------------
|
||||
|
||||
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:
|
||||
`https://github.com/python-telegram-bot/urllib3`.
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
|
|
26
README.rst
26
README.rst
|
@ -113,6 +113,20 @@ Telegram API support
|
|||
|
||||
All types and methods of the Telegram Bot API **5.7** are supported.
|
||||
|
||||
===========
|
||||
Concurrency
|
||||
===========
|
||||
|
||||
Since v14.0, ``python-telegram-bot`` is built on top of Pythons ``asyncio`` module.
|
||||
Because ``asyncio`` is in general single-threaded, ``python-telegram-bot`` does currently not aim to be thread-safe.
|
||||
Noteworthy parts of ``python-telegram-bots`` API that are likely to cause issues (e.g. race conditions) when used in a multi-threaded setting include:
|
||||
|
||||
* ``telegram.ext.Application/Updater.update_queue``
|
||||
* ``telegram.ext.ConversationHandler.check/handle_update``
|
||||
* ``telegram.ext.CallbackDataCache``
|
||||
* ``telegram.ext.BasePersistence``
|
||||
* all classes in the ``telegram.ext.filters`` module that allow to add/remove allowed users/chats at runtime
|
||||
|
||||
==========
|
||||
Installing
|
||||
==========
|
||||
|
@ -130,12 +144,6 @@ Or you can install from source with:
|
|||
$ git clone https://github.com/python-telegram-bot/python-telegram-bot --recursive
|
||||
$ cd python-telegram-bot
|
||||
$ python setup.py install
|
||||
|
||||
In case you have a previously cloned local repository already, you should initialize the added urllib3 submodule before installing with:
|
||||
|
||||
.. code:: shell
|
||||
|
||||
$ git submodule update --init --recursive
|
||||
|
||||
---------------------
|
||||
Optional Dependencies
|
||||
|
@ -182,8 +190,10 @@ This library uses the ``logging`` module. To set up logging to standard output,
|
|||
.. code:: python
|
||||
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
|
||||
at the beginning of your script.
|
||||
|
||||
|
|
|
@ -107,6 +107,13 @@ Telegram API support
|
|||
|
||||
All types and methods of the Telegram Bot API **5.7** are supported.
|
||||
|
||||
===========
|
||||
Concurrency
|
||||
===========
|
||||
|
||||
Since v14.0, ``python-telegram-bot`` is built on top of Pythons ``asyncio`` module.
|
||||
Because ``asyncio`` is in general single-threaded, ``python-telegram-bot`` does currently not aim to be thread-safe.
|
||||
|
||||
==========
|
||||
Installing
|
||||
==========
|
||||
|
@ -125,12 +132,6 @@ Or you can install from source with:
|
|||
$ cd python-telegram-bot
|
||||
$ python setup-raw.py install
|
||||
|
||||
In case you have a previously cloned local repository already, you should initialize the added urllib3 submodule before installing with:
|
||||
|
||||
.. code:: shell
|
||||
|
||||
$ git submodule update --init --recursive
|
||||
|
||||
----
|
||||
Note
|
||||
----
|
||||
|
@ -164,8 +165,10 @@ This library uses the ``logging`` module. To set up logging to standard output,
|
|||
.. code:: python
|
||||
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
|
||||
at the beginning of your script.
|
||||
|
||||
|
|
|
@ -127,6 +127,9 @@ exclude_patterns = []
|
|||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# Decides the language used for syntax highlighting of code blocks.
|
||||
highlight_language = 'python3'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
|
@ -155,7 +158,7 @@ html_theme_options = {
|
|||
"announcement": 'PTB has undergone significant changes in v14. Please read the documentation '
|
||||
'carefully and also check out the transition guide in the '
|
||||
'<a href="https://github.com/python-telegram-bot/python-telegram-bot/wiki">'
|
||||
'wiki</a>',
|
||||
'wiki</a>.',
|
||||
}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
|
@ -453,10 +456,14 @@ def _git_branch() -> str:
|
|||
"""Get's the current git sha if available or fall back to `master`"""
|
||||
try:
|
||||
output = subprocess.check_output( # skipcq: BAN-B607
|
||||
["git", "describe", "--tags"], stderr=subprocess.STDOUT
|
||||
["git", "describe", "--tags", "--always"], stderr=subprocess.STDOUT
|
||||
)
|
||||
return output.decode().strip()
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
sphinx_logger.exception(
|
||||
f'Failed to get a description of the current commit. Falling back to `master`.',
|
||||
exc_info=exc
|
||||
)
|
||||
return 'master'
|
||||
|
||||
|
||||
|
@ -510,7 +517,7 @@ def autodoc_process_bases(app, name, obj, option, bases: list):
|
|||
base = str(base)
|
||||
|
||||
# Special case because base classes are in std lib:
|
||||
if "_StringEnum" in base:
|
||||
if "StringEnum" in base == "<enum 'StringEnum'>":
|
||||
bases[idx] = ":class:`enum.Enum`"
|
||||
bases.insert(0, ':class:`str`')
|
||||
continue
|
||||
|
@ -521,24 +528,24 @@ def autodoc_process_bases(app, name, obj, option, bases: list):
|
|||
bases[idx] = f':class:`{base}`'
|
||||
|
||||
# Now convert `telegram._message.Message` to `telegram.Message` etc
|
||||
match = re.search(pattern=r"(telegram(\.ext|))\.", string=base)
|
||||
if match and '_utils' not in base:
|
||||
base = base.rstrip("'>")
|
||||
parts = base.rsplit(".", maxsplit=2)
|
||||
match = re.search(pattern=r"(telegram(\.ext|))\.[_\w\.]+", string=base)
|
||||
if not match or '_utils' in base:
|
||||
return
|
||||
|
||||
# Replace private base classes with their respective parent
|
||||
parts[-1] = PRIVATE_BASE_CLASSES.get(parts[-1], parts[-1])
|
||||
parts = match.group(0).split(".")
|
||||
|
||||
# To make sure that e.g. `telegram.ext.filters.BaseFilter` keeps the `filters` part
|
||||
if not parts[-2].startswith('_') and '_' not in parts[0]:
|
||||
base = '.'.join(parts[-2:])
|
||||
else:
|
||||
base = parts[-1]
|
||||
# Remove private paths
|
||||
for index, part in enumerate(parts):
|
||||
if part.startswith("_"):
|
||||
parts = parts[:index] + parts[-1:]
|
||||
break
|
||||
|
||||
# add `telegram(.ext).` back in front
|
||||
base = f'{match.group(0)}{base}'
|
||||
# Replace private base classes with their respective parent
|
||||
parts = [PRIVATE_BASE_CLASSES.get(part, part) for part in parts]
|
||||
|
||||
bases[idx] = f':class:`{base}`'
|
||||
base = ".".join(parts)
|
||||
|
||||
bases[idx] = f':class:`{base}`'
|
||||
|
||||
|
||||
def setup(app: Sphinx):
|
||||
|
|
|
@ -10,19 +10,19 @@ Guides and tutorials
|
|||
====================
|
||||
|
||||
If you're just starting out with the library, we recommend following our `"Your first Bot" <https://github.com/python-telegram-bot/python-telegram-bot/wiki/Extensions-%E2%80%93-Your-first-Bot>`_ tutorial that you can find on our `wiki <https://github.com/python-telegram-bot/python-telegram-bot/wiki>`_.
|
||||
On our wiki you will also find guides like how to use handlers, webhooks, emoji, proxies and much more.
|
||||
While being there, you will also find guides to learn how to use handlers, webhooks, proxies, making your bot persistent, and much more.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
A great way to learn is by looking at examples. Ours can be found in our `examples folder on Github <https://github.com/python-telegram-bot/python-telegram-bot/tree/master/examples>`_.
|
||||
A great way to learn is by looking at examples. Ours can be found in our `examples folder on Github <https://github.com/python-telegram-bot/python-telegram-bot/tree/master/examples#examples>`_.
|
||||
|
||||
|
||||
Reference
|
||||
=========
|
||||
|
||||
Below you can find a reference of all the classes and methods in python-telegram-bot.
|
||||
Apart from the `telegram.ext` package the objects should reflect the types defined in the `official Telegram Bot API documentation <https://core.telegram.org/bots/api>`_.
|
||||
Apart from the `telegram.ext` package and the `Auxiliary` modules, the objects reflect the types defined in the `official Telegram Bot API documentation <https://core.telegram.org/bots/api>`_.
|
||||
|
||||
.. toctree::
|
||||
telegram.ext
|
||||
|
|
8
docs/source/telegram.ext.application.rst
Normal file
8
docs/source/telegram.ext.application.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/_application.py
|
||||
|
||||
telegram.ext.Application
|
||||
========================
|
||||
|
||||
.. autoclass:: telegram.ext.Application
|
||||
:members:
|
||||
:show-inheritance:
|
7
docs/source/telegram.ext.applicationbuilder.rst
Normal file
7
docs/source/telegram.ext.applicationbuilder.rst
Normal file
|
@ -0,0 +1,7 @@
|
|||
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/_applicationbuilder.py
|
||||
|
||||
telegram.ext.ApplicationBuilder
|
||||
===============================
|
||||
|
||||
.. autoclass:: telegram.ext.ApplicationBuilder
|
||||
:members:
|
8
docs/source/telegram.ext.applicationhandlerstop.rst
Normal file
8
docs/source/telegram.ext.applicationhandlerstop.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/_application.py
|
||||
|
||||
telegram.ext.ApplicationHandlerStop
|
||||
===================================
|
||||
|
||||
.. autoclass:: telegram.ext.ApplicationHandlerStop
|
||||
:members:
|
||||
:show-inheritance:
|
|
@ -1,8 +0,0 @@
|
|||
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/dispatcher.py
|
||||
|
||||
telegram.ext.Dispatcher
|
||||
=======================
|
||||
|
||||
.. autoclass:: telegram.ext.Dispatcher
|
||||
:members:
|
||||
:show-inheritance:
|
|
@ -1,7 +0,0 @@
|
|||
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/builders.py
|
||||
|
||||
telegram.ext.DispatcherBuilder
|
||||
==============================
|
||||
|
||||
.. autoclass:: telegram.ext.DispatcherBuilder
|
||||
:members:
|
|
@ -1,8 +0,0 @@
|
|||
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/dispatcher.py
|
||||
|
||||
telegram.ext.DispatcherHandlerStop
|
||||
==================================
|
||||
|
||||
.. autoclass:: telegram.ext.DispatcherHandlerStop
|
||||
:members:
|
||||
:show-inheritance:
|
|
@ -4,11 +4,10 @@ telegram.ext package
|
|||
.. toctree::
|
||||
|
||||
telegram.ext.extbot
|
||||
telegram.ext.updaterbuilder
|
||||
telegram.ext.applicationbuilder
|
||||
telegram.ext.application
|
||||
telegram.ext.applicationhandlerstop
|
||||
telegram.ext.updater
|
||||
telegram.ext.dispatcherbuilder
|
||||
telegram.ext.dispatcher
|
||||
telegram.ext.dispatcherhandlerstop
|
||||
telegram.ext.callbackcontext
|
||||
telegram.ext.job
|
||||
telegram.ext.jobqueue
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/ext/builders.py
|
||||
|
||||
telegram.ext.UpdaterBuilder
|
||||
===========================
|
||||
|
||||
.. autoclass:: telegram.ext.UpdaterBuilder
|
||||
:members:
|
8
docs/source/telegram.request.baserequest.rst
Normal file
8
docs/source/telegram.request.baserequest.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/request/_baserequest.py
|
||||
|
||||
telegram.request.BaseRequest
|
||||
============================
|
||||
|
||||
.. autoclass:: telegram.request.BaseRequest
|
||||
:members:
|
||||
:show-inheritance:
|
8
docs/source/telegram.request.httpxrequest.rst
Normal file
8
docs/source/telegram.request.httpxrequest.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/request/_httpxrequest.py
|
||||
|
||||
telegram.request.HTTPXRequest
|
||||
=============================
|
||||
|
||||
.. autoclass:: telegram.request.HTTPXRequest
|
||||
:members:
|
||||
:show-inheritance:
|
8
docs/source/telegram.request.requestdata.rst
Normal file
8
docs/source/telegram.request.requestdata.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/request/_requestdata.py
|
||||
|
||||
telegram.request.RequestData
|
||||
============================
|
||||
|
||||
.. autoclass:: telegram.request.RequestData
|
||||
:members:
|
||||
:show-inheritance:
|
|
@ -1,8 +1,11 @@
|
|||
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/request.py
|
||||
:github_url: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/telegram/request
|
||||
|
||||
telegram.request Module
|
||||
=======================
|
||||
|
||||
.. automodule:: telegram.request
|
||||
:members:
|
||||
:show-inheritance:
|
||||
.. versionadded:: 14.0
|
||||
|
||||
.. toctree::
|
||||
telegram.request.baserequest
|
||||
telegram.request.requestdata
|
||||
telegram.request.httpxrequest
|
||||
|
|
|
@ -16,7 +16,7 @@ from telegram.ext import (
|
|||
CallbackQueryHandler,
|
||||
InvalidCallbackData,
|
||||
PicklePersistence,
|
||||
Updater,
|
||||
Application,
|
||||
CallbackContext,
|
||||
)
|
||||
|
||||
|
@ -28,25 +28,25 @@ logging.basicConfig(
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Sends a message with 5 inline buttons attached."""
|
||||
number_list: List[int] = []
|
||||
update.message.reply_text('Please choose:', reply_markup=build_keyboard(number_list))
|
||||
await update.message.reply_text('Please choose:', reply_markup=build_keyboard(number_list))
|
||||
|
||||
|
||||
def help_command(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def help_command(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Displays info on how to use the bot."""
|
||||
update.message.reply_text(
|
||||
await update.message.reply_text(
|
||||
"Use /start to test this bot. Use /clear to clear the stored data so that you can see "
|
||||
"what happens, if the button data is not available. "
|
||||
)
|
||||
|
||||
|
||||
def clear(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def clear(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Clears the callback data cache"""
|
||||
context.bot.callback_data_cache.clear_callback_data()
|
||||
context.bot.callback_data_cache.clear_callback_queries()
|
||||
update.effective_message.reply_text('All clear!')
|
||||
await update.effective_message.reply_text('All clear!')
|
||||
|
||||
|
||||
def build_keyboard(current_list: List[int]) -> InlineKeyboardMarkup:
|
||||
|
@ -56,10 +56,10 @@ def build_keyboard(current_list: List[int]) -> InlineKeyboardMarkup:
|
|||
)
|
||||
|
||||
|
||||
def list_button(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def list_button(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Parses the CallbackQuery and updates the message text."""
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
await query.answer()
|
||||
# Get the data from the callback_data.
|
||||
# If you're using a type checker like MyPy, you'll have to use typing.cast
|
||||
# to make the checker get the expected type of the callback_data
|
||||
|
@ -67,7 +67,7 @@ def list_button(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
|||
# append the number to the list
|
||||
number_list.append(number)
|
||||
|
||||
query.edit_message_text(
|
||||
await query.edit_message_text(
|
||||
text=f"So far you've selected {number_list}. Choose the next item:",
|
||||
reply_markup=build_keyboard(number_list),
|
||||
)
|
||||
|
@ -76,10 +76,10 @@ def list_button(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
|||
context.drop_callback_data(query)
|
||||
|
||||
|
||||
def handle_invalid_button(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def handle_invalid_button(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Informs the user that the button is no longer available."""
|
||||
update.callback_query.answer()
|
||||
update.effective_message.edit_text(
|
||||
await update.callback_query.answer()
|
||||
await update.effective_message.edit_text(
|
||||
'Sorry, I could not process this button click 😕 Please send /start to get a new keyboard.'
|
||||
)
|
||||
|
||||
|
@ -88,29 +88,25 @@ def main() -> None:
|
|||
"""Run the bot."""
|
||||
# We use persistence to demonstrate how buttons can still work after the bot was restarted
|
||||
persistence = PicklePersistence(filepath='arbitrarycallbackdatabot')
|
||||
# Create the Updater and pass it your bot's token.
|
||||
updater = (
|
||||
Updater.builder()
|
||||
# Create the Application and pass it your bot's token.
|
||||
application = (
|
||||
Application.builder()
|
||||
.token("TOKEN")
|
||||
.persistence(persistence)
|
||||
.arbitrary_callback_data(True)
|
||||
.build()
|
||||
)
|
||||
|
||||
updater.dispatcher.add_handler(CommandHandler('start', start))
|
||||
updater.dispatcher.add_handler(CommandHandler('help', help_command))
|
||||
updater.dispatcher.add_handler(CommandHandler('clear', clear))
|
||||
updater.dispatcher.add_handler(
|
||||
application.add_handler(CommandHandler('start', start))
|
||||
application.add_handler(CommandHandler('help', help_command))
|
||||
application.add_handler(CommandHandler('clear', clear))
|
||||
application.add_handler(
|
||||
CallbackQueryHandler(handle_invalid_button, pattern=InvalidCallbackData)
|
||||
)
|
||||
updater.dispatcher.add_handler(CallbackQueryHandler(list_button))
|
||||
application.add_handler(CallbackQueryHandler(list_button))
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Run the bot until the user presses Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT
|
||||
updater.idle()
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -19,7 +19,7 @@ from telegram.constants import ParseMode
|
|||
from telegram.ext import (
|
||||
CommandHandler,
|
||||
ChatMemberHandler,
|
||||
Updater,
|
||||
Application,
|
||||
CallbackContext,
|
||||
)
|
||||
|
||||
|
@ -68,7 +68,7 @@ def extract_status_change(
|
|||
return was_member, is_member
|
||||
|
||||
|
||||
def track_chats(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def track_chats(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Tracks the chats the bot is in."""
|
||||
result = extract_status_change(update.my_chat_member)
|
||||
if result is None:
|
||||
|
@ -103,7 +103,7 @@ def track_chats(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
|||
context.bot_data.setdefault("channel_ids", set()).discard(chat.id)
|
||||
|
||||
|
||||
def show_chats(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def show_chats(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Shows which chats the bot is in"""
|
||||
user_ids = ", ".join(str(uid) for uid in context.bot_data.setdefault("user_ids", set()))
|
||||
group_ids = ", ".join(str(gid) for gid in context.bot_data.setdefault("group_ids", set()))
|
||||
|
@ -113,10 +113,10 @@ def show_chats(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
|||
f" Moreover it is a member of the groups with IDs {group_ids} "
|
||||
f"and administrator in the channels with IDs {channel_ids}."
|
||||
)
|
||||
update.effective_message.reply_text(text)
|
||||
await update.effective_message.reply_text(text)
|
||||
|
||||
|
||||
def greet_chat_members(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def greet_chat_members(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Greets new users in chats and announces when someone leaves"""
|
||||
result = extract_status_change(update.chat_member)
|
||||
if result is None:
|
||||
|
@ -127,12 +127,12 @@ def greet_chat_members(update: Update, context: CallbackContext.DEFAULT_TYPE) ->
|
|||
member_name = update.chat_member.new_chat_member.user.mention_html()
|
||||
|
||||
if not was_member and is_member:
|
||||
update.effective_chat.send_message(
|
||||
await update.effective_chat.send_message(
|
||||
f"{member_name} was added by {cause_name}. Welcome!",
|
||||
parse_mode=ParseMode.HTML,
|
||||
)
|
||||
elif was_member and not is_member:
|
||||
update.effective_chat.send_message(
|
||||
await update.effective_chat.send_message(
|
||||
f"{member_name} is no longer with us. Thanks a lot, {cause_name} ...",
|
||||
parse_mode=ParseMode.HTML,
|
||||
)
|
||||
|
@ -140,28 +140,20 @@ def greet_chat_members(update: Update, context: CallbackContext.DEFAULT_TYPE) ->
|
|||
|
||||
def main() -> None:
|
||||
"""Start the bot."""
|
||||
# Create the Updater and pass it your bot's token.
|
||||
updater = Updater.builder().token("TOKEN").build()
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dispatcher = updater.dispatcher
|
||||
# Create the Application and pass it your bot's token.
|
||||
application = Application.builder().token("TOKEN").build()
|
||||
|
||||
# Keep track of which chats the bot is in
|
||||
dispatcher.add_handler(ChatMemberHandler(track_chats, ChatMemberHandler.MY_CHAT_MEMBER))
|
||||
dispatcher.add_handler(CommandHandler("show_chats", show_chats))
|
||||
application.add_handler(ChatMemberHandler(track_chats, ChatMemberHandler.MY_CHAT_MEMBER))
|
||||
application.add_handler(CommandHandler("show_chats", show_chats))
|
||||
|
||||
# Handle members joining/leaving chats.
|
||||
dispatcher.add_handler(ChatMemberHandler(greet_chat_members, ChatMemberHandler.CHAT_MEMBER))
|
||||
application.add_handler(ChatMemberHandler(greet_chat_members, ChatMemberHandler.CHAT_MEMBER))
|
||||
|
||||
# Start the Bot
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
# We pass 'allowed_updates' handle *all* updates including `chat_member` updates
|
||||
# To reset this, simply pass `allowed_updates=[]`
|
||||
updater.start_polling(allowed_updates=Update.ALL_TYPES)
|
||||
|
||||
# Run the bot until you press Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT. This should be used most of the time, since
|
||||
# start_polling() is non-blocking and will stop the bot gracefully.
|
||||
updater.idle()
|
||||
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -10,6 +10,7 @@ Press Ctrl-C on the command line or send a signal to the process to stop the
|
|||
bot.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from typing import DefaultDict, Optional, Set
|
||||
|
||||
|
@ -21,11 +22,16 @@ from telegram.ext import (
|
|||
ContextTypes,
|
||||
CallbackQueryHandler,
|
||||
TypeHandler,
|
||||
Dispatcher,
|
||||
ExtBot,
|
||||
Updater,
|
||||
Application,
|
||||
)
|
||||
|
||||
# Enable logging
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ChatData:
|
||||
"""Custom class for chat_data. Here we store data per message."""
|
||||
|
@ -38,8 +44,8 @@ class ChatData:
|
|||
class CustomContext(CallbackContext[ExtBot, dict, ChatData, dict]):
|
||||
"""Custom class for context."""
|
||||
|
||||
def __init__(self, dispatcher: Dispatcher):
|
||||
super().__init__(dispatcher=dispatcher)
|
||||
def __init__(self, application: Application):
|
||||
super().__init__(application=application)
|
||||
self._message_id: Optional[int] = None
|
||||
|
||||
@property
|
||||
|
@ -62,10 +68,10 @@ class CustomContext(CallbackContext[ExtBot, dict, ChatData, dict]):
|
|||
self.chat_data.clicks_per_message[self._message_id] = value
|
||||
|
||||
@classmethod
|
||||
def from_update(cls, update: object, dispatcher: 'Dispatcher') -> 'CustomContext':
|
||||
def from_update(cls, update: object, application: 'Application') -> 'CustomContext':
|
||||
"""Override from_update to set _message_id."""
|
||||
# Make sure to call super()
|
||||
context = super().from_update(update, dispatcher)
|
||||
context = super().from_update(update, application)
|
||||
|
||||
if context.chat_data and isinstance(update, Update) and update.effective_message:
|
||||
# pylint: disable=protected-access
|
||||
|
@ -75,9 +81,9 @@ class CustomContext(CallbackContext[ExtBot, dict, ChatData, dict]):
|
|||
return context
|
||||
|
||||
|
||||
def start(update: Update, context: CustomContext) -> None:
|
||||
async def start(update: Update, context: CustomContext) -> None:
|
||||
"""Display a message with a button."""
|
||||
update.message.reply_html(
|
||||
await update.message.reply_html(
|
||||
'This button was clicked <i>0</i> times.',
|
||||
reply_markup=InlineKeyboardMarkup.from_button(
|
||||
InlineKeyboardButton(text='Click me!', callback_data='button')
|
||||
|
@ -85,11 +91,11 @@ def start(update: Update, context: CustomContext) -> None:
|
|||
)
|
||||
|
||||
|
||||
def count_click(update: Update, context: CustomContext) -> None:
|
||||
async def count_click(update: Update, context: CustomContext) -> None:
|
||||
"""Update the click count for the message."""
|
||||
context.message_clicks += 1
|
||||
update.callback_query.answer()
|
||||
update.effective_message.edit_text(
|
||||
await update.callback_query.answer()
|
||||
await update.effective_message.edit_text(
|
||||
f'This button was clicked <i>{context.message_clicks}</i> times.',
|
||||
reply_markup=InlineKeyboardMarkup.from_button(
|
||||
InlineKeyboardButton(text='Click me!', callback_data='button')
|
||||
|
@ -98,15 +104,15 @@ def count_click(update: Update, context: CustomContext) -> None:
|
|||
)
|
||||
|
||||
|
||||
def print_users(update: Update, context: CustomContext) -> None:
|
||||
async def print_users(update: Update, context: CustomContext) -> None:
|
||||
"""Show which users have been using this bot."""
|
||||
update.message.reply_text(
|
||||
await update.message.reply_text(
|
||||
'The following user IDs have used this bot: '
|
||||
f'{", ".join(map(str, context.bot_user_ids))}'
|
||||
)
|
||||
|
||||
|
||||
def track_users(update: Update, context: CustomContext) -> None:
|
||||
async def track_users(update: Update, context: CustomContext) -> None:
|
||||
"""Store the user id of the incoming update, if any."""
|
||||
if update.effective_user:
|
||||
context.bot_user_ids.add(update.effective_user.id)
|
||||
|
@ -115,17 +121,15 @@ def track_users(update: Update, context: CustomContext) -> None:
|
|||
def main() -> None:
|
||||
"""Run the bot."""
|
||||
context_types = ContextTypes(context=CustomContext, chat_data=ChatData)
|
||||
updater = Updater.builder().token("TOKEN").context_types(context_types).build()
|
||||
application = Application.builder().token("TOKEN").context_types(context_types).build()
|
||||
|
||||
dispatcher = updater.dispatcher
|
||||
# run track_users in its own group to not interfere with the user handlers
|
||||
dispatcher.add_handler(TypeHandler(Update, track_users), group=-1)
|
||||
dispatcher.add_handler(CommandHandler("start", start))
|
||||
dispatcher.add_handler(CallbackQueryHandler(count_click))
|
||||
dispatcher.add_handler(CommandHandler("print_users", print_users))
|
||||
application.add_handler(TypeHandler(Update, track_users), group=-1)
|
||||
application.add_handler(CommandHandler("start", start))
|
||||
application.add_handler(CallbackQueryHandler(count_click))
|
||||
application.add_handler(CommandHandler("print_users", print_users))
|
||||
|
||||
updater.start_polling()
|
||||
updater.idle()
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
"""
|
||||
First, a few callback functions are defined. Then, those functions are passed to
|
||||
the Dispatcher and registered at their respective places.
|
||||
the Application and registered at their respective places.
|
||||
Then, the bot is started and runs until we press Ctrl-C on the command line.
|
||||
|
||||
Usage:
|
||||
|
@ -22,7 +22,7 @@ from telegram.ext import (
|
|||
MessageHandler,
|
||||
filters,
|
||||
ConversationHandler,
|
||||
Updater,
|
||||
Application,
|
||||
CallbackContext,
|
||||
)
|
||||
|
||||
|
@ -36,11 +36,11 @@ logger = logging.getLogger(__name__)
|
|||
GENDER, PHOTO, LOCATION, BIO = range(4)
|
||||
|
||||
|
||||
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Starts the conversation and asks the user about their gender."""
|
||||
reply_keyboard = [['Boy', 'Girl', 'Other']]
|
||||
|
||||
update.message.reply_text(
|
||||
await 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?',
|
||||
|
@ -52,11 +52,11 @@ def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
|||
return GENDER
|
||||
|
||||
|
||||
def gender(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def gender(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Stores the selected gender and asks for a photo."""
|
||||
user = update.message.from_user
|
||||
logger.info("Gender of %s: %s", user.first_name, update.message.text)
|
||||
update.message.reply_text(
|
||||
await 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(),
|
||||
|
@ -65,69 +65,69 @@ def gender(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
|||
return PHOTO
|
||||
|
||||
|
||||
def photo(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def photo(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Stores the photo and asks for a location."""
|
||||
user = update.message.from_user
|
||||
photo_file = update.message.photo[-1].get_file()
|
||||
photo_file.download('user_photo.jpg')
|
||||
photo_file = await update.message.photo[-1].get_file()
|
||||
await photo_file.download('user_photo.jpg')
|
||||
logger.info("Photo of %s: %s", user.first_name, 'user_photo.jpg')
|
||||
update.message.reply_text(
|
||||
await 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: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def skip_photo(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Skips the photo and asks for a location."""
|
||||
user = update.message.from_user
|
||||
logger.info("User %s did not send a photo.", user.first_name)
|
||||
update.message.reply_text(
|
||||
await update.message.reply_text(
|
||||
'I bet you look great! Now, send me your location please, or send /skip.'
|
||||
)
|
||||
|
||||
return LOCATION
|
||||
|
||||
|
||||
def location(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def location(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Stores the location and asks for some info about the user."""
|
||||
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(
|
||||
await update.message.reply_text(
|
||||
'Maybe I can visit you sometime! At last, tell me something about yourself.'
|
||||
)
|
||||
|
||||
return BIO
|
||||
|
||||
|
||||
def skip_location(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def skip_location(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Skips the location and asks for info about the user."""
|
||||
user = update.message.from_user
|
||||
logger.info("User %s did not send a location.", user.first_name)
|
||||
update.message.reply_text(
|
||||
await update.message.reply_text(
|
||||
'You seem a bit paranoid! At last, tell me something about yourself.'
|
||||
)
|
||||
|
||||
return BIO
|
||||
|
||||
|
||||
def bio(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def bio(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Stores the info about the user and ends the conversation."""
|
||||
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.')
|
||||
await update.message.reply_text('Thank you! I hope we can talk again some day.')
|
||||
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
def cancel(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def cancel(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Cancels and ends the conversation."""
|
||||
user = update.message.from_user
|
||||
logger.info("User %s canceled the conversation.", user.first_name)
|
||||
update.message.reply_text(
|
||||
await update.message.reply_text(
|
||||
'Bye! I hope we can talk again some day.', reply_markup=ReplyKeyboardRemove()
|
||||
)
|
||||
|
||||
|
@ -136,11 +136,8 @@ def cancel(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
|||
|
||||
def main() -> None:
|
||||
"""Run the bot."""
|
||||
# Create the Updater and pass it your bot's token.
|
||||
updater = Updater.builder().token("TOKEN").build()
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dispatcher = updater.dispatcher
|
||||
# Create the Application and pass it your bot's token.
|
||||
application = Application.builder().token("TOKEN").build()
|
||||
|
||||
# Add conversation handler with the states GENDER, PHOTO, LOCATION and BIO
|
||||
conv_handler = ConversationHandler(
|
||||
|
@ -157,15 +154,10 @@ def main() -> None:
|
|||
fallbacks=[CommandHandler('cancel', cancel)],
|
||||
)
|
||||
|
||||
dispatcher.add_handler(conv_handler)
|
||||
application.add_handler(conv_handler)
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Run the bot until you press Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT. This should be used most of the time, since
|
||||
# start_polling() is non-blocking and will stop the bot gracefully.
|
||||
updater.idle()
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
"""
|
||||
First, a few callback functions are defined. Then, those functions are passed to
|
||||
the Dispatcher and registered at their respective places.
|
||||
the Application and registered at their respective places.
|
||||
Then, the bot is started and runs until we press Ctrl-C on the command line.
|
||||
|
||||
Usage:
|
||||
|
@ -23,7 +23,7 @@ from telegram.ext import (
|
|||
MessageHandler,
|
||||
filters,
|
||||
ConversationHandler,
|
||||
Updater,
|
||||
Application,
|
||||
CallbackContext,
|
||||
)
|
||||
|
||||
|
@ -50,9 +50,9 @@ def facts_to_str(user_data: Dict[str, str]) -> str:
|
|||
return "\n".join(facts).join(['\n', '\n'])
|
||||
|
||||
|
||||
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Start the conversation and ask user for input."""
|
||||
update.message.reply_text(
|
||||
await 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,
|
||||
|
@ -61,25 +61,25 @@ def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
|||
return CHOOSING
|
||||
|
||||
|
||||
def regular_choice(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def regular_choice(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Ask the user for info about the selected predefined choice."""
|
||||
text = update.message.text
|
||||
context.user_data['choice'] = text
|
||||
update.message.reply_text(f'Your {text.lower()}? Yes, I would love to hear about that!')
|
||||
await update.message.reply_text(f'Your {text.lower()}? Yes, I would love to hear about that!')
|
||||
|
||||
return TYPING_REPLY
|
||||
|
||||
|
||||
def custom_choice(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def custom_choice(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Ask the user for a description of a custom category."""
|
||||
update.message.reply_text(
|
||||
await update.message.reply_text(
|
||||
'Alright, please send me the category first, for example "Most impressive skill"'
|
||||
)
|
||||
|
||||
return TYPING_CHOICE
|
||||
|
||||
|
||||
def received_information(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def received_information(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Store info provided by user and ask for the next category."""
|
||||
user_data = context.user_data
|
||||
text = update.message.text
|
||||
|
@ -87,9 +87,9 @@ def received_information(update: Update, context: CallbackContext.DEFAULT_TYPE)
|
|||
user_data[category] = text
|
||||
del user_data['choice']
|
||||
|
||||
update.message.reply_text(
|
||||
await 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"
|
||||
f"{facts_to_str(user_data)}You can tell me more, or change your opinion"
|
||||
" on something.",
|
||||
reply_markup=markup,
|
||||
)
|
||||
|
@ -97,13 +97,13 @@ def received_information(update: Update, context: CallbackContext.DEFAULT_TYPE)
|
|||
return CHOOSING
|
||||
|
||||
|
||||
def done(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def done(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Display the gathered info and end the conversation."""
|
||||
user_data = context.user_data
|
||||
if 'choice' in user_data:
|
||||
del user_data['choice']
|
||||
|
||||
update.message.reply_text(
|
||||
await update.message.reply_text(
|
||||
f"I learned these facts about you: {facts_to_str(user_data)}Until next time!",
|
||||
reply_markup=ReplyKeyboardRemove(),
|
||||
)
|
||||
|
@ -114,11 +114,8 @@ def done(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
|||
|
||||
def main() -> None:
|
||||
"""Run the bot."""
|
||||
# Create the Updater and pass it your bot's token.
|
||||
updater = Updater.builder().token("TOKEN").build()
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dispatcher = updater.dispatcher
|
||||
# Create the Application and pass it your bot's token.
|
||||
application = Application.builder().token("TOKEN").build()
|
||||
|
||||
# Add conversation handler with the states CHOOSING, TYPING_CHOICE and TYPING_REPLY
|
||||
conv_handler = ConversationHandler(
|
||||
|
@ -145,15 +142,10 @@ def main() -> None:
|
|||
fallbacks=[MessageHandler(filters.Regex('^Done$'), done)],
|
||||
)
|
||||
|
||||
dispatcher.add_handler(conv_handler)
|
||||
application.add_handler(conv_handler)
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Run the bot until you press Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT. This should be used most of the time, since
|
||||
# start_polling() is non-blocking and will stop the bot gracefully.
|
||||
updater.idle()
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
|
||||
This program is dedicated to the public domain under the CC0 license.
|
||||
|
||||
This Bot uses the Updater class to handle the bot.
|
||||
This Bot uses the Application class to handle the bot.
|
||||
|
||||
First, a few handler functions are defined. Then, those functions are passed to
|
||||
the Dispatcher and registered at their respective places.
|
||||
the Application and registered at their respective places.
|
||||
Then, the bot is started and runs until we press Ctrl-C on the command line.
|
||||
|
||||
Usage:
|
||||
|
@ -26,7 +26,7 @@ from telegram.ext import (
|
|||
CommandHandler,
|
||||
CallbackQueryHandler,
|
||||
filters,
|
||||
Updater,
|
||||
Application,
|
||||
CallbackContext,
|
||||
)
|
||||
|
||||
|
@ -47,15 +47,15 @@ SO_COOL = "so-cool"
|
|||
KEYBOARD_CALLBACKDATA = "keyboard-callback-data"
|
||||
|
||||
|
||||
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Send a deep-linked URL when the command /start is issued."""
|
||||
bot = context.bot
|
||||
url = helpers.create_deep_linked_url(bot.username, CHECK_THIS_OUT, group=True)
|
||||
text = "Feel free to tell your friends about it:\n\n" + url
|
||||
update.message.reply_text(text)
|
||||
await update.message.reply_text(text)
|
||||
|
||||
|
||||
def deep_linked_level_1(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def deep_linked_level_1(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Reached through the CHECK_THIS_OUT payload"""
|
||||
bot = context.bot
|
||||
url = helpers.create_deep_linked_url(bot.username, SO_COOL)
|
||||
|
@ -66,20 +66,20 @@ def deep_linked_level_1(update: Update, context: CallbackContext.DEFAULT_TYPE) -
|
|||
keyboard = InlineKeyboardMarkup.from_button(
|
||||
InlineKeyboardButton(text="Continue here!", url=url)
|
||||
)
|
||||
update.message.reply_text(text, reply_markup=keyboard)
|
||||
await update.message.reply_text(text, reply_markup=keyboard)
|
||||
|
||||
|
||||
def deep_linked_level_2(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def deep_linked_level_2(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Reached through the SO_COOL payload"""
|
||||
bot = context.bot
|
||||
url = helpers.create_deep_linked_url(bot.username, USING_ENTITIES)
|
||||
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)
|
||||
text = f"You can also mask the deep-linked URLs as links: <a href=\"{url}\">▶️ CLICK HERE</a>."
|
||||
await update.message.reply_text(text, parse_mode=ParseMode.HTML, disable_web_page_preview=True)
|
||||
|
||||
|
||||
def deep_linked_level_3(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def deep_linked_level_3(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Reached through the USING_ENTITIES payload"""
|
||||
update.message.reply_text(
|
||||
await update.message.reply_text(
|
||||
"It is also possible to make deep-linking using InlineKeyboardButtons.",
|
||||
reply_markup=InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton(text="Like this!", callback_data=KEYBOARD_CALLBACKDATA)]]
|
||||
|
@ -87,65 +87,59 @@ def deep_linked_level_3(update: Update, context: CallbackContext.DEFAULT_TYPE) -
|
|||
)
|
||||
|
||||
|
||||
def deep_link_level_3_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def deep_link_level_3_callback(
|
||||
update: Update, context: CallbackContext.DEFAULT_TYPE
|
||||
) -> None:
|
||||
"""Answers CallbackQuery with deeplinking url."""
|
||||
bot = context.bot
|
||||
url = helpers.create_deep_linked_url(bot.username, USING_KEYBOARD)
|
||||
update.callback_query.answer(url=url)
|
||||
await update.callback_query.answer(url=url)
|
||||
|
||||
|
||||
def deep_linked_level_4(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def deep_linked_level_4(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Reached through the USING_KEYBOARD payload"""
|
||||
payload = context.args
|
||||
update.message.reply_text(
|
||||
await update.message.reply_text(
|
||||
f"Congratulations! This is as deep as it gets 👏🏻\n\nThe payload was: {payload}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Start the bot."""
|
||||
# Create the Updater and pass it your bot's token.
|
||||
updater = Updater.builder().token("TOKEN").build()
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dispatcher = updater.dispatcher
|
||||
# Create the Application and pass it your bot's token.
|
||||
application = Application.builder().token("TOKEN").build()
|
||||
|
||||
# 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
|
||||
dispatcher.add_handler(
|
||||
application.add_handler(
|
||||
CommandHandler("start", deep_linked_level_1, filters.Regex(CHECK_THIS_OUT))
|
||||
)
|
||||
|
||||
# This one works with a textual link instead of an URL
|
||||
dispatcher.add_handler(CommandHandler("start", deep_linked_level_2, filters.Regex(SO_COOL)))
|
||||
application.add_handler(CommandHandler("start", deep_linked_level_2, filters.Regex(SO_COOL)))
|
||||
|
||||
# We can also pass on the deep-linking payload
|
||||
dispatcher.add_handler(
|
||||
application.add_handler(
|
||||
CommandHandler("start", deep_linked_level_3, filters.Regex(USING_ENTITIES))
|
||||
)
|
||||
|
||||
# Possible with inline keyboard buttons as well
|
||||
dispatcher.add_handler(
|
||||
application.add_handler(
|
||||
CommandHandler("start", deep_linked_level_4, filters.Regex(USING_KEYBOARD))
|
||||
)
|
||||
|
||||
# register callback handler for inline keyboard button
|
||||
dispatcher.add_handler(
|
||||
application.add_handler(
|
||||
CallbackQueryHandler(deep_link_level_3_callback, pattern=KEYBOARD_CALLBACKDATA)
|
||||
)
|
||||
|
||||
# Make sure the deep-linking handlers occur *before* the normal /start handler.
|
||||
dispatcher.add_handler(CommandHandler("start", start))
|
||||
application.add_handler(CommandHandler("start", start))
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Run the bot until you press Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT. This should be used most of the time, since
|
||||
# start_polling() is non-blocking and will stop the bot gracefully.
|
||||
updater.idle()
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
Simple Bot to reply to Telegram messages.
|
||||
|
||||
First, a few handler functions are defined. Then, those functions are passed to
|
||||
the Dispatcher and registered at their respective places.
|
||||
the Application and registered at their respective places.
|
||||
Then, the bot is started and runs until we press Ctrl-C on the command line.
|
||||
|
||||
Usage:
|
||||
|
@ -22,7 +22,7 @@ from telegram.ext import (
|
|||
CommandHandler,
|
||||
MessageHandler,
|
||||
filters,
|
||||
Updater,
|
||||
Application,
|
||||
CallbackContext,
|
||||
)
|
||||
|
||||
|
@ -36,47 +36,39 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
# Define a few command handlers. These usually take the two arguments update and
|
||||
# context.
|
||||
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Send a message when the command /start is issued."""
|
||||
user = update.effective_user
|
||||
update.message.reply_markdown_v2(
|
||||
fr'Hi {user.mention_markdown_v2()}\!',
|
||||
await update.message.reply_html(
|
||||
fr'Hi {user.mention_html()}!',
|
||||
reply_markup=ForceReply(selective=True),
|
||||
)
|
||||
|
||||
|
||||
def help_command(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def help_command(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Send a message when the command /help is issued."""
|
||||
update.message.reply_text('Help!')
|
||||
await update.message.reply_text('Help!')
|
||||
|
||||
|
||||
def echo(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def echo(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Echo the user message."""
|
||||
update.message.reply_text(update.message.text)
|
||||
await update.message.reply_text(update.message.text)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Start the bot."""
|
||||
# Create the Updater and pass it your bot's token.
|
||||
updater = Updater.builder().token("TOKEN").build()
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dispatcher = updater.dispatcher
|
||||
# Create the Application and pass it your bot's token.
|
||||
application = Application.builder().token("TOKEN").build()
|
||||
|
||||
# on different commands - answer in Telegram
|
||||
dispatcher.add_handler(CommandHandler("start", start))
|
||||
dispatcher.add_handler(CommandHandler("help", help_command))
|
||||
application.add_handler(CommandHandler("start", start))
|
||||
application.add_handler(CommandHandler("help", help_command))
|
||||
|
||||
# on non command i.e message - echo the message on Telegram
|
||||
dispatcher.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
|
||||
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Run the bot until you press Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT. This should be used most of the time, since
|
||||
# start_polling() is non-blocking and will stop the bot gracefully.
|
||||
updater.idle()
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -10,7 +10,7 @@ import traceback
|
|||
|
||||
from telegram import Update
|
||||
from telegram.constants import ParseMode
|
||||
from telegram.ext import CommandHandler, Updater, CallbackContext
|
||||
from telegram.ext import CommandHandler, Application, CallbackContext
|
||||
|
||||
# Enable logging
|
||||
logging.basicConfig(
|
||||
|
@ -18,15 +18,12 @@ logging.basicConfig(
|
|||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# The token you got from @botfather when you created the bot
|
||||
BOT_TOKEN = "TOKEN"
|
||||
|
||||
# This can be your own ID, or one for a developer group/channel.
|
||||
# You can use the /start command of this bot to see your chat id.
|
||||
DEVELOPER_CHAT_ID = 123456789
|
||||
|
||||
|
||||
def error_handler(update: object, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def error_handler(update: object, context: CallbackContext.DEFAULT_TYPE) -> 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)
|
||||
|
@ -49,17 +46,19 @@ def error_handler(update: object, context: CallbackContext.DEFAULT_TYPE) -> None
|
|||
)
|
||||
|
||||
# Finally, send the message
|
||||
context.bot.send_message(chat_id=DEVELOPER_CHAT_ID, text=message, parse_mode=ParseMode.HTML)
|
||||
await context.bot.send_message(
|
||||
chat_id=DEVELOPER_CHAT_ID, text=message, parse_mode=ParseMode.HTML
|
||||
)
|
||||
|
||||
|
||||
def bad_command(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def bad_command(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Raise an error to trigger the error handler."""
|
||||
context.bot.wrong_method_name() # type: ignore[attr-defined]
|
||||
await context.bot.wrong_method_name() # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Displays info on how to trigger an error."""
|
||||
update.effective_message.reply_html(
|
||||
await update.effective_message.reply_html(
|
||||
'Use /bad_command to cause an error.\n'
|
||||
f'Your chat id is <code>{update.effective_chat.id}</code>.'
|
||||
)
|
||||
|
@ -67,26 +66,18 @@ def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
|||
|
||||
def main() -> None:
|
||||
"""Run the bot."""
|
||||
# Create the Updater and pass it your bot's token.
|
||||
updater = Updater.builder().token(BOT_TOKEN).build()
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dispatcher = updater.dispatcher
|
||||
# Create the Application and pass it your bot's token.
|
||||
application = Application.builder().token("TOKEN").build()
|
||||
|
||||
# Register the commands...
|
||||
dispatcher.add_handler(CommandHandler('start', start))
|
||||
dispatcher.add_handler(CommandHandler('bad_command', bad_command))
|
||||
application.add_handler(CommandHandler('start', start))
|
||||
application.add_handler(CommandHandler('bad_command', bad_command))
|
||||
|
||||
# ...and the error handler
|
||||
dispatcher.add_error_handler(error_handler)
|
||||
application.add_error_handler(error_handler)
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Run the bot until you press Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT. This should be used most of the time, since
|
||||
# start_polling() is non-blocking and will stop the bot gracefully.
|
||||
updater.idle()
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
"""
|
||||
First, a few handler functions are defined. Then, those functions are passed to
|
||||
the Dispatcher and registered at their respective places.
|
||||
the Application and registered at their respective places.
|
||||
Then, the bot is started and runs until we press Ctrl-C on the command line.
|
||||
|
||||
Usage:
|
||||
|
@ -14,11 +14,11 @@ bot.
|
|||
"""
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
from html import escape
|
||||
|
||||
from telegram import InlineQueryResultArticle, InputTextMessageContent, Update
|
||||
from telegram.constants import ParseMode
|
||||
from telegram.helpers import escape_markdown
|
||||
from telegram.ext import Updater, InlineQueryHandler, CommandHandler, CallbackContext
|
||||
from telegram.ext import Application, InlineQueryHandler, CommandHandler, CallbackContext
|
||||
|
||||
# Enable logging
|
||||
logging.basicConfig(
|
||||
|
@ -28,19 +28,19 @@ 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: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
# context.
|
||||
async def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Send a message when the command /start is issued."""
|
||||
update.message.reply_text('Hi!')
|
||||
await update.message.reply_text('Hi!')
|
||||
|
||||
|
||||
def help_command(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def help_command(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Send a message when the command /help is issued."""
|
||||
update.message.reply_text('Help!')
|
||||
await update.message.reply_text('Help!')
|
||||
|
||||
|
||||
def inlinequery(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Handle the inline query."""
|
||||
async def inline_query(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Handle the inline query. This is run when you type: @botusername <query>"""
|
||||
query = update.inline_query.query
|
||||
|
||||
if query == "":
|
||||
|
@ -56,43 +56,35 @@ def inlinequery(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
|||
id=str(uuid4()),
|
||||
title="Bold",
|
||||
input_message_content=InputTextMessageContent(
|
||||
f"*{escape_markdown(query)}*", parse_mode=ParseMode.MARKDOWN
|
||||
f"<b>{escape(query)}</b>", parse_mode=ParseMode.HTML
|
||||
),
|
||||
),
|
||||
InlineQueryResultArticle(
|
||||
id=str(uuid4()),
|
||||
title="Italic",
|
||||
input_message_content=InputTextMessageContent(
|
||||
f"_{escape_markdown(query)}_", parse_mode=ParseMode.MARKDOWN
|
||||
f"<i>{escape(query)}</i>", parse_mode=ParseMode.HTML
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
update.inline_query.answer(results)
|
||||
await update.inline_query.answer(results)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the bot."""
|
||||
# Create the Updater and pass it your bot's token.
|
||||
updater = Updater.builder().token("TOKEN").build()
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dispatcher = updater.dispatcher
|
||||
# Create the Application and pass it your bot's token.
|
||||
application = Application.builder().token("TOKEN").build()
|
||||
|
||||
# on different commands - answer in Telegram
|
||||
dispatcher.add_handler(CommandHandler("start", start))
|
||||
dispatcher.add_handler(CommandHandler("help", help_command))
|
||||
application.add_handler(CommandHandler("start", start))
|
||||
application.add_handler(CommandHandler("help", help_command))
|
||||
|
||||
# on non command i.e message - echo the message on Telegram
|
||||
dispatcher.add_handler(InlineQueryHandler(inlinequery))
|
||||
application.add_handler(InlineQueryHandler(inline_query))
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Block until the user presses Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT. This should be used most of the time, since
|
||||
# start_polling() is non-blocking and will stop the bot gracefully.
|
||||
updater.idle()
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -12,7 +12,7 @@ from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
|
|||
from telegram.ext import (
|
||||
CommandHandler,
|
||||
CallbackQueryHandler,
|
||||
Updater,
|
||||
Application,
|
||||
CallbackContext,
|
||||
)
|
||||
|
||||
|
@ -24,7 +24,7 @@ logging.basicConfig(
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Sends a message with three inline buttons attached."""
|
||||
keyboard = [
|
||||
[
|
||||
|
@ -36,40 +36,36 @@ def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
|||
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
update.message.reply_text('Please choose:', reply_markup=reply_markup)
|
||||
await update.message.reply_text('Please choose:', reply_markup=reply_markup)
|
||||
|
||||
|
||||
def button(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def button(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Parses the CallbackQuery and updates the message text."""
|
||||
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()
|
||||
await query.answer()
|
||||
|
||||
query.edit_message_text(text=f"Selected option: {query.data}")
|
||||
await query.edit_message_text(text=f"Selected option: {query.data}")
|
||||
|
||||
|
||||
def help_command(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def help_command(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Displays info on how to use the bot."""
|
||||
update.message.reply_text("Use /start to test this bot.")
|
||||
await update.message.reply_text("Use /start to test this bot.")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the bot."""
|
||||
# Create the Updater and pass it your bot's token.
|
||||
updater = Updater.builder().token("TOKEN").build()
|
||||
# Create the Application and pass it your bot's token.
|
||||
application = Application.builder().token("TOKEN").build()
|
||||
|
||||
updater.dispatcher.add_handler(CommandHandler('start', start))
|
||||
updater.dispatcher.add_handler(CallbackQueryHandler(button))
|
||||
updater.dispatcher.add_handler(CommandHandler('help', help_command))
|
||||
application.add_handler(CommandHandler('start', start))
|
||||
application.add_handler(CallbackQueryHandler(button))
|
||||
application.add_handler(CommandHandler('help', help_command))
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Run the bot until the user presses Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT
|
||||
updater.idle()
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
|
||||
"""Simple inline keyboard bot with multiple CallbackQueryHandlers.
|
||||
|
||||
This Bot uses the Updater class to handle the bot.
|
||||
This Bot uses the Application class to handle the bot.
|
||||
First, a few callback functions are defined as callback query handler. Then, those functions are
|
||||
passed to the Dispatcher and registered at their respective places.
|
||||
passed to the Application and registered at their respective places.
|
||||
Then, the bot is started and runs until we press Ctrl-C on the command line.
|
||||
Usage:
|
||||
Example of a bot that uses inline keyboard that has multiple CallbackQueryHandlers arranged in a
|
||||
|
@ -20,7 +20,7 @@ from telegram.ext import (
|
|||
CommandHandler,
|
||||
CallbackQueryHandler,
|
||||
ConversationHandler,
|
||||
Updater,
|
||||
Application,
|
||||
CallbackContext,
|
||||
)
|
||||
|
||||
|
@ -32,12 +32,12 @@ logging.basicConfig(
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Stages
|
||||
FIRST, SECOND = range(2)
|
||||
START_ROUTES, END_ROUTES = range(2)
|
||||
# Callback data
|
||||
ONE, TWO, THREE, FOUR = range(4)
|
||||
|
||||
|
||||
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Send message on `/start`."""
|
||||
# Get user that sent /start and log his name
|
||||
user = update.message.from_user
|
||||
|
@ -54,18 +54,18 @@ def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
|||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
# Send message with text and appended InlineKeyboard
|
||||
update.message.reply_text("Start handler, Choose a route", reply_markup=reply_markup)
|
||||
await update.message.reply_text("Start handler, Choose a route", reply_markup=reply_markup)
|
||||
# Tell ConversationHandler that we're in state `FIRST` now
|
||||
return FIRST
|
||||
return START_ROUTES
|
||||
|
||||
|
||||
def start_over(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def start_over(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Prompt same text & keyboard as `start` does but not as new message"""
|
||||
# Get CallbackQuery from Update
|
||||
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()
|
||||
await query.answer()
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("1", callback_data=str(ONE)),
|
||||
|
@ -76,14 +76,14 @@ def start_over(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
|||
# 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)
|
||||
return FIRST
|
||||
await query.edit_message_text(text="Start handler, Choose a route", reply_markup=reply_markup)
|
||||
return START_ROUTES
|
||||
|
||||
|
||||
def one(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def one(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Show new choice of buttons"""
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
await query.answer()
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("3", callback_data=str(THREE)),
|
||||
|
@ -91,16 +91,16 @@ def one(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
|||
]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
query.edit_message_text(
|
||||
await query.edit_message_text(
|
||||
text="First CallbackQueryHandler, Choose a route", reply_markup=reply_markup
|
||||
)
|
||||
return FIRST
|
||||
return START_ROUTES
|
||||
|
||||
|
||||
def two(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def two(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Show new choice of buttons"""
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
await query.answer()
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("1", callback_data=str(ONE)),
|
||||
|
@ -108,16 +108,16 @@ def two(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
|||
]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
query.edit_message_text(
|
||||
await query.edit_message_text(
|
||||
text="Second CallbackQueryHandler, Choose a route", reply_markup=reply_markup
|
||||
)
|
||||
return FIRST
|
||||
return START_ROUTES
|
||||
|
||||
|
||||
def three(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Show new choice of buttons"""
|
||||
async def three(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Show new choice of buttons. This is the end point of the conversation."""
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
await query.answer()
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("Yes, let's do it again!", callback_data=str(ONE)),
|
||||
|
@ -125,17 +125,17 @@ def three(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
|||
]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
query.edit_message_text(
|
||||
await query.edit_message_text(
|
||||
text="Third CallbackQueryHandler. Do want to start over?", reply_markup=reply_markup
|
||||
)
|
||||
# Transfer to conversation state `SECOND`
|
||||
return SECOND
|
||||
return END_ROUTES
|
||||
|
||||
|
||||
def four(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def four(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Show new choice of buttons"""
|
||||
query = update.callback_query
|
||||
query.answer()
|
||||
await query.answer()
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("2", callback_data=str(TWO)),
|
||||
|
@ -143,29 +143,26 @@ def four(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
|||
]
|
||||
]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
query.edit_message_text(
|
||||
await query.edit_message_text(
|
||||
text="Fourth CallbackQueryHandler, Choose a route", reply_markup=reply_markup
|
||||
)
|
||||
return FIRST
|
||||
return START_ROUTES
|
||||
|
||||
|
||||
def end(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def end(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""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!")
|
||||
await query.answer()
|
||||
await query.edit_message_text(text="See you next time!")
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the bot."""
|
||||
# Create the Updater and pass it your bot's token.
|
||||
updater = Updater.builder().token("TOKEN").build()
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dispatcher = updater.dispatcher
|
||||
# Create the Application and pass it your bot's token.
|
||||
application = Application.builder().token("TOKEN").build()
|
||||
|
||||
# Setup conversation handler with the states FIRST and SECOND
|
||||
# Use the pattern parameter to pass CallbackQueries with specific
|
||||
|
@ -176,13 +173,13 @@ def main() -> None:
|
|||
conv_handler = ConversationHandler(
|
||||
entry_points=[CommandHandler('start', start)],
|
||||
states={
|
||||
FIRST: [
|
||||
START_ROUTES: [
|
||||
CallbackQueryHandler(one, pattern='^' + str(ONE) + '$'),
|
||||
CallbackQueryHandler(two, pattern='^' + str(TWO) + '$'),
|
||||
CallbackQueryHandler(three, pattern='^' + str(THREE) + '$'),
|
||||
CallbackQueryHandler(four, pattern='^' + str(FOUR) + '$'),
|
||||
],
|
||||
SECOND: [
|
||||
END_ROUTES: [
|
||||
CallbackQueryHandler(start_over, pattern='^' + str(ONE) + '$'),
|
||||
CallbackQueryHandler(end, pattern='^' + str(TWO) + '$'),
|
||||
],
|
||||
|
@ -190,16 +187,11 @@ def main() -> None:
|
|||
fallbacks=[CommandHandler('start', start)],
|
||||
)
|
||||
|
||||
# Add ConversationHandler to dispatcher that will be used for handling updates
|
||||
dispatcher.add_handler(conv_handler)
|
||||
# Add ConversationHandler to application that will be used for handling updates
|
||||
application.add_handler(conv_handler)
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Run the bot until you press Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT. This should be used most of the time, since
|
||||
# start_polling() is non-blocking and will stop the bot gracefully.
|
||||
updater.idle()
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
"""
|
||||
First, a few callback functions are defined. Then, those functions are passed to
|
||||
the Dispatcher and registered at their respective places.
|
||||
the Application and registered at their respective places.
|
||||
Then, the bot is started and runs until we press Ctrl-C on the command line.
|
||||
|
||||
Usage:
|
||||
|
@ -24,7 +24,7 @@ from telegram.ext import (
|
|||
filters,
|
||||
ConversationHandler,
|
||||
CallbackQueryHandler,
|
||||
Updater,
|
||||
Application,
|
||||
CallbackContext,
|
||||
)
|
||||
|
||||
|
@ -71,7 +71,7 @@ def _name_switcher(level: str) -> Tuple[str, str]:
|
|||
|
||||
|
||||
# Top level conversation callbacks
|
||||
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
async def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
"""Select an action: Adding parent/child or show data."""
|
||||
text = (
|
||||
"You may choose to add a family member, yourself, show the gathered data, or end the "
|
||||
|
@ -92,85 +92,87 @@ def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
|||
|
||||
# If we're starting over we don't need to send a new message
|
||||
if context.user_data.get(START_OVER):
|
||||
update.callback_query.answer()
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
await update.callback_query.answer()
|
||||
await update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
else:
|
||||
update.message.reply_text(
|
||||
await update.message.reply_text(
|
||||
"Hi, I'm Family Bot and I'm here to help you gather information about your family."
|
||||
)
|
||||
update.message.reply_text(text=text, reply_markup=keyboard)
|
||||
await update.message.reply_text(text=text, reply_markup=keyboard)
|
||||
|
||||
context.user_data[START_OVER] = False
|
||||
return SELECTING_ACTION
|
||||
|
||||
|
||||
def adding_self(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
async def adding_self(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
"""Add information about yourself."""
|
||||
context.user_data[CURRENT_LEVEL] = SELF
|
||||
text = 'Okay, please tell me about yourself.'
|
||||
button = InlineKeyboardButton(text='Add info', callback_data=str(MALE))
|
||||
keyboard = InlineKeyboardMarkup.from_button(button)
|
||||
|
||||
update.callback_query.answer()
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
await update.callback_query.answer()
|
||||
await update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
|
||||
return DESCRIBING_SELF
|
||||
|
||||
|
||||
def show_data(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
async def show_data(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
"""Pretty print gathered data."""
|
||||
|
||||
def prettyprint(user_data: Dict[str, Any], level: str) -> str:
|
||||
people = user_data.get(level)
|
||||
def pretty_print(data: Dict[str, Any], level: str) -> str:
|
||||
people = data.get(level)
|
||||
if not people:
|
||||
return '\nNo information yet.'
|
||||
|
||||
text = ''
|
||||
return_str = ''
|
||||
if level == SELF:
|
||||
for person in user_data[level]:
|
||||
text += f"\nName: {person.get(NAME, '-')}, Age: {person.get(AGE, '-')}"
|
||||
for person in data[level]:
|
||||
return_str += f"\nName: {person.get(NAME, '-')}, Age: {person.get(AGE, '-')}"
|
||||
else:
|
||||
male, female = _name_switcher(level)
|
||||
|
||||
for person in user_data[level]:
|
||||
for person in data[level]:
|
||||
gender = female if person[GENDER] == FEMALE else male
|
||||
text += f"\n{gender}: Name: {person.get(NAME, '-')}, Age: {person.get(AGE, '-')}"
|
||||
return text
|
||||
return_str += (
|
||||
f"\n{gender}: Name: {person.get(NAME, '-')}, Age: {person.get(AGE, '-')}"
|
||||
)
|
||||
return return_str
|
||||
|
||||
user_data = context.user_data
|
||||
text = f"Yourself:{prettyprint(user_data, SELF)}"
|
||||
text += f"\n\nParents:{prettyprint(user_data, PARENTS)}"
|
||||
text += f"\n\nChildren:{prettyprint(user_data, CHILDREN)}"
|
||||
text = f"Yourself:{pretty_print(user_data, SELF)}"
|
||||
text += f"\n\nParents:{pretty_print(user_data, PARENTS)}"
|
||||
text += f"\n\nChildren:{pretty_print(user_data, CHILDREN)}"
|
||||
|
||||
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)
|
||||
await update.callback_query.answer()
|
||||
await update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
user_data[START_OVER] = True
|
||||
|
||||
return SHOWING
|
||||
|
||||
|
||||
def stop(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def stop(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""End Conversation by command."""
|
||||
update.message.reply_text('Okay, bye.')
|
||||
await update.message.reply_text('Okay, bye.')
|
||||
|
||||
return END
|
||||
|
||||
|
||||
def end(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def end(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""End conversation from InlineKeyboardButton."""
|
||||
update.callback_query.answer()
|
||||
await update.callback_query.answer()
|
||||
|
||||
text = 'See you around!'
|
||||
update.callback_query.edit_message_text(text=text)
|
||||
await update.callback_query.edit_message_text(text=text)
|
||||
|
||||
return END
|
||||
|
||||
|
||||
# Second level conversation callbacks
|
||||
def select_level(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
async def select_level(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
"""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 = [
|
||||
|
@ -185,13 +187,13 @@ def select_level(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
|||
]
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
|
||||
update.callback_query.answer()
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
await update.callback_query.answer()
|
||||
await update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
|
||||
return SELECTING_LEVEL
|
||||
|
||||
|
||||
def select_gender(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
async def select_gender(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
"""Choose to add mother or father."""
|
||||
level = update.callback_query.data
|
||||
context.user_data[CURRENT_LEVEL] = level
|
||||
|
@ -212,22 +214,22 @@ def select_gender(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
|||
]
|
||||
keyboard = InlineKeyboardMarkup(buttons)
|
||||
|
||||
update.callback_query.answer()
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
await update.callback_query.answer()
|
||||
await update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
|
||||
return SELECTING_GENDER
|
||||
|
||||
|
||||
def end_second_level(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def end_second_level(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Return to top level conversation."""
|
||||
context.user_data[START_OVER] = True
|
||||
start(update, context)
|
||||
await start(update, context)
|
||||
|
||||
return END
|
||||
|
||||
|
||||
# Third level callbacks
|
||||
def select_feature(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
async def select_feature(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
"""Select a feature to update for the person."""
|
||||
buttons = [
|
||||
[
|
||||
|
@ -243,39 +245,39 @@ def select_feature(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str
|
|||
context.user_data[FEATURES] = {GENDER: update.callback_query.data}
|
||||
text = 'Please select a feature to update.'
|
||||
|
||||
update.callback_query.answer()
|
||||
update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
await update.callback_query.answer()
|
||||
await update.callback_query.edit_message_text(text=text, reply_markup=keyboard)
|
||||
# But after we do that, we need to send a new message
|
||||
else:
|
||||
text = 'Got it! Please select a feature to update.'
|
||||
update.message.reply_text(text=text, reply_markup=keyboard)
|
||||
await update.message.reply_text(text=text, reply_markup=keyboard)
|
||||
|
||||
context.user_data[START_OVER] = False
|
||||
return SELECTING_FEATURE
|
||||
|
||||
|
||||
def ask_for_input(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
async def ask_for_input(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
"""Prompt user to input data for selected feature."""
|
||||
context.user_data[CURRENT_FEATURE] = update.callback_query.data
|
||||
text = 'Okay, tell me.'
|
||||
|
||||
update.callback_query.answer()
|
||||
update.callback_query.edit_message_text(text=text)
|
||||
await update.callback_query.answer()
|
||||
await update.callback_query.edit_message_text(text=text)
|
||||
|
||||
return TYPING
|
||||
|
||||
|
||||
def save_input(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
async def save_input(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
"""Save input for feature and return to feature selection."""
|
||||
user_data = context.user_data
|
||||
user_data[FEATURES][user_data[CURRENT_FEATURE]] = update.message.text
|
||||
|
||||
user_data[START_OVER] = True
|
||||
|
||||
return select_feature(update, context)
|
||||
return await select_feature(update, context)
|
||||
|
||||
|
||||
def end_describing(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def end_describing(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""End gathering of features and return to parent conversation."""
|
||||
user_data = context.user_data
|
||||
level = user_data[CURRENT_LEVEL]
|
||||
|
@ -286,27 +288,24 @@ def end_describing(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int
|
|||
# Print upper level menu
|
||||
if level == SELF:
|
||||
user_data[START_OVER] = True
|
||||
start(update, context)
|
||||
await start(update, context)
|
||||
else:
|
||||
select_level(update, context)
|
||||
await select_level(update, context)
|
||||
|
||||
return END
|
||||
|
||||
|
||||
def stop_nested(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
async def stop_nested(update: Update, context: CallbackContext.DEFAULT_TYPE) -> str:
|
||||
"""Completely end conversation from within nested conversation."""
|
||||
update.message.reply_text('Okay, bye.')
|
||||
await update.message.reply_text('Okay, bye.')
|
||||
|
||||
return STOPPING
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the bot."""
|
||||
# Create the Updater and pass it your bot's token.
|
||||
updater = Updater.builder().token("TOKEN").build()
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dispatcher = updater.dispatcher
|
||||
# Create the Application and pass it your bot's token.
|
||||
application = Application.builder().token("TOKEN").build()
|
||||
|
||||
# Set up third level ConversationHandler (collecting features)
|
||||
description_conv = ConversationHandler(
|
||||
|
@ -378,15 +377,10 @@ def main() -> None:
|
|||
fallbacks=[CommandHandler('stop', stop)],
|
||||
)
|
||||
|
||||
dispatcher.add_handler(conv_handler)
|
||||
application.add_handler(conv_handler)
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Run the bot until you press Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT. This should be used most of the time, since
|
||||
# start_polling() is non-blocking and will stop the bot gracefully.
|
||||
updater.idle()
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -3,27 +3,32 @@
|
|||
<head>
|
||||
<title>Telegram passport test!</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<!--- Needs file from https://github.com/TelegramMessenger/TGPassportJsSDK downloaded --->
|
||||
<script src="telegram-passport.js"></script>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
Telegram.Passport.createAuthButton('telegram_passport_auth', {
|
||||
bot_id: BOT_ID, // YOUR BOT ID
|
||||
scope: {data: [{type: 'id_document', selfie: true}, 'address_document', 'phone_number', 'email'], v: 1}, // WHAT DATA YOU WANT TO RECEIVE
|
||||
public_key: '-----BEGIN PUBLIC KEY----- ...', // YOUR PUBLIC KEY
|
||||
payload: 'thisisatest', // YOUR BOT WILL RECEIVE THIS DATA WITH THE REQUEST
|
||||
callback_url: 'https://example.org' // TELEGRAM WILL SEND YOUR USER BACK TO THIS URL
|
||||
});
|
||||
|
||||
</script>
|
||||
<meta content="IE=edge" http-equiv="X-UA-Compatible">
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Telegram passport test</h1>
|
||||
|
||||
<div id="telegram_passport_auth"></div>
|
||||
</body>
|
||||
|
||||
<!--- Needs file from https://github.com/TelegramMessenger/TGPassportJsSDK downloaded --->
|
||||
<script src="telegram-passport.js"></script>
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
Telegram.Passport.createAuthButton('telegram_passport_auth', {
|
||||
bot_id: 1234567890, // YOUR BOT ID
|
||||
scope: {
|
||||
data: [{
|
||||
type: 'id_document',
|
||||
selfie: true
|
||||
}, 'address_document', 'phone_number', 'email'], v: 1
|
||||
}, // WHAT DATA YOU WANT TO RECEIVE
|
||||
public_key: '-----BEGIN PUBLIC KEY-----\n', // YOUR PUBLIC KEY
|
||||
nonce: 'thisisatest', // YOUR BOT WILL RECEIVE THIS DATA WITH THE REQUEST
|
||||
callback_url: 'https://example.org' // TELEGRAM WILL SEND YOUR USER BACK TO THIS URL
|
||||
});
|
||||
|
||||
</script>
|
||||
</html>
|
||||
|
|
|
@ -15,18 +15,18 @@ import logging
|
|||
from pathlib import Path
|
||||
|
||||
from telegram import Update
|
||||
from telegram.ext import MessageHandler, filters, Updater, CallbackContext
|
||||
from telegram.ext import MessageHandler, filters, Application, CallbackContext
|
||||
|
||||
# Enable logging
|
||||
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.DEBUG
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def msg(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def msg(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Downloads and prints the received passport data."""
|
||||
# Retrieve passport data
|
||||
passport_data = update.message.passport_data
|
||||
|
@ -62,28 +62,28 @@ def msg(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
|||
):
|
||||
print(data.type, len(data.files), 'files')
|
||||
for file in data.files:
|
||||
actual_file = file.get_file()
|
||||
actual_file = await file.get_file()
|
||||
print(actual_file)
|
||||
actual_file.download()
|
||||
await actual_file.download()
|
||||
if (
|
||||
data.type in ('passport', 'driver_license', 'identity_card', 'internal_passport')
|
||||
and data.front_side
|
||||
):
|
||||
front_file = data.front_side.get_file()
|
||||
front_file = await data.front_side.get_file()
|
||||
print(data.type, front_file)
|
||||
front_file.download()
|
||||
await front_file.download()
|
||||
if data.type in ('driver_license' and 'identity_card') and data.reverse_side:
|
||||
reverse_file = data.reverse_side.get_file()
|
||||
reverse_file = await data.reverse_side.get_file()
|
||||
print(data.type, reverse_file)
|
||||
reverse_file.download()
|
||||
await reverse_file.download()
|
||||
if (
|
||||
data.type in ('passport', 'driver_license', 'identity_card', 'internal_passport')
|
||||
and data.selfie
|
||||
):
|
||||
selfie_file = data.selfie.get_file()
|
||||
selfie_file = await data.selfie.get_file()
|
||||
print(data.type, selfie_file)
|
||||
selfie_file.download()
|
||||
if data.type in (
|
||||
await selfie_file.download()
|
||||
if data.translation and data.type in (
|
||||
'passport',
|
||||
'driver_license',
|
||||
'identity_card',
|
||||
|
@ -96,30 +96,24 @@ def msg(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
|||
):
|
||||
print(data.type, len(data.translation), 'translation')
|
||||
for file in data.translation:
|
||||
actual_file = file.get_file()
|
||||
actual_file = await file.get_file()
|
||||
print(actual_file)
|
||||
actual_file.download()
|
||||
await actual_file.download()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Start the bot."""
|
||||
# Create the Updater and pass it your token and private key
|
||||
# Create the Application and pass it your token and private key
|
||||
private_key = Path('private.key')
|
||||
updater = Updater.builder().token("TOKEN").private_key(private_key.read_bytes()).build()
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dispatcher = updater.dispatcher
|
||||
application = (
|
||||
Application.builder().token("TOKEN").private_key(private_key.read_bytes()).build()
|
||||
)
|
||||
|
||||
# On messages that include passport data call msg
|
||||
dispatcher.add_handler(MessageHandler(filters.PASSPORT_DATA, msg))
|
||||
application.add_handler(MessageHandler(filters.PASSPORT_DATA, msg))
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Run the bot until you press Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT. This should be used most of the time, since
|
||||
# start_polling() is non-blocking and will stop the bot gracefully.
|
||||
updater.idle()
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -13,7 +13,7 @@ from telegram.ext import (
|
|||
filters,
|
||||
PreCheckoutQueryHandler,
|
||||
ShippingQueryHandler,
|
||||
Updater,
|
||||
Application,
|
||||
CallbackContext,
|
||||
)
|
||||
|
||||
|
@ -24,18 +24,22 @@ logging.basicConfig(
|
|||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PAYMENT_PROVIDER_TOKEN = "PAYMENT_PROVIDER_TOKEN"
|
||||
|
||||
def start_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
|
||||
async def start_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Displays info on how to use the bot."""
|
||||
msg = (
|
||||
"Use /shipping to get an invoice for shipping-payment, or /noshipping for an "
|
||||
"invoice without shipping."
|
||||
)
|
||||
|
||||
update.message.reply_text(msg)
|
||||
await update.message.reply_text(msg)
|
||||
|
||||
|
||||
def start_with_shipping_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def start_with_shipping_callback(
|
||||
update: Update, context: CallbackContext.DEFAULT_TYPE
|
||||
) -> None:
|
||||
"""Sends an invoice with shipping-payment."""
|
||||
chat_id = update.message.chat_id
|
||||
title = "Payment Example"
|
||||
|
@ -43,7 +47,6 @@ def start_with_shipping_callback(update: Update, context: CallbackContext.DEFAUL
|
|||
# select a payload just for you to recognize its the donation from your bot
|
||||
payload = "Custom-Payload"
|
||||
# In order to get a provider_token see https://core.telegram.org/bots/payments#getting-a-token
|
||||
provider_token = "PROVIDER_TOKEN"
|
||||
currency = "USD"
|
||||
# price in dollars
|
||||
price = 1
|
||||
|
@ -53,12 +56,12 @@ def start_with_shipping_callback(update: Update, context: CallbackContext.DEFAUL
|
|||
|
||||
# optionally pass need_name=True, need_phone_number=True,
|
||||
# need_email=True, need_shipping_address=True, is_flexible=True
|
||||
context.bot.send_invoice(
|
||||
await context.bot.send_invoice(
|
||||
chat_id,
|
||||
title,
|
||||
description,
|
||||
payload,
|
||||
provider_token,
|
||||
PAYMENT_PROVIDER_TOKEN,
|
||||
currency,
|
||||
prices,
|
||||
need_name=True,
|
||||
|
@ -69,7 +72,9 @@ def start_with_shipping_callback(update: Update, context: CallbackContext.DEFAUL
|
|||
)
|
||||
|
||||
|
||||
def start_without_shipping_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def start_without_shipping_callback(
|
||||
update: Update, context: CallbackContext.DEFAULT_TYPE
|
||||
) -> None:
|
||||
"""Sends an invoice without shipping-payment."""
|
||||
chat_id = update.message.chat_id
|
||||
title = "Payment Example"
|
||||
|
@ -77,7 +82,6 @@ def start_without_shipping_callback(update: Update, context: CallbackContext.DEF
|
|||
# select a payload just for you to recognize its the donation from your bot
|
||||
payload = "Custom-Payload"
|
||||
# In order to get a provider_token see https://core.telegram.org/bots/payments#getting-a-token
|
||||
provider_token = "PROVIDER_TOKEN"
|
||||
currency = "USD"
|
||||
# price in dollars
|
||||
price = 1
|
||||
|
@ -86,18 +90,18 @@ def start_without_shipping_callback(update: Update, context: CallbackContext.DEF
|
|||
|
||||
# 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, currency, prices
|
||||
await context.bot.send_invoice(
|
||||
chat_id, title, description, payload, PAYMENT_PROVIDER_TOKEN, currency, prices
|
||||
)
|
||||
|
||||
|
||||
def shipping_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def shipping_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Answers the ShippingQuery with ShippingOptions"""
|
||||
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...")
|
||||
await query.answer(ok=False, error_message="Something went wrong...")
|
||||
return
|
||||
|
||||
# First option has a single LabeledPrice
|
||||
|
@ -105,59 +109,55 @@ def shipping_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) ->
|
|||
# second option has 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)
|
||||
await query.answer(ok=True, shipping_options=options)
|
||||
|
||||
|
||||
# after (optional) shipping, it's the pre-checkout
|
||||
def precheckout_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def precheckout_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Answers the PreQecheckoutQuery"""
|
||||
query = update.pre_checkout_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...")
|
||||
await query.answer(ok=False, error_message="Something went wrong...")
|
||||
else:
|
||||
query.answer(ok=True)
|
||||
await query.answer(ok=True)
|
||||
|
||||
|
||||
# finally, after contacting the payment provider...
|
||||
def successful_payment_callback(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def successful_payment_callback(
|
||||
update: Update, context: CallbackContext.DEFAULT_TYPE
|
||||
) -> None:
|
||||
"""Confirms the successful payment."""
|
||||
# do something after successfully receiving payment?
|
||||
update.message.reply_text("Thank you for your payment!")
|
||||
await update.message.reply_text("Thank you for your payment!")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the bot."""
|
||||
# Create the Updater and pass it your bot's token.
|
||||
updater = Updater.builder().token("TOKEN").build()
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dispatcher = updater.dispatcher
|
||||
# Create the Application and pass it your bot's token.
|
||||
application = Application.builder().token("TOKEN").build()
|
||||
|
||||
# simple start function
|
||||
dispatcher.add_handler(CommandHandler("start", start_callback))
|
||||
application.add_handler(CommandHandler("start", start_callback))
|
||||
|
||||
# Add command handler to start the payment invoice
|
||||
dispatcher.add_handler(CommandHandler("shipping", start_with_shipping_callback))
|
||||
dispatcher.add_handler(CommandHandler("noshipping", start_without_shipping_callback))
|
||||
application.add_handler(CommandHandler("shipping", start_with_shipping_callback))
|
||||
application.add_handler(CommandHandler("noshipping", start_without_shipping_callback))
|
||||
|
||||
# Optional handler if your product requires shipping
|
||||
dispatcher.add_handler(ShippingQueryHandler(shipping_callback))
|
||||
application.add_handler(ShippingQueryHandler(shipping_callback))
|
||||
|
||||
# Pre-checkout handler to final check
|
||||
dispatcher.add_handler(PreCheckoutQueryHandler(precheckout_callback))
|
||||
application.add_handler(PreCheckoutQueryHandler(precheckout_callback))
|
||||
|
||||
# Success! Notify your user!
|
||||
dispatcher.add_handler(MessageHandler(filters.SUCCESSFUL_PAYMENT, successful_payment_callback))
|
||||
application.add_handler(
|
||||
MessageHandler(filters.SUCCESSFUL_PAYMENT, successful_payment_callback)
|
||||
)
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Run the bot until you press Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT. This should be used most of the time, since
|
||||
# start_polling() is non-blocking and will stop the bot gracefully.
|
||||
updater.idle()
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
"""
|
||||
First, a few callback functions are defined. Then, those functions are passed to
|
||||
the Dispatcher and registered at their respective places.
|
||||
the Application and registered at their respective places.
|
||||
Then, the bot is started and runs until we press Ctrl-C on the command line.
|
||||
|
||||
Usage:
|
||||
|
@ -24,7 +24,7 @@ from telegram.ext import (
|
|||
filters,
|
||||
ConversationHandler,
|
||||
PicklePersistence,
|
||||
Updater,
|
||||
Application,
|
||||
CallbackContext,
|
||||
)
|
||||
|
||||
|
@ -51,7 +51,7 @@ def facts_to_str(user_data: Dict[str, str]) -> str:
|
|||
return "\n".join(facts).join(['\n', '\n'])
|
||||
|
||||
|
||||
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Start the conversation, display any stored data and ask user for input."""
|
||||
reply_text = "Hi! My name is Doctor Botter."
|
||||
if context.user_data:
|
||||
|
@ -64,12 +64,12 @@ def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
|||
" 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)
|
||||
await update.message.reply_text(reply_text, reply_markup=markup)
|
||||
|
||||
return CHOOSING
|
||||
|
||||
|
||||
def regular_choice(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def regular_choice(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Ask the user for info about the selected predefined choice."""
|
||||
text = update.message.text.lower()
|
||||
context.user_data['choice'] = text
|
||||
|
@ -79,28 +79,28 @@ def regular_choice(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int
|
|||
)
|
||||
else:
|
||||
reply_text = f'Your {text}? Yes, I would love to hear about that!'
|
||||
update.message.reply_text(reply_text)
|
||||
await update.message.reply_text(reply_text)
|
||||
|
||||
return TYPING_REPLY
|
||||
|
||||
|
||||
def custom_choice(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def custom_choice(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Ask the user for a description of a custom category."""
|
||||
update.message.reply_text(
|
||||
await update.message.reply_text(
|
||||
'Alright, please send me the category first, for example "Most impressive skill"'
|
||||
)
|
||||
|
||||
return TYPING_CHOICE
|
||||
|
||||
|
||||
def received_information(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def received_information(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Store info provided by user and ask for the next category."""
|
||||
text = update.message.text
|
||||
category = context.user_data['choice']
|
||||
context.user_data[category] = text.lower()
|
||||
del context.user_data['choice']
|
||||
|
||||
update.message.reply_text(
|
||||
await 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.",
|
||||
|
@ -110,19 +110,19 @@ def received_information(update: Update, context: CallbackContext.DEFAULT_TYPE)
|
|||
return CHOOSING
|
||||
|
||||
|
||||
def show_data(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def show_data(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Display the gathered info."""
|
||||
update.message.reply_text(
|
||||
await update.message.reply_text(
|
||||
f"This is what you already told me: {facts_to_str(context.user_data)}"
|
||||
)
|
||||
|
||||
|
||||
def done(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
async def done(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
||||
"""Display the gathered info and end the conversation."""
|
||||
if 'choice' in context.user_data:
|
||||
del context.user_data['choice']
|
||||
|
||||
update.message.reply_text(
|
||||
await update.message.reply_text(
|
||||
f"I learned these facts about you: {facts_to_str(context.user_data)}Until next time!",
|
||||
reply_markup=ReplyKeyboardRemove(),
|
||||
)
|
||||
|
@ -131,12 +131,9 @@ def done(update: Update, context: CallbackContext.DEFAULT_TYPE) -> int:
|
|||
|
||||
def main() -> None:
|
||||
"""Run the bot."""
|
||||
# Create the Updater and pass it your bot's token.
|
||||
# Create the Application and pass it your bot's token.
|
||||
persistence = PicklePersistence(filepath='conversationbot')
|
||||
updater = Updater.builder().token("TOKEN").persistence(persistence).build()
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dispatcher = updater.dispatcher
|
||||
application = Application.builder().token("TOKEN").persistence(persistence).build()
|
||||
|
||||
# Add conversation handler with the states CHOOSING, TYPING_CHOICE and TYPING_REPLY
|
||||
conv_handler = ConversationHandler(
|
||||
|
@ -165,18 +162,13 @@ def main() -> None:
|
|||
persistent=True,
|
||||
)
|
||||
|
||||
dispatcher.add_handler(conv_handler)
|
||||
application.add_handler(conv_handler)
|
||||
|
||||
show_data_handler = CommandHandler('show_data', show_data)
|
||||
dispatcher.add_handler(show_data_handler)
|
||||
application.add_handler(show_data_handler)
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Run the bot until you press Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT. This should be used most of the time, since
|
||||
# start_polling() is non-blocking and will stop the bot gracefully.
|
||||
updater.idle()
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -24,7 +24,7 @@ from telegram.ext import (
|
|||
PollHandler,
|
||||
MessageHandler,
|
||||
filters,
|
||||
Updater,
|
||||
Application,
|
||||
CallbackContext,
|
||||
)
|
||||
|
||||
|
@ -36,18 +36,18 @@ logging.basicConfig(
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Inform user about what this bot can do"""
|
||||
update.message.reply_text(
|
||||
await 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: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def poll(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Sends a predefined poll"""
|
||||
questions = ["Good", "Really good", "Fantastic", "Great"]
|
||||
message = context.bot.send_poll(
|
||||
message = await context.bot.send_poll(
|
||||
update.effective_chat.id,
|
||||
"How are you?",
|
||||
questions,
|
||||
|
@ -66,12 +66,12 @@ def poll(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
|||
context.bot_data.update(payload)
|
||||
|
||||
|
||||
def receive_poll_answer(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def receive_poll_answer(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Summarize a users poll vote"""
|
||||
answer = update.poll_answer
|
||||
poll_id = answer.poll_id
|
||||
answered_poll = context.bot_data[answer.poll_id]
|
||||
try:
|
||||
questions = context.bot_data[poll_id]["questions"]
|
||||
questions = answered_poll["questions"]
|
||||
# this means this poll answer update is from an old poll, we can't do our answering then
|
||||
except KeyError:
|
||||
return
|
||||
|
@ -82,23 +82,21 @@ def receive_poll_answer(update: Update, context: CallbackContext.DEFAULT_TYPE) -
|
|||
answer_string += questions[question_id] + " and "
|
||||
else:
|
||||
answer_string += questions[question_id]
|
||||
context.bot.send_message(
|
||||
context.bot_data[poll_id]["chat_id"],
|
||||
await context.bot.send_message(
|
||||
answered_poll["chat_id"],
|
||||
f"{update.effective_user.mention_html()} feels {answer_string}!",
|
||||
parse_mode=ParseMode.HTML,
|
||||
)
|
||||
context.bot_data[poll_id]["answers"] += 1
|
||||
answered_poll["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"]
|
||||
)
|
||||
if answered_poll["answers"] == 3:
|
||||
await context.bot.stop_poll(answered_poll["chat_id"], answered_poll["message_id"])
|
||||
|
||||
|
||||
def quiz(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def quiz(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Send a predefined poll"""
|
||||
questions = ["1", "2", "4", "20"]
|
||||
message = update.effective_message.reply_poll(
|
||||
message = await 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
|
||||
|
@ -108,7 +106,7 @@ def quiz(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
|||
context.bot_data.update(payload)
|
||||
|
||||
|
||||
def receive_quiz_answer(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def receive_quiz_answer(update: Update, context: CallbackContext.DEFAULT_TYPE) -> 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:
|
||||
|
@ -119,26 +117,26 @@ def receive_quiz_answer(update: Update, context: CallbackContext.DEFAULT_TYPE) -
|
|||
# this means this poll answer update is from an old poll, we can't stop it then
|
||||
except KeyError:
|
||||
return
|
||||
context.bot.stop_poll(quiz_data["chat_id"], quiz_data["message_id"])
|
||||
await context.bot.stop_poll(quiz_data["chat_id"], quiz_data["message_id"])
|
||||
|
||||
|
||||
def preview(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def preview(update: Update, context: CallbackContext.DEFAULT_TYPE) -> 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(
|
||||
await update.effective_message.reply_text(
|
||||
message, reply_markup=ReplyKeyboardMarkup(button, one_time_keyboard=True)
|
||||
)
|
||||
|
||||
|
||||
def receive_poll(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def receive_poll(update: Update, context: CallbackContext.DEFAULT_TYPE) -> 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
|
||||
# a closed poll
|
||||
update.effective_message.reply_poll(
|
||||
await update.effective_message.reply_poll(
|
||||
question=actual_poll.question,
|
||||
options=[o.text for o in actual_poll.options],
|
||||
# with is_closed true, the poll/quiz is immediately closed
|
||||
|
@ -147,31 +145,26 @@ def receive_poll(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
|||
)
|
||||
|
||||
|
||||
def help_handler(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def help_handler(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Display a help message"""
|
||||
update.message.reply_text("Use /quiz, /poll or /preview to test this bot.")
|
||||
await update.message.reply_text("Use /quiz, /poll or /preview to test this bot.")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run bot."""
|
||||
# Create the Updater and pass it your bot's token.
|
||||
updater = Updater.builder().token("TOKEN").build()
|
||||
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))
|
||||
# Create the Application and pass it your bot's token.
|
||||
application = Application.builder().token("TOKEN").build()
|
||||
application.add_handler(CommandHandler('start', start))
|
||||
application.add_handler(CommandHandler('poll', poll))
|
||||
application.add_handler(CommandHandler('quiz', quiz))
|
||||
application.add_handler(CommandHandler('preview', preview))
|
||||
application.add_handler(CommandHandler('help', help_handler))
|
||||
application.add_handler(MessageHandler(filters.POLL, receive_poll))
|
||||
application.add_handler(PollAnswerHandler(receive_poll_answer))
|
||||
application.add_handler(PollHandler(receive_quiz_answer))
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Run the bot until the user presses Ctrl-C or the process receives SIGINT,
|
||||
# SIGTERM or SIGABRT
|
||||
updater.idle()
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -6,55 +6,61 @@ This is built on the API wrapper, see echobot.py to see the same example built
|
|||
on the telegram.ext bot framework.
|
||||
This program is dedicated to the public domain under the CC0 license.
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import NoReturn
|
||||
from time import sleep
|
||||
|
||||
import telegram
|
||||
from telegram.error import NetworkError, Unauthorized
|
||||
from telegram import Bot
|
||||
from telegram.error import NetworkError, Forbidden
|
||||
|
||||
|
||||
UPDATE_ID = None
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main() -> NoReturn:
|
||||
async def main() -> NoReturn:
|
||||
"""Run the bot."""
|
||||
global UPDATE_ID
|
||||
# Telegram Bot Authorization Token
|
||||
bot = telegram.Bot('TOKEN')
|
||||
|
||||
# get the first pending update_id, this is so we can skip over it in case
|
||||
# we get an "Unauthorized" exception.
|
||||
try:
|
||||
UPDATE_ID = bot.get_updates()[0].update_id
|
||||
except IndexError:
|
||||
UPDATE_ID = None
|
||||
|
||||
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
|
||||
while True:
|
||||
# Here we use the `async with` syntax to properly initialize and shutdown resources.
|
||||
async with Bot("TOKEN") as bot:
|
||||
# get the first pending update_id, this is so we can skip over it in case
|
||||
# we get a "Forbidden" exception.
|
||||
try:
|
||||
echo(bot)
|
||||
except NetworkError:
|
||||
sleep(1)
|
||||
except Unauthorized:
|
||||
# The user has removed or blocked the bot.
|
||||
UPDATE_ID += 1
|
||||
update_id = (await bot.get_updates())[0].update_id
|
||||
except IndexError:
|
||||
update_id = None
|
||||
|
||||
logger.info("listening for new messages...")
|
||||
while True:
|
||||
try:
|
||||
update_id = await echo(bot, update_id)
|
||||
except NetworkError:
|
||||
await asyncio.sleep(1)
|
||||
except Forbidden:
|
||||
# The user has removed or blocked the bot.
|
||||
update_id += 1
|
||||
|
||||
|
||||
def echo(bot: telegram.Bot) -> None:
|
||||
async def echo(bot: Bot, update_id: int) -> int:
|
||||
"""Echo the message the user sent."""
|
||||
global UPDATE_ID
|
||||
# Request updates after the last update_id
|
||||
for update in bot.get_updates(offset=UPDATE_ID, timeout=10):
|
||||
UPDATE_ID = update.update_id + 1
|
||||
updates = await bot.get_updates(offset=update_id, timeout=10)
|
||||
for update in updates:
|
||||
next_update_id = update.update_id + 1
|
||||
|
||||
# your bot can receive updates without messages
|
||||
# and not all messages contain text
|
||||
if update.message and update.message.text:
|
||||
# Reply to the message
|
||||
update.message.reply_text(update.message.text)
|
||||
logger.info("Found message %s!", update.message.text)
|
||||
await update.message.reply_text(update.message.text)
|
||||
return next_update_id
|
||||
return update_id
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt: # Ignore exception when Ctrl-C is pressed
|
||||
pass
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
"""
|
||||
Simple Bot to send timed Telegram messages.
|
||||
|
||||
This Bot uses the Updater class to handle the bot and the JobQueue to send
|
||||
This Bot uses the Application class to handle the bot and the JobQueue to send
|
||||
timed messages.
|
||||
|
||||
First, a few handler functions are defined. Then, those functions are passed to
|
||||
the Dispatcher and registered at their respective places.
|
||||
the Application and registered at their respective places.
|
||||
Then, the bot is started and runs until we press Ctrl-C on the command line.
|
||||
|
||||
Usage:
|
||||
|
@ -21,30 +21,29 @@ bot.
|
|||
import logging
|
||||
|
||||
from telegram import Update
|
||||
from telegram.ext import CommandHandler, Updater, CallbackContext
|
||||
from telegram.ext import CommandHandler, Application, CallbackContext
|
||||
|
||||
# Enable logging
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Define a few command handlers. These usually take the two arguments update and
|
||||
# context. Error handlers also receive the raised TelegramError object in error.
|
||||
# context.
|
||||
# Best practice would be to replace context with an underscore,
|
||||
# since context is an unused local variable.
|
||||
# This being an example and not having context present confusing beginners,
|
||||
# we decided to have it present as context.
|
||||
def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def start(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Sends explanation on how to use the bot."""
|
||||
update.message.reply_text('Hi! Use /set <seconds> to set a timer')
|
||||
await update.message.reply_text('Hi! Use /set <seconds> to set a timer')
|
||||
|
||||
|
||||
def alarm(context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def alarm(context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Send the alarm message."""
|
||||
job = context.job
|
||||
context.bot.send_message(job.context, text='Beep!')
|
||||
await context.bot.send_message(job.chat_id, text=f'Beep! {job.context} seconds are over!')
|
||||
|
||||
|
||||
def remove_job_if_exists(name: str, context: CallbackContext.DEFAULT_TYPE) -> bool:
|
||||
|
@ -57,57 +56,48 @@ def remove_job_if_exists(name: str, context: CallbackContext.DEFAULT_TYPE) -> bo
|
|||
return True
|
||||
|
||||
|
||||
def set_timer(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def set_timer(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Add a job to the queue."""
|
||||
chat_id = update.message.chat_id
|
||||
chat_id = update.effective_message.chat_id
|
||||
try:
|
||||
# args[0] should contain the time for the timer in seconds
|
||||
due = int(context.args[0])
|
||||
if due < 0:
|
||||
update.message.reply_text('Sorry we can not go back to future!')
|
||||
await update.message.reply_text('Sorry we can not go back to future!')
|
||||
return
|
||||
|
||||
job_removed = remove_job_if_exists(str(chat_id), context)
|
||||
context.job_queue.run_once(alarm, due, context=chat_id, name=str(chat_id))
|
||||
context.job_queue.run_once(alarm, due, chat_id=chat_id, name=str(chat_id), context=due)
|
||||
|
||||
text = 'Timer successfully set!'
|
||||
if job_removed:
|
||||
text += ' Old one was removed.'
|
||||
update.message.reply_text(text)
|
||||
await update.message.reply_text(text)
|
||||
|
||||
except (IndexError, ValueError):
|
||||
update.message.reply_text('Usage: /set <seconds>')
|
||||
await update.effective_message.reply_text('Usage: /set <seconds>')
|
||||
|
||||
|
||||
def unset(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
async def unset(update: Update, context: CallbackContext.DEFAULT_TYPE) -> None:
|
||||
"""Remove the job if the user changed their mind."""
|
||||
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)
|
||||
await update.message.reply_text(text)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run bot."""
|
||||
# Create the Updater and pass it your bot's token.
|
||||
updater = Updater.builder().token("TOKEN").build()
|
||||
|
||||
# Get the dispatcher to register handlers
|
||||
dispatcher = updater.dispatcher
|
||||
# Create the Application and pass it your bot's token.
|
||||
application = Application.builder().token("TOKEN").build()
|
||||
|
||||
# on different commands - answer in Telegram
|
||||
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))
|
||||
application.add_handler(CommandHandler(["start", "help"], start))
|
||||
application.add_handler(CommandHandler("set", set_timer))
|
||||
application.add_handler(CommandHandler("unset", unset))
|
||||
|
||||
# Start the Bot
|
||||
updater.start_polling()
|
||||
|
||||
# Block until you press Ctrl-C or the process receives SIGINT, SIGTERM or
|
||||
# SIGABRT. This should be used most of the time, since start_polling() is
|
||||
# non-blocking and will stop the bot gracefully.
|
||||
updater.idle()
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
application.run_polling()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -8,4 +8,3 @@ skip-string-normalization = true
|
|||
# see https://github.com/psf/black/issues/1778
|
||||
force-exclude = '^(?!/(telegram|examples|tests)/).*\.py$'
|
||||
include = '(telegram|examples|tests)/.*\.py$'
|
||||
exclude = 'telegram/vendor'
|
|
@ -4,12 +4,15 @@ cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3
|
|||
pre-commit
|
||||
# Make sure that the versions specified here match the pre-commit settings!
|
||||
black==21.9b0
|
||||
# hardpinned dependency for black
|
||||
click==8.0.2
|
||||
flake8==4.0.1
|
||||
pylint==2.12.1
|
||||
mypy==0.910
|
||||
pyupgrade==2.29.0
|
||||
|
||||
pytest==6.2.5
|
||||
pytest-asyncio==0.16.0
|
||||
|
||||
flaky
|
||||
beautifulsoup4
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Make sure to install those as additional_dependencies in the
|
||||
# pre-commit hooks for pylint & mypy
|
||||
certifi
|
||||
httpx ~= 0.22.0
|
||||
# only telegram.ext: # Keep this line here; used in setup(-raw).py
|
||||
tornado>=6.1
|
||||
APScheduler==3.6.3
|
||||
APScheduler==3.8.1
|
||||
pytz>=2018.6
|
||||
cachetools==4.2.2
|
||||
|
|
18
setup.cfg
18
setup.cfg
|
@ -13,13 +13,13 @@ upload-dir = docs/build/html
|
|||
max-line-length = 99
|
||||
ignore = W503, W605
|
||||
extend-ignore = E203
|
||||
exclude = setup.py, setup-raw.py docs/source/conf.py, telegram/vendor
|
||||
|
||||
[pylint]
|
||||
ignore=vendor
|
||||
exclude = setup.py, setup-raw.py docs/source/conf.py
|
||||
|
||||
[pylint.message-control]
|
||||
disable = C0330,R0801,R0913,R0904,R0903,R0902,W0511,C0116,C0115,W0703,R0914,R0914,C0302,R0912,R0915,R0401
|
||||
disable = duplicate-code,too-many-arguments,too-many-public-methods,too-few-public-methods,
|
||||
broad-except,too-many-instance-attributes,fixme,missing-function-docstring,
|
||||
missing-class-docstring,too-many-locals,too-many-lines,too-many-branches,
|
||||
too-many-statements,cyclic-import
|
||||
|
||||
[tool:pytest]
|
||||
testpaths = tests
|
||||
|
@ -27,6 +27,10 @@ addopts = --no-success-flaky-report -rsxX
|
|||
filterwarnings =
|
||||
error
|
||||
ignore::DeprecationWarning
|
||||
ignore:Tasks created via `Application\.create_task` while the application is not running
|
||||
ignore::ResourceWarning
|
||||
; TODO: Write so good code that we don't need to ignore ResourceWarnings anymore
|
||||
|
||||
; Unfortunately due to https://github.com/pytest-dev/pytest/issues/8343 we can't have this here
|
||||
; and instead do a trick directly in tests/conftest.py
|
||||
; ignore::telegram.utils.deprecate.TelegramDeprecationWarning
|
||||
|
@ -40,7 +44,6 @@ concurrency = thread, multiprocessing
|
|||
omit =
|
||||
tests/
|
||||
telegram/__main__.py
|
||||
telegram/vendor/*
|
||||
|
||||
[coverage:report]
|
||||
exclude_lines =
|
||||
|
@ -69,8 +72,5 @@ strict_optional = False
|
|||
[mypy-telegram.ext._utils.webhookhandler]
|
||||
warn_unused_ignores = False
|
||||
|
||||
[mypy-urllib3.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-apscheduler.*]
|
||||
ignore_missing_imports = True
|
||||
|
|
11
setup.py
11
setup.py
|
@ -1,13 +1,11 @@
|
|||
#!/usr/bin/env python
|
||||
"""The setup and build script for the python-telegram-bot library."""
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
UPSTREAM_URLLIB3_FLAG = '--with-upstream-urllib3'
|
||||
|
||||
|
||||
def get_requirements(raw=False):
|
||||
"""Build the requirements list for this project"""
|
||||
|
@ -33,11 +31,6 @@ def get_packages_requirements(raw=False):
|
|||
exclude.append('telegram.ext*')
|
||||
|
||||
packs = find_packages(exclude=exclude)
|
||||
# Allow for a package install to not use the vendored urllib3
|
||||
if UPSTREAM_URLLIB3_FLAG in sys.argv:
|
||||
sys.argv.remove(UPSTREAM_URLLIB3_FLAG)
|
||||
reqs.append('urllib3 >= 1.19.1')
|
||||
packs = [x for x in packs if not x.startswith('telegram.vendor.ptb_urllib3')]
|
||||
|
||||
return packs, reqs
|
||||
|
||||
|
@ -79,7 +72,7 @@ def get_setup_kwargs(raw=False):
|
|||
install_requires=requirements,
|
||||
extras_require={
|
||||
'json': 'ujson',
|
||||
'socks': 'PySocks',
|
||||
'socks': 'httpx[socks]',
|
||||
# 3.4-3.4.3 contained some cyclical import bugs
|
||||
'passport': 'cryptography!=3.4,!=3.4.1,!=3.4.2,!=3.4.3',
|
||||
},
|
||||
|
|
|
@ -21,8 +21,6 @@ import subprocess
|
|||
import sys
|
||||
from typing import Optional
|
||||
|
||||
import certifi
|
||||
|
||||
from . import __version__ as telegram_ver
|
||||
from .constants import BOT_API_VERSION
|
||||
|
||||
|
@ -41,7 +39,6 @@ def print_ver_info() -> None: # skipcq: PY-D0003
|
|||
git_revision = _git_revision()
|
||||
print(f'python-telegram-bot {telegram_ver}' + (f' ({git_revision})' if git_revision else ''))
|
||||
print(f'Bot API {BOT_API_VERSION}')
|
||||
print(f'certifi {certifi.__version__}') # type: ignore[attr-defined]
|
||||
sys_version = sys.version.replace('\n', ' ')
|
||||
print(f'Python {sys_version}')
|
||||
|
||||
|
|
2936
telegram/_bot.py
2936
telegram/_bot.py
File diff suppressed because it is too large
Load diff
|
@ -47,7 +47,7 @@ class CallbackQuery(TelegramObject):
|
|||
considered equal, if their :attr:`id` is equal.
|
||||
|
||||
Note:
|
||||
* In Python :keyword:`from` is a reserved word, :paramref:`from_user`
|
||||
* In Python :keyword:`from` is a reserved word use :paramref:`from_user` instead.
|
||||
* Exactly one of the fields :attr:`data` or :attr:`game_short_name` will be present.
|
||||
* After the user presses an inline button, Telegram clients will display a progress bar
|
||||
until you call :attr:`answer`. It is, therefore, necessary to react
|
||||
|
@ -142,18 +142,21 @@ class CallbackQuery(TelegramObject):
|
|||
|
||||
return cls(bot=bot, **data)
|
||||
|
||||
def answer(
|
||||
async def answer(
|
||||
self,
|
||||
text: str = None,
|
||||
show_alert: bool = False,
|
||||
url: str = None,
|
||||
cache_time: int = None,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> bool:
|
||||
"""Shortcut for::
|
||||
|
||||
bot.answer_callback_query(update.callback_query.id, *args, **kwargs)
|
||||
await bot.answer_callback_query(update.callback_query.id, *args, **kwargs)
|
||||
|
||||
For the documentation of the arguments, please see
|
||||
:meth:`telegram.Bot.answer_callback_query`.
|
||||
|
@ -162,23 +165,29 @@ class CallbackQuery(TelegramObject):
|
|||
:obj:`bool`: On success, :obj:`True` is returned.
|
||||
|
||||
"""
|
||||
return self.get_bot().answer_callback_query(
|
||||
return await self.get_bot().answer_callback_query(
|
||||
callback_query_id=self.id,
|
||||
text=text,
|
||||
show_alert=show_alert,
|
||||
url=url,
|
||||
cache_time=cache_time,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
def edit_message_text(
|
||||
async def edit_message_text(
|
||||
self,
|
||||
text: str,
|
||||
parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
disable_web_page_preview: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: 'InlineKeyboardMarkup' = None,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
|
||||
) -> Union[Message, bool]:
|
||||
|
@ -200,33 +209,42 @@ class CallbackQuery(TelegramObject):
|
|||
|
||||
"""
|
||||
if self.inline_message_id:
|
||||
return self.get_bot().edit_message_text(
|
||||
return await self.get_bot().edit_message_text(
|
||||
inline_message_id=self.inline_message_id,
|
||||
text=text,
|
||||
parse_mode=parse_mode,
|
||||
disable_web_page_preview=disable_web_page_preview,
|
||||
reply_markup=reply_markup,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
entities=entities,
|
||||
chat_id=None,
|
||||
message_id=None,
|
||||
)
|
||||
return self.message.edit_text(
|
||||
return await self.message.edit_text(
|
||||
text=text,
|
||||
parse_mode=parse_mode,
|
||||
disable_web_page_preview=disable_web_page_preview,
|
||||
reply_markup=reply_markup,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
entities=entities,
|
||||
)
|
||||
|
||||
def edit_message_caption(
|
||||
async def edit_message_caption(
|
||||
self,
|
||||
caption: str = None,
|
||||
reply_markup: 'InlineKeyboardMarkup' = None,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
caption_entities: Union[List['MessageEntity'], Tuple['MessageEntity', ...]] = None,
|
||||
|
@ -250,30 +268,39 @@ class CallbackQuery(TelegramObject):
|
|||
|
||||
"""
|
||||
if self.inline_message_id:
|
||||
return self.get_bot().edit_message_caption(
|
||||
return await self.get_bot().edit_message_caption(
|
||||
caption=caption,
|
||||
inline_message_id=self.inline_message_id,
|
||||
reply_markup=reply_markup,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
parse_mode=parse_mode,
|
||||
api_kwargs=api_kwargs,
|
||||
caption_entities=caption_entities,
|
||||
chat_id=None,
|
||||
message_id=None,
|
||||
)
|
||||
return self.message.edit_caption(
|
||||
return await self.message.edit_caption(
|
||||
caption=caption,
|
||||
reply_markup=reply_markup,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
parse_mode=parse_mode,
|
||||
api_kwargs=api_kwargs,
|
||||
caption_entities=caption_entities,
|
||||
)
|
||||
|
||||
def edit_message_reply_markup(
|
||||
async def edit_message_reply_markup(
|
||||
self,
|
||||
reply_markup: Optional['InlineKeyboardMarkup'] = None,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> Union[Message, bool]:
|
||||
"""Shortcut for either::
|
||||
|
@ -303,25 +330,34 @@ class CallbackQuery(TelegramObject):
|
|||
|
||||
"""
|
||||
if self.inline_message_id:
|
||||
return self.get_bot().edit_message_reply_markup(
|
||||
return await self.get_bot().edit_message_reply_markup(
|
||||
reply_markup=reply_markup,
|
||||
inline_message_id=self.inline_message_id,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
chat_id=None,
|
||||
message_id=None,
|
||||
)
|
||||
return self.message.edit_reply_markup(
|
||||
return await self.message.edit_reply_markup(
|
||||
reply_markup=reply_markup,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
def edit_message_media(
|
||||
async def edit_message_media(
|
||||
self,
|
||||
media: 'InputMedia',
|
||||
reply_markup: 'InlineKeyboardMarkup' = None,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> Union[Message, bool]:
|
||||
"""Shortcut for either::
|
||||
|
@ -342,29 +378,38 @@ class CallbackQuery(TelegramObject):
|
|||
|
||||
"""
|
||||
if self.inline_message_id:
|
||||
return self.get_bot().edit_message_media(
|
||||
return await self.get_bot().edit_message_media(
|
||||
inline_message_id=self.inline_message_id,
|
||||
media=media,
|
||||
reply_markup=reply_markup,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
chat_id=None,
|
||||
message_id=None,
|
||||
)
|
||||
return self.message.edit_media(
|
||||
return await self.message.edit_media(
|
||||
media=media,
|
||||
reply_markup=reply_markup,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
def edit_message_live_location(
|
||||
async def edit_message_live_location(
|
||||
self,
|
||||
latitude: float = None,
|
||||
longitude: float = None,
|
||||
location: Location = None,
|
||||
reply_markup: 'InlineKeyboardMarkup' = None,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
horizontal_accuracy: float = None,
|
||||
heading: int = None,
|
||||
|
@ -391,13 +436,16 @@ class CallbackQuery(TelegramObject):
|
|||
|
||||
"""
|
||||
if self.inline_message_id:
|
||||
return self.get_bot().edit_message_live_location(
|
||||
return await self.get_bot().edit_message_live_location(
|
||||
inline_message_id=self.inline_message_id,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
location=location,
|
||||
reply_markup=reply_markup,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
horizontal_accuracy=horizontal_accuracy,
|
||||
heading=heading,
|
||||
|
@ -405,22 +453,28 @@ class CallbackQuery(TelegramObject):
|
|||
chat_id=None,
|
||||
message_id=None,
|
||||
)
|
||||
return self.message.edit_live_location(
|
||||
return await self.message.edit_live_location(
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
location=location,
|
||||
reply_markup=reply_markup,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
horizontal_accuracy=horizontal_accuracy,
|
||||
heading=heading,
|
||||
proximity_alert_radius=proximity_alert_radius,
|
||||
)
|
||||
|
||||
def stop_message_live_location(
|
||||
async def stop_message_live_location(
|
||||
self,
|
||||
reply_markup: 'InlineKeyboardMarkup' = None,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> Union[Message, bool]:
|
||||
"""Shortcut for either::
|
||||
|
@ -444,27 +498,36 @@ class CallbackQuery(TelegramObject):
|
|||
|
||||
"""
|
||||
if self.inline_message_id:
|
||||
return self.get_bot().stop_message_live_location(
|
||||
return await self.get_bot().stop_message_live_location(
|
||||
inline_message_id=self.inline_message_id,
|
||||
reply_markup=reply_markup,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
chat_id=None,
|
||||
message_id=None,
|
||||
)
|
||||
return self.message.stop_live_location(
|
||||
return await self.message.stop_live_location(
|
||||
reply_markup=reply_markup,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
def set_game_score(
|
||||
async def set_game_score(
|
||||
self,
|
||||
user_id: Union[int, str],
|
||||
score: int,
|
||||
force: bool = None,
|
||||
disable_edit_message: bool = None,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> Union[Message, bool]:
|
||||
"""Shortcut for either::
|
||||
|
@ -485,30 +548,39 @@ class CallbackQuery(TelegramObject):
|
|||
|
||||
"""
|
||||
if self.inline_message_id:
|
||||
return self.get_bot().set_game_score(
|
||||
return await self.get_bot().set_game_score(
|
||||
inline_message_id=self.inline_message_id,
|
||||
user_id=user_id,
|
||||
score=score,
|
||||
force=force,
|
||||
disable_edit_message=disable_edit_message,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
chat_id=None,
|
||||
message_id=None,
|
||||
)
|
||||
return self.message.set_game_score(
|
||||
return await self.message.set_game_score(
|
||||
user_id=user_id,
|
||||
score=score,
|
||||
force=force,
|
||||
disable_edit_message=disable_edit_message,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
def get_game_high_scores(
|
||||
async def get_game_high_scores(
|
||||
self,
|
||||
user_id: Union[int, str],
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> List['GameHighScore']:
|
||||
"""Shortcut for either::
|
||||
|
@ -529,23 +601,32 @@ class CallbackQuery(TelegramObject):
|
|||
|
||||
"""
|
||||
if self.inline_message_id:
|
||||
return self.get_bot().get_game_high_scores(
|
||||
return await self.get_bot().get_game_high_scores(
|
||||
inline_message_id=self.inline_message_id,
|
||||
user_id=user_id,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
chat_id=None,
|
||||
message_id=None,
|
||||
)
|
||||
return self.message.get_game_high_scores(
|
||||
return await self.message.get_game_high_scores(
|
||||
user_id=user_id,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
def delete_message(
|
||||
async def delete_message(
|
||||
self,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> bool:
|
||||
"""Shortcut for::
|
||||
|
@ -559,15 +640,21 @@ class CallbackQuery(TelegramObject):
|
|||
:obj:`bool`: On success, :obj:`True` is returned.
|
||||
|
||||
"""
|
||||
return self.message.delete(
|
||||
timeout=timeout,
|
||||
return await self.message.delete(
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
def pin_message(
|
||||
async def pin_message(
|
||||
self,
|
||||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> bool:
|
||||
"""Shortcut for::
|
||||
|
@ -581,15 +668,21 @@ class CallbackQuery(TelegramObject):
|
|||
:obj:`bool`: On success, :obj:`True` is returned.
|
||||
|
||||
"""
|
||||
return self.message.pin(
|
||||
return await self.message.pin(
|
||||
disable_notification=disable_notification,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
def unpin_message(
|
||||
async def unpin_message(
|
||||
self,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> bool:
|
||||
"""Shortcut for::
|
||||
|
@ -603,12 +696,15 @@ class CallbackQuery(TelegramObject):
|
|||
:obj:`bool`: On success, :obj:`True` is returned.
|
||||
|
||||
"""
|
||||
return self.message.unpin(
|
||||
timeout=timeout,
|
||||
return await self.message.unpin(
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
def copy_message(
|
||||
async def copy_message(
|
||||
self,
|
||||
chat_id: Union[int, str],
|
||||
caption: str = None,
|
||||
|
@ -618,7 +714,10 @@ class CallbackQuery(TelegramObject):
|
|||
reply_to_message_id: int = None,
|
||||
allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: ReplyMarkup = None,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
) -> 'MessageId':
|
||||
|
@ -629,7 +728,8 @@ class CallbackQuery(TelegramObject):
|
|||
from_chat_id=update.message.chat_id,
|
||||
message_id=update.message.message_id,
|
||||
*args,
|
||||
**kwargs)
|
||||
**kwargs
|
||||
)
|
||||
|
||||
For the documentation of the arguments, please see
|
||||
:meth:`telegram.Message.copy`.
|
||||
|
@ -638,7 +738,7 @@ class CallbackQuery(TelegramObject):
|
|||
:class:`telegram.MessageId`: On success, returns the MessageId of the sent message.
|
||||
|
||||
"""
|
||||
return self.message.copy(
|
||||
return await self.message.copy(
|
||||
chat_id=chat_id,
|
||||
caption=caption,
|
||||
parse_mode=parse_mode,
|
||||
|
@ -647,7 +747,10 @@ class CallbackQuery(TelegramObject):
|
|||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
protect_content=protect_content,
|
||||
)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -62,13 +62,7 @@ class ChatJoinRequest(TelegramObject):
|
|||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'chat',
|
||||
'from_user',
|
||||
'date',
|
||||
'bio',
|
||||
'invite_link',
|
||||
)
|
||||
__slots__ = ('chat', 'from_user', 'date', 'bio', 'invite_link')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -115,15 +109,19 @@ class ChatJoinRequest(TelegramObject):
|
|||
|
||||
return data
|
||||
|
||||
def approve(
|
||||
async def approve(
|
||||
self,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> bool:
|
||||
"""Shortcut for::
|
||||
|
||||
bot.approve_chat_join_request(chat_id=update.effective_chat.id,
|
||||
user_id=update.effective_user.id, *args, **kwargs)
|
||||
await bot.approve_chat_join_request(
|
||||
chat_id=update.effective_chat.id, user_id=update.effective_user.id, *args, **kwargs
|
||||
)
|
||||
|
||||
For the documentation of the arguments, please see
|
||||
:meth:`telegram.Bot.approve_chat_join_request`.
|
||||
|
@ -132,19 +130,29 @@ class ChatJoinRequest(TelegramObject):
|
|||
:obj:`bool`: On success, :obj:`True` is returned.
|
||||
|
||||
"""
|
||||
return self.get_bot().approve_chat_join_request(
|
||||
chat_id=self.chat.id, user_id=self.from_user.id, timeout=timeout, api_kwargs=api_kwargs
|
||||
return await self.get_bot().approve_chat_join_request(
|
||||
chat_id=self.chat.id,
|
||||
user_id=self.from_user.id,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
def decline(
|
||||
async def decline(
|
||||
self,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> bool:
|
||||
"""Shortcut for::
|
||||
|
||||
bot.decline_chat_join_request(chat_id=update.effective_chat.id,
|
||||
user_id=update.effective_user.id, *args, **kwargs)
|
||||
await bot.decline_chat_join_request(
|
||||
chat_id=update.effective_chat.id, user_id=update.effective_user.id, *args, **kwargs
|
||||
)
|
||||
|
||||
For the documentation of the arguments, please see
|
||||
:meth:`telegram.Bot.decline_chat_join_request`.
|
||||
|
@ -153,6 +161,12 @@ class ChatJoinRequest(TelegramObject):
|
|||
:obj:`bool`: On success, :obj:`True` is returned.
|
||||
|
||||
"""
|
||||
return self.get_bot().decline_chat_join_request(
|
||||
chat_id=self.chat.id, user_id=self.from_user.id, timeout=timeout, api_kwargs=api_kwargs
|
||||
return await self.get_bot().decline_chat_join_request(
|
||||
chat_id=self.chat.id,
|
||||
user_id=self.from_user.id,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
|
|
@ -38,7 +38,7 @@ class ChatMemberUpdated(TelegramObject):
|
|||
.. versionadded:: 13.4
|
||||
|
||||
Note:
|
||||
In Python :keyword:`from` is a reserved word, :paramref:`from_user`
|
||||
In Python :keyword:`from` is a reserved word use :paramref:`from_user` instead.
|
||||
|
||||
Args:
|
||||
chat (:class:`telegram.Chat`): Chat the user belongs to.
|
||||
|
@ -136,7 +136,7 @@ class ChatMemberUpdated(TelegramObject):
|
|||
"""Computes the difference between :attr:`old_chat_member` and :attr:`new_chat_member`.
|
||||
|
||||
Example:
|
||||
.. code:: python
|
||||
.. code:: pycon
|
||||
|
||||
>>> chat_member_updated.difference()
|
||||
{'custom_title': ('old title', 'new title')}
|
||||
|
|
|
@ -37,7 +37,7 @@ class ChosenInlineResult(TelegramObject):
|
|||
considered equal, if their :attr:`result_id` is equal.
|
||||
|
||||
Note:
|
||||
* In Python :keyword:`from` is a reserved word, :paramref:`from_user`
|
||||
* In Python :keyword:`from` is a reserved word use :paramref:`from_user` instead.
|
||||
* It is necessary to enable inline feedback via `@Botfather <https://t.me/BotFather>`_ in
|
||||
order to receive these objects in updates.
|
||||
|
||||
|
|
|
@ -65,8 +65,13 @@ class _BaseMedium(TelegramObject):
|
|||
|
||||
self._id_attrs = (self.file_unique_id,)
|
||||
|
||||
def get_file(
|
||||
self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
|
||||
async def get_file(
|
||||
self,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> 'File':
|
||||
"""Convenience wrapper over :attr:`telegram.Bot.get_file`
|
||||
|
||||
|
@ -79,6 +84,11 @@ class _BaseMedium(TelegramObject):
|
|||
:class:`telegram.error.TelegramError`
|
||||
|
||||
"""
|
||||
return self.get_bot().get_file(
|
||||
file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs
|
||||
return await self.get_bot().get_file(
|
||||
file_id=self.file_id,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
|
|
@ -30,8 +30,8 @@ ThumbedMT = TypeVar('ThumbedMT', bound='_BaseThumbedMedium', covariant=True)
|
|||
|
||||
|
||||
class _BaseThumbedMedium(_BaseMedium):
|
||||
"""Base class for objects representing the various media file types that may include a
|
||||
thumbnail.
|
||||
"""
|
||||
Base class for objects representing the various media file types that may include a thumbnail.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`file_unique_id` is equal.
|
||||
|
|
|
@ -93,8 +93,13 @@ class ChatPhoto(TelegramObject):
|
|||
self.big_file_unique_id,
|
||||
)
|
||||
|
||||
def get_small_file(
|
||||
self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
|
||||
async def get_small_file(
|
||||
self,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> 'File':
|
||||
"""Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the
|
||||
small (160x160) chat photo
|
||||
|
@ -108,12 +113,22 @@ class ChatPhoto(TelegramObject):
|
|||
:class:`telegram.error.TelegramError`
|
||||
|
||||
"""
|
||||
return self.get_bot().get_file(
|
||||
file_id=self.small_file_id, timeout=timeout, api_kwargs=api_kwargs
|
||||
return await self.get_bot().get_file(
|
||||
file_id=self.small_file_id,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
def get_big_file(
|
||||
self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
|
||||
async def get_big_file(
|
||||
self,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> 'File':
|
||||
"""Convenience wrapper over :attr:`telegram.Bot.get_file` for getting the
|
||||
big (640x640) chat photo
|
||||
|
@ -127,6 +142,11 @@ class ChatPhoto(TelegramObject):
|
|||
:class:`telegram.error.TelegramError`
|
||||
|
||||
"""
|
||||
return self.get_bot().get_file(
|
||||
file_id=self.big_file_id, timeout=timeout, api_kwargs=api_kwargs
|
||||
return await self.get_bot().get_file(
|
||||
file_id=self.big_file_id,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
|
|
@ -25,8 +25,9 @@ from typing import IO, TYPE_CHECKING, Any, Optional, Union
|
|||
|
||||
from telegram import TelegramObject
|
||||
from telegram._passport.credentials import decrypt
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.files import is_local_file
|
||||
from telegram._utils.types import FilePathInput
|
||||
from telegram._utils.types import FilePathInput, ODVInput
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Bot, FileCredentials
|
||||
|
@ -45,7 +46,7 @@ class File(TelegramObject):
|
|||
* Maximum file size to download is
|
||||
:tg-const:`telegram.constants.FileSizeLimit.FILESIZE_DOWNLOAD`.
|
||||
* If you obtain an instance of this class from :attr:`telegram.PassportFile.get_file`,
|
||||
then it will automatically be decrypted as it downloads when you call :attr:`download()`.
|
||||
then it will automatically be decrypted as it downloads when you call :meth:`download()`.
|
||||
|
||||
Args:
|
||||
file_id (:obj:`str`): Identifier for this file, which can be used to download
|
||||
|
@ -64,7 +65,7 @@ class File(TelegramObject):
|
|||
is supposed to be the same over time and for different bots.
|
||||
Can't be used to download or reuse the file.
|
||||
file_size (:obj:`str`): Optional. File size in bytes.
|
||||
file_path (:obj:`str`): Optional. File path. Use :attr:`download` to get the file.
|
||||
file_path (:obj:`str`): Optional. File path. Use :meth:`download` to get the file.
|
||||
|
||||
"""
|
||||
|
||||
|
@ -96,8 +97,14 @@ class File(TelegramObject):
|
|||
|
||||
self._id_attrs = (self.file_unique_id,)
|
||||
|
||||
def download(
|
||||
self, custom_path: FilePathInput = None, out: IO = None, timeout: int = None
|
||||
async def download(
|
||||
self,
|
||||
custom_path: FilePathInput = None,
|
||||
out: IO = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
) -> Union[Path, IO]:
|
||||
"""
|
||||
Download this file. By default, the file is saved in the current working directory with its
|
||||
|
@ -122,9 +129,18 @@ class File(TelegramObject):
|
|||
custom_path (:class:`pathlib.Path` | :obj:`str`, optional): Custom path.
|
||||
out (:obj:`io.BufferedWriter`, optional): A file-like object. Must be opened for
|
||||
writing in binary mode, if applicable.
|
||||
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).
|
||||
read_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.request.BaseRequest.post.read_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.request.BaseRequest.post.write_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.request.BaseRequest.post.connect_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.request.BaseRequest.post.pool_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
|
||||
Returns:
|
||||
:class:`pathlib.Path` | :obj:`io.BufferedWriter`: The same object as :paramref:`out` if
|
||||
|
@ -146,7 +162,7 @@ class File(TelegramObject):
|
|||
if local_file:
|
||||
buf = path.read_bytes()
|
||||
else:
|
||||
buf = self.get_bot().request.retrieve(url)
|
||||
buf = await self.get_bot().request.retrieve(url)
|
||||
if self._credentials:
|
||||
buf = decrypt(
|
||||
b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf
|
||||
|
@ -167,7 +183,13 @@ class File(TelegramObject):
|
|||
else:
|
||||
filename = Path.cwd() / self.file_id
|
||||
|
||||
buf = self.get_bot().request.retrieve(url, timeout=timeout)
|
||||
buf = await self.get_bot().request.retrieve(
|
||||
url,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
)
|
||||
if self._credentials:
|
||||
buf = decrypt(
|
||||
b64decode(self._credentials.secret), b64decode(self._credentials.hash), buf
|
||||
|
@ -184,7 +206,7 @@ class File(TelegramObject):
|
|||
)
|
||||
)
|
||||
|
||||
def download_as_bytearray(self, buf: bytearray = None) -> bytearray:
|
||||
async def download_as_bytearray(self, buf: bytearray = None) -> bytearray:
|
||||
"""Download this file and return it as a bytearray.
|
||||
|
||||
Args:
|
||||
|
@ -200,7 +222,7 @@ class File(TelegramObject):
|
|||
if is_local_file(self.file_path):
|
||||
buf.extend(Path(self.file_path).read_bytes())
|
||||
else:
|
||||
buf.extend(self.get_bot().request.retrieve(self._get_encoded_url()))
|
||||
buf.extend(await self.get_bot().request.retrieve(self._get_encoded_url()))
|
||||
return buf
|
||||
|
||||
def set_credentials(self, credentials: 'FileCredentials') -> None:
|
||||
|
|
|
@ -23,42 +23,58 @@ import imghdr
|
|||
import logging
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
from typing import IO, Optional, Tuple, Union
|
||||
from typing import IO, Optional, Union
|
||||
from uuid import uuid4
|
||||
|
||||
DEFAULT_MIME_TYPE = 'application/octet-stream'
|
||||
from telegram._utils.types import FieldTuple
|
||||
|
||||
_DEFAULT_MIME_TYPE = 'application/octet-stream'
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InputFile:
|
||||
"""This object represents a Telegram InputFile.
|
||||
|
||||
Args:
|
||||
obj (:term:`file object` | :obj:`bytes`): An open file descriptor or the files content as
|
||||
bytes.
|
||||
filename (:obj:`str`, optional): Filename for this InputFile.
|
||||
attach (:obj:`bool`, optional): Whether this should be send as one file or is part of a
|
||||
collection of files.
|
||||
.. versionchanged:: 14.0
|
||||
The former attribute ``attach`` was renamed to :attr:`attach_name`.
|
||||
|
||||
Raises:
|
||||
TelegramError
|
||||
Args:
|
||||
obj (:term:`file object` | :obj:`bytes` | :obj:`str`): An open file descriptor or the files
|
||||
content as bytes or string.
|
||||
|
||||
Note:
|
||||
If :paramref:`obj` is a string, it will be encoded as bytes via
|
||||
:external:obj:`obj.encode('utf-8') <str.encode>`.
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
Accept string input.
|
||||
filename (:obj:`str`, optional): Filename for this InputFile.
|
||||
attach (:obj:`bool`, optional): Pass :obj:`True` if the parameter this file belongs to in
|
||||
the request to Telegram should point to the multipart data via an ``attach://`` URI.
|
||||
Defaults to `False`.
|
||||
|
||||
Attributes:
|
||||
input_file_content (:obj:`bytes`): The binary content of the file to send.
|
||||
filename (:obj:`str`): Optional. Filename for the file to be sent.
|
||||
attach (:obj:`str`): Optional. Attach id for sending multiple files.
|
||||
mimetype (:obj:`str`): Optional. The mimetype inferred from the file to be sent.
|
||||
attach_name (:obj:`str`): Optional. If present, the parameter this file belongs to in
|
||||
the request to Telegram should point to the multipart data via a an URI of the form
|
||||
``attach://<attach_name>`` URI.
|
||||
filename (:obj:`str`): Filename for the file to be sent.
|
||||
mimetype (:obj:`str`): The mimetype inferred from the file to be sent.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ('filename', 'attach', 'input_file_content', 'mimetype')
|
||||
__slots__ = ('filename', 'attach_name', 'input_file_content', 'mimetype')
|
||||
|
||||
def __init__(self, obj: Union[IO, bytes], filename: str = None, attach: bool = None):
|
||||
def __init__(
|
||||
self, obj: Union[IO[bytes], bytes, str], filename: str = None, attach: bool = False
|
||||
):
|
||||
if isinstance(obj, bytes):
|
||||
self.input_file_content = obj
|
||||
elif isinstance(obj, str):
|
||||
self.input_file_content = obj.encode('utf-8')
|
||||
else:
|
||||
self.input_file_content = obj.read()
|
||||
self.attach = 'attached' + uuid4().hex if attach else None
|
||||
self.attach_name: Optional[str] = 'attached' + uuid4().hex if attach else None
|
||||
|
||||
if (
|
||||
not filename
|
||||
|
@ -71,16 +87,12 @@ class InputFile:
|
|||
if image_mime_type:
|
||||
self.mimetype = image_mime_type
|
||||
elif filename:
|
||||
self.mimetype = mimetypes.guess_type(filename)[0] or DEFAULT_MIME_TYPE
|
||||
self.mimetype = mimetypes.guess_type(filename)[0] or _DEFAULT_MIME_TYPE
|
||||
else:
|
||||
self.mimetype = DEFAULT_MIME_TYPE
|
||||
self.mimetype = _DEFAULT_MIME_TYPE
|
||||
|
||||
self.filename = filename or self.mimetype.replace('/', '.')
|
||||
|
||||
@property
|
||||
def field_tuple(self) -> Tuple[str, bytes, str]: # skipcq: PY-D0003
|
||||
return self.filename, self.input_file_content, self.mimetype
|
||||
|
||||
@staticmethod
|
||||
def is_image(stream: bytes) -> Optional[str]:
|
||||
"""Check if the content file is an image by analyzing its headers.
|
||||
|
@ -104,12 +116,18 @@ class InputFile:
|
|||
)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def is_file(obj: object) -> bool: # skipcq: PY-D0003
|
||||
return hasattr(obj, 'read')
|
||||
@property
|
||||
def field_tuple(self) -> FieldTuple:
|
||||
"""Field tuple representing the contents of the file for upload to the Telegram servers.
|
||||
|
||||
def to_dict(self) -> Optional[str]:
|
||||
"""See :meth:`telegram.TelegramObject.to_dict`."""
|
||||
if self.attach:
|
||||
return 'attach://' + self.attach
|
||||
return None
|
||||
Returns:
|
||||
Tuple[:obj:`str`, :obj:`bytes`, :obj:`str`]:
|
||||
"""
|
||||
return self.filename, self.input_file_content, self.mimetype
|
||||
|
||||
@property
|
||||
def attach_uri(self) -> Optional[str]:
|
||||
"""URI to insert into the JSON data for uploading the file. Returns :obj:`None`, if
|
||||
:attr:`attach_name` is :obj:`None`.
|
||||
"""
|
||||
return f'attach://{self.attach_name}' if self.attach_name else None
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""Base class for Telegram InputMedia Objects."""
|
||||
|
||||
from typing import Union, List, Tuple, Optional
|
||||
|
||||
from telegram import (
|
||||
|
@ -47,7 +46,7 @@ class InputMedia(TelegramObject):
|
|||
:attr:`caption_entities`, :paramref:`parse_mode`.
|
||||
|
||||
Args:
|
||||
media_type (:obj:`str`) Type of media that the instance represents.
|
||||
media_type (:obj:`str`): Type of media that the instance represents.
|
||||
media (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | \
|
||||
:class:`telegram.Animation` | :class:`telegram.Audio` | \
|
||||
:class:`telegram.Document` | :class:`telegram.PhotoSize` | \
|
||||
|
@ -56,10 +55,12 @@ class InputMedia(TelegramObject):
|
|||
(recommended), pass an HTTP URL for Telegram to get a file from the Internet.
|
||||
Lastly you can pass an existing telegram media object of the corresponding type
|
||||
to send.
|
||||
caption (:obj:`str`, optional): Caption of the media to be sent, 0-1024 characters
|
||||
after entities parsing.
|
||||
caption (:obj:`str`, optional): Caption of the media to be sent,
|
||||
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities
|
||||
parsing.
|
||||
caption_entities (List[:class:`telegram.MessageEntity`], optional): List of special
|
||||
entities that appear in the caption, which can be specified instead of parse_mode.
|
||||
entities that appear in the caption, which can be specified instead of
|
||||
:paramref:`parse_mode`.
|
||||
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.constants.ParseMode` for the available modes.
|
||||
|
@ -109,7 +110,7 @@ class InputMediaAnimation(InputMedia):
|
|||
"""Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent.
|
||||
|
||||
Note:
|
||||
When using a :class:`telegram.Animation` for the :attr:`media` attribute. It will take the
|
||||
When using a :class:`telegram.Animation` for the :attr:`media` attribute, it will take the
|
||||
width, height and duration from that video, unless otherwise specified with the optional
|
||||
arguments.
|
||||
|
||||
|
@ -130,8 +131,8 @@ class InputMediaAnimation(InputMedia):
|
|||
thumb (:term:`file object` | :obj:`bytes` | :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.
|
||||
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.
|
||||
|
||||
.. versionchanged:: 13.2
|
||||
|
@ -143,7 +144,8 @@ class InputMediaAnimation(InputMedia):
|
|||
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
|
||||
in :class:`telegram.constants.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.
|
||||
entities that appear in the caption, which can be specified instead of
|
||||
:paramref:`parse_mode`.
|
||||
width (:obj:`int`, optional): Animation width.
|
||||
height (:obj:`int`, optional): Animation height.
|
||||
duration (:obj:`int`, optional): Animation duration in seconds.
|
||||
|
@ -182,7 +184,7 @@ class InputMediaAnimation(InputMedia):
|
|||
duration = media.duration if duration is None else duration
|
||||
media = media.file_id
|
||||
else:
|
||||
media = parse_file_input(media, attach=True, filename=filename)
|
||||
media = parse_file_input(media, filename=filename, attach=True)
|
||||
|
||||
super().__init__(InputMediaType.ANIMATION, media, caption, caption_entities, parse_mode)
|
||||
self.thumb = self._parse_thumb_input(thumb)
|
||||
|
@ -215,7 +217,8 @@ class InputMediaPhoto(InputMedia):
|
|||
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
|
||||
in :class:`telegram.constants.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.
|
||||
entities that appear in the caption, which can be specified instead of
|
||||
:paramref:`parse_mode`.
|
||||
|
||||
Attributes:
|
||||
type (:obj:`str`): :tg-const:`telegram.constants.InputMediaType.PHOTO`.
|
||||
|
@ -237,7 +240,7 @@ class InputMediaPhoto(InputMedia):
|
|||
caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
|
||||
filename: str = None,
|
||||
):
|
||||
media = parse_file_input(media, PhotoSize, attach=True, filename=filename)
|
||||
media = parse_file_input(media, PhotoSize, filename=filename, attach=True)
|
||||
super().__init__(InputMediaType.PHOTO, media, caption, caption_entities, parse_mode)
|
||||
|
||||
|
||||
|
@ -245,10 +248,10 @@ class InputMediaVideo(InputMedia):
|
|||
"""Represents a video to be sent.
|
||||
|
||||
Note:
|
||||
* When using a :class:`telegram.Video` for the :attr:`media` attribute. It will take the
|
||||
* When using a :class:`telegram.Video` for the :attr:`media` attribute, it will take the
|
||||
width, height and duration from that video, unless otherwise specified with the optional
|
||||
arguments.
|
||||
* ``thumb`` will be ignored for small video files, for which Telegram can easily
|
||||
* :paramref:`thumb` will be ignored for small video files, for which Telegram can easily
|
||||
generate thumbnails. However, this behaviour is undocumented and might be changed
|
||||
by Telegram.
|
||||
|
||||
|
@ -273,7 +276,8 @@ class InputMediaVideo(InputMedia):
|
|||
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
|
||||
in :class:`telegram.constants.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.
|
||||
entities that appear in the caption, which can be specified instead of
|
||||
:paramref:`parse_mode`.
|
||||
width (:obj:`int`, optional): Video width.
|
||||
height (:obj:`int`, optional): Video height.
|
||||
duration (:obj:`int`, optional): Video duration in seconds.
|
||||
|
@ -282,8 +286,8 @@ class InputMediaVideo(InputMedia):
|
|||
thumb (:term:`file object` | :obj:`bytes` | :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.
|
||||
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.
|
||||
|
||||
.. versionchanged:: 13.2
|
||||
|
@ -327,7 +331,7 @@ class InputMediaVideo(InputMedia):
|
|||
duration = duration if duration is not None else media.duration
|
||||
media = media.file_id
|
||||
else:
|
||||
media = parse_file_input(media, attach=True, filename=filename)
|
||||
media = parse_file_input(media, filename=filename, attach=True)
|
||||
|
||||
super().__init__(InputMediaType.VIDEO, media, caption, caption_entities, parse_mode)
|
||||
self.width = width
|
||||
|
@ -341,7 +345,7 @@ class InputMediaAudio(InputMedia):
|
|||
"""Represents an audio file to be treated as music to be sent.
|
||||
|
||||
Note:
|
||||
When using a :class:`telegram.Audio` for the :attr:`media` attribute. It will take the
|
||||
When using a :class:`telegram.Audio` for the :attr:`media` attribute, it will take the
|
||||
duration, performer and title from that video, unless otherwise specified with the
|
||||
optional arguments.
|
||||
|
||||
|
@ -367,7 +371,8 @@ class InputMediaAudio(InputMedia):
|
|||
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
|
||||
in :class:`telegram.constants.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.
|
||||
entities that appear in the caption, which can be specified instead of
|
||||
:paramref:`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.
|
||||
|
@ -375,8 +380,8 @@ class InputMediaAudio(InputMedia):
|
|||
thumb (:term:`file object` | :obj:`bytes` | :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.
|
||||
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.
|
||||
|
||||
.. versionchanged:: 13.2
|
||||
|
@ -417,7 +422,7 @@ class InputMediaAudio(InputMedia):
|
|||
title = media.title if title is None else title
|
||||
media = media.file_id
|
||||
else:
|
||||
media = parse_file_input(media, attach=True, filename=filename)
|
||||
media = parse_file_input(media, filename=filename, attach=True)
|
||||
|
||||
super().__init__(InputMediaType.AUDIO, media, caption, caption_entities, parse_mode)
|
||||
self.thumb = self._parse_thumb_input(thumb)
|
||||
|
@ -450,19 +455,20 @@ class InputMediaDocument(InputMedia):
|
|||
bold, italic, fixed-width text or inline URLs in the media caption. See the constants
|
||||
in :class:`telegram.constants.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.
|
||||
entities that appear in the caption, which can be specified instead of
|
||||
:paramref:`parse_mode`.
|
||||
thumb (:term:`file object` | :obj:`bytes` | :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.
|
||||
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.
|
||||
|
||||
.. versionchanged:: 13.2
|
||||
Accept :obj:`bytes` as input.
|
||||
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.
|
||||
content type detection for files uploaded using multipart/form-data. Always
|
||||
:obj:`True`, if the document is sent as part of an album.
|
||||
|
||||
Attributes:
|
||||
type (:obj:`str`): :tg-const:`telegram.constants.InputMediaType.DOCUMENT`.
|
||||
|
@ -490,7 +496,7 @@ class InputMediaDocument(InputMedia):
|
|||
caption_entities: Union[List[MessageEntity], Tuple[MessageEntity, ...]] = None,
|
||||
filename: str = None,
|
||||
):
|
||||
media = parse_file_input(media, Document, attach=True, filename=filename)
|
||||
media = parse_file_input(media, Document, filename=filename, attach=True)
|
||||
super().__init__(InputMediaType.DOCUMENT, media, caption, caption_entities, parse_mode)
|
||||
self.thumb = self._parse_thumb_input(thumb)
|
||||
self.disable_content_type_detection = disable_content_type_detection
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains objects that represents stickers."""
|
||||
"""This module contains objects that represent stickers."""
|
||||
|
||||
from typing import TYPE_CHECKING, Any, List, Optional, ClassVar
|
||||
|
||||
|
@ -35,7 +35,7 @@ class Sticker(_BaseThumbedMedium):
|
|||
considered equal, if their :attr:`file_unique_id` is equal.
|
||||
|
||||
Note:
|
||||
As of v13.11 ``is_video`` is a required argument and therefore the order of the
|
||||
As of v13.11 :paramref:`is_video` is a required argument and therefore the order of the
|
||||
arguments had to be changed. Use keyword arguments to make sure that the arguments are
|
||||
passed correctly.
|
||||
|
||||
|
@ -51,8 +51,8 @@ class Sticker(_BaseThumbedMedium):
|
|||
is_video (:obj:`bool`): :obj:`True`, if the sticker is a video sticker.
|
||||
|
||||
.. versionadded:: 13.11
|
||||
thumb (:class:`telegram.PhotoSize`, optional): Sticker thumbnail in the .WEBP or .JPG
|
||||
format.
|
||||
thumb (:class:`telegram.PhotoSize`, optional): Sticker thumbnail in the ``.WEBP`` or
|
||||
``.JPG`` format.
|
||||
emoji (:obj:`str`, optional): Emoji associated with the sticker
|
||||
set_name (:obj:`str`, optional): Name of the sticker set to which the sticker
|
||||
belongs.
|
||||
|
@ -73,8 +73,8 @@ class Sticker(_BaseThumbedMedium):
|
|||
is_video (:obj:`bool`): :obj:`True`, if the sticker is a video sticker.
|
||||
|
||||
.. versionadded:: 13.11
|
||||
thumb (:class:`telegram.PhotoSize`): Optional. Sticker thumbnail in the .webp or .jpg
|
||||
format.
|
||||
thumb (:class:`telegram.PhotoSize`): Optional. Sticker thumbnail in the ``.WEBP`` or
|
||||
``.JPG`` format.
|
||||
emoji (:obj:`str`): Optional. Emoji associated with the sticker.
|
||||
set_name (:obj:`str`): Optional. Name of the sticker set to which the sticker belongs.
|
||||
mask_position (:class:`telegram.MaskPosition`): Optional. For mask stickers, the position
|
||||
|
@ -148,7 +148,7 @@ class StickerSet(TelegramObject):
|
|||
considered equal, if their :attr:`name` is equal.
|
||||
|
||||
Note:
|
||||
As of v13.11 ``is_video`` is a required argument and therefore the order of the
|
||||
As of v13.11 :paramref:`is_video` is a required argument and therefore the order of the
|
||||
arguments had to be changed. Use keyword arguments to make sure that the arguments are
|
||||
passed correctly.
|
||||
|
||||
|
@ -241,12 +241,12 @@ class MaskPosition(TelegramObject):
|
|||
point (:obj:`str`): The part of the face relative to which the mask should be placed.
|
||||
One of :attr:`FOREHEAD`, :attr:`EYES`, :attr:`MOUTH`, or :attr:`CHIN`.
|
||||
x_shift (:obj:`float`): Shift by X-axis measured in widths of the mask scaled to the face
|
||||
size, from left to right. For example, choosing -1.0 will place mask just to the left
|
||||
of the default mask position.
|
||||
size, from left to right. For example, choosing ``-1.0`` will place mask just to the
|
||||
left of the default mask position.
|
||||
y_shift (:obj:`float`): Shift by Y-axis measured in heights of the mask scaled to the face
|
||||
size, from top to bottom. For example, 1.0 will place the mask just below the default
|
||||
mask position.
|
||||
scale (:obj:`float`): Mask scaling coefficient. For example, 2.0 means double size.
|
||||
size, from top to bottom. For example, ``1.0`` will place the mask just below the
|
||||
default mask position.
|
||||
scale (:obj:`float`): Mask scaling coefficient. For example, ``2.0`` means double size.
|
||||
|
||||
Attributes:
|
||||
point (:obj:`str`): The part of the face relative to which the mask should be placed.
|
||||
|
@ -255,7 +255,7 @@ class MaskPosition(TelegramObject):
|
|||
size, from left to right.
|
||||
y_shift (:obj:`float`): Shift by Y-axis measured in heights of the mask scaled to the face
|
||||
size, from top to bottom.
|
||||
scale (:obj:`float`): Mask scaling coefficient. For example, 2.0 means double size.
|
||||
scale (:obj:`float`): Mask scaling coefficient. For example, ``2.0`` means double size.
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ class Video(_BaseThumbedMedium):
|
|||
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.
|
||||
mime_type (:obj:`str`, optional): MIME type of a file as defined by sender.
|
||||
file_size (:obj:`int`, optional): File size in bytes.
|
||||
bot (:class:`telegram.Bot`, optional): The Bot to use for instance methods.
|
||||
**kwargs (:obj:`dict`): Arbitrary keyword arguments.
|
||||
|
@ -59,7 +59,7 @@ class Video(_BaseThumbedMedium):
|
|||
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.
|
||||
mime_type (:obj:`str`): Optional. MIME type of a file as defined by sender.
|
||||
file_size (:obj:`int`): Optional. File size in bytes.
|
||||
bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods.
|
||||
|
||||
|
|
|
@ -154,8 +154,9 @@ class Game(TelegramObject):
|
|||
def parse_text_entities(self, types: List[str] = None) -> Dict[MessageEntity, str]:
|
||||
"""
|
||||
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
|
||||
It contains entities from this message filtered by their ``type`` attribute as the key, and
|
||||
the text that each entity belongs to as the value of the :obj:`dict`.
|
||||
It contains entities from this message filtered by their
|
||||
:attr:`~telegram.MessageEntity.type` attribute as the key, and the text that each entity
|
||||
belongs to as the value of the :obj:`dict`.
|
||||
|
||||
Note:
|
||||
This method should always be used instead of the :attr:`text_entities` attribute, since
|
||||
|
@ -163,9 +164,10 @@ class Game(TelegramObject):
|
|||
See :attr:`parse_text_entity` for more info.
|
||||
|
||||
Args:
|
||||
types (List[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the
|
||||
``type`` attribute of an entity is contained in this list, it will be returned.
|
||||
Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`.
|
||||
types (List[:obj:`str`], optional): List of :class:`telegram.MessageEntity` types as
|
||||
strings. If the :attr:`~telegram.MessageEntity.type` attribute of an entity is
|
||||
contained in this list, it will be returned. Defaults to
|
||||
:attr:`telegram.MessageEntity.ALL_TYPES`.
|
||||
|
||||
Returns:
|
||||
Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
|
||||
|
|
|
@ -38,7 +38,7 @@ class InlineQuery(TelegramObject):
|
|||
considered equal, if their :attr:`id` is equal.
|
||||
|
||||
Note:
|
||||
In Python :keyword:`from` is a reserved word, :paramref:`from_user`
|
||||
In Python :keyword:`from` is a reserved word use :paramref:`from_user` instead.
|
||||
|
||||
Args:
|
||||
id (:obj:`str`): Unique identifier for this query.
|
||||
|
@ -110,7 +110,7 @@ class InlineQuery(TelegramObject):
|
|||
|
||||
return cls(bot=bot, **data)
|
||||
|
||||
def answer(
|
||||
async def answer(
|
||||
self,
|
||||
results: Union[
|
||||
Sequence['InlineQueryResult'], Callable[[int], Optional[Sequence['InlineQueryResult']]]
|
||||
|
@ -120,14 +120,17 @@ class InlineQuery(TelegramObject):
|
|||
next_offset: str = None,
|
||||
switch_pm_text: str = None,
|
||||
switch_pm_parameter: str = None,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
current_offset: str = None,
|
||||
api_kwargs: JSONDict = None,
|
||||
auto_pagination: bool = False,
|
||||
) -> bool:
|
||||
"""Shortcut for::
|
||||
|
||||
bot.answer_inline_query(
|
||||
await bot.answer_inline_query(
|
||||
update.inline_query.id,
|
||||
*args,
|
||||
current_offset=self.offset if auto_pagination else None,
|
||||
|
@ -146,13 +149,12 @@ class InlineQuery(TelegramObject):
|
|||
Defaults to :obj:`False`.
|
||||
|
||||
Raises:
|
||||
ValueError: If both
|
||||
:paramref:`~telegram.Bot.answer_inline_query.current_offset` and
|
||||
ValueError: If both :paramref:`~telegram.Bot.answer_inline_query.current_offset` and
|
||||
:paramref:`auto_pagination` are supplied.
|
||||
"""
|
||||
if current_offset and auto_pagination:
|
||||
raise ValueError('current_offset and auto_pagination are mutually exclusive!')
|
||||
return self.get_bot().answer_inline_query(
|
||||
return await self.get_bot().answer_inline_query(
|
||||
inline_query_id=self.id,
|
||||
current_offset=self.offset if auto_pagination else current_offset,
|
||||
results=results,
|
||||
|
@ -161,7 +163,10 @@ class InlineQuery(TelegramObject):
|
|||
next_offset=next_offset,
|
||||
switch_pm_text=switch_pm_text,
|
||||
switch_pm_parameter=switch_pm_parameter,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -136,8 +136,13 @@ class PassportFile(TelegramObject):
|
|||
for i, passport_file in enumerate(data)
|
||||
]
|
||||
|
||||
def get_file(
|
||||
self, timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: JSONDict = None
|
||||
async def get_file(
|
||||
self,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> 'File':
|
||||
"""
|
||||
Wrapper over :attr:`telegram.Bot.get_file`. Will automatically assign the correct
|
||||
|
@ -153,8 +158,13 @@ class PassportFile(TelegramObject):
|
|||
:class:`telegram.error.TelegramError`
|
||||
|
||||
"""
|
||||
file = self.get_bot().get_file(
|
||||
file_id=self.file_id, timeout=timeout, api_kwargs=api_kwargs
|
||||
file = await self.get_bot().get_file(
|
||||
file_id=self.file_id,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
file.set_credentials(self._credentials)
|
||||
return file
|
||||
|
|
|
@ -35,7 +35,7 @@ class PreCheckoutQuery(TelegramObject):
|
|||
considered equal, if their :attr:`id` is equal.
|
||||
|
||||
Note:
|
||||
In Python :keyword:`from` is a reserved word, :paramref:`from_user`
|
||||
In Python :keyword:`from` is a reserved word use :paramref:`from_user` instead.
|
||||
|
||||
Args:
|
||||
id (:obj:`str`): Unique query identifier.
|
||||
|
@ -114,25 +114,31 @@ class PreCheckoutQuery(TelegramObject):
|
|||
|
||||
return cls(bot=bot, **data)
|
||||
|
||||
def answer( # pylint: disable=invalid-name
|
||||
async def answer( # pylint: disable=invalid-name
|
||||
self,
|
||||
ok: bool,
|
||||
error_message: str = None,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> bool:
|
||||
"""Shortcut for::
|
||||
|
||||
bot.answer_pre_checkout_query(update.pre_checkout_query.id, *args, **kwargs)
|
||||
await bot.answer_pre_checkout_query(update.pre_checkout_query.id, *args, **kwargs)
|
||||
|
||||
For the documentation of the arguments, please see
|
||||
:meth:`telegram.Bot.answer_pre_checkout_query`.
|
||||
|
||||
"""
|
||||
return self.get_bot().answer_pre_checkout_query(
|
||||
return await self.get_bot().answer_pre_checkout_query(
|
||||
pre_checkout_query_id=self.id,
|
||||
ok=ok,
|
||||
error_message=error_message,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
|
|
@ -35,7 +35,7 @@ class ShippingQuery(TelegramObject):
|
|||
considered equal, if their :attr:`id` is equal.
|
||||
|
||||
Note:
|
||||
In Python :keyword:`from` is a reserved word, :paramref:`from_user`
|
||||
In Python :keyword:`from` is a reserved word use :paramref:`from_user` instead.
|
||||
|
||||
Args:
|
||||
id (:obj:`str`): Unique query identifier.
|
||||
|
@ -87,27 +87,33 @@ class ShippingQuery(TelegramObject):
|
|||
|
||||
return cls(bot=bot, **data)
|
||||
|
||||
def answer( # pylint: disable=invalid-name
|
||||
async def answer( # pylint: disable=invalid-name
|
||||
self,
|
||||
ok: bool,
|
||||
shipping_options: List[ShippingOption] = None,
|
||||
error_message: str = None,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> bool:
|
||||
"""Shortcut for::
|
||||
|
||||
bot.answer_shipping_query(update.shipping_query.id, *args, **kwargs)
|
||||
await bot.answer_shipping_query(update.shipping_query.id, *args, **kwargs)
|
||||
|
||||
For the documentation of the arguments, please see
|
||||
:meth:`telegram.Bot.answer_shipping_query`.
|
||||
|
||||
"""
|
||||
return self.get_bot().answer_shipping_query(
|
||||
return await self.get_bot().answer_shipping_query(
|
||||
shipping_query_id=self.id,
|
||||
ok=ok,
|
||||
shipping_options=shipping_options,
|
||||
error_message=error_message,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
|
|
@ -29,7 +29,7 @@ class ReplyKeyboardMarkup(TelegramObject):
|
|||
"""This object represents a custom keyboard with reply options.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their the size of :attr:`keyboard` and all the buttons are equal.
|
||||
considered equal, if their size of :attr:`keyboard` and all the buttons are equal.
|
||||
|
||||
Example:
|
||||
A user requests to change the bot's language, bot replies to the request with a keyboard
|
||||
|
@ -37,7 +37,7 @@ class ReplyKeyboardMarkup(TelegramObject):
|
|||
|
||||
Args:
|
||||
keyboard (List[List[:obj:`str` | :class:`telegram.KeyboardButton`]]): Array of button rows,
|
||||
each represented by an Array of :class:`telegram.KeyboardButton` objects.
|
||||
each represented by an Array of :class:`telegram.KeyboardButton` objects.
|
||||
resize_keyboard (:obj:`bool`, optional): Requests clients to resize the keyboard vertically
|
||||
for optimal fit (e.g., make the keyboard smaller if there are just two rows of
|
||||
buttons). Defaults to :obj:`False`, in which case the custom keyboard is always of the
|
||||
|
|
|
@ -85,10 +85,11 @@ class Update(TelegramObject):
|
|||
.. versionadded:: 13.4
|
||||
chat_member (:class:`telegram.ChatMemberUpdated`, optional): A chat member's status was
|
||||
updated in a chat. The bot must be an administrator in the chat and must explicitly
|
||||
specify ``'chat_member'`` in the list of ``'allowed_updates'`` to receive these
|
||||
specify :attr:`CHAT_MEMBER` in the list of
|
||||
:paramref:`telegram.ext.Application.run_polling.allowed_updates` to receive these
|
||||
updates (see :meth:`telegram.Bot.get_updates`, :meth:`telegram.Bot.set_webhook`,
|
||||
:meth:`telegram.ext.Updater.start_polling` and
|
||||
:meth:`telegram.ext.Updater.start_webhook`).
|
||||
:meth:`telegram.ext.Application.run_polling` and
|
||||
:meth:`telegram.ext.Application.run_webhook`).
|
||||
|
||||
.. versionadded:: 13.4
|
||||
chat_join_request (:class:`telegram.ChatJoinRequest`, optional): A request to join the
|
||||
|
@ -124,15 +125,17 @@ class Update(TelegramObject):
|
|||
.. versionadded:: 13.4
|
||||
chat_member (:class:`telegram.ChatMemberUpdated`): Optional. A chat member's status was
|
||||
updated in a chat. The bot must be an administrator in the chat and must explicitly
|
||||
specify ``'chat_member'`` in the list of ``'allowed_updates'`` to receive these
|
||||
specify :attr:`CHAT_MEMBER` in the list of
|
||||
:paramref:`telegram.ext.Application.run_polling.allowed_updates` to receive these
|
||||
updates (see :meth:`telegram.Bot.get_updates`, :meth:`telegram.Bot.set_webhook`,
|
||||
:meth:`telegram.ext.Updater.start_polling` and
|
||||
:meth:`telegram.ext.Updater.start_webhook`).
|
||||
:meth:`telegram.ext.Application.run_polling` and
|
||||
:meth:`telegram.ext.Application.run_webhook`).
|
||||
|
||||
.. versionadded:: 13.4
|
||||
chat_join_request (:class:`telegram.ChatJoinRequest`): Optional. A request to join the
|
||||
chat has been sent. The bot must have the ``'can_invite_users'`` administrator
|
||||
right in the chat to receive these updates.
|
||||
chat has been sent. The bot must have the
|
||||
:attr:`telegram.ChatPermissions.can_invite_users` administrator right in the chat to
|
||||
receive these updates.
|
||||
|
||||
.. versionadded:: 13.8
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -28,7 +28,7 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class UserProfilePhotos(TelegramObject):
|
||||
"""This object represent a user's profile pictures.
|
||||
"""This object represents a user's profile pictures.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`total_count` and :attr:`photos` are equal.
|
||||
|
|
|
@ -57,7 +57,7 @@ def to_float_timestamp(
|
|||
Converts a given time object to a float POSIX timestamp.
|
||||
Used to convert different time specifications to a common format. The time object
|
||||
can be relative (i.e. indicate a time increment, or a time of day) or absolute.
|
||||
object objects from the :class:`datetime` module that are timezone-naive will be assumed
|
||||
Objects from the :class:`datetime` module that are timezone-naive will be assumed
|
||||
to be in UTC, if ``bot`` is not passed or ``bot.defaults`` is :obj:`None`.
|
||||
|
||||
Args:
|
||||
|
@ -65,33 +65,36 @@ def to_float_timestamp(
|
|||
:obj:`datetime.datetime` | :obj:`datetime.time`):
|
||||
Time value to convert. The semantics of this parameter will depend on its type:
|
||||
|
||||
* :obj:`int` or :obj:`float` will be interpreted as "seconds from ``reference_t``"
|
||||
* :obj:`int` or :obj:`float` will be interpreted as "seconds from
|
||||
:paramref:`reference_t`"
|
||||
* :obj:`datetime.timedelta` will be interpreted as
|
||||
"time increment from ``reference_t``"
|
||||
"time increment from :paramref:`reference_timestamp`"
|
||||
* :obj:`datetime.datetime` will be interpreted as an absolute date/time value
|
||||
* :obj:`datetime.time` will be interpreted as a specific time of day
|
||||
|
||||
reference_timestamp (:obj:`float`, optional): POSIX timestamp that indicates the absolute
|
||||
time from which relative calculations are to be performed (e.g. when ``t`` is given as
|
||||
an :obj:`int`, indicating "seconds from ``reference_t``"). Defaults to now (the time at
|
||||
which this function is called).
|
||||
time from which relative calculations are to be performed (e.g. when
|
||||
:paramref:`time_object` is given as an :obj:`int`, indicating "seconds from
|
||||
:paramref:`reference_time`"). Defaults to now (the time at which this function is
|
||||
called).
|
||||
|
||||
If ``t`` is given as an absolute representation of date & time (i.e. a
|
||||
:obj:`datetime.datetime` object), ``reference_timestamp`` is not relevant and so its
|
||||
value should be :obj:`None`. If this is not the case, a ``ValueError`` will be raised.
|
||||
tzinfo (:obj:`pytz.BaseTzInfo`, optional): If ``t`` is a naive object from the
|
||||
:class:`datetime` module, it will be interpreted as this timezone. Defaults to
|
||||
If :paramref:`time_object` is given as an absolute representation of date & time (i.e.
|
||||
a :obj:`datetime.datetime` object), :paramref:`reference_timestamp` is not relevant
|
||||
and so its value should be :obj:`None`. If this is not the case, a :exc:`ValueError`
|
||||
will be raised.
|
||||
tzinfo (:obj:`pytz.BaseTzInfo`, optional): If :paramref:`time_object` is a naive object
|
||||
from the :mod:`datetime` module, it will be interpreted as this timezone. Defaults to
|
||||
``pytz.utc``.
|
||||
|
||||
Note:
|
||||
Only to be used by ``telegram.ext``.
|
||||
|
||||
|
||||
Returns:
|
||||
:obj:`float` | :obj:`None`:
|
||||
The return value depends on the type of argument ``t``.
|
||||
If ``t`` is given as a time increment (i.e. as a :obj:`int`, :obj:`float` or
|
||||
:obj:`datetime.timedelta`), then the return value will be ``reference_t`` + ``t``.
|
||||
The return value depends on the type of argument :paramref:`time_object`.
|
||||
If :paramref:`time_object` is given as a time increment (i.e. as a :obj:`int`,
|
||||
:obj:`float` or :obj:`datetime.timedelta`), then the return value will be
|
||||
:paramref:`reference_timestamp` + :paramref:`time_object`.
|
||||
|
||||
Else if it is given as an absolute date/time value (i.e. a :obj:`datetime.datetime`
|
||||
object), the equivalent value as a POSIX timestamp will be returned.
|
||||
|
@ -100,9 +103,9 @@ def to_float_timestamp(
|
|||
object), the return value is the nearest future occurrence of that time of day.
|
||||
|
||||
Raises:
|
||||
TypeError: If ``t``'s type is not one of those described above.
|
||||
ValueError: If ``t`` is a :obj:`datetime.datetime` and :obj:`reference_timestamp` is not
|
||||
:obj:`None`.
|
||||
TypeError: If :paramref:`time_object` s type is not one of those described above.
|
||||
ValueError: If :paramref:`time_object` is a :obj:`datetime.datetime` and
|
||||
:paramref:`reference_timestamp` is not :obj:`None`.
|
||||
"""
|
||||
if reference_timestamp is None:
|
||||
reference_timestamp = time.time()
|
||||
|
@ -169,7 +172,7 @@ def from_timestamp(unixtime: Optional[int], tzinfo: dtm.tzinfo = UTC) -> Optiona
|
|||
converted to. Defaults to UTC.
|
||||
|
||||
Returns:
|
||||
Timezone aware equivalent :obj:`datetime.datetime` value if ``unixtime`` is not
|
||||
Timezone aware equivalent :obj:`datetime.datetime` value if :paramref:`unixtime` is not
|
||||
:obj:`None`; else :obj:`None`.
|
||||
"""
|
||||
if unixtime is None:
|
||||
|
|
|
@ -101,8 +101,7 @@ class DefaultValue(Generic[DVType]):
|
|||
|
||||
@staticmethod
|
||||
def get_value(obj: Union[OT, 'DefaultValue[OT]']) -> OT:
|
||||
"""
|
||||
Shortcut for::
|
||||
"""Shortcut for::
|
||||
|
||||
return obj.value if isinstance(obj, DefaultValue) else obj
|
||||
|
||||
|
@ -129,5 +128,11 @@ DEFAULT_NONE: DefaultValue = DefaultValue(None)
|
|||
DEFAULT_FALSE: DefaultValue = DefaultValue(False)
|
||||
""":class:`DefaultValue`: Default :obj:`False`"""
|
||||
|
||||
DEFAULT_TRUE: DefaultValue = DefaultValue(True)
|
||||
""":class:`DefaultValue`: Default :obj:`True`
|
||||
|
||||
.. versionadded:: 14.0
|
||||
"""
|
||||
|
||||
DEFAULT_20: DefaultValue = DefaultValue(20)
|
||||
""":class:`DefaultValue`: Default :obj:`20`"""
|
||||
|
|
36
telegram/_utils/enum.py
Normal file
36
telegram/_utils/enum.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2022
|
||||
# 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 a helper class for Enums that should be subclasses of `str`.
|
||||
|
||||
Warning:
|
||||
Contents of this module are intended to be used internally by the library and *not* by the
|
||||
user. Changes to this module are not considered breaking changes and may not be documented in
|
||||
the changelog.
|
||||
"""
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class StringEnum(str, Enum):
|
||||
"""Helper class for string enums where the value is not important to be displayed on
|
||||
stringification.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<{self.__class__.__name__}.{self.name}>'
|
|
@ -57,8 +57,8 @@ def is_local_file(obj: Optional[FilePathInput]) -> bool:
|
|||
def parse_file_input(
|
||||
file_input: Union[FileInput, 'TelegramObject'],
|
||||
tg_type: Type['TelegramObject'] = None,
|
||||
attach: bool = None,
|
||||
filename: str = None,
|
||||
attach: bool = False,
|
||||
) -> Union[str, 'InputFile', Any]:
|
||||
"""
|
||||
Parses input for sending files:
|
||||
|
@ -76,11 +76,11 @@ def parse_file_input(
|
|||
input to parse.
|
||||
tg_type (:obj:`type`, optional): The Telegram media type the input can be. E.g.
|
||||
:class:`telegram.Animation`.
|
||||
attach (:obj:`bool`, optional): Whether this file should be send as one file or is part of
|
||||
a collection of files. Only relevant in case an :class:`telegram.InputFile` is
|
||||
returned.
|
||||
filename (:obj:`str`, optional): The filename. Only relevant in case an
|
||||
:class:`telegram.InputFile` is returned.
|
||||
attach (:obj:`bool`, optional): Pass :obj:`True` if the parameter this file belongs to in
|
||||
the request to Telegram should point to the multipart data via an ``attach://`` URI.
|
||||
Defaults to `False`. Only relevant if an :class:`telegram.InputFile` is returned.
|
||||
|
||||
Returns:
|
||||
:obj:`str` | :class:`telegram.InputFile` | :obj:`object`: The parsed input or the untouched
|
||||
|
@ -98,10 +98,9 @@ def parse_file_input(
|
|||
out = file_input # type: ignore[assignment]
|
||||
return out
|
||||
if isinstance(file_input, bytes):
|
||||
return InputFile(file_input, attach=attach, filename=filename)
|
||||
if InputFile.is_file(file_input):
|
||||
file_input = cast(IO, file_input)
|
||||
return InputFile(file_input, attach=attach, filename=filename)
|
||||
return InputFile(file_input, filename=filename, attach=attach)
|
||||
if hasattr(file_input, 'read'):
|
||||
return InputFile(cast(IO, file_input), filename=filename, attach=attach)
|
||||
if tg_type and isinstance(file_input, tg_type):
|
||||
return file_input.file_id # type: ignore[attr-defined]
|
||||
return file_input
|
||||
|
|
|
@ -41,15 +41,16 @@ if TYPE_CHECKING:
|
|||
from telegram._utils.defaultvalue import DefaultValue # noqa: F401
|
||||
from telegram import InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, ForceReply
|
||||
|
||||
FileLike = Union[IO, 'InputFile']
|
||||
"""Either an open file handler or a :class:`telegram.InputFile`."""
|
||||
FileLike = Union[IO[bytes], 'InputFile']
|
||||
"""Either a bytes-stream (e.g. open file handler) or a :class:`telegram.InputFile`."""
|
||||
|
||||
FilePathInput = Union[str, Path]
|
||||
"""A filepath either as string or as :obj:`pathlib.Path` object."""
|
||||
|
||||
FileInput = Union[FilePathInput, bytes, FileLike]
|
||||
FileInput = Union[FilePathInput, FileLike, bytes, str]
|
||||
"""Valid input for passing files to Telegram. Either a file id as string, a file like object,
|
||||
a local file path as string, :class:`pathlib.Path` or the file contents as :obj:`bytes`."""
|
||||
a local file path as string, :class:`pathlib.Path` or the file contents as :obj:`bytes` or
|
||||
:obj:`str`."""
|
||||
|
||||
JSONDict = Dict[str, Any]
|
||||
"""Dictionary containing response from Telegram or data to send to the API."""
|
||||
|
@ -73,3 +74,8 @@ ReplyMarkup = Union[
|
|||
|
||||
.. versionadded:: 14.0
|
||||
"""
|
||||
|
||||
FieldTuple = Tuple[str, bytes, str]
|
||||
"""Alias for return type of `InputFile.field_tuple`."""
|
||||
UploadFileDict = Dict[str, FieldTuple]
|
||||
"""Dictionary containing file data to be uploaded to the API."""
|
||||
|
|
|
@ -63,20 +63,10 @@ __all__ = [
|
|||
'UpdateType',
|
||||
]
|
||||
|
||||
from enum import Enum, IntEnum
|
||||
from enum import IntEnum
|
||||
from typing import List
|
||||
|
||||
|
||||
class _StringEnum(str, Enum):
|
||||
"""Helper class for string enums where the value is not important to be displayed on
|
||||
stringification.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'<{self.__class__.__name__}.{self.name}>'
|
||||
|
||||
from telegram._utils.enum import StringEnum
|
||||
|
||||
BOT_API_VERSION = '5.7'
|
||||
|
||||
|
@ -85,7 +75,7 @@ BOT_API_VERSION = '5.7'
|
|||
SUPPORTED_WEBHOOK_PORTS: List[int] = [443, 80, 88, 8443]
|
||||
|
||||
|
||||
class BotCommandScopeType(_StringEnum):
|
||||
class BotCommandScopeType(StringEnum):
|
||||
"""This enum contains the available types of :class:`telegram.BotCommandScope`. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
||||
|
@ -125,7 +115,7 @@ class CallbackQueryLimit(IntEnum):
|
|||
:meth:`telegram.Bot.answer_callback_query`."""
|
||||
|
||||
|
||||
class ChatAction(_StringEnum):
|
||||
class ChatAction(StringEnum):
|
||||
"""This enum contains the available chat actions for :meth:`telegram.Bot.send_chat_action`.
|
||||
The enum members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
||||
|
@ -210,7 +200,7 @@ class ChatInviteLinkLimit(IntEnum):
|
|||
:meth:`telegram.Bot.create_chat_invite_link` and :meth:`telegram.Bot.edit_chat_invite_link`."""
|
||||
|
||||
|
||||
class ChatMemberStatus(_StringEnum):
|
||||
class ChatMemberStatus(StringEnum):
|
||||
"""This enum contains the available states for :class:`telegram.ChatMember`. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
||||
|
@ -233,7 +223,7 @@ class ChatMemberStatus(_StringEnum):
|
|||
""":obj:`str`: A :class:`telegram.ChatMember` who was restricted in this chat."""
|
||||
|
||||
|
||||
class ChatType(_StringEnum):
|
||||
class ChatType(StringEnum):
|
||||
"""This enum contains the available types of :class:`telegram.Chat`. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
||||
|
@ -255,7 +245,7 @@ class ChatType(_StringEnum):
|
|||
""":obj:`str`: A :class:`telegram.Chat` that is a channel."""
|
||||
|
||||
|
||||
class DiceEmoji(_StringEnum):
|
||||
class DiceEmoji(StringEnum):
|
||||
"""This enum contains the available emoji for :class:`telegram.Dice`/
|
||||
:meth:`telegram.Bot.send_dice`. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
@ -344,7 +334,7 @@ class InlineKeyboardMarkupLimit(IntEnum):
|
|||
"""
|
||||
|
||||
|
||||
class InputMediaType(_StringEnum):
|
||||
class InputMediaType(StringEnum):
|
||||
"""This enum contains the available types of :class:`telegram.InputMedia`. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
||||
|
@ -383,7 +373,7 @@ class InlineQueryLimit(IntEnum):
|
|||
:meth:`telegram.Bot.answer_inline_query`."""
|
||||
|
||||
|
||||
class InlineQueryResultType(_StringEnum):
|
||||
class InlineQueryResultType(StringEnum):
|
||||
"""This enum contains the available types of :class:`telegram.InlineQueryResult`. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
||||
|
@ -457,7 +447,7 @@ class LocationLimit(IntEnum):
|
|||
"""
|
||||
|
||||
|
||||
class MaskPosition(_StringEnum):
|
||||
class MaskPosition(StringEnum):
|
||||
"""This enum contains the available positions for :class:`telegram.MaskPosition`. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
||||
|
@ -476,7 +466,7 @@ class MaskPosition(_StringEnum):
|
|||
""":obj:`str`: Mask position for a sticker on the chin."""
|
||||
|
||||
|
||||
class MessageAttachmentType(_StringEnum):
|
||||
class MessageAttachmentType(StringEnum):
|
||||
"""This enum contains the available types of :class:`telegram.Message` that can bee seens
|
||||
as attachment. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
@ -525,7 +515,7 @@ class MessageAttachmentType(_StringEnum):
|
|||
""":obj:`str`: Messages with :attr:`telegram.Message.venue`."""
|
||||
|
||||
|
||||
class MessageEntityType(_StringEnum):
|
||||
class MessageEntityType(StringEnum):
|
||||
"""This enum contains the available types of :class:`telegram.MessageEntity`. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
||||
|
@ -592,7 +582,7 @@ class MessageLimit(IntEnum):
|
|||
"""
|
||||
|
||||
|
||||
class MessageType(_StringEnum):
|
||||
class MessageType(StringEnum):
|
||||
"""This enum contains the available types of :class:`telegram.Message` that can be seen
|
||||
as attachment. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
@ -679,7 +669,7 @@ class MessageType(_StringEnum):
|
|||
""":obj:`str`: Messages with :attr:`telegram.Message.voice_chat_participants_invited`."""
|
||||
|
||||
|
||||
class ParseMode(_StringEnum):
|
||||
class ParseMode(StringEnum):
|
||||
"""This enum contains the available parse modes. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
||||
|
@ -719,7 +709,7 @@ class PollLimit(IntEnum):
|
|||
""":obj:`str`: Maximum number of available options for the poll."""
|
||||
|
||||
|
||||
class PollType(_StringEnum):
|
||||
class PollType(StringEnum):
|
||||
"""This enum contains the available types for :class:`telegram.Poll`/
|
||||
:meth:`telegram.Bot.send_poll`. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
@ -735,7 +725,7 @@ class PollType(_StringEnum):
|
|||
""":obj:`str`: quiz polls."""
|
||||
|
||||
|
||||
class UpdateType(_StringEnum):
|
||||
class UpdateType(StringEnum):
|
||||
"""This enum contains the available types of :class:`telegram.Update`. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
||||
|
|
|
@ -16,12 +16,17 @@
|
|||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains an classes that represent Telegram errors."""
|
||||
"""This module contains classes that represent Telegram errors.
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
Replaced ``Unauthorized`` by :class:`Forbidden`.
|
||||
"""
|
||||
|
||||
__all__ = (
|
||||
'BadRequest',
|
||||
'ChatMigrated',
|
||||
'Conflict',
|
||||
'Forbidden',
|
||||
'InvalidToken',
|
||||
'NetworkError',
|
||||
'PassportDecryptionError',
|
||||
|
@ -30,7 +35,7 @@ __all__ = (
|
|||
'TimedOut',
|
||||
)
|
||||
|
||||
from typing import Tuple, Union
|
||||
from typing import Tuple, Union, Optional
|
||||
|
||||
|
||||
def _lstrip_str(in_s: str, lstr: str) -> str:
|
||||
|
@ -69,26 +74,40 @@ class TelegramError(Exception):
|
|||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}('{self.message}')"
|
||||
|
||||
def __reduce__(self) -> Tuple[type, Tuple[str]]:
|
||||
return self.__class__, (self.message,)
|
||||
|
||||
|
||||
class Unauthorized(TelegramError):
|
||||
"""Raised when the bot has not enough rights to perform the requested action."""
|
||||
class Forbidden(TelegramError):
|
||||
"""Raised when the bot has not enough rights to perform the requested action.
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
This class was previously named ``Unauthorized``.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class InvalidToken(TelegramError):
|
||||
"""Raised when the token is invalid."""
|
||||
"""Raised when the token is invalid.
|
||||
|
||||
__slots__ = ()
|
||||
Args:
|
||||
message (:obj:`str`, optional): Any additional information about the exception.
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__('Invalid token')
|
||||
.. versionadded:: 14.0
|
||||
"""
|
||||
|
||||
def __reduce__(self) -> Tuple[type, Tuple]: # type: ignore[override]
|
||||
return self.__class__, ()
|
||||
__slots__ = ('_message',)
|
||||
|
||||
def __init__(self, message: str = None) -> None:
|
||||
self._message = message
|
||||
super().__init__('Invalid token' if self._message is None else self._message)
|
||||
|
||||
def __reduce__(self) -> Tuple[type, Tuple[Optional[str]]]: # type: ignore[override]
|
||||
return self.__class__, (self._message,)
|
||||
|
||||
|
||||
class NetworkError(TelegramError):
|
||||
|
@ -104,15 +123,18 @@ class BadRequest(NetworkError):
|
|||
|
||||
|
||||
class TimedOut(NetworkError):
|
||||
"""Raised when a request took too long to finish."""
|
||||
"""Raised when a request took too long to finish.
|
||||
|
||||
Args:
|
||||
message (:obj:`str`, optional): Any additional information about the exception.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__('Timed out')
|
||||
|
||||
def __reduce__(self) -> Tuple[type, Tuple]: # type: ignore[override]
|
||||
return self.__class__, ()
|
||||
def __init__(self, message: str = None) -> None:
|
||||
super().__init__(message or 'Timed out')
|
||||
|
||||
|
||||
class ChatMigrated(TelegramError):
|
||||
|
@ -128,7 +150,7 @@ class ChatMigrated(TelegramError):
|
|||
|
||||
def __init__(self, new_chat_id: int):
|
||||
super().__init__(f'Group migrated to supergroup. New chat id: {new_chat_id}')
|
||||
self.new_chat_id = new_chat_id
|
||||
self.new_chat_id = int(new_chat_id)
|
||||
|
||||
def __reduce__(self) -> Tuple[type, Tuple[int]]: # type: ignore[override]
|
||||
return self.__class__, (self.new_chat_id,)
|
||||
|
|
|
@ -19,6 +19,9 @@
|
|||
"""Extensions over the Telegram Bot API to facilitate bot making"""
|
||||
|
||||
__all__ = (
|
||||
'Application',
|
||||
'ApplicationBuilder',
|
||||
'ApplicationHandlerStop',
|
||||
'BasePersistence',
|
||||
'CallbackContext',
|
||||
'CallbackDataCache',
|
||||
|
@ -31,9 +34,6 @@ __all__ = (
|
|||
'ConversationHandler',
|
||||
'Defaults',
|
||||
'DictPersistence',
|
||||
'Dispatcher',
|
||||
'DispatcherBuilder',
|
||||
'DispatcherHandlerStop',
|
||||
'ExtBot',
|
||||
'filters',
|
||||
'Handler',
|
||||
|
@ -53,7 +53,6 @@ __all__ = (
|
|||
'StringRegexHandler',
|
||||
'TypeHandler',
|
||||
'Updater',
|
||||
'UpdaterBuilder',
|
||||
)
|
||||
|
||||
from ._extbot import ExtBot
|
||||
|
@ -63,9 +62,9 @@ from ._dictpersistence import DictPersistence
|
|||
from ._handler import Handler
|
||||
from ._callbackcontext import CallbackContext
|
||||
from ._contexttypes import ContextTypes
|
||||
from ._dispatcher import Dispatcher, DispatcherHandlerStop
|
||||
from ._jobqueue import JobQueue, Job
|
||||
from ._updater import Updater
|
||||
from ._application import Application, ApplicationHandlerStop
|
||||
from ._callbackqueryhandler import CallbackQueryHandler
|
||||
from ._choseninlineresulthandler import ChosenInlineResultHandler
|
||||
from ._inlinequeryhandler import InlineQueryHandler
|
||||
|
@ -84,4 +83,4 @@ from ._chatmemberhandler import ChatMemberHandler
|
|||
from ._chatjoinrequesthandler import ChatJoinRequestHandler
|
||||
from ._defaults import Defaults
|
||||
from ._callbackdatacache import CallbackDataCache, InvalidCallbackData
|
||||
from ._builders import DispatcherBuilder, UpdaterBuilder
|
||||
from ._applicationbuilder import ApplicationBuilder
|
||||
|
|
1454
telegram/ext/_application.py
Normal file
1454
telegram/ext/_application.py
Normal file
File diff suppressed because it is too large
Load diff
903
telegram/ext/_applicationbuilder.py
Normal file
903
telegram/ext/_applicationbuilder.py
Normal file
|
@ -0,0 +1,903 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2022
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the Builder classes for the telegram.ext module."""
|
||||
from asyncio import Queue
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
TypeVar,
|
||||
Generic,
|
||||
TYPE_CHECKING,
|
||||
Dict,
|
||||
Union,
|
||||
Type,
|
||||
Optional,
|
||||
)
|
||||
|
||||
from telegram import Bot
|
||||
from telegram._utils.types import ODVInput, DVInput, FilePathInput
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue, DEFAULT_FALSE
|
||||
from telegram.ext import Application, JobQueue, ExtBot, ContextTypes, CallbackContext, Updater
|
||||
from telegram.request._httpxrequest import HTTPXRequest
|
||||
from telegram.ext._utils.types import CCT, UD, CD, BD, BT, JQ
|
||||
from telegram.request import BaseRequest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import (
|
||||
Defaults,
|
||||
BasePersistence,
|
||||
)
|
||||
|
||||
# Type hinting is a bit complicated here because we try to get to a sane level of
|
||||
# leveraging generics and therefore need a number of type variables.
|
||||
InBT = TypeVar('InBT', bound=Bot) # 'In' stands for input - used in parameters of methods below
|
||||
InJQ = TypeVar('InJQ', bound=Union[None, JobQueue])
|
||||
InCCT = TypeVar('InCCT', bound='CallbackContext')
|
||||
InUD = TypeVar('InUD')
|
||||
InCD = TypeVar('InCD')
|
||||
InBD = TypeVar('InBD')
|
||||
BuilderType = TypeVar('BuilderType', bound='ApplicationBuilder')
|
||||
|
||||
|
||||
_BOT_CHECKS = [
|
||||
('request', 'request instance'),
|
||||
('get_updates_request', 'get_updates_request instance'),
|
||||
('connection_pool_size', 'connection_pool_size'),
|
||||
('proxy_url', 'proxy_url'),
|
||||
('pool_timeout', 'pool_timeout'),
|
||||
('connect_timeout', 'connect_timeout'),
|
||||
('read_timeout', 'read_timeout'),
|
||||
('write_timeout', 'write_timeout'),
|
||||
('get_updates_connection_pool_size', 'get_updates_connection_pool_size'),
|
||||
('get_updates_proxy_url', 'get_updates_proxy_url'),
|
||||
('get_updates_pool_timeout', 'get_updates_pool_timeout'),
|
||||
('get_updates_connect_timeout', 'get_updates_connect_timeout'),
|
||||
('get_updates_read_timeout', 'get_updates_read_timeout'),
|
||||
('get_updates_write_timeout', 'get_updates_write_timeout'),
|
||||
('base_file_url', 'base_file_url'),
|
||||
('base_url', 'base_url'),
|
||||
('token', 'token'),
|
||||
('defaults', 'defaults'),
|
||||
('arbitrary_callback_data', 'arbitrary_callback_data'),
|
||||
('private_key', 'private_key'),
|
||||
]
|
||||
|
||||
_TWO_ARGS_REQ = "The parameter `{}` may only be set, if no {} was set."
|
||||
|
||||
|
||||
class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
||||
"""This class serves as initializer for :class:`telegram.ext.Application` via the so called
|
||||
`builder pattern`_. To build a :class:`telegram.ext.Application`, one first initializes an
|
||||
instance of this class. Arguments for the :class:`telegram.ext.Application` to build are then
|
||||
added by subsequently calling the methods of the builder. Finally, the
|
||||
:class:`telegram.ext.Application` is built by calling :meth:`build`. In the simplest case this
|
||||
can look like the following example.
|
||||
|
||||
Example:
|
||||
.. code:: python
|
||||
|
||||
application = ApplicationBuilder().token("TOKEN").build()
|
||||
|
||||
Please see the description of the individual methods for information on which arguments can be
|
||||
set and what the defaults are when not called. When no default is mentioned, the argument will
|
||||
not be used by default.
|
||||
|
||||
Note:
|
||||
* Some arguments are mutually exclusive. E.g. after calling :meth:`token`, you can't set
|
||||
a custom bot with :meth:`bot` and vice versa.
|
||||
* Unless a custom :class:`telegram.Bot` instance is set via :meth:`bot`, :meth:`build` will
|
||||
use :class:`telegram.ext.ExtBot` for the bot.
|
||||
|
||||
.. _`builder pattern`: https://en.wikipedia.org/wiki/Builder_pattern
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'_token',
|
||||
'_base_url',
|
||||
'_base_file_url',
|
||||
'_connection_pool_size',
|
||||
'_proxy_url',
|
||||
'_connect_timeout',
|
||||
'_read_timeout',
|
||||
'_write_timeout',
|
||||
'_pool_timeout',
|
||||
'_request',
|
||||
'_get_updates_connection_pool_size',
|
||||
'_get_updates_proxy_url',
|
||||
'_get_updates_connect_timeout',
|
||||
'_get_updates_read_timeout',
|
||||
'_get_updates_write_timeout',
|
||||
'_get_updates_pool_timeout',
|
||||
'_get_updates_request',
|
||||
'_private_key',
|
||||
'_private_key_password',
|
||||
'_defaults',
|
||||
'_arbitrary_callback_data',
|
||||
'_bot',
|
||||
'_update_queue',
|
||||
'_job_queue',
|
||||
'_persistence',
|
||||
'_context_types',
|
||||
'_application_class',
|
||||
'_application_kwargs',
|
||||
'_concurrent_updates',
|
||||
'_updater',
|
||||
)
|
||||
|
||||
def __init__(self: 'InitApplicationBuilder'):
|
||||
self._token: DVInput[str] = DefaultValue('')
|
||||
self._base_url: DVInput[str] = DefaultValue('https://api.telegram.org/bot')
|
||||
self._base_file_url: DVInput[str] = DefaultValue('https://api.telegram.org/file/bot')
|
||||
self._connection_pool_size: DVInput[int] = DEFAULT_NONE
|
||||
self._proxy_url: DVInput[str] = DEFAULT_NONE
|
||||
self._connect_timeout: ODVInput[float] = DEFAULT_NONE
|
||||
self._read_timeout: ODVInput[float] = DEFAULT_NONE
|
||||
self._write_timeout: ODVInput[float] = DEFAULT_NONE
|
||||
self._pool_timeout: ODVInput[float] = DEFAULT_NONE
|
||||
self._request: DVInput['BaseRequest'] = DEFAULT_NONE
|
||||
self._get_updates_connection_pool_size: DVInput[int] = DEFAULT_NONE
|
||||
self._get_updates_proxy_url: DVInput[str] = DEFAULT_NONE
|
||||
self._get_updates_connect_timeout: ODVInput[float] = DEFAULT_NONE
|
||||
self._get_updates_read_timeout: ODVInput[float] = DEFAULT_NONE
|
||||
self._get_updates_write_timeout: ODVInput[float] = DEFAULT_NONE
|
||||
self._get_updates_pool_timeout: ODVInput[float] = DEFAULT_NONE
|
||||
self._get_updates_request: DVInput['BaseRequest'] = DEFAULT_NONE
|
||||
self._private_key: ODVInput[bytes] = DEFAULT_NONE
|
||||
self._private_key_password: ODVInput[bytes] = DEFAULT_NONE
|
||||
self._defaults: ODVInput['Defaults'] = DEFAULT_NONE
|
||||
self._arbitrary_callback_data: DVInput[Union[bool, int]] = DEFAULT_FALSE
|
||||
self._bot: DVInput[Bot] = DEFAULT_NONE
|
||||
self._update_queue: DVInput[Queue] = DefaultValue(Queue())
|
||||
self._job_queue: ODVInput['JobQueue'] = DefaultValue(JobQueue())
|
||||
self._persistence: ODVInput['BasePersistence'] = DEFAULT_NONE
|
||||
self._context_types: DVInput[ContextTypes] = DefaultValue(ContextTypes())
|
||||
self._application_class: DVInput[Type[Application]] = DefaultValue(Application)
|
||||
self._application_kwargs: Dict[str, object] = {}
|
||||
self._concurrent_updates: DVInput[Union[int, bool]] = DEFAULT_FALSE
|
||||
self._updater: ODVInput[Updater] = DEFAULT_NONE
|
||||
|
||||
def _build_request(self, get_updates: bool) -> BaseRequest:
|
||||
prefix = '_get_updates_' if get_updates else '_'
|
||||
if not isinstance(getattr(self, f'{prefix}request'), DefaultValue):
|
||||
return getattr(self, f'{prefix}request')
|
||||
|
||||
proxy_url = DefaultValue.get_value(getattr(self, f'{prefix}proxy_url'))
|
||||
if get_updates:
|
||||
connection_pool_size = (
|
||||
DefaultValue.get_value(getattr(self, f'{prefix}connection_pool_size')) or 1
|
||||
)
|
||||
else:
|
||||
connection_pool_size = (
|
||||
DefaultValue.get_value(getattr(self, f'{prefix}connection_pool_size')) or 128
|
||||
)
|
||||
|
||||
timeouts = dict(
|
||||
connect_timeout=getattr(self, f'{prefix}connect_timeout'),
|
||||
read_timeout=getattr(self, f'{prefix}read_timeout'),
|
||||
write_timeout=getattr(self, f'{prefix}write_timeout'),
|
||||
pool_timeout=getattr(self, f'{prefix}pool_timeout'),
|
||||
)
|
||||
# Get timeouts that were actually set-
|
||||
effective_timeouts = {
|
||||
key: value for key, value in timeouts.items() if not isinstance(value, DefaultValue)
|
||||
}
|
||||
|
||||
return HTTPXRequest(
|
||||
connection_pool_size=connection_pool_size,
|
||||
proxy_url=proxy_url,
|
||||
**effective_timeouts,
|
||||
)
|
||||
|
||||
def _build_ext_bot(self) -> ExtBot:
|
||||
if isinstance(self._token, DefaultValue):
|
||||
raise RuntimeError('No bot token was set.')
|
||||
|
||||
return ExtBot(
|
||||
token=self._token,
|
||||
base_url=DefaultValue.get_value(self._base_url),
|
||||
base_file_url=DefaultValue.get_value(self._base_file_url),
|
||||
private_key=DefaultValue.get_value(self._private_key),
|
||||
private_key_password=DefaultValue.get_value(self._private_key_password),
|
||||
defaults=DefaultValue.get_value(self._defaults),
|
||||
arbitrary_callback_data=DefaultValue.get_value(self._arbitrary_callback_data),
|
||||
request=self._build_request(get_updates=False),
|
||||
get_updates_request=self._build_request(get_updates=True),
|
||||
)
|
||||
|
||||
def build(
|
||||
self: 'ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]',
|
||||
) -> Application[BT, CCT, UD, CD, BD, JQ]:
|
||||
"""Builds a :class:`telegram.ext.Application` with the provided arguments.
|
||||
|
||||
Calls :meth:`telegram.ext.JobQueue.set_application` and
|
||||
:meth:`telegram.ext.BasePersistence.set_bot` if appropriate.
|
||||
|
||||
Returns:
|
||||
:class:`telegram.ext.Application`
|
||||
"""
|
||||
job_queue = DefaultValue.get_value(self._job_queue)
|
||||
persistence = DefaultValue.get_value(self._persistence)
|
||||
# If user didn't set updater
|
||||
if isinstance(self._updater, DefaultValue) or self._updater is None:
|
||||
if isinstance(self._bot, DefaultValue): # and didn't set a bot
|
||||
bot: Bot = self._build_ext_bot() # build a bot
|
||||
else:
|
||||
bot = self._bot
|
||||
# now also build an updater/update_queue for them
|
||||
update_queue = DefaultValue.get_value(self._update_queue)
|
||||
|
||||
if self._updater is None:
|
||||
updater = None
|
||||
else:
|
||||
updater = Updater(bot=bot, update_queue=update_queue)
|
||||
else: # if they set an updater, get all necessary attributes for Application from Updater:
|
||||
updater = self._updater
|
||||
bot = self._updater.bot
|
||||
update_queue = self._updater.update_queue
|
||||
|
||||
application: Application[
|
||||
BT, CCT, UD, CD, BD, JQ
|
||||
] = DefaultValue.get_value( # type: ignore[call-arg] # pylint: disable=not-callable
|
||||
self._application_class
|
||||
)(
|
||||
bot=bot,
|
||||
update_queue=update_queue,
|
||||
updater=updater,
|
||||
concurrent_updates=DefaultValue.get_value(self._concurrent_updates),
|
||||
job_queue=job_queue,
|
||||
persistence=persistence,
|
||||
context_types=DefaultValue.get_value(self._context_types),
|
||||
**self._application_kwargs, # For custom Application subclasses
|
||||
)
|
||||
|
||||
if job_queue is not None:
|
||||
job_queue.set_application(application)
|
||||
|
||||
if persistence is not None:
|
||||
# This raises an exception if persistence.store_data.callback_data is True
|
||||
# but self.bot is not an instance of ExtBot - so no need to check that later on
|
||||
persistence.set_bot(bot)
|
||||
|
||||
return application
|
||||
|
||||
def application_class(
|
||||
self: BuilderType, application_class: Type[Application], kwargs: Dict[str, object] = None
|
||||
) -> BuilderType:
|
||||
"""Sets a custom subclass instead of :class:`telegram.ext.Application`. The
|
||||
subclass's ``__init__`` should look like this
|
||||
|
||||
.. code:: python
|
||||
|
||||
def __init__(self, custom_arg_1, custom_arg_2, ..., **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.custom_arg_1 = custom_arg_1
|
||||
self.custom_arg_2 = custom_arg_2
|
||||
|
||||
Args:
|
||||
application_class (:obj:`type`): A subclass of :class:`telegram.ext.Application`
|
||||
kwargs (Dict[:obj:`str`, :obj:`object`], optional): Keyword arguments for the
|
||||
initialization. Defaults to an empty dict.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._application_class = application_class
|
||||
self._application_kwargs = kwargs or {}
|
||||
return self
|
||||
|
||||
def token(self: BuilderType, token: str) -> BuilderType:
|
||||
"""Sets the token for :attr:`telegram.ext.Application.bot`.
|
||||
|
||||
Args:
|
||||
token (:obj:`str`): The token.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
if self._bot is not DEFAULT_NONE:
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('token', 'bot instance'))
|
||||
if self._updater not in (DEFAULT_NONE, None):
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('token', 'updater'))
|
||||
self._token = token
|
||||
return self
|
||||
|
||||
def base_url(self: BuilderType, base_url: str) -> BuilderType:
|
||||
"""Sets the base URL for :attr:`telegram.ext.Application.bot`. If not called,
|
||||
will default to ``'https://api.telegram.org/bot'``.
|
||||
|
||||
.. seealso:: :paramref:`telegram.Bot.base_url`, `Local Bot API Server <https://github.com/\
|
||||
python-telegram-bot/python-telegram-bot/wiki/Local-Bot-API-Server>`_,
|
||||
:meth:`base_file_url`
|
||||
|
||||
Args:
|
||||
base_url (:obj:`str`): The URL.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
if self._bot is not DEFAULT_NONE:
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('base_url', 'bot instance'))
|
||||
if self._updater not in (DEFAULT_NONE, None):
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('base_url', 'updater'))
|
||||
self._base_url = base_url
|
||||
return self
|
||||
|
||||
def base_file_url(self: BuilderType, base_file_url: str) -> BuilderType:
|
||||
"""Sets the base file URL for :attr:`telegram.ext.Application.bot`. If not
|
||||
called, will default to ``'https://api.telegram.org/file/bot'``.
|
||||
|
||||
.. seealso:: :paramref:`telegram.Bot.base_file_url`, `Local Bot API Server <https://\
|
||||
github.com/python-telegram-bot/python-telegram-bot/wiki/Local-Bot-API-Server>`_,
|
||||
:meth:`base_url`
|
||||
|
||||
Args:
|
||||
base_file_url (:obj:`str`): The URL.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
if self._bot is not DEFAULT_NONE:
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('base_file_url', 'bot instance'))
|
||||
if self._updater not in (DEFAULT_NONE, None):
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('base_file_url', 'updater'))
|
||||
self._base_file_url = base_file_url
|
||||
return self
|
||||
|
||||
def _request_check(self, get_updates: bool) -> None:
|
||||
prefix = 'get_updates_' if get_updates else ''
|
||||
name = prefix + 'request'
|
||||
|
||||
# Code below tests if it's okay to set a Request object. Only okay if no other request args
|
||||
# or instances containing a Request were set previously
|
||||
for attr in ('connect_timeout', 'read_timeout', 'write_timeout', 'pool_timeout'):
|
||||
if not isinstance(getattr(self, f"_{prefix}{attr}"), DefaultValue):
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format(name, attr))
|
||||
if not isinstance(getattr(self, f'_{prefix}connection_pool_size'), DefaultValue):
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format(name, 'connection_pool_size'))
|
||||
if not isinstance(getattr(self, f'_{prefix}proxy_url'), DefaultValue):
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format(name, 'proxy_url'))
|
||||
if self._bot is not DEFAULT_NONE:
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format(name, 'bot instance'))
|
||||
if self._updater not in (DEFAULT_NONE, None):
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format(name, 'updater instance'))
|
||||
|
||||
def _request_param_check(self, name: str, get_updates: bool) -> None:
|
||||
if get_updates and self._get_updates_request is not DEFAULT_NONE:
|
||||
raise RuntimeError( # disallow request args for get_updates if Request for that is set
|
||||
_TWO_ARGS_REQ.format(f'get_updates_{name}', 'get_updates_request instance')
|
||||
)
|
||||
if self._request is not DEFAULT_NONE: # disallow request args if request is set
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format(name, 'request instance'))
|
||||
|
||||
if self._bot is not DEFAULT_NONE: # disallow request args if bot is set (has Request)
|
||||
raise RuntimeError(
|
||||
_TWO_ARGS_REQ.format(
|
||||
f'get_updates_{name}' if get_updates else name, 'bot instance'
|
||||
)
|
||||
)
|
||||
|
||||
if self._updater not in (DEFAULT_NONE, None): # disallow request args for updater(has bot)
|
||||
raise RuntimeError(
|
||||
_TWO_ARGS_REQ.format(f'get_updates_{name}' if get_updates else name, 'updater')
|
||||
)
|
||||
|
||||
def request(self: BuilderType, request: BaseRequest) -> BuilderType:
|
||||
"""Sets a :class:`telegram.request.BaseRequest` instance for the
|
||||
:paramref:`telegram.Bot.request` parameter of :attr:`telegram.ext.Application.bot`.
|
||||
|
||||
.. seealso:: :meth:`get_updates_request`
|
||||
|
||||
Args:
|
||||
request (:class:`telegram.request.BaseRequest`): The request instance.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._request_check(get_updates=False)
|
||||
self._request = request
|
||||
return self
|
||||
|
||||
def connection_pool_size(self: BuilderType, connection_pool_size: int) -> BuilderType:
|
||||
"""Sets the size of the connection pool for the
|
||||
:paramref:`~telegram.request.HTTPXRequest.connection_pool_size` parameter of
|
||||
:attr:`telegram.Bot.request`. Defaults to ``128``.
|
||||
|
||||
Args:
|
||||
connection_pool_size (:obj:`int`): The size of the connection pool.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._request_param_check(name='connection_pool_size', get_updates=False)
|
||||
self._connection_pool_size = connection_pool_size
|
||||
return self
|
||||
|
||||
def proxy_url(self: BuilderType, proxy_url: str) -> BuilderType:
|
||||
"""Sets the proxy for the :paramref:`~telegram.request.HTTPXRequest.proxy_url`
|
||||
parameter of :attr:`telegram.Bot.request`. Defaults to :obj:`None`.
|
||||
|
||||
Args:
|
||||
proxy_url (:obj:`str`): The URL to the proxy server. See
|
||||
:paramref:`telegram.request.HTTPXRequest.proxy_url` for more information.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._request_param_check(name='proxy_url', get_updates=False)
|
||||
self._proxy_url = proxy_url
|
||||
return self
|
||||
|
||||
def connect_timeout(self: BuilderType, connect_timeout: Optional[float]) -> BuilderType:
|
||||
"""Sets the connection attempt timeout for the
|
||||
:paramref:`~telegram.request.HTTPXRequest.connect_timeout` parameter of
|
||||
:attr:`telegram.Bot.request`. Defaults to ``5.0``.
|
||||
|
||||
Args:
|
||||
connect_timeout (:obj:`float`): See
|
||||
:paramref:`telegram.request.HTTPXRequest.connect_timeout` for more information.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._request_param_check(name='connect_timeout', get_updates=False)
|
||||
self._connect_timeout = connect_timeout
|
||||
return self
|
||||
|
||||
def read_timeout(self: BuilderType, read_timeout: Optional[float]) -> BuilderType:
|
||||
"""Sets the waiting timeout for the
|
||||
:paramref:`~telegram.request.HTTPXRequest.read_timeout` parameter of
|
||||
:attr:`telegram.Bot.request`. Defaults to ``5.0``.
|
||||
|
||||
Args:
|
||||
read_timeout (:obj:`float`): See
|
||||
:paramref:`telegram.request.HTTPXRequest.read_timeout` for more information.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._request_param_check(name='read_timeout', get_updates=False)
|
||||
self._read_timeout = read_timeout
|
||||
return self
|
||||
|
||||
def write_timeout(self: BuilderType, write_timeout: Optional[float]) -> BuilderType:
|
||||
"""Sets the write operation timeout for the
|
||||
:paramref:`~telegram.request.HTTPXRequest.write_timeout` parameter of
|
||||
:attr:`telegram.Bot.request`. Defaults to ``5.0``.
|
||||
|
||||
Args:
|
||||
write_timeout (:obj:`float`): See
|
||||
:paramref:`telegram.request.HTTPXRequest.write_timeout` for more information.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._request_param_check(name='write_timeout', get_updates=False)
|
||||
self._write_timeout = write_timeout
|
||||
return self
|
||||
|
||||
def pool_timeout(self: BuilderType, pool_timeout: Optional[float]) -> BuilderType:
|
||||
"""Sets the connection pool's connection freeing timeout for the
|
||||
:paramref:`~telegram.request.HTTPXRequest.pool_timeout` parameter of
|
||||
:attr:`telegram.Bot.request`. Defaults to :obj:`None`.
|
||||
|
||||
Args:
|
||||
pool_timeout (:obj:`float`): See
|
||||
:paramref:`telegram.request.HTTPXRequest.pool_timeout` for more information.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._request_param_check(name='pool_timeout', get_updates=False)
|
||||
self._pool_timeout = pool_timeout
|
||||
return self
|
||||
|
||||
def get_updates_request(self: BuilderType, get_updates_request: BaseRequest) -> BuilderType:
|
||||
"""Sets a :class:`telegram.request.BaseRequest` instance for the
|
||||
:paramref:`~telegram.Bot.get_updates_request` parameter of
|
||||
:attr:`telegram.ext.Application.bot`.
|
||||
|
||||
.. seealso:: :meth:`request`
|
||||
|
||||
Args:
|
||||
get_updates_request (:class:`telegram.request.BaseRequest`): The request instance.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._request_check(get_updates=True)
|
||||
self._get_updates_request = get_updates_request
|
||||
return self
|
||||
|
||||
def get_updates_connection_pool_size(
|
||||
self: BuilderType, get_updates_connection_pool_size: int
|
||||
) -> BuilderType:
|
||||
"""Sets the size of the connection pool for the
|
||||
:paramref:`telegram.request.HTTPXRequest.connection_pool_size` parameter which is used
|
||||
for the :meth:`telegram.Bot.get_updates` request. Defaults to ``1``.
|
||||
|
||||
Args:
|
||||
get_updates_connection_pool_size (:obj:`int`): The size of the connection pool.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._request_param_check(name='connection_pool_size', get_updates=True)
|
||||
self._get_updates_connection_pool_size = get_updates_connection_pool_size
|
||||
return self
|
||||
|
||||
def get_updates_proxy_url(self: BuilderType, get_updates_proxy_url: str) -> BuilderType:
|
||||
"""Sets the proxy for the :paramref:`telegram.request.HTTPXRequest.proxy_url`
|
||||
parameter which is used for :meth:`telegram.Bot.get_updates`. Defaults to :obj:`None`.
|
||||
|
||||
Args:
|
||||
get_updates_proxy_url (:obj:`str`): The URL to the proxy server. See
|
||||
:paramref:`telegram.request.HTTPXRequest.proxy_url` for more information.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._request_param_check(name='proxy_url', get_updates=True)
|
||||
self._get_updates_proxy_url = get_updates_proxy_url
|
||||
return self
|
||||
|
||||
def get_updates_connect_timeout(
|
||||
self: BuilderType, get_updates_connect_timeout: Optional[float]
|
||||
) -> BuilderType:
|
||||
"""Sets the connection attempt timeout for the
|
||||
:paramref:`telegram.request.HTTPXRequest.connect_timeout` parameter which is used for
|
||||
the :meth:`telegram.Bot.get_updates` request. Defaults to ``5.0``.
|
||||
|
||||
Args:
|
||||
get_updates_connect_timeout (:obj:`float`): See
|
||||
:paramref:`telegram.request.HTTPXRequest.connect_timeout` for more information.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._request_param_check(name='connect_timeout', get_updates=True)
|
||||
self._get_updates_connect_timeout = get_updates_connect_timeout
|
||||
return self
|
||||
|
||||
def get_updates_read_timeout(
|
||||
self: BuilderType, get_updates_read_timeout: Optional[float]
|
||||
) -> BuilderType:
|
||||
"""Sets the waiting timeout for the
|
||||
:paramref:`telegram.request.HTTPXRequest.read_timeout` parameter which is used for the
|
||||
:meth:`telegram.Bot.get_updates` request. Defaults to ``5.0``.
|
||||
|
||||
Args:
|
||||
get_updates_read_timeout (:obj:`float`): See
|
||||
:paramref:`telegram.request.HTTPXRequest.read_timeout` for more information.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._request_param_check(name='read_timeout', get_updates=True)
|
||||
self._get_updates_read_timeout = get_updates_read_timeout
|
||||
return self
|
||||
|
||||
def get_updates_write_timeout(
|
||||
self: BuilderType, get_updates_write_timeout: Optional[float]
|
||||
) -> BuilderType:
|
||||
"""Sets the write operation timeout for the
|
||||
:paramref:`telegram.request.HTTPXRequest.write_timeout` parameter which is used for
|
||||
the :meth:`telegram.Bot.get_updates` request. Defaults to ``5.0``.
|
||||
|
||||
Args:
|
||||
get_updates_write_timeout (:obj:`float`): See
|
||||
:paramref:`telegram.request.HTTPXRequest.write_timeout` for more information.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._request_param_check(name='write_timeout', get_updates=True)
|
||||
self._get_updates_write_timeout = get_updates_write_timeout
|
||||
return self
|
||||
|
||||
def get_updates_pool_timeout(
|
||||
self: BuilderType, get_updates_pool_timeout: Optional[float]
|
||||
) -> BuilderType:
|
||||
"""Sets the connection pool's connection freeing timeout for the
|
||||
:paramref:`~telegram.request.HTTPXRequest.pool_timeout` parameter which is used for the
|
||||
:meth:`telegram.Bot.get_updates` request. Defaults to :obj:`None`.
|
||||
|
||||
Args:
|
||||
get_updates_pool_timeout (:obj:`float`): See
|
||||
:paramref:`telegram.request.HTTPXRequest.pool_timeout` for more information.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._request_param_check(name='pool_timeout', get_updates=True)
|
||||
self._get_updates_pool_timeout = get_updates_pool_timeout
|
||||
return self
|
||||
|
||||
def private_key(
|
||||
self: BuilderType,
|
||||
private_key: Union[bytes, FilePathInput],
|
||||
password: Union[bytes, FilePathInput] = None,
|
||||
) -> BuilderType:
|
||||
"""Sets the private key and corresponding password for decryption of telegram passport data
|
||||
for :attr:`telegram.ext.Application.bot`.
|
||||
|
||||
.. seealso:: `passportbot.py <https://github.com/python-telegram-bot/python-telegram-bot\
|
||||
/tree/master/examples#passportbotpy>`_, `Telegram Passports
|
||||
<https://github.com/python-telegram-bot/python-telegram-bot/wiki/Telegram-Passport>`_
|
||||
|
||||
Args:
|
||||
private_key (:obj:`bytes` | :obj:`str` | :obj:`pathlib.Path`): The private key or the
|
||||
file path of a file that contains the key. In the latter case, the file's content
|
||||
will be read automatically.
|
||||
password (:obj:`bytes` | :obj:`str` | :obj:`pathlib.Path`, optional): The corresponding
|
||||
password or the file path of a file that contains the password. In the latter case,
|
||||
the file's content will be read automatically.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
if self._bot is not DEFAULT_NONE:
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('private_key', 'bot instance'))
|
||||
if self._updater not in (DEFAULT_NONE, None):
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('private_key', 'updater'))
|
||||
|
||||
self._private_key = (
|
||||
private_key if isinstance(private_key, bytes) else Path(private_key).read_bytes()
|
||||
)
|
||||
if password is None or isinstance(password, bytes):
|
||||
self._private_key_password = password
|
||||
else:
|
||||
self._private_key_password = Path(password).read_bytes()
|
||||
|
||||
return self
|
||||
|
||||
def defaults(self: BuilderType, defaults: 'Defaults') -> BuilderType:
|
||||
"""Sets the :class:`telegram.ext.Defaults` instance for
|
||||
:attr:`telegram.ext.Application.bot`.
|
||||
|
||||
.. seealso:: `Adding Defaults <https://github.com/python-telegram-bot/python-telegram-bot\
|
||||
/wiki/Adding-defaults-to-your-bot>`_
|
||||
|
||||
Args:
|
||||
defaults (:class:`telegram.ext.Defaults`): The defaults instance.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
if self._bot is not DEFAULT_NONE:
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('defaults', 'bot instance'))
|
||||
if self._updater not in (DEFAULT_NONE, None):
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('defaults', 'updater'))
|
||||
self._defaults = defaults
|
||||
return self
|
||||
|
||||
def arbitrary_callback_data(
|
||||
self: BuilderType, arbitrary_callback_data: Union[bool, int]
|
||||
) -> BuilderType:
|
||||
"""Specifies whether :attr:`telegram.ext.Application.bot` should allow arbitrary objects as
|
||||
callback data for :class:`telegram.InlineKeyboardButton` and how many keyboards should be
|
||||
cached in memory. If not called, only strings can be used as callback data and no data will
|
||||
be stored in memory.
|
||||
|
||||
.. seealso:: `Arbitrary callback_data <https://github.com/python-telegram-bot\
|
||||
/python-telegram-bot/wiki/Arbitrary-callback_data>`_,
|
||||
`arbitrarycallbackdatabot.py <https://github.com/python-telegram-bot\
|
||||
/python-telegram-bot/tree/master/examples#arbitrarycallbackdatabotpy>`_
|
||||
|
||||
Args:
|
||||
arbitrary_callback_data (:obj:`bool` | :obj:`int`): If :obj:`True` is passed, the
|
||||
default cache size of ``1024`` will be used. Pass an integer to specify a different
|
||||
cache size.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
if self._bot is not DEFAULT_NONE:
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('arbitrary_callback_data', 'bot instance'))
|
||||
if self._updater not in (DEFAULT_NONE, None):
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('arbitrary_callback_data', 'updater'))
|
||||
self._arbitrary_callback_data = arbitrary_callback_data
|
||||
return self
|
||||
|
||||
def bot(
|
||||
self: 'ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]',
|
||||
bot: InBT,
|
||||
) -> 'ApplicationBuilder[InBT, CCT, UD, CD, BD, JQ]':
|
||||
"""Sets a :class:`telegram.Bot` instance for
|
||||
:attr:`telegram.ext.Application.bot`. Instances of subclasses like
|
||||
:class:`telegram.ext.ExtBot` are also valid.
|
||||
|
||||
Args:
|
||||
bot (:class:`telegram.Bot`): The bot.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
if self._updater not in (DEFAULT_NONE, None):
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('bot', 'updater'))
|
||||
for attr, error in _BOT_CHECKS:
|
||||
if not isinstance(getattr(self, f'_{attr}'), DefaultValue):
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('bot', error))
|
||||
self._bot = bot
|
||||
return self # type: ignore[return-value]
|
||||
|
||||
def update_queue(self: BuilderType, update_queue: Queue) -> BuilderType:
|
||||
"""Sets a :class:`asyncio.Queue` instance for
|
||||
:attr:`telegram.ext.Application.update_queue`, i.e. the queue that the application will
|
||||
fetch updates from. Will also be used for the :attr:`telegram.ext.Application.updater`.
|
||||
If not called, a queue will be instantiated.
|
||||
|
||||
.. seealso:: :attr:`telegram.ext.Updater.update_queue`
|
||||
|
||||
Args:
|
||||
update_queue (:class:`asyncio.Queue`): The queue.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
if self._updater not in (DEFAULT_NONE, None):
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('update_queue', 'updater instance'))
|
||||
self._update_queue = update_queue
|
||||
return self
|
||||
|
||||
def concurrent_updates(self: BuilderType, concurrent_updates: Union[bool, int]) -> BuilderType:
|
||||
"""Specifies if and how many updates may be processed concurrently instead of one by one.
|
||||
|
||||
Warning:
|
||||
Processing updates concurrently is not recommended when stateful handlers like
|
||||
:class:`telegram.ext.ConversationHandler` are used. Only use this if you are sure
|
||||
that your bot does not (explicitly or implicitly) rely on updates being processed
|
||||
sequentially.
|
||||
|
||||
.. seealso:: :paramref:`telegram.ext.Application.concurrent_updates`
|
||||
|
||||
Args:
|
||||
concurrent_updates (:obj:`bool` | :obj:`int`): Passing :obj:`True` will allow for
|
||||
``4096`` updates to be processed concurrently. Pass an integer to specify a
|
||||
different number of updates that may be processed concurrently.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._concurrent_updates = concurrent_updates
|
||||
return self
|
||||
|
||||
def job_queue(
|
||||
self: 'ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]',
|
||||
job_queue: InJQ,
|
||||
) -> 'ApplicationBuilder[BT, CCT, UD, CD, BD, InJQ]':
|
||||
"""Sets a :class:`telegram.ext.JobQueue` instance for
|
||||
:attr:`telegram.ext.Application.job_queue`. If not called, a job queue will be
|
||||
instantiated.
|
||||
|
||||
.. seealso:: `JobQueue <https://github.com/python-telegram-bot/python-telegram-bot/wiki\
|
||||
/Extensions-%E2%80%93-JobQueue>`_, `timerbot.py <https://github.com\
|
||||
/python-telegram-bot/python-telegram-bot/tree/master/examples#timerbotpy>`_
|
||||
|
||||
Note:
|
||||
* :meth:`telegram.ext.JobQueue.set_application` will be called automatically by
|
||||
:meth:`build`.
|
||||
* The job queue will be automatically started and stopped by
|
||||
:meth:`telegram.ext.Application.start` and :meth:`telegram.ext.Application.stop`,
|
||||
respectively.
|
||||
* When passing :obj:`None`,
|
||||
:attr:`telegram.ext.ConversationHandler.conversation_timeout` can not be used, as
|
||||
this uses :attr:`telegram.ext.Application.job_queue` internally.
|
||||
|
||||
Args:
|
||||
job_queue (:class:`telegram.ext.JobQueue`): The job queue. Pass :obj:`None` if you
|
||||
don't want to use a job queue.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._job_queue = job_queue
|
||||
return self # type: ignore[return-value]
|
||||
|
||||
def persistence(self: BuilderType, persistence: 'BasePersistence') -> BuilderType:
|
||||
"""Sets a :class:`telegram.ext.BasePersistence` instance for
|
||||
:attr:`telegram.ext.Application.persistence`.
|
||||
|
||||
Note:
|
||||
When using a persistence, note that all
|
||||
data stored in :attr:`context.user_data <telegram.ext.CallbackContext.user_data>`,
|
||||
:attr:`context.chat_data <telegram.ext.CallbackContext.chat_data>`,
|
||||
:attr:`context.bot_data <telegram.ext.CallbackContext.bot_data>` and
|
||||
in :attr:`telegram.ext.ExtBot.callback_data_cache` must be copyable with
|
||||
:func:`copy.deepcopy`. This is due to the data being deep copied before handing it over
|
||||
to the persistence in order to avoid race conditions.
|
||||
|
||||
.. seealso:: `Making your bot persistent <https://github.com/python-telegram-bot\
|
||||
/python-telegram-bot/wiki/Making-your-bot-persistent>`_,
|
||||
`persistentconversationbot.py <https://github.com/python-telegram-bot\
|
||||
/python-telegram-bot/tree/master/examples#persistentconversationbotpy>`_
|
||||
|
||||
Warning:
|
||||
If a :class:`telegram.ext.ContextTypes` instance is set via :meth:`context_types`,
|
||||
the persistence instance must use the same types!
|
||||
|
||||
Args:
|
||||
persistence (:class:`telegram.ext.BasePersistence`): The persistence instance.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._persistence = persistence
|
||||
return self
|
||||
|
||||
def context_types(
|
||||
self: 'ApplicationBuilder[BT, CCT, UD, CD, BD, JQ]',
|
||||
context_types: 'ContextTypes[InCCT, InUD, InCD, InBD]',
|
||||
) -> 'ApplicationBuilder[BT, InCCT, InUD, InCD, InBD, JQ]':
|
||||
"""Sets a :class:`telegram.ext.ContextTypes` instance for
|
||||
:attr:`telegram.ext.Application.context_types`.
|
||||
|
||||
.. seealso:: `contexttypesbot.py <https://github.com/python-telegram-bot\
|
||||
/python-telegram-bot/tree/master/examples#contexttypesbotpy>`_
|
||||
|
||||
Args:
|
||||
context_types (:class:`telegram.ext.ContextTypes`): The context types.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
self._context_types = context_types
|
||||
return self # type: ignore[return-value]
|
||||
|
||||
def updater(self: BuilderType, updater: Optional[Updater]) -> BuilderType:
|
||||
"""Sets a :class:`telegram.ext.Updater` instance for
|
||||
:attr:`telegram.ext.Application.updater`. The :attr:`telegram.ext.Updater.bot` and
|
||||
:attr:`telegram.ext.Updater.update_queue` will be used for
|
||||
:attr:`telegram.ext.Application.bot` and :attr:`telegram.ext.Application.update_queue`,
|
||||
respectively.
|
||||
|
||||
Args:
|
||||
updater (:class:`telegram.ext.Updater` | :obj:`None`): The updater instance or
|
||||
:obj:`None` if no updater should be used.
|
||||
|
||||
Returns:
|
||||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
if updater is None:
|
||||
self._updater = updater
|
||||
return self
|
||||
|
||||
for attr, error in (
|
||||
(self._bot, 'bot instance'),
|
||||
(self._update_queue, 'update_queue'),
|
||||
):
|
||||
if not isinstance(attr, DefaultValue):
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('updater', error))
|
||||
|
||||
for attr_name, error in _BOT_CHECKS:
|
||||
if not isinstance(getattr(self, f'_{attr_name}'), DefaultValue):
|
||||
raise RuntimeError(_TWO_ARGS_REQ.format('updater', error))
|
||||
|
||||
self._updater = updater
|
||||
return self
|
||||
|
||||
|
||||
InitApplicationBuilder = ( # This is defined all the way down here so that its type is inferred
|
||||
ApplicationBuilder[ # by Pylance correctly.
|
||||
ExtBot,
|
||||
CallbackContext.DEFAULT_TYPE,
|
||||
Dict,
|
||||
Dict,
|
||||
Dict,
|
||||
JobQueue,
|
||||
]
|
||||
)
|
|
@ -18,12 +18,18 @@
|
|||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the BasePersistence class."""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Optional, Tuple, Generic, NamedTuple
|
||||
from typing import (
|
||||
Dict,
|
||||
Optional,
|
||||
Generic,
|
||||
NamedTuple,
|
||||
NoReturn,
|
||||
)
|
||||
|
||||
from telegram import Bot
|
||||
from telegram.ext import ExtBot
|
||||
|
||||
from telegram.ext._utils.types import UD, CD, BD, ConversationDict, CDCData
|
||||
from telegram.ext._utils.types import UD, CD, BD, ConversationDict, CDCData, ConversationKey
|
||||
|
||||
|
||||
class PersistenceInput(NamedTuple): # skipcq: PYL-E0239
|
||||
|
@ -59,8 +65,8 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
|
|||
|
||||
Attention:
|
||||
The interface provided by this class is intended to be accessed exclusively by
|
||||
:class:`~telegram.ext.Dispatcher`. Calling any of the methods below manually might
|
||||
interfere with the integration of persistence into :class:`~telegram.ext.Dispatcher`.
|
||||
:class:`~telegram.ext.Application`. Calling any of the methods below manually might
|
||||
interfere with the integration of persistence into :class:`~telegram.ext.Application`.
|
||||
|
||||
All relevant methods must be overwritten. This includes:
|
||||
|
||||
|
@ -108,6 +114,12 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
|
|||
store_data (:class:`PersistenceInput`, optional): Specifies which kinds of data will be
|
||||
saved by this persistence instance. By default, all available kinds of data will be
|
||||
saved.
|
||||
update_interval (:obj:`int` | :obj:`float`, optional): The
|
||||
:class:`~telegram.ext.Application` will update
|
||||
the persistence in regular intervals. This parameter specifies the time (in seconds) to
|
||||
wait between two consecutive runs of updating the persistence. Defaults to 60 seconds.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
||||
Attributes:
|
||||
store_data (:class:`PersistenceInput`): Specifies which kinds of data will be saved by this
|
||||
|
@ -115,18 +127,46 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
|
|||
bot (:class:`telegram.Bot`): The bot associated with the persistence.
|
||||
"""
|
||||
|
||||
__slots__ = ('bot', 'store_data')
|
||||
__slots__ = (
|
||||
'bot',
|
||||
'store_data',
|
||||
'_update_interval',
|
||||
)
|
||||
|
||||
def __init__(self, store_data: PersistenceInput = None):
|
||||
def __init__(
|
||||
self,
|
||||
store_data: PersistenceInput = None,
|
||||
update_interval: float = 60,
|
||||
):
|
||||
self.store_data = store_data or PersistenceInput()
|
||||
self._update_interval = update_interval
|
||||
|
||||
self.bot: Bot = None # type: ignore[assignment]
|
||||
|
||||
@property
|
||||
def update_interval(self) -> float:
|
||||
""":obj:`float`: Time (in seconds) that the :class:`~telegram.ext.Application`
|
||||
will wait between two consecutive runs of updating the persistence.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
"""
|
||||
return self._update_interval
|
||||
|
||||
@update_interval.setter
|
||||
def update_interval(self, value: object) -> NoReturn: # pylint: disable=no-self-use
|
||||
raise AttributeError(
|
||||
"You can not assign a new value to update_interval after initialization."
|
||||
)
|
||||
|
||||
def set_bot(self, bot: Bot) -> None:
|
||||
"""Set the Bot to be used by this persistence instance.
|
||||
|
||||
Args:
|
||||
bot (:class:`telegram.Bot`): The bot.
|
||||
|
||||
Raises:
|
||||
:exc:`TypeError`: If :attr:`PersistenceInput.callback_data` is :obj:`True` and the
|
||||
:paramref:`bot` is not an instance of :class:`telegram.ext.ExtBot`.
|
||||
"""
|
||||
if self.store_data.callback_data and not isinstance(bot, ExtBot):
|
||||
raise TypeError('callback_data can only be stored when using telegram.ext.ExtBot.')
|
||||
|
@ -134,8 +174,8 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
|
|||
self.bot = bot
|
||||
|
||||
@abstractmethod
|
||||
def get_user_data(self) -> Dict[int, UD]:
|
||||
"""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
|
||||
async def get_user_data(self) -> Dict[int, UD]:
|
||||
"""Will be called by :class:`telegram.ext.Application` upon creation with a
|
||||
persistence object. It should return the ``user_data`` if stored, or an empty
|
||||
:obj:`dict`. In the latter case, the dictionary should produce values
|
||||
corresponding to one of the following:
|
||||
|
@ -153,8 +193,8 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
|
|||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_chat_data(self) -> Dict[int, CD]:
|
||||
"""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
|
||||
async def get_chat_data(self) -> Dict[int, CD]:
|
||||
"""Will be called by :class:`telegram.ext.Application` upon creation with a
|
||||
persistence object. It should return the ``chat_data`` if stored, or an empty
|
||||
:obj:`dict`. In the latter case, the dictionary should produce values
|
||||
corresponding to one of the following:
|
||||
|
@ -172,8 +212,8 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
|
|||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_bot_data(self) -> BD:
|
||||
"""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
|
||||
async def get_bot_data(self) -> BD:
|
||||
"""Will be called by :class:`telegram.ext.Application` upon creation with a
|
||||
persistence object. It should return the ``bot_data`` if stored, or an empty
|
||||
:obj:`dict`. In the latter case, the :obj:`dict` should produce values
|
||||
corresponding to one of the following:
|
||||
|
@ -188,27 +228,28 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
|
|||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_callback_data(self) -> Optional[CDCData]:
|
||||
"""Will be called by :class:`telegram.ext.Dispatcher` upon creation with a
|
||||
async def get_callback_data(self) -> Optional[CDCData]:
|
||||
"""Will be called by :class:`telegram.ext.Application` upon creation with a
|
||||
persistence object. If callback data was stored, it should be returned.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
Changed this method into an ``@abstractmethod``.
|
||||
Changed this method into an :external:func:`~abc.abstractmethod`.
|
||||
|
||||
Returns:
|
||||
Optional[Tuple[List[Tuple[:obj:`str`, :obj:`float`, \
|
||||
Dict[:obj:`str`, :class:`object`]]], Dict[:obj:`str`, :obj:`str`]]]:
|
||||
The restored meta data or :obj:`None`, if no data was stored.
|
||||
The restored metadata or :obj:`None`, if no data was stored.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_conversations(self, name: str) -> ConversationDict:
|
||||
"""Will be called by :class:`telegram.ext.Dispatcher` when a
|
||||
async def get_conversations(self, name: str) -> ConversationDict:
|
||||
"""Will be called by :class:`telegram.ext.Application` 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`
|
||||
It should return the conversations for the handler with :paramref:`name` or an empty
|
||||
:obj:`dict`.
|
||||
|
||||
Args:
|
||||
name (:obj:`str`): The handlers name.
|
||||
|
@ -218,8 +259,8 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
|
|||
"""
|
||||
|
||||
@abstractmethod
|
||||
def update_conversation(
|
||||
self, name: str, key: Tuple[int, ...], new_state: Optional[object]
|
||||
async def update_conversation(
|
||||
self, name: str, key: ConversationKey, new_state: Optional[object]
|
||||
) -> None:
|
||||
"""Will be called when a :class:`telegram.ext.ConversationHandler` changes states.
|
||||
This allows the storage of the new state in the persistence.
|
||||
|
@ -227,50 +268,50 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
|
|||
Args:
|
||||
name (:obj:`str`): The handler's name.
|
||||
key (:obj:`tuple`): The key the state is changed for.
|
||||
new_state (:obj:`tuple` | :class:`object`): The new state for the given key.
|
||||
new_state (:class:`object`): The new state for the given key.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def update_user_data(self, user_id: int, data: UD) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
|
||||
async def update_user_data(self, user_id: int, data: UD) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Application` after a handler has
|
||||
handled an update.
|
||||
|
||||
Args:
|
||||
user_id (:obj:`int`): The user the data might have been changed for.
|
||||
data (:obj:`dict` | :attr:`telegram.ext.ContextTypes.user_data`):
|
||||
The :attr:`telegram.ext.Dispatcher.user_data` ``[user_id]``.
|
||||
The :attr:`telegram.ext.Application.user_data` ``[user_id]``.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def update_chat_data(self, chat_id: int, data: CD) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
|
||||
async def update_chat_data(self, chat_id: int, data: CD) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Application` after a handler has
|
||||
handled an update.
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int`): The chat the data might have been changed for.
|
||||
data (:obj:`dict` | :attr:`telegram.ext.ContextTypes.chat_data`):
|
||||
The :attr:`telegram.ext.Dispatcher.chat_data` ``[chat_id]``.
|
||||
The :attr:`telegram.ext.Application.chat_data` ``[chat_id]``.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def update_bot_data(self, data: BD) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
|
||||
async def update_bot_data(self, data: BD) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Application` after a handler has
|
||||
handled an update.
|
||||
|
||||
Args:
|
||||
data (:obj:`dict` | :attr:`telegram.ext.ContextTypes.bot_data`):
|
||||
The :attr:`telegram.ext.Dispatcher.bot_data`.
|
||||
The :attr:`telegram.ext.Application.bot_data`.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def update_callback_data(self, data: CDCData) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Dispatcher` after a handler has
|
||||
async def update_callback_data(self, data: CDCData) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Application` after a handler has
|
||||
handled an update.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
Changed this method into an ``@abstractmethod``.
|
||||
Changed this method into an :external:func:`~abc.abstractmethod`.
|
||||
|
||||
Args:
|
||||
data (Optional[Tuple[List[Tuple[:obj:`str`, :obj:`float`, \
|
||||
|
@ -279,9 +320,9 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
|
|||
"""
|
||||
|
||||
@abstractmethod
|
||||
def drop_chat_data(self, chat_id: int) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Dispatcher`, when using
|
||||
:meth:`~telegram.ext.Dispatcher.drop_chat_data`.
|
||||
async def drop_chat_data(self, chat_id: int) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Application`, when using
|
||||
:meth:`~telegram.ext.Application.drop_chat_data`.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
||||
|
@ -290,9 +331,9 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
|
|||
"""
|
||||
|
||||
@abstractmethod
|
||||
def drop_user_data(self, user_id: int) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Dispatcher`, when using
|
||||
:meth:`~telegram.ext.Dispatcher.drop_user_data`.
|
||||
async def drop_user_data(self, user_id: int) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Application`, when using
|
||||
:meth:`~telegram.ext.Application.drop_user_data`.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
||||
|
@ -301,51 +342,51 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
|
|||
"""
|
||||
|
||||
@abstractmethod
|
||||
def refresh_user_data(self, user_id: int, user_data: UD) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Dispatcher` before passing the
|
||||
:attr:`~telegram.ext.Dispatcher.user_data` to a callback. Can be used to update data stored
|
||||
in :attr:`~telegram.ext.Dispatcher.user_data` from an external source.
|
||||
async def refresh_user_data(self, user_id: int, user_data: UD) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Application` before passing the
|
||||
:attr:`~telegram.ext.Application.user_data` to a callback. Can be used to update data
|
||||
stored in :attr:`~telegram.ext.Application.user_data` from an external source.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
Changed this method into an ``@abstractmethod``.
|
||||
Changed this method into an :external:func:`~abc.abstractmethod`.
|
||||
|
||||
Args:
|
||||
user_id (:obj:`int`): The user ID this :attr:`~telegram.ext.Dispatcher.user_data` is
|
||||
user_id (:obj:`int`): The user ID this :attr:`~telegram.ext.Application.user_data` is
|
||||
associated with.
|
||||
user_data (:obj:`dict` | :attr:`telegram.ext.ContextTypes.user_data`):
|
||||
The ``user_data`` of a single user.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Dispatcher` before passing the
|
||||
:attr:`~telegram.ext.Dispatcher.chat_data` to a callback. Can be used to update data stored
|
||||
in :attr:`~telegram.ext.Dispatcher.chat_data` from an external source.
|
||||
async def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Application` before passing the
|
||||
:attr:`~telegram.ext.Application.chat_data` to a callback. Can be used to update data
|
||||
stored in :attr:`~telegram.ext.Application.chat_data` from an external source.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
Changed this method into an ``@abstractmethod``.
|
||||
Changed this method into an :external:func:`~abc.abstractmethod`.
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int`): The chat ID this :attr:`~telegram.ext.Dispatcher.chat_data` is
|
||||
chat_id (:obj:`int`): The chat ID this :attr:`~telegram.ext.Application.chat_data` is
|
||||
associated with.
|
||||
chat_data (:obj:`dict` | :attr:`telegram.ext.ContextTypes.chat_data`):
|
||||
The ``chat_data`` of a single chat.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def refresh_bot_data(self, bot_data: BD) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Dispatcher` before passing the
|
||||
:attr:`~telegram.ext.Dispatcher.bot_data` to a callback. Can be used to update data stored
|
||||
in :attr:`~telegram.ext.Dispatcher.bot_data` from an external source.
|
||||
async def refresh_bot_data(self, bot_data: BD) -> None:
|
||||
"""Will be called by the :class:`telegram.ext.Application` before passing the
|
||||
:attr:`~telegram.ext.Application.bot_data` to a callback. Can be used to update data stored
|
||||
in :attr:`~telegram.ext.Application.bot_data` from an external source.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
Changed this method into an ``@abstractmethod``.
|
||||
Changed this method into an :external:func:`~abc.abstractmethod`.
|
||||
|
||||
Args:
|
||||
bot_data (:obj:`dict` | :attr:`telegram.ext.ContextTypes.bot_data`):
|
||||
|
@ -353,10 +394,10 @@ class BasePersistence(Generic[UD, CD, BD], ABC):
|
|||
"""
|
||||
|
||||
@abstractmethod
|
||||
def flush(self) -> None:
|
||||
"""Will be called by :class:`telegram.ext.Updater` upon receiving a stop signal. Gives the
|
||||
async def flush(self) -> None:
|
||||
"""Will be called by :meth:`telegram.ext.Application.stop`. Gives the
|
||||
persistence a chance to finish up saving or close a database connection gracefully.
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
Changed this method into an ``@abstractmethod``.
|
||||
Changed this method into an :external:func:`~abc.abstractmethod`.
|
||||
"""
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -18,7 +18,7 @@
|
|||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
# pylint: disable=no-self-use
|
||||
"""This module contains the CallbackContext class."""
|
||||
from queue import Queue
|
||||
from asyncio import Queue
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Dict,
|
||||
|
@ -27,17 +27,17 @@ from typing import (
|
|||
NoReturn,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
Generic,
|
||||
Type,
|
||||
Coroutine,
|
||||
)
|
||||
|
||||
from telegram import Update, CallbackQuery
|
||||
from telegram.ext import ExtBot
|
||||
from telegram.ext._utils.types import UD, CD, BD, BT, JQ, PT # pylint: disable=unused-import
|
||||
from telegram.ext._utils.types import UD, CD, BD, BT, JQ # pylint: disable=unused-import
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Dispatcher, Job, JobQueue
|
||||
from telegram.ext import Application, Job, JobQueue
|
||||
from telegram.ext._utils.types import CCT
|
||||
|
||||
_STORING_DATA_WIKI = (
|
||||
|
@ -49,46 +49,43 @@ _STORING_DATA_WIKI = (
|
|||
class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||
"""
|
||||
This is a context object passed to the callback called by :class:`telegram.ext.Handler`
|
||||
or by the :class:`telegram.ext.Dispatcher` in an error handler added by
|
||||
:attr:`telegram.ext.Dispatcher.add_error_handler` or to the callback of a
|
||||
or by the :class:`telegram.ext.Application` in an error handler added by
|
||||
:attr:`telegram.ext.Application.add_error_handler` or to the callback of a
|
||||
:class:`telegram.ext.Job`.
|
||||
|
||||
Note:
|
||||
:class:`telegram.ext.Dispatcher` will create a single context for an entire update. This
|
||||
:class:`telegram.ext.Application` will create a single context for an entire update. This
|
||||
means that if you got 2 handlers in different groups and they both get called, they will
|
||||
get passed the same `CallbackContext` object (of course with proper attributes like
|
||||
`.matches` differing). This allows you to add custom attributes in a lower handler group
|
||||
callback, and then subsequently access those attributes in a higher handler group callback.
|
||||
Note that the attributes on `CallbackContext` might change in the future, so make sure to
|
||||
use a fairly unique name for the attributes.
|
||||
receive the same :class:`CallbackContext` object (of course with proper attributes like
|
||||
:attr:`matches` differing). This allows you to add custom attributes in a lower handler
|
||||
group callback, and then subsequently access those attributes in a higher handler group
|
||||
callback. Note that the attributes on :class:`CallbackContext` might change in the future,
|
||||
so make sure to use a fairly unique name for the attributes.
|
||||
|
||||
Warning:
|
||||
Do not combine custom attributes and ``@run_async``/
|
||||
:func:`telegram.ext.Dispatcher.run_async`. Due to how ``run_async`` works, it will
|
||||
almost certainly execute the callbacks for an update out of order, and the attributes
|
||||
that you think you added will not be present.
|
||||
Do not combine custom attributes with :paramref:`telegram.ext.Handler.block` set to
|
||||
:obj:`False` or :paramref:`telegram.ext.Application.concurrent_updates` set to
|
||||
:obj:`True`. Due to how those work, it will almost certainly execute the callbacks for an
|
||||
update out of order, and the attributes that you think you added will not be present.
|
||||
|
||||
Args:
|
||||
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this context.
|
||||
application (:class:`telegram.ext.Application`): The application associated with this
|
||||
context.
|
||||
|
||||
Attributes:
|
||||
coroutine (:term:`coroutine function`): Optional. Only present in error handlers if the
|
||||
error was caused by a coroutine run with :meth:`Application.create_task` or a handler
|
||||
callback with :attr:`block=False <Handler.block>`.
|
||||
matches (List[:meth:`re.Match <re.Match.expand>`]): Optional. If the associated update
|
||||
originated from
|
||||
a :class:`filters.Regex`, this will contain a list of match objects for every pattern
|
||||
where ``re.search(pattern, string)`` returned a match. Note that filters short circuit,
|
||||
so combined regex filters will not always be evaluated.
|
||||
originated from a :class:`filters.Regex`, this will contain a list of match objects for
|
||||
every pattern where ``re.search(pattern, string)`` returned a match. Note that filters
|
||||
short circuit, so combined regex filters will not always be evaluated.
|
||||
args (List[:obj:`str`]): Optional. Arguments passed to a command if the associated update
|
||||
is handled by :class:`telegram.ext.CommandHandler`, :class:`telegram.ext.PrefixHandler`
|
||||
or :class:`telegram.ext.StringCommandHandler`. It contains a list of the words in the
|
||||
text after the command, using any whitespace string as a delimiter.
|
||||
error (:obj:`Exception`): Optional. The error that was raised. Only present when passed
|
||||
to a error handler registered with :attr:`telegram.ext.Dispatcher.add_error_handler`.
|
||||
async_args (List[:obj:`object`]): Optional. Positional arguments of the function that
|
||||
raised the error. Only present when the raising function was run asynchronously using
|
||||
:meth:`telegram.ext.Dispatcher.run_async`.
|
||||
async_kwargs (Dict[:obj:`str`, :obj:`object`]): Optional. Keyword arguments of the function
|
||||
that raised the error. Only present when the raising function was run asynchronously
|
||||
using :meth:`telegram.ext.Dispatcher.run_async`.
|
||||
error (:exc:`Exception`): Optional. The error that was raised. Only present when passed
|
||||
to an error handler registered with :attr:`telegram.ext.Application.add_error_handler`.
|
||||
job (:class:`telegram.ext.Job`): Optional. The job which originated this callback.
|
||||
Only present when passed to the callback of :class:`telegram.ext.Job` or in error
|
||||
handlers if the error is caused by a job.
|
||||
|
@ -112,51 +109,45 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
|||
Example:
|
||||
.. code:: python
|
||||
|
||||
def callback(update: Update, context: CallbackContext.DEFAULT_TYPE):
|
||||
async def callback(update: Update, context: CallbackContext.DEFAULT_TYPE):
|
||||
...
|
||||
|
||||
.. versionadded: 14.0
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'_dispatcher',
|
||||
'_application',
|
||||
'_chat_id_and_data',
|
||||
'_user_id_and_data',
|
||||
'args',
|
||||
'matches',
|
||||
'error',
|
||||
'job',
|
||||
'async_args',
|
||||
'async_kwargs',
|
||||
'coroutine',
|
||||
'__dict__',
|
||||
)
|
||||
|
||||
def __init__(self: 'CCT', dispatcher: 'Dispatcher[BT, CCT, UD, CD, BD, JQ, PT]'):
|
||||
"""
|
||||
Args:
|
||||
dispatcher (:class:`telegram.ext.Dispatcher`):
|
||||
"""
|
||||
self._dispatcher = dispatcher
|
||||
def __init__(self: 'CCT', application: 'Application[BT, CCT, UD, CD, BD, JQ]'):
|
||||
self._application = application
|
||||
self._chat_id_and_data: Optional[Tuple[int, CD]] = None
|
||||
self._user_id_and_data: Optional[Tuple[int, UD]] = None
|
||||
self.args: Optional[List[str]] = None
|
||||
self.matches: Optional[List[Match]] = None
|
||||
self.error: Optional[Exception] = None
|
||||
self.job: Optional['Job'] = None
|
||||
self.async_args: Optional[Union[List, Tuple]] = None
|
||||
self.async_kwargs: Optional[Dict[str, object]] = None
|
||||
self.coroutine: Optional[Coroutine] = None
|
||||
|
||||
@property
|
||||
def dispatcher(self) -> 'Dispatcher[BT, CCT, UD, CD, BD, JQ, PT]':
|
||||
""":class:`telegram.ext.Dispatcher`: The dispatcher associated with this context."""
|
||||
return self._dispatcher
|
||||
def application(self) -> 'Application[BT, CCT, UD, CD, BD, JQ]':
|
||||
""":class:`telegram.ext.Application`: The application associated with this context."""
|
||||
return self._application
|
||||
|
||||
@property
|
||||
def bot_data(self) -> BD:
|
||||
""":obj:`dict`: Optional. A dict that can be used to keep any data in. For each
|
||||
update it will be the same ``dict``.
|
||||
""":obj:`ContextTypes.bot_data`: Optional. An object that can be used to keep any data in.
|
||||
For each update it will be the same :attr:`ContextTypes.bot_data`. Defaults to :obj:`dict`.
|
||||
"""
|
||||
return self.dispatcher.bot_data
|
||||
return self.application.bot_data
|
||||
|
||||
@bot_data.setter
|
||||
def bot_data(self, value: object) -> NoReturn:
|
||||
|
@ -166,8 +157,9 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
|||
|
||||
@property
|
||||
def chat_data(self) -> Optional[CD]:
|
||||
""":obj:`dict`: Optional. A dict that can be used to keep any data in. For each
|
||||
update from the same chat id it will be the same ``dict``.
|
||||
""":obj:`ContextTypes.chat_data`: Optional. An object that can be used to keep any data in.
|
||||
For each update from the same chat id it will be the same :obj:`ContextTypes.chat_data`.
|
||||
Defaults to :obj:`dict`.
|
||||
|
||||
Warning:
|
||||
When a group chat migrates to a supergroup, its chat id will change and the
|
||||
|
@ -187,8 +179,9 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
|||
|
||||
@property
|
||||
def user_data(self) -> Optional[UD]:
|
||||
""":obj:`dict`: Optional. A dict that can be used to keep any data in. For each
|
||||
update from the same user it will be the same ``dict``.
|
||||
""":obj:`ContextTypes.user_data`: Optional. An object that can be used to keep any data in.
|
||||
For each update from the same user it will be the same :obj:`ContextTypes.user_data`.
|
||||
Defaults to :obj:`dict`.
|
||||
"""
|
||||
if self._user_id_and_data:
|
||||
return self._user_id_and_data[1]
|
||||
|
@ -200,28 +193,31 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
|||
f"You can not assign a new value to user_data, see {_STORING_DATA_WIKI}"
|
||||
)
|
||||
|
||||
def refresh_data(self) -> None:
|
||||
"""If :attr:`dispatcher` uses persistence, calls
|
||||
async def refresh_data(self) -> None:
|
||||
"""If :attr:`application` uses persistence, calls
|
||||
:meth:`telegram.ext.BasePersistence.refresh_bot_data` on :attr:`bot_data`,
|
||||
:meth:`telegram.ext.BasePersistence.refresh_chat_data` on :attr:`chat_data` and
|
||||
:meth:`telegram.ext.BasePersistence.refresh_user_data` on :attr:`user_data`, if
|
||||
appropriate.
|
||||
|
||||
Will be called by :meth:`telegram.ext.Application.process_update` and
|
||||
:meth:`telegram.ext.Job.run`.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
"""
|
||||
if self.dispatcher.persistence:
|
||||
if self.dispatcher.persistence.store_data.bot_data:
|
||||
self.dispatcher.persistence.refresh_bot_data(self.bot_data)
|
||||
if self.application.persistence:
|
||||
if self.application.persistence.store_data.bot_data:
|
||||
await self.application.persistence.refresh_bot_data(self.bot_data)
|
||||
if (
|
||||
self.dispatcher.persistence.store_data.chat_data
|
||||
self.application.persistence.store_data.chat_data
|
||||
and self._chat_id_and_data is not None
|
||||
):
|
||||
self.dispatcher.persistence.refresh_chat_data(*self._chat_id_and_data)
|
||||
await self.application.persistence.refresh_chat_data(*self._chat_id_and_data)
|
||||
if (
|
||||
self.dispatcher.persistence.store_data.user_data
|
||||
self.application.persistence.store_data.user_data
|
||||
and self._user_id_and_data is not None
|
||||
):
|
||||
self.dispatcher.persistence.refresh_user_data(*self._user_id_and_data)
|
||||
await self.application.persistence.refresh_user_data(*self._user_id_and_data)
|
||||
|
||||
def drop_callback_data(self, callback_query: CallbackQuery) -> None:
|
||||
"""
|
||||
|
@ -231,15 +227,14 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
|||
|
||||
Note:
|
||||
Will *not* raise exceptions in case the data is not found in the cache.
|
||||
*Will* raise :class:`KeyError` in case the callback query can not be found in the
|
||||
cache.
|
||||
*Will* raise :exc:`KeyError` in case the callback query can not be found in the cache.
|
||||
|
||||
Args:
|
||||
callback_query (:class:`telegram.CallbackQuery`): The callback query.
|
||||
|
||||
Raises:
|
||||
KeyError | RuntimeError: :class:`KeyError`, if the callback query can not be found in
|
||||
the cache and :class:`RuntimeError`, if the bot doesn't allow for arbitrary
|
||||
KeyError | RuntimeError: :exc:`KeyError`, if the callback query can not be found in
|
||||
the cache and :exc:`RuntimeError`, if the bot doesn't allow for arbitrary
|
||||
callback data.
|
||||
"""
|
||||
if isinstance(self.bot, ExtBot):
|
||||
|
@ -256,29 +251,25 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
|||
cls: Type['CCT'],
|
||||
update: object,
|
||||
error: Exception,
|
||||
dispatcher: 'Dispatcher[BT, CCT, UD, CD, BD, JQ, PT]',
|
||||
async_args: Union[List, Tuple] = None,
|
||||
async_kwargs: Dict[str, object] = None,
|
||||
application: 'Application[BT, CCT, UD, CD, BD, JQ]',
|
||||
job: 'Job' = None,
|
||||
coroutine: Coroutine = None,
|
||||
) -> 'CCT':
|
||||
"""
|
||||
Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the error
|
||||
handlers.
|
||||
|
||||
.. seealso:: :meth:`telegram.ext.Dispatcher.add_error_handler`
|
||||
.. seealso:: :meth:`telegram.ext.Application.add_error_handler`
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
Removed arguments ``async_args`` and ``async_kwargs``.
|
||||
|
||||
Args:
|
||||
update (:obj:`object` | :class:`telegram.Update`): The update associated with the
|
||||
error. May be :obj:`None`, e.g. for errors in job callbacks.
|
||||
error (:obj:`Exception`): The error.
|
||||
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this
|
||||
application (:class:`telegram.ext.Application`): The application associated with this
|
||||
context.
|
||||
async_args (List[:obj:`object`], optional): Positional arguments of the function that
|
||||
raised the error. Pass only when the raising function was run asynchronously using
|
||||
:meth:`telegram.ext.Dispatcher.run_async`.
|
||||
async_kwargs (Dict[:obj:`str`, :obj:`object`], optional): Keyword arguments of the
|
||||
function that raised the error. Pass only when the raising function was run
|
||||
asynchronously using :meth:`telegram.ext.Dispatcher.run_async`.
|
||||
job (:class:`telegram.ext.Job`, optional): The job associated with the error.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
@ -286,32 +277,33 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
|||
Returns:
|
||||
:class:`telegram.ext.CallbackContext`
|
||||
"""
|
||||
self = cls.from_update(update, dispatcher)
|
||||
self = cls.from_update(update, application)
|
||||
self.error = error
|
||||
self.async_args = async_args
|
||||
self.async_kwargs = async_kwargs
|
||||
self.coroutine = coroutine
|
||||
self.job = job
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_update(
|
||||
cls: Type['CCT'], update: object, dispatcher: 'Dispatcher[BT, CCT, UD, CD, BD, JQ, PT]'
|
||||
cls: Type['CCT'],
|
||||
update: object,
|
||||
application: 'Application[BT, CCT, UD, CD, BD, JQ]',
|
||||
) -> 'CCT':
|
||||
"""
|
||||
Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the
|
||||
handlers.
|
||||
|
||||
.. seealso:: :meth:`telegram.ext.Dispatcher.add_handler`
|
||||
.. seealso:: :meth:`telegram.ext.Application.add_handler`
|
||||
|
||||
Args:
|
||||
update (:obj:`object` | :class:`telegram.Update`): The update.
|
||||
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this
|
||||
application (:class:`telegram.ext.Application`): The application associated with this
|
||||
context.
|
||||
|
||||
Returns:
|
||||
:class:`telegram.ext.CallbackContext`
|
||||
"""
|
||||
self = cls(dispatcher) # type: ignore[arg-type]
|
||||
self = cls(application) # type: ignore[arg-type]
|
||||
|
||||
if update is not None and isinstance(update, Update):
|
||||
chat = update.effective_chat
|
||||
|
@ -320,18 +312,20 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
|||
if chat:
|
||||
self._chat_id_and_data = (
|
||||
chat.id,
|
||||
dispatcher.chat_data[chat.id], # pylint: disable=protected-access
|
||||
application.chat_data[chat.id], # pylint: disable=protected-access
|
||||
)
|
||||
if user:
|
||||
self._user_id_and_data = (
|
||||
user.id,
|
||||
dispatcher.user_data[user.id], # pylint: disable=protected-access
|
||||
application.user_data[user.id], # pylint: disable=protected-access
|
||||
)
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_job(
|
||||
cls: Type['CCT'], job: 'Job', dispatcher: 'Dispatcher[BT, CCT, UD, CD, BD, JQ, PT]'
|
||||
cls: Type['CCT'],
|
||||
job: 'Job',
|
||||
application: 'Application[BT, CCT, UD, CD, BD, JQ]',
|
||||
) -> 'CCT':
|
||||
"""
|
||||
Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to a
|
||||
|
@ -341,14 +335,25 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
|||
|
||||
Args:
|
||||
job (:class:`telegram.ext.Job`): The job.
|
||||
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher associated with this
|
||||
application (:class:`telegram.ext.Application`): The application associated with this
|
||||
context.
|
||||
|
||||
Returns:
|
||||
:class:`telegram.ext.CallbackContext`
|
||||
"""
|
||||
self = cls(dispatcher) # type: ignore[arg-type]
|
||||
self = cls(application) # type: ignore[arg-type]
|
||||
self.job = job
|
||||
|
||||
if job.chat_id:
|
||||
self._chat_id_and_data = (
|
||||
job.chat_id,
|
||||
application.chat_data[job.chat_id], # pylint: disable=protected-access
|
||||
)
|
||||
if job.user_id:
|
||||
self._user_id_and_data = (
|
||||
job.user_id,
|
||||
application.user_data[job.user_id], # pylint: disable=protected-access
|
||||
)
|
||||
return self
|
||||
|
||||
def update(self, data: Dict[str, object]) -> None:
|
||||
|
@ -363,34 +368,33 @@ class CallbackContext(Generic[BT, UD, CD, BD]):
|
|||
@property
|
||||
def bot(self) -> BT:
|
||||
""":class:`telegram.Bot`: The bot associated with this context."""
|
||||
return self._dispatcher.bot
|
||||
return self._application.bot
|
||||
|
||||
@property
|
||||
def job_queue(self) -> Optional['JobQueue']:
|
||||
"""
|
||||
:class:`telegram.ext.JobQueue`: The ``JobQueue`` used by the
|
||||
:class:`telegram.ext.Dispatcher` and (usually) the :class:`telegram.ext.Updater`
|
||||
associated with this context.
|
||||
:class:`telegram.ext.JobQueue`: The :class:`JobQueue` used by the
|
||||
:class:`telegram.ext.Application`.
|
||||
|
||||
"""
|
||||
return self._dispatcher.job_queue
|
||||
return self._application.job_queue
|
||||
|
||||
@property
|
||||
def update_queue(self) -> Queue:
|
||||
def update_queue(self) -> 'Queue[object]':
|
||||
"""
|
||||
:class:`queue.Queue`: The ``Queue`` instance used by the
|
||||
:class:`telegram.ext.Dispatcher` and (usually) the :class:`telegram.ext.Updater`
|
||||
:class:`asyncio.Queue`: The :class:`asyncio.Queue` instance used by the
|
||||
:class:`telegram.ext.Application` and (usually) the :class:`telegram.ext.Updater`
|
||||
associated with this context.
|
||||
|
||||
"""
|
||||
return self._dispatcher.update_queue
|
||||
return self._application.update_queue
|
||||
|
||||
@property
|
||||
def match(self) -> Optional[Match[str]]:
|
||||
"""
|
||||
`Regex match type`: The first match from :attr:`matches`.
|
||||
:meth:`re.Match <re.Match.expand>`: The first match from :attr:`matches`.
|
||||
Useful if you are only filtering using a single regex filter.
|
||||
Returns `None` if :attr:`matches` is empty.
|
||||
Returns :obj:`None` if :attr:`matches` is empty.
|
||||
"""
|
||||
try:
|
||||
return self.matches[0] # type: ignore[index] # pylint: disable=unsubscriptable-object
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
from threading import Lock
|
||||
from typing import Dict, Tuple, Union, Optional, MutableMapping, TYPE_CHECKING, cast
|
||||
from uuid import uuid4
|
||||
|
||||
|
@ -106,7 +105,7 @@ class CallbackDataCache:
|
|||
Args:
|
||||
bot (:class:`telegram.ext.ExtBot`): The bot this cache is for.
|
||||
maxsize (:obj:`int`, optional): Maximum number of items in each of the internal mappings.
|
||||
Defaults to 1024.
|
||||
Defaults to ``1024``.
|
||||
|
||||
persistent_data (Tuple[List[Tuple[:obj:`str`, :obj:`float`, \
|
||||
Dict[:obj:`str`, :class:`object`]]], Dict[:obj:`str`, :obj:`str`]], optional): \
|
||||
|
@ -119,7 +118,7 @@ class CallbackDataCache:
|
|||
|
||||
"""
|
||||
|
||||
__slots__ = ('bot', 'maxsize', '_keyboard_data', '_callback_queries', '__lock', 'logger')
|
||||
__slots__ = ('bot', 'maxsize', '_keyboard_data', '_callback_queries', 'logger')
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -133,7 +132,6 @@ class CallbackDataCache:
|
|||
self.maxsize = maxsize
|
||||
self._keyboard_data: MutableMapping[str, _KeyboardData] = LRUCache(maxsize=maxsize)
|
||||
self._callback_queries: MutableMapping[str, str] = LRUCache(maxsize=maxsize)
|
||||
self.__lock = Lock()
|
||||
|
||||
if persistent_data:
|
||||
keyboard_data, callback_queries = persistent_data
|
||||
|
@ -153,16 +151,15 @@ class CallbackDataCache:
|
|||
# While building a list/dict from the LRUCaches has linear runtime (in the number of
|
||||
# entries), the runtime is bounded by maxsize and it has the big upside of not throwing a
|
||||
# highly customized data structure at users trying to implement a custom persistence class
|
||||
with self.__lock:
|
||||
return [data.to_tuple() for data in self._keyboard_data.values()], dict(
|
||||
self._callback_queries.items()
|
||||
)
|
||||
return [data.to_tuple() for data in self._keyboard_data.values()], dict(
|
||||
self._callback_queries.items()
|
||||
)
|
||||
|
||||
def process_keyboard(self, reply_markup: InlineKeyboardMarkup) -> InlineKeyboardMarkup:
|
||||
"""Registers the reply markup to the cache. If any of the buttons have
|
||||
:attr:`~telegram.InlineKeyboardButton.callback_data`, stores that data and builds a new
|
||||
keyboard with the correspondingly
|
||||
replaced buttons. Otherwise does nothing and returns the original reply markup.
|
||||
keyboard with the correspondingly replaced buttons. Otherwise, does nothing and returns
|
||||
the original reply markup.
|
||||
|
||||
Args:
|
||||
reply_markup (:class:`telegram.InlineKeyboardMarkup`): The keyboard.
|
||||
|
@ -171,10 +168,6 @@ class CallbackDataCache:
|
|||
:class:`telegram.InlineKeyboardMarkup`: The keyboard to be passed to Telegram.
|
||||
|
||||
"""
|
||||
with self.__lock:
|
||||
return self.__process_keyboard(reply_markup)
|
||||
|
||||
def __process_keyboard(self, reply_markup: InlineKeyboardMarkup) -> InlineKeyboardMarkup:
|
||||
keyboard_uuid = uuid4().hex
|
||||
keyboard_data = _KeyboardData(keyboard_uuid)
|
||||
|
||||
|
@ -228,10 +221,11 @@ class CallbackDataCache:
|
|||
|
||||
@staticmethod
|
||||
def extract_uuids(callback_data: str) -> Tuple[str, str]:
|
||||
"""Extracts the keyboard uuid and the button uuid from the given ``callback_data``.
|
||||
"""Extracts the keyboard uuid and the button uuid from the given :paramref:`callback_data`.
|
||||
|
||||
Args:
|
||||
callback_data (:obj:`str`): The ``callback_data`` as present in the button.
|
||||
callback_data (:obj:`str`): The
|
||||
:paramref:`~telegram.InlineKeyboardButton.callback_data` as present in the button.
|
||||
|
||||
Returns:
|
||||
(:obj:`str`, :obj:`str`): Tuple of keyboard and button uuid
|
||||
|
@ -247,7 +241,7 @@ class CallbackDataCache:
|
|||
|
||||
Note:
|
||||
Checks :attr:`telegram.Message.via_bot` and :attr:`telegram.Message.from_user` to check
|
||||
if the reply markup (if any) was actually sent by this caches bot. If it was not, the
|
||||
if the reply markup (if any) was actually sent by this cache's bot. If it was not, the
|
||||
message will be returned unchanged.
|
||||
|
||||
Note that this will fail for channel posts, as :attr:`telegram.Message.from_user` is
|
||||
|
@ -256,15 +250,14 @@ class CallbackDataCache:
|
|||
|
||||
Warning:
|
||||
* Does *not* consider :attr:`telegram.Message.reply_to_message` and
|
||||
:attr:`telegram.Message.pinned_message`. Pass them to these method separately.
|
||||
:attr:`telegram.Message.pinned_message`. Pass them to this method separately.
|
||||
* *In place*, i.e. the passed :class:`telegram.Message` will be changed!
|
||||
|
||||
Args:
|
||||
message (:class:`telegram.Message`): The message.
|
||||
|
||||
"""
|
||||
with self.__lock:
|
||||
self.__process_message(message)
|
||||
self.__process_message(message)
|
||||
|
||||
def __process_message(self, message: Message) -> Optional[str]:
|
||||
"""As documented in process_message, but returns the uuid of the attached keyboard, if any,
|
||||
|
@ -324,38 +317,37 @@ class CallbackDataCache:
|
|||
callback_query (:class:`telegram.CallbackQuery`): The callback query.
|
||||
|
||||
"""
|
||||
with self.__lock:
|
||||
mapped = False
|
||||
mapped = False
|
||||
|
||||
if callback_query.data:
|
||||
data = callback_query.data
|
||||
if callback_query.data:
|
||||
data = callback_query.data
|
||||
|
||||
# Get the cached callback data for the CallbackQuery
|
||||
keyboard_uuid, button_data = self.__get_keyboard_uuid_and_button_data(data)
|
||||
callback_query.data = button_data # type: ignore[assignment]
|
||||
# Get the cached callback data for the CallbackQuery
|
||||
keyboard_uuid, button_data = self.__get_keyboard_uuid_and_button_data(data)
|
||||
callback_query.data = button_data # type: ignore[assignment]
|
||||
|
||||
# Map the callback queries ID to the keyboards UUID for later use
|
||||
if not mapped and not isinstance(button_data, InvalidCallbackData):
|
||||
self._callback_queries[callback_query.id] = keyboard_uuid # type: ignore
|
||||
mapped = True
|
||||
# Map the callback queries ID to the keyboards UUID for later use
|
||||
if not mapped and not isinstance(button_data, InvalidCallbackData):
|
||||
self._callback_queries[callback_query.id] = keyboard_uuid # type: ignore
|
||||
mapped = True
|
||||
|
||||
# Get the cached callback data for the inline keyboard attached to the
|
||||
# CallbackQuery.
|
||||
if callback_query.message:
|
||||
self.__process_message(callback_query.message)
|
||||
for message in (
|
||||
callback_query.message.pinned_message,
|
||||
callback_query.message.reply_to_message,
|
||||
):
|
||||
if message:
|
||||
self.__process_message(message)
|
||||
# Get the cached callback data for the inline keyboard attached to the
|
||||
# CallbackQuery.
|
||||
if callback_query.message:
|
||||
self.__process_message(callback_query.message)
|
||||
for message in (
|
||||
callback_query.message.pinned_message,
|
||||
callback_query.message.reply_to_message,
|
||||
):
|
||||
if message:
|
||||
self.__process_message(message)
|
||||
|
||||
def drop_data(self, callback_query: CallbackQuery) -> None:
|
||||
"""Deletes the data for the specified callback query.
|
||||
|
||||
Note:
|
||||
Will *not* raise exceptions in case the callback data is not found in the cache.
|
||||
*Will* raise :class:`KeyError` in case the callback query can not be found in the
|
||||
*Will* raise :exc:`KeyError` in case the callback query can not be found in the
|
||||
cache.
|
||||
|
||||
Args:
|
||||
|
@ -364,12 +356,11 @@ class CallbackDataCache:
|
|||
Raises:
|
||||
KeyError: If the callback query can not be found in the cache
|
||||
"""
|
||||
with self.__lock:
|
||||
try:
|
||||
keyboard_uuid = self._callback_queries.pop(callback_query.id)
|
||||
self.__drop_keyboard(keyboard_uuid)
|
||||
except KeyError as exc:
|
||||
raise KeyError('CallbackQuery was not found in cache.') from exc
|
||||
try:
|
||||
keyboard_uuid = self._callback_queries.pop(callback_query.id)
|
||||
self.__drop_keyboard(keyboard_uuid)
|
||||
except KeyError as exc:
|
||||
raise KeyError('CallbackQuery was not found in cache.') from exc
|
||||
|
||||
def __drop_keyboard(self, keyboard_uuid: str) -> None:
|
||||
try:
|
||||
|
@ -387,13 +378,11 @@ class CallbackDataCache:
|
|||
bot will be used.
|
||||
|
||||
"""
|
||||
with self.__lock:
|
||||
self.__clear(self._keyboard_data, time_cutoff=time_cutoff)
|
||||
self.__clear(self._keyboard_data, time_cutoff=time_cutoff)
|
||||
|
||||
def clear_callback_queries(self) -> None:
|
||||
"""Clears the stored callback query IDs."""
|
||||
with self.__lock:
|
||||
self.__clear(self._callback_queries)
|
||||
self.__clear(self._callback_queries)
|
||||
|
||||
def __clear(self, mapping: MutableMapping, time_cutoff: Union[float, datetime] = None) -> None:
|
||||
if not time_cutoff:
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the CallbackQueryHandler class."""
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
|
@ -31,18 +31,20 @@ from typing import (
|
|||
)
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.types import DVInput
|
||||
from telegram.ext import Handler
|
||||
from telegram._utils.defaultvalue import DefaultValue, DEFAULT_FALSE
|
||||
from telegram.ext._utils.types import CCT
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Dispatcher
|
||||
from telegram.ext import Application
|
||||
|
||||
RT = TypeVar('RT')
|
||||
|
||||
|
||||
class CallbackQueryHandler(Handler[Update, CCT]):
|
||||
"""Handler class to handle Telegram callback queries. Optionally based on a regex.
|
||||
"""Handler class to handle Telegram :attr:`callback queries <telegram.Update.callback_query>`.
|
||||
Optionally based on a regex.
|
||||
|
||||
Read the documentation of the :mod:`re` module for more information.
|
||||
|
||||
|
@ -59,22 +61,25 @@ class CallbackQueryHandler(Handler[Update, CCT]):
|
|||
.. versionadded:: 13.6
|
||||
|
||||
Warning:
|
||||
When setting :paramref:`run_async` to :obj:`True`, you cannot rely on adding custom
|
||||
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
|
||||
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
|
||||
|
||||
Args:
|
||||
callback (:obj:`callable`): The callback function for this handler. Will be called when
|
||||
:attr:`check_update` has determined that an update should be processed by this handler.
|
||||
Callback signature: ``def callback(update: Update, context: CallbackContext)``
|
||||
callback (:term:`coroutine function`): The callback function for this handler. Will be
|
||||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
this handler. Callback signature::
|
||||
|
||||
async def callback(update: Update, context: CallbackContext)
|
||||
|
||||
The return value of the callback is usually ignored except for the special case of
|
||||
:class:`telegram.ext.ConversationHandler`.
|
||||
pattern (:obj:`str` | `Pattern` | :obj:`callable` | :obj:`type`, optional):
|
||||
pattern (:obj:`str` | :func:`re.Pattern <re.compile>` | :obj:`callable` | :obj:`type`, \
|
||||
optional):
|
||||
Pattern to test :attr:`telegram.CallbackQuery.data` against. If a string or a regex
|
||||
pattern is passed, :func:`re.match` is used on :attr:`telegram.CallbackQuery.data` to
|
||||
determine if an update should be handled by this handler. If your bot allows arbitrary
|
||||
objects as ``callback_data``, non-strings will be accepted. To filter arbitrary
|
||||
objects you may pass
|
||||
objects as :paramref:`~telegram.InlineKeyboardButton.callback_data`, non-strings will
|
||||
be accepted. To filter arbitrary objects you may pass:
|
||||
|
||||
* a callable, accepting exactly one argument, namely the
|
||||
:attr:`telegram.CallbackQuery.data`. It must return :obj:`True` or
|
||||
|
@ -87,17 +92,20 @@ class CallbackQueryHandler(Handler[Update, CCT]):
|
|||
|
||||
.. versionchanged:: 13.6
|
||||
Added support for arbitrary callback data.
|
||||
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
|
||||
Defaults to :obj:`False`.
|
||||
block (:obj:`bool`, optional): Determines whether the return value of the callback should
|
||||
be awaited before processing the next handler in
|
||||
:meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`.
|
||||
|
||||
Attributes:
|
||||
callback (:obj:`callable`): The callback function for this handler.
|
||||
pattern (`Pattern` | :obj:`callable` | :obj:`type`): Optional. Regex pattern, callback or
|
||||
type to test :attr:`telegram.CallbackQuery.data` against.
|
||||
callback (:term:`coroutine function`): The callback function for this handler.
|
||||
pattern (:func:`re.Pattern <re.compile>` | :obj:`callable` | :obj:`type`): Optional.
|
||||
Regex pattern, callback or type to test :attr:`telegram.CallbackQuery.data` against.
|
||||
|
||||
.. versionchanged:: 13.6
|
||||
Added support for arbitrary callback data.
|
||||
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
|
||||
block (:obj:`bool`): Determines whether the return value of the callback should be
|
||||
awaited before processing the next handler in
|
||||
:meth:`telegram.ext.Application.process_update`.
|
||||
|
||||
"""
|
||||
|
||||
|
@ -105,14 +113,16 @@ class CallbackQueryHandler(Handler[Update, CCT]):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
callback: Callable[[Update, CCT], RT],
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
pattern: Union[str, Pattern, type, Callable[[object], Optional[bool]]] = None,
|
||||
run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
|
||||
block: DVInput[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
super().__init__(
|
||||
callback,
|
||||
run_async=run_async,
|
||||
)
|
||||
super().__init__(callback, block=block)
|
||||
|
||||
if callable(pattern) and asyncio.iscoroutinefunction(pattern):
|
||||
raise TypeError(
|
||||
'The `pattern` must not be a coroutine function! Use an ordinary function instead.'
|
||||
)
|
||||
|
||||
if isinstance(pattern, str):
|
||||
pattern = re.compile(pattern)
|
||||
|
@ -120,7 +130,7 @@ class CallbackQueryHandler(Handler[Update, CCT]):
|
|||
self.pattern = pattern
|
||||
|
||||
def check_update(self, update: object) -> Optional[Union[bool, object]]:
|
||||
"""Determines whether an update should be passed to this handlers :attr:`callback`.
|
||||
"""Determines whether an update should be passed to this handler's :attr:`callback`.
|
||||
|
||||
Args:
|
||||
update (:class:`telegram.Update` | :obj:`object`): Incoming update.
|
||||
|
@ -149,7 +159,7 @@ class CallbackQueryHandler(Handler[Update, CCT]):
|
|||
self,
|
||||
context: CCT,
|
||||
update: Update,
|
||||
dispatcher: 'Dispatcher',
|
||||
application: 'Application',
|
||||
check_result: Union[bool, Match],
|
||||
) -> None:
|
||||
"""Add the result of ``re.match(pattern, update.callback_query.data)`` to
|
||||
|
|
|
@ -26,36 +26,38 @@ from telegram.ext._utils.types import CCT
|
|||
|
||||
|
||||
class ChatJoinRequestHandler(Handler[Update, CCT]):
|
||||
"""Handler class to handle Telegram updates that contain a chat join request.
|
||||
"""Handler class to handle Telegram updates that contain
|
||||
:attr:`telegram.Update.chat_join_request`.
|
||||
|
||||
Warning:
|
||||
When setting :paramref:`run_async` to :obj:`True`, you cannot rely on adding custom
|
||||
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
|
||||
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
|
||||
|
||||
.. versionadded:: 13.8
|
||||
|
||||
Args:
|
||||
callback (:obj:`callable`): The callback function for this handler. Will be called when
|
||||
:attr:`check_update` has determined that an update should be processed by this handler.
|
||||
Callback signature for context based API:
|
||||
callback (:term:`coroutine function`): The callback function for this handler. Will be
|
||||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
this handler. Callback signature::
|
||||
|
||||
``def callback(update: Update, context: CallbackContext)``
|
||||
async def callback(update: Update, context: CallbackContext)
|
||||
|
||||
The return value of the callback is usually ignored except for the special case of
|
||||
:class:`telegram.ext.ConversationHandler`.
|
||||
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
|
||||
Defaults to :obj:`False`.
|
||||
block (:obj:`bool`, optional): Determines whether the return value of the callback should
|
||||
be awaited before processing the next handler in
|
||||
:meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`.
|
||||
|
||||
Attributes:
|
||||
callback (:obj:`callable`): The callback function for this handler.
|
||||
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
|
||||
callback (:term:`coroutine function`): The callback function for this handler.
|
||||
block (:obj:`bool`): Determines whether the callback will run in a blocking way..
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
def check_update(self, update: object) -> bool:
|
||||
"""Determines whether an update should be passed to this handlers :attr:`callback`.
|
||||
"""Determines whether an update should be passed to this handler's :attr:`callback`.
|
||||
|
||||
Args:
|
||||
update (:class:`telegram.Update` | :obj:`object`): Incoming update.
|
||||
|
|
|
@ -16,13 +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/].
|
||||
"""This module contains the ChatMemberHandler classes."""
|
||||
from typing import ClassVar, TypeVar, Union, Callable
|
||||
"""This module contains the ChatMemberHandler class."""
|
||||
from typing import ClassVar, TypeVar
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.types import DVInput
|
||||
from telegram.ext import Handler
|
||||
from telegram._utils.defaultvalue import DefaultValue, DEFAULT_FALSE
|
||||
from telegram.ext._utils.types import CCT
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
RT = TypeVar('RT')
|
||||
|
||||
|
@ -33,13 +34,15 @@ class ChatMemberHandler(Handler[Update, CCT]):
|
|||
.. versionadded:: 13.4
|
||||
|
||||
Warning:
|
||||
When setting :paramref:`run_async` to :obj:`True`, you cannot rely on adding custom
|
||||
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
|
||||
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
|
||||
|
||||
Args:
|
||||
callback (:obj:`callable`): The callback function for this handler. Will be called when
|
||||
:attr:`check_update` has determined that an update should be processed by this handler.
|
||||
Callback signature: ``def callback(update: Update, context: CallbackContext)``
|
||||
callback (:term:`coroutine function`): The callback function for this handler. Will be
|
||||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
this handler. Callback signature::
|
||||
|
||||
async def callback(update: Update, context: CallbackContext)
|
||||
|
||||
The return value of the callback is usually ignored except for the special case of
|
||||
:class:`telegram.ext.ConversationHandler`.
|
||||
|
@ -47,15 +50,18 @@ class ChatMemberHandler(Handler[Update, CCT]):
|
|||
:attr:`CHAT_MEMBER` or :attr:`ANY_CHAT_MEMBER` to specify if this handler should handle
|
||||
only updates with :attr:`telegram.Update.my_chat_member`,
|
||||
:attr:`telegram.Update.chat_member` or both. Defaults to :attr:`MY_CHAT_MEMBER`.
|
||||
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
|
||||
Defaults to :obj:`False`.
|
||||
block (:obj:`bool`, optional): Determines whether the return value of the callback should
|
||||
be awaited before processing the next handler in
|
||||
:meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`.
|
||||
|
||||
Attributes:
|
||||
callback (:obj:`callable`): The callback function for this handler.
|
||||
callback (:term:`coroutine function`): The callback function for this handler.
|
||||
chat_member_types (:obj:`int`, optional): Specifies if this handler should handle
|
||||
only updates with :attr:`telegram.Update.my_chat_member`,
|
||||
:attr:`telegram.Update.chat_member` or both.
|
||||
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
|
||||
block (:obj:`bool`): Determines whether the return value of the callback should be
|
||||
awaited before processing the next handler in
|
||||
:meth:`telegram.ext.Application.process_update`.
|
||||
|
||||
"""
|
||||
|
||||
|
@ -65,24 +71,21 @@ class ChatMemberHandler(Handler[Update, CCT]):
|
|||
CHAT_MEMBER: ClassVar[int] = 0
|
||||
""":obj:`int`: Used as a constant to handle only :attr:`telegram.Update.chat_member`."""
|
||||
ANY_CHAT_MEMBER: ClassVar[int] = 1
|
||||
""":obj:`int`: Used as a constant to handle bot :attr:`telegram.Update.my_chat_member`
|
||||
""":obj:`int`: Used as a constant to handle both :attr:`telegram.Update.my_chat_member`
|
||||
and :attr:`telegram.Update.chat_member`."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
callback: Callable[[Update, CCT], RT],
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
chat_member_types: int = MY_CHAT_MEMBER,
|
||||
run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
|
||||
block: DVInput[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
super().__init__(
|
||||
callback,
|
||||
run_async=run_async,
|
||||
)
|
||||
super().__init__(callback, block=block)
|
||||
|
||||
self.chat_member_types = chat_member_types
|
||||
|
||||
def check_update(self, update: object) -> bool:
|
||||
"""Determines whether an update should be passed to this handlers :attr:`callback`.
|
||||
"""Determines whether an update should be passed to this handler's :attr:`callback`.
|
||||
|
||||
Args:
|
||||
update (:class:`telegram.Update` | :obj:`object`): Incoming update.
|
||||
|
|
|
@ -18,35 +18,40 @@
|
|||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the ChosenInlineResultHandler class."""
|
||||
import re
|
||||
from typing import Optional, TypeVar, Union, Callable, TYPE_CHECKING, Pattern, Match, cast
|
||||
from typing import Optional, TypeVar, Union, TYPE_CHECKING, Pattern, Match, cast
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import DVInput
|
||||
from telegram.ext import Handler
|
||||
from telegram._utils.defaultvalue import DefaultValue, DEFAULT_FALSE
|
||||
from telegram.ext._utils.types import CCT
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
RT = TypeVar('RT')
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import CallbackContext, Dispatcher
|
||||
from telegram.ext import CallbackContext, Application
|
||||
|
||||
|
||||
class ChosenInlineResultHandler(Handler[Update, CCT]):
|
||||
"""Handler class to handle Telegram updates that contain a chosen inline result.
|
||||
"""Handler class to handle Telegram updates that contain
|
||||
:attr:`telegram.Update.chosen_inline_result`.
|
||||
|
||||
Warning:
|
||||
When setting :paramref:`run_async` to :obj:`True`, you cannot rely on adding custom
|
||||
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
|
||||
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
|
||||
|
||||
Args:
|
||||
callback (:obj:`callable`): The callback function for this handler. Will be called when
|
||||
:attr:`check_update` has determined that an update should be processed by this handler.
|
||||
Callback signature: ``def callback(update: Update, context: CallbackContext)``
|
||||
callback (:term:`coroutine function`): The callback function for this handler. Will be
|
||||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
this handler. Callback signature::
|
||||
|
||||
async def callback(update: Update, context: CallbackContext)
|
||||
|
||||
The return value of the callback is usually ignored except for the special case of
|
||||
:class:`telegram.ext.ConversationHandler`.
|
||||
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
|
||||
Defaults to :obj:`False`.
|
||||
block (:obj:`bool`, optional): Determines whether the return value of the callback should
|
||||
be awaited before processing the next handler in
|
||||
:meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`.
|
||||
pattern (:obj:`str` | :func:`re.Pattern <re.compile>`, optional): Regex pattern. If not
|
||||
:obj:`None`, :func:`re.match`
|
||||
is used on :attr:`telegram.ChosenInlineResult.result_id` to determine if an update
|
||||
|
@ -56,8 +61,10 @@ class ChosenInlineResultHandler(Handler[Update, CCT]):
|
|||
.. versionadded:: 13.6
|
||||
|
||||
Attributes:
|
||||
callback (:obj:`callable`): The callback function for this handler.
|
||||
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
|
||||
callback (:term:`coroutine function`): The callback function for this handler.
|
||||
block (:obj:`bool`): Determines whether the return value of the callback should be
|
||||
awaited before processing the next handler in
|
||||
:meth:`telegram.ext.Application.process_update`.
|
||||
pattern (`Pattern`): Optional. Regex pattern to test
|
||||
:attr:`telegram.ChosenInlineResult.result_id` against.
|
||||
|
||||
|
@ -69,14 +76,11 @@ class ChosenInlineResultHandler(Handler[Update, CCT]):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
callback: Callable[[Update, 'CallbackContext'], RT],
|
||||
run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
block: DVInput[bool] = DEFAULT_TRUE,
|
||||
pattern: Union[str, Pattern] = None,
|
||||
):
|
||||
super().__init__(
|
||||
callback,
|
||||
run_async=run_async,
|
||||
)
|
||||
super().__init__(callback, block=block)
|
||||
|
||||
if isinstance(pattern, str):
|
||||
pattern = re.compile(pattern)
|
||||
|
@ -84,13 +88,13 @@ class ChosenInlineResultHandler(Handler[Update, CCT]):
|
|||
self.pattern = pattern
|
||||
|
||||
def check_update(self, update: object) -> Optional[Union[bool, object]]:
|
||||
"""Determines whether an update should be passed to this handlers :attr:`callback`.
|
||||
"""Determines whether an update should be passed to this handler's :attr:`callback`.
|
||||
|
||||
Args:
|
||||
update (:class:`telegram.Update` | :obj:`object`): Incoming update.
|
||||
|
||||
Returns:
|
||||
:obj:`bool`
|
||||
:obj:`bool` | :obj:`re.match`
|
||||
|
||||
"""
|
||||
if isinstance(update, Update) and update.chosen_inline_result:
|
||||
|
@ -106,7 +110,7 @@ class ChosenInlineResultHandler(Handler[Update, CCT]):
|
|||
self,
|
||||
context: 'CallbackContext',
|
||||
update: Update,
|
||||
dispatcher: 'Dispatcher',
|
||||
application: 'Application',
|
||||
check_result: Union[bool, Match],
|
||||
) -> None:
|
||||
"""This function adds the matched regex pattern result to
|
||||
|
|
|
@ -18,16 +18,16 @@
|
|||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the CommandHandler and PrefixHandler classes."""
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Tuple, TypeVar, Union
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, TypeVar, Union
|
||||
|
||||
from telegram import MessageEntity, Update
|
||||
from telegram.ext import filters as filters_module, Handler
|
||||
from telegram._utils.types import SLT
|
||||
from telegram._utils.defaultvalue import DefaultValue, DEFAULT_FALSE
|
||||
from telegram.ext._utils.types import CCT
|
||||
from telegram._utils.types import SLT, DVInput
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Dispatcher
|
||||
from telegram.ext import Application
|
||||
|
||||
RT = TypeVar('RT')
|
||||
|
||||
|
@ -36,48 +36,54 @@ class CommandHandler(Handler[Update, CCT]):
|
|||
"""Handler class to handle Telegram commands.
|
||||
|
||||
Commands are Telegram messages that start with ``/``, optionally followed by an ``@`` and the
|
||||
bot's name and/or some additional text. The handler will add a ``list`` to the
|
||||
bot's name and/or some additional text. The handler will add a :obj:`list` to the
|
||||
:class:`CallbackContext` named :attr:`CallbackContext.args`. It will contain a list of strings,
|
||||
which is the text following the command split on single or consecutive whitespace characters.
|
||||
|
||||
By default the handler listens to messages as well as edited messages. To change this behavior
|
||||
use ``~filters.UpdateType.EDITED_MESSAGE`` in the filter argument.
|
||||
By default, the handler listens to messages as well as edited messages. To change this behavior
|
||||
use :attr:`~filters.UpdateType.EDITED_MESSAGE <telegram.ext.filters.UpdateType.EDITED_MESSAGE>`
|
||||
in the filter argument.
|
||||
|
||||
Note:
|
||||
* :class:`CommandHandler` does *not* handle (edited) channel posts.
|
||||
|
||||
Warning:
|
||||
When setting :paramref:`run_async` to :obj:`True`, you cannot rely on adding custom
|
||||
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
|
||||
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
|
||||
|
||||
Args:
|
||||
command (:obj:`str` | Tuple[: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
|
||||
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: ``def callback(update: Update, context: CallbackContext)``
|
||||
Limitations are the same as described `here <https://core.telegram.org/bots#commands>`_
|
||||
callback (:term:`coroutine function`): The callback function for this handler. Will be
|
||||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
this handler. Callback signature::
|
||||
|
||||
async def callback(update: Update, context: CallbackContext)
|
||||
|
||||
The return value of the callback is usually ignored except for the special case of
|
||||
:class:`telegram.ext.ConversationHandler`.
|
||||
filters (:class:`telegram.ext.filters.BaseFilter`, optional): A filter inheriting from
|
||||
:class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in
|
||||
:mod:`telegram.ext.filters`. Filters can be combined using bitwise
|
||||
operators (& for and, | for or, ~ for not).
|
||||
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
|
||||
Defaults to :obj:`False`.
|
||||
operators (``&`` for :keyword:`and`, ``|`` for :keyword:`or`, ``~`` for :keyword:`not`)
|
||||
block (:obj:`bool`, optional): Determines whether the return value of the callback should
|
||||
be awaited before processing the next handler in
|
||||
:meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`.
|
||||
|
||||
Raises:
|
||||
ValueError: when command is too long or has illegal chars.
|
||||
:exc:`ValueError`: When the command is too long or has illegal chars.
|
||||
|
||||
Attributes:
|
||||
command (:obj:`str` | Tuple[: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
|
||||
callback (:obj:`callable`): The callback function for this handler.
|
||||
Limitations are the same as described `here <https://core.telegram.org/bots#commands>`_
|
||||
callback (:term:`coroutine function`): The callback function for this handler.
|
||||
filters (:class:`telegram.ext.filters.BaseFilter`): Optional. Only allow updates with these
|
||||
Filters.
|
||||
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
|
||||
block (:obj:`bool`): Determines whether the return value of the callback should be
|
||||
awaited before processing the next handler in
|
||||
:meth:`telegram.ext.Application.process_update`.
|
||||
"""
|
||||
|
||||
__slots__ = ('command', 'filters')
|
||||
|
@ -85,11 +91,11 @@ class CommandHandler(Handler[Update, CCT]):
|
|||
def __init__(
|
||||
self,
|
||||
command: SLT[str],
|
||||
callback: Callable[[Update, CCT], RT],
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
filters: filters_module.BaseFilter = None,
|
||||
run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
|
||||
block: DVInput[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
super().__init__(callback, run_async=run_async)
|
||||
super().__init__(callback, block=block)
|
||||
|
||||
if isinstance(command, str):
|
||||
self.command = [command.lower()]
|
||||
|
@ -104,7 +110,7 @@ class CommandHandler(Handler[Update, CCT]):
|
|||
def check_update(
|
||||
self, update: object
|
||||
) -> Optional[Union[bool, Tuple[List[str], Optional[Union[bool, Dict]]]]]:
|
||||
"""Determines whether an update should be passed to this handlers :attr:`callback`.
|
||||
"""Determines whether an update should be passed to this handler's :attr:`callback`.
|
||||
|
||||
Args:
|
||||
update (:class:`telegram.Update` | :obj:`object`): Incoming update.
|
||||
|
@ -144,7 +150,7 @@ class CommandHandler(Handler[Update, CCT]):
|
|||
self,
|
||||
context: CCT,
|
||||
update: Update,
|
||||
dispatcher: 'Dispatcher',
|
||||
application: 'Application',
|
||||
check_result: Optional[Union[bool, Tuple[List[str], Optional[bool]]]],
|
||||
) -> None:
|
||||
"""Add text after the command to :attr:`CallbackContext.args` as list, split on single
|
||||
|
@ -159,11 +165,12 @@ class CommandHandler(Handler[Update, CCT]):
|
|||
class PrefixHandler(CommandHandler):
|
||||
"""Handler class to handle custom prefix commands.
|
||||
|
||||
This is a intermediate handler between :class:`MessageHandler` and :class:`CommandHandler`.
|
||||
It supports configurable commands with the same options as CommandHandler. It will respond to
|
||||
every combination of :attr:`prefix` and :attr:`command`. It will add a :obj:`list` to the
|
||||
:class:`CallbackContext` named :attr:`CallbackContext.args`. It will contain a list of strings,
|
||||
which is the text following the command split on single or consecutive whitespace characters.
|
||||
This is an intermediate handler between :class:`MessageHandler` and :class:`CommandHandler`.
|
||||
It supports configurable commands with the same options as :class:`CommandHandler`. It will
|
||||
respond to every combination of :attr:`prefix` and :attr:`command`. It will add a :obj:`list`
|
||||
to the :class:`CallbackContext` named :attr:`CallbackContext.args`. It will contain a list of
|
||||
strings, which is the text following the command split on single or consecutive whitespace
|
||||
characters.
|
||||
|
||||
Examples:
|
||||
|
||||
|
@ -171,30 +178,31 @@ class PrefixHandler(CommandHandler):
|
|||
|
||||
.. code:: python
|
||||
|
||||
PrefixHandler('!', 'test', callback) # will respond to '!test'.
|
||||
PrefixHandler("!", "test", callback) # will respond to '!test'.
|
||||
|
||||
Multiple prefixes, single command:
|
||||
|
||||
.. code:: python
|
||||
|
||||
PrefixHandler(['!', '#'], 'test', callback) # will respond to '!test' and '#test'.
|
||||
PrefixHandler(["!", "#"], "test", callback) # will respond to '!test' and '#test'.
|
||||
|
||||
Multiple prefixes and commands:
|
||||
|
||||
.. code:: python
|
||||
|
||||
PrefixHandler(['!', '#'], ['test', 'help'], callback) # will respond to '!test', \
|
||||
'#test', '!help' and '#help'.
|
||||
PrefixHandler(
|
||||
["!", "#"], ["test", "help"], callback
|
||||
) # will respond to '!test', '#test', '!help' and '#help'.
|
||||
|
||||
|
||||
By default the handler listens to messages as well as edited messages. To change this behavior
|
||||
use ``~filters.UpdateType.EDITED_MESSAGE``.
|
||||
By default, the handler listens to messages as well as edited messages. To change this behavior
|
||||
use :attr:`~filters.UpdateType.EDITED_MESSAGE <telegram.ext.filters.UpdateType.EDITED_MESSAGE>`
|
||||
|
||||
Note:
|
||||
* :class:`PrefixHandler` does *not* handle (edited) channel posts.
|
||||
|
||||
Warning:
|
||||
When setting :paramref:`run_async` to :obj:`True`, you cannot rely on adding custom
|
||||
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
|
||||
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
|
||||
|
||||
Args:
|
||||
|
@ -202,24 +210,29 @@ class PrefixHandler(CommandHandler):
|
|||
The prefix(es) that will precede :attr:`command`.
|
||||
command (:obj:`str` | Tuple[:obj:`str`] | List[:obj:`str`]):
|
||||
The command or list of commands this handler should listen for.
|
||||
callback (:obj:`callable`): The callback function for this handler. Will be called when
|
||||
:attr:`check_update` has determined that an update should be processed by this handler.
|
||||
Callback signature: ``def callback(update: Update, context: CallbackContext)``
|
||||
callback (:term:`coroutine function`): The callback function for this handler. Will be
|
||||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
this handler. Callback signature::
|
||||
|
||||
async def callback(update: Update, context: CallbackContext)
|
||||
|
||||
The return value of the callback is usually ignored except for the special case of
|
||||
:class:`telegram.ext.ConversationHandler`.
|
||||
filters (:class:`telegram.ext.filters.BaseFilter`, optional): A filter inheriting from
|
||||
:class:`telegram.ext.filters.BaseFilter`. Standard filters can be found in
|
||||
:mod:`telegram.ext.filters`. Filters can be combined using bitwise
|
||||
operators (& for and, | for or, ~ for not).
|
||||
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
|
||||
Defaults to :obj:`False`.
|
||||
operators (``&`` for :keyword:`and`, ``|`` for :keyword:`or`, ``~`` for :keyword:`not`)
|
||||
block (:obj:`bool`, optional): Determines whether the return value of the callback should
|
||||
be awaited before processing the next handler in
|
||||
:meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`.
|
||||
|
||||
Attributes:
|
||||
callback (:obj:`callable`): The callback function for this handler.
|
||||
callback (:term:`coroutine function`): The callback function for this handler.
|
||||
filters (:class:`telegram.ext.filters.BaseFilter`): Optional. Only allow updates with these
|
||||
Filters.
|
||||
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
|
||||
block (:obj:`bool`): Determines whether the return value of the callback should be
|
||||
awaited before processing the next handler in
|
||||
:meth:`telegram.ext.Application.process_update`.
|
||||
|
||||
"""
|
||||
|
||||
|
@ -230,9 +243,9 @@ class PrefixHandler(CommandHandler):
|
|||
self,
|
||||
prefix: SLT[str],
|
||||
command: SLT[str],
|
||||
callback: Callable[[Update, CCT], RT],
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
filters: filters_module.BaseFilter = None,
|
||||
run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
|
||||
block: DVInput[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
|
||||
self._prefix: List[str] = []
|
||||
|
@ -243,7 +256,7 @@ class PrefixHandler(CommandHandler):
|
|||
'nocommand',
|
||||
callback,
|
||||
filters=filters,
|
||||
run_async=run_async,
|
||||
block=block,
|
||||
)
|
||||
|
||||
self.prefix = prefix # type: ignore[assignment]
|
||||
|
@ -292,7 +305,7 @@ class PrefixHandler(CommandHandler):
|
|||
def check_update(
|
||||
self, update: object
|
||||
) -> Optional[Union[bool, Tuple[List[str], Optional[Union[bool, Dict]]]]]:
|
||||
"""Determines whether an update should be passed to this handlers :attr:`callback`.
|
||||
"""Determines whether an update should be passed to this handler's :attr:`callback`.
|
||||
|
||||
Args:
|
||||
update (:class:`telegram.Update` | :obj:`object`): Incoming update.
|
||||
|
|
|
@ -39,15 +39,18 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
|
|||
(error-)handler callbacks and job callbacks. Must be a subclass of
|
||||
:class:`telegram.ext.CallbackContext`. Defaults to
|
||||
:class:`telegram.ext.CallbackContext`.
|
||||
bot_data (:obj:`type`, optional): Determines the type of ``context.bot_data`` of all
|
||||
(error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support
|
||||
instantiating without arguments.
|
||||
chat_data (:obj:`type`, optional): Determines the type of ``context.chat_data`` of all
|
||||
(error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support
|
||||
instantiating without arguments.
|
||||
user_data (:obj:`type`, optional): Determines the type of ``context.user_data`` of all
|
||||
(error-)handler callbacks and job callbacks. Defaults to :obj:`dict`. Must support
|
||||
instantiating without arguments.
|
||||
bot_data (:obj:`type`, optional): Determines the type of
|
||||
:attr:`context.bot_data <CallbackContext.bot_data>` of all (error-)handler callbacks
|
||||
and job callbacks. Defaults to :obj:`dict`. Must support instantiating without
|
||||
arguments.
|
||||
chat_data (:obj:`type`, optional): Determines the type of
|
||||
:attr:`context.chat_data <CallbackContext.chat_data>` of all (error-)handler callbacks
|
||||
and job callbacks. Defaults to :obj:`dict`. Must support instantiating without
|
||||
arguments.
|
||||
user_data (:obj:`type`, optional): Determines the type of
|
||||
:attr:`context.user_data <CallbackContext.user_data>` of all (error-)handler callbacks
|
||||
and job callbacks. Defaults to :obj:`dict`. Must support instantiating without
|
||||
arguments.
|
||||
|
||||
"""
|
||||
|
||||
|
@ -201,15 +204,21 @@ class ContextTypes(Generic[CCT, UD, CD, BD]):
|
|||
|
||||
@property
|
||||
def bot_data(self) -> Type[BD]:
|
||||
"""The type of ``context.bot_data`` of all (error-)handler callbacks and job callbacks."""
|
||||
"""The type of :attr:`context.bot_data <CallbackContext.bot_data>` of all (error-)handler
|
||||
callbacks and job callbacks.
|
||||
"""
|
||||
return self._bot_data
|
||||
|
||||
@property
|
||||
def chat_data(self) -> Type[CD]:
|
||||
"""The type of ``context.chat_data`` of all (error-)handler callbacks and job callbacks."""
|
||||
"""The type of :attr:`context.chat_data <CallbackContext.chat_data>` of all (error-)handler
|
||||
callbacks and job callbacks.
|
||||
"""
|
||||
return self._chat_data
|
||||
|
||||
@property
|
||||
def user_data(self) -> Type[UD]:
|
||||
"""The type of ``context.user_data`` of all (error-)handler callbacks and job callbacks."""
|
||||
"""The type of :attr:`context.user_data <CallbackContext.user_data>` of all (error-)handler
|
||||
callbacks and job callbacks.
|
||||
"""
|
||||
return self._user_data
|
||||
|
|
|
@ -18,11 +18,10 @@
|
|||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
# pylint: disable=no-self-use
|
||||
"""This module contains the ConversationHandler."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import functools
|
||||
import datetime
|
||||
from threading import Lock
|
||||
from dataclasses import dataclass
|
||||
from typing import ( # pylint: disable=unused-import # for the "Any" import
|
||||
TYPE_CHECKING,
|
||||
Dict,
|
||||
|
@ -34,83 +33,136 @@ from typing import ( # pylint: disable=unused-import # for the "Any" import
|
|||
cast,
|
||||
ClassVar,
|
||||
Any,
|
||||
Set,
|
||||
Generic,
|
||||
)
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE, DefaultValue
|
||||
from telegram._utils.types import DVInput
|
||||
from telegram.ext import (
|
||||
BasePersistence,
|
||||
CallbackContext,
|
||||
CallbackQueryHandler,
|
||||
ChosenInlineResultHandler,
|
||||
DispatcherHandlerStop,
|
||||
ApplicationHandlerStop,
|
||||
Handler,
|
||||
InlineQueryHandler,
|
||||
StringCommandHandler,
|
||||
StringRegexHandler,
|
||||
TypeHandler,
|
||||
ExtBot,
|
||||
)
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.ext._utils.promise import Promise
|
||||
from telegram.ext._utils.types import ConversationDict
|
||||
from telegram.ext._utils.trackingdict import TrackingDict
|
||||
from telegram.ext._utils.types import ConversationDict, ConversationKey
|
||||
from telegram.ext._utils.types import CCT
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Dispatcher, Job, JobQueue
|
||||
CheckUpdateType = Optional[Tuple[Tuple[int, ...], Handler, object]]
|
||||
from telegram.ext import Application, Job, JobQueue
|
||||
_CheckUpdateType = Tuple[object, ConversationKey, Handler, object]
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _ConversationTimeoutContext:
|
||||
__slots__ = ('conversation_key', 'update', 'dispatcher', 'callback_context')
|
||||
@dataclass
|
||||
class _ConversationTimeoutContext(Generic[CCT]):
|
||||
"""Used as a datastore for conversation timeouts. Passed in the
|
||||
:paramref:`JobQueue.run_once.context` parameter. See :meth:`_trigger_timeout`.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
conversation_key: Tuple[int, ...],
|
||||
update: Update,
|
||||
dispatcher: 'Dispatcher[Any, CCT, Any, Any, Any, JobQueue, Any]',
|
||||
callback_context: CallbackContext,
|
||||
):
|
||||
self.conversation_key = conversation_key
|
||||
self.update = update
|
||||
self.dispatcher = dispatcher
|
||||
self.callback_context = callback_context
|
||||
__slots__ = ('conversation_key', 'update', 'application', 'callback_context')
|
||||
|
||||
conversation_key: ConversationKey
|
||||
update: Update
|
||||
application: 'Application[Any, CCT, Any, Any, Any, JobQueue]'
|
||||
callback_context: CallbackContext
|
||||
|
||||
|
||||
@dataclass
|
||||
class PendingState:
|
||||
"""Thin wrapper around :class:`asyncio.Task` to handle block=False handlers. Note that this is
|
||||
a public class of this module, since :meth:`Application.update_persistence` needs to access it.
|
||||
It's still hidden from users, since this module itself is private.
|
||||
"""
|
||||
|
||||
__slots__ = ('task', 'old_state')
|
||||
|
||||
task: asyncio.Task
|
||||
old_state: object
|
||||
|
||||
def done(self) -> bool:
|
||||
return self.task.done()
|
||||
|
||||
def resolve(self) -> object:
|
||||
"""Returns the new state of the :class:`ConversationHandler` if available. If there was an
|
||||
exception during the task execution, then return the old state. If the returned state was
|
||||
:obj:`None`, then end the conversation.
|
||||
|
||||
Raises:
|
||||
:exc:`RuntimeError`: If the current task has not yet finished.
|
||||
"""
|
||||
if not self.task.done():
|
||||
raise RuntimeError('New state is not yet available')
|
||||
|
||||
exc = self.task.exception()
|
||||
if exc:
|
||||
_logger.exception(
|
||||
"Task function raised exception. Falling back to old state %s",
|
||||
self.old_state,
|
||||
exc_info=exc,
|
||||
)
|
||||
return self.old_state
|
||||
|
||||
res = self.task.result()
|
||||
if res is None and self.old_state is None:
|
||||
res = ConversationHandler.END
|
||||
|
||||
return res
|
||||
|
||||
|
||||
class ConversationHandler(Handler[Update, CCT]):
|
||||
"""
|
||||
A handler to hold a conversation with a single or multiple users through Telegram updates by
|
||||
managing four collections of other handlers.
|
||||
managing three collections of other handlers.
|
||||
|
||||
Warning:
|
||||
:class:`ConversationHandler` heavily relies on incoming updates being processed one by one.
|
||||
When using this handler, :attr:`telegram.ext.Application.concurrent_updates` should be
|
||||
:obj:`False`.
|
||||
|
||||
Note:
|
||||
``ConversationHandler`` will only accept updates that are (subclass-)instances of
|
||||
:class:`ConversationHandler` will only accept updates that are (subclass-)instances of
|
||||
:class:`telegram.Update`. This is, because depending on the :attr:`per_user` and
|
||||
:attr:`per_chat` ``ConversationHandler`` relies on
|
||||
:attr:`per_chat`, :class:`ConversationHandler` relies on
|
||||
:attr:`telegram.Update.effective_user` and/or :attr:`telegram.Update.effective_chat` in
|
||||
order to determine which conversation an update should belong to. For ``per_message=True``,
|
||||
``ConversationHandler`` uses ``update.callback_query.message.message_id`` when
|
||||
``per_chat=True`` and ``update.callback_query.inline_message_id`` when ``per_chat=False``.
|
||||
For a more detailed explanation, please see our `FAQ`_.
|
||||
order to determine which conversation an update should belong to. For
|
||||
:attr:`per_message=True <per_message>`, :class:`ConversationHandler` uses
|
||||
:attr:`update.callback_query.message.message_id <telegram.Message.message_id>` when
|
||||
:attr:`per_chat=True <per_chat>` and
|
||||
:attr:`update.callback_query.inline_message_id <.CallbackQuery.inline_message_id>` when
|
||||
:attr:`per_chat=False <per_chat>`. For a more detailed explanation, please see our `FAQ`_.
|
||||
|
||||
Finally, ``ConversationHandler``, does *not* handle (edited) channel posts.
|
||||
Finally, :class:`ConversationHandler`, does *not* handle (edited) channel posts.
|
||||
|
||||
.. _`FAQ`: https://github.com/python-telegram-bot/python-telegram-bot/wiki\
|
||||
/Frequently-Asked-Questions#what-do-the-per_-settings-in-conversationhandler-do
|
||||
/Frequently-Asked-Questions#what-do-the-per_-settings-in-conversation handler-do
|
||||
|
||||
The first collection, a ``list`` named :attr:`entry_points`, is used to initiate the
|
||||
The first collection, a :obj:`list` named :attr:`entry_points`, is used to initiate the
|
||||
conversation, for example with a :class:`telegram.ext.CommandHandler` or
|
||||
:class:`telegram.ext.MessageHandler`.
|
||||
|
||||
The second collection, a ``dict`` named :attr:`states`, contains the different conversation
|
||||
The second collection, a :obj:`dict` named :attr:`states`, contains the different conversation
|
||||
steps and one or more associated handlers that should be used if the user sends a message when
|
||||
the conversation with them is currently in that state. Here you can also define a state for
|
||||
:attr:`TIMEOUT` to define the behavior when :attr:`conversation_timeout` is exceeded, and a
|
||||
state for :attr:`WAITING` to define behavior when a new update is received while the previous
|
||||
``@run_async`` decorated handler is not finished.
|
||||
:attr:`block=False <block>` handler is not finished.
|
||||
|
||||
The third collection, a ``list`` named :attr:`fallbacks`, is used if the user is currently in a
|
||||
conversation but the state has either no associated handler or the handler that is associated
|
||||
to the state is inappropriate for the update, for example if the update contains a command, but
|
||||
a regular text message is expected. You could use this for a ``/cancel`` command or to let the
|
||||
user know their message was not recognized.
|
||||
The third collection, a :obj:`list` named :attr:`fallbacks`, is used if the user is currently
|
||||
in a conversation but the state has either no associated handler or the handler that is
|
||||
associated to the state is inappropriate for the update, for example if the update contains a
|
||||
command, but a regular text message is expected. You could use this for a ``/cancel`` command
|
||||
or to let the user know their message was not recognized.
|
||||
|
||||
To change the state of conversation, the callback function of a handler must return the new
|
||||
state after responding to the user. If it does not return anything (returning :obj:`None` by
|
||||
|
@ -118,115 +170,119 @@ class ConversationHandler(Handler[Update, CCT]):
|
|||
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.
|
||||
Finally, :class:`telegram.ext.ApplicationHandlerStop` can be used in conversations as described
|
||||
in its documentation.
|
||||
|
||||
Note:
|
||||
In each of the described collections of handlers, a handler may in turn be a
|
||||
:class:`ConversationHandler`. In that case, the nested :class:`ConversationHandler` should
|
||||
have the attribute :attr:`map_to_parent` which allows to return to the parent conversation
|
||||
at specified states within the nested conversation.
|
||||
:class:`ConversationHandler`. In that case, the child :class:`ConversationHandler` should
|
||||
have the attribute :attr:`map_to_parent` which allows returning to the parent conversation
|
||||
at specified states within the child conversation.
|
||||
|
||||
Note that the keys in :attr:`map_to_parent` must not appear as keys in :attr:`states`
|
||||
attribute or else the latter will be ignored. You may map :attr:`END` to one of the parents
|
||||
states to continue the parent conversation after this has ended or even map a state to
|
||||
:attr:`END` to end the *parent* conversation from within the nested one. For an example on
|
||||
nested :class:`ConversationHandler` s, see our `examples`_.
|
||||
states to continue the parent conversation after the child conversation has ended or even
|
||||
map a state to :attr:`END` to end the *parent* conversation from within the child
|
||||
conversation. For an example on nested :class:`ConversationHandler` s, see our `examples`_.
|
||||
|
||||
.. _`examples`: https://github.com/python-telegram-bot/python-telegram-bot/blob/master/examples
|
||||
.. _`examples`: https://github.com/python-telegram-bot/python-telegram-bot/tree/master\
|
||||
/examples#examples
|
||||
|
||||
Args:
|
||||
entry_points (List[:class:`telegram.ext.Handler`]): A list of ``Handler`` objects that can
|
||||
trigger the start of the conversation. The first handler which :attr:`check_update`
|
||||
entry_points (List[:class:`telegram.ext.Handler`]): A list of :obj:`Handler` objects that
|
||||
can trigger the start of the conversation. The first handler whose :meth:`check_update`
|
||||
method returns :obj:`True` will be used. If all return :obj:`False`, the update is not
|
||||
handled.
|
||||
states (Dict[:obj:`object`, List[:class:`telegram.ext.Handler`]]): A :obj:`dict` that
|
||||
defines the different states of conversation a user can be in and one or more
|
||||
associated ``Handler`` objects that should be used in that state. The first handler
|
||||
which :attr:`check_update` method returns :obj:`True` will be used.
|
||||
associated :obj:`Handler` objects that should be used in that state. The first handler
|
||||
whose :meth:`check_update` method returns :obj:`True` will be used.
|
||||
fallbacks (List[:class:`telegram.ext.Handler`]): A list of handlers that might be used if
|
||||
the user is in a conversation, but every handler for their current state returned
|
||||
:obj:`False` on :attr:`check_update`. The first handler which :attr:`check_update`
|
||||
:obj:`False` on :meth:`check_update`. The first handler which :meth:`check_update`
|
||||
method returns :obj:`True` will be used. If all return :obj:`False`, the update is not
|
||||
handled.
|
||||
allow_reentry (:obj:`bool`, optional): If set to :obj:`True`, a user that is currently in a
|
||||
conversation can restart the conversation by triggering one of the entry points.
|
||||
per_chat (:obj:`bool`, optional): If the conversationkey should contain the Chat's ID.
|
||||
per_chat (:obj:`bool`, optional): If the conversation key should contain the Chat's ID.
|
||||
Default is :obj:`True`.
|
||||
per_user (:obj:`bool`, optional): If the conversationkey should contain the User's ID.
|
||||
per_user (:obj:`bool`, optional): If the conversation key should contain the User's ID.
|
||||
Default is :obj:`True`.
|
||||
per_message (:obj:`bool`, optional): If the conversationkey should contain the Message's
|
||||
per_message (:obj:`bool`, optional): If the conversation key should contain the Message's
|
||||
ID. Default is :obj:`False`.
|
||||
conversation_timeout (:obj:`float` | :obj:`datetime.timedelta`, optional): When this
|
||||
handler is inactive more than this timeout (in seconds), it will be automatically
|
||||
ended. If this value is 0 or :obj:`None` (default), there will be no timeout. The last
|
||||
received update and the corresponding ``context`` will be handled by ALL the handler's
|
||||
who's :attr:`check_update` method returns :obj:`True` that are in the state
|
||||
:attr:`ConversationHandler.TIMEOUT`.
|
||||
ended. If this value is ``0`` or :obj:`None` (default), there will be no timeout. The
|
||||
last received update and the corresponding :class:`context <.CallbackContext>` will be
|
||||
handled by *ALL* the handler's whose :meth:`check_update` method returns :obj:`True`
|
||||
that are in the state :attr:`ConversationHandler.TIMEOUT`.
|
||||
|
||||
Note:
|
||||
Using `conversation_timeout` with nested conversations is currently not
|
||||
Using :paramref:`conversation_timeout` with nested conversations is currently not
|
||||
supported. You can still try to use it, but it will likely behave differently
|
||||
from what you expect.
|
||||
|
||||
|
||||
name (:obj:`str`, optional): The name for this conversationhandler. Required for
|
||||
name (:obj:`str`, optional): The name for this conversation handler. Required for
|
||||
persistence.
|
||||
persistent (:obj:`bool`, optional): If the conversations dict for this handler should be
|
||||
saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater`
|
||||
persistent (:obj:`bool`, optional): If the conversation's dict for this handler should be
|
||||
saved. :paramref:`name` is required and persistence has to be set in
|
||||
:attr:`Application <.Application.persistence>`.
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
Was previously named as ``persistence``.
|
||||
map_to_parent (Dict[:obj:`object`, :obj:`object`], optional): A :obj:`dict` that can be
|
||||
used to instruct a nested conversationhandler to transition into a mapped state on
|
||||
its parent conversationhandler in place of a specified nested state.
|
||||
run_async (:obj:`bool`, optional): Pass :obj:`True` to *override* the
|
||||
:attr:`Handler.run_async` setting of all handlers (in :attr:`entry_points`,
|
||||
:attr:`states` and :attr:`fallbacks`).
|
||||
used to instruct a child conversation handler to transition into a mapped state on
|
||||
its parent conversation handler in place of a specified nested state.
|
||||
block (:obj:`bool`, optional): Pass :obj:`False` or :obj:`True` to set a default value for
|
||||
the :attr:`Handler.block` setting of all handlers (in :attr:`entry_points`,
|
||||
:attr:`states` and :attr:`fallbacks`). The resolution order for checking if a handler
|
||||
should be run non-blocking is:
|
||||
|
||||
Note:
|
||||
If set to :obj:`True`, you should not pass a handler instance, that needs to be
|
||||
run synchronously in another context.
|
||||
1. :attr:`telegram.ext.Handler.block` (if set)
|
||||
2. the value passed to this parameter (if any)
|
||||
3. :attr:`telegram.ext.Defaults.block` (if defaults are used)
|
||||
|
||||
.. versionadded:: 13.2
|
||||
.. versionchanged:: 14.0
|
||||
No longer overrides the handlers settings. Resolution order was changed.
|
||||
|
||||
Raises:
|
||||
ValueError
|
||||
:exc:`ValueError`: If :paramref:`persistent` is used but :paramref:`name` was not set, or
|
||||
when :attr:`per_message`, :attr:`per_chat`, :attr:`per_user` are all :obj:`False`.
|
||||
|
||||
Attributes:
|
||||
persistent (:obj:`bool`): Optional. If the conversations dict for this handler should be
|
||||
saved. Name is required and persistence has to be set in :class:`telegram.ext.Updater`
|
||||
run_async (:obj:`bool`): If :obj:`True`, will override the
|
||||
:attr:`Handler.run_async` setting of all internal handlers on initialization.
|
||||
|
||||
.. versionadded:: 13.2
|
||||
block (:obj:`bool`): Determines whether the callback will run in a blocking way.. Always
|
||||
:obj:`True` since conversation handlers handle any non-blocking callbacks internally.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'_entry_points',
|
||||
'_states',
|
||||
'_fallbacks',
|
||||
'_allow_reentry',
|
||||
'_per_user',
|
||||
'_block',
|
||||
'_child_conversations',
|
||||
'_conversation_timeout',
|
||||
'_conversations',
|
||||
'_entry_points',
|
||||
'_fallbacks',
|
||||
'_map_to_parent',
|
||||
'_name',
|
||||
'_per_chat',
|
||||
'_per_message',
|
||||
'_conversation_timeout',
|
||||
'_name',
|
||||
'persistent',
|
||||
'_persistence',
|
||||
'_map_to_parent',
|
||||
'timeout_jobs',
|
||||
'_per_user',
|
||||
'_persistent',
|
||||
'_states',
|
||||
'_timeout_jobs_lock',
|
||||
'_conversations',
|
||||
'_conversations_lock',
|
||||
'logger',
|
||||
'timeout_jobs',
|
||||
)
|
||||
|
||||
END: ClassVar[int] = -1
|
||||
""":obj:`int`: Used as a constant to return when a conversation is ended."""
|
||||
TIMEOUT: ClassVar[int] = -2
|
||||
""":obj:`int`: Used as a constant to handle state when a conversation is timed out."""
|
||||
""":obj:`int`: Used as a constant to handle state when a conversation is timed out
|
||||
(exceeded :attr:`conversation_timeout`).
|
||||
"""
|
||||
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."""
|
||||
previous :attr:`block=False <block>` handler to finish."""
|
||||
# pylint: disable=super-init-not-called
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -241,7 +297,7 @@ class ConversationHandler(Handler[Update, CCT]):
|
|||
name: str = None,
|
||||
persistent: bool = False,
|
||||
map_to_parent: Dict[object, object] = None,
|
||||
run_async: bool = False,
|
||||
block: DVInput[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
# these imports need to be here because of circular import error otherwise
|
||||
from telegram.ext import ( # pylint: disable=import-outside-toplevel
|
||||
|
@ -251,7 +307,11 @@ class ConversationHandler(Handler[Update, CCT]):
|
|||
PollAnswerHandler,
|
||||
)
|
||||
|
||||
self.run_async = run_async
|
||||
# self.block is what the Application checks and we want it to always run CH in a blocking
|
||||
# way so that CH can take care of any non-blocking logic internally
|
||||
self.block = True
|
||||
# Store the actual setting in a protected variable instead
|
||||
self._block = block
|
||||
|
||||
self._entry_points = entry_points
|
||||
self._states = states
|
||||
|
@ -263,20 +323,18 @@ class ConversationHandler(Handler[Update, CCT]):
|
|||
self._per_message = per_message
|
||||
self._conversation_timeout = conversation_timeout
|
||||
self._name = name
|
||||
if persistent and not self.name:
|
||||
raise ValueError("Conversations can't be persistent when handler is unnamed.")
|
||||
self.persistent: bool = persistent
|
||||
self._persistence: Optional[BasePersistence] = None
|
||||
""":obj:`telegram.ext.BasePersistence`: The persistence used to store conversations.
|
||||
Set by dispatcher"""
|
||||
self._map_to_parent = map_to_parent
|
||||
|
||||
self.timeout_jobs: Dict[Tuple[int, ...], 'Job'] = {}
|
||||
self._timeout_jobs_lock = Lock()
|
||||
# if conversation_timeout is used, this dict is used to schedule a job which runs when the
|
||||
# conv has timed out.
|
||||
self.timeout_jobs: Dict[ConversationKey, 'Job'] = {}
|
||||
self._timeout_jobs_lock = asyncio.Lock()
|
||||
self._conversations: ConversationDict = {}
|
||||
self._conversations_lock = Lock()
|
||||
self._child_conversations: Set['ConversationHandler'] = set()
|
||||
|
||||
self.logger = logging.getLogger(__name__)
|
||||
if persistent and not self.name:
|
||||
raise ValueError("Conversations can't be persistent when handler is unnamed.")
|
||||
self._persistent: bool = persistent
|
||||
|
||||
if not any((self.per_user, self.per_chat, self.per_message)):
|
||||
raise ValueError("'per_user', 'per_chat' and 'per_message' can't all be 'False'")
|
||||
|
@ -295,8 +353,9 @@ class ConversationHandler(Handler[Update, CCT]):
|
|||
for state_handlers in states.values():
|
||||
all_handlers.extend(state_handlers)
|
||||
|
||||
# this loop is going to warn the user about handlers which can work unexpected
|
||||
# in conversations
|
||||
self._child_conversations.update(
|
||||
handler for handler in all_handlers if isinstance(handler, ConversationHandler)
|
||||
)
|
||||
|
||||
# this link will be added to all warnings tied to per_* setting
|
||||
per_faq_link = (
|
||||
|
@ -305,6 +364,8 @@ class ConversationHandler(Handler[Update, CCT]):
|
|||
"/Frequently-Asked-Questions#what-do-the-per_-settings-in-conversationhandler-do."
|
||||
)
|
||||
|
||||
# this loop is going to warn the user about handlers which can work unexpectedly
|
||||
# in conversations
|
||||
for handler in all_handlers:
|
||||
if isinstance(handler, (StringCommandHandler, StringRegexHandler)):
|
||||
warn(
|
||||
|
@ -367,13 +428,10 @@ class ConversationHandler(Handler[Update, CCT]):
|
|||
stacklevel=2,
|
||||
)
|
||||
|
||||
if self.run_async:
|
||||
handler.run_async = True
|
||||
|
||||
@property
|
||||
def entry_points(self) -> List[Handler]:
|
||||
"""List[:class:`telegram.ext.Handler`]: A list of ``Handler`` objects that can trigger the
|
||||
start of the conversation.
|
||||
"""List[:class:`telegram.ext.Handler`]: A list of :obj:`Handler` objects that can trigger
|
||||
the start of the conversation.
|
||||
"""
|
||||
return self._entry_points
|
||||
|
||||
|
@ -387,7 +445,7 @@ class ConversationHandler(Handler[Update, CCT]):
|
|||
def states(self) -> Dict[object, List[Handler]]:
|
||||
"""Dict[:obj:`object`, List[:class:`telegram.ext.Handler`]]: A :obj:`dict` that
|
||||
defines the different states of conversation a user can be in and one or more
|
||||
associated ``Handler`` objects that should be used in that state.
|
||||
associated :obj:`Handler` objects that should be used in that state.
|
||||
"""
|
||||
return self._states
|
||||
|
||||
|
@ -399,7 +457,7 @@ class ConversationHandler(Handler[Update, CCT]):
|
|||
def fallbacks(self) -> List[Handler]:
|
||||
"""List[:class:`telegram.ext.Handler`]: A list of handlers that might be used if
|
||||
the user is in a conversation, but every handler for their current state returned
|
||||
:obj:`False` on :attr:`check_update`.
|
||||
:obj:`False` on :meth:`check_update`.
|
||||
"""
|
||||
return self._fallbacks
|
||||
|
||||
|
@ -470,6 +528,18 @@ class ConversationHandler(Handler[Update, CCT]):
|
|||
def name(self, value: object) -> NoReturn:
|
||||
raise AttributeError("You can not assign a new value to name after initialization.")
|
||||
|
||||
@property
|
||||
def persistent(self) -> bool:
|
||||
""":obj:`bool`: Optional. If the conversations dict for this handler should be
|
||||
saved. :attr:`name` is required and persistence has to be set in
|
||||
:attr:`Application <.Application.persistence>`.
|
||||
"""
|
||||
return self._persistent
|
||||
|
||||
@persistent.setter
|
||||
def persistent(self, value: object) -> NoReturn:
|
||||
raise AttributeError("You can not assign a new value to persistent after initialization.")
|
||||
|
||||
@property
|
||||
def map_to_parent(self) -> Optional[Dict[object, object]]:
|
||||
"""Dict[:obj:`object`, :obj:`object`]: Optional. A :obj:`dict` that can be
|
||||
|
@ -484,96 +554,132 @@ class ConversationHandler(Handler[Update, CCT]):
|
|||
"You can not assign a new value to map_to_parent after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def persistence(self) -> Optional[BasePersistence]:
|
||||
"""The persistence class as provided by the :class:`Dispatcher`."""
|
||||
return self._persistence
|
||||
async def _initialize_persistence(
|
||||
self, application: 'Application'
|
||||
) -> Dict[str, TrackingDict[ConversationKey, object]]:
|
||||
"""Initializes the persistence for this handler and its child conversations.
|
||||
While this method is marked as protected, we expect it to be called by the
|
||||
Application/parent conversations. It's just protected to hide it from users.
|
||||
|
||||
@persistence.setter
|
||||
def persistence(self, persistence: BasePersistence) -> None:
|
||||
self._persistence = persistence
|
||||
# Set persistence for nested conversations
|
||||
for handlers in self.states.values():
|
||||
for handler in handlers:
|
||||
if isinstance(handler, ConversationHandler):
|
||||
handler.persistence = self.persistence
|
||||
Args:
|
||||
application (:class:`telegram.ext.Application`): The application.
|
||||
|
||||
@property
|
||||
def conversations(self) -> ConversationDict: # skipcq: PY-D0003
|
||||
return self._conversations
|
||||
Returns:
|
||||
A dict {conversation.name -> TrackingDict}, which contains all dict of this
|
||||
conversation and possible child conversations.
|
||||
|
||||
@conversations.setter
|
||||
def conversations(self, value: ConversationDict) -> None:
|
||||
self._conversations = value
|
||||
# Set conversations for nested conversations
|
||||
for handlers in self.states.values():
|
||||
for handler in handlers:
|
||||
if isinstance(handler, ConversationHandler) and self.persistence and handler.name:
|
||||
handler.conversations = self.persistence.get_conversations(handler.name)
|
||||
"""
|
||||
if not (self.persistent and self.name and application.persistence):
|
||||
raise RuntimeError(
|
||||
'This handler is not persistent, has no name or the application has no '
|
||||
'persistence!'
|
||||
)
|
||||
|
||||
def _get_key(self, update: Update) -> Tuple[int, ...]:
|
||||
current_conversations = self._conversations
|
||||
self._conversations = cast(
|
||||
TrackingDict[ConversationKey, object],
|
||||
TrackingDict(),
|
||||
)
|
||||
# In the conversation already processed updates
|
||||
self._conversations.update(current_conversations)
|
||||
# above might be partly overridden but that's okay since we warn about that in
|
||||
# add_handler
|
||||
self._conversations.update_no_track(
|
||||
await application.persistence.get_conversations(self.name)
|
||||
)
|
||||
|
||||
out = {self.name: self._conversations}
|
||||
|
||||
for handler in self._child_conversations:
|
||||
out.update(
|
||||
await handler._initialize_persistence( # pylint: disable=protected-access
|
||||
application=application
|
||||
)
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
def _get_key(self, update: Update) -> ConversationKey:
|
||||
"""Builds the conversation key associated with the update."""
|
||||
chat = update.effective_chat
|
||||
user = update.effective_user
|
||||
|
||||
key = []
|
||||
key: List[Union[int, str]] = []
|
||||
|
||||
if self.per_chat:
|
||||
key.append(chat.id) # type: ignore[union-attr]
|
||||
if chat is None:
|
||||
raise RuntimeError("Can't build key for update without effective chat!")
|
||||
key.append(chat.id)
|
||||
|
||||
if self.per_user and user is not None:
|
||||
if self.per_user:
|
||||
if user is None:
|
||||
raise RuntimeError("Can't build key for update without effective user!")
|
||||
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]
|
||||
)
|
||||
if update.callback_query is None:
|
||||
raise RuntimeError("Can't build key for update without CallbackQuery!")
|
||||
if update.callback_query.inline_message_id:
|
||||
key.append(update.callback_query.inline_message_id)
|
||||
else:
|
||||
key.append(update.callback_query.message.message_id) # type: ignore[union-attr]
|
||||
|
||||
return tuple(key)
|
||||
|
||||
def _resolve_promise(self, state: Tuple) -> object:
|
||||
old_state, new_state = state
|
||||
async def _schedule_job_delayed(
|
||||
self,
|
||||
new_state: asyncio.Task,
|
||||
application: 'Application[Any, CCT, Any, Any, Any, JobQueue]',
|
||||
update: Update,
|
||||
context: CallbackContext,
|
||||
conversation_key: ConversationKey,
|
||||
) -> None:
|
||||
try:
|
||||
res = new_state.result(0)
|
||||
res = res if res is not None else old_state
|
||||
effective_new_state = await new_state
|
||||
except Exception as exc:
|
||||
self.logger.exception("Promise function raised exception")
|
||||
self.logger.exception("%s", exc)
|
||||
res = old_state
|
||||
finally:
|
||||
if res is None and old_state is None:
|
||||
res = self.END
|
||||
return res
|
||||
_logger.debug(
|
||||
'Non-blocking handler callback raised exception. Not scheduling conversation '
|
||||
'timeout.',
|
||||
exc_info=exc,
|
||||
)
|
||||
return
|
||||
return self._schedule_job(
|
||||
new_state=effective_new_state,
|
||||
application=application,
|
||||
update=update,
|
||||
context=context,
|
||||
conversation_key=conversation_key,
|
||||
)
|
||||
|
||||
def _schedule_job(
|
||||
self,
|
||||
new_state: object,
|
||||
dispatcher: 'Dispatcher[Any, CCT, Any, Any, Any, JobQueue, Any]',
|
||||
application: 'Application[Any, CCT, Any, Any, Any, JobQueue]',
|
||||
update: Update,
|
||||
context: CallbackContext,
|
||||
conversation_key: Tuple[int, ...],
|
||||
conversation_key: ConversationKey,
|
||||
) -> None:
|
||||
if new_state != self.END:
|
||||
try:
|
||||
# both job_queue & conversation_timeout are checked before calling _schedule_job
|
||||
j_queue = dispatcher.job_queue
|
||||
self.timeout_jobs[conversation_key] = j_queue.run_once(
|
||||
self._trigger_timeout,
|
||||
self.conversation_timeout, # type: ignore[arg-type]
|
||||
context=_ConversationTimeoutContext(
|
||||
conversation_key, update, dispatcher, context
|
||||
),
|
||||
)
|
||||
except Exception as exc:
|
||||
self.logger.exception(
|
||||
"Failed to schedule timeout job due to the following exception:"
|
||||
)
|
||||
self.logger.exception("%s", exc)
|
||||
"""Schedules a job which executes :meth:`_trigger_timeout` upon conversation timeout."""
|
||||
if new_state == self.END:
|
||||
return
|
||||
|
||||
try:
|
||||
# both job_queue & conversation_timeout are checked before calling _schedule_job
|
||||
j_queue = application.job_queue
|
||||
self.timeout_jobs[conversation_key] = j_queue.run_once(
|
||||
self._trigger_timeout,
|
||||
self.conversation_timeout, # type: ignore[arg-type]
|
||||
context=_ConversationTimeoutContext(
|
||||
conversation_key, update, application, context
|
||||
),
|
||||
)
|
||||
except Exception as exc:
|
||||
_logger.exception("Failed to schedule timeout.", exc_info=exc)
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def check_update(self, update: object) -> CheckUpdateType:
|
||||
def check_update(self, update: object) -> Optional[_CheckUpdateType]:
|
||||
"""
|
||||
Determines whether an update should be handled by this conversationhandler, and if so in
|
||||
Determines whether an update should be handled by this conversation handler, and if so in
|
||||
which state the conversation currently is.
|
||||
|
||||
Args:
|
||||
|
@ -596,32 +702,31 @@ class ConversationHandler(Handler[Update, CCT]):
|
|||
return None
|
||||
|
||||
key = self._get_key(update)
|
||||
with self._conversations_lock:
|
||||
state = self.conversations.get(key)
|
||||
state = self._conversations.get(key)
|
||||
check: Optional[object] = None
|
||||
|
||||
# Resolve promises
|
||||
if isinstance(state, tuple) and len(state) == 2 and isinstance(state[1], Promise):
|
||||
self.logger.debug('waiting for promise...')
|
||||
# Resolve futures
|
||||
if isinstance(state, PendingState):
|
||||
_logger.debug('Waiting for asyncio Task to finish ...')
|
||||
|
||||
# check if promise is finished or not
|
||||
if state[1].done.wait(0):
|
||||
res = self._resolve_promise(state)
|
||||
# check if future is finished or not
|
||||
if state.done():
|
||||
res = state.resolve()
|
||||
self._update_state(res, key)
|
||||
with self._conversations_lock:
|
||||
state = self.conversations.get(key)
|
||||
state = self._conversations.get(key)
|
||||
|
||||
# if not then handle WAITING state instead
|
||||
else:
|
||||
hdlrs = self.states.get(self.WAITING, [])
|
||||
for hdlr in hdlrs:
|
||||
check = hdlr.check_update(update)
|
||||
handlers = self.states.get(self.WAITING, [])
|
||||
for handler_ in handlers:
|
||||
check = handler_.check_update(update)
|
||||
if check is not None and check is not False:
|
||||
return key, hdlr, check
|
||||
return self.WAITING, key, handler_, check
|
||||
return None
|
||||
|
||||
self.logger.debug('selecting conversation %s with state %s', str(key), str(state))
|
||||
_logger.debug('Selecting conversation %s with state %s', str(key), str(state))
|
||||
|
||||
handler = None
|
||||
handler: Optional[Handler] = None
|
||||
|
||||
# Search entry points for a match
|
||||
if state is None or self.allow_reentry:
|
||||
|
@ -636,10 +741,8 @@ class ConversationHandler(Handler[Update, CCT]):
|
|||
return None
|
||||
|
||||
# Get the handler list for current state, if we didn't find one yet and we're still here
|
||||
if state is not None and not handler:
|
||||
handlers = self.states.get(state)
|
||||
|
||||
for candidate in handlers or []:
|
||||
if state is not None and handler is None:
|
||||
for candidate in self.states.get(state, []):
|
||||
check = candidate.check_update(update)
|
||||
if check is not None and check is not False:
|
||||
handler = candidate
|
||||
|
@ -656,128 +759,161 @@ class ConversationHandler(Handler[Update, CCT]):
|
|||
else:
|
||||
return None
|
||||
|
||||
return key, handler, check # type: ignore[return-value]
|
||||
return state, key, handler, check # type: ignore[return-value]
|
||||
|
||||
def handle_update( # type: ignore[override]
|
||||
async def handle_update( # type: ignore[override]
|
||||
self,
|
||||
update: Update,
|
||||
dispatcher: 'Dispatcher',
|
||||
check_result: CheckUpdateType,
|
||||
application: 'Application',
|
||||
check_result: _CheckUpdateType,
|
||||
context: CallbackContext,
|
||||
) -> Optional[object]:
|
||||
"""Send the update to the callback for the current state and Handler
|
||||
|
||||
Args:
|
||||
check_result: The result from check_update. For this handler it's a tuple of key,
|
||||
handler, and the handler's check result.
|
||||
check_result: The result from :meth:`check_update`. For this handler it's a tuple of
|
||||
the conversation state, key, handler, and the handler's check result.
|
||||
update (:class:`telegram.Update`): Incoming telegram update.
|
||||
dispatcher (:class:`telegram.ext.Dispatcher`): Dispatcher that originated the Update.
|
||||
application (:class:`telegram.ext.Application`): Application that originated the
|
||||
update.
|
||||
context (:class:`telegram.ext.CallbackContext`): The context as provided by
|
||||
the dispatcher.
|
||||
the application.
|
||||
|
||||
"""
|
||||
conversation_key, handler, check_result = check_result # type: ignore[assignment,misc]
|
||||
current_state, conversation_key, handler, handler_check_result = check_result
|
||||
raise_dp_handler_stop = False
|
||||
|
||||
with self._timeout_jobs_lock:
|
||||
async with self._timeout_jobs_lock:
|
||||
# Remove the old timeout job (if present)
|
||||
timeout_job = self.timeout_jobs.pop(conversation_key, None)
|
||||
|
||||
if timeout_job is not None:
|
||||
timeout_job.schedule_removal()
|
||||
try:
|
||||
new_state = handler.handle_update(update, dispatcher, check_result, context)
|
||||
except DispatcherHandlerStop as exception:
|
||||
|
||||
# Resolution order of "block":
|
||||
# 1. Setting of the selected handler
|
||||
# 2. Setting of the ConversationHandler
|
||||
# 3. Default values of the bot
|
||||
if handler.block is not DEFAULT_TRUE:
|
||||
block = handler.block
|
||||
else:
|
||||
if self._block is not DEFAULT_TRUE:
|
||||
block = self._block
|
||||
elif isinstance(application.bot, ExtBot) and application.bot.defaults is not None:
|
||||
block = application.bot.defaults.block
|
||||
else:
|
||||
block = DefaultValue.get_value(handler.block)
|
||||
|
||||
try: # Now create task or await the callback
|
||||
if block:
|
||||
new_state: object = await handler.handle_update(
|
||||
update, application, handler_check_result, context
|
||||
)
|
||||
else:
|
||||
new_state = application.create_task(
|
||||
coroutine=handler.handle_update(
|
||||
update, application, handler_check_result, context
|
||||
),
|
||||
update=update,
|
||||
)
|
||||
except ApplicationHandlerStop as exception:
|
||||
new_state = exception.state
|
||||
raise_dp_handler_stop = True
|
||||
with self._timeout_jobs_lock:
|
||||
async with self._timeout_jobs_lock:
|
||||
if self.conversation_timeout:
|
||||
if dispatcher.job_queue is not None:
|
||||
# Add the new timeout job
|
||||
if isinstance(new_state, Promise):
|
||||
new_state.add_done_callback(
|
||||
functools.partial(
|
||||
self._schedule_job,
|
||||
dispatcher=dispatcher,
|
||||
update=update,
|
||||
context=context,
|
||||
conversation_key=conversation_key,
|
||||
)
|
||||
)
|
||||
elif new_state != self.END:
|
||||
self._schedule_job(
|
||||
new_state, dispatcher, update, context, conversation_key
|
||||
)
|
||||
else:
|
||||
if application.job_queue is None:
|
||||
warn(
|
||||
"Ignoring `conversation_timeout` because the Dispatcher has no JobQueue.",
|
||||
"Ignoring `conversation_timeout` because the Application has no JobQueue.",
|
||||
)
|
||||
elif not application.job_queue.scheduler.running:
|
||||
warn(
|
||||
"Ignoring `conversation_timeout` because the Applications JobQueue is "
|
||||
"not running.",
|
||||
)
|
||||
else:
|
||||
# Add the new timeout job
|
||||
# checking if the new state is self.END is done in _schedule_job
|
||||
if isinstance(new_state, asyncio.Task):
|
||||
application.create_task(
|
||||
self._schedule_job_delayed(
|
||||
new_state, application, update, context, conversation_key
|
||||
),
|
||||
update=update,
|
||||
)
|
||||
else:
|
||||
self._schedule_job(
|
||||
new_state, application, update, context, conversation_key
|
||||
)
|
||||
|
||||
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))
|
||||
raise ApplicationHandlerStop(self.map_to_parent.get(new_state))
|
||||
return self.map_to_parent.get(new_state)
|
||||
|
||||
self._update_state(new_state, conversation_key)
|
||||
if current_state != self.WAITING:
|
||||
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()
|
||||
raise ApplicationHandlerStop()
|
||||
# Signals a possible parent conversation to stay in the current state
|
||||
return None
|
||||
|
||||
def _update_state(self, new_state: object, key: Tuple[int, ...]) -> None:
|
||||
def _update_state(self, new_state: object, key: ConversationKey) -> None:
|
||||
if new_state == self.END:
|
||||
with self._conversations_lock:
|
||||
if key in self.conversations:
|
||||
# If there is no key in conversations, nothing is done.
|
||||
del self.conversations[key]
|
||||
if self.persistent and self.persistence and self.name:
|
||||
self.persistence.update_conversation(self.name, key, None)
|
||||
if key in self._conversations:
|
||||
# If there is no key in conversations, nothing is done.
|
||||
del self._conversations[key]
|
||||
|
||||
elif isinstance(new_state, Promise):
|
||||
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)
|
||||
)
|
||||
elif isinstance(new_state, asyncio.Task):
|
||||
self._conversations[key] = PendingState(
|
||||
old_state=self._conversations.get(key), task=new_state
|
||||
)
|
||||
|
||||
elif new_state is not None:
|
||||
if new_state not in self.states:
|
||||
warn(
|
||||
f"Handler returned state {new_state} which is unknown to the "
|
||||
f"ConversationHandler{' ' + self.name if self.name is not None else ''}.",
|
||||
stacklevel=2,
|
||||
)
|
||||
with self._conversations_lock:
|
||||
self.conversations[key] = new_state
|
||||
if self.persistent and self.persistence and self.name:
|
||||
self.persistence.update_conversation(self.name, key, new_state)
|
||||
|
||||
def _trigger_timeout(self, context: CallbackContext) -> None:
|
||||
self.logger.debug('conversation timeout was triggered!')
|
||||
self._conversations[key] = new_state
|
||||
|
||||
async def _trigger_timeout(self, context: CallbackContext) -> None:
|
||||
"""This is run whenever a conversation has timed out. Also makes sure that all handlers
|
||||
which are in the :attr:`TIMEOUT` state and whose :meth:`Handler.check_update` returns
|
||||
:obj:`True` is handled.
|
||||
"""
|
||||
job = cast('Job', context.job)
|
||||
ctxt = cast(_ConversationTimeoutContext, job.context)
|
||||
|
||||
_logger.debug(
|
||||
'Conversation timeout was triggered for conversation %s!', ctxt.conversation_key
|
||||
)
|
||||
|
||||
callback_context = ctxt.callback_context
|
||||
|
||||
with self._timeout_jobs_lock:
|
||||
found_job = self.timeout_jobs[ctxt.conversation_key]
|
||||
async with self._timeout_jobs_lock:
|
||||
found_job = self.timeout_jobs.get(ctxt.conversation_key)
|
||||
if found_job is not job:
|
||||
# The timeout has been cancelled in handle_update
|
||||
return
|
||||
del self.timeout_jobs[ctxt.conversation_key]
|
||||
|
||||
# Now run all handlers which are in TIMEOUT state
|
||||
handlers = self.states.get(self.TIMEOUT, [])
|
||||
for handler in handlers:
|
||||
check = handler.check_update(ctxt.update)
|
||||
if check is not None and check is not False:
|
||||
try:
|
||||
handler.handle_update(ctxt.update, ctxt.dispatcher, check, callback_context)
|
||||
except DispatcherHandlerStop:
|
||||
await handler.handle_update(
|
||||
ctxt.update, ctxt.application, check, callback_context
|
||||
)
|
||||
except ApplicationHandlerStop:
|
||||
warn(
|
||||
'DispatcherHandlerStop in TIMEOUT state of '
|
||||
'ApplicationHandlerStop in TIMEOUT state of '
|
||||
'ConversationHandler has no effect. Ignoring.',
|
||||
)
|
||||
|
||||
|
|
|
@ -17,20 +17,23 @@
|
|||
# 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=no-self-use
|
||||
"""This module contains the class Defaults, which allows to pass default values to Updater."""
|
||||
"""This module contains the class Defaults, which allows passing default values to Application."""
|
||||
from typing import NoReturn, Optional, Dict, Any
|
||||
|
||||
import pytz
|
||||
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.types import ODVInput
|
||||
|
||||
|
||||
class Defaults:
|
||||
"""Convenience Class to gather all parameters with a (user defined) default value
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
Removed the argument and attribute ``timeout``. Specify default timeout behavior for the
|
||||
networking backend directly via :class:`telegram.ext.ApplicationBuilder` instead.
|
||||
|
||||
|
||||
Parameters:
|
||||
parse_mode (:obj:`str`, optional): Send Markdown or HTML, if you want Telegram apps to show
|
||||
parse_mode (:obj:`str`, optional): Send :attr:`~telegram.constants.ParseMode.MARKDOWN` or
|
||||
:attr:`~telegram.constants.ParseMode.HTML`, if you want Telegram apps to show
|
||||
bold, italic, fixed-width text or URLs in your bot's message.
|
||||
disable_notification (:obj:`bool`, optional): Sends the message silently. Users will
|
||||
receive a notification with no sound.
|
||||
|
@ -38,22 +41,16 @@ class Defaults:
|
|||
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).
|
||||
|
||||
Note:
|
||||
Will *not* be used for :meth:`telegram.Bot.get_updates`!
|
||||
quote (:obj:`bool`, optional): If set to :obj:`True`, the reply is sent as an actual reply
|
||||
to the message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter will
|
||||
be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats.
|
||||
tzinfo (:obj:`tzinfo`, optional): A timezone to be used for all date(time) inputs
|
||||
appearing throughout PTB, i.e. if a timezone naive date(time) object is passed
|
||||
somewhere, it will be assumed to be in ``tzinfo``. Must be a timezone provided by the
|
||||
``pytz`` module. Defaults to UTC.
|
||||
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`.
|
||||
somewhere, it will be assumed to be in :paramref:`tzinfo`. Must be a timezone provided
|
||||
by the ``pytz`` module. Defaults to UTC.
|
||||
block (:obj:`bool`, optional): Default setting for the :paramref:`Handler.block` parameter
|
||||
of handlers and error handlers registered through :meth:`Application.add_handler` and
|
||||
:meth:`Application.add_error_handler`. Defaults to :obj:`True`.
|
||||
protect_content (:obj:`bool`, optional): Protects the contents of the sent message from
|
||||
forwarding and saving.
|
||||
|
||||
|
@ -61,10 +58,9 @@ class Defaults:
|
|||
"""
|
||||
|
||||
__slots__ = (
|
||||
'_timeout',
|
||||
'_tzinfo',
|
||||
'_disable_web_page_preview',
|
||||
'_run_async',
|
||||
'_block',
|
||||
'_quote',
|
||||
'_disable_notification',
|
||||
'_allow_sending_without_reply',
|
||||
|
@ -78,12 +74,9 @@ class Defaults:
|
|||
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: ODVInput[float] = DEFAULT_NONE,
|
||||
quote: bool = None,
|
||||
tzinfo: pytz.BaseTzInfo = pytz.utc,
|
||||
run_async: bool = False,
|
||||
block: bool = True,
|
||||
allow_sending_without_reply: bool = None,
|
||||
protect_content: bool = None,
|
||||
):
|
||||
|
@ -91,10 +84,9 @@ class Defaults:
|
|||
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
|
||||
self._block = block
|
||||
self._protect_content = protect_content
|
||||
|
||||
# Gather all defaults that actually have a default value
|
||||
|
@ -108,11 +100,8 @@ class Defaults:
|
|||
'protect_content',
|
||||
):
|
||||
value = getattr(self, kwarg)
|
||||
if value not in [None, DEFAULT_NONE]:
|
||||
if value is not None:
|
||||
self._api_defaults[kwarg] = value
|
||||
# Special casing, as None is a valid default value
|
||||
if self._timeout != DEFAULT_NONE:
|
||||
self._api_defaults['timeout'] = self._timeout
|
||||
|
||||
@property
|
||||
def api_defaults(self) -> Dict[str, Any]: # skip-cq: PY-D0003
|
||||
|
@ -181,18 +170,6 @@ class Defaults:
|
|||
"You can not assign a new value to allow_sending_without_reply after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def timeout(self) -> ODVInput[float]:
|
||||
""":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).
|
||||
"""
|
||||
return self._timeout
|
||||
|
||||
@timeout.setter
|
||||
def timeout(self, value: object) -> NoReturn:
|
||||
raise AttributeError("You can not assign a new value to timeout after initialization.")
|
||||
|
||||
@property
|
||||
def quote(self) -> Optional[bool]:
|
||||
""":obj:`bool`: Optional. If set to :obj:`True`, the reply is sent as an actual reply
|
||||
|
@ -217,16 +194,16 @@ class Defaults:
|
|||
raise AttributeError("You can not assign a new value to tzinfo after initialization.")
|
||||
|
||||
@property
|
||||
def run_async(self) -> bool:
|
||||
""":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`.
|
||||
def block(self) -> bool:
|
||||
""":obj:`bool`: Optional. Default setting for the :paramref:`Handler.block` parameter of
|
||||
handlers and error handlers registered through :meth:`Application.add_handler` and
|
||||
:meth:`Application.add_error_handler`.
|
||||
"""
|
||||
return self._run_async
|
||||
return self._block
|
||||
|
||||
@run_async.setter
|
||||
def run_async(self, value: object) -> NoReturn:
|
||||
raise AttributeError("You can not assign a new value to run_async after initialization.")
|
||||
@block.setter
|
||||
def block(self, value: object) -> NoReturn:
|
||||
raise AttributeError("You can not assign a new value to block after initialization.")
|
||||
|
||||
@property
|
||||
def protect_content(self) -> Optional[bool]:
|
||||
|
@ -250,10 +227,9 @@ class Defaults:
|
|||
self._disable_notification,
|
||||
self._disable_web_page_preview,
|
||||
self._allow_sending_without_reply,
|
||||
self._timeout,
|
||||
self._quote,
|
||||
self._tzinfo,
|
||||
self._run_async,
|
||||
self._block,
|
||||
self._protect_content,
|
||||
)
|
||||
)
|
||||
|
|
|
@ -18,13 +18,13 @@
|
|||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the DictPersistence class."""
|
||||
|
||||
from typing import Dict, Optional, Tuple, cast
|
||||
from typing import Dict, Optional, cast
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from telegram.ext import BasePersistence, PersistenceInput
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram.ext._utils.types import ConversationDict, CDCData
|
||||
from telegram.ext._utils.types import ConversationDict, CDCData, ConversationKey
|
||||
|
||||
try:
|
||||
import ujson as json
|
||||
|
@ -37,8 +37,8 @@ class DictPersistence(BasePersistence):
|
|||
|
||||
Attention:
|
||||
The interface provided by this class is intended to be accessed exclusively by
|
||||
:class:`~telegram.ext.Dispatcher`. Calling any of the methods below manually might
|
||||
interfere with the integration of persistence into :class:`~telegram.ext.Dispatcher`.
|
||||
:class:`~telegram.ext.Application`. Calling any of the methods below manually might
|
||||
interfere with the integration of persistence into :class:`~telegram.ext.Application`.
|
||||
|
||||
Note:
|
||||
* Data managed by :class:`DictPersistence` is in-memory only and will be lost when the bot
|
||||
|
@ -62,12 +62,18 @@ class DictPersistence(BasePersistence):
|
|||
chat_data on creating this persistence. Default is ``""``.
|
||||
bot_data_json (:obj:`str`, optional): JSON string that will be used to reconstruct
|
||||
bot_data on creating this persistence. Default is ``""``.
|
||||
conversations_json (:obj:`str`, optional): JSON string that will be used to reconstruct
|
||||
conversation on creating this persistence. Default is ``""``.
|
||||
callback_data_json (:obj:`str`, optional): Json string that will be used to reconstruct
|
||||
callback_data on creating this persistence. Default is ``""``.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
conversations_json (:obj:`str`, optional): JSON string that will be used to reconstruct
|
||||
conversation on creating this persistence. Default is ``""``.
|
||||
update_interval (:obj:`int` | :obj:`float`, optional): The
|
||||
:class:`~telegram.ext.Application` will update
|
||||
the persistence in regular intervals. This parameter specifies the time (in seconds) to
|
||||
wait between two consecutive runs of updating the persistence. Defaults to 60 seconds.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
||||
Attributes:
|
||||
store_data (:class:`PersistenceInput`): Specifies which kinds of data will be saved by this
|
||||
|
@ -95,8 +101,9 @@ class DictPersistence(BasePersistence):
|
|||
bot_data_json: str = '',
|
||||
conversations_json: str = '',
|
||||
callback_data_json: str = '',
|
||||
update_interval: float = 60,
|
||||
):
|
||||
super().__init__(store_data=store_data)
|
||||
super().__init__(store_data=store_data, update_interval=update_interval)
|
||||
self._user_data = None
|
||||
self._chat_data = None
|
||||
self._bot_data = None
|
||||
|
@ -230,9 +237,11 @@ class DictPersistence(BasePersistence):
|
|||
""":obj:`str`: The conversations serialized as a JSON-string."""
|
||||
if self._conversations_json:
|
||||
return self._conversations_json
|
||||
return self._encode_conversations_to_json(self.conversations) # type: ignore[arg-type]
|
||||
if self.conversations:
|
||||
return self._encode_conversations_to_json(self.conversations)
|
||||
return json.dumps(self.conversations)
|
||||
|
||||
def get_user_data(self) -> Dict[int, Dict[object, object]]:
|
||||
async def get_user_data(self) -> Dict[int, Dict[object, object]]:
|
||||
"""Returns the user_data created from the ``user_data_json`` or an empty :obj:`dict`.
|
||||
|
||||
Returns:
|
||||
|
@ -242,7 +251,7 @@ class DictPersistence(BasePersistence):
|
|||
self._user_data = {}
|
||||
return deepcopy(self.user_data) # type: ignore[arg-type]
|
||||
|
||||
def get_chat_data(self) -> Dict[int, Dict[object, object]]:
|
||||
async def get_chat_data(self) -> Dict[int, Dict[object, object]]:
|
||||
"""Returns the chat_data created from the ``chat_data_json`` or an empty :obj:`dict`.
|
||||
|
||||
Returns:
|
||||
|
@ -252,7 +261,7 @@ class DictPersistence(BasePersistence):
|
|||
self._chat_data = {}
|
||||
return deepcopy(self.chat_data) # type: ignore[arg-type]
|
||||
|
||||
def get_bot_data(self) -> Dict[object, object]:
|
||||
async def get_bot_data(self) -> Dict[object, object]:
|
||||
"""Returns the bot_data created from the ``bot_data_json`` or an empty :obj:`dict`.
|
||||
|
||||
Returns:
|
||||
|
@ -262,7 +271,7 @@ class DictPersistence(BasePersistence):
|
|||
self._bot_data = {}
|
||||
return deepcopy(self.bot_data) # type: ignore[arg-type]
|
||||
|
||||
def get_callback_data(self) -> Optional[CDCData]:
|
||||
async def get_callback_data(self) -> Optional[CDCData]:
|
||||
"""Returns the callback_data created from the ``callback_data_json`` or :obj:`None`.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
@ -275,9 +284,9 @@ class DictPersistence(BasePersistence):
|
|||
if self.callback_data is None:
|
||||
self._callback_data = None
|
||||
return None
|
||||
return deepcopy((self.callback_data[0], self.callback_data[1].copy()))
|
||||
return deepcopy(self.callback_data)
|
||||
|
||||
def get_conversations(self, name: str) -> ConversationDict:
|
||||
async def get_conversations(self, name: str) -> ConversationDict:
|
||||
"""Returns the conversations created from the ``conversations_json`` or an empty
|
||||
:obj:`dict`.
|
||||
|
||||
|
@ -288,8 +297,8 @@ 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]
|
||||
async def update_conversation(
|
||||
self, name: str, key: ConversationKey, new_state: Optional[object]
|
||||
) -> None:
|
||||
"""Will update the conversations for the given handler.
|
||||
|
||||
|
@ -305,46 +314,46 @@ class DictPersistence(BasePersistence):
|
|||
self._conversations[name][key] = new_state
|
||||
self._conversations_json = None
|
||||
|
||||
def update_user_data(self, user_id: int, data: Dict) -> None:
|
||||
async def update_user_data(self, user_id: int, data: Dict) -> None:
|
||||
"""Will update the user_data (if changed).
|
||||
|
||||
Args:
|
||||
user_id (:obj:`int`): The user the data might have been changed for.
|
||||
data (:obj:`dict`): The :attr:`telegram.ext.Dispatcher.user_data` ``[user_id]``.
|
||||
data (:obj:`dict`): The :attr:`telegram.ext.Application.user_data` ``[user_id]``.
|
||||
"""
|
||||
if self._user_data is None:
|
||||
self._user_data = {}
|
||||
if self._user_data.get(user_id) == data:
|
||||
return
|
||||
self._user_data[user_id] = deepcopy(data)
|
||||
self._user_data[user_id] = data
|
||||
self._user_data_json = None
|
||||
|
||||
def update_chat_data(self, chat_id: int, data: Dict) -> None:
|
||||
async def update_chat_data(self, chat_id: int, data: Dict) -> None:
|
||||
"""Will update the chat_data (if changed).
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int`): The chat the data might have been changed for.
|
||||
data (:obj:`dict`): The :attr:`telegram.ext.Dispatcher.chat_data` ``[chat_id]``.
|
||||
data (:obj:`dict`): The :attr:`telegram.ext.Application.chat_data` ``[chat_id]``.
|
||||
"""
|
||||
if self._chat_data is None:
|
||||
self._chat_data = {}
|
||||
if self._chat_data.get(chat_id) == data:
|
||||
return
|
||||
self._chat_data[chat_id] = deepcopy(data)
|
||||
self._chat_data[chat_id] = data
|
||||
self._chat_data_json = None
|
||||
|
||||
def update_bot_data(self, data: Dict) -> None:
|
||||
async def update_bot_data(self, data: Dict) -> None:
|
||||
"""Will update the bot_data (if changed).
|
||||
|
||||
Args:
|
||||
data (:obj:`dict`): The :attr:`telegram.ext.Dispatcher.bot_data`.
|
||||
data (:obj:`dict`): The :attr:`telegram.ext.Application.bot_data`.
|
||||
"""
|
||||
if self._bot_data == data:
|
||||
return
|
||||
self._bot_data = deepcopy(data)
|
||||
self._bot_data = data
|
||||
self._bot_data_json = None
|
||||
|
||||
def update_callback_data(self, data: CDCData) -> None:
|
||||
async def update_callback_data(self, data: CDCData) -> None:
|
||||
"""Will update the callback_data (if changed).
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
@ -356,10 +365,10 @@ class DictPersistence(BasePersistence):
|
|||
"""
|
||||
if self._callback_data == data:
|
||||
return
|
||||
self._callback_data = deepcopy((data[0], data[1].copy()))
|
||||
self._callback_data = data
|
||||
self._callback_data_json = None
|
||||
|
||||
def drop_chat_data(self, chat_id: int) -> None:
|
||||
async def drop_chat_data(self, chat_id: int) -> None:
|
||||
"""Will delete the specified key from the :attr:`chat_data`.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
@ -372,7 +381,7 @@ class DictPersistence(BasePersistence):
|
|||
self._chat_data.pop(chat_id, None)
|
||||
self._chat_data_json = None
|
||||
|
||||
def drop_user_data(self, user_id: int) -> None:
|
||||
async def drop_user_data(self, user_id: int) -> None:
|
||||
"""Will delete the specified key from the :attr:`user_data`.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
@ -385,28 +394,28 @@ class DictPersistence(BasePersistence):
|
|||
self._user_data.pop(user_id, None)
|
||||
self._user_data_json = None
|
||||
|
||||
def refresh_user_data(self, user_id: int, user_data: Dict) -> None:
|
||||
async def refresh_user_data(self, user_id: int, user_data: Dict) -> None:
|
||||
"""Does nothing.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
.. seealso:: :meth:`telegram.ext.BasePersistence.refresh_user_data`
|
||||
"""
|
||||
|
||||
def refresh_chat_data(self, chat_id: int, chat_data: Dict) -> None:
|
||||
async def refresh_chat_data(self, chat_id: int, chat_data: Dict) -> None:
|
||||
"""Does nothing.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
.. seealso:: :meth:`telegram.ext.BasePersistence.refresh_chat_data`
|
||||
"""
|
||||
|
||||
def refresh_bot_data(self, bot_data: Dict) -> None:
|
||||
async def refresh_bot_data(self, bot_data: Dict) -> None:
|
||||
"""Does nothing.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
.. seealso:: :meth:`telegram.ext.BasePersistence.refresh_bot_data`
|
||||
"""
|
||||
|
||||
def flush(self) -> None:
|
||||
async def flush(self) -> None:
|
||||
"""Does nothing.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
@ -414,7 +423,7 @@ class DictPersistence(BasePersistence):
|
|||
"""
|
||||
|
||||
@staticmethod
|
||||
def _encode_conversations_to_json(conversations: Dict[str, Dict[Tuple, object]]) -> str:
|
||||
def _encode_conversations_to_json(conversations: Dict[str, ConversationDict]) -> str:
|
||||
"""Helper method to encode a conversations dict (that uses tuples as keys) to a
|
||||
JSON-serializable way. Use :meth:`self._decode_conversations_from_json` to decode.
|
||||
|
||||
|
@ -432,7 +441,7 @@ class DictPersistence(BasePersistence):
|
|||
return json.dumps(tmp)
|
||||
|
||||
@staticmethod
|
||||
def _decode_conversations_from_json(json_string: str) -> Dict[str, Dict[Tuple, object]]:
|
||||
def _decode_conversations_from_json(json_string: str) -> Dict[str, ConversationDict]:
|
||||
"""Helper method to decode a conversations dict (that uses tuples as keys) from a
|
||||
JSON-string created with :meth:`self._encode_conversations_to_json`.
|
||||
|
||||
|
@ -443,7 +452,7 @@ class DictPersistence(BasePersistence):
|
|||
:obj:`dict`: The conversations dict after decoding
|
||||
"""
|
||||
tmp = json.loads(json_string)
|
||||
conversations: Dict[str, Dict[Tuple, object]] = {}
|
||||
conversations: Dict[str, ConversationDict] = {}
|
||||
for handler, states in tmp.items():
|
||||
conversations[handler] = {}
|
||||
for key, state in states.items():
|
||||
|
|
|
@ -1,893 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2022
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the Dispatcher class."""
|
||||
import inspect
|
||||
import logging
|
||||
import weakref
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from queue import Empty, Queue
|
||||
from threading import BoundedSemaphore, Event, Lock, Thread, current_thread
|
||||
from time import sleep
|
||||
from typing import (
|
||||
Callable,
|
||||
DefaultDict,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Union,
|
||||
Generic,
|
||||
TypeVar,
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Mapping,
|
||||
)
|
||||
from types import MappingProxyType
|
||||
from uuid import uuid4
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.types import DVInput
|
||||
from telegram.error import TelegramError
|
||||
from telegram.ext import BasePersistence, ContextTypes, ExtBot
|
||||
from telegram.ext._handler import Handler
|
||||
from telegram.ext._callbackdatacache import CallbackDataCache
|
||||
from telegram._utils.defaultvalue import DefaultValue, DEFAULT_FALSE
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.ext._utils.promise import Promise
|
||||
from telegram.ext._utils.types import CCT, UD, CD, BD, BT, JQ, PT
|
||||
from telegram.ext._utils.stack import was_called_by
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Message
|
||||
from telegram.ext._jobqueue import Job
|
||||
from telegram.ext._builders import InitDispatcherBuilder
|
||||
|
||||
DEFAULT_GROUP: int = 0
|
||||
|
||||
UT = TypeVar('UT')
|
||||
|
||||
|
||||
class DispatcherHandlerStop(Exception):
|
||||
"""
|
||||
Raise this in a handler or an error handler to prevent execution of any other handler (even in
|
||||
different group).
|
||||
|
||||
In order to use this exception in a :class:`telegram.ext.ConversationHandler`, pass the
|
||||
optional ``state`` parameter instead of returning the next state:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def callback(update, context):
|
||||
...
|
||||
raise DispatcherHandlerStop(next_state)
|
||||
|
||||
Note:
|
||||
Has no effect, if the handler or error handler is run asynchronously.
|
||||
|
||||
Args:
|
||||
state (:obj:`object`, optional): The next state of the conversation.
|
||||
|
||||
Attributes:
|
||||
state (:obj:`object`): Optional. The next state of the conversation.
|
||||
"""
|
||||
|
||||
__slots__ = ('state',)
|
||||
|
||||
def __init__(self, state: object = None) -> None:
|
||||
super().__init__()
|
||||
self.state = state
|
||||
|
||||
|
||||
class Dispatcher(Generic[BT, CCT, UD, CD, BD, JQ, PT]):
|
||||
"""This class dispatches all kinds of updates to its registered handlers.
|
||||
|
||||
Note:
|
||||
This class may not be initialized directly. Use :class:`telegram.ext.DispatcherBuilder` or
|
||||
:meth:`builder` (for convenience).
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
|
||||
* Initialization is now done through the :class:`telegram.ext.DispatcherBuilder`.
|
||||
* Removed the attribute ``groups``.
|
||||
|
||||
Attributes:
|
||||
bot (:class:`telegram.Bot`): The bot object that should be passed to the handlers.
|
||||
update_queue (:class:`queue.Queue`): The synchronized queue that will contain the updates.
|
||||
job_queue (:class:`telegram.ext.JobQueue`): Optional. The :class:`telegram.ext.JobQueue`
|
||||
instance to pass onto handler callbacks.
|
||||
workers (:obj:`int`, optional): Number of maximum concurrent worker threads for the
|
||||
``@run_async`` decorator and :meth:`run_async`.
|
||||
chat_data (:obj:`types.MappingProxyType`): A dictionary handlers can use to store data for
|
||||
the chat.
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
:attr:`chat_data` is now read-only
|
||||
|
||||
Tip:
|
||||
Manually modifying :attr:`chat_data` is almost never needed and unadvisable.
|
||||
|
||||
user_data (:obj:`types.MappingProxyType`): A dictionary handlers can use to store data for
|
||||
the user.
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
:attr:`user_data` is now read-only
|
||||
|
||||
Tip:
|
||||
Manually modifying :attr:`user_data` is almost never needed and unadvisable.
|
||||
|
||||
bot_data (:obj:`dict`): A dictionary handlers can use to store data for the bot.
|
||||
persistence (:class:`telegram.ext.BasePersistence`): Optional. The persistence class to
|
||||
store data that should be persistent over restarts.
|
||||
exception_event (:class:`threading.Event`): When this event is set, the dispatcher will
|
||||
stop processing updates. If this dispatcher is used together with an
|
||||
:class:`telegram.ext.Updater`, then this event will be the same object as
|
||||
:attr:`telegram.ext.Updater.exception_event`.
|
||||
handlers (Dict[:obj:`int`, List[:class:`telegram.ext.Handler`]]): A dictionary mapping each
|
||||
handler group to the list of handlers registered to that group.
|
||||
|
||||
.. seealso::
|
||||
:meth:`add_handler`, :meth:`add_handlers`.
|
||||
error_handlers (Dict[:obj:`callable`, :obj:`bool`]): A dict, where the keys are error
|
||||
handlers and the values indicate whether they are to be run asynchronously via
|
||||
:meth:`run_async`.
|
||||
|
||||
.. seealso::
|
||||
:meth:`add_error_handler`
|
||||
running (:obj:`bool`): Indicates if this dispatcher is running.
|
||||
|
||||
.. seealso::
|
||||
:meth:`start`, :meth:`stop`
|
||||
context_types (:class:`telegram.ext.ContextTypes`): Specifies the types used by this
|
||||
dispatcher for the ``context`` argument of handler and job callbacks.
|
||||
|
||||
"""
|
||||
|
||||
# Allowing '__weakref__' creation here since we need it for the singleton
|
||||
__slots__ = (
|
||||
'workers',
|
||||
'persistence',
|
||||
'update_queue',
|
||||
'job_queue',
|
||||
'_user_data',
|
||||
'user_data',
|
||||
'_chat_data',
|
||||
'chat_data',
|
||||
'bot_data',
|
||||
'_update_persistence_lock',
|
||||
'handlers',
|
||||
'error_handlers',
|
||||
'running',
|
||||
'__stop_event',
|
||||
'exception_event',
|
||||
'__async_queue',
|
||||
'__async_threads',
|
||||
'bot',
|
||||
'__weakref__',
|
||||
'context_types',
|
||||
)
|
||||
|
||||
__singleton_lock = Lock()
|
||||
__singleton_semaphore = BoundedSemaphore()
|
||||
__singleton = None
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def __init__(
|
||||
self: 'Dispatcher[BT, CCT, UD, CD, BD, JQ, PT]',
|
||||
*,
|
||||
bot: BT,
|
||||
update_queue: Queue,
|
||||
job_queue: JQ,
|
||||
workers: int,
|
||||
persistence: PT,
|
||||
context_types: ContextTypes[CCT, UD, CD, BD],
|
||||
exception_event: Event,
|
||||
stack_level: int = 4,
|
||||
):
|
||||
if not was_called_by(
|
||||
inspect.currentframe(), Path(__file__).parent.resolve() / '_builders.py'
|
||||
):
|
||||
warn(
|
||||
'`Dispatcher` instances should be built via the `DispatcherBuilder`.',
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
self.bot = bot
|
||||
self.update_queue = update_queue
|
||||
self.job_queue = job_queue
|
||||
self.workers = workers
|
||||
self.context_types = context_types
|
||||
self.exception_event = exception_event
|
||||
|
||||
if self.workers < 1:
|
||||
warn(
|
||||
'Asynchronous callbacks can not be processed without at least one worker thread.',
|
||||
stacklevel=stack_level,
|
||||
)
|
||||
|
||||
self._user_data: DefaultDict[int, UD] = defaultdict(self.context_types.user_data)
|
||||
self._chat_data: DefaultDict[int, CD] = defaultdict(self.context_types.chat_data)
|
||||
# Read only mapping-
|
||||
self.user_data: Mapping[int, UD] = MappingProxyType(self._user_data)
|
||||
self.chat_data: Mapping[int, CD] = MappingProxyType(self._chat_data)
|
||||
|
||||
self.bot_data = self.context_types.bot_data()
|
||||
|
||||
self.persistence: Optional[BasePersistence]
|
||||
self._update_persistence_lock = Lock()
|
||||
if persistence:
|
||||
if not isinstance(persistence, BasePersistence):
|
||||
raise TypeError("persistence must be based on telegram.ext.BasePersistence")
|
||||
|
||||
self.persistence = persistence
|
||||
# This raises an exception if persistence.store_data.callback_data is True
|
||||
# but self.bot is not an instance of ExtBot - so no need to check that later on
|
||||
self.persistence.set_bot(self.bot)
|
||||
|
||||
if self.persistence.store_data.user_data:
|
||||
self._user_data.update(self.persistence.get_user_data())
|
||||
if self.persistence.store_data.chat_data:
|
||||
self._chat_data.update(self.persistence.get_chat_data())
|
||||
if self.persistence.store_data.bot_data:
|
||||
self.bot_data = self.persistence.get_bot_data()
|
||||
if not isinstance(self.bot_data, self.context_types.bot_data):
|
||||
raise ValueError(
|
||||
f"bot_data must be of type {self.context_types.bot_data.__name__}"
|
||||
)
|
||||
if self.persistence.store_data.callback_data:
|
||||
persistent_data = self.persistence.get_callback_data()
|
||||
if persistent_data is not None:
|
||||
if not isinstance(persistent_data, tuple) and len(persistent_data) != 2:
|
||||
raise ValueError('callback_data must be a tuple of length 2')
|
||||
# Mypy doesn't know that persistence.set_bot (see above) already checks that
|
||||
# self.bot is an instance of ExtBot if callback_data should be stored ...
|
||||
self.bot.callback_data_cache = CallbackDataCache( # type: ignore[attr-defined]
|
||||
self.bot, # type: ignore[arg-type]
|
||||
self.bot.callback_data_cache.maxsize, # type: ignore[attr-defined]
|
||||
persistent_data=persistent_data,
|
||||
)
|
||||
else:
|
||||
self.persistence = None
|
||||
|
||||
self.handlers: Dict[int, List[Handler]] = {}
|
||||
self.error_handlers: Dict[Callable, Union[bool, DefaultValue]] = {}
|
||||
|
||||
self.running = False
|
||||
self.__stop_event = Event()
|
||||
self.__async_queue: Queue = Queue()
|
||||
self.__async_threads: Set[Thread] = set()
|
||||
|
||||
# For backward compatibility, we allow a "singleton" mode for the dispatcher. When there's
|
||||
# only one instance of Dispatcher, it will be possible to use the `run_async` decorator.
|
||||
with self.__singleton_lock:
|
||||
# pylint: disable=consider-using-with
|
||||
if self.__singleton_semaphore.acquire(blocking=False):
|
||||
self._set_singleton(self)
|
||||
else:
|
||||
self._set_singleton(None)
|
||||
|
||||
@staticmethod
|
||||
def builder() -> 'InitDispatcherBuilder':
|
||||
"""Convenience method. Returns a new :class:`telegram.ext.DispatcherBuilder`.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
"""
|
||||
# Unfortunately this needs to be here due to cyclical imports
|
||||
from telegram.ext import DispatcherBuilder # pylint: disable=import-outside-toplevel
|
||||
|
||||
return DispatcherBuilder()
|
||||
|
||||
def _init_async_threads(self, base_name: str, workers: int) -> None:
|
||||
base_name = f'{base_name}_' if base_name else ''
|
||||
|
||||
for i in range(workers):
|
||||
thread = Thread(target=self._pooled, name=f'Bot:{self.bot.id}:worker:{base_name}{i}')
|
||||
self.__async_threads.add(thread)
|
||||
thread.start()
|
||||
|
||||
@classmethod
|
||||
def _set_singleton(cls, val: Optional['Dispatcher']) -> None:
|
||||
cls.logger.debug('Setting singleton dispatcher as %s', val)
|
||||
cls.__singleton = weakref.ref(val) if val else None
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> 'Dispatcher':
|
||||
"""Get the singleton instance of this class.
|
||||
|
||||
Returns:
|
||||
:class:`telegram.ext.Dispatcher`
|
||||
|
||||
Raises:
|
||||
RuntimeError
|
||||
|
||||
"""
|
||||
if cls.__singleton is not None:
|
||||
return cls.__singleton() # type: ignore[return-value] # pylint: disable=not-callable
|
||||
raise RuntimeError(f'{cls.__name__} not initialized or multiple instances exist')
|
||||
|
||||
def _pooled(self) -> None:
|
||||
thr_name = current_thread().name
|
||||
while 1:
|
||||
promise = self.__async_queue.get()
|
||||
|
||||
# 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)
|
||||
)
|
||||
break
|
||||
|
||||
promise.run()
|
||||
|
||||
if not promise.exception:
|
||||
self.update_persistence(update=promise.update)
|
||||
continue
|
||||
|
||||
if isinstance(promise.exception, DispatcherHandlerStop):
|
||||
warn(
|
||||
'DispatcherHandlerStop is not supported with async functions; '
|
||||
f'func: {promise.pooled_function.__name__}',
|
||||
)
|
||||
continue
|
||||
|
||||
# Avoid infinite recursion of error handlers.
|
||||
if promise.pooled_function in self.error_handlers:
|
||||
self.logger.exception(
|
||||
'An error was raised and an uncaught error was raised while '
|
||||
'handling the error with an error_handler.',
|
||||
exc_info=promise.exception,
|
||||
)
|
||||
continue
|
||||
|
||||
# If we arrive here, an exception happened in the promise and was neither
|
||||
# DispatcherHandlerStop nor raised by an error handler. So we can and must handle it
|
||||
self.dispatch_error(promise.update, promise.exception, promise=promise)
|
||||
|
||||
def run_async(
|
||||
self, func: Callable[..., object], *args: object, update: object = None, **kwargs: object
|
||||
) -> Promise:
|
||||
"""
|
||||
Queue a function (with given args/kwargs) to be run asynchronously. Exceptions raised
|
||||
by the function will be handled by the error handlers registered with
|
||||
:meth:`add_error_handler`.
|
||||
|
||||
Warning:
|
||||
* If you're using ``@run_async``/:meth:`run_async` you cannot rely on adding custom
|
||||
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
|
||||
* Calling a function through :meth:`run_async` from within an error handler can lead to
|
||||
an infinite error handling loop.
|
||||
|
||||
Args:
|
||||
func (:obj:`callable`): The function to run in the thread.
|
||||
*args (:obj:`tuple`, optional): Arguments to :paramref:`func`.
|
||||
update (:class:`telegram.Update` | :obj:`object`, optional): The update associated with
|
||||
the functions call. If passed, it will be available in the error handlers, in case
|
||||
an exception is raised by :paramref:`func`.
|
||||
**kwargs (:obj:`dict`, optional): Keyword arguments to :paramref:`func`.
|
||||
|
||||
Returns:
|
||||
Promise
|
||||
|
||||
"""
|
||||
promise = Promise(func, args, kwargs, update=update)
|
||||
self.__async_queue.put(promise)
|
||||
return promise
|
||||
|
||||
def start(self, ready: Event = None) -> None:
|
||||
"""Thread target of thread 'dispatcher'.
|
||||
|
||||
Runs in background and processes the update queue. Also starts :attr:`job_queue`, if set.
|
||||
|
||||
Args:
|
||||
ready (:obj:`threading.Event`, optional): If specified, the event will be set once the
|
||||
dispatcher is ready.
|
||||
|
||||
"""
|
||||
if self.running:
|
||||
self.logger.warning('already running')
|
||||
if ready is not None:
|
||||
ready.set()
|
||||
return
|
||||
|
||||
if self.exception_event.is_set():
|
||||
msg = 'reusing dispatcher after exception event is forbidden'
|
||||
self.logger.error(msg)
|
||||
raise TelegramError(msg)
|
||||
|
||||
if self.job_queue:
|
||||
self.job_queue.start()
|
||||
self._init_async_threads(str(uuid4()), self.workers)
|
||||
self.running = True
|
||||
self.logger.debug('Dispatcher started')
|
||||
|
||||
if ready is not None:
|
||||
ready.set()
|
||||
|
||||
while 1:
|
||||
try:
|
||||
# Pop update from update queue.
|
||||
update = self.update_queue.get(True, 1)
|
||||
except Empty:
|
||||
if self.__stop_event.is_set():
|
||||
self.logger.debug('orderly stopping')
|
||||
break
|
||||
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.process_update(update)
|
||||
self.update_queue.task_done()
|
||||
|
||||
self.running = False
|
||||
self.logger.debug('Dispatcher thread stopped')
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stops the thread and :attr:`job_queue`, if set.
|
||||
Also calls :meth:`update_persistence` and :meth:`BasePersistence.flush` on
|
||||
:attr:`persistence`, if set.
|
||||
"""
|
||||
if self.running:
|
||||
self.__stop_event.set()
|
||||
while self.running:
|
||||
sleep(0.1)
|
||||
self.__stop_event.clear()
|
||||
|
||||
# async threads must be join()ed only after the dispatcher thread was joined,
|
||||
# otherwise we can still have new async threads dispatched
|
||||
threads = list(self.__async_threads)
|
||||
total = len(threads)
|
||||
|
||||
# Stop all threads in the thread pool by put()ting one non-tuple per thread
|
||||
for i in range(total):
|
||||
self.__async_queue.put(None)
|
||||
|
||||
for i, thr in enumerate(threads):
|
||||
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 %s/%s has ended', i + 1, total)
|
||||
|
||||
if self.job_queue:
|
||||
self.job_queue.stop()
|
||||
self.logger.debug('JobQueue was shut down.')
|
||||
|
||||
self.update_persistence()
|
||||
if self.persistence:
|
||||
self.persistence.flush()
|
||||
|
||||
# Clear the connection pool
|
||||
self.bot.request.stop()
|
||||
|
||||
@property
|
||||
def has_running_threads(self) -> bool: # skipcq: PY-D0003
|
||||
return self.running or bool(self.__async_threads)
|
||||
|
||||
def process_update(self, update: object) -> None:
|
||||
"""Processes a single update and updates the persistence.
|
||||
|
||||
Note:
|
||||
If the update is handled by least one synchronously running handlers (i.e.
|
||||
``run_async=False``), :meth:`update_persistence` is called *once* after all handlers
|
||||
synchronous handlers are done. Each asynchronously running handler will trigger
|
||||
:meth:`update_persistence` on its own.
|
||||
|
||||
Args:
|
||||
update (:class:`telegram.Update` | :obj:`object` | \
|
||||
:class:`telegram.error.TelegramError`):
|
||||
The update to process.
|
||||
|
||||
"""
|
||||
# An error happened while polling
|
||||
if isinstance(update, TelegramError):
|
||||
self.dispatch_error(None, update)
|
||||
return
|
||||
|
||||
context = None
|
||||
handled = False
|
||||
sync_modes = []
|
||||
|
||||
for handlers in self.handlers.values():
|
||||
try:
|
||||
for handler in handlers:
|
||||
check = handler.check_update(update)
|
||||
if check is not None and check is not False:
|
||||
if not context:
|
||||
context = self.context_types.context.from_update(update, self)
|
||||
context.refresh_data()
|
||||
handled = True
|
||||
sync_modes.append(handler.run_async)
|
||||
handler.handle_update(update, self, check, context)
|
||||
break
|
||||
|
||||
# Stop processing with any other handler.
|
||||
except DispatcherHandlerStop:
|
||||
self.logger.debug('Stopping further handlers due to DispatcherHandlerStop')
|
||||
self.update_persistence(update=update)
|
||||
break
|
||||
|
||||
# Dispatch any error.
|
||||
except Exception as exc:
|
||||
if self.dispatch_error(update, exc):
|
||||
self.logger.debug('Error handler stopped further handlers.')
|
||||
break
|
||||
|
||||
# Update persistence, if handled
|
||||
handled_only_async = all(sync_modes)
|
||||
if handled:
|
||||
# Respect default settings
|
||||
if (
|
||||
all(mode is DEFAULT_FALSE for mode in sync_modes)
|
||||
and isinstance(self.bot, ExtBot)
|
||||
and self.bot.defaults
|
||||
):
|
||||
handled_only_async = self.bot.defaults.run_async
|
||||
# If update was only handled by async handlers, we don't need to update here
|
||||
if not handled_only_async:
|
||||
self.update_persistence(update=update)
|
||||
|
||||
def add_handler(self, handler: Handler[UT, CCT], group: int = DEFAULT_GROUP) -> None:
|
||||
"""Register a handler.
|
||||
|
||||
TL;DR: Order and priority counts. 0 or 1 handlers per group will be used. End handling of
|
||||
update with :class:`telegram.ext.DispatcherHandlerStop`.
|
||||
|
||||
A handler must be an instance of a subclass of :class:`telegram.ext.Handler`. All handlers
|
||||
are organized in groups with a numeric value. The default group is 0. All groups will be
|
||||
evaluated for handling an update, but only 0 or 1 handler per group will be used. If
|
||||
:class:`telegram.ext.DispatcherHandlerStop` is raised from one of the handlers, no further
|
||||
handlers (regardless of the group) will be called.
|
||||
|
||||
The priority/order of handlers is determined as follows:
|
||||
|
||||
* Priority of the group (lower group number == higher priority)
|
||||
* The first handler in a group which should handle an update (see
|
||||
:attr:`telegram.ext.Handler.check_update`) will be used. Other handlers from the
|
||||
group will not be used. The order in which handlers were added to the group defines the
|
||||
priority.
|
||||
|
||||
Args:
|
||||
handler (:class:`telegram.ext.Handler`): A Handler instance.
|
||||
group (:obj:`int`, optional): The group identifier. Default is 0.
|
||||
|
||||
"""
|
||||
# Unfortunately due to circular imports this has to be here
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from telegram.ext._conversationhandler import ConversationHandler
|
||||
|
||||
if not isinstance(handler, Handler):
|
||||
raise TypeError(f'handler is not an instance of {Handler.__name__}')
|
||||
if not isinstance(group, int):
|
||||
raise TypeError('group is not int')
|
||||
# For some reason MyPy infers the type of handler is <nothing> here,
|
||||
# so for now we just ignore all the errors
|
||||
if (
|
||||
isinstance(handler, ConversationHandler)
|
||||
and handler.persistent # type: ignore[attr-defined]
|
||||
and handler.name # type: ignore[attr-defined]
|
||||
):
|
||||
if not self.persistence:
|
||||
raise ValueError(
|
||||
f"ConversationHandler {handler.name} " # type: ignore[attr-defined]
|
||||
f"can not be persistent if dispatcher has no persistence"
|
||||
)
|
||||
handler.persistence = self.persistence # type: ignore[attr-defined]
|
||||
handler.conversations = ( # type: ignore[attr-defined]
|
||||
self.persistence.get_conversations(handler.name) # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
if group not in self.handlers:
|
||||
self.handlers[group] = []
|
||||
self.handlers = dict(sorted(self.handlers.items())) # lower -> higher groups
|
||||
|
||||
self.handlers[group].append(handler)
|
||||
|
||||
def add_handlers(
|
||||
self,
|
||||
handlers: Union[
|
||||
Union[List[Handler], Tuple[Handler]], Dict[int, Union[List[Handler], Tuple[Handler]]]
|
||||
],
|
||||
group: DVInput[int] = DefaultValue(0),
|
||||
) -> None:
|
||||
"""Registers multiple handlers at once. The order of the handlers in the passed
|
||||
sequence(s) matters. See :meth:`add_handler` for details.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
.. seealso:: :meth:`add_handler`
|
||||
|
||||
Args:
|
||||
handlers (List[:obj:`telegram.ext.Handler`] | \
|
||||
Dict[int, List[:obj:`telegram.ext.Handler`]]): \
|
||||
Specify a sequence of handlers *or* a dictionary where the keys are groups and
|
||||
values are handlers.
|
||||
group (:obj:`int`, optional): Specify which group the sequence of ``handlers``
|
||||
should be added to. Defaults to ``0``.
|
||||
|
||||
"""
|
||||
if isinstance(handlers, dict) and not isinstance(group, DefaultValue):
|
||||
raise ValueError('The `group` argument can only be used with a sequence of handlers.')
|
||||
|
||||
if isinstance(handlers, dict):
|
||||
for handler_group, grp_handlers in handlers.items():
|
||||
if not isinstance(grp_handlers, (list, tuple)):
|
||||
raise ValueError(f'Handlers for group {handler_group} must be a list or tuple')
|
||||
|
||||
for handler in grp_handlers:
|
||||
self.add_handler(handler, handler_group)
|
||||
|
||||
elif isinstance(handlers, (list, tuple)):
|
||||
for handler in handlers:
|
||||
self.add_handler(handler, DefaultValue.get_value(group))
|
||||
|
||||
else:
|
||||
raise ValueError(
|
||||
"The `handlers` argument must be a sequence of handlers or a "
|
||||
"dictionary where the keys are groups and values are sequences of handlers."
|
||||
)
|
||||
|
||||
def remove_handler(self, handler: Handler, group: int = DEFAULT_GROUP) -> None:
|
||||
"""Remove a handler from the specified group.
|
||||
|
||||
Args:
|
||||
handler (:class:`telegram.ext.Handler`): A Handler instance.
|
||||
group (:obj:`object`, optional): The group identifier. Default is 0.
|
||||
|
||||
"""
|
||||
if handler in self.handlers[group]:
|
||||
self.handlers[group].remove(handler)
|
||||
if not self.handlers[group]:
|
||||
del self.handlers[group]
|
||||
|
||||
def drop_chat_data(self, chat_id: int) -> None:
|
||||
"""Used for deleting a key from the :attr:`chat_data`.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int`): The chat id to delete from the persistence. The entry
|
||||
will be deleted even if it is not empty.
|
||||
"""
|
||||
self._chat_data.pop(chat_id, None) # type: ignore[arg-type]
|
||||
|
||||
if self.persistence:
|
||||
self.persistence.drop_chat_data(chat_id)
|
||||
|
||||
def drop_user_data(self, user_id: int) -> None:
|
||||
"""Used for deleting a key from the :attr:`user_data`.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
||||
Args:
|
||||
user_id (:obj:`int`): The user id to delete from the persistence. The entry
|
||||
will be deleted even if it is not empty.
|
||||
"""
|
||||
self._user_data.pop(user_id, None) # type: ignore[arg-type]
|
||||
|
||||
if self.persistence:
|
||||
self.persistence.drop_user_data(user_id)
|
||||
|
||||
def migrate_chat_data(
|
||||
self, message: 'Message' = None, old_chat_id: int = None, new_chat_id: int = None
|
||||
) -> None:
|
||||
"""Moves the contents of :attr:`chat_data` at key old_chat_id to the key new_chat_id.
|
||||
Also updates the persistence by calling :attr:`update_persistence`.
|
||||
|
||||
Warning:
|
||||
|
||||
* Any data stored in :attr:`chat_data` at key `new_chat_id` will be overridden
|
||||
* The key `old_chat_id` of :attr:`chat_data` will be deleted
|
||||
|
||||
Args:
|
||||
message (:class:`telegram.Message`, optional): A message with either
|
||||
:attr:`telegram.Message.migrate_from_chat_id` or
|
||||
:attr:`telegram.Message.migrate_to_chat_id`.
|
||||
Mutually exclusive with passing ``old_chat_id`` and ``new_chat_id``
|
||||
|
||||
.. seealso: `telegram.ext.filters.StatusUpdate.MIGRATE`
|
||||
old_chat_id (:obj:`int`, optional): The old chat ID.
|
||||
Mutually exclusive with passing ``message``
|
||||
new_chat_id (:obj:`int`, optional): The new chat ID.
|
||||
Mutually exclusive with passing ``message``
|
||||
|
||||
"""
|
||||
if message and (old_chat_id or new_chat_id):
|
||||
raise ValueError("Message and chat_id pair are mutually exclusive")
|
||||
if not any((message, old_chat_id, new_chat_id)):
|
||||
raise ValueError("chat_id pair or message must be passed")
|
||||
|
||||
if message:
|
||||
if message.migrate_from_chat_id is None and message.migrate_to_chat_id is None:
|
||||
raise ValueError(
|
||||
"Invalid message instance. The message must have either "
|
||||
"`Message.migrate_from_chat_id` or `Message.migrate_to_chat_id`."
|
||||
)
|
||||
|
||||
old_chat_id = message.migrate_from_chat_id or message.chat.id
|
||||
new_chat_id = message.migrate_to_chat_id or message.chat.id
|
||||
|
||||
elif not (isinstance(old_chat_id, int) and isinstance(new_chat_id, int)):
|
||||
raise ValueError("old_chat_id and new_chat_id must be integers")
|
||||
|
||||
self._chat_data[new_chat_id] = self._chat_data[old_chat_id]
|
||||
self.drop_chat_data(old_chat_id)
|
||||
self.update_persistence()
|
||||
|
||||
def update_persistence(self, update: object = None) -> None:
|
||||
"""Update :attr:`user_data`, :attr:`chat_data` and :attr:`bot_data` in :attr:`persistence`.
|
||||
|
||||
Args:
|
||||
update (:class:`telegram.Update`, optional): The update to process. If passed, only the
|
||||
corresponding ``user_data`` and ``chat_data`` will be updated.
|
||||
"""
|
||||
with self._update_persistence_lock:
|
||||
self.__update_persistence(update)
|
||||
|
||||
def __update_persistence(self, update: object = None) -> None:
|
||||
if self.persistence:
|
||||
# We use list() here in order to decouple chat_ids from self.chat_data, as dict view
|
||||
# objects will change, when the dict does and we want to loop over chat_ids
|
||||
chat_ids = list(self.chat_data.keys())
|
||||
user_ids = list(self.user_data.keys())
|
||||
|
||||
if isinstance(update, Update):
|
||||
if update.effective_chat:
|
||||
chat_ids = [update.effective_chat.id]
|
||||
else:
|
||||
chat_ids = []
|
||||
if update.effective_user:
|
||||
user_ids = [update.effective_user.id]
|
||||
else:
|
||||
user_ids = []
|
||||
|
||||
if self.persistence.store_data.callback_data:
|
||||
try:
|
||||
# Mypy doesn't know that persistence.set_bot (see above) already checks that
|
||||
# self.bot is an instance of ExtBot if callback_data should be stored ...
|
||||
self.persistence.update_callback_data(
|
||||
self.bot.callback_data_cache.persistence_data # type: ignore[attr-defined]
|
||||
)
|
||||
except Exception as exc:
|
||||
self.dispatch_error(update, exc)
|
||||
if self.persistence.store_data.bot_data:
|
||||
try:
|
||||
self.persistence.update_bot_data(self.bot_data)
|
||||
except Exception as exc:
|
||||
self.dispatch_error(update, exc)
|
||||
if self.persistence.store_data.chat_data:
|
||||
for chat_id in chat_ids:
|
||||
try:
|
||||
self.persistence.update_chat_data(chat_id, self.chat_data[chat_id])
|
||||
except Exception as exc:
|
||||
self.dispatch_error(update, exc)
|
||||
if self.persistence.store_data.user_data:
|
||||
for user_id in user_ids:
|
||||
try:
|
||||
self.persistence.update_user_data(user_id, self.user_data[user_id])
|
||||
except Exception as exc:
|
||||
self.dispatch_error(update, exc)
|
||||
|
||||
def add_error_handler(
|
||||
self,
|
||||
callback: Callable[[object, CCT], None],
|
||||
run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
|
||||
) -> None:
|
||||
"""Registers an error handler in the Dispatcher. This handler will receive every error
|
||||
which happens in your bot. See the docs of :meth:`dispatch_error` for more details on how
|
||||
errors are handled.
|
||||
|
||||
Note:
|
||||
Attempts to add the same callback multiple times will be ignored.
|
||||
|
||||
Args:
|
||||
callback (:obj:`callable`): The callback function for this error handler. Will be
|
||||
called when an error is raised. Callback signature:
|
||||
``def callback(update: Update, context: CallbackContext)``.
|
||||
The error that happened will be present in ``context.error``.
|
||||
run_async (:obj:`bool`, optional): Whether this handlers callback should be run
|
||||
asynchronously using :meth:`run_async`. Defaults to :obj:`False`.
|
||||
"""
|
||||
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 isinstance(self.bot, ExtBot)
|
||||
and self.bot.defaults
|
||||
and self.bot.defaults.run_async
|
||||
):
|
||||
run_async = True
|
||||
|
||||
self.error_handlers[callback] = run_async
|
||||
|
||||
def remove_error_handler(self, callback: Callable[[object, CCT], None]) -> None:
|
||||
"""Removes an error handler.
|
||||
|
||||
Args:
|
||||
callback (:obj:`callable`): The error handler to remove.
|
||||
|
||||
"""
|
||||
self.error_handlers.pop(callback, None)
|
||||
|
||||
def dispatch_error(
|
||||
self,
|
||||
update: Optional[object],
|
||||
error: Exception,
|
||||
promise: Promise = None,
|
||||
job: 'Job' = None,
|
||||
) -> bool:
|
||||
"""Dispatches an error by passing it to all error handlers registered with
|
||||
:meth:`add_error_handler`. If one of the error handlers raises
|
||||
:class:`telegram.ext.DispatcherHandlerStop`, the update will not be handled by other error
|
||||
handlers or handlers (even in other groups). All other exceptions raised by an error
|
||||
handler will just be logged.
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
|
||||
* Exceptions raised by error handlers are now properly logged.
|
||||
* :class:`telegram.ext.DispatcherHandlerStop` is no longer reraised but converted into
|
||||
the return value.
|
||||
|
||||
Args:
|
||||
update (:obj:`object` | :class:`telegram.Update`): The update that caused the error.
|
||||
error (:obj:`Exception`): The error that was raised.
|
||||
job (:class:`telegram.ext.Job`, optional): The job that caused the error.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
||||
Returns:
|
||||
:obj:`bool`: :obj:`True` if one of the error handlers raised
|
||||
:class:`telegram.ext.DispatcherHandlerStop`. :obj:`False`, otherwise.
|
||||
"""
|
||||
async_args = None if not promise else promise.args
|
||||
async_kwargs = None if not promise else promise.kwargs
|
||||
|
||||
if self.error_handlers:
|
||||
for (
|
||||
callback,
|
||||
run_async,
|
||||
) in self.error_handlers.items(): # pylint: disable=redefined-outer-name
|
||||
context = self.context_types.context.from_error(
|
||||
update=update,
|
||||
error=error,
|
||||
dispatcher=self,
|
||||
async_args=async_args,
|
||||
async_kwargs=async_kwargs,
|
||||
job=job,
|
||||
)
|
||||
if run_async:
|
||||
self.run_async(callback, update, context, update=update)
|
||||
else:
|
||||
try:
|
||||
callback(update, context)
|
||||
except DispatcherHandlerStop:
|
||||
return True
|
||||
except Exception as exc:
|
||||
self.logger.exception(
|
||||
'An error was raised and an uncaught error was raised while '
|
||||
'handling the error with an error_handler.',
|
||||
exc_info=exc,
|
||||
)
|
||||
return False
|
||||
|
||||
self.logger.exception(
|
||||
'No error handlers are registered, logging exception.', exc_info=error
|
||||
)
|
||||
return False
|
|
@ -51,10 +51,10 @@ from telegram._utils.types import JSONDict, ODVInput, DVInput, ReplyMarkup
|
|||
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
|
||||
from telegram._utils.datetime import to_timestamp
|
||||
from telegram.ext._callbackdatacache import CallbackDataCache
|
||||
from telegram.request import BaseRequest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import InlineQueryResult, MessageEntity
|
||||
from telegram.request import Request
|
||||
from telegram.ext import Defaults
|
||||
|
||||
HandledTypes = TypeVar('HandledTypes', bound=Union[Message, CallbackQuery, Chat])
|
||||
|
@ -96,7 +96,8 @@ class ExtBot(Bot):
|
|||
token: str,
|
||||
base_url: str = 'https://api.telegram.org/bot',
|
||||
base_file_url: str = 'https://api.telegram.org/file/bot',
|
||||
request: 'Request' = None,
|
||||
request: BaseRequest = None,
|
||||
get_updates_request: BaseRequest = None,
|
||||
private_key: bytes = None,
|
||||
private_key_password: bytes = None,
|
||||
defaults: 'Defaults' = None,
|
||||
|
@ -107,6 +108,7 @@ class ExtBot(Bot):
|
|||
base_url=base_url,
|
||||
base_file_url=base_file_url,
|
||||
request=request,
|
||||
get_updates_request=get_updates_request,
|
||||
private_key=private_key,
|
||||
private_key_password=private_key_password,
|
||||
)
|
||||
|
@ -127,9 +129,7 @@ class ExtBot(Bot):
|
|||
# This is a property because defaults shouldn't be changed at runtime
|
||||
return self._defaults
|
||||
|
||||
def _insert_defaults(
|
||||
self, data: Dict[str, object], timeout: ODVInput[float]
|
||||
) -> Optional[float]:
|
||||
def _insert_defaults(self, data: Dict[str, object]) -> None:
|
||||
"""Inserts the defaults values for optional kwargs for which tg.ext.Defaults provides
|
||||
convenience functionality, i.e. the kwargs with a tg.utils.helpers.DefaultValue default
|
||||
|
||||
|
@ -166,17 +166,6 @@ class ExtBot(Bot):
|
|||
if media.parse_mode is DEFAULT_NONE:
|
||||
media.parse_mode = self.defaults.parse_mode if self.defaults else None
|
||||
|
||||
effective_timeout = DefaultValue.get_value(timeout)
|
||||
if isinstance(timeout, DefaultValue):
|
||||
# If we get here, we use Defaults.timeout, unless that's not set, which is the
|
||||
# case if isinstance(self.defaults.timeout, DefaultValue)
|
||||
return (
|
||||
self.defaults.timeout
|
||||
if self.defaults and not isinstance(self.defaults.timeout, DefaultValue)
|
||||
else effective_timeout
|
||||
)
|
||||
return effective_timeout
|
||||
|
||||
def _replace_keyboard(self, reply_markup: Optional[ReplyMarkup]) -> Optional[ReplyMarkup]:
|
||||
# If the reply_markup is an inline keyboard and we allow arbitrary callback data, let the
|
||||
# CallbackDataCache build a new keyboard with the data replaced. Otherwise return the input
|
||||
|
@ -190,12 +179,12 @@ class ExtBot(Bot):
|
|||
corresponding buttons within this update.
|
||||
|
||||
Note:
|
||||
Checks :attr:`telegram.Message.via_bot` and :attr:`telegram.Message.from_user` to check
|
||||
if the reply markup (if any) was actually sent by this caches bot. If it was not, the
|
||||
message will be returned unchanged.
|
||||
Checks :attr:`telegram.Message.via_bot` and :attr:`telegram.Message.from_user`
|
||||
to figure out if a) a reply markup exists and b) it was actually sent by this
|
||||
bot. If not, the message will be returned unchanged.
|
||||
|
||||
Note that this will fail for channel posts, as :attr:`telegram.Message.from_user` is
|
||||
:obj:`None` for those! In the corresponding reply markups the callback data will be
|
||||
:obj:`None` for those! In the corresponding reply markups, the callback data will be
|
||||
replaced by :class:`telegram.ext.InvalidCallbackData`.
|
||||
|
||||
Warning:
|
||||
|
@ -246,7 +235,7 @@ class ExtBot(Bot):
|
|||
|
||||
return obj
|
||||
|
||||
def _message(
|
||||
async def _send_message(
|
||||
self,
|
||||
endpoint: str,
|
||||
data: JSONDict,
|
||||
|
@ -254,20 +243,26 @@ class ExtBot(Bot):
|
|||
disable_notification: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: ReplyMarkup = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
) -> Union[bool, Message]:
|
||||
# We override this method to call self._replace_keyboard and self._insert_callback_data.
|
||||
# This covers most methods that have a reply_markup
|
||||
result = super()._message(
|
||||
result = await super()._send_message(
|
||||
endpoint=endpoint,
|
||||
data=data,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
disable_notification=disable_notification,
|
||||
reply_markup=self._replace_keyboard(reply_markup),
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
protect_content=protect_content,
|
||||
)
|
||||
|
@ -275,20 +270,26 @@ class ExtBot(Bot):
|
|||
self._insert_callback_data(result)
|
||||
return result
|
||||
|
||||
def get_updates(
|
||||
async def get_updates(
|
||||
self,
|
||||
offset: int = None,
|
||||
limit: int = 100,
|
||||
timeout: float = 0,
|
||||
read_latency: float = 2.0,
|
||||
timeout: int = 0,
|
||||
read_timeout: float = 2,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
allowed_updates: List[str] = None,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> List[Update]:
|
||||
updates = super().get_updates(
|
||||
updates = await super().get_updates(
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
timeout=timeout,
|
||||
read_latency=read_latency,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
allowed_updates=allowed_updates,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
@ -356,24 +357,30 @@ class ExtBot(Bot):
|
|||
self.defaults.disable_web_page_preview if self.defaults else None
|
||||
)
|
||||
|
||||
def stop_poll(
|
||||
async def stop_poll(
|
||||
self,
|
||||
chat_id: Union[int, str],
|
||||
message_id: int,
|
||||
reply_markup: InlineKeyboardMarkup = None,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> Poll:
|
||||
# We override this method to call self._replace_keyboard
|
||||
return super().stop_poll(
|
||||
return await super().stop_poll(
|
||||
chat_id=chat_id,
|
||||
message_id=message_id,
|
||||
reply_markup=self._replace_keyboard(reply_markup),
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
def copy_message(
|
||||
async def copy_message(
|
||||
self,
|
||||
chat_id: Union[int, str],
|
||||
from_chat_id: Union[str, int],
|
||||
|
@ -385,12 +392,15 @@ class ExtBot(Bot):
|
|||
reply_to_message_id: int = None,
|
||||
allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
|
||||
reply_markup: ReplyMarkup = None,
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
) -> MessageId:
|
||||
# We override this method to call self._replace_keyboard
|
||||
return super().copy_message(
|
||||
return await super().copy_message(
|
||||
chat_id=chat_id,
|
||||
from_chat_id=from_chat_id,
|
||||
message_id=message_id,
|
||||
|
@ -401,19 +411,32 @@ class ExtBot(Bot):
|
|||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=self._replace_keyboard(reply_markup),
|
||||
timeout=timeout,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
protect_content=protect_content,
|
||||
)
|
||||
|
||||
def get_chat(
|
||||
async def get_chat(
|
||||
self,
|
||||
chat_id: Union[str, int],
|
||||
timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: JSONDict = None,
|
||||
) -> Chat:
|
||||
# We override this method to call self._insert_callback_data
|
||||
result = super().get_chat(chat_id=chat_id, timeout=timeout, api_kwargs=api_kwargs)
|
||||
result = await super().get_chat(
|
||||
chat_id=chat_id,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
return self._insert_callback_data(result)
|
||||
|
||||
# updated camelCase aliases
|
||||
|
|
|
@ -16,17 +16,16 @@
|
|||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the base class for handlers as used by the Dispatcher."""
|
||||
"""This module contains the base class for handlers as used by the Application."""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union, Generic
|
||||
from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union, Generic
|
||||
|
||||
from telegram._utils.defaultvalue import DefaultValue, DEFAULT_FALSE
|
||||
from telegram.ext._utils.promise import Promise
|
||||
from telegram.ext._utils.types import CCT
|
||||
from telegram.ext._extbot import ExtBot
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import DVInput
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Dispatcher
|
||||
from telegram.ext import Application
|
||||
|
||||
RT = TypeVar('RT')
|
||||
UT = TypeVar('UT')
|
||||
|
@ -36,37 +35,43 @@ class Handler(Generic[UT, CCT], ABC):
|
|||
"""The base class for all update handlers. Create custom handlers by inheriting from it.
|
||||
|
||||
Warning:
|
||||
When setting :paramref:`run_async` to :obj:`True`, you cannot rely on adding custom
|
||||
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
|
||||
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
The attribute ``run_async`` is now :paramref:`block`.
|
||||
|
||||
Args:
|
||||
callback (:obj:`callable`): The callback function for this handler. Will be called when
|
||||
:attr:`check_update` has determined that an update should be processed by this handler.
|
||||
Callback signature: ``def callback(update: Update, context: CallbackContext)``
|
||||
callback (:term:`coroutine function`): The callback function for this handler. Will be
|
||||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
this handler. Callback signature::
|
||||
|
||||
async def callback(update: Update, context: CallbackContext)
|
||||
|
||||
The return value of the callback is usually ignored except for the special case of
|
||||
:class:`telegram.ext.ConversationHandler`.
|
||||
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
|
||||
Defaults to :obj:`False`.
|
||||
block (:obj:`bool`, optional): Determines whether the return value of the callback should
|
||||
be awaited before processing the next handler in
|
||||
:meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`.
|
||||
|
||||
Attributes:
|
||||
callback (:obj:`callable`): The callback function for this handler.
|
||||
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
|
||||
callback (:term:`coroutine function`): The callback function for this handler.
|
||||
block (:obj:`bool`): Determines whether the callback will run in a blocking way..
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
'callback',
|
||||
'run_async',
|
||||
'block',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
callback: Callable[[UT, CCT], RT],
|
||||
run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
|
||||
callback: HandlerCallback[UT, CCT, RT],
|
||||
block: DVInput[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
self.callback = callback
|
||||
self.run_async = run_async
|
||||
self.block = block
|
||||
|
||||
@abstractmethod
|
||||
def check_update(self, update: object) -> Optional[Union[bool, object]]:
|
||||
|
@ -75,7 +80,7 @@ class Handler(Generic[UT, CCT], ABC):
|
|||
this handler instance. It should always be overridden.
|
||||
|
||||
Note:
|
||||
Custom updates types can be handled by the dispatcher. Therefore, an implementation of
|
||||
Custom updates types can be handled by the application. Therefore, an implementation of
|
||||
this method should always check the type of :paramref:`update`.
|
||||
|
||||
Args:
|
||||
|
@ -88,13 +93,13 @@ class Handler(Generic[UT, CCT], ABC):
|
|||
|
||||
"""
|
||||
|
||||
def handle_update(
|
||||
async def handle_update(
|
||||
self,
|
||||
update: UT,
|
||||
dispatcher: 'Dispatcher',
|
||||
application: 'Application',
|
||||
check_result: object,
|
||||
context: CCT,
|
||||
) -> Union[RT, Promise]:
|
||||
) -> RT:
|
||||
"""
|
||||
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
|
||||
|
@ -104,31 +109,20 @@ class Handler(Generic[UT, CCT], ABC):
|
|||
|
||||
Args:
|
||||
update (:obj:`str` | :class:`telegram.Update`): The update to be handled.
|
||||
dispatcher (:class:`telegram.ext.Dispatcher`): The calling dispatcher.
|
||||
check_result (:class:`object`): The result from :attr:`check_update`.
|
||||
application (:class:`telegram.ext.Application`): The calling application.
|
||||
check_result (:class:`object`): The result from :meth:`check_update`.
|
||||
context (:class:`telegram.ext.CallbackContext`): The context as provided by
|
||||
the dispatcher.
|
||||
the application.
|
||||
|
||||
"""
|
||||
run_async = self.run_async
|
||||
if (
|
||||
self.run_async is DEFAULT_FALSE
|
||||
and isinstance(dispatcher.bot, ExtBot)
|
||||
and dispatcher.bot.defaults
|
||||
and dispatcher.bot.defaults.run_async
|
||||
):
|
||||
run_async = True
|
||||
|
||||
self.collect_additional_context(context, update, dispatcher, check_result)
|
||||
if run_async:
|
||||
return dispatcher.run_async(self.callback, update, context, update=update)
|
||||
return self.callback(update, context)
|
||||
self.collect_additional_context(context, update, application, check_result)
|
||||
return await self.callback(update, context)
|
||||
|
||||
def collect_additional_context(
|
||||
self,
|
||||
context: CCT,
|
||||
update: UT,
|
||||
dispatcher: 'Dispatcher',
|
||||
application: 'Application',
|
||||
check_result: Any,
|
||||
) -> None:
|
||||
"""Prepares additional arguments for the context. Override if needed.
|
||||
|
@ -136,7 +130,7 @@ class Handler(Generic[UT, CCT], ABC):
|
|||
Args:
|
||||
context (:class:`telegram.ext.CallbackContext`): The context object.
|
||||
update (:class:`telegram.Update`): The update to gather chat/user id from.
|
||||
dispatcher (:class:`telegram.ext.Dispatcher`): The calling dispatcher.
|
||||
check_result: The result (return value) from :attr:`check_update`.
|
||||
application (:class:`telegram.ext.Application`): The calling application.
|
||||
check_result: The result (return value) from :meth:`check_update`.
|
||||
|
||||
"""
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
import re
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
Match,
|
||||
Optional,
|
||||
Pattern,
|
||||
|
@ -31,12 +30,13 @@ from typing import (
|
|||
)
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.types import DVInput
|
||||
from telegram.ext import Handler
|
||||
from telegram._utils.defaultvalue import DefaultValue, DEFAULT_FALSE
|
||||
from telegram.ext._utils.types import CCT
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Dispatcher
|
||||
from telegram.ext import Application
|
||||
|
||||
RT = TypeVar('RT')
|
||||
|
||||
|
@ -47,37 +47,42 @@ class InlineQueryHandler(Handler[Update, CCT]):
|
|||
documentation of the :mod:`re` module for more information.
|
||||
|
||||
Warning:
|
||||
* When setting :paramref:`run_async` to :obj:`True`, you cannot rely on adding custom
|
||||
* When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
|
||||
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
|
||||
* :attr:`telegram.InlineQuery.chat_type` will not be set for inline queries from secret
|
||||
chats and may not be set for inline queries coming from third-party clients. These
|
||||
updates won't be handled, if :attr:`chat_types` is passed.
|
||||
|
||||
Args:
|
||||
callback (:obj:`callable`): The callback function for this handler. Will be called when
|
||||
:attr:`check_update` has determined that an update should be processed by this handler.
|
||||
Callback signature: ``def callback(update: Update, context: CallbackContext)``
|
||||
callback (:term:`coroutine function`): The callback function for this handler. Will be
|
||||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
this handler. Callback signature::
|
||||
|
||||
async def callback(update: Update, context: CallbackContext)
|
||||
|
||||
The return value of the callback is usually ignored except for the special case of
|
||||
:class:`telegram.ext.ConversationHandler`.
|
||||
pattern (:obj:`str` | :func:`re.Pattern <re.compile>`, optional): Regex pattern.
|
||||
If not :obj:`None`, :func:`re.match` is used on :attr:`telegram.InlineQuery.query`
|
||||
to determine if an update should be handled by this handler.
|
||||
block (:obj:`bool`, optional): Determines whether the return value of the callback should
|
||||
be awaited before processing the next handler in
|
||||
:meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`.
|
||||
chat_types (List[:obj:`str`], optional): List of allowed chat types. If passed, will only
|
||||
handle inline queries with the appropriate :attr:`telegram.InlineQuery.chat_type`.
|
||||
|
||||
.. versionadded:: 13.5
|
||||
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
|
||||
Defaults to :obj:`False`.
|
||||
|
||||
Attributes:
|
||||
callback (:obj:`callable`): The callback function for this handler.
|
||||
callback (:term:`coroutine function`): The callback function for this handler.
|
||||
pattern (:obj:`str` | :func:`re.Pattern <re.compile>`): Optional. Regex pattern to test
|
||||
:attr:`telegram.InlineQuery.query` against.
|
||||
chat_types (List[:obj:`str`], optional): List of allowed chat types.
|
||||
chat_types (List[:obj:`str`]): Optional. List of allowed chat types.
|
||||
|
||||
.. versionadded:: 13.5
|
||||
run_async (:obj:`bool`): Determines whether the callback will run asynchronously.
|
||||
block (:obj:`bool`): Determines whether the return value of the callback should be
|
||||
awaited before processing the next handler in
|
||||
:meth:`telegram.ext.Application.process_update`.
|
||||
|
||||
"""
|
||||
|
||||
|
@ -85,15 +90,12 @@ class InlineQueryHandler(Handler[Update, CCT]):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
callback: Callable[[Update, CCT], RT],
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
pattern: Union[str, Pattern] = None,
|
||||
run_async: Union[bool, DefaultValue] = DEFAULT_FALSE,
|
||||
block: DVInput[bool] = DEFAULT_TRUE,
|
||||
chat_types: List[str] = None,
|
||||
):
|
||||
super().__init__(
|
||||
callback,
|
||||
run_async=run_async,
|
||||
)
|
||||
super().__init__(callback, block=block)
|
||||
|
||||
if isinstance(pattern, str):
|
||||
pattern = re.compile(pattern)
|
||||
|
@ -103,13 +105,13 @@ class InlineQueryHandler(Handler[Update, CCT]):
|
|||
|
||||
def check_update(self, update: object) -> Optional[Union[bool, Match]]:
|
||||
"""
|
||||
Determines whether an update should be passed to this handlers :attr:`callback`.
|
||||
Determines whether an update should be passed to this handler's :attr:`callback`.
|
||||
|
||||
Args:
|
||||
update (:class:`telegram.Update` | :obj:`object`): Incoming update.
|
||||
|
||||
Returns:
|
||||
:obj:`bool`
|
||||
:obj:`bool` | :obj:`re.match`
|
||||
|
||||
"""
|
||||
if isinstance(update, Update) and update.inline_query:
|
||||
|
@ -130,7 +132,7 @@ class InlineQueryHandler(Handler[Update, CCT]):
|
|||
self,
|
||||
context: CCT,
|
||||
update: Update,
|
||||
dispatcher: 'Dispatcher',
|
||||
application: 'Application',
|
||||
check_result: Optional[Union[bool, Match]],
|
||||
) -> None:
|
||||
"""Add the result of ``re.match(pattern, update.inline_query.query)`` to
|
||||
|
|
|
@ -17,21 +17,22 @@
|
|||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the classes JobQueue and Job."""
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING, Callable, Optional, Tuple, Union, cast, overload
|
||||
from typing import TYPE_CHECKING, Optional, Tuple, Union, cast, overload
|
||||
|
||||
import pytz
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.executors.asyncio import AsyncIOExecutor
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.job import Job as APSJob
|
||||
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram.ext._extbot import ExtBot
|
||||
from telegram.ext._utils.types import JobCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Dispatcher, CallbackContext
|
||||
import apscheduler.job # noqa: F401
|
||||
from telegram.ext import Application
|
||||
|
||||
|
||||
class JobQueue:
|
||||
|
@ -39,15 +40,21 @@ class JobQueue:
|
|||
wrapper for the APScheduler library.
|
||||
|
||||
Attributes:
|
||||
scheduler (:class:`apscheduler.schedulers.background.BackgroundScheduler`): The APScheduler
|
||||
scheduler (:class:`apscheduler.schedulers.asyncio.AsyncIOScheduler`): The scheduler.
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
Use :class:`~apscheduler.schedulers.asyncio.AsyncIOScheduler` instead of
|
||||
:class:`~apscheduler.schedulers.background.BackgroundScheduler`
|
||||
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ('_dispatcher', 'scheduler')
|
||||
__slots__ = ('_application', 'scheduler', '_executor')
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._dispatcher: 'Optional[weakref.ReferenceType[Dispatcher]]' = None
|
||||
self.scheduler = BackgroundScheduler(timezone=pytz.utc)
|
||||
self._application: 'Optional[weakref.ReferenceType[Application]]' = None
|
||||
self._executor = AsyncIOExecutor()
|
||||
self.scheduler = AsyncIOScheduler(timezone=pytz.utc, executors={'default': self._executor})
|
||||
|
||||
def _tz_now(self) -> datetime.datetime:
|
||||
return datetime.datetime.now(self.scheduler.timezone)
|
||||
|
@ -84,43 +91,50 @@ class JobQueue:
|
|||
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
|
||||
|
||||
def set_dispatcher(self, dispatcher: 'Dispatcher') -> None:
|
||||
"""Set the dispatcher to be used by this JobQueue.
|
||||
def set_application(self, application: 'Application') -> None:
|
||||
"""Set the application to be used by this JobQueue.
|
||||
|
||||
Args:
|
||||
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher.
|
||||
application (:class:`telegram.ext.Application`): The application.
|
||||
|
||||
"""
|
||||
self._dispatcher = weakref.ref(dispatcher)
|
||||
if isinstance(dispatcher.bot, ExtBot) and dispatcher.bot.defaults:
|
||||
self.scheduler.configure(timezone=dispatcher.bot.defaults.tzinfo or pytz.utc)
|
||||
self._application = weakref.ref(application)
|
||||
if isinstance(application.bot, ExtBot) and application.bot.defaults:
|
||||
self.scheduler.configure(
|
||||
timezone=application.bot.defaults.tzinfo or pytz.utc,
|
||||
executors={'default': self._executor},
|
||||
)
|
||||
|
||||
@property
|
||||
def dispatcher(self) -> 'Dispatcher':
|
||||
"""The dispatcher this JobQueue is associated with."""
|
||||
if self._dispatcher is None:
|
||||
raise RuntimeError('No dispatcher was set for this JobQueue.')
|
||||
dispatcher = self._dispatcher()
|
||||
if dispatcher is not None:
|
||||
return dispatcher
|
||||
raise RuntimeError('The dispatcher instance is no longer alive.')
|
||||
def application(self) -> 'Application':
|
||||
"""The application this JobQueue is associated with."""
|
||||
if self._application is None:
|
||||
raise RuntimeError('No application was set for this JobQueue.')
|
||||
application = self._application()
|
||||
if application is not None:
|
||||
return application
|
||||
raise RuntimeError('The application instance is no longer alive.')
|
||||
|
||||
def run_once(
|
||||
self,
|
||||
callback: Callable[['CallbackContext'], None],
|
||||
callback: JobCallback,
|
||||
when: Union[float, datetime.timedelta, datetime.datetime, datetime.time],
|
||||
context: object = None,
|
||||
name: str = None,
|
||||
chat_id: int = None,
|
||||
user_id: int = None,
|
||||
job_kwargs: JSONDict = None,
|
||||
) -> 'Job':
|
||||
"""Creates a new :class:`Job` instance that runs once and adds it to the queue.
|
||||
|
||||
Args:
|
||||
callback (:obj:`callable`): The callback function that should be executed by the new
|
||||
job. Callback signature: ``def callback(context: CallbackContext)``
|
||||
callback (:term:`coroutine function`): The callback function that should be executed by
|
||||
the new job. Callback signature::
|
||||
|
||||
async def callback(context: CallbackContext)
|
||||
|
||||
when (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
|
||||
:obj:`datetime.datetime` | :obj:`datetime.time`):
|
||||
Time in or at which the job should run. This parameter will be interpreted
|
||||
|
@ -138,11 +152,22 @@ class JobQueue:
|
|||
tomorrow. If the timezone (:attr:`datetime.time.tzinfo`) is :obj:`None`, the
|
||||
default timezone of the bot will be used.
|
||||
|
||||
chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
||||
user_id (:obj:`int`, optional): User id of the user associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
context (:obj:`object`, optional): Additional data needed for the callback function.
|
||||
Can be accessed through :attr:`Job.context` in the callback. Defaults to
|
||||
:obj:`None`.
|
||||
name (:obj:`str`, optional): The name of the new job. Defaults to
|
||||
``callback.__name__``.
|
||||
:external:attr:`callback.__name__ <definition.__name__>`.
|
||||
job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
|
||||
:meth:`apscheduler.schedulers.base.BaseScheduler.add_job()`.
|
||||
|
||||
|
@ -155,15 +180,15 @@ class JobQueue:
|
|||
job_kwargs = {}
|
||||
|
||||
name = name or callback.__name__
|
||||
job = Job(callback, context, name)
|
||||
job = Job(callback=callback, context=context, name=name, chat_id=chat_id, user_id=user_id)
|
||||
date_time = self._parse_time_input(when, shift_day=True)
|
||||
|
||||
j = self.scheduler.add_job(
|
||||
job,
|
||||
job.run,
|
||||
name=name,
|
||||
trigger='date',
|
||||
run_date=date_time,
|
||||
args=(self.dispatcher,),
|
||||
args=(self.application,),
|
||||
timezone=date_time.tzinfo or self.scheduler.timezone,
|
||||
**job_kwargs,
|
||||
)
|
||||
|
@ -173,12 +198,14 @@ class JobQueue:
|
|||
|
||||
def run_repeating(
|
||||
self,
|
||||
callback: Callable[['CallbackContext'], None],
|
||||
callback: JobCallback,
|
||||
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,
|
||||
chat_id: int = None,
|
||||
user_id: int = None,
|
||||
job_kwargs: JSONDict = None,
|
||||
) -> 'Job':
|
||||
"""Creates a new :class:`Job` instance that runs at specified intervals and adds it to the
|
||||
|
@ -191,8 +218,11 @@ class JobQueue:
|
|||
#daylight-saving-time-behavior
|
||||
|
||||
Args:
|
||||
callback (:obj:`callable`): The callback function that should be executed by the new
|
||||
job. Callback signature: ``def callback(context: CallbackContext)``
|
||||
callback (:term:`coroutine function`): The callback function that should be executed by
|
||||
the new job. Callback signature::
|
||||
|
||||
async def callback(context: CallbackContext)
|
||||
|
||||
interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`): The interval in which
|
||||
the job will run. If it is an :obj:`int` or a :obj:`float`, it will be interpreted
|
||||
as seconds.
|
||||
|
@ -228,7 +258,18 @@ class JobQueue:
|
|||
Can be accessed through :attr:`Job.context` in the callback. Defaults to
|
||||
:obj:`None`.
|
||||
name (:obj:`str`, optional): The name of the new job. Defaults to
|
||||
``callback.__name__``.
|
||||
:external:attr:`callback.__name__ <definition.__name__>`.
|
||||
chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
||||
user_id (:obj:`int`, optional): User id of the user associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
|
||||
:meth:`apscheduler.schedulers.base.BaseScheduler.add_job()`.
|
||||
|
||||
|
@ -241,7 +282,7 @@ class JobQueue:
|
|||
job_kwargs = {}
|
||||
|
||||
name = name or callback.__name__
|
||||
job = Job(callback, context, name)
|
||||
job = Job(callback=callback, context=context, name=name, chat_id=chat_id, user_id=user_id)
|
||||
|
||||
dt_first = self._parse_time_input(first)
|
||||
dt_last = self._parse_time_input(last)
|
||||
|
@ -253,9 +294,9 @@ class JobQueue:
|
|||
interval = interval.total_seconds()
|
||||
|
||||
j = self.scheduler.add_job(
|
||||
job,
|
||||
job.run,
|
||||
trigger='interval',
|
||||
args=(self.dispatcher,),
|
||||
args=(self.application,),
|
||||
start_date=dt_first,
|
||||
end_date=dt_last,
|
||||
seconds=interval,
|
||||
|
@ -268,11 +309,13 @@ class JobQueue:
|
|||
|
||||
def run_monthly(
|
||||
self,
|
||||
callback: Callable[['CallbackContext'], None],
|
||||
callback: JobCallback,
|
||||
when: datetime.time,
|
||||
day: int,
|
||||
context: object = None,
|
||||
name: str = None,
|
||||
chat_id: int = None,
|
||||
user_id: int = None,
|
||||
job_kwargs: JSONDict = None,
|
||||
) -> 'Job':
|
||||
"""Creates a new :class:`Job` that runs on a monthly basis and adds it to the queue.
|
||||
|
@ -282,8 +325,11 @@ class JobQueue:
|
|||
parameter to have the job run on the last day of the month.
|
||||
|
||||
Args:
|
||||
callback (:obj:`callable`): The callback function that should be executed by the new
|
||||
job. Callback signature: ``def callback(context: CallbackContext)``
|
||||
callback (:term:`coroutine function`): The callback function that should be executed by
|
||||
the new job. Callback signature::
|
||||
|
||||
async def callback(context: CallbackContext)
|
||||
|
||||
when (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
|
||||
(``when.tzinfo``) is :obj:`None`, the default timezone of the bot will be used.
|
||||
day (:obj:`int`): Defines the day of the month whereby the job would run. It should
|
||||
|
@ -294,7 +340,18 @@ class JobQueue:
|
|||
Can be accessed through :attr:`Job.context` in the callback. Defaults to
|
||||
:obj:`None`.
|
||||
name (:obj:`str`, optional): The name of the new job. Defaults to
|
||||
``callback.__name__``.
|
||||
:external:attr:`callback.__name__ <definition.__name__>`.
|
||||
chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
||||
user_id (:obj:`int`, optional): User id of the user associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
|
||||
:meth:`apscheduler.schedulers.base.BaseScheduler.add_job()`.
|
||||
|
||||
|
@ -307,12 +364,12 @@ class JobQueue:
|
|||
job_kwargs = {}
|
||||
|
||||
name = name or callback.__name__
|
||||
job = Job(callback, context, name)
|
||||
job = Job(callback=callback, context=context, name=name, chat_id=chat_id, user_id=user_id)
|
||||
|
||||
j = self.scheduler.add_job(
|
||||
job,
|
||||
job.run,
|
||||
trigger='cron',
|
||||
args=(self.dispatcher,),
|
||||
args=(self.application,),
|
||||
name=name,
|
||||
day='last' if day == -1 else day,
|
||||
hour=when.hour,
|
||||
|
@ -326,11 +383,13 @@ class JobQueue:
|
|||
|
||||
def run_daily(
|
||||
self,
|
||||
callback: Callable[['CallbackContext'], None],
|
||||
callback: JobCallback,
|
||||
time: datetime.time,
|
||||
days: Tuple[int, ...] = tuple(range(7)),
|
||||
context: object = None,
|
||||
name: str = None,
|
||||
chat_id: int = None,
|
||||
user_id: int = None,
|
||||
job_kwargs: JSONDict = None,
|
||||
) -> 'Job':
|
||||
"""Creates a new :class:`Job` that runs on a daily basis and adds it to the queue.
|
||||
|
@ -342,8 +401,11 @@ class JobQueue:
|
|||
#daylight-saving-time-behavior
|
||||
|
||||
Args:
|
||||
callback (:obj:`callable`): The callback function that should be executed by the new
|
||||
job. Callback signature: ``def callback(context: CallbackContext)``
|
||||
callback (:term:`coroutine function`): The callback function that should be executed by
|
||||
the new job. Callback signature::
|
||||
|
||||
async def callback(context: CallbackContext)
|
||||
|
||||
time (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
|
||||
(:obj:`datetime.time.tzinfo`) is :obj:`None`, the default timezone of the bot will
|
||||
be used.
|
||||
|
@ -353,7 +415,18 @@ class JobQueue:
|
|||
Can be accessed through :attr:`Job.context` in the callback. Defaults to
|
||||
:obj:`None`.
|
||||
name (:obj:`str`, optional): The name of the new job. Defaults to
|
||||
``callback.__name__``.
|
||||
:external:attr:`callback.__name__ <definition.__name__>`.
|
||||
chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
||||
user_id (:obj:`int`, optional): User id of the user associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
|
||||
:meth:`apscheduler.schedulers.base.BaseScheduler.add_job()`.
|
||||
|
||||
|
@ -366,12 +439,12 @@ class JobQueue:
|
|||
job_kwargs = {}
|
||||
|
||||
name = name or callback.__name__
|
||||
job = Job(callback, context, name)
|
||||
job = Job(callback=callback, context=context, name=name, chat_id=chat_id, user_id=user_id)
|
||||
|
||||
j = self.scheduler.add_job(
|
||||
job,
|
||||
job.run,
|
||||
name=name,
|
||||
args=(self.dispatcher,),
|
||||
args=(self.application,),
|
||||
trigger='cron',
|
||||
day_of_week=','.join([str(d) for d in days]),
|
||||
hour=time.hour,
|
||||
|
@ -386,23 +459,39 @@ class JobQueue:
|
|||
|
||||
def run_custom(
|
||||
self,
|
||||
callback: Callable[['CallbackContext'], None],
|
||||
callback: JobCallback,
|
||||
job_kwargs: JSONDict,
|
||||
context: object = None,
|
||||
name: str = None,
|
||||
chat_id: int = None,
|
||||
user_id: int = None,
|
||||
) -> 'Job':
|
||||
"""Creates a new custom defined :class:`Job`.
|
||||
|
||||
Args:
|
||||
callback (:obj:`callable`): The callback function that should be executed by the new
|
||||
job. Callback signature: ``def callback(context: CallbackContext)``
|
||||
callback (:term:`coroutine function`): The callback function that should be executed by
|
||||
the new job. Callback signature::
|
||||
|
||||
async def callback(context: CallbackContext)
|
||||
|
||||
job_kwargs (:obj:`dict`): Arbitrary keyword arguments. Used as arguments for
|
||||
:meth:`apscheduler.schedulers.base.BaseScheduler.add_job`.
|
||||
context (:obj:`object`, optional): Additional data needed for the callback function.
|
||||
Can be accessed through :attr:`Job.context` in the callback. Defaults to
|
||||
:obj:`None`.
|
||||
name (:obj:`str`, optional): The name of the new job. Defaults to
|
||||
``callback.__name__``.
|
||||
:external:attr:`callback.__name__ <definition.__name__>`.
|
||||
chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
||||
user_id (:obj:`int`, optional): User id of the user associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
||||
Returns:
|
||||
:class:`telegram.ext.Job`: The new :class:`Job` instance that has been added to the job
|
||||
|
@ -410,22 +499,41 @@ class JobQueue:
|
|||
|
||||
"""
|
||||
name = name or callback.__name__
|
||||
job = Job(callback, context, name)
|
||||
job = Job(callback=callback, context=context, name=name, chat_id=chat_id, user_id=user_id)
|
||||
|
||||
j = self.scheduler.add_job(job, args=(self.dispatcher,), name=name, **job_kwargs)
|
||||
j = self.scheduler.add_job(job.run, args=(self.application,), name=name, **job_kwargs)
|
||||
|
||||
job.job = j
|
||||
return job
|
||||
|
||||
def start(self) -> None:
|
||||
"""Starts the job_queue thread."""
|
||||
async def start(self) -> None:
|
||||
# this method async just in case future versions need that
|
||||
"""Starts the job_queue."""
|
||||
if not self.scheduler.running:
|
||||
self.scheduler.start()
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stops the thread."""
|
||||
async def stop(self, wait: bool = True) -> None:
|
||||
"""Shuts down the :class:`~telegram.ext.JobQueue`.
|
||||
|
||||
Args:
|
||||
wait (:obj:`bool`, optional): Whether to wait until all currently running jobs
|
||||
have finished. Defaults to :obj:`True`.
|
||||
|
||||
"""
|
||||
# the interface methods of AsyncIOExecutor are currently not really asyncio-compatible
|
||||
# so we apply some small tweaks here to try and smoothen the integration into PTB
|
||||
# TODO: When APS 4.0 hits, we should be able to remove the tweaks
|
||||
if wait:
|
||||
# Unfortunately AsyncIOExecutor just cancels them all ...
|
||||
await asyncio.gather(
|
||||
*self._executor._pending_futures, # pylint: disable=protected-access
|
||||
return_exceptions=True,
|
||||
)
|
||||
if self.scheduler.running:
|
||||
self.scheduler.shutdown()
|
||||
self.scheduler.shutdown(wait=wait)
|
||||
# scheduler.shutdown schedules a task in the event loop but immediately returns
|
||||
# so give it a tiny bit of time to actually shut down.
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
def jobs(self) -> Tuple['Job', ...]:
|
||||
"""Returns a tuple of all *scheduled* jobs that are currently in the :class:`JobQueue`."""
|
||||
|
@ -452,8 +560,6 @@ class Job:
|
|||
Note:
|
||||
* All attributes and instance methods of :attr:`job` are also directly available as
|
||||
attributes/methods of the corresponding :class:`telegram.ext.Job` object.
|
||||
* Two instances of :class:`telegram.ext.Job` are considered equal, if their corresponding
|
||||
:attr:`job` attributes have the same ``id``.
|
||||
* If :attr:`job` isn't passed on initialization, it must be set manually afterwards for
|
||||
this :class:`telegram.ext.Job` to be useful.
|
||||
|
||||
|
@ -461,18 +567,35 @@ class Job:
|
|||
Removed argument and attribute ``job_queue``.
|
||||
|
||||
Args:
|
||||
callback (:obj:`callable`): The callback function that should be executed by the new job.
|
||||
Callback signature: ``def callback(context: CallbackContext)``
|
||||
callback (:term:`coroutine function`): The callback function that should be executed by the
|
||||
new job. Callback signature::
|
||||
|
||||
async def callback(context: CallbackContext)
|
||||
|
||||
context (:obj:`object`, optional): Additional data needed for the callback function. Can be
|
||||
accessed through :attr:`Job.context` in the callback. Defaults to :obj:`None`.
|
||||
name (:obj:`str`, optional): The name of the new job. Defaults to ``callback.__name__``.
|
||||
name (:obj:`str`, optional): The name of the new job. Defaults to
|
||||
:external:obj:`callback.__name__ <definition.__name__>`.
|
||||
job (:class:`apscheduler.job.Job`, optional): The APS Job this job is a wrapper for.
|
||||
chat_id (:obj:`int`, optional): Chat id of the chat that this job is associated with.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
user_id (:obj:`int`, optional): User id of the user that this job is associated with.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
||||
Attributes:
|
||||
callback (:obj:`callable`): The callback function that should be executed by the new job.
|
||||
callback (:term:`coroutine function`): The callback function that should be executed by the
|
||||
new job.
|
||||
context (:obj:`object`): Optional. Additional data needed for the callback function.
|
||||
name (:obj:`str`): Optional. The name of the new job.
|
||||
job (:class:`apscheduler.job.Job`): Optional. The APS Job this job is a wrapper for.
|
||||
chat_id (:obj:`int`): Optional. Chat id of the chat that this job is associated with.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
user_id (:obj:`int`): Optional. User id of the user that this job is associated with.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
|
@ -482,59 +605,55 @@ class Job:
|
|||
'_removed',
|
||||
'_enabled',
|
||||
'job',
|
||||
'chat_id',
|
||||
'user_id',
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
callback: Callable[['CallbackContext'], None],
|
||||
callback: JobCallback,
|
||||
context: object = None,
|
||||
name: str = None,
|
||||
job: APSJob = None,
|
||||
chat_id: int = None,
|
||||
user_id: int = None,
|
||||
):
|
||||
|
||||
self.callback = callback
|
||||
self.context = context
|
||||
self.name = name or callback.__name__
|
||||
self.chat_id = chat_id
|
||||
self.user_id = user_id
|
||||
|
||||
self._removed = False
|
||||
self._enabled = False
|
||||
|
||||
self.job = cast(APSJob, job) # skipcq: PTC-W0052
|
||||
|
||||
def run(self, dispatcher: 'Dispatcher') -> None:
|
||||
async def run(self, application: 'Application') -> None:
|
||||
"""Executes the callback function independently of the jobs schedule. Also calls
|
||||
:meth:`telegram.ext.Dispatcher.update_persistence`.
|
||||
:meth:`telegram.ext.Application.update_persistence`.
|
||||
|
||||
.. versionchanged:: 14.0
|
||||
Calls :meth:`telegram.ext.Dispatcher.update_persistence`.
|
||||
Calls :meth:`telegram.ext.Application.update_persistence`.
|
||||
|
||||
Args:
|
||||
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher this job is associated
|
||||
application (:class:`telegram.ext.Application`): The application this job is associated
|
||||
with.
|
||||
"""
|
||||
# We shield the task such that the job isn't cancelled mid-run
|
||||
await asyncio.shield(self._run(application))
|
||||
|
||||
async def _run(self, application: 'Application') -> None:
|
||||
try:
|
||||
self.callback(dispatcher.context_types.context.from_job(self, dispatcher))
|
||||
context = application.context_types.context.from_job(self, application)
|
||||
await context.refresh_data()
|
||||
await self.callback(context)
|
||||
except Exception as exc:
|
||||
dispatcher.dispatch_error(None, exc, job=self)
|
||||
await application.create_task(application.process_error(None, exc, job=self))
|
||||
finally:
|
||||
dispatcher.update_persistence(None)
|
||||
|
||||
def __call__(self, dispatcher: 'Dispatcher') -> None:
|
||||
"""Shortcut for::
|
||||
|
||||
job.run(dispatcher)
|
||||
|
||||
Warning:
|
||||
The fact that jobs are callable should be considered an implementation detail and not
|
||||
as part of PTBs public API.
|
||||
|
||||
.. versionadded:: 14.0
|
||||
|
||||
Args:
|
||||
dispatcher (:class:`telegram.ext.Dispatcher`): The dispatcher this job is associated
|
||||
with.
|
||||
"""
|
||||
self.run(dispatcher=dispatcher)
|
||||
# This is internal logic of application - let's keep it private for now
|
||||
application._mark_for_persistence_update(job=self) # pylint: disable=protected-access
|
||||
|
||||
def schedule_removal(self) -> None:
|
||||
"""
|
||||
|
@ -577,7 +696,7 @@ class Job:
|
|||
|
||||
@classmethod
|
||||
def _from_aps_job(cls, job: APSJob) -> 'Job':
|
||||
return job.func
|
||||
return job.func.__self__
|
||||
|
||||
def __getattr__(self, item: str) -> object:
|
||||
try:
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue