README.md edited
This commit is contained in:
104
.venv/lib/python3.10/site-packages/telegram/ext/__init__.py
Normal file
104
.venv/lib/python3.10/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-2024
|
||||
# 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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,277 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# 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._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.
|
||||
|
||||
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.
|
||||
|
||||
.. caution::
|
||||
This class currently doesn't take the
|
||||
:paramref:`~telegram.Bot.send_message.allow_paid_broadcast` parameter into account.
|
||||
This means that the rate limiting is applied just like for any other message.
|
||||
|
||||
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 ``30``.
|
||||
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 20.
|
||||
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__ = (
|
||||
"_base_limiter",
|
||||
"_group_limiters",
|
||||
"_group_max_rate",
|
||||
"_group_time_period",
|
||||
"_max_retries",
|
||||
"_retry_after_event",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
overall_max_rate: float = 30,
|
||||
overall_time_period: float = 1,
|
||||
group_max_rate: float = 20,
|
||||
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._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],
|
||||
callback: Callable[..., Coroutine[Any, Any, Union[bool, JSONDict, list[JSONDict]]]],
|
||||
args: Any,
|
||||
kwargs: dict[str, Any],
|
||||
) -> Union[bool, JSONDict, list[JSONDict]]:
|
||||
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:
|
||||
# In case a retry_after was hit, we wait with processing the request
|
||||
await self._retry_after_event.wait()
|
||||
|
||||
return await callback(*args, **kwargs)
|
||||
|
||||
# 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")
|
||||
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)
|
||||
|
||||
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, 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 + 0.1
|
||||
_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]
|
||||
1911
.venv/lib/python3.10/site-packages/telegram/ext/_application.py
Normal file
1911
.venv/lib/python3.10/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-2024
|
||||
# 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-2024
|
||||
# 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,188 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# 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 asyncio import BoundedSemaphore
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Any, Optional, TypeVar, final
|
||||
|
||||
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 = BoundedSemaphore(self.max_concurrent_updates)
|
||||
|
||||
async def __aenter__(self: _BUPT) -> _BUPT: # noqa: PYI019
|
||||
"""|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
|
||||
|
||||
@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-2024
|
||||
# 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
|
||||
|
||||
@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,467 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# 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 time
|
||||
from collections.abc import MutableMapping
|
||||
from datetime import datetime
|
||||
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, 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, datetime]] = None
|
||||
) -> None:
|
||||
if not time_cutoff:
|
||||
mapping.clear()
|
||||
return
|
||||
|
||||
if isinstance(time_cutoff, 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.10/site-packages/telegram/ext/_contexttypes.py
Normal file
226
.venv/lib/python3.10/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-2024
|
||||
# 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]
|
||||
413
.venv/lib/python3.10/site-packages/telegram/ext/_defaults.py
Normal file
413
.venv/lib/python3.10/site-packages/telegram/ext/_defaults.py
Normal file
@@ -0,0 +1,413 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# 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
|
||||
from typing import Any, NoReturn, Optional, final
|
||||
|
||||
from telegram import LinkPreviewOptions
|
||||
from telegram._utils.datetime import UTC
|
||||
from telegram._utils.types import ODVInput
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
Parameters:
|
||||
parse_mode (:obj:`str`, optional): |parse_mode|
|
||||
disable_notification (:obj:`bool`, optional): |disable_notification|
|
||||
disable_web_page_preview (:obj:`bool`, optional): Disables link previews for links in this
|
||||
message. Mutually exclusive with :paramref:`link_preview_options`.
|
||||
|
||||
.. deprecated:: 20.8
|
||||
Use :paramref:`link_preview_options` instead. This parameter will be removed in
|
||||
future versions.
|
||||
|
||||
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|.
|
||||
Will be used for :attr:`telegram.ReplyParameters.allow_sending_without_reply`.
|
||||
quote (:obj:`bool`, optional): |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
Use :paramref:`do_quote` instead. This parameter will be removed in future
|
||||
versions.
|
||||
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`. If the
|
||||
:class:`telegram.ext.JobQueue` is used, this must be a timezone provided
|
||||
by the ``pytz`` module. Defaults to ``pytz.utc``, if available, and
|
||||
:attr:`datetime.timezone.utc` otherwise.
|
||||
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,
|
||||
disable_web_page_preview: Optional[bool] = None,
|
||||
quote: Optional[bool] = None,
|
||||
tzinfo: datetime.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: datetime.tzinfo = tzinfo
|
||||
self._block: bool = block
|
||||
self._protect_content: Optional[bool] = protect_content
|
||||
|
||||
if disable_web_page_preview is not None and link_preview_options is not None:
|
||||
raise ValueError(
|
||||
"`disable_web_page_preview` and `link_preview_options` are mutually exclusive."
|
||||
)
|
||||
if quote is not None and do_quote is not None:
|
||||
raise ValueError("`quote` and `do_quote` are mutually exclusive")
|
||||
if disable_web_page_preview is not None:
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"20.8",
|
||||
"`Defaults.disable_web_page_preview` is deprecated. Use "
|
||||
"`Defaults.link_preview_options` instead.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
self._link_preview_options: Optional[LinkPreviewOptions] = LinkPreviewOptions(
|
||||
is_disabled=disable_web_page_preview
|
||||
)
|
||||
else:
|
||||
self._link_preview_options = link_preview_options
|
||||
|
||||
if quote is not None:
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"20.8", "`Defaults.quote` is deprecated. Use `Defaults.do_quote` instead."
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
self._do_quote: Optional[bool] = quote
|
||||
else:
|
||||
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.disable_web_page_preview,
|
||||
self._allow_sending_without_reply,
|
||||
self.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 disable_web_page_preview(self) -> ODVInput[bool]:
|
||||
""":obj:`bool`: Optional. Disables link previews for links in all outgoing
|
||||
messages.
|
||||
|
||||
.. deprecated:: 20.8
|
||||
Use :attr:`link_preview_options` instead. This attribute will be removed in future
|
||||
versions.
|
||||
"""
|
||||
return self._link_preview_options.is_disabled if self._link_preview_options else None
|
||||
|
||||
@disable_web_page_preview.setter
|
||||
def disable_web_page_preview(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
"You can not assign a new value to disable_web_page_preview 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 quote(self) -> Optional[bool]:
|
||||
""":obj:`bool`: Optional. |reply_quote|
|
||||
|
||||
.. deprecated:: 20.8
|
||||
Use :attr:`do_quote` instead. This attribute will be removed in future
|
||||
versions.
|
||||
"""
|
||||
return self._do_quote if self._do_quote is not None else None
|
||||
|
||||
@quote.setter
|
||||
def quote(self, _: object) -> NoReturn:
|
||||
raise AttributeError("You can not assign a new value to quote after initialization.")
|
||||
|
||||
@property
|
||||
def tzinfo(self) -> datetime.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-2024
|
||||
# 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
|
||||
4619
.venv/lib/python3.10/site-packages/telegram/ext/_extbot.py
Normal file
4619
.venv/lib/python3.10/site-packages/telegram/ext/_extbot.py
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# 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-2024
|
||||
# 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-2024
|
||||
# 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-2024
|
||||
# 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-2024
|
||||
# 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-2024
|
||||
# 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-2024
|
||||
# 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-2024
|
||||
# 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-2024
|
||||
# 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,960 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# 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
|
||||
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, datetime.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
|
||||
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, datetime.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, datetime.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,143 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# 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 update.inline_query.query
|
||||
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-2024
|
||||
# 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-2024
|
||||
# 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-2024
|
||||
# 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-2024
|
||||
# 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-2024
|
||||
# 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-2024
|
||||
# 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-2024
|
||||
# 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-2024
|
||||
# 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-2024
|
||||
# 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-2024
|
||||
# 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-2024
|
||||
# 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
|
||||
987
.venv/lib/python3.10/site-packages/telegram/ext/_jobqueue.py
Normal file
987
.venv/lib/python3.10/site-packages/telegram/ext/_jobqueue.py
Normal file
@@ -0,0 +1,987 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# 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 classes JobQueue and Job."""
|
||||
import asyncio
|
||||
import datetime
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING, Any, Generic, Optional, Union, cast, overload
|
||||
|
||||
try:
|
||||
import pytz
|
||||
from apscheduler.executors.asyncio import AsyncIOExecutor
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
|
||||
APS_AVAILABLE = True
|
||||
except ImportError:
|
||||
APS_AVAILABLE = False
|
||||
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.repr import build_repr_with_selected_attrs
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram.ext._extbot import ExtBot
|
||||
from telegram.ext._utils.types import CCT, JobCallback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if APS_AVAILABLE:
|
||||
from apscheduler.job import Job as APSJob
|
||||
|
||||
from telegram.ext import Application
|
||||
|
||||
|
||||
_ALL_DAYS = tuple(range(7))
|
||||
_LOGGER = get_logger(__name__, class_name="JobQueue")
|
||||
|
||||
|
||||
class JobQueue(Generic[CCT]):
|
||||
"""This class allows you to periodically perform tasks with the bot. It is a convenience
|
||||
wrapper for the APScheduler library.
|
||||
|
||||
This class is a :class:`~typing.Generic` class and accepts one type variable that specifies
|
||||
the type of the argument ``context`` of the job callbacks (:paramref:`~run_once.callback`) of
|
||||
:meth:`run_once` and the other scheduling methods.
|
||||
|
||||
Important:
|
||||
If you want to use this class, you must install PTB with the optional requirement
|
||||
``job-queue``, i.e.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install "python-telegram-bot[job-queue]"
|
||||
|
||||
Examples:
|
||||
:any:`Timer Bot <examples.timerbot>`
|
||||
|
||||
.. seealso:: :wiki:`Architecture Overview <Architecture>`,
|
||||
:wiki:`Job Queue <Extensions---JobQueue>`
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
To use this class, PTB must be installed via
|
||||
``pip install "python-telegram-bot[job-queue]"``.
|
||||
|
||||
Attributes:
|
||||
scheduler (:class:`apscheduler.schedulers.asyncio.AsyncIOScheduler`): The scheduler.
|
||||
|
||||
Warning:
|
||||
This scheduler is configured by :meth:`set_application`. Additional configuration
|
||||
settings can be made by users. However, calling
|
||||
:meth:`~apscheduler.schedulers.base.BaseScheduler.configure` will delete any
|
||||
previous configuration settings. Therefore, please make sure to pass the values
|
||||
returned by :attr:`scheduler_configuration` to the method call in addition to your
|
||||
custom values.
|
||||
Alternatively, you can also use methods like
|
||||
:meth:`~apscheduler.schedulers.base.BaseScheduler.add_jobstore` to avoid using
|
||||
:meth:`~apscheduler.schedulers.base.BaseScheduler.configure` altogether.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Uses :class:`~apscheduler.schedulers.asyncio.AsyncIOScheduler` instead of
|
||||
:class:`~apscheduler.schedulers.background.BackgroundScheduler`
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("_application", "_executor", "scheduler")
|
||||
_CRON_MAPPING = ("sun", "mon", "tue", "wed", "thu", "fri", "sat")
|
||||
|
||||
def __init__(self) -> None:
|
||||
if not APS_AVAILABLE:
|
||||
raise RuntimeError(
|
||||
"To use `JobQueue`, PTB must be installed via `pip install "
|
||||
'"python-telegram-bot[job-queue]"`.'
|
||||
)
|
||||
|
||||
self._application: Optional[weakref.ReferenceType[Application]] = None
|
||||
self._executor = AsyncIOExecutor()
|
||||
self.scheduler: "AsyncIOScheduler" = AsyncIOScheduler( # noqa: UP037
|
||||
**self.scheduler_configuration
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Give a string representation of the JobQueue in the form ``JobQueue[application=...]``.
|
||||
|
||||
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, application=self.application)
|
||||
|
||||
@property
|
||||
def application(self) -> "Application[Any, CCT, Any, Any, Any, JobQueue[CCT]]":
|
||||
"""The application this JobQueue is associated with."""
|
||||
if self._application is None:
|
||||
raise RuntimeError("No application was set for this JobQueue.")
|
||||
application = self._application()
|
||||
if application is not None:
|
||||
return application
|
||||
raise RuntimeError("The application instance is no longer alive.")
|
||||
|
||||
@property
|
||||
def scheduler_configuration(self) -> JSONDict:
|
||||
"""Provides configuration values that are used by :class:`JobQueue` for :attr:`scheduler`.
|
||||
|
||||
Tip:
|
||||
Since calling
|
||||
:meth:`scheduler.configure() <apscheduler.schedulers.base.BaseScheduler.configure>`
|
||||
deletes any previous setting, please make sure to pass these values to the method call
|
||||
in addition to your custom values:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
scheduler.configure(..., **job_queue.scheduler_configuration)
|
||||
|
||||
Alternatively, you can also use methods like
|
||||
:meth:`~apscheduler.schedulers.base.BaseScheduler.add_jobstore` to avoid using
|
||||
:meth:`~apscheduler.schedulers.base.BaseScheduler.configure` altogether.
|
||||
|
||||
.. versionadded:: 20.7
|
||||
|
||||
Returns:
|
||||
dict[:obj:`str`, :obj:`object`]: The configuration values as dictionary.
|
||||
|
||||
"""
|
||||
timezone: object = pytz.utc
|
||||
if (
|
||||
self._application
|
||||
and isinstance(self.application.bot, ExtBot)
|
||||
and self.application.bot.defaults
|
||||
):
|
||||
timezone = self.application.bot.defaults.tzinfo or pytz.utc
|
||||
|
||||
return {
|
||||
"timezone": timezone,
|
||||
"executors": {"default": self._executor},
|
||||
}
|
||||
|
||||
def _tz_now(self) -> datetime.datetime:
|
||||
return datetime.datetime.now(self.scheduler.timezone)
|
||||
|
||||
@overload
|
||||
def _parse_time_input(self, time: None, shift_day: bool = False) -> None: ...
|
||||
|
||||
@overload
|
||||
def _parse_time_input(
|
||||
self,
|
||||
time: Union[float, datetime.timedelta, datetime.datetime, datetime.time],
|
||||
shift_day: bool = False,
|
||||
) -> datetime.datetime: ...
|
||||
|
||||
def _parse_time_input(
|
||||
self,
|
||||
time: Union[float, datetime.timedelta, datetime.datetime, datetime.time, None],
|
||||
shift_day: bool = False,
|
||||
) -> Optional[datetime.datetime]:
|
||||
if time is None:
|
||||
return None
|
||||
if isinstance(time, (int, float)):
|
||||
return self._tz_now() + datetime.timedelta(seconds=time)
|
||||
if isinstance(time, datetime.timedelta):
|
||||
return self._tz_now() + time
|
||||
if isinstance(time, datetime.time):
|
||||
date_time = datetime.datetime.combine(
|
||||
datetime.datetime.now(tz=time.tzinfo or self.scheduler.timezone).date(), time
|
||||
)
|
||||
if date_time.tzinfo is None:
|
||||
date_time = self.scheduler.timezone.localize(date_time)
|
||||
if shift_day and date_time <= datetime.datetime.now(pytz.utc):
|
||||
date_time += datetime.timedelta(days=1)
|
||||
return date_time
|
||||
return time
|
||||
|
||||
def set_application(
|
||||
self, application: "Application[Any, CCT, Any, Any, Any, JobQueue[CCT]]"
|
||||
) -> None:
|
||||
"""Set the application to be used by this JobQueue.
|
||||
|
||||
Args:
|
||||
application (:class:`telegram.ext.Application`): The application.
|
||||
|
||||
"""
|
||||
self._application = weakref.ref(application)
|
||||
self.scheduler.configure(**self.scheduler_configuration)
|
||||
|
||||
@staticmethod
|
||||
async def job_callback(job_queue: "JobQueue[CCT]", job: "Job[CCT]") -> None:
|
||||
"""This method is used as a callback for the APScheduler jobs.
|
||||
|
||||
More precisely, the ``func`` argument of :class:`apscheduler.job.Job` is set to this method
|
||||
and the ``arg`` argument (representing positional arguments to ``func``) is set to a tuple
|
||||
containing the :class:`JobQueue` itself and the :class:`~telegram.ext.Job` instance.
|
||||
|
||||
Tip:
|
||||
This method is a static method rather than a bound method. This makes the arguments
|
||||
more transparent and allows for easier handling of PTBs integration of APScheduler
|
||||
when utilizing advanced features of APScheduler.
|
||||
|
||||
Hint:
|
||||
This method is effectively a wrapper for :meth:`telegram.ext.Job.run`.
|
||||
|
||||
.. versionadded:: 20.4
|
||||
|
||||
Args:
|
||||
job_queue (:class:`JobQueue`): The job queue that created the job.
|
||||
job (:class:`~telegram.ext.Job`): The job to run.
|
||||
"""
|
||||
await job.run(job_queue.application)
|
||||
|
||||
def run_once(
|
||||
self,
|
||||
callback: JobCallback[CCT],
|
||||
when: Union[float, datetime.timedelta, datetime.datetime, datetime.time],
|
||||
data: Optional[object] = None,
|
||||
name: Optional[str] = None,
|
||||
chat_id: Optional[int] = None,
|
||||
user_id: Optional[int] = None,
|
||||
job_kwargs: Optional[JSONDict] = None,
|
||||
) -> "Job[CCT]":
|
||||
"""Creates a new :class:`Job` instance that runs once and adds it to the queue.
|
||||
|
||||
Args:
|
||||
callback (:term:`coroutine function`): The callback function that should be executed by
|
||||
the new job. Callback signature::
|
||||
|
||||
async def callback(context: CallbackContext)
|
||||
|
||||
when (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
|
||||
:obj:`datetime.datetime` | :obj:`datetime.time`):
|
||||
Time in or at which the job should run. This parameter will be interpreted
|
||||
depending on its type.
|
||||
|
||||
* :obj:`int` or :obj:`float` will be interpreted as "seconds from now" in which the
|
||||
job should run.
|
||||
* :obj:`datetime.timedelta` will be interpreted as "time from now" in which the
|
||||
job should run.
|
||||
* :obj:`datetime.datetime` will be interpreted as a specific date and time at
|
||||
which the job should run. If the timezone (:attr:`datetime.datetime.tzinfo`) is
|
||||
:obj:`None`, the default timezone of the bot will be used, which is UTC unless
|
||||
:attr:`telegram.ext.Defaults.tzinfo` is used.
|
||||
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
|
||||
job should run. This could be either today or, if the time has already passed,
|
||||
tomorrow. If the timezone (:attr:`datetime.time.tzinfo`) is :obj:`None`, the
|
||||
default timezone of the bot will be used, which is UTC unless
|
||||
:attr:`telegram.ext.Defaults.tzinfo` is used.
|
||||
|
||||
chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
user_id (:obj:`int`, optional): User id of the user associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
data (:obj:`object`, optional): Additional data needed for the callback function.
|
||||
Can be accessed through :attr:`Job.data` in the callback. Defaults to
|
||||
:obj:`None`.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Renamed the parameter ``context`` to :paramref:`data`.
|
||||
name (:obj:`str`, optional): The name of the new job. Defaults to
|
||||
:external:attr:`callback.__name__ <definition.__name__>`.
|
||||
job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
|
||||
:meth:`apscheduler.schedulers.base.BaseScheduler.add_job()`.
|
||||
|
||||
Returns:
|
||||
:class:`telegram.ext.Job`: The new :class:`Job` instance that has been added to the job
|
||||
queue.
|
||||
|
||||
"""
|
||||
if not job_kwargs:
|
||||
job_kwargs = {}
|
||||
|
||||
name = name or callback.__name__
|
||||
job = Job(callback=callback, data=data, name=name, chat_id=chat_id, user_id=user_id)
|
||||
date_time = self._parse_time_input(when, shift_day=True)
|
||||
|
||||
j = self.scheduler.add_job(
|
||||
self.job_callback,
|
||||
name=name,
|
||||
trigger="date",
|
||||
run_date=date_time,
|
||||
args=(self, job),
|
||||
timezone=date_time.tzinfo or self.scheduler.timezone,
|
||||
**job_kwargs,
|
||||
)
|
||||
|
||||
job._job = j # pylint: disable=protected-access
|
||||
return job
|
||||
|
||||
def run_repeating(
|
||||
self,
|
||||
callback: JobCallback[CCT],
|
||||
interval: Union[float, datetime.timedelta],
|
||||
first: Optional[Union[float, datetime.timedelta, datetime.datetime, datetime.time]] = None,
|
||||
last: Optional[Union[float, datetime.timedelta, datetime.datetime, datetime.time]] = None,
|
||||
data: Optional[object] = None,
|
||||
name: Optional[str] = None,
|
||||
chat_id: Optional[int] = None,
|
||||
user_id: Optional[int] = None,
|
||||
job_kwargs: Optional[JSONDict] = None,
|
||||
) -> "Job[CCT]":
|
||||
"""Creates a new :class:`Job` instance that runs at specified intervals and adds it to the
|
||||
queue.
|
||||
|
||||
Note:
|
||||
For a note about DST, please see the documentation of `APScheduler`_.
|
||||
|
||||
.. _`APScheduler`: https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html
|
||||
#daylight-saving-time-behavior
|
||||
|
||||
Args:
|
||||
callback (:term:`coroutine function`): The callback function that should be executed by
|
||||
the new job. Callback signature::
|
||||
|
||||
async def callback(context: CallbackContext)
|
||||
|
||||
interval (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta`): The interval in which
|
||||
the job will run. If it is an :obj:`int` or a :obj:`float`, it will be interpreted
|
||||
as seconds.
|
||||
first (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
|
||||
:obj:`datetime.datetime` | :obj:`datetime.time`, optional):
|
||||
Time in or at which the job should run. This parameter will be interpreted
|
||||
depending on its type.
|
||||
|
||||
* :obj:`int` or :obj:`float` will be interpreted as "seconds from now" in which the
|
||||
job should run.
|
||||
* :obj:`datetime.timedelta` will be interpreted as "time from now" in which the
|
||||
job should run.
|
||||
* :obj:`datetime.datetime` will be interpreted as a specific date and time at
|
||||
which the job should run. If the timezone (:attr:`datetime.datetime.tzinfo`) is
|
||||
:obj:`None`, the default timezone of the bot will be used.
|
||||
* :obj:`datetime.time` will be interpreted as a specific time of day at which the
|
||||
job should run. This could be either today or, if the time has already passed,
|
||||
tomorrow. If the timezone (:attr:`datetime.time.tzinfo`) is :obj:`None`, the
|
||||
default timezone of the bot will be used, which is UTC unless
|
||||
:attr:`telegram.ext.Defaults.tzinfo` is used.
|
||||
|
||||
Defaults to :paramref:`interval`
|
||||
|
||||
Note:
|
||||
Setting :paramref:`first` to ``0``, ``datetime.datetime.now()`` or another
|
||||
value that indicates that the job should run immediately will not work due
|
||||
to how the APScheduler library works. If you want to run a job immediately,
|
||||
we recommend to use an approach along the lines of::
|
||||
|
||||
job = context.job_queue.run_repeating(callback, interval=5)
|
||||
await job.run(context.application)
|
||||
|
||||
.. seealso:: :meth:`telegram.ext.Job.run`
|
||||
|
||||
last (:obj:`int` | :obj:`float` | :obj:`datetime.timedelta` | \
|
||||
:obj:`datetime.datetime` | :obj:`datetime.time`, optional):
|
||||
Latest possible time for the job to run. This parameter will be interpreted
|
||||
depending on its type. See :paramref:`first` for details.
|
||||
|
||||
If :paramref:`last` is :obj:`datetime.datetime` or :obj:`datetime.time` type
|
||||
and ``last.tzinfo`` is :obj:`None`, the default timezone of the bot will be
|
||||
assumed, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used.
|
||||
|
||||
Defaults to :obj:`None`.
|
||||
data (:obj:`object`, optional): Additional data needed for the callback function.
|
||||
Can be accessed through :attr:`Job.data` in the callback. Defaults to
|
||||
:obj:`None`.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Renamed the parameter ``context`` to :paramref:`data`.
|
||||
name (:obj:`str`, optional): The name of the new job. Defaults to
|
||||
:external:attr:`callback.__name__ <definition.__name__>`.
|
||||
chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
user_id (:obj:`int`, optional): User id of the user associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
|
||||
:meth:`apscheduler.schedulers.base.BaseScheduler.add_job()`.
|
||||
|
||||
Returns:
|
||||
:class:`telegram.ext.Job`: The new :class:`Job` instance that has been added to the job
|
||||
queue.
|
||||
|
||||
"""
|
||||
if not job_kwargs:
|
||||
job_kwargs = {}
|
||||
|
||||
name = name or callback.__name__
|
||||
job = Job(callback=callback, data=data, name=name, chat_id=chat_id, user_id=user_id)
|
||||
|
||||
dt_first = self._parse_time_input(first)
|
||||
dt_last = self._parse_time_input(last)
|
||||
|
||||
if dt_last and dt_first and dt_last < dt_first:
|
||||
raise ValueError("'last' must not be before 'first'!")
|
||||
|
||||
if isinstance(interval, datetime.timedelta):
|
||||
interval = interval.total_seconds()
|
||||
|
||||
j = self.scheduler.add_job(
|
||||
self.job_callback,
|
||||
trigger="interval",
|
||||
args=(self, job),
|
||||
start_date=dt_first,
|
||||
end_date=dt_last,
|
||||
seconds=interval,
|
||||
name=name,
|
||||
**job_kwargs,
|
||||
)
|
||||
|
||||
job._job = j # pylint: disable=protected-access
|
||||
return job
|
||||
|
||||
def run_monthly(
|
||||
self,
|
||||
callback: JobCallback[CCT],
|
||||
when: datetime.time,
|
||||
day: int,
|
||||
data: Optional[object] = None,
|
||||
name: Optional[str] = None,
|
||||
chat_id: Optional[int] = None,
|
||||
user_id: Optional[int] = None,
|
||||
job_kwargs: Optional[JSONDict] = None,
|
||||
) -> "Job[CCT]":
|
||||
"""Creates a new :class:`Job` that runs on a monthly basis and adds it to the queue.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
The ``day_is_strict`` argument was removed. Instead one can now pass ``-1`` to the
|
||||
:paramref:`day` parameter to have the job run on the last day of the month.
|
||||
|
||||
Args:
|
||||
callback (:term:`coroutine function`): The callback function that should be executed by
|
||||
the new job. Callback signature::
|
||||
|
||||
async def callback(context: CallbackContext)
|
||||
|
||||
when (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
|
||||
(``when.tzinfo``) is :obj:`None`, the default timezone of the bot will be used,
|
||||
which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used.
|
||||
day (:obj:`int`): Defines the day of the month whereby the job would run. It should
|
||||
be within the range of ``1`` and ``31``, inclusive. If a month has fewer days than
|
||||
this number, the job will not run in this month. Passing ``-1`` leads to the job
|
||||
running on the last day of the month.
|
||||
data (:obj:`object`, optional): Additional data needed for the callback function.
|
||||
Can be accessed through :attr:`Job.data` in the callback. Defaults to
|
||||
:obj:`None`.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Renamed the parameter ``context`` to :paramref:`data`.
|
||||
name (:obj:`str`, optional): The name of the new job. Defaults to
|
||||
:external:attr:`callback.__name__ <definition.__name__>`.
|
||||
chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
user_id (:obj:`int`, optional): User id of the user associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
|
||||
:meth:`apscheduler.schedulers.base.BaseScheduler.add_job()`.
|
||||
|
||||
Returns:
|
||||
:class:`telegram.ext.Job`: The new :class:`Job` instance that has been added to the job
|
||||
queue.
|
||||
|
||||
"""
|
||||
if not job_kwargs:
|
||||
job_kwargs = {}
|
||||
|
||||
name = name or callback.__name__
|
||||
job = Job(callback=callback, data=data, name=name, chat_id=chat_id, user_id=user_id)
|
||||
|
||||
j = self.scheduler.add_job(
|
||||
self.job_callback,
|
||||
trigger="cron",
|
||||
args=(self, job),
|
||||
name=name,
|
||||
day="last" if day == -1 else day,
|
||||
hour=when.hour,
|
||||
minute=when.minute,
|
||||
second=when.second,
|
||||
timezone=when.tzinfo or self.scheduler.timezone,
|
||||
**job_kwargs,
|
||||
)
|
||||
job._job = j # pylint: disable=protected-access
|
||||
return job
|
||||
|
||||
def run_daily(
|
||||
self,
|
||||
callback: JobCallback[CCT],
|
||||
time: datetime.time,
|
||||
days: tuple[int, ...] = _ALL_DAYS,
|
||||
data: Optional[object] = None,
|
||||
name: Optional[str] = None,
|
||||
chat_id: Optional[int] = None,
|
||||
user_id: Optional[int] = None,
|
||||
job_kwargs: Optional[JSONDict] = None,
|
||||
) -> "Job[CCT]":
|
||||
"""Creates a new :class:`Job` that runs on a daily basis and adds it to the queue.
|
||||
|
||||
Note:
|
||||
For a note about DST, please see the documentation of `APScheduler`_.
|
||||
|
||||
.. _`APScheduler`: https://apscheduler.readthedocs.io/en/stable/modules/triggers/cron.html
|
||||
#daylight-saving-time-behavior
|
||||
|
||||
Args:
|
||||
callback (:term:`coroutine function`): The callback function that should be executed by
|
||||
the new job. Callback signature::
|
||||
|
||||
async def callback(context: CallbackContext)
|
||||
|
||||
time (:obj:`datetime.time`): Time of day at which the job should run. If the timezone
|
||||
(:obj:`datetime.time.tzinfo`) is :obj:`None`, the default timezone of the bot will
|
||||
be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used.
|
||||
days (tuple[:obj:`int`], optional): Defines on which days of the week the job should
|
||||
run (where ``0-6`` correspond to sunday - saturday). By default, the job will run
|
||||
every day.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Changed day of the week mapping of 0-6 from monday-sunday to sunday-saturday.
|
||||
|
||||
data (:obj:`object`, optional): Additional data needed for the callback function.
|
||||
Can be accessed through :attr:`Job.data` in the callback. Defaults to
|
||||
:obj:`None`.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Renamed the parameter ``context`` to :paramref:`data`.
|
||||
name (:obj:`str`, optional): The name of the new job. Defaults to
|
||||
:external:attr:`callback.__name__ <definition.__name__>`.
|
||||
chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
user_id (:obj:`int`, optional): User id of the user associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
job_kwargs (:obj:`dict`, optional): Arbitrary keyword arguments to pass to the
|
||||
:meth:`apscheduler.schedulers.base.BaseScheduler.add_job()`.
|
||||
|
||||
Returns:
|
||||
:class:`telegram.ext.Job`: The new :class:`Job` instance that has been added to the job
|
||||
queue.
|
||||
|
||||
"""
|
||||
if not job_kwargs:
|
||||
job_kwargs = {}
|
||||
|
||||
name = name or callback.__name__
|
||||
job = Job(callback=callback, data=data, name=name, chat_id=chat_id, user_id=user_id)
|
||||
|
||||
j = self.scheduler.add_job(
|
||||
self.job_callback,
|
||||
name=name,
|
||||
args=(self, job),
|
||||
trigger="cron",
|
||||
day_of_week=",".join([self._CRON_MAPPING[d] for d in days]),
|
||||
hour=time.hour,
|
||||
minute=time.minute,
|
||||
second=time.second,
|
||||
timezone=time.tzinfo or self.scheduler.timezone,
|
||||
**job_kwargs,
|
||||
)
|
||||
|
||||
job._job = j # pylint: disable=protected-access
|
||||
return job
|
||||
|
||||
def run_custom(
|
||||
self,
|
||||
callback: JobCallback[CCT],
|
||||
job_kwargs: JSONDict,
|
||||
data: Optional[object] = None,
|
||||
name: Optional[str] = None,
|
||||
chat_id: Optional[int] = None,
|
||||
user_id: Optional[int] = None,
|
||||
) -> "Job[CCT]":
|
||||
"""Creates a new custom defined :class:`Job`.
|
||||
|
||||
Args:
|
||||
callback (:term:`coroutine function`): The callback function that should be executed by
|
||||
the new job. Callback signature::
|
||||
|
||||
async def callback(context: CallbackContext)
|
||||
|
||||
job_kwargs (:obj:`dict`): Arbitrary keyword arguments. Used as arguments for
|
||||
:meth:`apscheduler.schedulers.base.BaseScheduler.add_job`.
|
||||
data (:obj:`object`, optional): Additional data needed for the callback function.
|
||||
Can be accessed through :attr:`Job.data` in the callback. Defaults to
|
||||
:obj:`None`.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Renamed the parameter ``context`` to :paramref:`data`.
|
||||
name (:obj:`str`, optional): The name of the new job. Defaults to
|
||||
:external:attr:`callback.__name__ <definition.__name__>`.
|
||||
chat_id (:obj:`int`, optional): Chat id of the chat associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.chat_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
user_id (:obj:`int`, optional): User id of the user associated with this job. If
|
||||
passed, the corresponding :attr:`~telegram.ext.CallbackContext.user_data` will
|
||||
be available in the callback.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Returns:
|
||||
:class:`telegram.ext.Job`: The new :class:`Job` instance that has been added to the job
|
||||
queue.
|
||||
|
||||
"""
|
||||
name = name or callback.__name__
|
||||
job = Job(callback=callback, data=data, name=name, chat_id=chat_id, user_id=user_id)
|
||||
|
||||
j = self.scheduler.add_job(self.job_callback, args=(self, job), name=name, **job_kwargs)
|
||||
|
||||
job._job = j # pylint: disable=protected-access
|
||||
return job
|
||||
|
||||
async def start(self) -> None:
|
||||
# this method async just in case future versions need that
|
||||
"""Starts the :class:`~telegram.ext.JobQueue`."""
|
||||
if not self.scheduler.running:
|
||||
self.scheduler.start()
|
||||
|
||||
async def stop(self, wait: bool = True) -> None:
|
||||
"""Shuts down the :class:`~telegram.ext.JobQueue`.
|
||||
|
||||
Args:
|
||||
wait (:obj:`bool`, optional): Whether to wait until all currently running jobs
|
||||
have finished. Defaults to :obj:`True`.
|
||||
|
||||
"""
|
||||
# the interface methods of AsyncIOExecutor are currently not really asyncio-compatible
|
||||
# so we apply some small tweaks here to try and smoothen the integration into PTB
|
||||
# TODO: When APS 4.0 hits, we should be able to remove the tweaks
|
||||
if wait:
|
||||
# Unfortunately AsyncIOExecutor just cancels them all ...
|
||||
await asyncio.gather(
|
||||
*self._executor._pending_futures, # pylint: disable=protected-access
|
||||
return_exceptions=True,
|
||||
)
|
||||
if self.scheduler.running:
|
||||
self.scheduler.shutdown(wait=wait)
|
||||
# scheduler.shutdown schedules a task in the event loop but immediately returns
|
||||
# so give it a tiny bit of time to actually shut down.
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
def jobs(self) -> tuple["Job[CCT]", ...]:
|
||||
"""Returns a tuple of all *scheduled* jobs that are currently in the :class:`JobQueue`.
|
||||
|
||||
Returns:
|
||||
tuple[:class:`Job`]: Tuple of all *scheduled* jobs.
|
||||
"""
|
||||
return tuple(Job.from_aps_job(job) for job in self.scheduler.get_jobs())
|
||||
|
||||
def get_jobs_by_name(self, name: str) -> tuple["Job[CCT]", ...]:
|
||||
"""Returns a tuple of all *pending/scheduled* jobs with the given name that are currently
|
||||
in the :class:`JobQueue`.
|
||||
|
||||
Returns:
|
||||
tuple[:class:`Job`]: Tuple of all *pending* or *scheduled* jobs matching the name.
|
||||
"""
|
||||
return tuple(job for job in self.jobs() if job.name == name)
|
||||
|
||||
|
||||
class Job(Generic[CCT]):
|
||||
"""This class is a convenience wrapper for the jobs held in a :class:`telegram.ext.JobQueue`.
|
||||
With the current backend APScheduler, :attr:`job` holds a :class:`apscheduler.job.Job`
|
||||
instance.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :class:`id <apscheduler.job.Job>` is equal.
|
||||
|
||||
This class is a :class:`~typing.Generic` class and accepts one type variable that specifies
|
||||
the type of the argument ``context`` of :paramref:`callback`.
|
||||
|
||||
Important:
|
||||
If you want to use this class, you must install PTB with the optional requirement
|
||||
``job-queue``, i.e.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install "python-telegram-bot[job-queue]"
|
||||
|
||||
Note:
|
||||
All attributes and instance methods of :attr:`job` are also directly available as
|
||||
attributes/methods of the corresponding :class:`telegram.ext.Job` object.
|
||||
|
||||
Warning:
|
||||
This class should not be instantiated manually.
|
||||
Use the methods of :class:`telegram.ext.JobQueue` to schedule jobs.
|
||||
|
||||
.. seealso:: :wiki:`Job Queue <Extensions---JobQueue>`
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
|
||||
* Removed argument and attribute ``job_queue``.
|
||||
* Renamed ``Job.context`` to :attr:`Job.data`.
|
||||
* Removed argument ``job``
|
||||
* To use this class, PTB must be installed via
|
||||
``pip install "python-telegram-bot[job-queue]"``.
|
||||
|
||||
Args:
|
||||
callback (:term:`coroutine function`): The callback function that should be executed by the
|
||||
new job. Callback signature::
|
||||
|
||||
async def callback(context: CallbackContext)
|
||||
|
||||
data (:obj:`object`, optional): Additional data needed for the :paramref:`callback`
|
||||
function. Can be accessed through :attr:`Job.data` in the callback. Defaults to
|
||||
:obj:`None`.
|
||||
name (:obj:`str`, optional): The name of the new job. Defaults to
|
||||
:external:obj:`callback.__name__ <definition.__name__>`.
|
||||
chat_id (:obj:`int`, optional): Chat id of the chat that this job is associated with.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
user_id (:obj:`int`, optional): User id of the user that this job is associated with.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
Attributes:
|
||||
callback (:term:`coroutine function`): The callback function that should be executed by the
|
||||
new job.
|
||||
data (:obj:`object`): Optional. Additional data needed for the :attr:`callback` function.
|
||||
name (:obj:`str`): Optional. The name of the new job.
|
||||
chat_id (:obj:`int`): Optional. Chat id of the chat that this job is associated with.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
user_id (:obj:`int`): Optional. User id of the user that this job is associated with.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_enabled",
|
||||
"_job",
|
||||
"_removed",
|
||||
"callback",
|
||||
"chat_id",
|
||||
"data",
|
||||
"name",
|
||||
"user_id",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
callback: JobCallback[CCT],
|
||||
data: Optional[object] = None,
|
||||
name: Optional[str] = None,
|
||||
chat_id: Optional[int] = None,
|
||||
user_id: Optional[int] = None,
|
||||
):
|
||||
if not APS_AVAILABLE:
|
||||
raise RuntimeError(
|
||||
"To use `Job`, PTB must be installed via `pip install "
|
||||
'"python-telegram-bot[job-queue]"`.'
|
||||
)
|
||||
|
||||
self.callback: JobCallback[CCT] = callback
|
||||
self.data: Optional[object] = data
|
||||
self.name: Optional[str] = name or callback.__name__
|
||||
self.chat_id: Optional[int] = chat_id
|
||||
self.user_id: Optional[int] = user_id
|
||||
|
||||
self._removed = False
|
||||
self._enabled = False
|
||||
|
||||
self._job = cast("APSJob", None)
|
||||
|
||||
def __getattr__(self, item: str) -> object:
|
||||
"""Overrides :py:meth:`object.__getattr__` to get specific attribute of the
|
||||
:class:`telegram.ext.Job` object or of its attribute :class:`apscheduler.job.Job`,
|
||||
if exists.
|
||||
|
||||
Args:
|
||||
item (:obj:`str`): The name of the attribute.
|
||||
|
||||
Returns:
|
||||
:object: The value of the attribute.
|
||||
|
||||
Raises:
|
||||
:exc:`AttributeError`: If the attribute does not exist in both
|
||||
:class:`telegram.ext.Job` and :class:`apscheduler.job.Job` objects.
|
||||
"""
|
||||
try:
|
||||
return getattr(self.job, item)
|
||||
except AttributeError as exc:
|
||||
raise AttributeError(
|
||||
f"Neither 'telegram.ext.Job' nor 'apscheduler.job.Job' has attribute '{item}'"
|
||||
) from exc
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""Defines equality condition for the :class:`telegram.ext.Job` object.
|
||||
Two objects of this class are considered to be equal if their
|
||||
:class:`id <apscheduler.job.Job>` are equal.
|
||||
|
||||
Returns:
|
||||
:obj:`True` if both objects have :paramref:`id` parameters identical.
|
||||
:obj:`False` otherwise.
|
||||
"""
|
||||
if isinstance(other, self.__class__):
|
||||
return self.id == other.id
|
||||
return False
|
||||
|
||||
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.id)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Give a string representation of the job in the form
|
||||
``Job[id=..., name=..., callback=..., trigger=...]``.
|
||||
|
||||
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,
|
||||
id=self.job.id,
|
||||
name=self.name,
|
||||
callback=self.callback.__name__,
|
||||
trigger=self.job.trigger,
|
||||
)
|
||||
|
||||
@property
|
||||
def job(self) -> "APSJob":
|
||||
""":class:`apscheduler.job.Job`: The APS Job this job is a wrapper for.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
This property is now read-only.
|
||||
"""
|
||||
return self._job
|
||||
|
||||
@property
|
||||
def removed(self) -> bool:
|
||||
""":obj:`bool`: Whether this job is due to be removed."""
|
||||
return self._removed
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
""":obj:`bool`: Whether this job is enabled."""
|
||||
return self._enabled
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, status: bool) -> None:
|
||||
if status:
|
||||
self.job.resume()
|
||||
else:
|
||||
self.job.pause()
|
||||
self._enabled = status
|
||||
|
||||
@property
|
||||
def next_t(self) -> Optional[datetime.datetime]:
|
||||
"""
|
||||
:class:`datetime.datetime`: Datetime for the next job execution.
|
||||
Datetime is localized according to :attr:`datetime.datetime.tzinfo`.
|
||||
If job is removed or already ran it equals to :obj:`None`.
|
||||
|
||||
Warning:
|
||||
This attribute is only available, if the :class:`telegram.ext.JobQueue` this job
|
||||
belongs to is already started. Otherwise APScheduler raises an :exc:`AttributeError`.
|
||||
"""
|
||||
return self.job.next_run_time
|
||||
|
||||
@classmethod
|
||||
def from_aps_job(cls, aps_job: "APSJob") -> "Job[CCT]":
|
||||
"""Provides the :class:`telegram.ext.Job` that is associated with the given APScheduler
|
||||
job.
|
||||
|
||||
Tip:
|
||||
This method can be useful when using advanced APScheduler features along with
|
||||
:class:`telegram.ext.JobQueue`.
|
||||
|
||||
.. versionadded:: 20.4
|
||||
|
||||
Args:
|
||||
aps_job (:class:`apscheduler.job.Job`): The APScheduler job
|
||||
|
||||
Returns:
|
||||
:class:`telegram.ext.Job`
|
||||
"""
|
||||
ext_job = aps_job.args[1]
|
||||
ext_job._job = aps_job # pylint: disable=protected-access
|
||||
return ext_job
|
||||
|
||||
async def run(
|
||||
self, application: "Application[Any, CCT, Any, Any, Any, JobQueue[CCT]]"
|
||||
) -> None:
|
||||
"""Executes the callback function independently of the jobs schedule. Also calls
|
||||
:meth:`telegram.ext.Application.update_persistence`.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
Calls :meth:`telegram.ext.Application.update_persistence`.
|
||||
|
||||
Args:
|
||||
application (:class:`telegram.ext.Application`): The application this job is associated
|
||||
with.
|
||||
"""
|
||||
# We shield the task such that the job isn't cancelled mid-run
|
||||
await asyncio.shield(self._run(application))
|
||||
|
||||
async def _run(
|
||||
self, application: "Application[Any, CCT, Any, Any, Any, JobQueue[CCT]]"
|
||||
) -> None:
|
||||
try:
|
||||
try:
|
||||
context = application.context_types.context.from_job(self, application)
|
||||
except Exception as exc:
|
||||
_LOGGER.critical(
|
||||
"Error while building CallbackContext for job %s. Job will not be run.",
|
||||
self._job,
|
||||
exc_info=exc,
|
||||
)
|
||||
return
|
||||
|
||||
await context.refresh_data()
|
||||
await self.callback(context)
|
||||
except Exception as exc:
|
||||
await application.create_task(
|
||||
application.process_error(None, exc, job=self),
|
||||
name=f"Job:{self.id}:run:process_error",
|
||||
)
|
||||
finally:
|
||||
# This is internal logic of application - let's keep it private for now
|
||||
application._mark_for_persistence_update(job=self) # pylint: disable=protected-access
|
||||
|
||||
def schedule_removal(self) -> None:
|
||||
"""
|
||||
Schedules this job for removal from the :class:`JobQueue`. It will be removed without
|
||||
executing its callback function again.
|
||||
"""
|
||||
self.job.remove()
|
||||
self._removed = True
|
||||
@@ -0,0 +1,562 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# 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)
|
||||
897
.venv/lib/python3.10/site-packages/telegram/ext/_updater.py
Normal file
897
.venv/lib/python3.10/site-packages/telegram/ext/_updater.py
Normal file
@@ -0,0 +1,897 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# 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 ssl
|
||||
from collections.abc import Coroutine
|
||||
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, DEFAULT_NONE, DefaultValue
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.repr import build_repr_with_selected_attrs
|
||||
from telegram._utils.types import DVType, ODVInput
|
||||
from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut
|
||||
|
||||
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: # noqa: PYI019
|
||||
"""
|
||||
|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: int = 10, # noqa: ASYNC109
|
||||
bootstrap_retries: int = -1,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
write_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
allowed_updates: Optional[list[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`.
|
||||
|
||||
Args:
|
||||
poll_interval (:obj:`float`, optional): Time to wait between polling updates from
|
||||
Telegram in seconds. Default is ``0.0``.
|
||||
timeout (:obj:`int`, optional): Passed to
|
||||
:paramref:`telegram.Bot.get_updates.timeout`. Defaults to ``10`` seconds.
|
||||
bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of the
|
||||
:class:`telegram.ext.Updater` will retry on failures on the Telegram server.
|
||||
|
||||
* < 0 - retry indefinitely (default)
|
||||
* 0 - no retries
|
||||
* > 0 - retry up to X times
|
||||
read_timeout (:obj:`float`, optional): Value to pass to
|
||||
:paramref:`telegram.Bot.get_updates.read_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
|
||||
.. versionchanged:: 20.7
|
||||
Defaults to :attr:`~telegram.request.BaseRequest.DEFAULT_NONE` instead of
|
||||
``2``.
|
||||
.. deprecated:: 20.7
|
||||
Deprecated in favor of setting the timeout via
|
||||
:meth:`telegram.ext.ApplicationBuilder.get_updates_read_timeout` or
|
||||
:paramref:`telegram.Bot.get_updates_request`.
|
||||
write_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.Bot.get_updates.write_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
|
||||
.. deprecated:: 20.7
|
||||
Deprecated in favor of setting the timeout via
|
||||
:meth:`telegram.ext.ApplicationBuilder.get_updates_write_timeout` or
|
||||
:paramref:`telegram.Bot.get_updates_request`.
|
||||
connect_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.Bot.get_updates.connect_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
|
||||
.. deprecated:: 20.7
|
||||
Deprecated in favor of setting the timeout via
|
||||
:meth:`telegram.ext.ApplicationBuilder.get_updates_connect_timeout` or
|
||||
:paramref:`telegram.Bot.get_updates_request`.
|
||||
pool_timeout (:obj:`float` | :obj:`None`, optional): Value to pass to
|
||||
:paramref:`telegram.Bot.get_updates.pool_timeout`. Defaults to
|
||||
:attr:`~telegram.request.BaseRequest.DEFAULT_NONE`.
|
||||
|
||||
.. deprecated:: 20.7
|
||||
Deprecated in favor of setting the timeout via
|
||||
:meth:`telegram.ext.ApplicationBuilder.get_updates_pool_timeout` or
|
||||
:paramref:`telegram.Bot.get_updates_request`.
|
||||
allowed_updates (list[:obj:`str`], optional): Passed to
|
||||
:meth:`telegram.Bot.get_updates`.
|
||||
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,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_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: int, # noqa: ASYNC109
|
||||
read_timeout: ODVInput[float],
|
||||
write_timeout: ODVInput[float],
|
||||
connect_timeout: ODVInput[float],
|
||||
pool_timeout: ODVInput[float],
|
||||
bootstrap_retries: int,
|
||||
drop_pending_updates: Optional[bool],
|
||||
allowed_updates: Optional[list[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,
|
||||
read_timeout=read_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
write_timeout=write_timeout,
|
||||
pool_timeout=pool_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(
|
||||
self._network_loop_retry(
|
||||
action_cb=polling_action_cb,
|
||||
on_err_cb=error_callback or default_error_callback,
|
||||
description="getting Updates",
|
||||
interval=poll_interval,
|
||||
stop_event=self.__polling_task_stop_event,
|
||||
),
|
||||
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=0,
|
||||
read_timeout=read_timeout,
|
||||
connect_timeout=connect_timeout,
|
||||
write_timeout=write_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
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[list[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 the
|
||||
:class:`telegram.ext.Updater` 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 (list[:obj:`str`], optional): Passed to
|
||||
:meth:`telegram.Bot.set_webhook`. Defaults to :obj:`None`.
|
||||
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[list[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 _network_loop_retry(
|
||||
self,
|
||||
action_cb: Callable[..., Coroutine],
|
||||
on_err_cb: Callable[[TelegramError], None],
|
||||
description: str,
|
||||
interval: float,
|
||||
stop_event: Optional[asyncio.Event],
|
||||
) -> None:
|
||||
"""Perform a loop calling `action_cb`, retrying after network errors.
|
||||
|
||||
Stop condition for loop: `self.running` evaluates :obj:`False` or return value of
|
||||
`action_cb` evaluates :obj:`False`.
|
||||
|
||||
Args:
|
||||
action_cb (:term:`coroutine function`): Network oriented callback function to call.
|
||||
on_err_cb (:obj:`callable`): Callback to call when TelegramError is caught. Receives
|
||||
the exception object as a parameter.
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
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("Network loop retry %s was cancelled", description)
|
||||
return False
|
||||
|
||||
return action_cb_task.result()
|
||||
|
||||
_LOGGER.debug("Start network loop retry %s", description)
|
||||
cur_interval = interval
|
||||
while self.running:
|
||||
try:
|
||||
if not await do_action():
|
||||
break
|
||||
except RetryAfter as exc:
|
||||
_LOGGER.info("%s", exc)
|
||||
cur_interval = 0.5 + exc.retry_after
|
||||
except TimedOut as toe:
|
||||
_LOGGER.debug("Timed out %s: %s", description, toe)
|
||||
# If failure is due to timeout, we should retry asap.
|
||||
cur_interval = 0
|
||||
except InvalidToken:
|
||||
_LOGGER.exception("Invalid token; aborting")
|
||||
raise
|
||||
except TelegramError as telegram_exc:
|
||||
on_err_cb(telegram_exc)
|
||||
|
||||
# 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
|
||||
|
||||
if cur_interval:
|
||||
await asyncio.sleep(cur_interval)
|
||||
|
||||
async def _bootstrap(
|
||||
self,
|
||||
max_retries: int,
|
||||
webhook_url: Optional[str],
|
||||
allowed_updates: Optional[list[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`.
|
||||
"""
|
||||
retries = 0
|
||||
|
||||
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
|
||||
|
||||
def bootstrap_on_err_cb(exc: Exception) -> None:
|
||||
# We need this since retries is an immutable object otherwise and the changes
|
||||
# wouldn't propagate outside of thi function
|
||||
nonlocal retries
|
||||
|
||||
if not isinstance(exc, InvalidToken) and (max_retries < 0 or retries < max_retries):
|
||||
retries += 1
|
||||
_LOGGER.warning(
|
||||
"Failed bootstrap phase; try=%s max_retries=%s", retries, max_retries
|
||||
)
|
||||
else:
|
||||
_LOGGER.error("Failed bootstrap phase after %s retries (%s)", retries, exc)
|
||||
raise exc
|
||||
|
||||
# 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 self._network_loop_retry(
|
||||
bootstrap_del_webhook,
|
||||
bootstrap_on_err_cb,
|
||||
"bootstrap del webhook",
|
||||
bootstrap_interval,
|
||||
stop_event=None,
|
||||
)
|
||||
|
||||
# Reset the retries counter for the next _network_loop_retry call
|
||||
retries = 0
|
||||
|
||||
# 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 self._network_loop_retry(
|
||||
bootstrap_set_webhook,
|
||||
bootstrap_on_err_cb,
|
||||
"bootstrap set webhook",
|
||||
bootstrap_interval,
|
||||
stop_event=None,
|
||||
)
|
||||
|
||||
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-2024
|
||||
# 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/].
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# 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,76 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# 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-2024
|
||||
# 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.10/site-packages/telegram/ext/_utils/types.py
Normal file
106
.venv/lib/python3.10/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-2024
|
||||
# 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-2024
|
||||
# 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,
|
||||
)
|
||||
2939
.venv/lib/python3.10/site-packages/telegram/ext/filters.py
Normal file
2939
.venv/lib/python3.10/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