This commit is contained in:
104
.venv/lib/python3.12/site-packages/telegram/ext/__init__.py
Normal file
104
.venv/lib/python3.12/site-packages/telegram/ext/__init__.py
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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/].
|
||||
"""Extensions over the Telegram Bot API to facilitate bot making"""
|
||||
|
||||
__all__ = (
|
||||
"AIORateLimiter",
|
||||
"Application",
|
||||
"ApplicationBuilder",
|
||||
"ApplicationHandlerStop",
|
||||
"BaseHandler",
|
||||
"BasePersistence",
|
||||
"BaseRateLimiter",
|
||||
"BaseUpdateProcessor",
|
||||
"BusinessConnectionHandler",
|
||||
"BusinessMessagesDeletedHandler",
|
||||
"CallbackContext",
|
||||
"CallbackDataCache",
|
||||
"CallbackQueryHandler",
|
||||
"ChatBoostHandler",
|
||||
"ChatJoinRequestHandler",
|
||||
"ChatMemberHandler",
|
||||
"ChosenInlineResultHandler",
|
||||
"CommandHandler",
|
||||
"ContextTypes",
|
||||
"ConversationHandler",
|
||||
"Defaults",
|
||||
"DictPersistence",
|
||||
"ExtBot",
|
||||
"InlineQueryHandler",
|
||||
"InvalidCallbackData",
|
||||
"Job",
|
||||
"JobQueue",
|
||||
"MessageHandler",
|
||||
"MessageReactionHandler",
|
||||
"PaidMediaPurchasedHandler",
|
||||
"PersistenceInput",
|
||||
"PicklePersistence",
|
||||
"PollAnswerHandler",
|
||||
"PollHandler",
|
||||
"PreCheckoutQueryHandler",
|
||||
"PrefixHandler",
|
||||
"ShippingQueryHandler",
|
||||
"SimpleUpdateProcessor",
|
||||
"StringCommandHandler",
|
||||
"StringRegexHandler",
|
||||
"TypeHandler",
|
||||
"Updater",
|
||||
"filters",
|
||||
)
|
||||
|
||||
from . import filters
|
||||
from ._aioratelimiter import AIORateLimiter
|
||||
from ._application import Application, ApplicationHandlerStop
|
||||
from ._applicationbuilder import ApplicationBuilder
|
||||
from ._basepersistence import BasePersistence, PersistenceInput
|
||||
from ._baseratelimiter import BaseRateLimiter
|
||||
from ._baseupdateprocessor import BaseUpdateProcessor, SimpleUpdateProcessor
|
||||
from ._callbackcontext import CallbackContext
|
||||
from ._callbackdatacache import CallbackDataCache, InvalidCallbackData
|
||||
from ._contexttypes import ContextTypes
|
||||
from ._defaults import Defaults
|
||||
from ._dictpersistence import DictPersistence
|
||||
from ._extbot import ExtBot
|
||||
from ._handlers.basehandler import BaseHandler
|
||||
from ._handlers.businessconnectionhandler import BusinessConnectionHandler
|
||||
from ._handlers.businessmessagesdeletedhandler import BusinessMessagesDeletedHandler
|
||||
from ._handlers.callbackqueryhandler import CallbackQueryHandler
|
||||
from ._handlers.chatboosthandler import ChatBoostHandler
|
||||
from ._handlers.chatjoinrequesthandler import ChatJoinRequestHandler
|
||||
from ._handlers.chatmemberhandler import ChatMemberHandler
|
||||
from ._handlers.choseninlineresulthandler import ChosenInlineResultHandler
|
||||
from ._handlers.commandhandler import CommandHandler
|
||||
from ._handlers.conversationhandler import ConversationHandler
|
||||
from ._handlers.inlinequeryhandler import InlineQueryHandler
|
||||
from ._handlers.messagehandler import MessageHandler
|
||||
from ._handlers.messagereactionhandler import MessageReactionHandler
|
||||
from ._handlers.paidmediapurchasedhandler import PaidMediaPurchasedHandler
|
||||
from ._handlers.pollanswerhandler import PollAnswerHandler
|
||||
from ._handlers.pollhandler import PollHandler
|
||||
from ._handlers.precheckoutqueryhandler import PreCheckoutQueryHandler
|
||||
from ._handlers.prefixhandler import PrefixHandler
|
||||
from ._handlers.shippingqueryhandler import ShippingQueryHandler
|
||||
from ._handlers.stringcommandhandler import StringCommandHandler
|
||||
from ._handlers.stringregexhandler import StringRegexHandler
|
||||
from ._handlers.typehandler import TypeHandler
|
||||
from ._jobqueue import Job, JobQueue
|
||||
from ._picklepersistence import PicklePersistence
|
||||
from ._updater import Updater
|
||||
@@ -0,0 +1,299 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains an implementation of the BaseRateLimiter class based on the aiolimiter
|
||||
library.
|
||||
"""
|
||||
import asyncio
|
||||
import contextlib
|
||||
import sys
|
||||
from collections.abc import AsyncIterator, Coroutine
|
||||
from typing import Any, Callable, Optional, Union
|
||||
|
||||
try:
|
||||
from aiolimiter import AsyncLimiter
|
||||
|
||||
AIO_LIMITER_AVAILABLE = True
|
||||
except ImportError:
|
||||
AIO_LIMITER_AVAILABLE = False
|
||||
|
||||
from telegram import constants
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram.error import RetryAfter
|
||||
from telegram.ext._baseratelimiter import BaseRateLimiter
|
||||
|
||||
# Useful for something like:
|
||||
# async with group_limiter if group else null_context():
|
||||
# so we don't have to differentiate between "I'm using a context manager" and "I'm not"
|
||||
if sys.version_info >= (3, 10):
|
||||
null_context = contextlib.nullcontext # pylint: disable=invalid-name
|
||||
else:
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def null_context() -> AsyncIterator[None]:
|
||||
yield None
|
||||
|
||||
|
||||
_LOGGER = get_logger(__name__, class_name="AIORateLimiter")
|
||||
|
||||
|
||||
class AIORateLimiter(BaseRateLimiter[int]):
|
||||
"""
|
||||
Implementation of :class:`~telegram.ext.BaseRateLimiter` using the library
|
||||
`aiolimiter <https://aiolimiter.readthedocs.io/en/stable>`_.
|
||||
|
||||
Important:
|
||||
If you want to use this class, you must install PTB with the optional requirement
|
||||
``rate-limiter``, i.e.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install "python-telegram-bot[rate-limiter]"
|
||||
|
||||
The rate limiting is applied by combining two levels of throttling and :meth:`process_request`
|
||||
roughly boils down to::
|
||||
|
||||
async with group_limiter(group_id):
|
||||
async with overall_limiter:
|
||||
await callback(*args, **kwargs)
|
||||
|
||||
Here, ``group_id`` is determined by checking if there is a ``chat_id`` parameter in the
|
||||
:paramref:`~telegram.ext.BaseRateLimiter.process_request.data`.
|
||||
The ``overall_limiter`` is applied only if a ``chat_id`` argument is present at all.
|
||||
|
||||
Attention:
|
||||
* Some bot methods accept a ``chat_id`` parameter in form of a ``@username`` for
|
||||
supergroups and channels. As we can't know which ``@username`` corresponds to which
|
||||
integer ``chat_id``, these will be treated as different groups, which may lead to
|
||||
exceeding the rate limit.
|
||||
* As channels can't be differentiated from supergroups by the ``@username`` or integer
|
||||
``chat_id``, this also applies the group related rate limits to channels.
|
||||
* A :exc:`~telegram.error.RetryAfter` exception will halt *all* requests for
|
||||
:attr:`~telegram.error.RetryAfter.retry_after` + 0.1 seconds. This may be stricter than
|
||||
necessary in some cases, e.g. the bot may hit a rate limit in one group but might still
|
||||
be allowed to send messages in another group or with
|
||||
:paramref:`~telegram.Bot.send_message.allow_paid_broadcast` set to :obj:`True`.
|
||||
|
||||
Tip:
|
||||
With `Bot API 7.1 <https://core.telegram.org/bots/api-changelog#october-31-2024>`_
|
||||
(PTB v27.1), Telegram introduced the parameter
|
||||
:paramref:`~telegram.Bot.send_message.allow_paid_broadcast`.
|
||||
This allows bots to send up to
|
||||
:tg-const:`telegram.constants.FloodLimit.PAID_MESSAGES_PER_SECOND` messages per second by
|
||||
paying a fee in Telegram Stars.
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
This class automatically takes the
|
||||
:paramref:`~telegram.Bot.send_message.allow_paid_broadcast` parameter into account and
|
||||
throttles the requests accordingly.
|
||||
|
||||
Note:
|
||||
This class is to be understood as minimal effort reference implementation.
|
||||
If you would like to handle rate limiting in a more sophisticated, fine-tuned way, we
|
||||
welcome you to implement your own subclass of :class:`~telegram.ext.BaseRateLimiter`.
|
||||
Feel free to check out the source code of this class for inspiration.
|
||||
|
||||
.. seealso:: :wiki:`Avoiding Flood Limits <Avoiding-flood-limits>`
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Args:
|
||||
overall_max_rate (:obj:`float`): The maximum number of requests allowed for the entire bot
|
||||
per :paramref:`overall_time_period`. When set to 0, no rate limiting will be applied.
|
||||
Defaults to :tg-const:`telegram.constants.FloodLimit.MESSAGES_PER_SECOND`.
|
||||
overall_time_period (:obj:`float`): The time period (in seconds) during which the
|
||||
:paramref:`overall_max_rate` is enforced. When set to 0, no rate limiting will be
|
||||
applied. Defaults to ``1``.
|
||||
group_max_rate (:obj:`float`): The maximum number of requests allowed for requests related
|
||||
to groups and channels per :paramref:`group_time_period`. When set to 0, no rate
|
||||
limiting will be applied. Defaults to
|
||||
:tg-const:`telegram.constants.FloodLimit.MESSAGES_PER_MINUTE_PER_GROUP`.
|
||||
group_time_period (:obj:`float`): The time period (in seconds) during which the
|
||||
:paramref:`group_max_rate` is enforced. When set to 0, no rate limiting will be
|
||||
applied. Defaults to ``60``.
|
||||
max_retries (:obj:`int`): The maximum number of retries to be made in case of a
|
||||
:exc:`~telegram.error.RetryAfter` exception.
|
||||
If set to 0, no retries will be made. Defaults to ``0``.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_apb_limiter",
|
||||
"_base_limiter",
|
||||
"_group_limiters",
|
||||
"_group_max_rate",
|
||||
"_group_time_period",
|
||||
"_max_retries",
|
||||
"_retry_after_event",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
overall_max_rate: float = constants.FloodLimit.MESSAGES_PER_SECOND,
|
||||
overall_time_period: float = 1,
|
||||
group_max_rate: float = constants.FloodLimit.MESSAGES_PER_MINUTE_PER_GROUP,
|
||||
group_time_period: float = 60,
|
||||
max_retries: int = 0,
|
||||
) -> None:
|
||||
if not AIO_LIMITER_AVAILABLE:
|
||||
raise RuntimeError(
|
||||
"To use `AIORateLimiter`, PTB must be installed via `pip install "
|
||||
'"python-telegram-bot[rate-limiter]"`.'
|
||||
)
|
||||
if overall_max_rate and overall_time_period:
|
||||
self._base_limiter: Optional[AsyncLimiter] = AsyncLimiter(
|
||||
max_rate=overall_max_rate, time_period=overall_time_period
|
||||
)
|
||||
else:
|
||||
self._base_limiter = None
|
||||
|
||||
if group_max_rate and group_time_period:
|
||||
self._group_max_rate: float = group_max_rate
|
||||
self._group_time_period: float = group_time_period
|
||||
else:
|
||||
self._group_max_rate = 0
|
||||
self._group_time_period = 0
|
||||
|
||||
self._group_limiters: dict[Union[str, int], AsyncLimiter] = {}
|
||||
self._apb_limiter: AsyncLimiter = AsyncLimiter(
|
||||
max_rate=constants.FloodLimit.PAID_MESSAGES_PER_SECOND, time_period=1
|
||||
)
|
||||
self._max_retries: int = max_retries
|
||||
self._retry_after_event = asyncio.Event()
|
||||
self._retry_after_event.set()
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Does nothing."""
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Does nothing."""
|
||||
|
||||
def _get_group_limiter(self, group_id: Union[str, int, bool]) -> "AsyncLimiter":
|
||||
# Remove limiters that haven't been used for so long that all their capacity is unused
|
||||
# We only do that if we have a lot of limiters lying around to avoid looping on every call
|
||||
# This is a minimal effort approach - a full-fledged cache could use a TTL approach
|
||||
# or at least adapt the threshold dynamically depending on the number of active limiters
|
||||
if len(self._group_limiters) > 512:
|
||||
# We copy to avoid modifying the dict while we iterate over it
|
||||
for key, limiter in self._group_limiters.copy().items():
|
||||
if key == group_id:
|
||||
continue
|
||||
if limiter.has_capacity(limiter.max_rate):
|
||||
del self._group_limiters[key]
|
||||
|
||||
if group_id not in self._group_limiters:
|
||||
self._group_limiters[group_id] = AsyncLimiter(
|
||||
max_rate=self._group_max_rate,
|
||||
time_period=self._group_time_period,
|
||||
)
|
||||
return self._group_limiters[group_id]
|
||||
|
||||
async def _run_request(
|
||||
self,
|
||||
chat: bool,
|
||||
group: Union[str, int, bool],
|
||||
allow_paid_broadcast: bool,
|
||||
callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, list[JSONDict]]]],
|
||||
args: Any,
|
||||
kwargs: dict[str, Any],
|
||||
) -> Union[bool, JSONDict, list[JSONDict]]:
|
||||
async def inner() -> Union[bool, JSONDict, list[JSONDict]]:
|
||||
# In case a retry_after was hit, we wait with processing the request
|
||||
await self._retry_after_event.wait()
|
||||
return await callback(*args, **kwargs)
|
||||
|
||||
if allow_paid_broadcast:
|
||||
async with self._apb_limiter:
|
||||
return await inner()
|
||||
else:
|
||||
base_context = self._base_limiter if (chat and self._base_limiter) else null_context()
|
||||
group_context = (
|
||||
self._get_group_limiter(group)
|
||||
if group and self._group_max_rate
|
||||
else null_context()
|
||||
)
|
||||
|
||||
async with group_context, base_context:
|
||||
return await inner()
|
||||
|
||||
# mypy doesn't understand that the last run of the for loop raises an exception
|
||||
async def process_request(
|
||||
self,
|
||||
callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, list[JSONDict]]]],
|
||||
args: Any,
|
||||
kwargs: dict[str, Any],
|
||||
endpoint: str, # noqa: ARG002
|
||||
data: dict[str, Any],
|
||||
rate_limit_args: Optional[int],
|
||||
) -> Union[bool, JSONDict, list[JSONDict]]:
|
||||
"""
|
||||
Processes a request by applying rate limiting.
|
||||
|
||||
See :meth:`telegram.ext.BaseRateLimiter.process_request` for detailed information on the
|
||||
arguments.
|
||||
|
||||
Args:
|
||||
rate_limit_args (:obj:`None` | :obj:`int`): If set, specifies the maximum number of
|
||||
retries to be made in case of a :exc:`~telegram.error.RetryAfter` exception.
|
||||
Defaults to :paramref:`AIORateLimiter.max_retries`.
|
||||
"""
|
||||
max_retries = rate_limit_args or self._max_retries
|
||||
|
||||
group: Union[int, str, bool] = False
|
||||
chat: bool = False
|
||||
chat_id = data.get("chat_id")
|
||||
allow_paid_broadcast = data.get("allow_paid_broadcast", False)
|
||||
if chat_id is not None:
|
||||
chat = True
|
||||
|
||||
# In case user passes integer chat id as string
|
||||
with contextlib.suppress(ValueError, TypeError):
|
||||
chat_id = int(chat_id) # type: ignore[arg-type]
|
||||
|
||||
if (isinstance(chat_id, int) and chat_id < 0) or isinstance(chat_id, str):
|
||||
# string chat_id only works for channels and supergroups
|
||||
# We can't really tell channels from groups though ...
|
||||
group = chat_id
|
||||
|
||||
for i in range(max_retries + 1):
|
||||
try:
|
||||
return await self._run_request(
|
||||
chat=chat,
|
||||
group=group,
|
||||
allow_paid_broadcast=allow_paid_broadcast,
|
||||
callback=callback,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
)
|
||||
except RetryAfter as exc:
|
||||
if i == max_retries:
|
||||
_LOGGER.exception(
|
||||
"Rate limit hit after maximum of %d retries", max_retries, exc_info=exc
|
||||
)
|
||||
raise
|
||||
|
||||
sleep = exc._retry_after.total_seconds() + 0.1 # pylint: disable=protected-access
|
||||
_LOGGER.info("Rate limit hit. Retrying after %f seconds", sleep)
|
||||
# Make sure we don't allow other requests to be processed
|
||||
self._retry_after_event.clear()
|
||||
await asyncio.sleep(sleep)
|
||||
finally:
|
||||
# Allow other requests to be processed
|
||||
self._retry_after_event.set()
|
||||
return None # type: ignore[return-value]
|
||||
1937
.venv/lib/python3.12/site-packages/telegram/ext/_application.py
Normal file
1937
.venv/lib/python3.12/site-packages/telegram/ext/_application.py
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,440 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the BasePersistence class."""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Generic, NamedTuple, NoReturn, Optional
|
||||
|
||||
from telegram._bot import Bot
|
||||
from telegram.ext._extbot import ExtBot
|
||||
from telegram.ext._utils.types import BD, CD, UD, CDCData, ConversationDict, ConversationKey
|
||||
|
||||
|
||||
class PersistenceInput(NamedTuple):
|
||||
"""Convenience wrapper to group boolean input for the :paramref:`~BasePersistence.store_data`
|
||||
parameter for :class:`BasePersistence`.
|
||||
|
||||
Args:
|
||||
bot_data (:obj:`bool`, optional): Whether the setting should be applied for ``bot_data``.
|
||||
Defaults to :obj:`True`.
|
||||
chat_data (:obj:`bool`, optional): Whether the setting should be applied for ``chat_data``.
|
||||
Defaults to :obj:`True`.
|
||||
user_data (:obj:`bool`, optional): Whether the setting should be applied for ``user_data``.
|
||||
Defaults to :obj:`True`.
|
||||
callback_data (:obj:`bool`, optional): Whether the setting should be applied for
|
||||
``callback_data``. Defaults to :obj:`True`.
|
||||
|
||||
Attributes:
|
||||
bot_data (:obj:`bool`): Whether the setting should be applied for ``bot_data``.
|
||||
chat_data (:obj:`bool`): Whether the setting should be applied for ``chat_data``.
|
||||
user_data (:obj:`bool`): Whether the setting should be applied for ``user_data``.
|
||||
callback_data (:obj:`bool`): Whether the setting should be applied for ``callback_data``.
|
||||
|
||||
"""
|
||||
|
||||
bot_data: bool = True
|
||||
chat_data: bool = True
|
||||
user_data: bool = True
|
||||
callback_data: bool = True
|
||||
|
||||
|
||||
class BasePersistence(Generic[UD, CD, BD], ABC):
|
||||
"""Interface class for adding persistence to your bot.
|
||||
Subclass this object for different implementations of a persistent bot.
|
||||
|
||||
Attention:
|
||||
The interface provided by this class is intended to be accessed exclusively by
|
||||
: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:
|
||||
|
||||
* :meth:`get_bot_data`
|
||||
* :meth:`update_bot_data`
|
||||
* :meth:`refresh_bot_data`
|
||||
* :meth:`get_chat_data`
|
||||
* :meth:`update_chat_data`
|
||||
* :meth:`refresh_chat_data`
|
||||
* :meth:`drop_chat_data`
|
||||
* :meth:`get_user_data`
|
||||
* :meth:`update_user_data`
|
||||
* :meth:`refresh_user_data`
|
||||
* :meth:`drop_user_data`
|
||||
* :meth:`get_callback_data`
|
||||
* :meth:`update_callback_data`
|
||||
* :meth:`get_conversations`
|
||||
* :meth:`update_conversation`
|
||||
* :meth:`flush`
|
||||
|
||||
If you don't actually need one of those methods, a simple :keyword:`pass` is enough.
|
||||
For example, if you don't store ``bot_data``, you don't need :meth:`get_bot_data`,
|
||||
:meth:`update_bot_data` or :meth:`refresh_bot_data`.
|
||||
|
||||
Note:
|
||||
You should avoid saving :class:`telegram.Bot` instances. This is because if you change e.g.
|
||||
the bots token, this won't propagate to the serialized instances and may lead to exceptions.
|
||||
|
||||
To prevent this, the implementation may use :attr:`bot` to replace bot instances with a
|
||||
placeholder before serialization and insert :attr:`bot` back when loading the data.
|
||||
Since :attr:`bot` will be set when the process starts, this will be the up-to-date bot
|
||||
instance.
|
||||
|
||||
If the persistence implementation does not take care of this, you should make sure not to
|
||||
store any bot instances in the data that will be persisted. E.g. in case of
|
||||
:class:`telegram.TelegramObject`, one may call :meth:`set_bot` to ensure that shortcuts like
|
||||
:meth:`telegram.Message.reply_text` are available.
|
||||
|
||||
This class is a :class:`~typing.Generic` class and accepts three type variables:
|
||||
|
||||
1. The type of the second argument of :meth:`update_user_data`, which must coincide with the
|
||||
type of the second argument of :meth:`refresh_user_data` and the values in the dictionary
|
||||
returned by :meth:`get_user_data`.
|
||||
2. The type of the second argument of :meth:`update_chat_data`, which must coincide with the
|
||||
type of the second argument of :meth:`refresh_chat_data` and the values in the dictionary
|
||||
returned by :meth:`get_chat_data`.
|
||||
3. The type of the argument of :meth:`update_bot_data`, which must coincide with the
|
||||
type of the argument of :meth:`refresh_bot_data` and the return value of
|
||||
:meth:`get_bot_data`.
|
||||
|
||||
.. seealso:: :wiki:`Architecture Overview <Architecture>`,
|
||||
:wiki:`Making Your Bot Persistent <Making-your-bot-persistent>`
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
|
||||
* The parameters and attributes ``store_*_data`` were replaced by :attr:`store_data`.
|
||||
* ``insert/replace_bot`` was dropped. Serialization of bot instances now needs to be
|
||||
handled by the specific implementation - see above note.
|
||||
|
||||
Args:
|
||||
store_data (:class:`~telegram.ext.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:: 20.0
|
||||
Attributes:
|
||||
store_data (:class:`~telegram.ext.PersistenceInput`): Specifies which kinds of data will
|
||||
be saved by this persistence instance.
|
||||
bot (:class:`telegram.Bot`): The bot associated with the persistence.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_update_interval",
|
||||
"bot",
|
||||
"store_data",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
store_data: Optional[PersistenceInput] = None,
|
||||
update_interval: float = 60,
|
||||
):
|
||||
self.store_data: PersistenceInput = store_data or PersistenceInput()
|
||||
self._update_interval: float = 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:: 20.0
|
||||
"""
|
||||
return self._update_interval
|
||||
|
||||
@update_interval.setter
|
||||
def update_interval(self, _: object) -> NoReturn:
|
||||
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.")
|
||||
|
||||
self.bot = bot
|
||||
|
||||
@abstractmethod
|
||||
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:
|
||||
|
||||
- :obj:`dict`
|
||||
- The type from :attr:`telegram.ext.ContextTypes.user_data`
|
||||
if :class:`telegram.ext.ContextTypes` is used.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
This method may now return a :obj:`dict` instead of a :obj:`collections.defaultdict`
|
||||
|
||||
Returns:
|
||||
dict[:obj:`int`, :obj:`dict` | :attr:`telegram.ext.ContextTypes.user_data`]:
|
||||
The restored user data.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
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:
|
||||
|
||||
- :obj:`dict`
|
||||
- The type from :attr:`telegram.ext.ContextTypes.chat_data`
|
||||
if :class:`telegram.ext.ContextTypes` is used.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
This method may now return a :obj:`dict` instead of a :obj:`collections.defaultdict`
|
||||
|
||||
Returns:
|
||||
dict[:obj:`int`, :obj:`dict` | :attr:`telegram.ext.ContextTypes.chat_data`]:
|
||||
The restored chat data.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
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:
|
||||
|
||||
- :obj:`dict`
|
||||
- The type from :attr:`telegram.ext.ContextTypes.bot_data`
|
||||
if :class:`telegram.ext.ContextTypes` are used.
|
||||
|
||||
Returns:
|
||||
dict[:obj:`int`, :obj:`dict` | :attr:`telegram.ext.ContextTypes.bot_data`]:
|
||||
The restored bot data.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
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:: 20.0
|
||||
Changed this method into an :external:func:`~abc.abstractmethod`.
|
||||
|
||||
Returns:
|
||||
tuple[list[tuple[:obj:`str`, :obj:`float`, dict[:obj:`str`, :class:`object`]]],
|
||||
dict[:obj:`str`, :obj:`str`]] | :obj:`None`: The restored metadata or :obj:`None`,
|
||||
if no data was stored.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
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 :paramref:`name` or an empty
|
||||
:obj:`dict`.
|
||||
|
||||
Args:
|
||||
name (:obj:`str`): The handlers name.
|
||||
|
||||
Returns:
|
||||
:obj:`dict`: The restored conversations for the handler.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
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.
|
||||
|
||||
Args:
|
||||
name (:obj:`str`): The handler's name.
|
||||
key (:obj:`tuple`): The key the state is changed for.
|
||||
new_state (:class:`object`): The new state for the given key.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
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.Application.user_data` ``[user_id]``.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
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.Application.chat_data` ``[chat_id]``.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
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.Application.bot_data`.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
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:: 20.0
|
||||
Changed this method into an :external:func:`~abc.abstractmethod`.
|
||||
|
||||
Args:
|
||||
data (tuple[list[tuple[:obj:`str`, :obj:`float`, \
|
||||
dict[:obj:`str`, :obj:`Any`]]], dict[:obj:`str`, :obj:`str`]] | :obj:`None`):
|
||||
The relevant data to restore :class:`telegram.ext.CallbackDataCache`.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
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:: 20.0
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int`): The chat id to delete from the persistence.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
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:: 20.0
|
||||
|
||||
Args:
|
||||
user_id (:obj:`int`): The user id to delete from the persistence.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
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.
|
||||
|
||||
Tip:
|
||||
This method is expected to edit the object :paramref:`user_data` in-place instead of
|
||||
returning a new object.
|
||||
|
||||
Warning:
|
||||
When using :meth:`~telegram.ext.ApplicationBuilder.concurrent_updates`, this method
|
||||
may be called while a handler callback is still running. This might lead to race
|
||||
conditions.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Changed this method into an :external:func:`~abc.abstractmethod`.
|
||||
|
||||
Args:
|
||||
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
|
||||
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.
|
||||
|
||||
Tip:
|
||||
This method is expected to edit the object :paramref:`chat_data` in-place instead of
|
||||
returning a new object.
|
||||
|
||||
Warning:
|
||||
When using :meth:`~telegram.ext.ApplicationBuilder.concurrent_updates`, this method
|
||||
may be called while a handler callback is still running. This might lead to race
|
||||
conditions.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Changed this method into an :external:func:`~abc.abstractmethod`.
|
||||
|
||||
Args:
|
||||
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
|
||||
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.
|
||||
|
||||
Tip:
|
||||
This method is expected to edit the object :paramref:`bot_data` in-place instead of
|
||||
returning a new object.
|
||||
|
||||
Warning:
|
||||
When using :meth:`~telegram.ext.ApplicationBuilder.concurrent_updates`, this method
|
||||
may be called while a handler callback is still running. This might lead to race
|
||||
conditions.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Changed this method into an :external:func:`~abc.abstractmethod`.
|
||||
|
||||
Args:
|
||||
bot_data (:obj:`dict` | :attr:`telegram.ext.ContextTypes.bot_data`):
|
||||
The ``bot_data``.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
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:: 20.0
|
||||
Changed this method into an :external:func:`~abc.abstractmethod`.
|
||||
"""
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 class that allows to rate limit requests to the Bot API."""
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Coroutine
|
||||
from typing import Any, Callable, Generic, Optional, Union
|
||||
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram.ext._utils.types import RLARGS
|
||||
|
||||
|
||||
class BaseRateLimiter(ABC, Generic[RLARGS]):
|
||||
"""
|
||||
Abstract interface class that allows to rate limit the requests that python-telegram-bot
|
||||
sends to the Telegram Bot API. An implementation of this class
|
||||
must implement all abstract methods and properties.
|
||||
|
||||
This class is a :class:`~typing.Generic` class and accepts one type variable that specifies
|
||||
the type of the argument :paramref:`~process_request.rate_limit_args` of
|
||||
:meth:`process_request` and the methods of :class:`~telegram.ext.ExtBot`.
|
||||
|
||||
Hint:
|
||||
Requests to :meth:`~telegram.Bot.get_updates` are never rate limited.
|
||||
|
||||
.. seealso:: :wiki:`Architecture Overview <Architecture>`,
|
||||
:wiki:`Avoiding Flood Limits <Avoiding-flood-limits>`
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
@abstractmethod
|
||||
async def initialize(self) -> None:
|
||||
"""Initialize resources used by this class. Must be implemented by a subclass."""
|
||||
|
||||
@abstractmethod
|
||||
async def shutdown(self) -> None:
|
||||
"""Stop & clear resources used by this class. Must be implemented by a subclass."""
|
||||
|
||||
@abstractmethod
|
||||
async def process_request(
|
||||
self,
|
||||
callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, list[JSONDict]]]],
|
||||
args: Any,
|
||||
kwargs: dict[str, Any],
|
||||
endpoint: str,
|
||||
data: dict[str, Any],
|
||||
rate_limit_args: Optional[RLARGS],
|
||||
) -> Union[bool, JSONDict, list[JSONDict]]:
|
||||
"""
|
||||
Process a request. Must be implemented by a subclass.
|
||||
|
||||
This method must call :paramref:`callback` and return the result of the call.
|
||||
`When` the callback is called is up to the implementation.
|
||||
|
||||
Important:
|
||||
This method must only return once the result of :paramref:`callback` is known!
|
||||
|
||||
If a :exc:`~telegram.error.RetryAfter` error is raised, this method may try to make
|
||||
a new request by calling the callback again.
|
||||
|
||||
Warning:
|
||||
This method *should not* handle any other exception raised by :paramref:`callback`!
|
||||
|
||||
There are basically two different approaches how a rate limiter can be implemented:
|
||||
|
||||
1. React only if necessary. In this case, the :paramref:`callback` is called without any
|
||||
precautions. If a :exc:`~telegram.error.RetryAfter` error is raised, processing requests
|
||||
is halted for the :attr:`~telegram.error.RetryAfter.retry_after` and finally the
|
||||
:paramref:`callback` is called again. This approach is often amendable for bots that
|
||||
don't have a large user base and/or don't send more messages than they get updates.
|
||||
2. Throttle all outgoing requests. In this case the implementation makes sure that the
|
||||
requests are spread out over a longer time interval in order to stay below the rate
|
||||
limits. This approach is often amendable for bots that have a large user base and/or
|
||||
send more messages than they get updates.
|
||||
|
||||
An implementation can use the information provided by :paramref:`data`,
|
||||
:paramref:`endpoint` and :paramref:`rate_limit_args` to handle each request differently.
|
||||
|
||||
Examples:
|
||||
* It is usually desirable to call :meth:`telegram.Bot.answer_inline_query`
|
||||
as quickly as possible, while delaying :meth:`telegram.Bot.send_message`
|
||||
is acceptable.
|
||||
* There are `different <https://core.telegram.org/bots/faq\
|
||||
#my-bot-is-hitting-limits-how-do-i-avoid-this>`_ rate limits for group chats and
|
||||
private chats.
|
||||
* When sending broadcast messages to a large number of users, these requests can
|
||||
typically be delayed for a longer time than messages that are direct replies to a
|
||||
user input.
|
||||
|
||||
Args:
|
||||
callback (Callable[..., :term:`coroutine`]): The coroutine function that must be called
|
||||
to make the request.
|
||||
args (tuple[:obj:`object`]): The positional arguments for the :paramref:`callback`
|
||||
function.
|
||||
kwargs (dict[:obj:`str`, :obj:`object`]): The keyword arguments for the
|
||||
:paramref:`callback` function.
|
||||
endpoint (:obj:`str`): The endpoint that the request is made for, e.g.
|
||||
``"sendMessage"``.
|
||||
data (dict[:obj:`str`, :obj:`object`]): The parameters that were passed to the method
|
||||
of :class:`~telegram.ext.ExtBot`. Any ``api_kwargs`` are included in this and
|
||||
any :paramref:`~telegram.ext.ExtBot.defaults` are already applied.
|
||||
|
||||
Example:
|
||||
|
||||
When calling::
|
||||
|
||||
await ext_bot.send_message(
|
||||
chat_id=1,
|
||||
text="Hello world!",
|
||||
api_kwargs={"custom": "arg"}
|
||||
)
|
||||
|
||||
then :paramref:`data` will be::
|
||||
|
||||
{"chat_id": 1, "text": "Hello world!", "custom": "arg"}
|
||||
|
||||
rate_limit_args (:obj:`None` | :class:`object`): Custom arguments passed to the methods
|
||||
of :class:`~telegram.ext.ExtBot`. Can e.g. be used to specify the priority of
|
||||
the request.
|
||||
|
||||
Returns:
|
||||
:obj:`bool` | dict[:obj:`str`, :obj:`object`] | :obj:`None`: The result of the
|
||||
callback function.
|
||||
"""
|
||||
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 BaseProcessor class."""
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Any, Optional, TypeVar, final
|
||||
|
||||
from telegram.ext._utils.asyncio import TrackedBoundedSemaphore
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Awaitable
|
||||
|
||||
_BUPT = TypeVar("_BUPT", bound="BaseUpdateProcessor")
|
||||
|
||||
|
||||
class BaseUpdateProcessor(AbstractAsyncContextManager["BaseUpdateProcessor"], ABC):
|
||||
"""An abstract base class for update processors. You can use this class to implement
|
||||
your own update processor.
|
||||
|
||||
Instances of this class can be used as asyncio context managers, where
|
||||
|
||||
.. code:: python
|
||||
|
||||
async with processor:
|
||||
# code
|
||||
|
||||
is roughly equivalent to
|
||||
|
||||
.. code:: python
|
||||
|
||||
try:
|
||||
await processor.initialize()
|
||||
# code
|
||||
finally:
|
||||
await processor.shutdown()
|
||||
|
||||
.. seealso:: :meth:`__aenter__` and :meth:`__aexit__`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
|
||||
.. versionadded:: 20.4
|
||||
|
||||
Args:
|
||||
max_concurrent_updates (:obj:`int`): The maximum number of updates to be processed
|
||||
concurrently. If this number is exceeded, new updates will be queued until the number
|
||||
of currently processed updates decreases.
|
||||
|
||||
Raises:
|
||||
:exc:`ValueError`: If :paramref:`max_concurrent_updates` is a non-positive integer.
|
||||
"""
|
||||
|
||||
__slots__ = ("_max_concurrent_updates", "_semaphore")
|
||||
|
||||
def __init__(self, max_concurrent_updates: int):
|
||||
self._max_concurrent_updates = max_concurrent_updates
|
||||
if self.max_concurrent_updates < 1:
|
||||
raise ValueError("`max_concurrent_updates` must be a positive integer!")
|
||||
self._semaphore = TrackedBoundedSemaphore(self.max_concurrent_updates)
|
||||
|
||||
async def __aenter__(self: _BUPT) -> _BUPT:
|
||||
"""|async_context_manager| :meth:`initializes <initialize>` the Processor.
|
||||
|
||||
Returns:
|
||||
The initialized Processor instance.
|
||||
|
||||
Raises:
|
||||
:exc:`Exception`: If an exception is raised during initialization, :meth:`shutdown`
|
||||
is called in this case.
|
||||
"""
|
||||
try:
|
||||
await self.initialize()
|
||||
except Exception:
|
||||
await self.shutdown()
|
||||
raise
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: Optional[type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
) -> None:
|
||||
"""|async_context_manager| :meth:`shuts down <shutdown>` the Processor."""
|
||||
await self.shutdown()
|
||||
|
||||
@property
|
||||
def max_concurrent_updates(self) -> int:
|
||||
""":obj:`int`: The maximum number of updates that can be processed concurrently."""
|
||||
return self._max_concurrent_updates
|
||||
|
||||
@property
|
||||
def current_concurrent_updates(self) -> int:
|
||||
""":obj:`int`: The number of updates currently being processed.
|
||||
|
||||
Caution:
|
||||
This value is a snapshot of the current number of updates being processed. It may
|
||||
change immediately after being read.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
"""
|
||||
return self.max_concurrent_updates - self._semaphore.current_value
|
||||
|
||||
@abstractmethod
|
||||
async def do_process_update(
|
||||
self,
|
||||
update: object,
|
||||
coroutine: "Awaitable[Any]",
|
||||
) -> None:
|
||||
"""Custom implementation of how to process an update. Must be implemented by a subclass.
|
||||
|
||||
Warning:
|
||||
This method will be called by :meth:`process_update`. It should *not* be called
|
||||
manually.
|
||||
|
||||
Args:
|
||||
update (:obj:`object`): The update to be processed.
|
||||
coroutine (:term:`Awaitable`): The coroutine that will be awaited to process the
|
||||
update.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def initialize(self) -> None:
|
||||
"""Initializes the processor so resources can be allocated. Must be implemented by a
|
||||
subclass.
|
||||
|
||||
.. seealso::
|
||||
:meth:`shutdown`
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def shutdown(self) -> None:
|
||||
"""Shutdown the processor so resources can be freed. Must be implemented by a subclass.
|
||||
|
||||
.. seealso::
|
||||
:meth:`initialize`
|
||||
"""
|
||||
|
||||
@final
|
||||
async def process_update(
|
||||
self,
|
||||
update: object,
|
||||
coroutine: "Awaitable[Any]",
|
||||
) -> None:
|
||||
"""Calls :meth:`do_process_update` with a semaphore to limit the number of concurrent
|
||||
updates.
|
||||
|
||||
Args:
|
||||
update (:obj:`object`): The update to be processed.
|
||||
coroutine (:term:`Awaitable`): The coroutine that will be awaited to process the
|
||||
update.
|
||||
"""
|
||||
async with self._semaphore:
|
||||
await self.do_process_update(update, coroutine)
|
||||
|
||||
|
||||
class SimpleUpdateProcessor(BaseUpdateProcessor):
|
||||
"""Instance of :class:`telegram.ext.BaseUpdateProcessor` that immediately awaits the
|
||||
coroutine, i.e. does not apply any additional processing. This is used by default when
|
||||
:attr:`telegram.ext.ApplicationBuilder.concurrent_updates` is :obj:`int`.
|
||||
|
||||
.. versionadded:: 20.4
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
async def do_process_update(
|
||||
self,
|
||||
update: object, # noqa: ARG002
|
||||
coroutine: "Awaitable[Any]",
|
||||
) -> None:
|
||||
"""Immediately awaits the coroutine, i.e. does not apply any additional processing.
|
||||
|
||||
Args:
|
||||
update (:obj:`object`): The update to be processed.
|
||||
coroutine (:term:`Awaitable`): The coroutine that will be awaited to process the
|
||||
update.
|
||||
"""
|
||||
await coroutine
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Does nothing."""
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Does nothing."""
|
||||
@@ -0,0 +1,429 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the CallbackContext class."""
|
||||
|
||||
from collections.abc import Awaitable, Generator
|
||||
from re import Match
|
||||
from typing import TYPE_CHECKING, Any, Generic, NoReturn, Optional, TypeVar, Union
|
||||
|
||||
from telegram._callbackquery import CallbackQuery
|
||||
from telegram._update import Update
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.ext._extbot import ExtBot
|
||||
from telegram.ext._utils.types import BD, BT, CD, UD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from asyncio import Future, Queue
|
||||
|
||||
from telegram.ext import Application, Job, JobQueue
|
||||
from telegram.ext._utils.types import CCT
|
||||
|
||||
_STORING_DATA_WIKI = (
|
||||
"https://github.com/python-telegram-bot/python-telegram-bot"
|
||||
"/wiki/Storing-bot%2C-user-and-chat-related-data"
|
||||
)
|
||||
|
||||
# something like poor mans "typing.Self" for py<3.11
|
||||
ST = TypeVar("ST", bound="CallbackContext[Any, Any, Any, Any]")
|
||||
|
||||
|
||||
class CallbackContext(Generic[BT, UD, CD, BD]):
|
||||
"""
|
||||
This is a context object passed to the callback called by :class:`telegram.ext.BaseHandler`
|
||||
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.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
|
||||
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 with :paramref:`telegram.ext.BaseHandler.block` set to
|
||||
:obj:`False` or :attr:`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.
|
||||
|
||||
This class is a :class:`~typing.Generic` class and accepts four type variables:
|
||||
|
||||
1. The type of :attr:`bot`. Must be :class:`telegram.Bot` or a subclass of that class.
|
||||
2. The type of :attr:`user_data` (if :attr:`user_data` is not :obj:`None`).
|
||||
3. The type of :attr:`chat_data` (if :attr:`chat_data` is not :obj:`None`).
|
||||
4. The type of :attr:`bot_data` (if :attr:`bot_data` is not :obj:`None`).
|
||||
|
||||
Examples:
|
||||
* :any:`Context Types Bot <examples.contexttypesbot>`
|
||||
* :any:`Custom Webhook Bot <examples.customwebhookbot>`
|
||||
|
||||
.. seealso:: :attr:`telegram.ext.ContextTypes.DEFAULT_TYPE`,
|
||||
:wiki:`Job Queue <Extensions---JobQueue>`
|
||||
|
||||
Args:
|
||||
application (:class:`telegram.ext.Application`): The application associated with this
|
||||
context.
|
||||
chat_id (:obj:`int`, optional): The ID of the chat associated with this object. Used
|
||||
to provide :attr:`chat_data`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
user_id (:obj:`int`, optional): The ID of the user associated with this object. Used
|
||||
to provide :attr:`user_data`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
Attributes:
|
||||
coroutine (:term:`awaitable`): Optional. Only present in error handlers if the
|
||||
error was caused by an awaitable run with :meth:`Application.create_task` or a handler
|
||||
callback with :attr:`block=False <BaseHandler.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.
|
||||
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 (: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.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
:attr:`job` is now also present in error handlers if the error is caused by a job.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"__dict__",
|
||||
"_application",
|
||||
"_chat_id",
|
||||
"_user_id",
|
||||
"args",
|
||||
"coroutine",
|
||||
"error",
|
||||
"job",
|
||||
"matches",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self: ST,
|
||||
application: "Application[BT, ST, UD, CD, BD, Any]",
|
||||
chat_id: Optional[int] = None,
|
||||
user_id: Optional[int] = None,
|
||||
):
|
||||
self._application: Application[BT, ST, UD, CD, BD, Any] = application
|
||||
self._chat_id: Optional[int] = chat_id
|
||||
self._user_id: Optional[int] = user_id
|
||||
self.args: Optional[list[str]] = None
|
||||
self.matches: Optional[list[Match[str]]] = None
|
||||
self.error: Optional[Exception] = None
|
||||
self.job: Optional[Job[Any]] = None
|
||||
self.coroutine: Optional[
|
||||
Union[Generator[Optional[Future[object]], None, Any], Awaitable[Any]]
|
||||
] = None
|
||||
|
||||
@property
|
||||
def application(self) -> "Application[BT, ST, UD, CD, BD, Any]":
|
||||
""":class:`telegram.ext.Application`: The application associated with this context."""
|
||||
return self._application # type: ignore[return-value]
|
||||
|
||||
@property
|
||||
def bot_data(self) -> BD:
|
||||
""":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`.
|
||||
|
||||
.. seealso:: :wiki:`Storing Bot, User and Chat Related Data\
|
||||
<Storing-bot%2C-user-and-chat-related-data>`
|
||||
"""
|
||||
return self.application.bot_data
|
||||
|
||||
@bot_data.setter
|
||||
def bot_data(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
f"You can not assign a new value to bot_data, see {_STORING_DATA_WIKI}"
|
||||
)
|
||||
|
||||
@property
|
||||
def chat_data(self) -> Optional[CD]:
|
||||
""":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
|
||||
``chat_data`` needs to be transferred. For details see our
|
||||
:wiki:`wiki page <Storing-bot,-user-and-chat-related-data#chat-migration>`.
|
||||
|
||||
.. seealso:: :wiki:`Storing Bot, User and Chat Related Data\
|
||||
<Storing-bot%2C-user-and-chat-related-data>`
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
The chat data is now also present in error handlers if the error is caused by a job.
|
||||
"""
|
||||
if self._chat_id is not None:
|
||||
return self._application.chat_data[self._chat_id]
|
||||
return None
|
||||
|
||||
@chat_data.setter
|
||||
def chat_data(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
f"You can not assign a new value to chat_data, see {_STORING_DATA_WIKI}"
|
||||
)
|
||||
|
||||
@property
|
||||
def user_data(self) -> Optional[UD]:
|
||||
""":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`.
|
||||
|
||||
.. seealso:: :wiki:`Storing Bot, User and Chat Related Data\
|
||||
<Storing-bot%2C-user-and-chat-related-data>`
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
The user data is now also present in error handlers if the error is caused by a job.
|
||||
"""
|
||||
if self._user_id is not None:
|
||||
return self._application.user_data[self._user_id]
|
||||
return None
|
||||
|
||||
@user_data.setter
|
||||
def user_data(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
f"You can not assign a new value to user_data, see {_STORING_DATA_WIKI}"
|
||||
)
|
||||
|
||||
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.application.persistence:
|
||||
if self.application.persistence.store_data.bot_data:
|
||||
await self.application.persistence.refresh_bot_data(self.bot_data)
|
||||
if self.application.persistence.store_data.chat_data and self._chat_id is not None:
|
||||
await self.application.persistence.refresh_chat_data(
|
||||
chat_id=self._chat_id,
|
||||
chat_data=self.chat_data, # type: ignore[arg-type]
|
||||
)
|
||||
if self.application.persistence.store_data.user_data and self._user_id is not None:
|
||||
await self.application.persistence.refresh_user_data(
|
||||
user_id=self._user_id,
|
||||
user_data=self.user_data, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
def drop_callback_data(self, callback_query: CallbackQuery) -> None:
|
||||
"""
|
||||
Deletes the cached data for the specified callback query.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
Note:
|
||||
Will *not* raise exceptions in case the data is not found in the cache.
|
||||
*Will* raise :exc:`KeyError` in case the callback query can not be found in the cache.
|
||||
|
||||
.. seealso:: :wiki:`Arbitrary callback_data <Arbitrary-callback_data>`
|
||||
|
||||
Args:
|
||||
callback_query (:class:`telegram.CallbackQuery`): The callback query.
|
||||
|
||||
Raises:
|
||||
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):
|
||||
if self.bot.callback_data_cache is None:
|
||||
raise RuntimeError(
|
||||
"This telegram.ext.ExtBot instance does not use arbitrary callback data."
|
||||
)
|
||||
self.bot.callback_data_cache.drop_data(callback_query)
|
||||
else:
|
||||
raise RuntimeError( # noqa: TRY004
|
||||
"telegram.Bot does not allow for arbitrary callback data."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_error(
|
||||
cls: type["CCT"],
|
||||
update: object,
|
||||
error: Exception,
|
||||
application: "Application[BT, CCT, UD, CD, BD, Any]",
|
||||
job: Optional["Job[Any]"] = None,
|
||||
coroutine: Optional[
|
||||
Union[Generator[Optional["Future[object]"], None, Any], Awaitable[Any]]
|
||||
] = None,
|
||||
) -> "CCT":
|
||||
"""
|
||||
Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the error
|
||||
handlers.
|
||||
|
||||
.. seealso:: :meth:`telegram.ext.Application.add_error_handler`
|
||||
|
||||
.. versionchanged:: 20.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.
|
||||
application (:class:`telegram.ext.Application`): The application associated with this
|
||||
context.
|
||||
job (:class:`telegram.ext.Job`, optional): The job associated with the error.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
coroutine (:term:`awaitable`, optional): The awaitable associated
|
||||
with this error if the error was caused by a coroutine run with
|
||||
:meth:`Application.create_task` or a handler callback with
|
||||
:attr:`block=False <BaseHandler.block>`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. versionchanged:: 20.2
|
||||
Accepts :class:`asyncio.Future` and generator-based coroutine functions.
|
||||
Returns:
|
||||
:class:`telegram.ext.CallbackContext`
|
||||
"""
|
||||
# update and job will never be present at the same time
|
||||
if update is not None:
|
||||
self = cls.from_update(update, application)
|
||||
elif job is not None:
|
||||
self = cls.from_job(job, application)
|
||||
else:
|
||||
self = cls(application) # type: ignore
|
||||
|
||||
self.error = error
|
||||
self.coroutine = coroutine
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_update(
|
||||
cls: type["CCT"],
|
||||
update: object,
|
||||
application: "Application[Any, CCT, Any, Any, Any, Any]",
|
||||
) -> "CCT":
|
||||
"""
|
||||
Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to the
|
||||
handlers.
|
||||
|
||||
.. seealso:: :meth:`telegram.ext.Application.add_handler`
|
||||
|
||||
Args:
|
||||
update (:obj:`object` | :class:`telegram.Update`): The update.
|
||||
application (:class:`telegram.ext.Application`): The application associated with this
|
||||
context.
|
||||
|
||||
Returns:
|
||||
:class:`telegram.ext.CallbackContext`
|
||||
"""
|
||||
if isinstance(update, Update):
|
||||
chat = update.effective_chat
|
||||
user = update.effective_user
|
||||
|
||||
chat_id = chat.id if chat else None
|
||||
user_id = user.id if user else None
|
||||
|
||||
return cls(application, chat_id=chat_id, user_id=user_id) # type: ignore
|
||||
return cls(application) # type: ignore
|
||||
|
||||
@classmethod
|
||||
def from_job(
|
||||
cls: type["CCT"],
|
||||
job: "Job[CCT]",
|
||||
application: "Application[Any, CCT, Any, Any, Any, Any]",
|
||||
) -> "CCT":
|
||||
"""
|
||||
Constructs an instance of :class:`telegram.ext.CallbackContext` to be passed to a
|
||||
job callback.
|
||||
|
||||
.. seealso:: :meth:`telegram.ext.JobQueue`
|
||||
|
||||
Args:
|
||||
job (:class:`telegram.ext.Job`): The job.
|
||||
application (:class:`telegram.ext.Application`): The application associated with this
|
||||
context.
|
||||
|
||||
Returns:
|
||||
:class:`telegram.ext.CallbackContext`
|
||||
"""
|
||||
self = cls(application, chat_id=job.chat_id, user_id=job.user_id) # type: ignore
|
||||
self.job = job
|
||||
return self
|
||||
|
||||
def update(self, data: dict[str, object]) -> None:
|
||||
"""Updates ``self.__slots__`` with the passed data.
|
||||
|
||||
Args:
|
||||
data (dict[:obj:`str`, :obj:`object`]): The data.
|
||||
"""
|
||||
for key, value in data.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
@property
|
||||
def bot(self) -> BT:
|
||||
""":class:`telegram.Bot`: The bot associated with this context."""
|
||||
return self._application.bot
|
||||
|
||||
@property
|
||||
def job_queue(self) -> Optional["JobQueue[ST]"]:
|
||||
"""
|
||||
:class:`telegram.ext.JobQueue`: The :class:`JobQueue` used by the
|
||||
:class:`telegram.ext.Application`.
|
||||
|
||||
.. seealso:: :wiki:`Job Queue <Extensions---JobQueue>`
|
||||
"""
|
||||
if self._application._job_queue is None: # pylint: disable=protected-access
|
||||
warn(
|
||||
"No `JobQueue` set up. To use `JobQueue`, you must install PTB via "
|
||||
'`pip install "python-telegram-bot[job-queue]"`.',
|
||||
stacklevel=2,
|
||||
)
|
||||
return self._application._job_queue # pylint: disable=protected-access
|
||||
|
||||
@property
|
||||
def update_queue(self) -> "Queue[object]":
|
||||
"""
|
||||
: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._application.update_queue
|
||||
|
||||
@property
|
||||
def match(self) -> Optional[Match[str]]:
|
||||
"""
|
||||
:meth:`re.Match <re.Match.expand>`: The first match from :attr:`matches`.
|
||||
Useful if you are only filtering using a single regex filter.
|
||||
Returns :obj:`None` if :attr:`matches` is empty.
|
||||
"""
|
||||
try:
|
||||
return self.matches[0] # type: ignore[index] # pylint: disable=unsubscriptable-object
|
||||
except (IndexError, TypeError):
|
||||
return None
|
||||
@@ -0,0 +1,469 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 CallbackDataCache class."""
|
||||
import datetime as dtm
|
||||
import time
|
||||
from collections.abc import MutableMapping
|
||||
from typing import TYPE_CHECKING, Any, Optional, Union, cast
|
||||
from uuid import uuid4
|
||||
|
||||
try:
|
||||
from cachetools import LRUCache
|
||||
|
||||
CACHE_TOOLS_AVAILABLE = True
|
||||
except ImportError:
|
||||
CACHE_TOOLS_AVAILABLE = False
|
||||
|
||||
|
||||
import contextlib
|
||||
|
||||
from telegram import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, User
|
||||
from telegram._utils.datetime import to_float_timestamp
|
||||
from telegram.error import TelegramError
|
||||
from telegram.ext._utils.types import CDCData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import ExtBot
|
||||
|
||||
|
||||
class InvalidCallbackData(TelegramError):
|
||||
"""
|
||||
Raised when the received callback data has been tampered with or deleted from cache.
|
||||
|
||||
Examples:
|
||||
:any:`Arbitrary Callback Data Bot <examples.arbitrarycallbackdatabot>`
|
||||
|
||||
.. seealso:: :wiki:`Arbitrary callback_data <Arbitrary-callback_data>`
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
Args:
|
||||
callback_data (:obj:`int`, optional): The button data of which the callback data could not
|
||||
be found.
|
||||
|
||||
Attributes:
|
||||
callback_data (:obj:`int`): Optional. The button data of which the callback data could not
|
||||
be found.
|
||||
"""
|
||||
|
||||
__slots__ = ("callback_data",)
|
||||
|
||||
def __init__(self, callback_data: Optional[str] = None) -> None:
|
||||
super().__init__(
|
||||
"The object belonging to this callback_data was deleted or the callback_data was "
|
||||
"manipulated."
|
||||
)
|
||||
self.callback_data: Optional[str] = callback_data
|
||||
|
||||
def __reduce__(self) -> tuple[type, tuple[Optional[str]]]: # type: ignore[override]
|
||||
"""Defines how to serialize the exception for pickle. See
|
||||
:py:meth:`object.__reduce__` for more info.
|
||||
|
||||
Returns:
|
||||
:obj:`tuple`
|
||||
"""
|
||||
return self.__class__, (self.callback_data,)
|
||||
|
||||
|
||||
class _KeyboardData:
|
||||
__slots__ = ("access_time", "button_data", "keyboard_uuid")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
keyboard_uuid: str,
|
||||
access_time: Optional[float] = None,
|
||||
button_data: Optional[dict[str, object]] = None,
|
||||
):
|
||||
self.keyboard_uuid = keyboard_uuid
|
||||
self.button_data = button_data or {}
|
||||
self.access_time = access_time or time.time()
|
||||
|
||||
def update_access_time(self) -> None:
|
||||
"""Updates the access time with the current time."""
|
||||
self.access_time = time.time()
|
||||
|
||||
def to_tuple(self) -> tuple[str, float, dict[str, object]]:
|
||||
"""Gives a tuple representation consisting of the keyboard uuid, the access time and the
|
||||
button data.
|
||||
"""
|
||||
return self.keyboard_uuid, self.access_time, self.button_data
|
||||
|
||||
|
||||
class CallbackDataCache:
|
||||
"""A custom cache for storing the callback data of a :class:`telegram.ext.ExtBot`. Internally,
|
||||
it keeps two mappings with fixed maximum size:
|
||||
|
||||
* One for mapping the data received in callback queries to the cached objects
|
||||
* One for mapping the IDs of received callback queries to the cached objects
|
||||
|
||||
The second mapping allows to manually drop data that has been cached for keyboards of messages
|
||||
sent via inline mode.
|
||||
If necessary, will drop the least recently used items.
|
||||
|
||||
Important:
|
||||
If you want to use this class, you must install PTB with the optional requirement
|
||||
``callback-data``, i.e.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install "python-telegram-bot[callback-data]"
|
||||
|
||||
Examples:
|
||||
:any:`Arbitrary Callback Data Bot <examples.arbitrarycallbackdatabot>`
|
||||
|
||||
.. seealso:: :wiki:`Architecture Overview <Architecture>`,
|
||||
:wiki:`Arbitrary callback_data <Arbitrary-callback_data>`
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
To use this class, PTB must be installed via
|
||||
``pip install "python-telegram-bot[callback-data]"``.
|
||||
|
||||
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``.
|
||||
|
||||
persistent_data (tuple[list[tuple[:obj:`str`, :obj:`float`, \
|
||||
dict[:obj:`str`, :class:`object`]]], dict[:obj:`str`, :obj:`str`]], optional): \
|
||||
Data to initialize the cache with, as returned by \
|
||||
:meth:`telegram.ext.BasePersistence.get_callback_data`.
|
||||
|
||||
Attributes:
|
||||
bot (:class:`telegram.ext.ExtBot`): The bot this cache is for.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("_callback_queries", "_keyboard_data", "_maxsize", "bot")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bot: "ExtBot[Any]",
|
||||
maxsize: int = 1024,
|
||||
persistent_data: Optional[CDCData] = None,
|
||||
):
|
||||
if not CACHE_TOOLS_AVAILABLE:
|
||||
raise RuntimeError(
|
||||
"To use `CallbackDataCache`, PTB must be installed via `pip install "
|
||||
'"python-telegram-bot[callback-data]"`.'
|
||||
)
|
||||
|
||||
self.bot: ExtBot[Any] = bot
|
||||
self._maxsize: int = maxsize
|
||||
self._keyboard_data: MutableMapping[str, _KeyboardData] = LRUCache(maxsize=maxsize)
|
||||
self._callback_queries: MutableMapping[str, str] = LRUCache(maxsize=maxsize)
|
||||
|
||||
if persistent_data:
|
||||
self.load_persistence_data(persistent_data)
|
||||
|
||||
def load_persistence_data(self, persistent_data: CDCData) -> None:
|
||||
"""Loads data into the cache.
|
||||
|
||||
Warning:
|
||||
This method is not intended to be called by users directly.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Args:
|
||||
persistent_data (tuple[list[tuple[:obj:`str`, :obj:`float`, \
|
||||
dict[:obj:`str`, :class:`object`]]], dict[:obj:`str`, :obj:`str`]], optional): \
|
||||
Data to load, as returned by \
|
||||
:meth:`telegram.ext.BasePersistence.get_callback_data`.
|
||||
"""
|
||||
keyboard_data, callback_queries = persistent_data
|
||||
for key, value in callback_queries.items():
|
||||
self._callback_queries[key] = value
|
||||
for uuid, access_time, data in keyboard_data:
|
||||
self._keyboard_data[uuid] = _KeyboardData(
|
||||
keyboard_uuid=uuid, access_time=access_time, button_data=data
|
||||
)
|
||||
|
||||
@property
|
||||
def maxsize(self) -> int:
|
||||
""":obj:`int`: The maximum size of the cache.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
This property is now read-only.
|
||||
"""
|
||||
return self._maxsize
|
||||
|
||||
@property
|
||||
def persistence_data(self) -> CDCData:
|
||||
"""tuple[list[tuple[:obj:`str`, :obj:`float`, dict[:obj:`str`, :class:`object`]]],
|
||||
dict[:obj:`str`, :obj:`str`]]: The data that needs to be persisted to allow
|
||||
caching callback data across bot reboots.
|
||||
"""
|
||||
# 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
|
||||
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.
|
||||
|
||||
Args:
|
||||
reply_markup (:class:`telegram.InlineKeyboardMarkup`): The keyboard.
|
||||
|
||||
Returns:
|
||||
:class:`telegram.InlineKeyboardMarkup`: The keyboard to be passed to Telegram.
|
||||
|
||||
"""
|
||||
keyboard_uuid = uuid4().hex
|
||||
keyboard_data = _KeyboardData(keyboard_uuid)
|
||||
|
||||
# Built a new nested list of buttons by replacing the callback data if needed
|
||||
buttons = [
|
||||
[
|
||||
(
|
||||
# We create a new button instead of replacing callback_data in case the
|
||||
# same object is used elsewhere
|
||||
InlineKeyboardButton(
|
||||
btn.text,
|
||||
callback_data=self.__put_button(btn.callback_data, keyboard_data),
|
||||
)
|
||||
if btn.callback_data
|
||||
else btn
|
||||
)
|
||||
for btn in column
|
||||
]
|
||||
for column in reply_markup.inline_keyboard
|
||||
]
|
||||
|
||||
if not keyboard_data.button_data:
|
||||
# If we arrive here, no data had to be replaced and we can return the input
|
||||
return reply_markup
|
||||
|
||||
self._keyboard_data[keyboard_uuid] = keyboard_data
|
||||
return InlineKeyboardMarkup(buttons)
|
||||
|
||||
@staticmethod
|
||||
def __put_button(callback_data: object, keyboard_data: _KeyboardData) -> str:
|
||||
"""Stores the data for a single button in :attr:`keyboard_data`.
|
||||
Returns the string that should be passed instead of the callback_data, which is
|
||||
``keyboard_uuid + button_uuids``.
|
||||
"""
|
||||
uuid = uuid4().hex
|
||||
keyboard_data.button_data[uuid] = callback_data
|
||||
return f"{keyboard_data.keyboard_uuid}{uuid}"
|
||||
|
||||
def __get_keyboard_uuid_and_button_data(
|
||||
self, callback_data: str
|
||||
) -> Union[tuple[str, object], tuple[None, InvalidCallbackData]]:
|
||||
keyboard, button = self.extract_uuids(callback_data)
|
||||
try:
|
||||
# we get the values before calling update() in case KeyErrors are raised
|
||||
# we don't want to update in that case
|
||||
keyboard_data = self._keyboard_data[keyboard]
|
||||
button_data = keyboard_data.button_data[button]
|
||||
# Update the timestamp for the LRU
|
||||
keyboard_data.update_access_time()
|
||||
except KeyError:
|
||||
return None, InvalidCallbackData(callback_data)
|
||||
return keyboard, button_data
|
||||
|
||||
@staticmethod
|
||||
def extract_uuids(callback_data: str) -> tuple[str, str]:
|
||||
"""Extracts the keyboard uuid and the button uuid from the given :paramref:`callback_data`.
|
||||
|
||||
Args:
|
||||
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
|
||||
|
||||
"""
|
||||
# Extract the uuids as put in __put_button
|
||||
return callback_data[:32], callback_data[32:]
|
||||
|
||||
def process_message(self, message: Message) -> None:
|
||||
"""Replaces the data in the inline keyboard attached to the message with the cached
|
||||
objects, if necessary. If the data could not be found,
|
||||
:class:`telegram.ext.InvalidCallbackData` will be inserted.
|
||||
|
||||
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 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
|
||||
:obj:`None` for those! In the corresponding reply markups the callback data will be
|
||||
replaced by :class:`telegram.ext.InvalidCallbackData`.
|
||||
|
||||
Warning:
|
||||
* Does *not* consider :attr:`telegram.Message.reply_to_message` and
|
||||
: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.
|
||||
|
||||
"""
|
||||
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,
|
||||
which is relevant for process_callback_query.
|
||||
|
||||
**IN PLACE**
|
||||
"""
|
||||
if not message.reply_markup:
|
||||
return None
|
||||
|
||||
if message.via_bot:
|
||||
sender: Optional[User] = message.via_bot
|
||||
elif message.from_user:
|
||||
sender = message.from_user
|
||||
else:
|
||||
sender = None
|
||||
|
||||
if sender is not None and sender != self.bot.bot:
|
||||
return None
|
||||
|
||||
keyboard_uuid = None
|
||||
|
||||
for row in message.reply_markup.inline_keyboard:
|
||||
for button in row:
|
||||
if button.callback_data:
|
||||
button_data = cast("str", button.callback_data)
|
||||
keyboard_id, callback_data = self.__get_keyboard_uuid_and_button_data(
|
||||
button_data
|
||||
)
|
||||
# update_callback_data makes sure that the _id_attrs are updated
|
||||
button.update_callback_data(callback_data)
|
||||
|
||||
# This is lazy loaded. The firsts time we find a button
|
||||
# we load the associated keyboard - afterwards, there is
|
||||
if not keyboard_uuid and not isinstance(callback_data, InvalidCallbackData):
|
||||
keyboard_uuid = keyboard_id
|
||||
|
||||
return keyboard_uuid
|
||||
|
||||
def process_callback_query(self, callback_query: CallbackQuery) -> None:
|
||||
"""Replaces the data in the callback query and the attached messages keyboard with the
|
||||
cached objects, if necessary. If the data could not be found,
|
||||
:class:`telegram.ext.InvalidCallbackData` will be inserted.
|
||||
If :attr:`telegram.CallbackQuery.data` or :attr:`telegram.CallbackQuery.message` is
|
||||
present, this also saves the callback queries ID in order to be able to resolve it to the
|
||||
stored data.
|
||||
|
||||
Note:
|
||||
Also considers inserts data into the buttons of
|
||||
:attr:`telegram.Message.reply_to_message` and :attr:`telegram.Message.pinned_message`
|
||||
if necessary.
|
||||
|
||||
Warning:
|
||||
*In place*, i.e. the passed :class:`telegram.CallbackQuery` will be changed!
|
||||
|
||||
Args:
|
||||
callback_query (:class:`telegram.CallbackQuery`): The callback query.
|
||||
|
||||
"""
|
||||
mapped = False
|
||||
|
||||
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)
|
||||
with callback_query._unfrozen():
|
||||
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
|
||||
|
||||
# Get the cached callback data for the inline keyboard attached to the
|
||||
# CallbackQuery.
|
||||
if isinstance(callback_query.message, Message):
|
||||
self.__process_message(callback_query.message)
|
||||
for maybe_message in (
|
||||
callback_query.message.pinned_message,
|
||||
callback_query.message.reply_to_message,
|
||||
):
|
||||
if isinstance(maybe_message, Message):
|
||||
self.__process_message(maybe_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 :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: If the callback query can not be found in the cache
|
||||
"""
|
||||
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:
|
||||
with contextlib.suppress(KeyError):
|
||||
self._keyboard_data.pop(keyboard_uuid)
|
||||
|
||||
def clear_callback_data(
|
||||
self, time_cutoff: Optional[Union[float, dtm.datetime]] = None
|
||||
) -> None:
|
||||
"""Clears the stored callback data.
|
||||
|
||||
Args:
|
||||
time_cutoff (:obj:`float` | :obj:`datetime.datetime`, optional): Pass a UNIX timestamp
|
||||
or a :obj:`datetime.datetime` to clear only entries which are older.
|
||||
|tz-naive-dtms|
|
||||
|
||||
"""
|
||||
self.__clear(self._keyboard_data, time_cutoff=time_cutoff)
|
||||
|
||||
def clear_callback_queries(self) -> None:
|
||||
"""Clears the stored callback query IDs."""
|
||||
self.__clear(self._callback_queries)
|
||||
|
||||
def __clear(
|
||||
self, mapping: MutableMapping, time_cutoff: Optional[Union[float, dtm.datetime]] = None
|
||||
) -> None:
|
||||
if not time_cutoff:
|
||||
mapping.clear()
|
||||
return
|
||||
|
||||
if isinstance(time_cutoff, dtm.datetime):
|
||||
effective_cutoff = to_float_timestamp(
|
||||
time_cutoff, tzinfo=self.bot.defaults.tzinfo if self.bot.defaults else None
|
||||
)
|
||||
else:
|
||||
effective_cutoff = time_cutoff
|
||||
|
||||
# We need a list instead of a generator here, as the list doesn't change it's size
|
||||
# during the iteration
|
||||
to_drop = [key for key, data in mapping.items() if data.access_time < effective_cutoff]
|
||||
for key in to_drop:
|
||||
mapping.pop(key)
|
||||
226
.venv/lib/python3.12/site-packages/telegram/ext/_contexttypes.py
Normal file
226
.venv/lib/python3.12/site-packages/telegram/ext/_contexttypes.py
Normal file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 auxiliary class ContextTypes."""
|
||||
from typing import Any, Generic, overload
|
||||
|
||||
from telegram.ext._callbackcontext import CallbackContext
|
||||
from telegram.ext._extbot import ExtBot
|
||||
from telegram.ext._utils.types import BD, CCT, CD, UD
|
||||
|
||||
ADict = dict[Any, Any]
|
||||
|
||||
|
||||
class ContextTypes(Generic[CCT, UD, CD, BD]):
|
||||
"""
|
||||
Convenience class to gather customizable types of the :class:`telegram.ext.CallbackContext`
|
||||
interface.
|
||||
|
||||
Examples:
|
||||
:any:`ContextTypes Bot <examples.contexttypesbot>`
|
||||
|
||||
.. seealso:: :wiki:`Architecture Overview <Architecture>`,
|
||||
:wiki:`Storing Bot, User and Chat Related Data <Storing-bot%2C-user-and-chat-related-data>`
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
Args:
|
||||
context (:obj:`type`, optional): Determines the type of the ``context`` argument of all
|
||||
(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
|
||||
: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.
|
||||
|
||||
"""
|
||||
|
||||
DEFAULT_TYPE = CallbackContext[ExtBot[None], ADict, ADict, ADict]
|
||||
"""Shortcut for the type annotation for the ``context`` argument that's correct for the
|
||||
default settings, i.e. if :class:`telegram.ext.ContextTypes` is not used.
|
||||
|
||||
Example:
|
||||
.. code:: python
|
||||
|
||||
async def callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
...
|
||||
|
||||
.. versionadded: 20.0
|
||||
"""
|
||||
|
||||
__slots__ = ("_bot_data", "_chat_data", "_context", "_user_data")
|
||||
|
||||
# overload signatures generated with
|
||||
# https://gist.github.com/Bibo-Joshi/399382cda537fb01bd86b13c3d03a956
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "ContextTypes[CallbackContext[ExtBot[Any], ADict, ADict, ADict], ADict, ADict, ADict]", # pylint: disable=line-too-long # noqa: E501
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def __init__(self: "ContextTypes[CCT, ADict, ADict, ADict]", context: type[CCT]): ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "ContextTypes[CallbackContext[ExtBot[Any], UD, ADict, ADict], UD, ADict, ADict]",
|
||||
user_data: type[UD],
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "ContextTypes[CallbackContext[ExtBot[Any], ADict, CD, ADict], ADict, CD, ADict]",
|
||||
chat_data: type[CD],
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "ContextTypes[CallbackContext[ExtBot[Any], ADict, ADict, BD], ADict, ADict, BD]",
|
||||
bot_data: type[BD],
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "ContextTypes[CCT, UD, ADict, ADict]", context: type[CCT], user_data: type[UD]
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "ContextTypes[CCT, ADict, CD, ADict]", context: type[CCT], chat_data: type[CD]
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "ContextTypes[CCT, ADict, ADict, BD]", context: type[CCT], bot_data: type[BD]
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "ContextTypes[CallbackContext[ExtBot[Any], UD, CD, ADict], UD, CD, ADict]",
|
||||
user_data: type[UD],
|
||||
chat_data: type[CD],
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "ContextTypes[CallbackContext[ExtBot[Any], UD, ADict, BD], UD, ADict, BD]",
|
||||
user_data: type[UD],
|
||||
bot_data: type[BD],
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "ContextTypes[CallbackContext[ExtBot[Any], ADict, CD, BD], ADict, CD, BD]",
|
||||
chat_data: type[CD],
|
||||
bot_data: type[BD],
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "ContextTypes[CCT, UD, CD, ADict]",
|
||||
context: type[CCT],
|
||||
user_data: type[UD],
|
||||
chat_data: type[CD],
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "ContextTypes[CCT, UD, ADict, BD]",
|
||||
context: type[CCT],
|
||||
user_data: type[UD],
|
||||
bot_data: type[BD],
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "ContextTypes[CCT, ADict, CD, BD]",
|
||||
context: type[CCT],
|
||||
chat_data: type[CD],
|
||||
bot_data: type[BD],
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "ContextTypes[CallbackContext[ExtBot[Any], UD, CD, BD], UD, CD, BD]",
|
||||
user_data: type[UD],
|
||||
chat_data: type[CD],
|
||||
bot_data: type[BD],
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "ContextTypes[CCT, UD, CD, BD]",
|
||||
context: type[CCT],
|
||||
user_data: type[UD],
|
||||
chat_data: type[CD],
|
||||
bot_data: type[BD],
|
||||
): ...
|
||||
|
||||
def __init__( # type: ignore[misc]
|
||||
self,
|
||||
context: "type[CallbackContext[ExtBot[Any], ADict, ADict, ADict]]" = CallbackContext,
|
||||
bot_data: type[ADict] = dict,
|
||||
chat_data: type[ADict] = dict,
|
||||
user_data: type[ADict] = dict,
|
||||
):
|
||||
if not issubclass(context, CallbackContext):
|
||||
raise TypeError("context must be a subclass of CallbackContext.")
|
||||
|
||||
# We make all those only accessible via properties because we don't currently support
|
||||
# changing this at runtime, so overriding the attributes doesn't make sense
|
||||
self._context = context
|
||||
self._bot_data = bot_data
|
||||
self._chat_data = chat_data
|
||||
self._user_data = user_data
|
||||
|
||||
@property
|
||||
def context(self) -> type[CCT]:
|
||||
"""The type of the ``context`` argument of all (error-)handler callbacks and job
|
||||
callbacks.
|
||||
"""
|
||||
return self._context # type: ignore[return-value]
|
||||
|
||||
@property
|
||||
def bot_data(self) -> type[BD]:
|
||||
"""The type of :attr:`context.bot_data <CallbackContext.bot_data>` of all (error-)handler
|
||||
callbacks and job callbacks.
|
||||
"""
|
||||
return self._bot_data # type: ignore[return-value]
|
||||
|
||||
@property
|
||||
def chat_data(self) -> type[CD]:
|
||||
"""The type of :attr:`context.chat_data <CallbackContext.chat_data>` of all (error-)handler
|
||||
callbacks and job callbacks.
|
||||
"""
|
||||
return self._chat_data # type: ignore[return-value]
|
||||
|
||||
@property
|
||||
def user_data(self) -> type[UD]:
|
||||
"""The type of :attr:`context.user_data <CallbackContext.user_data>` of all (error-)handler
|
||||
callbacks and job callbacks.
|
||||
"""
|
||||
return self._user_data # type: ignore[return-value]
|
||||
361
.venv/lib/python3.12/site-packages/telegram/ext/_defaults.py
Normal file
361
.venv/lib/python3.12/site-packages/telegram/ext/_defaults.py
Normal file
@@ -0,0 +1,361 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 class Defaults, which allows passing default values to Application."""
|
||||
import datetime as dtm
|
||||
from typing import TYPE_CHECKING, Any, NoReturn, Optional, final
|
||||
|
||||
from telegram._utils.datetime import UTC
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import LinkPreviewOptions
|
||||
|
||||
|
||||
@final
|
||||
class Defaults:
|
||||
"""Convenience Class to gather all parameters with a (user defined) default value
|
||||
|
||||
.. seealso:: :wiki:`Architecture Overview <Architecture>`,
|
||||
:wiki:`Adding Defaults to Your Bot <Adding-defaults-to-your-bot>`
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Removed the argument and attribute ``timeout``. Specify default timeout behavior for the
|
||||
networking backend directly via :class:`telegram.ext.ApplicationBuilder` instead.
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
Removed deprecated arguments and properties ``disable_web_page_preview`` and ``quote``.
|
||||
Use :paramref:`link_preview_options` and :paramref:`do_quote` instead.
|
||||
|
||||
Parameters:
|
||||
parse_mode (:obj:`str`, optional): |parse_mode|
|
||||
disable_notification (:obj:`bool`, optional): |disable_notification|
|
||||
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|.
|
||||
Will be used for :attr:`telegram.ReplyParameters.allow_sending_without_reply`.
|
||||
tzinfo (:class:`datetime.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 :paramref:`tzinfo`. Defaults to
|
||||
:attr:`datetime.timezone.utc` otherwise.
|
||||
|
||||
.. deprecated:: 21.10
|
||||
Support for ``pytz`` timezones is deprecated and will be removed in future
|
||||
versions.
|
||||
|
||||
block (:obj:`bool`, optional): Default setting for the :paramref:`BaseHandler.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): |protect_content|
|
||||
|
||||
.. versionadded:: 20.0
|
||||
link_preview_options (:class:`telegram.LinkPreviewOptions`, optional):
|
||||
Link preview generation options for all outgoing messages. Mutually exclusive with
|
||||
:paramref:`disable_web_page_preview`.
|
||||
This object is used for the corresponding parameter of
|
||||
:meth:`telegram.Bot.send_message`, :meth:`telegram.Bot.edit_message_text`,
|
||||
and :class:`telegram.InputTextMessageContent` if not specified. If a value is specified
|
||||
for the corresponding parameter, only those parameters of
|
||||
:class:`telegram.LinkPreviewOptions` will be overridden that are not
|
||||
explicitly set.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from telegram import LinkPreviewOptions
|
||||
from telegram.ext import Defaults, ExtBot
|
||||
|
||||
defaults = Defaults(
|
||||
link_preview_options=LinkPreviewOptions(show_above_text=True)
|
||||
)
|
||||
chat_id = 123
|
||||
|
||||
async def main():
|
||||
async with ExtBot("Token", defaults=defaults) as bot:
|
||||
# The link preview will be shown above the text.
|
||||
await bot.send_message(chat_id, "https://python-telegram-bot.org")
|
||||
|
||||
# The link preview will be shown below the text.
|
||||
await bot.send_message(
|
||||
chat_id,
|
||||
"https://python-telegram-bot.org",
|
||||
link_preview_options=LinkPreviewOptions(show_above_text=False)
|
||||
)
|
||||
|
||||
# The link preview will be shown above the text, but the preview will
|
||||
# show Telegram.
|
||||
await bot.send_message(
|
||||
chat_id,
|
||||
"https://python-telegram-bot.org",
|
||||
link_preview_options=LinkPreviewOptions(url="https://telegram.org")
|
||||
)
|
||||
|
||||
.. versionadded:: 20.8
|
||||
do_quote(:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. versionadded:: 20.8
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_allow_sending_without_reply",
|
||||
"_api_defaults",
|
||||
"_block",
|
||||
"_disable_notification",
|
||||
"_do_quote",
|
||||
"_link_preview_options",
|
||||
"_parse_mode",
|
||||
"_protect_content",
|
||||
"_tzinfo",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parse_mode: Optional[str] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
tzinfo: dtm.tzinfo = UTC,
|
||||
block: bool = True,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
link_preview_options: Optional["LinkPreviewOptions"] = None,
|
||||
do_quote: Optional[bool] = None,
|
||||
):
|
||||
self._parse_mode: Optional[str] = parse_mode
|
||||
self._disable_notification: Optional[bool] = disable_notification
|
||||
self._allow_sending_without_reply: Optional[bool] = allow_sending_without_reply
|
||||
self._tzinfo: dtm.tzinfo = tzinfo
|
||||
self._block: bool = block
|
||||
self._protect_content: Optional[bool] = protect_content
|
||||
|
||||
if "pytz" in str(self._tzinfo.__class__):
|
||||
# TODO: When dropping support, make sure to update _utils.datetime accordingly
|
||||
warn(
|
||||
message=PTBDeprecationWarning(
|
||||
version="21.10",
|
||||
message=(
|
||||
"Support for pytz timezones is deprecated and will be removed in "
|
||||
"future versions."
|
||||
),
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
self._link_preview_options = link_preview_options
|
||||
self._do_quote = do_quote
|
||||
|
||||
# Gather all defaults that actually have a default value
|
||||
self._api_defaults = {}
|
||||
for kwarg in (
|
||||
"allow_sending_without_reply",
|
||||
"disable_notification",
|
||||
"do_quote",
|
||||
"explanation_parse_mode",
|
||||
"link_preview_options",
|
||||
"parse_mode",
|
||||
"text_parse_mode",
|
||||
"protect_content",
|
||||
"question_parse_mode",
|
||||
):
|
||||
value = getattr(self, kwarg)
|
||||
if value is not None:
|
||||
self._api_defaults[kwarg] = value
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Builds a hash value for this object such that the hash of two objects is equal if and
|
||||
only if the objects are equal in terms of :meth:`__eq__`.
|
||||
|
||||
Returns:
|
||||
:obj:`int` The hash value of the object.
|
||||
"""
|
||||
return hash(
|
||||
(
|
||||
self._parse_mode,
|
||||
self._disable_notification,
|
||||
self._link_preview_options,
|
||||
self._allow_sending_without_reply,
|
||||
self._do_quote,
|
||||
self._tzinfo,
|
||||
self._block,
|
||||
self._protect_content,
|
||||
)
|
||||
)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""Defines equality condition for the :class:`Defaults` object.
|
||||
Two objects of this class are considered to be equal if all their parameters
|
||||
are identical.
|
||||
|
||||
Returns:
|
||||
:obj:`True` if both objects have all parameters identical. :obj:`False` otherwise.
|
||||
"""
|
||||
if isinstance(other, Defaults):
|
||||
return all(getattr(self, attr) == getattr(other, attr) for attr in self.__slots__)
|
||||
return False
|
||||
|
||||
@property
|
||||
def api_defaults(self) -> dict[str, Any]: # skip-cq: PY-D0003
|
||||
return self._api_defaults
|
||||
|
||||
@property
|
||||
def parse_mode(self) -> Optional[str]:
|
||||
""":obj:`str`: Optional. Send Markdown or HTML, if you want Telegram apps to show
|
||||
bold, italic, fixed-width text or URLs in your bot's message.
|
||||
"""
|
||||
return self._parse_mode
|
||||
|
||||
@parse_mode.setter
|
||||
def parse_mode(self, _: object) -> NoReturn:
|
||||
raise AttributeError("You can not assign a new value to parse_mode after initialization.")
|
||||
|
||||
@property
|
||||
def explanation_parse_mode(self) -> Optional[str]:
|
||||
""":obj:`str`: Optional. Alias for :attr:`parse_mode`, used for
|
||||
the corresponding parameter of :meth:`telegram.Bot.send_poll`.
|
||||
"""
|
||||
return self._parse_mode
|
||||
|
||||
@explanation_parse_mode.setter
|
||||
def explanation_parse_mode(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
"You can not assign a new value to explanation_parse_mode after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def quote_parse_mode(self) -> Optional[str]:
|
||||
""":obj:`str`: Optional. Alias for :attr:`parse_mode`, used for
|
||||
the corresponding parameter of :meth:`telegram.ReplyParameters`.
|
||||
"""
|
||||
return self._parse_mode
|
||||
|
||||
@quote_parse_mode.setter
|
||||
def quote_parse_mode(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
"You can not assign a new value to quote_parse_mode after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def text_parse_mode(self) -> Optional[str]:
|
||||
""":obj:`str`: Optional. Alias for :attr:`parse_mode`, used for
|
||||
the corresponding parameter of :class:`telegram.InputPollOption` and
|
||||
:meth:`telegram.Bot.send_gift`.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
"""
|
||||
return self._parse_mode
|
||||
|
||||
@text_parse_mode.setter
|
||||
def text_parse_mode(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
"You can not assign a new value to text_parse_mode after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def question_parse_mode(self) -> Optional[str]:
|
||||
""":obj:`str`: Optional. Alias for :attr:`parse_mode`, used for
|
||||
the corresponding parameter of :meth:`telegram.Bot.send_poll`.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
"""
|
||||
return self._parse_mode
|
||||
|
||||
@question_parse_mode.setter
|
||||
def question_parse_mode(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
"You can not assign a new value to question_parse_mode after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def disable_notification(self) -> Optional[bool]:
|
||||
""":obj:`bool`: Optional. Sends the message silently. Users will
|
||||
receive a notification with no sound.
|
||||
"""
|
||||
return self._disable_notification
|
||||
|
||||
@disable_notification.setter
|
||||
def disable_notification(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
"You can not assign a new value to disable_notification after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def allow_sending_without_reply(self) -> Optional[bool]:
|
||||
""":obj:`bool`: Optional. Pass :obj:`True`, if the message
|
||||
should be sent even if the specified replied-to message is not found.
|
||||
"""
|
||||
return self._allow_sending_without_reply
|
||||
|
||||
@allow_sending_without_reply.setter
|
||||
def allow_sending_without_reply(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
"You can not assign a new value to allow_sending_without_reply after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def tzinfo(self) -> dtm.tzinfo:
|
||||
""":obj:`tzinfo`: A timezone to be used for all date(time) objects appearing
|
||||
throughout PTB.
|
||||
"""
|
||||
return self._tzinfo
|
||||
|
||||
@tzinfo.setter
|
||||
def tzinfo(self, _: object) -> NoReturn:
|
||||
raise AttributeError("You can not assign a new value to tzinfo after initialization.")
|
||||
|
||||
@property
|
||||
def block(self) -> bool:
|
||||
""":obj:`bool`: Optional. Default setting for the :paramref:`BaseHandler.block` parameter
|
||||
of handlers and error handlers registered through :meth:`Application.add_handler` and
|
||||
:meth:`Application.add_error_handler`.
|
||||
"""
|
||||
return self._block
|
||||
|
||||
@block.setter
|
||||
def block(self, _: object) -> NoReturn:
|
||||
raise AttributeError("You can not assign a new value to block after initialization.")
|
||||
|
||||
@property
|
||||
def protect_content(self) -> Optional[bool]:
|
||||
""":obj:`bool`: Optional. Protects the contents of the sent message from forwarding and
|
||||
saving.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
return self._protect_content
|
||||
|
||||
@protect_content.setter
|
||||
def protect_content(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
"You can't assign a new value to protect_content after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def link_preview_options(self) -> Optional["LinkPreviewOptions"]:
|
||||
""":class:`telegram.LinkPreviewOptions`: Optional. Link preview generation options for all
|
||||
outgoing messages.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
"""
|
||||
return self._link_preview_options
|
||||
|
||||
@property
|
||||
def do_quote(self) -> Optional[bool]:
|
||||
""":obj:`bool`: Optional. |reply_quote|
|
||||
|
||||
.. versionadded:: 20.8
|
||||
"""
|
||||
return self._do_quote
|
||||
@@ -0,0 +1,481 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the DictPersistence class."""
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from typing import TYPE_CHECKING, Any, Optional, cast
|
||||
|
||||
from telegram.ext import BasePersistence, PersistenceInput
|
||||
from telegram.ext._utils.types import CDCData, ConversationDict, ConversationKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
|
||||
class DictPersistence(BasePersistence[dict[Any, Any], dict[Any, Any], dict[Any, Any]]):
|
||||
"""Using Python's :obj:`dict` and :mod:`json` for making your bot persistent.
|
||||
|
||||
Attention:
|
||||
The interface provided by this class is intended to be accessed exclusively by
|
||||
: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
|
||||
shuts down. This is, because :class:`DictPersistence` is mainly intended as starting
|
||||
point for custom persistence classes that need to JSON-serialize the stored data before
|
||||
writing them to file/database.
|
||||
|
||||
* This implementation of :class:`BasePersistence` does not handle data that cannot be
|
||||
serialized by :func:`json.dumps`.
|
||||
|
||||
.. seealso:: :wiki:`Making Your Bot Persistent <Making-your-bot-persistent>`
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
The parameters and attributes ``store_*_data`` were replaced by :attr:`store_data`.
|
||||
|
||||
Args:
|
||||
store_data (:class:`~telegram.ext.PersistenceInput`, optional): Specifies which kinds of
|
||||
data will be saved by this persistence instance. By default, all available kinds of
|
||||
data will be saved.
|
||||
user_data_json (:obj:`str`, optional): JSON string that will be used to reconstruct
|
||||
user_data on creating this persistence. Default is ``""``.
|
||||
chat_data_json (:obj:`str`, optional): JSON string that will be used to reconstruct
|
||||
chat_data on creating this persistence. Default is ``""``.
|
||||
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
|
||||
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:: 20.0
|
||||
Attributes:
|
||||
store_data (:class:`~telegram.ext.PersistenceInput`): Specifies which kinds of data will
|
||||
be saved by this persistence instance.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_bot_data",
|
||||
"_bot_data_json",
|
||||
"_callback_data",
|
||||
"_callback_data_json",
|
||||
"_chat_data",
|
||||
"_chat_data_json",
|
||||
"_conversations",
|
||||
"_conversations_json",
|
||||
"_user_data",
|
||||
"_user_data_json",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
store_data: Optional[PersistenceInput] = None,
|
||||
user_data_json: str = "",
|
||||
chat_data_json: str = "",
|
||||
bot_data_json: str = "",
|
||||
conversations_json: str = "",
|
||||
callback_data_json: str = "",
|
||||
update_interval: float = 60,
|
||||
):
|
||||
super().__init__(store_data=store_data, update_interval=update_interval)
|
||||
self._user_data = None
|
||||
self._chat_data = None
|
||||
self._bot_data = None
|
||||
self._callback_data = None
|
||||
self._conversations = None
|
||||
self._user_data_json: Optional[str] = None
|
||||
self._chat_data_json: Optional[str] = None
|
||||
self._bot_data_json: Optional[str] = None
|
||||
self._callback_data_json: Optional[str] = None
|
||||
self._conversations_json: Optional[str] = None
|
||||
if user_data_json:
|
||||
try:
|
||||
self._user_data = self._decode_user_chat_data_from_json(user_data_json)
|
||||
self._user_data_json = user_data_json
|
||||
except (ValueError, AttributeError) as exc:
|
||||
raise TypeError("Unable to deserialize user_data_json. Not valid JSON") from exc
|
||||
if chat_data_json:
|
||||
try:
|
||||
self._chat_data = self._decode_user_chat_data_from_json(chat_data_json)
|
||||
self._chat_data_json = chat_data_json
|
||||
except (ValueError, AttributeError) as exc:
|
||||
raise TypeError("Unable to deserialize chat_data_json. Not valid JSON") from exc
|
||||
if bot_data_json:
|
||||
try:
|
||||
self._bot_data = json.loads(bot_data_json)
|
||||
self._bot_data_json = bot_data_json
|
||||
except (ValueError, AttributeError) as exc:
|
||||
raise TypeError("Unable to deserialize bot_data_json. Not valid JSON") from exc
|
||||
if not isinstance(self._bot_data, dict):
|
||||
raise TypeError("bot_data_json must be serialized dict")
|
||||
if callback_data_json:
|
||||
try:
|
||||
data = json.loads(callback_data_json)
|
||||
except (ValueError, AttributeError) as exc:
|
||||
raise TypeError(
|
||||
"Unable to deserialize callback_data_json. Not valid JSON"
|
||||
) from exc
|
||||
# We are a bit more thorough with the checking of the format here, because it's
|
||||
# more complicated than for the other things
|
||||
try:
|
||||
if data is None:
|
||||
self._callback_data = None
|
||||
else:
|
||||
self._callback_data = cast(
|
||||
"CDCData",
|
||||
([(one, float(two), three) for one, two, three in data[0]], data[1]),
|
||||
)
|
||||
self._callback_data_json = callback_data_json
|
||||
except (ValueError, IndexError) as exc:
|
||||
raise TypeError("callback_data_json is not in the required format") from exc
|
||||
if self._callback_data is not None and (
|
||||
not all(
|
||||
isinstance(entry[2], dict) and isinstance(entry[0], str)
|
||||
for entry in self._callback_data[0]
|
||||
)
|
||||
or not isinstance(self._callback_data[1], dict)
|
||||
):
|
||||
raise TypeError("callback_data_json is not in the required format")
|
||||
|
||||
if conversations_json:
|
||||
try:
|
||||
self._conversations = self._decode_conversations_from_json(conversations_json)
|
||||
self._conversations_json = conversations_json
|
||||
except (ValueError, AttributeError) as exc:
|
||||
raise TypeError(
|
||||
"Unable to deserialize conversations_json. Not valid JSON"
|
||||
) from exc
|
||||
|
||||
@property
|
||||
def user_data(self) -> Optional[dict[int, dict[Any, Any]]]:
|
||||
""":obj:`dict`: The user_data as a dict."""
|
||||
return self._user_data
|
||||
|
||||
@property
|
||||
def user_data_json(self) -> str:
|
||||
""":obj:`str`: The user_data serialized as a JSON-string."""
|
||||
if self._user_data_json:
|
||||
return self._user_data_json
|
||||
return json.dumps(self.user_data)
|
||||
|
||||
@property
|
||||
def chat_data(self) -> Optional[dict[int, dict[Any, Any]]]:
|
||||
""":obj:`dict`: The chat_data as a dict."""
|
||||
return self._chat_data
|
||||
|
||||
@property
|
||||
def chat_data_json(self) -> str:
|
||||
""":obj:`str`: The chat_data serialized as a JSON-string."""
|
||||
if self._chat_data_json:
|
||||
return self._chat_data_json
|
||||
return json.dumps(self.chat_data)
|
||||
|
||||
@property
|
||||
def bot_data(self) -> Optional[dict[Any, Any]]:
|
||||
""":obj:`dict`: The bot_data as a dict."""
|
||||
return self._bot_data
|
||||
|
||||
@property
|
||||
def bot_data_json(self) -> str:
|
||||
""":obj:`str`: The bot_data serialized as a JSON-string."""
|
||||
if self._bot_data_json:
|
||||
return self._bot_data_json
|
||||
return json.dumps(self.bot_data)
|
||||
|
||||
@property
|
||||
def callback_data(self) -> Optional[CDCData]:
|
||||
"""tuple[list[tuple[:obj:`str`, :obj:`float`, dict[:obj:`str`, :class:`object`]]], \
|
||||
dict[:obj:`str`, :obj:`str`]]: The metadata on the stored callback data.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
"""
|
||||
return self._callback_data
|
||||
|
||||
@property
|
||||
def callback_data_json(self) -> str:
|
||||
""":obj:`str`: The metadata on the stored callback data as a JSON-string.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
"""
|
||||
if self._callback_data_json:
|
||||
return self._callback_data_json
|
||||
return json.dumps(self.callback_data)
|
||||
|
||||
@property
|
||||
def conversations(self) -> Optional[dict[str, ConversationDict]]:
|
||||
""":obj:`dict`: The conversations as a dict."""
|
||||
return self._conversations
|
||||
|
||||
@property
|
||||
def conversations_json(self) -> str:
|
||||
""":obj:`str`: The conversations serialized as a JSON-string."""
|
||||
if self._conversations_json:
|
||||
return self._conversations_json
|
||||
if self.conversations:
|
||||
return self._encode_conversations_to_json(self.conversations)
|
||||
return json.dumps(self.conversations)
|
||||
|
||||
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:
|
||||
:obj:`dict`: The restored user data.
|
||||
"""
|
||||
if self.user_data is None:
|
||||
self._user_data = {}
|
||||
return deepcopy(self.user_data) # type: ignore[arg-type]
|
||||
|
||||
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:
|
||||
:obj:`dict`: The restored chat data.
|
||||
"""
|
||||
if self.chat_data is None:
|
||||
self._chat_data = {}
|
||||
return deepcopy(self.chat_data) # type: ignore[arg-type]
|
||||
|
||||
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:
|
||||
:obj:`dict`: The restored bot data.
|
||||
"""
|
||||
if self.bot_data is None:
|
||||
self._bot_data = {}
|
||||
return deepcopy(self.bot_data) # type: ignore[arg-type]
|
||||
|
||||
async def get_callback_data(self) -> Optional[CDCData]:
|
||||
"""Returns the callback_data created from the ``callback_data_json`` or :obj:`None`.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
Returns:
|
||||
tuple[list[tuple[:obj:`str`, :obj:`float`, dict[:obj:`str`, :class:`object`]]], \
|
||||
dict[:obj:`str`, :obj:`str`]]: The restored metadata or :obj:`None`, \
|
||||
if no data was stored.
|
||||
"""
|
||||
if self.callback_data is None:
|
||||
self._callback_data = None
|
||||
return None
|
||||
return deepcopy(self.callback_data)
|
||||
|
||||
async def get_conversations(self, name: str) -> ConversationDict:
|
||||
"""Returns the conversations created from the ``conversations_json`` or an empty
|
||||
:obj:`dict`.
|
||||
|
||||
Returns:
|
||||
:obj:`dict`: The restored conversations data.
|
||||
"""
|
||||
if self.conversations is None:
|
||||
self._conversations = {}
|
||||
return self.conversations.get(name, {}).copy() # type: ignore[union-attr]
|
||||
|
||||
async def update_conversation(
|
||||
self, name: str, key: ConversationKey, new_state: Optional[object]
|
||||
) -> None:
|
||||
"""Will update the conversations for the given handler.
|
||||
|
||||
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.
|
||||
"""
|
||||
if not self._conversations:
|
||||
self._conversations = {}
|
||||
if self._conversations.setdefault(name, {}).get(key) == new_state:
|
||||
return
|
||||
self._conversations[name][key] = new_state
|
||||
self._conversations_json = None
|
||||
|
||||
async def update_user_data(self, user_id: int, data: dict[Any, Any]) -> 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.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] = data
|
||||
self._user_data_json = None
|
||||
|
||||
async def update_chat_data(self, chat_id: int, data: dict[Any, Any]) -> 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.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] = data
|
||||
self._chat_data_json = None
|
||||
|
||||
async def update_bot_data(self, data: dict[Any, Any]) -> None:
|
||||
"""Will update the bot_data (if changed).
|
||||
|
||||
Args:
|
||||
data (:obj:`dict`): The :attr:`telegram.ext.Application.bot_data`.
|
||||
"""
|
||||
if self._bot_data == data:
|
||||
return
|
||||
self._bot_data = data
|
||||
self._bot_data_json = None
|
||||
|
||||
async def update_callback_data(self, data: CDCData) -> None:
|
||||
"""Will update the callback_data (if changed).
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
Args:
|
||||
data (tuple[list[tuple[:obj:`str`, :obj:`float`, dict[:obj:`str`, :class:`object`]]], \
|
||||
dict[:obj:`str`, :obj:`str`]]): The relevant data to restore
|
||||
:class:`telegram.ext.CallbackDataCache`.
|
||||
"""
|
||||
if self._callback_data == data:
|
||||
return
|
||||
self._callback_data = data
|
||||
self._callback_data_json = None
|
||||
|
||||
async def drop_chat_data(self, chat_id: int) -> None:
|
||||
"""Will delete the specified key from the :attr:`chat_data`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int`): The chat id to delete from the persistence.
|
||||
"""
|
||||
if self._chat_data is None:
|
||||
return
|
||||
self._chat_data.pop(chat_id, None)
|
||||
self._chat_data_json = None
|
||||
|
||||
async def drop_user_data(self, user_id: int) -> None:
|
||||
"""Will delete the specified key from the :attr:`user_data`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Args:
|
||||
user_id (:obj:`int`): The user id to delete from the persistence.
|
||||
"""
|
||||
if self._user_data is None:
|
||||
return
|
||||
self._user_data.pop(user_id, None)
|
||||
self._user_data_json = None
|
||||
|
||||
async def refresh_user_data(self, user_id: int, user_data: dict[Any, Any]) -> None:
|
||||
"""Does nothing.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
.. seealso:: :meth:`telegram.ext.BasePersistence.refresh_user_data`
|
||||
"""
|
||||
|
||||
async def refresh_chat_data(self, chat_id: int, chat_data: dict[Any, Any]) -> None:
|
||||
"""Does nothing.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
.. seealso:: :meth:`telegram.ext.BasePersistence.refresh_chat_data`
|
||||
"""
|
||||
|
||||
async def refresh_bot_data(self, bot_data: dict[Any, Any]) -> None:
|
||||
"""Does nothing.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
.. seealso:: :meth:`telegram.ext.BasePersistence.refresh_bot_data`
|
||||
"""
|
||||
|
||||
async def flush(self) -> None:
|
||||
"""Does nothing.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
.. seealso:: :meth:`telegram.ext.BasePersistence.flush`
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
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.
|
||||
|
||||
Args:
|
||||
conversations (:obj:`dict`): The conversations dict to transform to JSON.
|
||||
|
||||
Returns:
|
||||
:obj:`str`: The JSON-serialized conversations dict
|
||||
"""
|
||||
tmp: dict[str, JSONDict] = {}
|
||||
for handler, states in conversations.items():
|
||||
tmp[handler] = {}
|
||||
for key, state in states.items():
|
||||
tmp[handler][json.dumps(key)] = state
|
||||
return json.dumps(tmp)
|
||||
|
||||
@staticmethod
|
||||
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`.
|
||||
|
||||
Args:
|
||||
json_string (:obj:`str`): The conversations dict as JSON string.
|
||||
|
||||
Returns:
|
||||
:obj:`dict`: The conversations dict after decoding
|
||||
"""
|
||||
tmp = json.loads(json_string)
|
||||
conversations: dict[str, ConversationDict] = {}
|
||||
for handler, states in tmp.items():
|
||||
conversations[handler] = {}
|
||||
for key, state in states.items():
|
||||
conversations[handler][tuple(json.loads(key))] = state
|
||||
return conversations
|
||||
|
||||
@staticmethod
|
||||
def _decode_user_chat_data_from_json(data: str) -> dict[int, dict[object, object]]:
|
||||
"""Helper method to decode chat or user data (that uses ints as keys) from a
|
||||
JSON-string.
|
||||
|
||||
Args:
|
||||
data (:obj:`str`): The user/chat_data dict as JSON string.
|
||||
|
||||
Returns:
|
||||
:obj:`dict`: The user/chat_data defaultdict after decoding
|
||||
"""
|
||||
tmp: dict[int, dict[object, object]] = {}
|
||||
decoded_data = json.loads(data)
|
||||
for user, user_data in decoded_data.items():
|
||||
int_user_id = int(user)
|
||||
tmp[int_user_id] = {}
|
||||
for key, value in user_data.items():
|
||||
try:
|
||||
_id = int(key)
|
||||
except ValueError:
|
||||
_id = key
|
||||
tmp[int_user_id][_id] = value
|
||||
return tmp
|
||||
5300
.venv/lib/python3.12/site-packages/telegram/ext/_extbot.py
Normal file
5300
.venv/lib/python3.12/site-packages/telegram/ext/_extbot.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 base class for handlers as used by the Application."""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar, Union
|
||||
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.repr import build_repr_with_selected_attrs
|
||||
from telegram._utils.types import DVType
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Application
|
||||
|
||||
RT = TypeVar("RT")
|
||||
UT = TypeVar("UT")
|
||||
|
||||
|
||||
class BaseHandler(Generic[UT, CCT, RT], ABC):
|
||||
"""The base class for all update handlers. Create custom handlers by inheriting from it.
|
||||
|
||||
Warning:
|
||||
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.
|
||||
|
||||
This class is a :class:`~typing.Generic` class and accepts three type variables:
|
||||
|
||||
1. The type of the updates that this handler will handle. Must coincide with the type of the
|
||||
first argument of :paramref:`callback`. :meth:`check_update` must only accept
|
||||
updates of this type.
|
||||
2. The type of the second argument of :paramref:`callback`. Must coincide with the type of the
|
||||
parameters :paramref:`handle_update.context` and
|
||||
:paramref:`collect_additional_context.context` as well as the second argument of
|
||||
:paramref:`callback`. Must be either :class:`~telegram.ext.CallbackContext` or a subclass
|
||||
of that class.
|
||||
|
||||
.. tip::
|
||||
For this type variable, one should usually provide a :class:`~typing.TypeVar` that is
|
||||
also used for the mentioned method arguments. That way, a type checker can check whether
|
||||
this handler fits the definition of the :class:`~Application`.
|
||||
3. The return type of the :paramref:`callback` function accepted by this handler.
|
||||
|
||||
.. seealso:: :wiki:`Types of Handlers <Types-of-Handlers>`
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
|
||||
* The attribute ``run_async`` is now :paramref:`block`.
|
||||
* This class was previously named ``Handler``.
|
||||
|
||||
Args:
|
||||
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`.
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
|
||||
Attributes:
|
||||
callback (:term:`coroutine function`): The callback function for this handler.
|
||||
block (:obj:`bool`): Determines whether the callback will run in a blocking way.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"block",
|
||||
"callback",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self: "BaseHandler[UT, CCT, RT]",
|
||||
callback: HandlerCallback[UT, CCT, RT],
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
self.callback: HandlerCallback[UT, CCT, RT] = callback
|
||||
self.block: DVType[bool] = block
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Give a string representation of the handler in the form ``ClassName[callback=...]``.
|
||||
|
||||
As this class doesn't implement :meth:`object.__str__`, the default implementation
|
||||
will be used, which is equivalent to :meth:`__repr__`.
|
||||
|
||||
Returns:
|
||||
:obj:`str`
|
||||
"""
|
||||
try:
|
||||
callback_name = self.callback.__qualname__
|
||||
except AttributeError:
|
||||
callback_name = repr(self.callback)
|
||||
return build_repr_with_selected_attrs(self, callback=callback_name)
|
||||
|
||||
@abstractmethod
|
||||
def check_update(self, update: object) -> Optional[Union[bool, object]]:
|
||||
"""
|
||||
This method is called to determine if an update should be handled by
|
||||
this handler instance. It should always be overridden.
|
||||
|
||||
Note:
|
||||
Custom updates types can be handled by the application. Therefore, an implementation of
|
||||
this method should always check the type of :paramref:`update`.
|
||||
|
||||
Args:
|
||||
update (:obj:`object` | :class:`telegram.Update`): The update to be tested.
|
||||
|
||||
Returns:
|
||||
Either :obj:`None` or :obj:`False` if the update should not be handled. Otherwise an
|
||||
object that will be passed to :meth:`handle_update` and
|
||||
:meth:`collect_additional_context` when the update gets handled.
|
||||
|
||||
"""
|
||||
|
||||
async def handle_update(
|
||||
self,
|
||||
update: UT,
|
||||
application: "Application[Any, CCT, Any, Any, Any, Any]",
|
||||
check_result: object,
|
||||
context: CCT,
|
||||
) -> 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
|
||||
arguments. To work with the :class:`telegram.ext.ConversationHandler`, this method
|
||||
returns the value returned from :attr:`callback`.
|
||||
Note that it can be overridden if needed by the subclassing handler.
|
||||
|
||||
Args:
|
||||
update (:obj:`str` | :class:`telegram.Update`): The update to be handled.
|
||||
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 application.
|
||||
|
||||
"""
|
||||
self.collect_additional_context(context, update, application, check_result)
|
||||
return await self.callback(update, context)
|
||||
|
||||
def collect_additional_context(
|
||||
self,
|
||||
context: CCT,
|
||||
update: UT,
|
||||
application: "Application[Any, CCT, Any, Any, Any, Any]",
|
||||
check_result: Any,
|
||||
) -> None:
|
||||
"""Prepares additional arguments for the context. Override if needed.
|
||||
|
||||
Args:
|
||||
context (:class:`telegram.ext.CallbackContext`): The context object.
|
||||
update (:class:`telegram.Update`): The update to gather chat/user id from.
|
||||
application (:class:`telegram.ext.Application`): The calling application.
|
||||
check_result: The result (return value) from :meth:`check_update`.
|
||||
|
||||
"""
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 BusinessConnectionHandler class."""
|
||||
from typing import Optional, TypeVar
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import SCT, DVType
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils._update_parsing import parse_chat_id, parse_username
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
RT = TypeVar("RT")
|
||||
|
||||
|
||||
class BusinessConnectionHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""Handler class to handle Telegram
|
||||
:attr:`Business Connections <telegram.Update.business_connection>`.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
Args:
|
||||
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)
|
||||
user_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only
|
||||
those which are from the specified user ID(s).
|
||||
|
||||
username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only
|
||||
those which are from the specified username(s).
|
||||
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
Attributes:
|
||||
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`.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_user_ids",
|
||||
"_usernames",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self: "BusinessConnectionHandler[CCT, RT]",
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
user_id: Optional[SCT[int]] = None,
|
||||
username: Optional[SCT[str]] = None,
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
super().__init__(callback, block=block)
|
||||
|
||||
self._user_ids = parse_chat_id(user_id)
|
||||
self._usernames = parse_username(username)
|
||||
|
||||
def check_update(self, update: object) -> bool:
|
||||
"""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`
|
||||
|
||||
"""
|
||||
if isinstance(update, Update) and update.business_connection:
|
||||
if not self._user_ids and not self._usernames:
|
||||
return True
|
||||
if update.business_connection.user.id in self._user_ids:
|
||||
return True
|
||||
return update.business_connection.user.username in self._usernames
|
||||
return False
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 BusinessMessagesDeletedHandler class."""
|
||||
from typing import Optional, TypeVar
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import SCT, DVType
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils._update_parsing import parse_chat_id, parse_username
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
RT = TypeVar("RT")
|
||||
|
||||
|
||||
class BusinessMessagesDeletedHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""Handler class to handle
|
||||
:attr:`deleted Telegram Business messages <telegram.Update.deleted_business_messages>`.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
Args:
|
||||
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)
|
||||
chat_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only
|
||||
those which are from the specified chat ID(s).
|
||||
|
||||
username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only
|
||||
those which are from the specified username(s).
|
||||
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
Attributes:
|
||||
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`.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_chat_ids",
|
||||
"_usernames",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self: "BusinessMessagesDeletedHandler[CCT, RT]",
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
chat_id: Optional[SCT[int]] = None,
|
||||
username: Optional[SCT[str]] = None,
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
super().__init__(callback, block=block)
|
||||
|
||||
self._chat_ids = parse_chat_id(chat_id)
|
||||
self._usernames = parse_username(username)
|
||||
|
||||
def check_update(self, update: object) -> bool:
|
||||
"""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`
|
||||
|
||||
"""
|
||||
if isinstance(update, Update) and update.deleted_business_messages:
|
||||
if not self._chat_ids and not self._usernames:
|
||||
return True
|
||||
if update.deleted_business_messages.chat.id in self._chat_ids:
|
||||
return True
|
||||
return update.deleted_business_messages.chat.username in self._usernames
|
||||
return False
|
||||
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 CallbackQueryHandler class."""
|
||||
import asyncio
|
||||
import re
|
||||
from re import Match, Pattern
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union, cast
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import DVType
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Application
|
||||
|
||||
RT = TypeVar("RT")
|
||||
|
||||
|
||||
class CallbackQueryHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""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.
|
||||
|
||||
Note:
|
||||
* If your bot allows arbitrary objects as
|
||||
:paramref:`~telegram.InlineKeyboardButton.callback_data`, it may happen that the
|
||||
original :attr:`~telegram.InlineKeyboardButton.callback_data` for the incoming
|
||||
:class:`telegram.CallbackQuery` can not be found. This is the case when either a
|
||||
malicious client tempered with the :attr:`telegram.CallbackQuery.data` or the data was
|
||||
simply dropped from cache or not persisted. In these
|
||||
cases, an instance of :class:`telegram.ext.InvalidCallbackData` will be set as
|
||||
:attr:`telegram.CallbackQuery.data`.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
* If neither :paramref:`pattern` nor :paramref:`game_pattern` is set, `any`
|
||||
``CallbackQuery`` will be handled. If only :paramref:`pattern` is set, queries with
|
||||
:attr:`~telegram.CallbackQuery.game_short_name` will `not` be considered and vice versa.
|
||||
If both patterns are set, queries with either :attr:
|
||||
`~telegram.CallbackQuery.game_short_name` or :attr:`~telegram.CallbackQuery.data`
|
||||
matching the defined pattern will be handled
|
||||
|
||||
.. versionadded:: 21.5
|
||||
|
||||
Warning:
|
||||
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 (: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>` | :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 :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
|
||||
:obj:`False`/:obj:`None` to indicate, whether the update should be handled.
|
||||
- a :obj:`type`. If :attr:`telegram.CallbackQuery.data` is an instance of that type
|
||||
(or a subclass), the update will be handled.
|
||||
|
||||
If :attr:`telegram.CallbackQuery.data` is :obj:`None`, the
|
||||
:class:`telegram.CallbackQuery` update will not be handled.
|
||||
|
||||
.. seealso:: :wiki:`Arbitrary callback_data <Arbitrary-callback_data>`
|
||||
|
||||
.. versionchanged:: 13.6
|
||||
Added support for arbitrary callback data.
|
||||
game_pattern (:obj:`str` | :func:`re.Pattern <re.compile>` | optional)
|
||||
Pattern to test :attr:`telegram.CallbackQuery.game_short_name` against. If a string or
|
||||
a regex pattern is passed, :func:`re.match` is used on
|
||||
:attr:`telegram.CallbackQuery.game_short_name` to determine if an update should be
|
||||
handled by this handler.
|
||||
|
||||
.. versionadded:: 21.5
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
|
||||
Attributes:
|
||||
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.
|
||||
game_pattern (:func:`re.Pattern <re.compile>`): Optional.
|
||||
Regex pattern to test :attr:`telegram.CallbackQuery.game_short_name`
|
||||
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__ = ("game_pattern", "pattern")
|
||||
|
||||
def __init__(
|
||||
self: "CallbackQueryHandler[CCT, RT]",
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
pattern: Optional[
|
||||
Union[str, Pattern[str], type, Callable[[object], Optional[bool]]]
|
||||
] = None,
|
||||
game_pattern: Optional[Union[str, Pattern[str]]] = None,
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
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)
|
||||
|
||||
if isinstance(game_pattern, str):
|
||||
game_pattern = re.compile(game_pattern)
|
||||
self.pattern: Optional[
|
||||
Union[str, Pattern[str], type, Callable[[object], Optional[bool]]]
|
||||
] = pattern
|
||||
self.game_pattern: Optional[Union[str, Pattern[str]]] = game_pattern
|
||||
|
||||
def check_update(self, update: object) -> Optional[Union[bool, object]]:
|
||||
"""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`
|
||||
|
||||
"""
|
||||
# pylint: disable=too-many-return-statements
|
||||
if not (isinstance(update, Update) and update.callback_query):
|
||||
return None
|
||||
|
||||
callback_data = update.callback_query.data
|
||||
game_short_name = update.callback_query.game_short_name
|
||||
|
||||
if not any([self.pattern, self.game_pattern]):
|
||||
return True
|
||||
|
||||
# we check for .data or .game_short_name from update to filter based on whats coming
|
||||
# this gives xor-like behavior
|
||||
if callback_data:
|
||||
if not self.pattern:
|
||||
return False
|
||||
if isinstance(self.pattern, type):
|
||||
return isinstance(callback_data, self.pattern)
|
||||
if callable(self.pattern):
|
||||
return self.pattern(callback_data)
|
||||
if not isinstance(callback_data, str):
|
||||
return False
|
||||
if match := re.match(self.pattern, callback_data):
|
||||
return match
|
||||
|
||||
elif game_short_name:
|
||||
if not self.game_pattern:
|
||||
return False
|
||||
if match := re.match(self.game_pattern, game_short_name):
|
||||
return match
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
def collect_additional_context(
|
||||
self,
|
||||
context: CCT,
|
||||
update: Update, # noqa: ARG002
|
||||
application: "Application[Any, CCT, Any, Any, Any, Any]", # noqa: ARG002
|
||||
check_result: Union[bool, Match[str]],
|
||||
) -> None:
|
||||
"""Add the result of ``re.match(pattern, update.callback_query.data)`` to
|
||||
:attr:`CallbackContext.matches` as list with one element.
|
||||
"""
|
||||
if self.pattern:
|
||||
check_result = cast("Match", check_result)
|
||||
context.matches = [check_result]
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 ChatBoostHandler class."""
|
||||
|
||||
from typing import Final, Optional
|
||||
|
||||
from telegram import Update
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils._update_parsing import parse_chat_id, parse_username
|
||||
from telegram.ext._utils.types import CCT, RT, HandlerCallback
|
||||
|
||||
|
||||
class ChatBoostHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""
|
||||
Handler class to handle Telegram updates that contain a chat boost.
|
||||
|
||||
Warning:
|
||||
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:: 20.8
|
||||
|
||||
Args:
|
||||
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`.
|
||||
chat_boost_types (:obj:`int`, optional): Pass one of
|
||||
:attr:`CHAT_BOOST`, :attr:`REMOVED_CHAT_BOOST` or
|
||||
:attr:`ANY_CHAT_BOOST` to specify if this handler should handle only updates with
|
||||
:attr:`telegram.Update.chat_boost`,
|
||||
:attr:`telegram.Update.removed_chat_boost` or both. Defaults to
|
||||
:attr:`CHAT_BOOST`.
|
||||
chat_id (:obj:`int` | Collection[:obj:`int`], optional): Filters reactions to allow
|
||||
only those which happen in the specified chat ID(s).
|
||||
chat_username (:obj:`str` | Collection[:obj:`str`], optional): Filters reactions to allow
|
||||
only those which happen in the specified username(s).
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
|
||||
Attributes:
|
||||
callback (:term:`coroutine function`): The callback function for this handler.
|
||||
chat_boost_types (:obj:`int`): Optional. Specifies if this handler should handle only
|
||||
updates with :attr:`telegram.Update.chat_boost`,
|
||||
:attr:`telegram.Update.removed_chat_boost` or both.
|
||||
block (:obj:`bool`): Determines whether the callback will run in a blocking way.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_chat_ids",
|
||||
"_chat_usernames",
|
||||
"chat_boost_types",
|
||||
)
|
||||
|
||||
CHAT_BOOST: Final[int] = -1
|
||||
""" :obj:`int`: Used as a constant to handle only :attr:`telegram.Update.chat_boost`."""
|
||||
REMOVED_CHAT_BOOST: Final[int] = 0
|
||||
""":obj:`int`: Used as a constant to handle only :attr:`telegram.Update.removed_chat_boost`."""
|
||||
ANY_CHAT_BOOST: Final[int] = 1
|
||||
""":obj:`int`: Used as a constant to handle both :attr:`telegram.Update.chat_boost`
|
||||
and :attr:`telegram.Update.removed_chat_boost`."""
|
||||
|
||||
def __init__(
|
||||
self: "ChatBoostHandler[CCT, RT]",
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
chat_boost_types: int = CHAT_BOOST,
|
||||
chat_id: Optional[int] = None,
|
||||
chat_username: Optional[str] = None,
|
||||
block: bool = True,
|
||||
):
|
||||
super().__init__(callback, block=block)
|
||||
self.chat_boost_types: int = chat_boost_types
|
||||
self._chat_ids = parse_chat_id(chat_id)
|
||||
self._chat_usernames = parse_username(chat_username)
|
||||
|
||||
def check_update(self, update: object) -> bool:
|
||||
"""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`
|
||||
|
||||
"""
|
||||
if not isinstance(update, Update):
|
||||
return False
|
||||
|
||||
if not (update.chat_boost or update.removed_chat_boost):
|
||||
return False
|
||||
|
||||
if self.chat_boost_types == self.CHAT_BOOST and not update.chat_boost:
|
||||
return False
|
||||
|
||||
if self.chat_boost_types == self.REMOVED_CHAT_BOOST and not update.removed_chat_boost:
|
||||
return False
|
||||
|
||||
if not any((self._chat_ids, self._chat_usernames)):
|
||||
return True
|
||||
|
||||
# Extract chat and user IDs and usernames from the update for comparison
|
||||
chat_id = chat.id if (chat := update.effective_chat) else None
|
||||
chat_username = chat.username if chat else None
|
||||
|
||||
return bool(self._chat_ids and (chat_id in self._chat_ids)) or bool(
|
||||
self._chat_usernames and (chat_username in self._chat_usernames)
|
||||
)
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 ChatJoinRequestHandler class."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import RT, SCT, DVType
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils._update_parsing import parse_chat_id, parse_username
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
|
||||
class ChatJoinRequestHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""Handler class to handle Telegram updates that contain
|
||||
:attr:`telegram.Update.chat_join_request`.
|
||||
|
||||
Note:
|
||||
If neither of :paramref:`username` and the :paramref:`chat_id` are passed, this handler
|
||||
accepts *any* join request. Otherwise, this handler accepts all requests to join chats
|
||||
for which the chat ID is listed in :paramref:`chat_id` or the username is listed in
|
||||
:paramref:`username`, or both.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Warning:
|
||||
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 (: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`.
|
||||
chat_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only
|
||||
those which are asking to join the specified chat ID(s).
|
||||
|
||||
.. versionadded:: 20.0
|
||||
username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only
|
||||
those which are asking to join the specified username(s).
|
||||
|
||||
.. versionadded:: 20.0
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
|
||||
Attributes:
|
||||
callback (:term:`coroutine function`): The callback function for this handler.
|
||||
block (:obj:`bool`): Determines whether the callback will run in a blocking way..
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_chat_ids",
|
||||
"_usernames",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self: "ChatJoinRequestHandler[CCT, RT]",
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
chat_id: Optional[SCT[int]] = None,
|
||||
username: Optional[SCT[str]] = None,
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
super().__init__(callback, block=block)
|
||||
|
||||
self._chat_ids = parse_chat_id(chat_id)
|
||||
self._usernames = parse_username(username)
|
||||
|
||||
def check_update(self, update: object) -> bool:
|
||||
"""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`
|
||||
|
||||
"""
|
||||
if isinstance(update, Update) and update.chat_join_request:
|
||||
if not self._chat_ids and not self._usernames:
|
||||
return True
|
||||
if update.chat_join_request.chat.id in self._chat_ids:
|
||||
return True
|
||||
return update.chat_join_request.from_user.username in self._usernames
|
||||
return False
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 ChatMemberHandler class."""
|
||||
from typing import Final, Optional, TypeVar
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import SCT, DVType
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils._update_parsing import parse_chat_id
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
RT = TypeVar("RT")
|
||||
|
||||
|
||||
class ChatMemberHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""Handler class to handle Telegram updates that contain a chat member update.
|
||||
|
||||
Warning:
|
||||
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.
|
||||
|
||||
Examples:
|
||||
:any:`Chat Member Bot <examples.chatmemberbot>`
|
||||
|
||||
.. versionadded:: 13.4
|
||||
|
||||
Args:
|
||||
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`.
|
||||
chat_member_types (:obj:`int`, optional): Pass one of :attr:`MY_CHAT_MEMBER`,
|
||||
: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`.
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
chat_id (:obj:`int` | Collection[:obj:`int`], optional): Filters chat member updates from
|
||||
specified chat ID(s) only.
|
||||
.. versionadded:: 21.3
|
||||
|
||||
Attributes:
|
||||
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.
|
||||
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__ = (
|
||||
"_chat_ids",
|
||||
"chat_member_types",
|
||||
)
|
||||
MY_CHAT_MEMBER: Final[int] = -1
|
||||
""":obj:`int`: Used as a constant to handle only :attr:`telegram.Update.my_chat_member`."""
|
||||
CHAT_MEMBER: Final[int] = 0
|
||||
""":obj:`int`: Used as a constant to handle only :attr:`telegram.Update.chat_member`."""
|
||||
ANY_CHAT_MEMBER: Final[int] = 1
|
||||
""":obj:`int`: Used as a constant to handle both :attr:`telegram.Update.my_chat_member`
|
||||
and :attr:`telegram.Update.chat_member`."""
|
||||
|
||||
def __init__(
|
||||
self: "ChatMemberHandler[CCT, RT]",
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
chat_member_types: int = MY_CHAT_MEMBER,
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
chat_id: Optional[SCT[int]] = None,
|
||||
):
|
||||
super().__init__(callback, block=block)
|
||||
|
||||
self.chat_member_types: Optional[int] = chat_member_types
|
||||
self._chat_ids = parse_chat_id(chat_id)
|
||||
|
||||
def check_update(self, update: object) -> bool:
|
||||
"""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`
|
||||
|
||||
"""
|
||||
if not isinstance(update, Update):
|
||||
return False
|
||||
if not (update.my_chat_member or update.chat_member):
|
||||
return False
|
||||
if (
|
||||
self._chat_ids
|
||||
and update.effective_chat
|
||||
and update.effective_chat.id not in self._chat_ids
|
||||
):
|
||||
return False
|
||||
if self.chat_member_types == self.ANY_CHAT_MEMBER:
|
||||
return True
|
||||
if self.chat_member_types == self.CHAT_MEMBER:
|
||||
return bool(update.chat_member)
|
||||
return bool(update.my_chat_member)
|
||||
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 ChosenInlineResultHandler class."""
|
||||
import re
|
||||
from re import Match, Pattern
|
||||
from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union, cast
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import DVType
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
RT = TypeVar("RT")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Application
|
||||
|
||||
|
||||
class ChosenInlineResultHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""Handler class to handle Telegram updates that contain
|
||||
:attr:`telegram.Update.chosen_inline_result`.
|
||||
|
||||
Warning:
|
||||
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 (: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`.
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
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
|
||||
should be handled by this handler. This is accessible in the callback as
|
||||
:attr:`telegram.ext.CallbackContext.matches`.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
Attributes:
|
||||
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.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("pattern",)
|
||||
|
||||
def __init__(
|
||||
self: "ChosenInlineResultHandler[CCT, RT]",
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
pattern: Optional[Union[str, Pattern[str]]] = None,
|
||||
):
|
||||
super().__init__(callback, block=block)
|
||||
|
||||
if isinstance(pattern, str):
|
||||
pattern = re.compile(pattern)
|
||||
|
||||
self.pattern: Optional[Union[str, Pattern[str]]] = pattern
|
||||
|
||||
def check_update(self, update: object) -> Optional[Union[bool, object]]:
|
||||
"""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:`re.match`
|
||||
|
||||
"""
|
||||
if isinstance(update, Update) and update.chosen_inline_result:
|
||||
if self.pattern:
|
||||
if match := re.match(self.pattern, update.chosen_inline_result.result_id):
|
||||
return match
|
||||
else:
|
||||
return True
|
||||
return None
|
||||
|
||||
def collect_additional_context(
|
||||
self,
|
||||
context: CCT,
|
||||
update: Update, # noqa: ARG002
|
||||
application: "Application[Any, CCT, Any, Any, Any, Any]", # noqa: ARG002
|
||||
check_result: Union[bool, Match[str]],
|
||||
) -> None:
|
||||
"""This function adds the matched regex pattern result to
|
||||
:attr:`telegram.ext.CallbackContext.matches`.
|
||||
"""
|
||||
if self.pattern:
|
||||
check_result = cast("Match", check_result)
|
||||
context.matches = [check_result]
|
||||
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 CommandHandler class."""
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
|
||||
|
||||
from telegram import MessageEntity, Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import SCT, DVType
|
||||
from telegram.ext import filters as filters_module
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils.types import CCT, FilterDataDict, HandlerCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Application
|
||||
|
||||
RT = TypeVar("RT")
|
||||
|
||||
|
||||
class CommandHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""Handler class to handle Telegram commands.
|
||||
|
||||
Commands are Telegram messages that start with a :attr:`telegram.MessageEntity.BOT_COMMAND`
|
||||
(so with ``/``, optionally followed by an ``@`` and 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 :attr:`~filters.UpdateType.EDITED_MESSAGE <telegram.ext.filters.UpdateType.EDITED_MESSAGE>`
|
||||
in the filter argument.
|
||||
|
||||
Note:
|
||||
:class:`CommandHandler` does *not* handle (edited) channel posts and does *not* handle
|
||||
commands that are part of a caption. Please use :class:`~telegram.ext.MessageHandler`
|
||||
with a suitable combination of filters (e.g.
|
||||
:attr:`telegram.ext.filters.UpdateType.CHANNEL_POSTS`,
|
||||
:attr:`telegram.ext.filters.CAPTION` and :class:`telegram.ext.filters.Regex`) to handle
|
||||
those messages.
|
||||
|
||||
Note:
|
||||
If you want to support a different entity in the beginning, e.g. if a command message is
|
||||
wrapped in a :attr:`telegram.MessageEntity.CODE`, use the
|
||||
:class:`telegram.ext.PrefixHandler`.
|
||||
|
||||
Warning:
|
||||
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.
|
||||
|
||||
Examples:
|
||||
* :any:`Timer Bot <examples.timerbot>`
|
||||
* :any:`Error Handler Bot <examples.errorhandlerbot>`
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
|
||||
* Renamed the attribute ``command`` to :attr:`commands`, which now is always a
|
||||
:class:`frozenset`
|
||||
* Updating the commands this handler listens to is no longer possible.
|
||||
|
||||
Args:
|
||||
command (:obj:`str` | Collection[:obj:`str`]):
|
||||
The command or list of commands this handler should listen for. Case-insensitive.
|
||||
Limitations are the same as for :attr:`telegram.BotCommand.command`.
|
||||
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 :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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
has_args (:obj:`bool` | :obj:`int`, optional):
|
||||
Determines whether the command handler should process the update or not.
|
||||
If :obj:`True`, the handler will process any non-zero number of args.
|
||||
If :obj:`False`, the handler will only process if there are no args.
|
||||
if :obj:`int`, the handler will only process if there are exactly that many args.
|
||||
Defaults to :obj:`None`, which means the handler will process any or no args.
|
||||
|
||||
.. versionadded:: 20.5
|
||||
|
||||
Raises:
|
||||
:exc:`ValueError`: When the command is too long or has illegal chars.
|
||||
|
||||
Attributes:
|
||||
commands (frozenset[:obj:`str`]): The set of commands this handler should listen for.
|
||||
callback (:term:`coroutine function`): The callback function for this handler.
|
||||
filters (:class:`telegram.ext.filters.BaseFilter`): Optional. Only allow updates with these
|
||||
filters.
|
||||
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`.
|
||||
has_args (:obj:`bool` | :obj:`int` | None):
|
||||
Optional argument, otherwise all implementations of :class:`CommandHandler` will break.
|
||||
Defaults to :obj:`None`, which means the handler will process any args or no args.
|
||||
|
||||
.. versionadded:: 20.5
|
||||
"""
|
||||
|
||||
__slots__ = ("commands", "filters", "has_args")
|
||||
|
||||
def __init__(
|
||||
self: "CommandHandler[CCT, RT]",
|
||||
command: SCT[str],
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
filters: Optional[filters_module.BaseFilter] = None,
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
has_args: Optional[Union[bool, int]] = None,
|
||||
):
|
||||
super().__init__(callback, block=block)
|
||||
|
||||
if isinstance(command, str):
|
||||
commands = frozenset({command.lower()})
|
||||
else:
|
||||
commands = frozenset(x.lower() for x in command)
|
||||
for comm in commands:
|
||||
if not re.match(r"^[\da-z_]{1,32}$", comm):
|
||||
raise ValueError(f"Command `{comm}` is not a valid bot command")
|
||||
self.commands: frozenset[str] = commands
|
||||
|
||||
self.filters: filters_module.BaseFilter = (
|
||||
filters if filters is not None else filters_module.UpdateType.MESSAGES
|
||||
)
|
||||
|
||||
self.has_args: Optional[Union[bool, int]] = has_args
|
||||
|
||||
if (isinstance(self.has_args, int)) and (self.has_args < 0):
|
||||
raise ValueError("CommandHandler argument has_args cannot be a negative integer")
|
||||
|
||||
def _check_correct_args(self, args: list[str]) -> Optional[bool]:
|
||||
"""Determines whether the args are correct for this handler. Implemented in check_update().
|
||||
Args:
|
||||
args (:obj:`list`): The args for the handler.
|
||||
Returns:
|
||||
:obj:`bool`: Whether the args are valid for this handler.
|
||||
"""
|
||||
return bool(
|
||||
(self.has_args is None)
|
||||
or (self.has_args is True and args)
|
||||
or (self.has_args is False and not args)
|
||||
or (isinstance(self.has_args, int) and len(args) == self.has_args)
|
||||
)
|
||||
|
||||
def check_update(
|
||||
self, update: object
|
||||
) -> Optional[Union[bool, tuple[list[str], Optional[Union[bool, FilterDataDict]]]]]:
|
||||
"""Determines whether an update should be passed to this handler's :attr:`callback`.
|
||||
|
||||
Args:
|
||||
update (:class:`telegram.Update` | :obj:`object`): Incoming update.
|
||||
|
||||
Returns:
|
||||
:obj:`list`: The list of args for the handler.
|
||||
|
||||
"""
|
||||
if isinstance(update, Update) and update.effective_message:
|
||||
message = update.effective_message
|
||||
|
||||
if (
|
||||
message.entities
|
||||
and message.entities[0].type == MessageEntity.BOT_COMMAND
|
||||
and message.entities[0].offset == 0
|
||||
and message.text
|
||||
and message.get_bot()
|
||||
):
|
||||
command = message.text[1 : message.entities[0].length]
|
||||
args = message.text.split()[1:]
|
||||
command_parts = command.split("@")
|
||||
command_parts.append(message.get_bot().username)
|
||||
|
||||
if not (
|
||||
command_parts[0].lower() in self.commands
|
||||
and command_parts[1].lower() == message.get_bot().username.lower()
|
||||
):
|
||||
return None
|
||||
|
||||
if not self._check_correct_args(args):
|
||||
return None
|
||||
|
||||
filter_result = self.filters.check_update(update)
|
||||
if filter_result:
|
||||
return args, filter_result
|
||||
return False
|
||||
return None
|
||||
|
||||
def collect_additional_context(
|
||||
self,
|
||||
context: CCT,
|
||||
update: Update, # noqa: ARG002
|
||||
application: "Application[Any, CCT, Any, Any, Any, Any]", # noqa: ARG002
|
||||
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
|
||||
whitespaces and add output of data filters to :attr:`CallbackContext` as well.
|
||||
"""
|
||||
if isinstance(check_result, tuple):
|
||||
context.args = check_result[0]
|
||||
if isinstance(check_result[1], dict):
|
||||
context.update(check_result[1])
|
||||
@@ -0,0 +1,958 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 ConversationHandler."""
|
||||
import asyncio
|
||||
import datetime as dtm
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Final, Generic, NoReturn, Optional, Union, cast
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE, DefaultValue
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.repr import build_repr_with_selected_attrs
|
||||
from telegram._utils.types import DVType
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.ext._application import ApplicationHandlerStop
|
||||
from telegram.ext._extbot import ExtBot
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._handlers.callbackqueryhandler import CallbackQueryHandler
|
||||
from telegram.ext._handlers.choseninlineresulthandler import ChosenInlineResultHandler
|
||||
from telegram.ext._handlers.inlinequeryhandler import InlineQueryHandler
|
||||
from telegram.ext._handlers.stringcommandhandler import StringCommandHandler
|
||||
from telegram.ext._handlers.stringregexhandler import StringRegexHandler
|
||||
from telegram.ext._handlers.typehandler import TypeHandler
|
||||
from telegram.ext._utils.trackingdict import TrackingDict
|
||||
from telegram.ext._utils.types import CCT, ConversationDict, ConversationKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Application, Job, JobQueue
|
||||
_CheckUpdateType = tuple[object, ConversationKey, BaseHandler[Update, CCT, object], object]
|
||||
|
||||
_LOGGER = get_logger(__name__, class_name="ConversationHandler")
|
||||
|
||||
|
||||
@dataclass
|
||||
class _ConversationTimeoutContext(Generic[CCT]):
|
||||
"""Used as a datastore for conversation timeouts. Passed in the
|
||||
:paramref:`JobQueue.run_once.data` parameter. See :meth:`_trigger_timeout`.
|
||||
"""
|
||||
|
||||
__slots__ = ("application", "callback_context", "conversation_key", "update")
|
||||
|
||||
conversation_key: ConversationKey
|
||||
update: Update
|
||||
application: "Application[Any, CCT, Any, Any, Any, JobQueue]"
|
||||
callback_context: CCT
|
||||
|
||||
|
||||
@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__ = ("old_state", "task")
|
||||
|
||||
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 both the new and old
|
||||
state are :obj:`None`, return `CH.END`. If only the new state is :obj:`None`, return the
|
||||
old state.
|
||||
|
||||
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,
|
||||
)
|
||||
return self.old_state
|
||||
|
||||
res = self.task.result()
|
||||
if res is None and self.old_state is None:
|
||||
res = ConversationHandler.END
|
||||
elif res is None:
|
||||
# returning None from a callback means that we want to stay in the old state
|
||||
return self.old_state
|
||||
|
||||
return res
|
||||
|
||||
|
||||
class ConversationHandler(BaseHandler[Update, CCT, object]):
|
||||
"""
|
||||
A handler to hold a conversation with a single or multiple users through Telegram updates by
|
||||
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.ApplicationBuilder.concurrent_updates` should
|
||||
be set to :obj:`False`.
|
||||
|
||||
Note:
|
||||
: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`, :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
|
||||
: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, :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-conversation handler-do
|
||||
|
||||
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 :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
|
||||
:attr:`block=False <block>` handler is not finished.
|
||||
|
||||
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
|
||||
default), the state will not change. If an entry point callback function returns :obj:`None`,
|
||||
the conversation ends immediately after the execution of this callback function.
|
||||
To end the conversation, the callback function must return :attr:`END` or ``-1``. To
|
||||
handle the conversation timeout, use handler :attr:`TIMEOUT` or ``-2``.
|
||||
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 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 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
|
||||
:any:`examples.nestedconversationbot`.
|
||||
|
||||
Examples:
|
||||
* :any:`Conversation Bot <examples.conversationbot>`
|
||||
* :any:`Conversation Bot 2 <examples.conversationbot2>`
|
||||
* :any:`Nested Conversation Bot <examples.nestedconversationbot>`
|
||||
* :any:`Persistent Conversation Bot <examples.persistentconversationbot>`
|
||||
|
||||
Args:
|
||||
entry_points (list[:class:`telegram.ext.BaseHandler`]): A list of :obj:`BaseHandler`
|
||||
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.BaseHandler`]]): A :obj:`dict` that
|
||||
defines the different states of conversation a user can be in and one or more
|
||||
associated :obj:`BaseHandler` 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.BaseHandler`]): 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 :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.
|
||||
Default is :obj:`False`.
|
||||
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 conversation key should contain the User's ID.
|
||||
Default is :obj:`True`.
|
||||
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 :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`.
|
||||
|
||||
Caution:
|
||||
* This feature relies on the :attr:`telegram.ext.Application.job_queue` being set
|
||||
and hence requires that the dependencies that :class:`telegram.ext.JobQueue`
|
||||
relies on are installed.
|
||||
* 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 conversation handler. Required for
|
||||
persistence.
|
||||
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:: 20.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 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:`BaseHandler.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:
|
||||
|
||||
1. :attr:`telegram.ext.BaseHandler.block` (if set)
|
||||
2. the value passed to this parameter (if any)
|
||||
3. :attr:`telegram.ext.Defaults.block` (if defaults are used)
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
No longer overrides the handlers settings. Resolution order was changed.
|
||||
|
||||
Raises:
|
||||
: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:
|
||||
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__ = (
|
||||
"_allow_reentry",
|
||||
"_block",
|
||||
"_child_conversations",
|
||||
"_conversation_timeout",
|
||||
"_conversations",
|
||||
"_entry_points",
|
||||
"_fallbacks",
|
||||
"_map_to_parent",
|
||||
"_name",
|
||||
"_per_chat",
|
||||
"_per_message",
|
||||
"_per_user",
|
||||
"_persistent",
|
||||
"_states",
|
||||
"_timeout_jobs_lock",
|
||||
"timeout_jobs",
|
||||
)
|
||||
|
||||
END: Final[int] = -1
|
||||
""":obj:`int`: Used as a constant to return when a conversation is ended."""
|
||||
TIMEOUT: Final[int] = -2
|
||||
""":obj:`int`: Used as a constant to handle state when a conversation is timed out
|
||||
(exceeded :attr:`conversation_timeout`).
|
||||
"""
|
||||
WAITING: Final[int] = -3
|
||||
""":obj:`int`: Used as a constant to handle state when a conversation is still waiting on the
|
||||
previous :attr:`block=False <block>` handler to finish."""
|
||||
|
||||
# pylint: disable=super-init-not-called
|
||||
def __init__(
|
||||
self: "ConversationHandler[CCT]",
|
||||
entry_points: list[BaseHandler[Update, CCT, object]],
|
||||
states: dict[object, list[BaseHandler[Update, CCT, object]]],
|
||||
fallbacks: list[BaseHandler[Update, CCT, object]],
|
||||
allow_reentry: bool = False,
|
||||
per_chat: bool = True,
|
||||
per_user: bool = True,
|
||||
per_message: bool = False,
|
||||
conversation_timeout: Optional[Union[float, dtm.timedelta]] = None,
|
||||
name: Optional[str] = None,
|
||||
persistent: bool = False,
|
||||
map_to_parent: Optional[dict[object, object]] = None,
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
# these imports need to be here because of circular import error otherwise
|
||||
from telegram.ext import ( # pylint: disable=import-outside-toplevel # noqa: PLC0415
|
||||
PollAnswerHandler,
|
||||
PollHandler,
|
||||
PreCheckoutQueryHandler,
|
||||
ShippingQueryHandler,
|
||||
)
|
||||
|
||||
# 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: DVType[bool] = True
|
||||
# Store the actual setting in a protected variable instead
|
||||
self._block: DVType[bool] = block
|
||||
|
||||
self._entry_points: list[BaseHandler[Update, CCT, object]] = entry_points
|
||||
self._states: dict[object, list[BaseHandler[Update, CCT, object]]] = states
|
||||
self._fallbacks: list[BaseHandler[Update, CCT, object]] = fallbacks
|
||||
|
||||
self._allow_reentry: bool = allow_reentry
|
||||
self._per_user: bool = per_user
|
||||
self._per_chat: bool = per_chat
|
||||
self._per_message: bool = per_message
|
||||
self._conversation_timeout: Optional[Union[float, dtm.timedelta]] = conversation_timeout
|
||||
self._name: Optional[str] = name
|
||||
self._map_to_parent: Optional[dict[object, object]] = map_to_parent
|
||||
|
||||
# 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[Any]] = {}
|
||||
self._timeout_jobs_lock = asyncio.Lock()
|
||||
self._conversations: ConversationDict = {}
|
||||
self._child_conversations: set[ConversationHandler] = set()
|
||||
|
||||
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'")
|
||||
|
||||
if self.per_message and not self.per_chat:
|
||||
warn(
|
||||
"If 'per_message=True' is used, 'per_chat=True' should also be used, "
|
||||
"since message IDs are not globally unique.",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
all_handlers: list[BaseHandler[Update, CCT, object]] = []
|
||||
all_handlers.extend(entry_points)
|
||||
all_handlers.extend(fallbacks)
|
||||
|
||||
for state_handlers in states.values():
|
||||
all_handlers.extend(state_handlers)
|
||||
|
||||
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 = (
|
||||
" Read this FAQ entry to learn more about the per_* settings: "
|
||||
"https://github.com/python-telegram-bot/python-telegram-bot/wiki"
|
||||
"/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(
|
||||
"The `ConversationHandler` only handles updates of type `telegram.Update`. "
|
||||
f"{handler.__class__.__name__} handles updates of type `str`.",
|
||||
stacklevel=2,
|
||||
)
|
||||
elif isinstance(handler, TypeHandler) and not issubclass(handler.type, Update):
|
||||
warn(
|
||||
"The `ConversationHandler` only handles updates of type `telegram.Update`."
|
||||
f" The TypeHandler is set to handle {handler.type.__name__}.",
|
||||
stacklevel=2,
|
||||
)
|
||||
elif isinstance(handler, PollHandler):
|
||||
warn(
|
||||
"PollHandler will never trigger in a conversation since it has no information "
|
||||
"about the chat or the user who voted in it. Do you mean the "
|
||||
"`PollAnswerHandler`?",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
elif self.per_chat and (
|
||||
isinstance(
|
||||
handler,
|
||||
(
|
||||
ShippingQueryHandler,
|
||||
InlineQueryHandler,
|
||||
ChosenInlineResultHandler,
|
||||
PreCheckoutQueryHandler,
|
||||
PollAnswerHandler,
|
||||
),
|
||||
)
|
||||
):
|
||||
warn(
|
||||
f"Updates handled by {handler.__class__.__name__} only have information about "
|
||||
"the user, so this handler won't ever be triggered if `per_chat=True`."
|
||||
f"{per_faq_link}",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
elif self.per_message and not isinstance(handler, CallbackQueryHandler):
|
||||
warn(
|
||||
"If 'per_message=True', all entry points, state handlers, and fallbacks"
|
||||
" must be 'CallbackQueryHandler', since no other handlers "
|
||||
f"have a message context.{per_faq_link}",
|
||||
stacklevel=2,
|
||||
)
|
||||
elif not self.per_message and isinstance(handler, CallbackQueryHandler):
|
||||
warn(
|
||||
"If 'per_message=False', 'CallbackQueryHandler' will not be "
|
||||
f"tracked for every message.{per_faq_link}",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
if self.conversation_timeout and isinstance(handler, self.__class__):
|
||||
warn(
|
||||
"Using `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.",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Give a string representation of the ConversationHandler in the form
|
||||
``ConversationHandler[name=..., states={...}]``.
|
||||
|
||||
If there are more than 3 states, only the first 3 states are listed.
|
||||
|
||||
As this class doesn't implement :meth:`object.__str__`, the default implementation
|
||||
will be used, which is equivalent to :meth:`__repr__`.
|
||||
|
||||
Returns:
|
||||
:obj:`str`
|
||||
"""
|
||||
truncation_threshold = 3
|
||||
states = dict(list(self.states.items())[:truncation_threshold])
|
||||
states_string = str(states)
|
||||
if len(self.states) > truncation_threshold:
|
||||
states_string = states_string[:-1] + ", ...}"
|
||||
|
||||
return build_repr_with_selected_attrs(
|
||||
self,
|
||||
name=self.name,
|
||||
states=states_string,
|
||||
)
|
||||
|
||||
@property
|
||||
def entry_points(self) -> list[BaseHandler[Update, CCT, object]]:
|
||||
"""list[:class:`telegram.ext.BaseHandler`]: A list of :obj:`BaseHandler` objects that can
|
||||
trigger the start of the conversation.
|
||||
"""
|
||||
return self._entry_points
|
||||
|
||||
@entry_points.setter
|
||||
def entry_points(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
"You can not assign a new value to entry_points after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def states(self) -> dict[object, list[BaseHandler[Update, CCT, object]]]:
|
||||
"""dict[:obj:`object`, list[:class:`telegram.ext.BaseHandler`]]: A :obj:`dict` that
|
||||
defines the different states of conversation a user can be in and one or more
|
||||
associated :obj:`BaseHandler` objects that should be used in that state.
|
||||
"""
|
||||
return self._states
|
||||
|
||||
@states.setter
|
||||
def states(self, _: object) -> NoReturn:
|
||||
raise AttributeError("You can not assign a new value to states after initialization.")
|
||||
|
||||
@property
|
||||
def fallbacks(self) -> list[BaseHandler[Update, CCT, object]]:
|
||||
"""list[:class:`telegram.ext.BaseHandler`]: 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 :meth:`check_update`.
|
||||
"""
|
||||
return self._fallbacks
|
||||
|
||||
@fallbacks.setter
|
||||
def fallbacks(self, _: object) -> NoReturn:
|
||||
raise AttributeError("You can not assign a new value to fallbacks after initialization.")
|
||||
|
||||
@property
|
||||
def allow_reentry(self) -> bool:
|
||||
""":obj:`bool`: Determines if a user can restart a conversation with an entry point."""
|
||||
return self._allow_reentry
|
||||
|
||||
@allow_reentry.setter
|
||||
def allow_reentry(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
"You can not assign a new value to allow_reentry after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def per_user(self) -> bool:
|
||||
""":obj:`bool`: If the conversation key should contain the User's ID."""
|
||||
return self._per_user
|
||||
|
||||
@per_user.setter
|
||||
def per_user(self, _: object) -> NoReturn:
|
||||
raise AttributeError("You can not assign a new value to per_user after initialization.")
|
||||
|
||||
@property
|
||||
def per_chat(self) -> bool:
|
||||
""":obj:`bool`: If the conversation key should contain the Chat's ID."""
|
||||
return self._per_chat
|
||||
|
||||
@per_chat.setter
|
||||
def per_chat(self, _: object) -> NoReturn:
|
||||
raise AttributeError("You can not assign a new value to per_chat after initialization.")
|
||||
|
||||
@property
|
||||
def per_message(self) -> bool:
|
||||
""":obj:`bool`: If the conversation key should contain the message's ID."""
|
||||
return self._per_message
|
||||
|
||||
@per_message.setter
|
||||
def per_message(self, _: object) -> NoReturn:
|
||||
raise AttributeError("You can not assign a new value to per_message after initialization.")
|
||||
|
||||
@property
|
||||
def conversation_timeout(
|
||||
self,
|
||||
) -> Optional[Union[float, dtm.timedelta]]:
|
||||
""":obj:`float` | :obj:`datetime.timedelta`: Optional. When this
|
||||
handler is inactive more than this timeout (in seconds), it will be automatically
|
||||
ended.
|
||||
"""
|
||||
return self._conversation_timeout
|
||||
|
||||
@conversation_timeout.setter
|
||||
def conversation_timeout(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
"You can not assign a new value to conversation_timeout after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> Optional[str]:
|
||||
""":obj:`str`: Optional. The name for this :class:`ConversationHandler`."""
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, _: 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, _: 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
|
||||
used to instruct a nested :class:`ConversationHandler` to transition into a mapped state on
|
||||
its parent :class:`ConversationHandler` in place of a specified nested state.
|
||||
"""
|
||||
return self._map_to_parent
|
||||
|
||||
@map_to_parent.setter
|
||||
def map_to_parent(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
"You can not assign a new value to map_to_parent after initialization."
|
||||
)
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
application (:class:`telegram.ext.Application`): The application.
|
||||
|
||||
Returns:
|
||||
A dict {conversation.name -> TrackingDict}, which contains all dict of this
|
||||
conversation and possible child conversations.
|
||||
|
||||
"""
|
||||
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!"
|
||||
)
|
||||
|
||||
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
|
||||
stored_data = await application.persistence.get_conversations(self.name)
|
||||
self._conversations.update_no_track(stored_data)
|
||||
|
||||
# Since CH.END is stored as normal state, we need to properly parse it here in order to
|
||||
# actually end the conversation, i.e. delete the key from the _conversations dict
|
||||
# This also makes sure that these entries are deleted from the persisted data on the next
|
||||
# run of Application.update_persistence
|
||||
for key, state in stored_data.items():
|
||||
if state == self.END:
|
||||
self._update_state(new_state=self.END, key=key)
|
||||
|
||||
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: list[Union[int, str]] = []
|
||||
|
||||
if self.per_chat:
|
||||
if chat is None:
|
||||
raise RuntimeError("Can't build key for update without effective chat!")
|
||||
key.append(chat.id)
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
async def _schedule_job_delayed(
|
||||
self,
|
||||
new_state: asyncio.Task,
|
||||
application: "Application[Any, CCT, Any, Any, Any, JobQueue]",
|
||||
update: Update,
|
||||
context: CCT,
|
||||
conversation_key: ConversationKey,
|
||||
) -> None:
|
||||
try:
|
||||
effective_new_state = await new_state
|
||||
except Exception as exc:
|
||||
_LOGGER.debug(
|
||||
"Non-blocking handler callback raised exception. Not scheduling conversation "
|
||||
"timeout.",
|
||||
exc_info=exc,
|
||||
)
|
||||
return None
|
||||
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,
|
||||
application: "Application[Any, CCT, Any, Any, Any, JobQueue]",
|
||||
update: Update,
|
||||
context: CCT,
|
||||
conversation_key: ConversationKey,
|
||||
) -> None:
|
||||
"""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( # type: ignore[union-attr]
|
||||
self._trigger_timeout,
|
||||
self.conversation_timeout, # type: ignore[arg-type]
|
||||
data=_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) -> Optional[_CheckUpdateType[CCT]]:
|
||||
"""
|
||||
Determines whether an update should be handled by this conversation handler, and if so in
|
||||
which state the conversation currently is.
|
||||
|
||||
Args:
|
||||
update (:class:`telegram.Update` | :obj:`object`): Incoming update.
|
||||
|
||||
Returns:
|
||||
:obj:`bool`
|
||||
|
||||
"""
|
||||
if not isinstance(update, Update):
|
||||
return None
|
||||
# Ignore messages in channels
|
||||
if update.channel_post or update.edited_channel_post:
|
||||
return None
|
||||
if self.per_chat and not update.effective_chat:
|
||||
return None
|
||||
if self.per_user and not update.effective_user:
|
||||
return None
|
||||
if self.per_message and not update.callback_query:
|
||||
return None
|
||||
if update.callback_query and self.per_chat and not update.callback_query.message:
|
||||
return None
|
||||
|
||||
key = self._get_key(update)
|
||||
state = self._conversations.get(key)
|
||||
check: Optional[object] = None
|
||||
|
||||
# Resolve futures
|
||||
if isinstance(state, PendingState):
|
||||
_LOGGER.debug("Waiting for asyncio Task to finish ...")
|
||||
|
||||
# check if future is finished or not
|
||||
if state.done():
|
||||
res = state.resolve()
|
||||
# Special case if an error was raised in a non-blocking entry-point
|
||||
if state.old_state is None and state.task.exception():
|
||||
self._conversations.pop(key, None)
|
||||
state = None
|
||||
else:
|
||||
self._update_state(res, key)
|
||||
state = self._conversations.get(key)
|
||||
|
||||
# if not then handle WAITING state instead
|
||||
else:
|
||||
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 self.WAITING, key, handler_, check
|
||||
return None
|
||||
|
||||
_LOGGER.debug("Selecting conversation %s with state %s", str(key), str(state))
|
||||
|
||||
handler: Optional[BaseHandler] = None
|
||||
|
||||
# Search entry points for a match
|
||||
if state is None or self.allow_reentry:
|
||||
for entry_point in self.entry_points:
|
||||
check = entry_point.check_update(update)
|
||||
if check is not None and check is not False:
|
||||
handler = entry_point
|
||||
break
|
||||
|
||||
else:
|
||||
if state is None:
|
||||
return 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 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
|
||||
break
|
||||
|
||||
# Find a fallback handler if all other handlers fail
|
||||
else:
|
||||
for fallback in self.fallbacks:
|
||||
check = fallback.check_update(update)
|
||||
if check is not None and check is not False:
|
||||
handler = fallback
|
||||
break
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
return state, key, handler, check # type: ignore[return-value]
|
||||
|
||||
async def handle_update( # type: ignore[override]
|
||||
self,
|
||||
update: Update,
|
||||
application: "Application[Any, CCT, Any, Any, Any, Any]",
|
||||
check_result: _CheckUpdateType[CCT],
|
||||
context: CCT,
|
||||
) -> Optional[object]:
|
||||
"""Send the update to the callback for the current state and BaseHandler
|
||||
|
||||
Args:
|
||||
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.
|
||||
application (:class:`telegram.ext.Application`): Application that originated the
|
||||
update.
|
||||
context (:class:`telegram.ext.CallbackContext`): The context as provided by
|
||||
the application.
|
||||
|
||||
"""
|
||||
current_state, conversation_key, handler, handler_check_result = check_result
|
||||
raise_dp_handler_stop = False
|
||||
|
||||
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()
|
||||
|
||||
# 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
|
||||
elif 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,
|
||||
name=f"ConversationHandler:{update.update_id}:handle_update:non_blocking_cb",
|
||||
)
|
||||
except ApplicationHandlerStop as exception:
|
||||
new_state = exception.state
|
||||
raise_dp_handler_stop = True
|
||||
async with self._timeout_jobs_lock:
|
||||
if self.conversation_timeout:
|
||||
if application.job_queue is None:
|
||||
warn(
|
||||
"Ignoring `conversation_timeout` because the Application has no JobQueue.",
|
||||
stacklevel=1,
|
||||
)
|
||||
elif not application.job_queue.scheduler.running:
|
||||
warn(
|
||||
"Ignoring `conversation_timeout` because the Applications JobQueue is "
|
||||
"not running.",
|
||||
stacklevel=1,
|
||||
)
|
||||
elif isinstance(new_state, asyncio.Task):
|
||||
# Add the new timeout job
|
||||
# checking if the new state is self.END is done in _schedule_job
|
||||
application.create_task(
|
||||
self._schedule_job_delayed(
|
||||
new_state, application, update, context, conversation_key
|
||||
),
|
||||
update=update,
|
||||
name=f"ConversationHandler:{update.update_id}:handle_update:timeout_job",
|
||||
)
|
||||
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, handler)
|
||||
if raise_dp_handler_stop:
|
||||
raise ApplicationHandlerStop(self.map_to_parent.get(new_state))
|
||||
return self.map_to_parent.get(new_state)
|
||||
|
||||
if current_state != self.WAITING:
|
||||
self._update_state(new_state, conversation_key, handler)
|
||||
|
||||
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 ApplicationHandlerStop
|
||||
# Signals a possible parent conversation to stay in the current state
|
||||
return None
|
||||
|
||||
def _update_state(
|
||||
self, new_state: object, key: ConversationKey, handler: Optional[BaseHandler] = None
|
||||
) -> None:
|
||||
if new_state == self.END:
|
||||
if key in self._conversations:
|
||||
# If there is no key in conversations, nothing is done.
|
||||
del self._conversations[key]
|
||||
|
||||
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"{repr(handler.callback.__name__) if handler is not None else 'BaseHandler'} "
|
||||
f"returned state {new_state} which is unknown to the "
|
||||
f"ConversationHandler{' ' + self.name if self.name is not None else ''}.",
|
||||
stacklevel=2,
|
||||
)
|
||||
self._conversations[key] = new_state
|
||||
|
||||
async def _trigger_timeout(self, context: CCT) -> 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:`BaseHandler.check_update` returns
|
||||
:obj:`True` is handled.
|
||||
"""
|
||||
job = cast("Job", context.job)
|
||||
ctxt = cast("_ConversationTimeoutContext", job.data)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Conversation timeout was triggered for conversation %s!", ctxt.conversation_key
|
||||
)
|
||||
|
||||
callback_context = ctxt.callback_context
|
||||
|
||||
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:
|
||||
await handler.handle_update(
|
||||
ctxt.update, ctxt.application, check, callback_context
|
||||
)
|
||||
except ApplicationHandlerStop:
|
||||
warn(
|
||||
"ApplicationHandlerStop in TIMEOUT state of "
|
||||
"ConversationHandler has no effect. Ignoring.",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
self._update_state(self.END, ctxt.conversation_key)
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 InlineQueryHandler class."""
|
||||
import re
|
||||
from re import Match, Pattern
|
||||
from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union, cast
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import DVType
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Application
|
||||
|
||||
RT = TypeVar("RT")
|
||||
|
||||
|
||||
class InlineQueryHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""
|
||||
BaseHandler class to handle Telegram updates that contain a
|
||||
:attr:`telegram.Update.inline_query`.
|
||||
Optionally based on a regex. Read the documentation of the :mod:`re` module for more
|
||||
information.
|
||||
|
||||
Warning:
|
||||
* 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.
|
||||
|
||||
Examples:
|
||||
:any:`Inline Bot <examples.inlinebot>`
|
||||
|
||||
|
||||
Args:
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
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
|
||||
Attributes:
|
||||
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.
|
||||
|
||||
.. versionadded:: 13.5
|
||||
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__ = ("chat_types", "pattern")
|
||||
|
||||
def __init__(
|
||||
self: "InlineQueryHandler[CCT, RT]",
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
pattern: Optional[Union[str, Pattern[str]]] = None,
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
chat_types: Optional[list[str]] = None,
|
||||
):
|
||||
super().__init__(callback, block=block)
|
||||
|
||||
if isinstance(pattern, str):
|
||||
pattern = re.compile(pattern)
|
||||
|
||||
self.pattern: Optional[Union[str, Pattern[str]]] = pattern
|
||||
self.chat_types: Optional[list[str]] = chat_types
|
||||
|
||||
def check_update(self, update: object) -> Optional[Union[bool, Match[str]]]:
|
||||
"""
|
||||
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:`re.match`
|
||||
|
||||
"""
|
||||
if isinstance(update, Update) and update.inline_query:
|
||||
if (self.chat_types is not None) and (
|
||||
update.inline_query.chat_type not in self.chat_types
|
||||
):
|
||||
return False
|
||||
if self.pattern and (match := re.match(self.pattern, update.inline_query.query)):
|
||||
return match
|
||||
if not self.pattern:
|
||||
return True
|
||||
return None
|
||||
|
||||
def collect_additional_context(
|
||||
self,
|
||||
context: CCT,
|
||||
update: Update, # noqa: ARG002
|
||||
application: "Application[Any, CCT, Any, Any, Any, Any]", # noqa: ARG002
|
||||
check_result: Optional[Union[bool, Match[str]]],
|
||||
) -> None:
|
||||
"""Add the result of ``re.match(pattern, update.inline_query.query)`` to
|
||||
:attr:`CallbackContext.matches` as list with one element.
|
||||
"""
|
||||
if self.pattern:
|
||||
check_result = cast("Match", check_result)
|
||||
context.matches = [check_result]
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 MessageHandler class."""
|
||||
from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import DVType
|
||||
from telegram.ext import filters as filters_module
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Application
|
||||
|
||||
RT = TypeVar("RT")
|
||||
|
||||
|
||||
class MessageHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""Handler class to handle Telegram messages. They might contain text, media or status
|
||||
updates.
|
||||
|
||||
Warning:
|
||||
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:
|
||||
filters (:class:`telegram.ext.filters.BaseFilter`): 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). Passing :obj:`None` is a shortcut
|
||||
to passing :class:`telegram.ext.filters.ALL`.
|
||||
|
||||
.. seealso:: :wiki:`Advanced Filters <Extensions---Advanced-Filters>`
|
||||
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`.
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
|
||||
Attributes:
|
||||
filters (:class:`telegram.ext.filters.BaseFilter`): Only allow updates with these Filters.
|
||||
See :mod:`telegram.ext.filters` for a full list of all available filters.
|
||||
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`.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("filters",)
|
||||
|
||||
def __init__(
|
||||
self: "MessageHandler[CCT, RT]",
|
||||
filters: Optional[filters_module.BaseFilter],
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
super().__init__(callback, block=block)
|
||||
self.filters: filters_module.BaseFilter = (
|
||||
filters if filters is not None else filters_module.ALL
|
||||
)
|
||||
|
||||
def check_update(self, update: object) -> Optional[Union[bool, dict[str, list[Any]]]]:
|
||||
"""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`
|
||||
|
||||
"""
|
||||
if isinstance(update, Update):
|
||||
return self.filters.check_update(update) or False
|
||||
return None
|
||||
|
||||
def collect_additional_context(
|
||||
self,
|
||||
context: CCT,
|
||||
update: Update, # noqa: ARG002
|
||||
application: "Application[Any, CCT, Any, Any, Any, Any]", # noqa: ARG002
|
||||
check_result: Optional[Union[bool, dict[str, object]]],
|
||||
) -> None:
|
||||
"""Adds possible output of data filters to the :class:`CallbackContext`."""
|
||||
if isinstance(check_result, dict):
|
||||
context.update(check_result)
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 MessageReactionHandler class."""
|
||||
|
||||
from typing import Final, Optional
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import RT, SCT, DVType
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils._update_parsing import parse_chat_id, parse_username
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
|
||||
class MessageReactionHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""Handler class to handle Telegram updates that contain a message reaction.
|
||||
|
||||
Note:
|
||||
The following rules apply to both ``username`` and the ``chat_id`` param groups,
|
||||
respectively:
|
||||
|
||||
* If none of them are passed, the handler does not filter the update for that specific
|
||||
attribute.
|
||||
* If a chat ID **or** a username is passed, the updates will be filtered with that
|
||||
specific attribute.
|
||||
* If a chat ID **and** a username are passed, an update containing **any** of them will be
|
||||
filtered.
|
||||
* :attr:`telegram.MessageReactionUpdated.actor_chat` is *not* considered for
|
||||
:paramref:`user_id` and :paramref:`user_username` filtering.
|
||||
|
||||
Warning:
|
||||
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:: 20.8
|
||||
|
||||
Args:
|
||||
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`.
|
||||
message_reaction_types (:obj:`int`, optional): Pass one of
|
||||
:attr:`MESSAGE_REACTION_UPDATED`, :attr:`MESSAGE_REACTION_COUNT_UPDATED` or
|
||||
:attr:`MESSAGE_REACTION` to specify if this handler should handle only updates with
|
||||
:attr:`telegram.Update.message_reaction`,
|
||||
:attr:`telegram.Update.message_reaction_count` or both. Defaults to
|
||||
:attr:`MESSAGE_REACTION`.
|
||||
chat_id (:obj:`int` | Collection[:obj:`int`], optional): Filters reactions to allow
|
||||
only those which happen in the specified chat ID(s).
|
||||
chat_username (:obj:`str` | Collection[:obj:`str`], optional): Filters reactions to allow
|
||||
only those which happen in the specified username(s).
|
||||
user_id (:obj:`int` | Collection[:obj:`int`], optional): Filters reactions to allow
|
||||
only those which are set by the specified chat ID(s) (this can be the chat itself in
|
||||
the case of anonymous users, see the
|
||||
:paramref:`telegram.MessageReactionUpdated.actor_chat`).
|
||||
user_username (:obj:`str` | Collection[:obj:`str`], optional): Filters reactions to allow
|
||||
only those which are set by the specified username(s) (this can be the chat itself in
|
||||
the case of anonymous users, see the
|
||||
:paramref:`telegram.MessageReactionUpdated.actor_chat`).
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
|
||||
Attributes:
|
||||
callback (:term:`coroutine function`): The callback function for this handler.
|
||||
message_reaction_types (:obj:`int`): Optional. Specifies if this handler should handle only
|
||||
updates with :attr:`telegram.Update.message_reaction`,
|
||||
:attr:`telegram.Update.message_reaction_count` or both.
|
||||
block (:obj:`bool`): Determines whether the callback will run in a blocking way.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_chat_ids",
|
||||
"_chat_usernames",
|
||||
"_user_ids",
|
||||
"_user_usernames",
|
||||
"message_reaction_types",
|
||||
)
|
||||
|
||||
MESSAGE_REACTION_UPDATED: Final[int] = -1
|
||||
""":obj:`int`: Used as a constant to handle only :attr:`telegram.Update.message_reaction`."""
|
||||
MESSAGE_REACTION_COUNT_UPDATED: Final[int] = 0
|
||||
""":obj:`int`: Used as a constant to handle only
|
||||
:attr:`telegram.Update.message_reaction_count`."""
|
||||
MESSAGE_REACTION: Final[int] = 1
|
||||
""":obj:`int`: Used as a constant to handle both :attr:`telegram.Update.message_reaction`
|
||||
and :attr:`telegram.Update.message_reaction_count`."""
|
||||
|
||||
def __init__(
|
||||
self: "MessageReactionHandler[CCT, RT]",
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
chat_id: Optional[SCT[int]] = None,
|
||||
chat_username: Optional[SCT[str]] = None,
|
||||
user_id: Optional[SCT[int]] = None,
|
||||
user_username: Optional[SCT[str]] = None,
|
||||
message_reaction_types: int = MESSAGE_REACTION,
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
super().__init__(callback, block=block)
|
||||
self.message_reaction_types: int = message_reaction_types
|
||||
|
||||
self._chat_ids = parse_chat_id(chat_id)
|
||||
self._chat_usernames = parse_username(chat_username)
|
||||
if (user_id or user_username) and message_reaction_types in (
|
||||
self.MESSAGE_REACTION,
|
||||
self.MESSAGE_REACTION_COUNT_UPDATED,
|
||||
):
|
||||
raise ValueError(
|
||||
"You can not filter for users and include anonymous reactions. Set "
|
||||
"`message_reaction_types` to MESSAGE_REACTION_UPDATED."
|
||||
)
|
||||
self._user_ids = parse_chat_id(user_id)
|
||||
self._user_usernames = parse_username(user_username)
|
||||
|
||||
def check_update(self, update: object) -> bool:
|
||||
"""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`
|
||||
|
||||
"""
|
||||
if not isinstance(update, Update):
|
||||
return False
|
||||
|
||||
if not (update.message_reaction or update.message_reaction_count):
|
||||
return False
|
||||
|
||||
if (
|
||||
self.message_reaction_types == self.MESSAGE_REACTION_UPDATED
|
||||
and update.message_reaction_count
|
||||
):
|
||||
return False
|
||||
|
||||
if (
|
||||
self.message_reaction_types == self.MESSAGE_REACTION_COUNT_UPDATED
|
||||
and update.message_reaction
|
||||
):
|
||||
return False
|
||||
|
||||
if not any((self._chat_ids, self._chat_usernames, self._user_ids, self._user_usernames)):
|
||||
return True
|
||||
|
||||
# Extract chat and user IDs and usernames from the update for comparison
|
||||
chat_id = chat.id if (chat := update.effective_chat) else None
|
||||
chat_username = chat.username if chat else None
|
||||
user_id = user.id if (user := update.effective_user) else None
|
||||
user_username = user.username if user else None
|
||||
|
||||
return (
|
||||
bool(self._chat_ids and (chat_id in self._chat_ids))
|
||||
or bool(self._chat_usernames and (chat_username in self._chat_usernames))
|
||||
or bool(self._user_ids and (user_id in self._user_ids))
|
||||
or bool(self._user_usernames and (user_username in self._user_usernames))
|
||||
)
|
||||
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 PaidMediaPurchased class."""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import SCT, DVType
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils._update_parsing import parse_chat_id, parse_username
|
||||
from telegram.ext._utils.types import CCT, RT, HandlerCallback
|
||||
|
||||
|
||||
class PaidMediaPurchasedHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""Handler class to handle Telegram
|
||||
:attr:`purchased paid media <telegram.Update.purchased_paid_media>`.
|
||||
|
||||
.. versionadded:: 21.6
|
||||
|
||||
Args:
|
||||
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)
|
||||
user_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only
|
||||
those which are from the specified user ID(s).
|
||||
|
||||
username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only
|
||||
those which are from the specified username(s).
|
||||
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
Attributes:
|
||||
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`.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_user_ids",
|
||||
"_usernames",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self: "PaidMediaPurchasedHandler[CCT, RT]",
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
user_id: Optional[SCT[int]] = None,
|
||||
username: Optional[SCT[str]] = None,
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
super().__init__(callback, block=block)
|
||||
|
||||
self._user_ids = parse_chat_id(user_id)
|
||||
self._usernames = parse_username(username)
|
||||
|
||||
def check_update(self, update: object) -> bool:
|
||||
"""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`
|
||||
|
||||
"""
|
||||
if not isinstance(update, Update) or not update.purchased_paid_media:
|
||||
return False
|
||||
|
||||
if not self._user_ids and not self._usernames:
|
||||
return True
|
||||
if update.purchased_paid_media.from_user.id in self._user_ids:
|
||||
return True
|
||||
return update.purchased_paid_media.from_user.username in self._usernames
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 PollAnswerHandler class."""
|
||||
|
||||
|
||||
from telegram import Update
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils.types import CCT, RT
|
||||
|
||||
|
||||
class PollAnswerHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""Handler class to handle Telegram updates that contain a
|
||||
:attr:`poll answer <telegram.Update.poll_answer>`.
|
||||
|
||||
Warning:
|
||||
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.
|
||||
|
||||
Examples:
|
||||
:any:`Poll Bot <examples.pollbot>`
|
||||
|
||||
Args:
|
||||
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`.
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
|
||||
Attributes:
|
||||
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 handler's :attr:`callback`.
|
||||
|
||||
Args:
|
||||
update (:class:`telegram.Update` | :obj:`object`): Incoming update.
|
||||
|
||||
Returns:
|
||||
:obj:`bool`
|
||||
|
||||
"""
|
||||
return isinstance(update, Update) and bool(update.poll_answer)
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 PollHandler class."""
|
||||
|
||||
|
||||
from telegram import Update
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils.types import CCT, RT
|
||||
|
||||
|
||||
class PollHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""Handler class to handle Telegram updates that contain a
|
||||
:attr:`poll <telegram.Update.poll>`.
|
||||
|
||||
Warning:
|
||||
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.
|
||||
|
||||
Examples:
|
||||
:any:`Poll Bot <examples.pollbot>`
|
||||
|
||||
Args:
|
||||
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`.
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
|
||||
Attributes:
|
||||
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 handler's :attr:`callback`.
|
||||
|
||||
Args:
|
||||
update (:class:`telegram.Update` | :obj:`object`): Incoming update.
|
||||
|
||||
Returns:
|
||||
:obj:`bool`
|
||||
|
||||
"""
|
||||
return isinstance(update, Update) and bool(update.poll)
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 PreCheckoutQueryHandler class."""
|
||||
|
||||
|
||||
import re
|
||||
from re import Pattern
|
||||
from typing import Optional, TypeVar, Union
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import DVType
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
RT = TypeVar("RT")
|
||||
|
||||
|
||||
class PreCheckoutQueryHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""Handler class to handle Telegram :attr:`telegram.Update.pre_checkout_query`.
|
||||
|
||||
Warning:
|
||||
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.
|
||||
|
||||
Examples:
|
||||
:any:`Payment Bot <examples.paymentbot>`
|
||||
|
||||
Args:
|
||||
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`.
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
pattern (:obj:`str` | :func:`re.Pattern <re.compile>`, optional): Optional. Regex pattern
|
||||
to test :attr:`telegram.PreCheckoutQuery.invoice_payload` against.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
Attributes:
|
||||
callback (:term:`coroutine function`): The callback function for this handler.
|
||||
block (:obj:`bool`): Determines whether the callback will run in a blocking way..
|
||||
pattern (:obj:`str` | :func:`re.Pattern <re.compile>`, optional): Optional. Regex pattern
|
||||
to test :attr:`telegram.PreCheckoutQuery.invoice_payload` against.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("pattern",)
|
||||
|
||||
def __init__(
|
||||
self: "PreCheckoutQueryHandler[CCT, RT]",
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
pattern: Optional[Union[str, Pattern[str]]] = None,
|
||||
):
|
||||
super().__init__(callback, block=block)
|
||||
|
||||
self.pattern: Optional[Pattern[str]] = re.compile(pattern) if pattern is not None else None
|
||||
|
||||
def check_update(self, update: object) -> bool:
|
||||
"""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`
|
||||
|
||||
"""
|
||||
if isinstance(update, Update) and update.pre_checkout_query:
|
||||
invoice_payload = update.pre_checkout_query.invoice_payload
|
||||
if self.pattern:
|
||||
if self.pattern.match(invoice_payload):
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 PrefixHandler class."""
|
||||
import itertools
|
||||
from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import SCT, DVType
|
||||
from telegram.ext import filters as filters_module
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Application
|
||||
|
||||
RT = TypeVar("RT")
|
||||
|
||||
|
||||
class PrefixHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""Handler class to handle custom prefix commands.
|
||||
|
||||
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 :paramref:`prefix` and :paramref:`command`.
|
||||
It will add a :obj:`list` to the :class:`CallbackContext` named :attr:`CallbackContext.args`,
|
||||
containing a list of strings, which is the text following the command split on single or
|
||||
consecutive whitespace characters.
|
||||
|
||||
Examples:
|
||||
|
||||
Single prefix and command:
|
||||
|
||||
.. code:: python
|
||||
|
||||
PrefixHandler("!", "test", callback) # will respond to '!test'.
|
||||
|
||||
Multiple prefixes, single command:
|
||||
|
||||
.. code:: python
|
||||
|
||||
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'.
|
||||
|
||||
|
||||
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:`block` to :obj:`False`, you cannot rely on adding custom
|
||||
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
|
||||
* :class:`PrefixHandler` is no longer a subclass of :class:`CommandHandler`.
|
||||
* Removed the attributes ``command`` and ``prefix``. Instead, the new :attr:`commands`
|
||||
contains all commands that this handler listens to as a :class:`frozenset`, which
|
||||
includes the prefixes.
|
||||
* Updating the prefixes and commands this handler listens to is no longer possible.
|
||||
|
||||
Args:
|
||||
prefix (:obj:`str` | Collection[:obj:`str`]):
|
||||
The prefix(es) that will precede :paramref:`command`.
|
||||
command (:obj:`str` | Collection[:obj:`str`]):
|
||||
The command or list of commands this handler should listen for. Case-insensitive.
|
||||
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 :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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
|
||||
Attributes:
|
||||
commands (frozenset[:obj:`str`]): The commands that this handler will listen for, i.e. the
|
||||
combinations of :paramref:`prefix` and :paramref:`command`.
|
||||
callback (:term:`coroutine function`): The callback function for this handler.
|
||||
filters (:class:`telegram.ext.filters.BaseFilter`): Optional. Only allow updates with these
|
||||
Filters.
|
||||
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`.
|
||||
|
||||
"""
|
||||
|
||||
# 'prefix' is a class property, & 'command' is included in the superclass, so they're left out.
|
||||
__slots__ = ("commands", "filters")
|
||||
|
||||
def __init__(
|
||||
self: "PrefixHandler[CCT, RT]",
|
||||
prefix: SCT[str],
|
||||
command: SCT[str],
|
||||
callback: HandlerCallback[Update, CCT, RT],
|
||||
filters: Optional[filters_module.BaseFilter] = None,
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
super().__init__(callback=callback, block=block)
|
||||
|
||||
prefixes = {prefix.lower()} if isinstance(prefix, str) else {x.lower() for x in prefix}
|
||||
|
||||
commands = {command.lower()} if isinstance(command, str) else {x.lower() for x in command}
|
||||
|
||||
self.commands: frozenset[str] = frozenset(
|
||||
p + c for p, c in itertools.product(prefixes, commands)
|
||||
)
|
||||
self.filters: filters_module.BaseFilter = (
|
||||
filters if filters is not None else filters_module.UpdateType.MESSAGES
|
||||
)
|
||||
|
||||
def check_update(
|
||||
self, update: object
|
||||
) -> Optional[Union[bool, tuple[list[str], Optional[Union[bool, dict[Any, Any]]]]]]:
|
||||
"""Determines whether an update should be passed to this handler's :attr:`callback`.
|
||||
|
||||
Args:
|
||||
update (:class:`telegram.Update` | :obj:`object`): Incoming update.
|
||||
|
||||
Returns:
|
||||
:obj:`list`: The list of args for the handler.
|
||||
|
||||
"""
|
||||
if isinstance(update, Update) and update.effective_message:
|
||||
message = update.effective_message
|
||||
|
||||
if message.text:
|
||||
text_list = message.text.split()
|
||||
if text_list[0].lower() not in self.commands:
|
||||
return None
|
||||
filter_result = self.filters.check_update(update)
|
||||
if filter_result:
|
||||
return text_list[1:], filter_result
|
||||
return False
|
||||
return None
|
||||
|
||||
def collect_additional_context(
|
||||
self,
|
||||
context: CCT,
|
||||
update: Update, # noqa: ARG002
|
||||
application: "Application[Any, CCT, Any, Any, Any, Any]", # noqa: ARG002
|
||||
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
|
||||
whitespaces and add output of data filters to :attr:`CallbackContext` as well.
|
||||
"""
|
||||
if isinstance(check_result, tuple):
|
||||
context.args = check_result[0]
|
||||
if isinstance(check_result[1], dict):
|
||||
context.update(check_result[1])
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 ShippingQueryHandler class."""
|
||||
|
||||
|
||||
from telegram import Update
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils.types import CCT, RT
|
||||
|
||||
|
||||
class ShippingQueryHandler(BaseHandler[Update, CCT, RT]):
|
||||
"""Handler class to handle Telegram :attr:`telegram.Update.shipping_query`.
|
||||
|
||||
Warning:
|
||||
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.
|
||||
|
||||
Examples:
|
||||
:any:`Payment Bot <examples.paymentbot>`
|
||||
|
||||
Args:
|
||||
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`.
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
|
||||
Attributes:
|
||||
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 handler's :attr:`callback`.
|
||||
|
||||
Args:
|
||||
update (:class:`telegram.Update` | :obj:`object`): Incoming update.
|
||||
|
||||
Returns:
|
||||
:obj:`bool`
|
||||
|
||||
"""
|
||||
return isinstance(update, Update) and bool(update.shipping_query)
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 StringCommandHandler class."""
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import DVType
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils.types import CCT, RT, HandlerCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Application
|
||||
|
||||
|
||||
class StringCommandHandler(BaseHandler[str, CCT, RT]):
|
||||
"""Handler class to handle string commands. Commands are string updates that start with
|
||||
``/``. 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 whitespace characters.
|
||||
|
||||
Note:
|
||||
This handler is not used to handle Telegram :class:`telegram.Update`, but strings manually
|
||||
put in the queue. For example to send messages with the bot using command line or API.
|
||||
|
||||
Warning:
|
||||
When setting :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`): The command this handler should listen for.
|
||||
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: str, context: CallbackContext)
|
||||
|
||||
The return value of the callback is usually ignored except for the special case of
|
||||
:class:`telegram.ext.ConversationHandler`.
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
|
||||
Attributes:
|
||||
command (:obj:`str`): The command this handler should listen for.
|
||||
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`.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("command",)
|
||||
|
||||
def __init__(
|
||||
self: "StringCommandHandler[CCT, RT]",
|
||||
command: str,
|
||||
callback: HandlerCallback[str, CCT, RT],
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
super().__init__(callback, block=block)
|
||||
self.command: str = command
|
||||
|
||||
def check_update(self, update: object) -> Optional[list[str]]:
|
||||
"""Determines whether an update should be passed to this handler's :attr:`callback`.
|
||||
|
||||
Args:
|
||||
update (:obj:`object`): The incoming update.
|
||||
|
||||
Returns:
|
||||
list[:obj:`str`]: List containing the text command split on whitespace.
|
||||
|
||||
"""
|
||||
if isinstance(update, str) and update.startswith("/"):
|
||||
args = update[1:].split(" ")
|
||||
if args[0] == self.command:
|
||||
return args[1:]
|
||||
return None
|
||||
|
||||
def collect_additional_context(
|
||||
self,
|
||||
context: CCT,
|
||||
update: str, # noqa: ARG002
|
||||
application: "Application[Any, CCT, Any, Any, Any, Any]", # noqa: ARG002
|
||||
check_result: Optional[list[str]],
|
||||
) -> None:
|
||||
"""Add text after the command to :attr:`CallbackContext.args` as list, split on single
|
||||
whitespaces.
|
||||
"""
|
||||
context.args = check_result
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 StringRegexHandler class."""
|
||||
|
||||
import re
|
||||
from re import Match, Pattern
|
||||
from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
|
||||
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import DVType
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram.ext import Application
|
||||
|
||||
RT = TypeVar("RT")
|
||||
|
||||
|
||||
class StringRegexHandler(BaseHandler[str, CCT, RT]):
|
||||
"""Handler class to handle string updates based on a regex which checks the update content.
|
||||
|
||||
Read the documentation of the :mod:`re` module for more information. The :func:`re.match`
|
||||
function is used to determine if an update should be handled by this handler.
|
||||
|
||||
Note:
|
||||
This handler is not used to handle Telegram :class:`telegram.Update`, but strings manually
|
||||
put in the queue. For example to send messages with the bot using command line or API.
|
||||
|
||||
Warning:
|
||||
When setting :paramref:`block` to :obj:`False`, you cannot rely on adding custom
|
||||
attributes to :class:`telegram.ext.CallbackContext`. See its docs for more info.
|
||||
|
||||
Args:
|
||||
pattern (:obj:`str` | :func:`re.Pattern <re.compile>`): The regex pattern.
|
||||
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: str, context: CallbackContext)
|
||||
|
||||
The return value of the callback is usually ignored except for the special case of
|
||||
:class:`telegram.ext.ConversationHandler`.
|
||||
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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
|
||||
Attributes:
|
||||
pattern (:obj:`str` | :func:`re.Pattern <re.compile>`): The regex pattern.
|
||||
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`.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("pattern",)
|
||||
|
||||
def __init__(
|
||||
self: "StringRegexHandler[CCT, RT]",
|
||||
pattern: Union[str, Pattern[str]],
|
||||
callback: HandlerCallback[str, CCT, RT],
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
super().__init__(callback, block=block)
|
||||
|
||||
if isinstance(pattern, str):
|
||||
pattern = re.compile(pattern)
|
||||
|
||||
self.pattern: Union[str, Pattern[str]] = pattern
|
||||
|
||||
def check_update(self, update: object) -> Optional[Match[str]]:
|
||||
"""Determines whether an update should be passed to this handler's :attr:`callback`.
|
||||
|
||||
Args:
|
||||
update (:obj:`object`): The incoming update.
|
||||
|
||||
Returns:
|
||||
:obj:`None` | :obj:`re.match`
|
||||
|
||||
"""
|
||||
if isinstance(update, str) and (match := re.match(self.pattern, update)):
|
||||
return match
|
||||
return None
|
||||
|
||||
def collect_additional_context(
|
||||
self,
|
||||
context: CCT,
|
||||
update: str, # noqa: ARG002
|
||||
application: "Application[Any, CCT, Any, Any, Any, Any]", # noqa: ARG002
|
||||
check_result: Optional[Match[str]],
|
||||
) -> None:
|
||||
"""Add the result of ``re.match(pattern, update)`` to :attr:`CallbackContext.matches` as
|
||||
list with one element.
|
||||
"""
|
||||
if self.pattern and check_result:
|
||||
context.matches = [check_result]
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 TypeHandler class."""
|
||||
|
||||
from typing import Optional, TypeVar
|
||||
|
||||
from telegram._utils.defaultvalue import DEFAULT_TRUE
|
||||
from telegram._utils.types import DVType
|
||||
from telegram.ext._handlers.basehandler import BaseHandler
|
||||
from telegram.ext._utils.types import CCT, HandlerCallback
|
||||
|
||||
RT = TypeVar("RT")
|
||||
UT = TypeVar("UT")
|
||||
# If this is written directly next to the type variable mypy gets confused with [valid-type]. This
|
||||
# could be reported to them, but I doubt they would change this since we override a builtin type
|
||||
GenericUT = type[UT]
|
||||
|
||||
|
||||
class TypeHandler(BaseHandler[UT, CCT, RT]):
|
||||
"""Handler class to handle updates of custom types.
|
||||
|
||||
Warning:
|
||||
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:
|
||||
type (:external:class:`type`): The :external:class:`type` of updates this handler should
|
||||
process, as determined by :obj:`isinstance`
|
||||
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: object, context: CallbackContext)
|
||||
|
||||
The return value of the callback is usually ignored except for the special case of
|
||||
:class:`telegram.ext.ConversationHandler`.
|
||||
strict (:obj:`bool`, optional): Use ``type`` instead of :obj:`isinstance`.
|
||||
Default is :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`.
|
||||
|
||||
.. seealso:: :wiki:`Concurrency`
|
||||
|
||||
Attributes:
|
||||
type (:external:class:`type`): The :external:class:`type` of updates this handler should
|
||||
process.
|
||||
callback (:term:`coroutine function`): The callback function for this handler.
|
||||
strict (:obj:`bool`): Use :external:class:`type` instead of :obj:`isinstance`. Default is
|
||||
:obj:`False`.
|
||||
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__ = ("strict", "type")
|
||||
|
||||
def __init__(
|
||||
self: "TypeHandler[UT, CCT, RT]",
|
||||
type: GenericUT[UT], # pylint: disable=redefined-builtin
|
||||
callback: HandlerCallback[UT, CCT, RT],
|
||||
strict: bool = False,
|
||||
block: DVType[bool] = DEFAULT_TRUE,
|
||||
):
|
||||
super().__init__(callback, block=block)
|
||||
self.type: GenericUT[UT] = type
|
||||
self.strict: Optional[bool] = strict
|
||||
|
||||
def check_update(self, update: object) -> bool:
|
||||
"""Determines whether an update should be passed to this handler's :attr:`callback`.
|
||||
|
||||
Args:
|
||||
update (:obj:`object`): Incoming update.
|
||||
|
||||
Returns:
|
||||
:obj:`bool`
|
||||
|
||||
"""
|
||||
if not self.strict:
|
||||
return isinstance(update, self.type)
|
||||
return type(update) is self.type # pylint: disable=unidiomatic-typecheck
|
||||
1013
.venv/lib/python3.12/site-packages/telegram/ext/_jobqueue.py
Normal file
1013
.venv/lib/python3.12/site-packages/telegram/ext/_jobqueue.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,562 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains the PicklePersistence class."""
|
||||
import pickle
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Optional, TypeVar, Union, cast, overload
|
||||
|
||||
from telegram import Bot, TelegramObject
|
||||
from telegram._utils.types import FilePathInput
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.ext import BasePersistence, PersistenceInput
|
||||
from telegram.ext._contexttypes import ContextTypes
|
||||
from telegram.ext._utils.types import BD, CD, UD, CDCData, ConversationDict, ConversationKey
|
||||
|
||||
_REPLACED_KNOWN_BOT = "a known bot replaced by PTB's PicklePersistence"
|
||||
_REPLACED_UNKNOWN_BOT = "an unknown bot replaced by PTB's PicklePersistence"
|
||||
|
||||
TelegramObj = TypeVar("TelegramObj", bound=TelegramObject)
|
||||
|
||||
|
||||
def _all_subclasses(cls: type[TelegramObj]) -> set[type[TelegramObj]]:
|
||||
"""Gets all subclasses of the specified object, recursively. from
|
||||
https://stackoverflow.com/a/3862957/9706202
|
||||
"""
|
||||
subclasses = cls.__subclasses__()
|
||||
return set(subclasses).union([s for c in subclasses for s in _all_subclasses(c)])
|
||||
|
||||
|
||||
def _reconstruct_to(cls: type[TelegramObj], kwargs: dict) -> TelegramObj:
|
||||
"""
|
||||
This method is used for unpickling. The data, which is in the form a dictionary, is
|
||||
converted back into a class. Works mostly the same as :meth:`TelegramObject.__setstate__`.
|
||||
This function should be kept in place for backwards compatibility even if the pickling logic
|
||||
is changed, since `_custom_reduction` places references to this function into the pickled data.
|
||||
"""
|
||||
obj = cls.__new__(cls)
|
||||
obj.__setstate__(kwargs)
|
||||
return obj
|
||||
|
||||
|
||||
def _custom_reduction(cls: TelegramObj) -> tuple[Callable, tuple[type[TelegramObj], dict]]:
|
||||
"""
|
||||
This method is used for pickling. The bot attribute is preserved so _BotPickler().persistent_id
|
||||
works as intended.
|
||||
"""
|
||||
data = cls._get_attrs(include_private=True) # pylint: disable=protected-access
|
||||
# MappingProxyType is not pickable, so we convert it to a dict
|
||||
# no need to convert back to MPT in _reconstruct_to, since it's done in __setstate__
|
||||
data["api_kwargs"] = dict(data["api_kwargs"]) # type: ignore[arg-type]
|
||||
return _reconstruct_to, (cls.__class__, data)
|
||||
|
||||
|
||||
class _BotPickler(pickle.Pickler):
|
||||
__slots__ = ("_bot",)
|
||||
|
||||
def __init__(self, bot: Bot, *args: Any, **kwargs: Any):
|
||||
self._bot = bot
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def reducer_override(
|
||||
self, obj: TelegramObj
|
||||
) -> tuple[Callable, tuple[type[TelegramObj], dict]]:
|
||||
"""
|
||||
This method is used for pickling. The bot attribute is preserved so
|
||||
_BotPickler().persistent_id works as intended.
|
||||
"""
|
||||
if not isinstance(obj, TelegramObject):
|
||||
return NotImplemented
|
||||
|
||||
return _custom_reduction(obj)
|
||||
|
||||
def persistent_id(self, obj: object) -> Optional[str]:
|
||||
"""Used to 'mark' the Bot, so it can be replaced later. See
|
||||
https://docs.python.org/3/library/pickle.html#pickle.Pickler.persistent_id for more info
|
||||
"""
|
||||
if obj is self._bot:
|
||||
return _REPLACED_KNOWN_BOT
|
||||
if isinstance(obj, Bot):
|
||||
warn(
|
||||
"Unknown bot instance found. Will be replaced by `None` during unpickling",
|
||||
stacklevel=2,
|
||||
)
|
||||
return _REPLACED_UNKNOWN_BOT
|
||||
return None # pickles as usual
|
||||
|
||||
|
||||
class _BotUnpickler(pickle.Unpickler):
|
||||
__slots__ = ("_bot",)
|
||||
|
||||
def __init__(self, bot: Bot, *args: Any, **kwargs: Any):
|
||||
self._bot = bot
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def persistent_load(self, pid: str) -> Optional[Bot]:
|
||||
"""Replaces the bot with the current bot if known, else it is replaced by :obj:`None`."""
|
||||
if pid == _REPLACED_KNOWN_BOT:
|
||||
return self._bot
|
||||
if pid == _REPLACED_UNKNOWN_BOT:
|
||||
return None
|
||||
raise pickle.UnpicklingError("Found unknown persistent id when unpickling!")
|
||||
|
||||
|
||||
class PicklePersistence(BasePersistence[UD, CD, BD]):
|
||||
"""Using python's builtin :mod:`pickle` for making your bot persistent.
|
||||
|
||||
Attention:
|
||||
The interface provided by this class is intended to be accessed exclusively by
|
||||
:class:`~telegram.ext.Application`. Calling any of the methods below manually might
|
||||
interfere with the integration of persistence into :class:`~telegram.ext.Application`.
|
||||
|
||||
Note:
|
||||
This implementation of :class:`BasePersistence` uses the functionality of the pickle module
|
||||
to support serialization of bot instances. Specifically any reference to
|
||||
:attr:`~BasePersistence.bot` will be replaced by a placeholder before pickling and
|
||||
:attr:`~BasePersistence.bot` will be inserted back when loading the data.
|
||||
|
||||
Examples:
|
||||
:any:`Persistent Conversation Bot <examples.persistentconversationbot>`
|
||||
|
||||
.. seealso:: :wiki:`Making Your Bot Persistent <Making-your-bot-persistent>`
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
|
||||
* The parameters and attributes ``store_*_data`` were replaced by :attr:`store_data`.
|
||||
* The parameter and attribute ``filename`` were replaced by :attr:`filepath`.
|
||||
* :attr:`filepath` now also accepts :obj:`pathlib.Path` as argument.
|
||||
|
||||
Args:
|
||||
filepath (:obj:`str` | :obj:`pathlib.Path`): The filepath for storing the pickle files.
|
||||
When :attr:`single_file` is :obj:`False` this will be used as a prefix.
|
||||
store_data (:class:`~telegram.ext.PersistenceInput`, optional): Specifies which kinds of
|
||||
data will be saved by this persistence instance. By default, all available kinds of
|
||||
data will be saved.
|
||||
single_file (:obj:`bool`, optional): When :obj:`False` will store 5 separate files of
|
||||
`filename_user_data`, `filename_bot_data`, `filename_chat_data`,
|
||||
`filename_callback_data` and `filename_conversations`. Default is :obj:`True`.
|
||||
on_flush (:obj:`bool`, optional): When :obj:`True` will only save to file when
|
||||
:meth:`flush` is called and keep data in memory until that happens. When
|
||||
:obj:`False` will store data on any transaction *and* on call to :meth:`flush`.
|
||||
Default is :obj:`False`.
|
||||
context_types (:class:`telegram.ext.ContextTypes`, optional): Pass an instance
|
||||
of :class:`telegram.ext.ContextTypes` to customize the types used in the
|
||||
``context`` interface. If not passed, the defaults documented in
|
||||
:class:`telegram.ext.ContextTypes` will be used.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
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:: 20.0
|
||||
Attributes:
|
||||
filepath (:obj:`str` | :obj:`pathlib.Path`): The filepath for storing the pickle files.
|
||||
When :attr:`single_file` is :obj:`False` this will be used as a prefix.
|
||||
store_data (:class:`~telegram.ext.PersistenceInput`): Specifies which kinds of data will
|
||||
be saved by this persistence instance.
|
||||
single_file (:obj:`bool`): Optional. When :obj:`False` will store 5 separate files of
|
||||
`filename_user_data`, `filename_bot_data`, `filename_chat_data`,
|
||||
`filename_callback_data` and `filename_conversations`. Default is :obj:`True`.
|
||||
on_flush (:obj:`bool`): Optional. When :obj:`True` will only save to file when
|
||||
:meth:`flush` is called and keep data in memory until that happens. When
|
||||
:obj:`False` will store data on any transaction *and* on call to :meth:`flush`.
|
||||
Default is :obj:`False`.
|
||||
context_types (:class:`telegram.ext.ContextTypes`): Container for the types used
|
||||
in the ``context`` interface.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"bot_data",
|
||||
"callback_data",
|
||||
"chat_data",
|
||||
"context_types",
|
||||
"conversations",
|
||||
"filepath",
|
||||
"on_flush",
|
||||
"single_file",
|
||||
"user_data",
|
||||
)
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "PicklePersistence[dict[Any, Any], dict[Any, Any], dict[Any, Any]]",
|
||||
filepath: FilePathInput,
|
||||
store_data: Optional[PersistenceInput] = None,
|
||||
single_file: bool = True,
|
||||
on_flush: bool = False,
|
||||
update_interval: float = 60,
|
||||
): ...
|
||||
|
||||
@overload
|
||||
def __init__(
|
||||
self: "PicklePersistence[UD, CD, BD]",
|
||||
filepath: FilePathInput,
|
||||
store_data: Optional[PersistenceInput] = None,
|
||||
single_file: bool = True,
|
||||
on_flush: bool = False,
|
||||
update_interval: float = 60,
|
||||
context_types: Optional[ContextTypes[Any, UD, CD, BD]] = None,
|
||||
): ...
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
filepath: FilePathInput,
|
||||
store_data: Optional[PersistenceInput] = None,
|
||||
single_file: bool = True,
|
||||
on_flush: bool = False,
|
||||
update_interval: float = 60,
|
||||
context_types: Optional[ContextTypes[Any, UD, CD, BD]] = None,
|
||||
):
|
||||
super().__init__(store_data=store_data, update_interval=update_interval)
|
||||
self.filepath: Path = Path(filepath)
|
||||
self.single_file: Optional[bool] = single_file
|
||||
self.on_flush: Optional[bool] = on_flush
|
||||
self.user_data: Optional[dict[int, UD]] = None
|
||||
self.chat_data: Optional[dict[int, CD]] = None
|
||||
self.bot_data: Optional[BD] = None
|
||||
self.callback_data: Optional[CDCData] = None
|
||||
self.conversations: Optional[dict[str, dict[tuple[Union[int, str], ...], object]]] = None
|
||||
self.context_types: ContextTypes[Any, UD, CD, BD] = cast(
|
||||
"ContextTypes[Any, UD, CD, BD]", context_types or ContextTypes()
|
||||
)
|
||||
|
||||
def _load_singlefile(self) -> None:
|
||||
try:
|
||||
with self.filepath.open("rb") as file:
|
||||
data = _BotUnpickler(self.bot, file).load()
|
||||
|
||||
self.user_data = data["user_data"]
|
||||
self.chat_data = data["chat_data"]
|
||||
# For backwards compatibility with files not containing bot data
|
||||
self.bot_data = data.get("bot_data", self.context_types.bot_data())
|
||||
self.callback_data = data.get("callback_data", {})
|
||||
self.conversations = data["conversations"]
|
||||
except OSError:
|
||||
self.conversations = {}
|
||||
self.user_data = {}
|
||||
self.chat_data = {}
|
||||
self.bot_data = self.context_types.bot_data()
|
||||
self.callback_data = None
|
||||
except pickle.UnpicklingError as exc:
|
||||
filename = self.filepath.name
|
||||
raise TypeError(f"File {filename} does not contain valid pickle data") from exc
|
||||
except Exception as exc:
|
||||
raise TypeError(f"Something went wrong unpickling {self.filepath.name}") from exc
|
||||
|
||||
def _load_file(self, filepath: Path) -> Any:
|
||||
try:
|
||||
with filepath.open("rb") as file:
|
||||
return _BotUnpickler(self.bot, file).load()
|
||||
|
||||
except OSError:
|
||||
return None
|
||||
except pickle.UnpicklingError as exc:
|
||||
raise TypeError(f"File {filepath.name} does not contain valid pickle data") from exc
|
||||
except Exception as exc:
|
||||
raise TypeError(f"Something went wrong unpickling {filepath.name}") from exc
|
||||
|
||||
def _dump_singlefile(self) -> None:
|
||||
data = {
|
||||
"conversations": self.conversations,
|
||||
"user_data": self.user_data,
|
||||
"chat_data": self.chat_data,
|
||||
"bot_data": self.bot_data,
|
||||
"callback_data": self.callback_data,
|
||||
}
|
||||
with self.filepath.open("wb") as file:
|
||||
_BotPickler(self.bot, file, protocol=pickle.HIGHEST_PROTOCOL).dump(data)
|
||||
|
||||
def _dump_file(self, filepath: Path, data: object) -> None:
|
||||
with filepath.open("wb") as file:
|
||||
_BotPickler(self.bot, file, protocol=pickle.HIGHEST_PROTOCOL).dump(data)
|
||||
|
||||
async def get_user_data(self) -> dict[int, UD]:
|
||||
"""Returns the user_data from the pickle file if it exists or an empty :obj:`dict`.
|
||||
|
||||
Returns:
|
||||
dict[:obj:`int`, :obj:`dict`]: The restored user data.
|
||||
"""
|
||||
if self.user_data:
|
||||
pass
|
||||
elif not self.single_file:
|
||||
data = self._load_file(Path(f"{self.filepath}_user_data"))
|
||||
if not data:
|
||||
data = {}
|
||||
self.user_data = data
|
||||
else:
|
||||
self._load_singlefile()
|
||||
return deepcopy(self.user_data) # type: ignore[arg-type]
|
||||
|
||||
async def get_chat_data(self) -> dict[int, CD]:
|
||||
"""Returns the chat_data from the pickle file if it exists or an empty :obj:`dict`.
|
||||
|
||||
Returns:
|
||||
dict[:obj:`int`, :obj:`dict`]: The restored chat data.
|
||||
"""
|
||||
if self.chat_data:
|
||||
pass
|
||||
elif not self.single_file:
|
||||
data = self._load_file(Path(f"{self.filepath}_chat_data"))
|
||||
if not data:
|
||||
data = {}
|
||||
self.chat_data = data
|
||||
else:
|
||||
self._load_singlefile()
|
||||
return deepcopy(self.chat_data) # type: ignore[arg-type]
|
||||
|
||||
async def get_bot_data(self) -> BD:
|
||||
"""Returns the bot_data from the pickle file if it exists or an empty object of type
|
||||
:obj:`dict` | :attr:`telegram.ext.ContextTypes.bot_data`.
|
||||
|
||||
Returns:
|
||||
:obj:`dict` | :attr:`telegram.ext.ContextTypes.bot_data`: The restored bot data.
|
||||
"""
|
||||
if self.bot_data:
|
||||
pass
|
||||
elif not self.single_file:
|
||||
data = self._load_file(Path(f"{self.filepath}_bot_data"))
|
||||
if not data:
|
||||
data = self.context_types.bot_data()
|
||||
self.bot_data = data
|
||||
else:
|
||||
self._load_singlefile()
|
||||
return deepcopy(self.bot_data) # type: ignore[return-value]
|
||||
|
||||
async def get_callback_data(self) -> Optional[CDCData]:
|
||||
"""Returns the callback data from the pickle file if it exists or :obj:`None`.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
Returns:
|
||||
tuple[list[tuple[:obj:`str`, :obj:`float`, dict[:obj:`str`, :class:`object`]]],
|
||||
dict[:obj:`str`, :obj:`str`]] | :obj:`None`: The restored metadata or :obj:`None`,
|
||||
if no data was stored.
|
||||
"""
|
||||
if self.callback_data:
|
||||
pass
|
||||
elif not self.single_file:
|
||||
data = self._load_file(Path(f"{self.filepath}_callback_data"))
|
||||
if not data:
|
||||
data = None
|
||||
self.callback_data = data
|
||||
else:
|
||||
self._load_singlefile()
|
||||
if self.callback_data is None:
|
||||
return None
|
||||
return deepcopy(self.callback_data)
|
||||
|
||||
async def get_conversations(self, name: str) -> ConversationDict:
|
||||
"""Returns the conversations from the pickle file if it exists or an empty dict.
|
||||
|
||||
Args:
|
||||
name (:obj:`str`): The handlers name.
|
||||
|
||||
Returns:
|
||||
:obj:`dict`: The restored conversations for the handler.
|
||||
"""
|
||||
if self.conversations:
|
||||
pass
|
||||
elif not self.single_file:
|
||||
data = self._load_file(Path(f"{self.filepath}_conversations"))
|
||||
if not data:
|
||||
data = {name: {}}
|
||||
self.conversations = data
|
||||
else:
|
||||
self._load_singlefile()
|
||||
return self.conversations.get(name, {}).copy() # type: ignore[union-attr]
|
||||
|
||||
async def update_conversation(
|
||||
self, name: str, key: ConversationKey, new_state: Optional[object]
|
||||
) -> None:
|
||||
"""Will update the conversations for the given handler and depending on :attr:`on_flush`
|
||||
save the pickle file.
|
||||
|
||||
Args:
|
||||
name (:obj:`str`): The handler's name.
|
||||
key (:obj:`tuple`): The key the state is changed for.
|
||||
new_state (:class:`object`): The new state for the given key.
|
||||
"""
|
||||
if not self.conversations:
|
||||
self.conversations = {}
|
||||
if self.conversations.setdefault(name, {}).get(key) == new_state:
|
||||
return
|
||||
self.conversations[name][key] = new_state
|
||||
if not self.on_flush:
|
||||
if not self.single_file:
|
||||
self._dump_file(Path(f"{self.filepath}_conversations"), self.conversations)
|
||||
else:
|
||||
self._dump_singlefile()
|
||||
|
||||
async def update_user_data(self, user_id: int, data: UD) -> None:
|
||||
"""Will update the user_data and depending on :attr:`on_flush` save the pickle file.
|
||||
|
||||
Args:
|
||||
user_id (:obj:`int`): The user the data might have been changed for.
|
||||
data (:obj:`dict`): The :attr:`telegram.ext.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] = data
|
||||
if not self.on_flush:
|
||||
if not self.single_file:
|
||||
self._dump_file(Path(f"{self.filepath}_user_data"), self.user_data)
|
||||
else:
|
||||
self._dump_singlefile()
|
||||
|
||||
async def update_chat_data(self, chat_id: int, data: CD) -> None:
|
||||
"""Will update the chat_data and depending on :attr:`on_flush` save the pickle file.
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int`): The chat the data might have been changed for.
|
||||
data (:obj:`dict`): The :attr:`telegram.ext.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] = data
|
||||
if not self.on_flush:
|
||||
if not self.single_file:
|
||||
self._dump_file(Path(f"{self.filepath}_chat_data"), self.chat_data)
|
||||
else:
|
||||
self._dump_singlefile()
|
||||
|
||||
async def update_bot_data(self, data: BD) -> None:
|
||||
"""Will update the bot_data and depending on :attr:`on_flush` save the pickle file.
|
||||
|
||||
Args:
|
||||
data (:obj:`dict` | :attr:`telegram.ext.ContextTypes.bot_data`): The
|
||||
:attr:`telegram.ext.Application.bot_data`.
|
||||
"""
|
||||
if self.bot_data == data:
|
||||
return
|
||||
self.bot_data = data
|
||||
if not self.on_flush:
|
||||
if not self.single_file:
|
||||
self._dump_file(Path(f"{self.filepath}_bot_data"), self.bot_data)
|
||||
else:
|
||||
self._dump_singlefile()
|
||||
|
||||
async def update_callback_data(self, data: CDCData) -> None:
|
||||
"""Will update the callback_data (if changed) and depending on :attr:`on_flush` save the
|
||||
pickle file.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
Args:
|
||||
data (tuple[list[tuple[:obj:`str`, :obj:`float`, \
|
||||
dict[:obj:`str`, :class:`object`]]], dict[:obj:`str`, :obj:`str`]]):
|
||||
The relevant data to restore :class:`telegram.ext.CallbackDataCache`.
|
||||
"""
|
||||
if self.callback_data == data:
|
||||
return
|
||||
self.callback_data = data
|
||||
if not self.on_flush:
|
||||
if not self.single_file:
|
||||
self._dump_file(Path(f"{self.filepath}_callback_data"), self.callback_data)
|
||||
else:
|
||||
self._dump_singlefile()
|
||||
|
||||
async def drop_chat_data(self, chat_id: int) -> None:
|
||||
"""Will delete the specified key from the ``chat_data`` and depending on
|
||||
:attr:`on_flush` save the pickle file.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int`): The chat id to delete from the persistence.
|
||||
"""
|
||||
if self.chat_data is None:
|
||||
return
|
||||
self.chat_data.pop(chat_id, None)
|
||||
|
||||
if not self.on_flush:
|
||||
if not self.single_file:
|
||||
self._dump_file(Path(f"{self.filepath}_chat_data"), self.chat_data)
|
||||
else:
|
||||
self._dump_singlefile()
|
||||
|
||||
async def drop_user_data(self, user_id: int) -> None:
|
||||
"""Will delete the specified key from the ``user_data`` and depending on
|
||||
:attr:`on_flush` save the pickle file.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Args:
|
||||
user_id (:obj:`int`): The user id to delete from the persistence.
|
||||
"""
|
||||
if self.user_data is None:
|
||||
return
|
||||
self.user_data.pop(user_id, None)
|
||||
|
||||
if not self.on_flush:
|
||||
if not self.single_file:
|
||||
self._dump_file(Path(f"{self.filepath}_user_data"), self.user_data)
|
||||
else:
|
||||
self._dump_singlefile()
|
||||
|
||||
async def refresh_user_data(self, user_id: int, user_data: UD) -> None:
|
||||
"""Does nothing.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
.. seealso:: :meth:`telegram.ext.BasePersistence.refresh_user_data`
|
||||
"""
|
||||
|
||||
async def refresh_chat_data(self, chat_id: int, chat_data: CD) -> None:
|
||||
"""Does nothing.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
.. seealso:: :meth:`telegram.ext.BasePersistence.refresh_chat_data`
|
||||
"""
|
||||
|
||||
async def refresh_bot_data(self, bot_data: BD) -> None:
|
||||
"""Does nothing.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
.. seealso:: :meth:`telegram.ext.BasePersistence.refresh_bot_data`
|
||||
"""
|
||||
|
||||
async def flush(self) -> None:
|
||||
"""Will save all data in memory to pickle file(s)."""
|
||||
if self.single_file:
|
||||
if (
|
||||
self.user_data
|
||||
or self.chat_data
|
||||
or self.bot_data
|
||||
or self.callback_data
|
||||
or self.conversations
|
||||
):
|
||||
self._dump_singlefile()
|
||||
else:
|
||||
if self.user_data:
|
||||
self._dump_file(Path(f"{self.filepath}_user_data"), self.user_data)
|
||||
if self.chat_data:
|
||||
self._dump_file(Path(f"{self.filepath}_chat_data"), self.chat_data)
|
||||
if self.bot_data:
|
||||
self._dump_file(Path(f"{self.filepath}_bot_data"), self.bot_data)
|
||||
if self.callback_data:
|
||||
self._dump_file(Path(f"{self.filepath}_callback_data"), self.callback_data)
|
||||
if self.conversations:
|
||||
self._dump_file(Path(f"{self.filepath}_conversations"), self.conversations)
|
||||
779
.venv/lib/python3.12/site-packages/telegram/ext/_updater.py
Normal file
779
.venv/lib/python3.12/site-packages/telegram/ext/_updater.py
Normal file
@@ -0,0 +1,779 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 class Updater, which tries to make creating Telegram bots intuitive."""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import datetime as dtm
|
||||
import ssl
|
||||
from collections.abc import Coroutine, Sequence
|
||||
from pathlib import Path
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union
|
||||
|
||||
from telegram._utils.defaultvalue import DEFAULT_80, DEFAULT_IP, DefaultValue
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.repr import build_repr_with_selected_attrs
|
||||
from telegram._utils.types import DVType, TimePeriod
|
||||
from telegram.error import TelegramError
|
||||
from telegram.ext._utils.networkloop import network_retry_loop
|
||||
|
||||
try:
|
||||
from telegram.ext._utils.webhookhandler import WebhookAppClass, WebhookServer
|
||||
|
||||
WEBHOOKS_AVAILABLE = True
|
||||
except ImportError:
|
||||
WEBHOOKS_AVAILABLE = False
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from socket import socket
|
||||
|
||||
from telegram import Bot
|
||||
|
||||
|
||||
_UpdaterType = TypeVar("_UpdaterType", bound="Updater") # pylint: disable=invalid-name
|
||||
_LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
class Updater(contextlib.AbstractAsyncContextManager["Updater"]):
|
||||
"""This class fetches updates for the bot either via long polling or by starting a webhook
|
||||
server. Received updates are enqueued into the :attr:`update_queue` and may be fetched from
|
||||
there to handle them appropriately.
|
||||
|
||||
Instances of this class can be used as asyncio context managers, where
|
||||
|
||||
.. code:: python
|
||||
|
||||
async with updater:
|
||||
# code
|
||||
|
||||
is roughly equivalent to
|
||||
|
||||
.. code:: python
|
||||
|
||||
try:
|
||||
await updater.initialize()
|
||||
# code
|
||||
finally:
|
||||
await updater.shutdown()
|
||||
|
||||
.. seealso:: :meth:`__aenter__` and :meth:`__aexit__`.
|
||||
|
||||
.. seealso:: :wiki:`Architecture Overview <Architecture>`,
|
||||
:wiki:`Builder Pattern <Builder-Pattern>`
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
|
||||
* Removed argument and attribute ``user_sig_handler``
|
||||
* The only arguments and attributes are now :attr:`bot` and :attr:`update_queue` as now
|
||||
the sole purpose of this class is to fetch updates. The entry point to a PTB application
|
||||
is now :class:`telegram.ext.Application`.
|
||||
|
||||
Args:
|
||||
bot (:class:`telegram.Bot`): The bot used with this Updater.
|
||||
update_queue (:class:`asyncio.Queue`): Queue for the updates.
|
||||
|
||||
Attributes:
|
||||
bot (:class:`telegram.Bot`): The bot used with this Updater.
|
||||
update_queue (:class:`asyncio.Queue`): Queue for the updates.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"__lock",
|
||||
"__polling_cleanup_cb",
|
||||
"__polling_task",
|
||||
"__polling_task_stop_event",
|
||||
"_httpd",
|
||||
"_initialized",
|
||||
"_last_update_id",
|
||||
"_running",
|
||||
"bot",
|
||||
"update_queue",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bot: "Bot",
|
||||
update_queue: "asyncio.Queue[object]",
|
||||
):
|
||||
self.bot: Bot = bot
|
||||
self.update_queue: asyncio.Queue[object] = update_queue
|
||||
|
||||
self._last_update_id = 0
|
||||
self._running = False
|
||||
self._initialized = False
|
||||
self._httpd: Optional[WebhookServer] = None
|
||||
self.__lock = asyncio.Lock()
|
||||
self.__polling_task: Optional[asyncio.Task] = None
|
||||
self.__polling_task_stop_event: asyncio.Event = asyncio.Event()
|
||||
self.__polling_cleanup_cb: Optional[Callable[[], Coroutine[Any, Any, None]]] = None
|
||||
|
||||
async def __aenter__(self: _UpdaterType) -> _UpdaterType:
|
||||
"""
|
||||
|async_context_manager| :meth:`initializes <initialize>` the Updater.
|
||||
|
||||
Returns:
|
||||
The initialized Updater instance.
|
||||
|
||||
Raises:
|
||||
:exc:`Exception`: If an exception is raised during initialization, :meth:`shutdown`
|
||||
is called in this case.
|
||||
"""
|
||||
try:
|
||||
await self.initialize()
|
||||
except Exception:
|
||||
await self.shutdown()
|
||||
raise
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: Optional[type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
) -> None:
|
||||
"""|async_context_manager| :meth:`shuts down <shutdown>` the Updater."""
|
||||
# Make sure not to return `True` so that exceptions are not suppressed
|
||||
# https://docs.python.org/3/reference/datamodel.html?#object.__aexit__
|
||||
await self.shutdown()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Give a string representation of the updater in the form ``Updater[bot=...]``.
|
||||
|
||||
As this class doesn't implement :meth:`object.__str__`, the default implementation
|
||||
will be used, which is equivalent to :meth:`__repr__`.
|
||||
|
||||
Returns:
|
||||
:obj:`str`
|
||||
"""
|
||||
return build_repr_with_selected_attrs(self, bot=self.bot)
|
||||
|
||||
@property
|
||||
def running(self) -> bool:
|
||||
return self._running
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Initializes the Updater & the associated :attr:`bot` by calling
|
||||
:meth:`telegram.Bot.initialize`.
|
||||
|
||||
.. seealso::
|
||||
:meth:`shutdown`
|
||||
"""
|
||||
if self._initialized:
|
||||
_LOGGER.debug("This Updater is already initialized.")
|
||||
return
|
||||
|
||||
await self.bot.initialize()
|
||||
self._initialized = True
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""
|
||||
Shutdown the Updater & the associated :attr:`bot` by calling :meth:`telegram.Bot.shutdown`.
|
||||
|
||||
.. seealso::
|
||||
:meth:`initialize`
|
||||
|
||||
Raises:
|
||||
:exc:`RuntimeError`: If the updater is still running.
|
||||
"""
|
||||
if self.running:
|
||||
raise RuntimeError("This Updater is still running!")
|
||||
|
||||
if not self._initialized:
|
||||
_LOGGER.debug("This Updater is already shut down. Returning.")
|
||||
return
|
||||
|
||||
await self.bot.shutdown()
|
||||
self._initialized = False
|
||||
_LOGGER.debug("Shut down of Updater complete")
|
||||
|
||||
async def start_polling(
|
||||
self,
|
||||
poll_interval: float = 0.0,
|
||||
timeout: TimePeriod = dtm.timedelta(seconds=10),
|
||||
bootstrap_retries: int = 0,
|
||||
allowed_updates: Optional[Sequence[str]] = None,
|
||||
drop_pending_updates: Optional[bool] = None,
|
||||
error_callback: Optional[Callable[[TelegramError], None]] = None,
|
||||
) -> "asyncio.Queue[object]":
|
||||
"""Starts polling updates from Telegram.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Removed the ``clean`` argument in favor of :paramref:`drop_pending_updates`.
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
Removed the deprecated arguments ``read_timeout``, ``write_timeout``,
|
||||
``connect_timeout``, and ``pool_timeout`` in favor of setting the timeouts via
|
||||
the corresponding methods of :class:`telegram.ext.ApplicationBuilder`. or
|
||||
by specifying the timeout via :paramref:`telegram.Bot.get_updates_request`.
|
||||
|
||||
Args:
|
||||
poll_interval (:obj:`float`, optional): Time to wait between polling updates from
|
||||
Telegram in seconds. Default is ``0.0``.
|
||||
timeout (:obj:`int` | :class:`datetime.timedelta`, optional): Passed to
|
||||
:paramref:`telegram.Bot.get_updates.timeout`. Defaults to
|
||||
``timedelta(seconds=10)``.
|
||||
|
||||
.. versionchanged:: v22.2
|
||||
|time-period-input|
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of
|
||||
will retry on failures on the Telegram server.
|
||||
|
||||
* < 0 - retry indefinitely
|
||||
* 0 - no retries (default)
|
||||
* > 0 - retry up to X times
|
||||
|
||||
.. versionchanged:: 21.11
|
||||
The default value will be changed to from ``-1`` to ``0``. Indefinite retries
|
||||
during bootstrapping are not recommended.
|
||||
allowed_updates (Sequence[:obj:`str`], optional): Passed to
|
||||
:meth:`telegram.Bot.get_updates`.
|
||||
|
||||
.. versionchanged:: 21.9
|
||||
Accepts any :class:`collections.abc.Sequence` as input instead of just a list
|
||||
drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on
|
||||
Telegram servers before actually starting to poll. Default is :obj:`False`.
|
||||
|
||||
.. versionadded :: 13.4
|
||||
error_callback (Callable[[:exc:`telegram.error.TelegramError`], :obj:`None`], \
|
||||
optional): Callback to handle :exc:`telegram.error.TelegramError` s that occur
|
||||
while calling :meth:`telegram.Bot.get_updates` during polling. Defaults to
|
||||
:obj:`None`, in which case errors will be logged. Callback signature::
|
||||
|
||||
def callback(error: telegram.error.TelegramError)
|
||||
|
||||
Note:
|
||||
The :paramref:`error_callback` must *not* be a :term:`coroutine function`! If
|
||||
asynchronous behavior of the callback is wanted, please schedule a task from
|
||||
within the callback.
|
||||
|
||||
Returns:
|
||||
:class:`asyncio.Queue`: The update queue that can be filled from the main thread.
|
||||
|
||||
Raises:
|
||||
:exc:`RuntimeError`: If the updater is already running or was not initialized.
|
||||
|
||||
"""
|
||||
# We refrain from issuing deprecation warnings for the timeout parameters here, as we
|
||||
# already issue them in `Application`. This means that there are no warnings when using
|
||||
# `Updater` without `Application`, but this is a rather special use case.
|
||||
|
||||
if error_callback and asyncio.iscoroutinefunction(error_callback):
|
||||
raise TypeError(
|
||||
"The `error_callback` must not be a coroutine function! Use an ordinary function "
|
||||
"instead. "
|
||||
)
|
||||
|
||||
async with self.__lock:
|
||||
if self.running:
|
||||
raise RuntimeError("This Updater is already running!")
|
||||
if not self._initialized:
|
||||
raise RuntimeError("This Updater was not initialized via `Updater.initialize`!")
|
||||
|
||||
self._running = True
|
||||
|
||||
try:
|
||||
# Create & start tasks
|
||||
polling_ready = asyncio.Event()
|
||||
|
||||
await self._start_polling(
|
||||
poll_interval=poll_interval,
|
||||
timeout=timeout,
|
||||
bootstrap_retries=bootstrap_retries,
|
||||
drop_pending_updates=drop_pending_updates,
|
||||
allowed_updates=allowed_updates,
|
||||
ready=polling_ready,
|
||||
error_callback=error_callback,
|
||||
)
|
||||
|
||||
_LOGGER.debug("Waiting for polling to start")
|
||||
await polling_ready.wait()
|
||||
_LOGGER.debug("Polling updates from Telegram started")
|
||||
except Exception:
|
||||
self._running = False
|
||||
raise
|
||||
return self.update_queue
|
||||
|
||||
async def _start_polling(
|
||||
self,
|
||||
poll_interval: float,
|
||||
timeout: TimePeriod,
|
||||
bootstrap_retries: int,
|
||||
drop_pending_updates: Optional[bool],
|
||||
allowed_updates: Optional[Sequence[str]],
|
||||
ready: asyncio.Event,
|
||||
error_callback: Optional[Callable[[TelegramError], None]],
|
||||
) -> None:
|
||||
_LOGGER.debug("Updater started (polling)")
|
||||
|
||||
# the bootstrapping phase does two things:
|
||||
# 1) make sure there is no webhook set
|
||||
# 2) apply drop_pending_updates
|
||||
await self._bootstrap(
|
||||
bootstrap_retries,
|
||||
drop_pending_updates=drop_pending_updates,
|
||||
webhook_url="",
|
||||
allowed_updates=None,
|
||||
)
|
||||
|
||||
_LOGGER.debug("Bootstrap done")
|
||||
|
||||
async def polling_action_cb() -> bool:
|
||||
try:
|
||||
updates = await self.bot.get_updates(
|
||||
offset=self._last_update_id,
|
||||
timeout=timeout,
|
||||
allowed_updates=allowed_updates,
|
||||
)
|
||||
except TelegramError:
|
||||
# TelegramErrors should be processed by the network retry loop
|
||||
raise
|
||||
except Exception as exc:
|
||||
# Other exceptions should not. Let's log them for now.
|
||||
_LOGGER.critical(
|
||||
"Something went wrong processing the data received from Telegram. "
|
||||
"Received data was *not* processed!",
|
||||
exc_info=exc,
|
||||
)
|
||||
return True
|
||||
|
||||
if updates:
|
||||
if not self.running:
|
||||
_LOGGER.critical(
|
||||
"Updater stopped unexpectedly. Pulled updates will be ignored and pulled "
|
||||
"again on restart."
|
||||
)
|
||||
else:
|
||||
for update in updates:
|
||||
await self.update_queue.put(update)
|
||||
self._last_update_id = updates[-1].update_id + 1 # Add one to 'confirm' it
|
||||
|
||||
return True # Keep fetching updates & don't quit. Polls with poll_interval.
|
||||
|
||||
def default_error_callback(exc: TelegramError) -> None:
|
||||
_LOGGER.exception("Exception happened while polling for updates.", exc_info=exc)
|
||||
|
||||
# Start task that runs in background, pulls
|
||||
# updates from Telegram and inserts them in the update queue of the
|
||||
# Application.
|
||||
self.__polling_task = asyncio.create_task(
|
||||
network_retry_loop(
|
||||
is_running=lambda: self.running,
|
||||
action_cb=polling_action_cb,
|
||||
on_err_cb=error_callback or default_error_callback,
|
||||
description="Polling Updates",
|
||||
interval=poll_interval,
|
||||
stop_event=self.__polling_task_stop_event,
|
||||
max_retries=-1,
|
||||
),
|
||||
name="Updater:start_polling:polling_task",
|
||||
)
|
||||
|
||||
# Prepare a cleanup callback to await on _stop_polling
|
||||
# Calling get_updates one more time with the latest `offset` parameter ensures that
|
||||
# all updates that where put into the update queue are also marked as "read" to TG,
|
||||
# so we do not receive them again on the next startup
|
||||
# We define this here so that we can use the same parameters as in the polling task
|
||||
async def _get_updates_cleanup() -> None:
|
||||
_LOGGER.debug(
|
||||
"Calling `get_updates` one more time to mark all fetched updates as read."
|
||||
)
|
||||
try:
|
||||
await self.bot.get_updates(
|
||||
offset=self._last_update_id,
|
||||
# We don't want to do long polling here!
|
||||
timeout=dtm.timedelta(seconds=0),
|
||||
allowed_updates=allowed_updates,
|
||||
)
|
||||
except TelegramError:
|
||||
_LOGGER.exception(
|
||||
"Error while calling `get_updates` one more time to mark all fetched updates "
|
||||
"as read: %s. Suppressing error to ensure graceful shutdown. When polling for "
|
||||
"updates is restarted, updates may be fetched again. Please adjust timeouts "
|
||||
"via `ApplicationBuilder` or the parameter `get_updates_request` of `Bot`.",
|
||||
)
|
||||
|
||||
self.__polling_cleanup_cb = _get_updates_cleanup
|
||||
|
||||
if ready is not None:
|
||||
ready.set()
|
||||
|
||||
async def start_webhook(
|
||||
self,
|
||||
listen: DVType[str] = DEFAULT_IP,
|
||||
port: DVType[int] = DEFAULT_80,
|
||||
url_path: str = "",
|
||||
cert: Optional[Union[str, Path]] = None,
|
||||
key: Optional[Union[str, Path]] = None,
|
||||
bootstrap_retries: int = 0,
|
||||
webhook_url: Optional[str] = None,
|
||||
allowed_updates: Optional[Sequence[str]] = None,
|
||||
drop_pending_updates: Optional[bool] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
max_connections: int = 40,
|
||||
secret_token: Optional[str] = None,
|
||||
unix: Optional[Union[str, Path, "socket"]] = None,
|
||||
) -> "asyncio.Queue[object]":
|
||||
"""
|
||||
Starts a small http server to listen for updates via webhook. If :paramref:`cert`
|
||||
and :paramref:`key` are not provided, the webhook will be started directly on
|
||||
``http://listen:port/url_path``, so SSL can be handled by another
|
||||
application. Else, the webhook will be started on
|
||||
``https://listen:port/url_path``. Also calls :meth:`telegram.Bot.set_webhook` as required.
|
||||
|
||||
Important:
|
||||
If you want to use this method, you must install PTB with the optional requirement
|
||||
``webhooks``, i.e.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install "python-telegram-bot[webhooks]"
|
||||
|
||||
.. seealso:: :wiki:`Webhooks`
|
||||
|
||||
.. versionchanged:: 13.4
|
||||
:meth:`start_webhook` now *always* calls :meth:`telegram.Bot.set_webhook`, so pass
|
||||
``webhook_url`` instead of calling ``updater.bot.set_webhook(webhook_url)`` manually.
|
||||
.. versionchanged:: 20.0
|
||||
|
||||
* Removed the ``clean`` argument in favor of :paramref:`drop_pending_updates` and
|
||||
removed the deprecated argument ``force_event_loop``.
|
||||
|
||||
Args:
|
||||
listen (:obj:`str`, optional): IP-Address to listen on. Defaults to
|
||||
`127.0.0.1 <https://en.wikipedia.org/wiki/Localhost>`_.
|
||||
port (:obj:`int`, optional): Port the bot should be listening on. Must be one of
|
||||
:attr:`telegram.constants.SUPPORTED_WEBHOOK_PORTS` unless the bot is running
|
||||
behind a proxy. Defaults to ``80``.
|
||||
url_path (:obj:`str`, optional): Path inside url (http(s)://listen:port/<url_path>).
|
||||
Defaults to ``''``.
|
||||
cert (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL certificate file.
|
||||
key (:class:`pathlib.Path` | :obj:`str`, optional): Path to the SSL key file.
|
||||
drop_pending_updates (:obj:`bool`, optional): Whether to clean any pending updates on
|
||||
Telegram servers before actually starting to poll. Default is :obj:`False`.
|
||||
|
||||
.. versionadded :: 13.4
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of
|
||||
will retry on failures on the Telegram server.
|
||||
|
||||
* < 0 - retry indefinitely
|
||||
* 0 - no retries (default)
|
||||
* > 0 - retry up to X times
|
||||
webhook_url (:obj:`str`, optional): Explicitly specify the webhook url. Useful behind
|
||||
NAT, reverse proxy, etc. Default is derived from :paramref:`listen`,
|
||||
:paramref:`port`, :paramref:`url_path`, :paramref:`cert`, and :paramref:`key`.
|
||||
ip_address (:obj:`str`, optional): Passed to :meth:`telegram.Bot.set_webhook`.
|
||||
Defaults to :obj:`None`.
|
||||
|
||||
.. versionadded :: 13.4
|
||||
allowed_updates (Sequence[:obj:`str`], optional): Passed to
|
||||
:meth:`telegram.Bot.set_webhook`. Defaults to :obj:`None`.
|
||||
|
||||
.. versionchanged:: 21.9
|
||||
Accepts any :class:`collections.abc.Sequence` as input instead of just a list
|
||||
max_connections (:obj:`int`, optional): Passed to
|
||||
:meth:`telegram.Bot.set_webhook`. Defaults to ``40``.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
secret_token (:obj:`str`, optional): Passed to :meth:`telegram.Bot.set_webhook`.
|
||||
Defaults to :obj:`None`.
|
||||
|
||||
When added, the web server started by this call will expect the token to be set in
|
||||
the ``X-Telegram-Bot-Api-Secret-Token`` header of an incoming request and will
|
||||
raise a :class:`http.HTTPStatus.FORBIDDEN <http.HTTPStatus>` error if either the
|
||||
header isn't set or it is set to a wrong token.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
unix (:class:`pathlib.Path` | :obj:`str` | :class:`socket.socket`, optional): Can be
|
||||
either:
|
||||
|
||||
* the path to the unix socket file as :class:`pathlib.Path` or :obj:`str`. This
|
||||
will be passed to `tornado.netutil.bind_unix_socket <https://www.tornadoweb.org/
|
||||
en/stable/netutil.html#tornado.netutil.bind_unix_socket>`_ to create the socket.
|
||||
If the Path does not exist, the file will be created.
|
||||
|
||||
* or the socket itself. This option allows you to e.g. restrict the permissions of
|
||||
the socket for improved security. Note that you need to pass the correct family,
|
||||
type and socket options yourself.
|
||||
|
||||
Caution:
|
||||
This parameter is a replacement for the default TCP bind. Therefore, it is
|
||||
mutually exclusive with :paramref:`listen` and :paramref:`port`. When using
|
||||
this param, you must also run a reverse proxy to the unix socket and set the
|
||||
appropriate :paramref:`webhook_url`.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
.. versionchanged:: 21.1
|
||||
Added support to pass a socket instance itself.
|
||||
Returns:
|
||||
:class:`queue.Queue`: The update queue that can be filled from the main thread.
|
||||
|
||||
Raises:
|
||||
:exc:`RuntimeError`: If the updater is already running or was not initialized.
|
||||
"""
|
||||
if not WEBHOOKS_AVAILABLE:
|
||||
raise RuntimeError(
|
||||
"To use `start_webhook`, PTB must be installed via `pip install "
|
||||
'"python-telegram-bot[webhooks]"`.'
|
||||
)
|
||||
# unix has special requirements what must and mustn't be set when using it
|
||||
if unix:
|
||||
error_msg = (
|
||||
"You can not pass unix and {0}, only use one. Unix if you want to "
|
||||
"initialize a unix socket, or {0} for a standard TCP server."
|
||||
)
|
||||
if not isinstance(listen, DefaultValue):
|
||||
raise RuntimeError(error_msg.format("listen"))
|
||||
if not isinstance(port, DefaultValue):
|
||||
raise RuntimeError(error_msg.format("port"))
|
||||
if not webhook_url:
|
||||
raise RuntimeError(
|
||||
"Since you set unix, you also need to set the URL to the webhook "
|
||||
"of the proxy you run in front of the unix socket."
|
||||
)
|
||||
|
||||
async with self.__lock:
|
||||
if self.running:
|
||||
raise RuntimeError("This Updater is already running!")
|
||||
if not self._initialized:
|
||||
raise RuntimeError("This Updater was not initialized via `Updater.initialize`!")
|
||||
|
||||
self._running = True
|
||||
|
||||
try:
|
||||
# Create & start tasks
|
||||
webhook_ready = asyncio.Event()
|
||||
|
||||
await self._start_webhook(
|
||||
listen=DefaultValue.get_value(listen),
|
||||
port=DefaultValue.get_value(port),
|
||||
url_path=url_path,
|
||||
cert=cert,
|
||||
key=key,
|
||||
bootstrap_retries=bootstrap_retries,
|
||||
drop_pending_updates=drop_pending_updates,
|
||||
webhook_url=webhook_url,
|
||||
allowed_updates=allowed_updates,
|
||||
ready=webhook_ready,
|
||||
ip_address=ip_address,
|
||||
max_connections=max_connections,
|
||||
secret_token=secret_token,
|
||||
unix=unix,
|
||||
)
|
||||
|
||||
_LOGGER.debug("Waiting for webhook server to start")
|
||||
await webhook_ready.wait()
|
||||
_LOGGER.debug("Webhook server started")
|
||||
except Exception:
|
||||
self._running = False
|
||||
raise
|
||||
|
||||
# Return the update queue so the main thread can insert updates
|
||||
return self.update_queue
|
||||
|
||||
async def _start_webhook(
|
||||
self,
|
||||
listen: str,
|
||||
port: int,
|
||||
url_path: str,
|
||||
bootstrap_retries: int,
|
||||
allowed_updates: Optional[Sequence[str]],
|
||||
cert: Optional[Union[str, Path]] = None,
|
||||
key: Optional[Union[str, Path]] = None,
|
||||
drop_pending_updates: Optional[bool] = None,
|
||||
webhook_url: Optional[str] = None,
|
||||
ready: Optional[asyncio.Event] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
max_connections: int = 40,
|
||||
secret_token: Optional[str] = None,
|
||||
unix: Optional[Union[str, Path, "socket"]] = None,
|
||||
) -> None:
|
||||
_LOGGER.debug("Updater thread started (webhook)")
|
||||
|
||||
if not url_path.startswith("/"):
|
||||
url_path = f"/{url_path}"
|
||||
|
||||
# Create Tornado app instance
|
||||
app = WebhookAppClass(url_path, self.bot, self.update_queue, secret_token)
|
||||
|
||||
# Form SSL Context
|
||||
# An SSLError is raised if the private key does not match with the certificate
|
||||
# Note that we only use the SSL certificate for the WebhookServer, if the key is also
|
||||
# present. This is because the WebhookServer may not actually be in charge of performing
|
||||
# the SSL handshake, e.g. in case a reverse proxy is used
|
||||
if cert is not None and key is not None:
|
||||
try:
|
||||
ssl_ctx: Optional[ssl.SSLContext] = ssl.create_default_context(
|
||||
ssl.Purpose.CLIENT_AUTH
|
||||
)
|
||||
ssl_ctx.load_cert_chain(cert, key) # type: ignore[union-attr]
|
||||
except ssl.SSLError as exc:
|
||||
raise TelegramError("Invalid SSL Certificate") from exc
|
||||
else:
|
||||
ssl_ctx = None
|
||||
# Create and start server
|
||||
self._httpd = WebhookServer(listen, port, app, ssl_ctx, unix)
|
||||
|
||||
if not webhook_url:
|
||||
webhook_url = self._gen_webhook_url(
|
||||
protocol="https" if ssl_ctx else "http",
|
||||
listen=DefaultValue.get_value(listen),
|
||||
port=port,
|
||||
url_path=url_path,
|
||||
)
|
||||
|
||||
# We pass along the cert to the webhook if present.
|
||||
await self._bootstrap(
|
||||
# Passing a Path or string only works if the bot is running against a local bot API
|
||||
# server, so let's read the contents
|
||||
cert=Path(cert).read_bytes() if cert else None,
|
||||
max_retries=bootstrap_retries,
|
||||
drop_pending_updates=drop_pending_updates,
|
||||
webhook_url=webhook_url,
|
||||
allowed_updates=allowed_updates,
|
||||
ip_address=ip_address,
|
||||
max_connections=max_connections,
|
||||
secret_token=secret_token,
|
||||
)
|
||||
|
||||
await self._httpd.serve_forever(ready=ready)
|
||||
|
||||
@staticmethod
|
||||
def _gen_webhook_url(protocol: str, listen: str, port: int, url_path: str) -> str:
|
||||
# TODO: double check if this should be https in any case - the docs of start_webhook
|
||||
# say differently!
|
||||
return f"{protocol}://{listen}:{port}{url_path}"
|
||||
|
||||
async def _bootstrap(
|
||||
self,
|
||||
max_retries: int,
|
||||
webhook_url: Optional[str],
|
||||
allowed_updates: Optional[Sequence[str]],
|
||||
drop_pending_updates: Optional[bool] = None,
|
||||
cert: Optional[bytes] = None,
|
||||
bootstrap_interval: float = 1,
|
||||
ip_address: Optional[str] = None,
|
||||
max_connections: int = 40,
|
||||
secret_token: Optional[str] = None,
|
||||
) -> None:
|
||||
"""Prepares the setup for fetching updates: delete or set the webhook and drop pending
|
||||
updates if appropriate. If there are unsuccessful attempts, this will retry as specified by
|
||||
:paramref:`max_retries`.
|
||||
"""
|
||||
|
||||
async def bootstrap_del_webhook() -> bool:
|
||||
_LOGGER.debug("Deleting webhook")
|
||||
if drop_pending_updates:
|
||||
_LOGGER.debug("Dropping pending updates from Telegram server")
|
||||
await self.bot.delete_webhook(drop_pending_updates=drop_pending_updates)
|
||||
return False
|
||||
|
||||
async def bootstrap_set_webhook() -> bool:
|
||||
_LOGGER.debug("Setting webhook")
|
||||
if drop_pending_updates:
|
||||
_LOGGER.debug("Dropping pending updates from Telegram server")
|
||||
await self.bot.set_webhook(
|
||||
url=webhook_url, # type: ignore[arg-type]
|
||||
certificate=cert,
|
||||
allowed_updates=allowed_updates,
|
||||
ip_address=ip_address,
|
||||
drop_pending_updates=drop_pending_updates,
|
||||
max_connections=max_connections,
|
||||
secret_token=secret_token,
|
||||
)
|
||||
return False
|
||||
|
||||
# Dropping pending updates from TG can be efficiently done with the drop_pending_updates
|
||||
# parameter of delete/start_webhook, even in the case of polling. Also, we want to make
|
||||
# sure that no webhook is configured in case of polling, so we just always call
|
||||
# delete_webhook for polling
|
||||
if drop_pending_updates or not webhook_url:
|
||||
await network_retry_loop(
|
||||
is_running=lambda: self.running,
|
||||
action_cb=bootstrap_del_webhook,
|
||||
description="Bootstrap delete Webhook",
|
||||
interval=bootstrap_interval,
|
||||
stop_event=None,
|
||||
max_retries=max_retries,
|
||||
)
|
||||
|
||||
# Restore/set webhook settings, if needed. Again, we don't know ahead if a webhook is set,
|
||||
# so we set it anyhow.
|
||||
if webhook_url:
|
||||
await network_retry_loop(
|
||||
is_running=lambda: self.running,
|
||||
action_cb=bootstrap_set_webhook,
|
||||
description="Bootstrap Set Webhook",
|
||||
interval=bootstrap_interval,
|
||||
stop_event=None,
|
||||
max_retries=max_retries,
|
||||
)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stops the polling/webhook.
|
||||
|
||||
.. seealso::
|
||||
:meth:`start_polling`, :meth:`start_webhook`
|
||||
|
||||
Raises:
|
||||
:exc:`RuntimeError`: If the updater is not running.
|
||||
"""
|
||||
async with self.__lock:
|
||||
if not self.running:
|
||||
raise RuntimeError("This Updater is not running!")
|
||||
|
||||
_LOGGER.debug("Stopping Updater")
|
||||
|
||||
self._running = False
|
||||
|
||||
await self._stop_httpd()
|
||||
await self._stop_polling()
|
||||
|
||||
_LOGGER.debug("Updater.stop() is complete")
|
||||
|
||||
async def _stop_httpd(self) -> None:
|
||||
"""Stops the Webhook server by calling ``WebhookServer.shutdown()``"""
|
||||
if self._httpd:
|
||||
_LOGGER.debug("Waiting for current webhook connection to be closed.")
|
||||
await self._httpd.shutdown()
|
||||
self._httpd = None
|
||||
|
||||
async def _stop_polling(self) -> None:
|
||||
"""Stops the polling task by awaiting it."""
|
||||
if self.__polling_task:
|
||||
_LOGGER.debug("Waiting background polling task to finish up.")
|
||||
self.__polling_task_stop_event.set()
|
||||
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await self.__polling_task
|
||||
# It only fails in rare edge-cases, e.g. when `stop()` is called directly
|
||||
# after start_polling(), but lets better be safe than sorry ...
|
||||
|
||||
self.__polling_task = None
|
||||
self.__polling_task_stop_event.clear()
|
||||
|
||||
if self.__polling_cleanup_cb:
|
||||
await self.__polling_cleanup_cb()
|
||||
self.__polling_cleanup_cb = None
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"No polling cleanup callback defined. The last fetched updates may be "
|
||||
"fetched again on the next polling start."
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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/].
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 helper functions related to parsing updates and their contents.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
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 typing import Optional
|
||||
|
||||
from telegram._utils.types import SCT
|
||||
|
||||
|
||||
def parse_chat_id(chat_id: Optional[SCT[int]]) -> frozenset[int]:
|
||||
"""Accepts a chat id or collection of chat ids and returns a frozenset of chat ids."""
|
||||
if chat_id is None:
|
||||
return frozenset()
|
||||
if isinstance(chat_id, int):
|
||||
return frozenset({chat_id})
|
||||
return frozenset(chat_id)
|
||||
|
||||
|
||||
def parse_username(username: Optional[SCT[str]]) -> frozenset[str]:
|
||||
"""Accepts a username or collection of usernames and returns a frozenset of usernames.
|
||||
Strips the leading ``@`` if present.
|
||||
"""
|
||||
if username is None:
|
||||
return frozenset()
|
||||
if isinstance(username, str):
|
||||
return frozenset({username.removeprefix("@")})
|
||||
return frozenset(usr.removeprefix("@") for usr in username)
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 helper functions related to the std-lib asyncio module.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
|
||||
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.
|
||||
"""
|
||||
import asyncio
|
||||
from typing import Literal
|
||||
|
||||
|
||||
class TrackedBoundedSemaphore(asyncio.BoundedSemaphore):
|
||||
"""Simple subclass of :class:`asyncio.BoundedSemaphore` that tracks the current value of the
|
||||
semaphore. While there is an attribute ``_value`` in the superclass, it's private and we
|
||||
don't want to rely on it.
|
||||
"""
|
||||
|
||||
__slots__ = ("_current_value",)
|
||||
|
||||
def __init__(self, value: int = 1) -> None:
|
||||
super().__init__(value)
|
||||
self._current_value = value
|
||||
|
||||
@property
|
||||
def current_value(self) -> int:
|
||||
return self._current_value
|
||||
|
||||
async def acquire(self) -> Literal[True]:
|
||||
await super().acquire()
|
||||
self._current_value -= 1
|
||||
return True
|
||||
|
||||
def release(self) -> None:
|
||||
super().release()
|
||||
self._current_value += 1
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 network retry loop implementation.
|
||||
Its specifically tailored to handling the Telegram API and its errors.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
|
||||
Hint:
|
||||
It was originally part of the `Updater` class, but as part of #4657 it was extracted into its
|
||||
own module to be used by other parts of the library.
|
||||
|
||||
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.
|
||||
"""
|
||||
import asyncio
|
||||
import contextlib
|
||||
from collections.abc import Coroutine
|
||||
from typing import Callable, Optional
|
||||
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut
|
||||
|
||||
_LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
async def network_retry_loop(
|
||||
*,
|
||||
action_cb: Callable[..., Coroutine],
|
||||
on_err_cb: Optional[Callable[[TelegramError], None]] = None,
|
||||
description: str,
|
||||
interval: float,
|
||||
stop_event: Optional[asyncio.Event] = None,
|
||||
is_running: Optional[Callable[[], bool]] = None,
|
||||
max_retries: int,
|
||||
) -> None:
|
||||
"""Perform a loop calling `action_cb`, retrying after network errors.
|
||||
|
||||
Stop condition for loop:
|
||||
* `is_running()` evaluates :obj:`False` or
|
||||
* return value of `action_cb` evaluates :obj:`False`
|
||||
* or `stop_event` is set.
|
||||
* or `max_retries` is reached.
|
||||
|
||||
Args:
|
||||
action_cb (:term:`coroutine function`): Network oriented callback function to call.
|
||||
on_err_cb (:obj:`callable`): Optional. Callback to call when TelegramError is caught.
|
||||
Receives the exception object as a parameter.
|
||||
|
||||
Hint:
|
||||
Only required if you want to handle the error in a special way. Logging about
|
||||
the error is already handled by the loop.
|
||||
|
||||
Important:
|
||||
Must not raise exceptions! If it does, the loop will be aborted.
|
||||
description (:obj:`str`): Description text to use for logs and exception raised.
|
||||
interval (:obj:`float` | :obj:`int`): Interval to sleep between each call to
|
||||
`action_cb`.
|
||||
stop_event (:class:`asyncio.Event` | :obj:`None`): Event to wait on for stopping the
|
||||
loop. Setting the event will make the loop exit even if `action_cb` is currently
|
||||
running. Defaults to :obj:`None`.
|
||||
is_running (:obj:`callable`): Function to check if the loop should continue running.
|
||||
Must return a boolean value. Defaults to `lambda: True`.
|
||||
max_retries (:obj:`int`): Maximum number of retries before stopping the loop.
|
||||
|
||||
* < 0: Retry indefinitely.
|
||||
* 0: No retries.
|
||||
* > 0: Number of retries.
|
||||
|
||||
"""
|
||||
log_prefix = f"Network Retry Loop ({description}):"
|
||||
effective_is_running = is_running or (lambda: True)
|
||||
|
||||
async def do_action() -> bool:
|
||||
if not stop_event:
|
||||
return await action_cb()
|
||||
|
||||
action_cb_task = asyncio.create_task(action_cb())
|
||||
stop_task = asyncio.create_task(stop_event.wait())
|
||||
done, pending = await asyncio.wait(
|
||||
(action_cb_task, stop_task), return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
||||
if stop_task in done:
|
||||
_LOGGER.debug("%s Cancelled", log_prefix)
|
||||
return False
|
||||
|
||||
return action_cb_task.result()
|
||||
|
||||
_LOGGER.debug("%s Starting", log_prefix)
|
||||
cur_interval = interval
|
||||
retries = 0
|
||||
while effective_is_running():
|
||||
try:
|
||||
if not await do_action():
|
||||
break
|
||||
except RetryAfter as exc:
|
||||
slack_time = 0.5
|
||||
_LOGGER.info(
|
||||
"%s %s. Adding %s seconds to the specified time.", log_prefix, exc, slack_time
|
||||
)
|
||||
# pylint: disable=protected-access
|
||||
cur_interval = slack_time + exc._retry_after.total_seconds()
|
||||
except TimedOut as toe:
|
||||
_LOGGER.debug("%s Timed out: %s. Retrying immediately.", log_prefix, toe)
|
||||
# If failure is due to timeout, we should retry asap.
|
||||
cur_interval = 0
|
||||
except InvalidToken:
|
||||
_LOGGER.exception("%s Invalid token. Aborting retry loop.", log_prefix)
|
||||
raise
|
||||
except TelegramError as telegram_exc:
|
||||
if on_err_cb:
|
||||
on_err_cb(telegram_exc)
|
||||
|
||||
if max_retries < 0 or retries < max_retries:
|
||||
_LOGGER.debug(
|
||||
"%s Failed run number %s of %s. Retrying.", log_prefix, retries, max_retries
|
||||
)
|
||||
else:
|
||||
_LOGGER.exception(
|
||||
"%s Failed run number %s of %s. Aborting.", log_prefix, retries, max_retries
|
||||
)
|
||||
raise
|
||||
|
||||
# increase waiting times on subsequent errors up to 30secs
|
||||
cur_interval = 1 if cur_interval == 0 else min(30, 1.5 * cur_interval)
|
||||
else:
|
||||
cur_interval = interval
|
||||
finally:
|
||||
retries += 1
|
||||
|
||||
if cur_interval:
|
||||
await asyncio.sleep(cur_interval)
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 helper functions related to inspecting the program stack.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
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 pathlib import Path
|
||||
from types import FrameType
|
||||
from typing import Optional
|
||||
|
||||
from telegram._utils.logging import get_logger
|
||||
|
||||
_LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
def was_called_by(frame: Optional[FrameType], caller: Path) -> bool:
|
||||
"""Checks if the passed frame was called by the specified file.
|
||||
|
||||
Example:
|
||||
.. code:: pycon
|
||||
|
||||
>>> was_called_by(inspect.currentframe(), Path(__file__))
|
||||
True
|
||||
|
||||
Arguments:
|
||||
frame (:obj:`FrameType`): The frame - usually the return value of
|
||||
``inspect.currentframe()``. If :obj:`None` is passed, the return value will be
|
||||
:obj:`False`.
|
||||
caller (:obj:`pathlib.Path`): File that should be the caller.
|
||||
|
||||
Returns:
|
||||
:obj:`bool`: Whether the frame was called by the specified file.
|
||||
"""
|
||||
if frame is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
return _was_called_by(frame, caller)
|
||||
except Exception as exc:
|
||||
_LOGGER.debug(
|
||||
"Failed to check if frame was called by `caller`. Assuming that it was not.",
|
||||
exc_info=exc,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _was_called_by(frame: FrameType, caller: Path) -> bool:
|
||||
# https://stackoverflow.com/a/57712700/10606962
|
||||
if Path(frame.f_code.co_filename).resolve() == caller:
|
||||
return True
|
||||
while frame.f_back:
|
||||
frame = frame.f_back
|
||||
if Path(frame.f_code.co_filename).resolve() == caller:
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 mutable mapping that keeps track of the keys that where accessed.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
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 collections import UserDict
|
||||
from collections.abc import Mapping
|
||||
from typing import Final, Generic, Optional, TypeVar, Union
|
||||
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
|
||||
|
||||
_VT = TypeVar("_VT")
|
||||
_KT = TypeVar("_KT")
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class TrackingDict(UserDict, Generic[_KT, _VT]):
|
||||
"""Mutable mapping that keeps track of which keys where accessed with write access.
|
||||
Read-access is not tracked.
|
||||
|
||||
Note:
|
||||
* ``setdefault()`` and ``pop`` are considered writing only depending on whether the
|
||||
key is present
|
||||
* deleting values is considered writing
|
||||
"""
|
||||
|
||||
DELETED: Final = object()
|
||||
"""Special marker indicating that an entry was deleted."""
|
||||
|
||||
__slots__ = ("_write_access_keys",)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._write_access_keys: set[_KT] = set()
|
||||
|
||||
def __setitem__(self, key: _KT, value: _VT) -> None:
|
||||
self.__track_write(key)
|
||||
super().__setitem__(key, value)
|
||||
|
||||
def __delitem__(self, key: _KT) -> None:
|
||||
self.__track_write(key)
|
||||
super().__delitem__(key)
|
||||
|
||||
def __track_write(self, key: Union[_KT, set[_KT]]) -> None:
|
||||
if isinstance(key, set):
|
||||
self._write_access_keys |= key
|
||||
else:
|
||||
self._write_access_keys.add(key)
|
||||
|
||||
def pop_accessed_keys(self) -> set[_KT]:
|
||||
"""Returns all keys that were write-accessed since the last time this method was called."""
|
||||
out = self._write_access_keys
|
||||
self._write_access_keys = set()
|
||||
return out
|
||||
|
||||
def pop_accessed_write_items(self) -> list[tuple[_KT, _VT]]:
|
||||
"""
|
||||
Returns all keys & corresponding values as set of tuples that were write-accessed since
|
||||
the last time this method was called. If a key was deleted, the value will be
|
||||
:attr:`DELETED`.
|
||||
"""
|
||||
keys = self.pop_accessed_keys()
|
||||
return [(key, self.get(key, self.DELETED)) for key in keys]
|
||||
|
||||
def mark_as_accessed(self, key: _KT) -> None:
|
||||
"""Use this method have the key returned again in the next call to
|
||||
:meth:`pop_accessed_write_items` or :meth:`pop_accessed_keys`
|
||||
"""
|
||||
self._write_access_keys.add(key)
|
||||
|
||||
# Override methods to track access
|
||||
|
||||
def update_no_track(self, mapping: Mapping[_KT, _VT]) -> None:
|
||||
"""Like ``update``, but doesn't count towards write access."""
|
||||
for key, value in mapping.items():
|
||||
self.data[key] = value
|
||||
|
||||
# Mypy seems a bit inconsistent about what it wants as types for `default` and return value
|
||||
# so we just ignore a bit
|
||||
def pop( # type: ignore[override]
|
||||
self,
|
||||
key: _KT,
|
||||
default: _VT = DEFAULT_NONE, # type: ignore[assignment]
|
||||
) -> _VT:
|
||||
if key in self:
|
||||
self.__track_write(key)
|
||||
if isinstance(default, DefaultValue):
|
||||
return super().pop(key)
|
||||
return super().pop(key, default=default)
|
||||
|
||||
def clear(self) -> None:
|
||||
self.__track_write(set(super().keys()))
|
||||
super().clear()
|
||||
|
||||
# Mypy seems a bit inconsistent about what it wants as types for `default` and return value
|
||||
# so we just ignore a bit
|
||||
def setdefault(self: "TrackingDict[_KT, _T]", key: _KT, default: Optional[_T] = None) -> _T:
|
||||
if key in self:
|
||||
return self[key]
|
||||
|
||||
self.__track_write(key)
|
||||
self[key] = default # type: ignore[assignment]
|
||||
return default # type: ignore[return-value]
|
||||
106
.venv/lib/python3.12/site-packages/telegram/ext/_utils/types.py
Normal file
106
.venv/lib/python3.12/site-packages/telegram/ext/_utils/types.py
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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 custom typing aliases.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
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 collections.abc import Coroutine, MutableMapping
|
||||
from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional
|
||||
|
||||
from telegram import Bot
|
||||
from telegram.ext import BaseRateLimiter, CallbackContext, JobQueue
|
||||
|
||||
CCT = TypeVar("CCT", bound="CallbackContext[Any, Any, Any, Any]")
|
||||
"""An instance of :class:`telegram.ext.CallbackContext` or a custom subclass.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
"""
|
||||
|
||||
RT = TypeVar("RT")
|
||||
UT = TypeVar("UT")
|
||||
HandlerCallback = Callable[[UT, CCT], Coroutine[Any, Any, RT]]
|
||||
"""Type of a handler callback
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
JobCallback = Callable[[CCT], Coroutine[Any, Any, Any]]
|
||||
"""Type of a job callback
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
|
||||
ConversationKey = tuple[Union[int, str], ...]
|
||||
ConversationDict = MutableMapping[ConversationKey, object]
|
||||
"""dict[tuple[:obj:`int` | :obj:`str`, ...], Optional[:obj:`object`]]:
|
||||
Dicts as maintained by the :class:`telegram.ext.ConversationHandler`.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
"""
|
||||
|
||||
CDCData = tuple[list[tuple[str, float, dict[str, Any]]], dict[str, str]]
|
||||
"""tuple[list[tuple[:obj:`str`, :obj:`float`, dict[:obj:`str`, :class:`object`]]], \
|
||||
dict[:obj:`str`, :obj:`str`]]: Data returned by
|
||||
:attr:`telegram.ext.CallbackDataCache.persistence_data`.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
"""
|
||||
|
||||
BT = TypeVar("BT", bound="Bot")
|
||||
"""Type of the bot.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
UD = TypeVar("UD")
|
||||
"""Type of the user data for a single user.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
"""
|
||||
CD = TypeVar("CD")
|
||||
"""Type of the chat data for a single user.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
"""
|
||||
BD = TypeVar("BD")
|
||||
"""Type of the bot data.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
"""
|
||||
JQ = TypeVar("JQ", bound=Union[None, "JobQueue"])
|
||||
"""Type of the job queue.
|
||||
|
||||
.. versionadded:: 20.0"""
|
||||
|
||||
RL = TypeVar("RL", bound="Optional[BaseRateLimiter]")
|
||||
"""Type of the rate limiter.
|
||||
|
||||
.. versionadded:: 20.0"""
|
||||
|
||||
RLARGS = TypeVar("RLARGS")
|
||||
"""Type of the rate limiter arguments.
|
||||
|
||||
.. versionadded:: 20.0"""
|
||||
FilterDataDict = dict[str, list[Any]]
|
||||
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# 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/].
|
||||
# pylint: disable=missing-module-docstring
|
||||
import asyncio
|
||||
import json
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from socket import socket
|
||||
from ssl import SSLContext
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
# Instead of checking for ImportError here, we do that in `updater.py`, where we import from
|
||||
# this module. Doing it here would be tricky, as the classes below subclass tornado classes
|
||||
import tornado.web
|
||||
from tornado.httpserver import HTTPServer
|
||||
|
||||
try:
|
||||
from tornado.netutil import bind_unix_socket
|
||||
|
||||
UNIX_AVAILABLE = True
|
||||
except ImportError:
|
||||
UNIX_AVAILABLE = False
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram.ext._extbot import ExtBot
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Bot
|
||||
|
||||
# This module is not visible to users, so we log as Updater
|
||||
_LOGGER = get_logger(__name__, class_name="Updater")
|
||||
|
||||
|
||||
class WebhookServer:
|
||||
"""Thin wrapper around ``tornado.httpserver.HTTPServer``."""
|
||||
|
||||
__slots__ = (
|
||||
"_http_server",
|
||||
"_server_lock",
|
||||
"_shutdown_lock",
|
||||
"is_running",
|
||||
"listen",
|
||||
"port",
|
||||
"unix",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
listen: str,
|
||||
port: int,
|
||||
webhook_app: "WebhookAppClass",
|
||||
ssl_ctx: Optional[SSLContext],
|
||||
unix: Optional[Union[str, Path, socket]] = None,
|
||||
):
|
||||
if unix and not UNIX_AVAILABLE:
|
||||
raise RuntimeError("This OS does not support binding unix sockets.")
|
||||
self._http_server = HTTPServer(webhook_app, ssl_options=ssl_ctx)
|
||||
self.listen = listen
|
||||
self.port = port
|
||||
self.is_running = False
|
||||
self.unix = None
|
||||
if unix and isinstance(unix, socket):
|
||||
self.unix = unix
|
||||
elif unix:
|
||||
self.unix = bind_unix_socket(str(unix))
|
||||
self._server_lock = asyncio.Lock()
|
||||
self._shutdown_lock = asyncio.Lock()
|
||||
|
||||
async def serve_forever(self, ready: Optional[asyncio.Event] = None) -> None:
|
||||
async with self._server_lock:
|
||||
if self.unix:
|
||||
self._http_server.add_socket(self.unix)
|
||||
else:
|
||||
self._http_server.listen(self.port, address=self.listen)
|
||||
|
||||
self.is_running = True
|
||||
if ready is not None:
|
||||
ready.set()
|
||||
|
||||
_LOGGER.debug("Webhook Server started.")
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
async with self._shutdown_lock:
|
||||
if not self.is_running:
|
||||
_LOGGER.debug("Webhook Server is already shut down. Returning")
|
||||
return
|
||||
self.is_running = False
|
||||
self._http_server.stop()
|
||||
await self._http_server.close_all_connections()
|
||||
_LOGGER.debug("Webhook Server stopped")
|
||||
|
||||
|
||||
class WebhookAppClass(tornado.web.Application):
|
||||
"""Application used in the Webserver"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
webhook_path: str,
|
||||
bot: "Bot",
|
||||
update_queue: asyncio.Queue,
|
||||
secret_token: Optional[str] = None,
|
||||
):
|
||||
self.shared_objects = {
|
||||
"bot": bot,
|
||||
"update_queue": update_queue,
|
||||
"secret_token": secret_token,
|
||||
}
|
||||
handlers = [(rf"{webhook_path}/?", TelegramHandler, self.shared_objects)]
|
||||
tornado.web.Application.__init__(self, handlers) # type: ignore
|
||||
|
||||
def log_request(self, handler: tornado.web.RequestHandler) -> None:
|
||||
"""Overrides the default implementation since we have our own logging setup."""
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class TelegramHandler(tornado.web.RequestHandler):
|
||||
"""BaseHandler that processes incoming requests from Telegram"""
|
||||
|
||||
__slots__ = ("bot", "secret_token", "update_queue")
|
||||
|
||||
SUPPORTED_METHODS = ("POST",) # type: ignore[assignment]
|
||||
|
||||
def initialize(self, bot: "Bot", update_queue: asyncio.Queue, secret_token: str) -> None:
|
||||
"""Initialize for each request - that's the interface provided by tornado"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.bot = bot
|
||||
self.update_queue = update_queue
|
||||
self.secret_token = secret_token
|
||||
if secret_token:
|
||||
_LOGGER.debug(
|
||||
"The webhook server has a secret token, expecting it in incoming requests now"
|
||||
)
|
||||
|
||||
def set_default_headers(self) -> None:
|
||||
"""Sets default headers"""
|
||||
self.set_header("Content-Type", 'application/json; charset="utf-8"')
|
||||
|
||||
async def post(self) -> None:
|
||||
"""Handle incoming POST request"""
|
||||
_LOGGER.debug("Webhook triggered")
|
||||
self._validate_post()
|
||||
|
||||
json_string = self.request.body.decode()
|
||||
data = json.loads(json_string)
|
||||
self.set_status(HTTPStatus.OK)
|
||||
_LOGGER.debug("Webhook received data: %s", json_string)
|
||||
|
||||
try:
|
||||
update = Update.de_json(data, self.bot)
|
||||
except Exception as exc:
|
||||
_LOGGER.critical(
|
||||
"Something went wrong processing the data received from Telegram. "
|
||||
"Received data was *not* processed! Received data was: %r",
|
||||
data,
|
||||
exc_info=exc,
|
||||
)
|
||||
raise tornado.web.HTTPError(
|
||||
HTTPStatus.BAD_REQUEST, reason="Update could not be processed"
|
||||
) from exc
|
||||
|
||||
if update:
|
||||
_LOGGER.debug(
|
||||
"Received Update with ID %d on Webhook",
|
||||
# For some reason pylint thinks update is a general TelegramObject
|
||||
update.update_id, # pylint: disable=no-member
|
||||
)
|
||||
|
||||
# handle arbitrary callback data, if necessary
|
||||
if isinstance(self.bot, ExtBot):
|
||||
self.bot.insert_callback_data(update)
|
||||
|
||||
await self.update_queue.put(update)
|
||||
|
||||
def _validate_post(self) -> None:
|
||||
"""Only accept requests with content type JSON"""
|
||||
ct_header = self.request.headers.get("Content-Type", None)
|
||||
if ct_header != "application/json":
|
||||
raise tornado.web.HTTPError(HTTPStatus.FORBIDDEN)
|
||||
# verifying that the secret token is the one the user set when the user set one
|
||||
if self.secret_token is not None:
|
||||
token = self.request.headers.get("X-Telegram-Bot-Api-Secret-Token")
|
||||
if not token:
|
||||
_LOGGER.debug("Request did not include the secret token")
|
||||
raise tornado.web.HTTPError(
|
||||
HTTPStatus.FORBIDDEN, reason="Request did not include the secret token"
|
||||
)
|
||||
if token != self.secret_token:
|
||||
_LOGGER.debug("Request had the wrong secret token: %s", token)
|
||||
raise tornado.web.HTTPError(
|
||||
HTTPStatus.FORBIDDEN, reason="Request had the wrong secret token"
|
||||
)
|
||||
|
||||
def log_exception(
|
||||
self,
|
||||
typ: Optional[type[BaseException]],
|
||||
value: Optional[BaseException],
|
||||
tb: Optional[TracebackType],
|
||||
) -> None:
|
||||
"""Override the default logging and instead use our custom logging."""
|
||||
_LOGGER.debug(
|
||||
"%s - %s",
|
||||
self.request.remote_ip,
|
||||
"Exception in TelegramHandler",
|
||||
exc_info=(typ, value, tb) if typ and value and tb else value,
|
||||
)
|
||||
3003
.venv/lib/python3.12/site-packages/telegram/ext/filters.py
Normal file
3003
.venv/lib/python3.12/site-packages/telegram/ext/filters.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user