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:
Bibo-Joshi 2022-04-24 12:38:09 +02:00 committed by Hinrich Mahler
parent a743726b08
commit 42eaa67fd5
203 changed files with 22819 additions and 15229 deletions

View file

@ -5,7 +5,6 @@ test_patterns = ["tests/**"]
exclude_patterns = [
"tests/**",
"docs/**",
"telegram/vendor/**",
"setup.py",
"setup-raw.py"
]

View file

@ -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/

View file

@ -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)

View file

@ -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
View file

@ -1,4 +0,0 @@
[submodule "telegram/vendor/urllib3"]
path = telegram/vendor/ptb_urllib3
url = https://github.com/python-telegram-bot/urllib3.git
branch = ptb

View file

@ -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:

View file

@ -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
------------

View file

@ -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.

View file

@ -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.

View file

@ -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):

View file

@ -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

View 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:

View 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:

View 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:

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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

View file

@ -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:

View 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:

View 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:

View 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:

View file

@ -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

View file

@ -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__':

View file

@ -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__":

View file

@ -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__':

View file

@ -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__':

View file

@ -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__':

View file

@ -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__":

View file

@ -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__':

View file

@ -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__':

View file

@ -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__':

View file

@ -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__':

View file

@ -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__':

View file

@ -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__':

View file

@ -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>

View file

@ -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__':

View file

@ -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__':

View file

@ -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__':

View file

@ -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__':

View file

@ -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

View file

@ -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__':

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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',
},

View file

@ -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}')

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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,
)

View file

@ -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')}

View file

@ -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.

View file

@ -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,
)

View file

@ -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.

View file

@ -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,
)

View file

@ -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:

View file

@ -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

View file

@ -17,7 +17,6 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""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

View file

@ -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.
"""

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View 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,
)

View file

@ -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,
)

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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:

View file

@ -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
View 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}>'

View file

@ -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

View file

@ -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."""

View file

@ -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.

View file

@ -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,)

View file

@ -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

File diff suppressed because it is too large Load diff

View 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,
]
)

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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.',
)

View file

@ -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,
)
)

View file

@ -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():

View file

@ -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

View file

@ -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

View file

@ -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`.
"""

View file

@ -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

View file

@ -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