a begin that works
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
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,50 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains helper functions related to parsing updates and their contents.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
Warning:
|
||||
Contents of this module are intended to be used internally by the library and *not* by the
|
||||
user. Changes to this module are not considered breaking changes and may not be documented in
|
||||
the changelog.
|
||||
"""
|
||||
from typing import Optional
|
||||
|
||||
from telegram._utils.types import SCT
|
||||
|
||||
|
||||
def parse_chat_id(chat_id: Optional[SCT[int]]) -> frozenset[int]:
|
||||
"""Accepts a chat id or collection of chat ids and returns a frozenset of chat ids."""
|
||||
if chat_id is None:
|
||||
return frozenset()
|
||||
if isinstance(chat_id, int):
|
||||
return frozenset({chat_id})
|
||||
return frozenset(chat_id)
|
||||
|
||||
|
||||
def parse_username(username: Optional[SCT[str]]) -> frozenset[str]:
|
||||
"""Accepts a username or collection of usernames and returns a frozenset of usernames.
|
||||
Strips the leading ``@`` if present.
|
||||
"""
|
||||
if username is None:
|
||||
return frozenset()
|
||||
if isinstance(username, str):
|
||||
return frozenset({username.removeprefix("@")})
|
||||
return frozenset(usr.removeprefix("@") for usr in username)
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains helper functions related to the std-lib asyncio module.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
|
||||
Warning:
|
||||
Contents of this module are intended to be used internally by the library and *not* by the
|
||||
user. Changes to this module are not considered breaking changes and may not be documented in
|
||||
the changelog.
|
||||
"""
|
||||
import asyncio
|
||||
from typing import Literal
|
||||
|
||||
|
||||
class TrackedBoundedSemaphore(asyncio.BoundedSemaphore):
|
||||
"""Simple subclass of :class:`asyncio.BoundedSemaphore` that tracks the current value of the
|
||||
semaphore. While there is an attribute ``_value`` in the superclass, it's private and we
|
||||
don't want to rely on it.
|
||||
"""
|
||||
|
||||
__slots__ = ("_current_value",)
|
||||
|
||||
def __init__(self, value: int = 1) -> None:
|
||||
super().__init__(value)
|
||||
self._current_value = value
|
||||
|
||||
@property
|
||||
def current_value(self) -> int:
|
||||
return self._current_value
|
||||
|
||||
async def acquire(self) -> Literal[True]:
|
||||
await super().acquire()
|
||||
self._current_value -= 1
|
||||
return True
|
||||
|
||||
def release(self) -> None:
|
||||
super().release()
|
||||
self._current_value += 1
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains a network retry loop implementation.
|
||||
Its specifically tailored to handling the Telegram API and its errors.
|
||||
|
||||
.. versionadded:: 21.11
|
||||
|
||||
Hint:
|
||||
It was originally part of the `Updater` class, but as part of #4657 it was extracted into its
|
||||
own module to be used by other parts of the library.
|
||||
|
||||
Warning:
|
||||
Contents of this module are intended to be used internally by the library and *not* by the
|
||||
user. Changes to this module are not considered breaking changes and may not be documented in
|
||||
the changelog.
|
||||
"""
|
||||
import asyncio
|
||||
import contextlib
|
||||
from collections.abc import Coroutine
|
||||
from typing import Callable, Optional
|
||||
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram.error import InvalidToken, RetryAfter, TelegramError, TimedOut
|
||||
|
||||
_LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
async def network_retry_loop(
|
||||
*,
|
||||
action_cb: Callable[..., Coroutine],
|
||||
on_err_cb: Optional[Callable[[TelegramError], None]] = None,
|
||||
description: str,
|
||||
interval: float,
|
||||
stop_event: Optional[asyncio.Event] = None,
|
||||
is_running: Optional[Callable[[], bool]] = None,
|
||||
max_retries: int,
|
||||
) -> None:
|
||||
"""Perform a loop calling `action_cb`, retrying after network errors.
|
||||
|
||||
Stop condition for loop:
|
||||
* `is_running()` evaluates :obj:`False` or
|
||||
* return value of `action_cb` evaluates :obj:`False`
|
||||
* or `stop_event` is set.
|
||||
* or `max_retries` is reached.
|
||||
|
||||
Args:
|
||||
action_cb (:term:`coroutine function`): Network oriented callback function to call.
|
||||
on_err_cb (:obj:`callable`): Optional. Callback to call when TelegramError is caught.
|
||||
Receives the exception object as a parameter.
|
||||
|
||||
Hint:
|
||||
Only required if you want to handle the error in a special way. Logging about
|
||||
the error is already handled by the loop.
|
||||
|
||||
Important:
|
||||
Must not raise exceptions! If it does, the loop will be aborted.
|
||||
description (:obj:`str`): Description text to use for logs and exception raised.
|
||||
interval (:obj:`float` | :obj:`int`): Interval to sleep between each call to
|
||||
`action_cb`.
|
||||
stop_event (:class:`asyncio.Event` | :obj:`None`): Event to wait on for stopping the
|
||||
loop. Setting the event will make the loop exit even if `action_cb` is currently
|
||||
running. Defaults to :obj:`None`.
|
||||
is_running (:obj:`callable`): Function to check if the loop should continue running.
|
||||
Must return a boolean value. Defaults to `lambda: True`.
|
||||
max_retries (:obj:`int`): Maximum number of retries before stopping the loop.
|
||||
|
||||
* < 0: Retry indefinitely.
|
||||
* 0: No retries.
|
||||
* > 0: Number of retries.
|
||||
|
||||
"""
|
||||
log_prefix = f"Network Retry Loop ({description}):"
|
||||
effective_is_running = is_running or (lambda: True)
|
||||
|
||||
async def do_action() -> bool:
|
||||
if not stop_event:
|
||||
return await action_cb()
|
||||
|
||||
action_cb_task = asyncio.create_task(action_cb())
|
||||
stop_task = asyncio.create_task(stop_event.wait())
|
||||
done, pending = await asyncio.wait(
|
||||
(action_cb_task, stop_task), return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
||||
if stop_task in done:
|
||||
_LOGGER.debug("%s Cancelled", log_prefix)
|
||||
return False
|
||||
|
||||
return action_cb_task.result()
|
||||
|
||||
_LOGGER.debug("%s Starting", log_prefix)
|
||||
cur_interval = interval
|
||||
retries = 0
|
||||
while effective_is_running():
|
||||
try:
|
||||
if not await do_action():
|
||||
break
|
||||
except RetryAfter as exc:
|
||||
slack_time = 0.5
|
||||
_LOGGER.info(
|
||||
"%s %s. Adding %s seconds to the specified time.", log_prefix, exc, slack_time
|
||||
)
|
||||
# pylint: disable=protected-access
|
||||
cur_interval = slack_time + exc._retry_after.total_seconds()
|
||||
except TimedOut as toe:
|
||||
_LOGGER.debug("%s Timed out: %s. Retrying immediately.", log_prefix, toe)
|
||||
# If failure is due to timeout, we should retry asap.
|
||||
cur_interval = 0
|
||||
except InvalidToken:
|
||||
_LOGGER.exception("%s Invalid token. Aborting retry loop.", log_prefix)
|
||||
raise
|
||||
except TelegramError as telegram_exc:
|
||||
if on_err_cb:
|
||||
on_err_cb(telegram_exc)
|
||||
|
||||
if max_retries < 0 or retries < max_retries:
|
||||
_LOGGER.debug(
|
||||
"%s Failed run number %s of %s. Retrying.", log_prefix, retries, max_retries
|
||||
)
|
||||
else:
|
||||
_LOGGER.exception(
|
||||
"%s Failed run number %s of %s. Aborting.", log_prefix, retries, max_retries
|
||||
)
|
||||
raise
|
||||
|
||||
# increase waiting times on subsequent errors up to 30secs
|
||||
cur_interval = 1 if cur_interval == 0 else min(30, 1.5 * cur_interval)
|
||||
else:
|
||||
cur_interval = interval
|
||||
finally:
|
||||
retries += 1
|
||||
|
||||
if cur_interval:
|
||||
await asyncio.sleep(cur_interval)
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains helper functions related to inspecting the program stack.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Warning:
|
||||
Contents of this module are intended to be used internally by the library and *not* by the
|
||||
user. Changes to this module are not considered breaking changes and may not be documented in
|
||||
the changelog.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from types import FrameType
|
||||
from typing import Optional
|
||||
|
||||
from telegram._utils.logging import get_logger
|
||||
|
||||
_LOGGER = get_logger(__name__)
|
||||
|
||||
|
||||
def was_called_by(frame: Optional[FrameType], caller: Path) -> bool:
|
||||
"""Checks if the passed frame was called by the specified file.
|
||||
|
||||
Example:
|
||||
.. code:: pycon
|
||||
|
||||
>>> was_called_by(inspect.currentframe(), Path(__file__))
|
||||
True
|
||||
|
||||
Arguments:
|
||||
frame (:obj:`FrameType`): The frame - usually the return value of
|
||||
``inspect.currentframe()``. If :obj:`None` is passed, the return value will be
|
||||
:obj:`False`.
|
||||
caller (:obj:`pathlib.Path`): File that should be the caller.
|
||||
|
||||
Returns:
|
||||
:obj:`bool`: Whether the frame was called by the specified file.
|
||||
"""
|
||||
if frame is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
return _was_called_by(frame, caller)
|
||||
except Exception as exc:
|
||||
_LOGGER.debug(
|
||||
"Failed to check if frame was called by `caller`. Assuming that it was not.",
|
||||
exc_info=exc,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _was_called_by(frame: FrameType, caller: Path) -> bool:
|
||||
# https://stackoverflow.com/a/57712700/10606962
|
||||
if Path(frame.f_code.co_filename).resolve() == caller:
|
||||
return True
|
||||
while frame.f_back:
|
||||
frame = frame.f_back
|
||||
if Path(frame.f_code.co_filename).resolve() == caller:
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains a mutable mapping that keeps track of the keys that where accessed.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Warning:
|
||||
Contents of this module are intended to be used internally by the library and *not* by the
|
||||
user. Changes to this module are not considered breaking changes and may not be documented in
|
||||
the changelog.
|
||||
"""
|
||||
from collections import UserDict
|
||||
from collections.abc import Mapping
|
||||
from typing import Final, Generic, Optional, TypeVar, Union
|
||||
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
|
||||
|
||||
_VT = TypeVar("_VT")
|
||||
_KT = TypeVar("_KT")
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class TrackingDict(UserDict, Generic[_KT, _VT]):
|
||||
"""Mutable mapping that keeps track of which keys where accessed with write access.
|
||||
Read-access is not tracked.
|
||||
|
||||
Note:
|
||||
* ``setdefault()`` and ``pop`` are considered writing only depending on whether the
|
||||
key is present
|
||||
* deleting values is considered writing
|
||||
"""
|
||||
|
||||
DELETED: Final = object()
|
||||
"""Special marker indicating that an entry was deleted."""
|
||||
|
||||
__slots__ = ("_write_access_keys",)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._write_access_keys: set[_KT] = set()
|
||||
|
||||
def __setitem__(self, key: _KT, value: _VT) -> None:
|
||||
self.__track_write(key)
|
||||
super().__setitem__(key, value)
|
||||
|
||||
def __delitem__(self, key: _KT) -> None:
|
||||
self.__track_write(key)
|
||||
super().__delitem__(key)
|
||||
|
||||
def __track_write(self, key: Union[_KT, set[_KT]]) -> None:
|
||||
if isinstance(key, set):
|
||||
self._write_access_keys |= key
|
||||
else:
|
||||
self._write_access_keys.add(key)
|
||||
|
||||
def pop_accessed_keys(self) -> set[_KT]:
|
||||
"""Returns all keys that were write-accessed since the last time this method was called."""
|
||||
out = self._write_access_keys
|
||||
self._write_access_keys = set()
|
||||
return out
|
||||
|
||||
def pop_accessed_write_items(self) -> list[tuple[_KT, _VT]]:
|
||||
"""
|
||||
Returns all keys & corresponding values as set of tuples that were write-accessed since
|
||||
the last time this method was called. If a key was deleted, the value will be
|
||||
:attr:`DELETED`.
|
||||
"""
|
||||
keys = self.pop_accessed_keys()
|
||||
return [(key, self.get(key, self.DELETED)) for key in keys]
|
||||
|
||||
def mark_as_accessed(self, key: _KT) -> None:
|
||||
"""Use this method have the key returned again in the next call to
|
||||
:meth:`pop_accessed_write_items` or :meth:`pop_accessed_keys`
|
||||
"""
|
||||
self._write_access_keys.add(key)
|
||||
|
||||
# Override methods to track access
|
||||
|
||||
def update_no_track(self, mapping: Mapping[_KT, _VT]) -> None:
|
||||
"""Like ``update``, but doesn't count towards write access."""
|
||||
for key, value in mapping.items():
|
||||
self.data[key] = value
|
||||
|
||||
# Mypy seems a bit inconsistent about what it wants as types for `default` and return value
|
||||
# so we just ignore a bit
|
||||
def pop( # type: ignore[override]
|
||||
self,
|
||||
key: _KT,
|
||||
default: _VT = DEFAULT_NONE, # type: ignore[assignment]
|
||||
) -> _VT:
|
||||
if key in self:
|
||||
self.__track_write(key)
|
||||
if isinstance(default, DefaultValue):
|
||||
return super().pop(key)
|
||||
return super().pop(key, default=default)
|
||||
|
||||
def clear(self) -> None:
|
||||
self.__track_write(set(super().keys()))
|
||||
super().clear()
|
||||
|
||||
# Mypy seems a bit inconsistent about what it wants as types for `default` and return value
|
||||
# so we just ignore a bit
|
||||
def setdefault(self: "TrackingDict[_KT, _T]", key: _KT, default: Optional[_T] = None) -> _T:
|
||||
if key in self:
|
||||
return self[key]
|
||||
|
||||
self.__track_write(key)
|
||||
self[key] = default # type: ignore[assignment]
|
||||
return default # type: ignore[return-value]
|
||||
106
venv/lib/python3.12/site-packages/telegram/ext/_utils/types.py
Normal file
106
venv/lib/python3.12/site-packages/telegram/ext/_utils/types.py
Normal file
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains custom typing aliases.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
|
||||
Warning:
|
||||
Contents of this module are intended to be used internally by the library and *not* by the
|
||||
user. Changes to this module are not considered breaking changes and may not be documented in
|
||||
the changelog.
|
||||
"""
|
||||
from collections.abc import Coroutine, MutableMapping
|
||||
from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional
|
||||
|
||||
from telegram import Bot
|
||||
from telegram.ext import BaseRateLimiter, CallbackContext, JobQueue
|
||||
|
||||
CCT = TypeVar("CCT", bound="CallbackContext[Any, Any, Any, Any]")
|
||||
"""An instance of :class:`telegram.ext.CallbackContext` or a custom subclass.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
"""
|
||||
|
||||
RT = TypeVar("RT")
|
||||
UT = TypeVar("UT")
|
||||
HandlerCallback = Callable[[UT, CCT], Coroutine[Any, Any, RT]]
|
||||
"""Type of a handler callback
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
JobCallback = Callable[[CCT], Coroutine[Any, Any, Any]]
|
||||
"""Type of a job callback
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
|
||||
ConversationKey = tuple[Union[int, str], ...]
|
||||
ConversationDict = MutableMapping[ConversationKey, object]
|
||||
"""dict[tuple[:obj:`int` | :obj:`str`, ...], Optional[:obj:`object`]]:
|
||||
Dicts as maintained by the :class:`telegram.ext.ConversationHandler`.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
"""
|
||||
|
||||
CDCData = tuple[list[tuple[str, float, dict[str, Any]]], dict[str, str]]
|
||||
"""tuple[list[tuple[:obj:`str`, :obj:`float`, dict[:obj:`str`, :class:`object`]]], \
|
||||
dict[:obj:`str`, :obj:`str`]]: Data returned by
|
||||
:attr:`telegram.ext.CallbackDataCache.persistence_data`.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
"""
|
||||
|
||||
BT = TypeVar("BT", bound="Bot")
|
||||
"""Type of the bot.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
UD = TypeVar("UD")
|
||||
"""Type of the user data for a single user.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
"""
|
||||
CD = TypeVar("CD")
|
||||
"""Type of the chat data for a single user.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
"""
|
||||
BD = TypeVar("BD")
|
||||
"""Type of the bot data.
|
||||
|
||||
.. versionadded:: 13.6
|
||||
"""
|
||||
JQ = TypeVar("JQ", bound=Union[None, "JobQueue"])
|
||||
"""Type of the job queue.
|
||||
|
||||
.. versionadded:: 20.0"""
|
||||
|
||||
RL = TypeVar("RL", bound="Optional[BaseRateLimiter]")
|
||||
"""Type of the rate limiter.
|
||||
|
||||
.. versionadded:: 20.0"""
|
||||
|
||||
RLARGS = TypeVar("RLARGS")
|
||||
"""Type of the rate limiter arguments.
|
||||
|
||||
.. versionadded:: 20.0"""
|
||||
FilterDataDict = dict[str, list[Any]]
|
||||
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2025
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
# pylint: disable=missing-module-docstring
|
||||
import asyncio
|
||||
import json
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from socket import socket
|
||||
from ssl import SSLContext
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
# Instead of checking for ImportError here, we do that in `updater.py`, where we import from
|
||||
# this module. Doing it here would be tricky, as the classes below subclass tornado classes
|
||||
import tornado.web
|
||||
from tornado.httpserver import HTTPServer
|
||||
|
||||
try:
|
||||
from tornado.netutil import bind_unix_socket
|
||||
|
||||
UNIX_AVAILABLE = True
|
||||
except ImportError:
|
||||
UNIX_AVAILABLE = False
|
||||
|
||||
from telegram import Update
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram.ext._extbot import ExtBot
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Bot
|
||||
|
||||
# This module is not visible to users, so we log as Updater
|
||||
_LOGGER = get_logger(__name__, class_name="Updater")
|
||||
|
||||
|
||||
class WebhookServer:
|
||||
"""Thin wrapper around ``tornado.httpserver.HTTPServer``."""
|
||||
|
||||
__slots__ = (
|
||||
"_http_server",
|
||||
"_server_lock",
|
||||
"_shutdown_lock",
|
||||
"is_running",
|
||||
"listen",
|
||||
"port",
|
||||
"unix",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
listen: str,
|
||||
port: int,
|
||||
webhook_app: "WebhookAppClass",
|
||||
ssl_ctx: Optional[SSLContext],
|
||||
unix: Optional[Union[str, Path, socket]] = None,
|
||||
):
|
||||
if unix and not UNIX_AVAILABLE:
|
||||
raise RuntimeError("This OS does not support binding unix sockets.")
|
||||
self._http_server = HTTPServer(webhook_app, ssl_options=ssl_ctx)
|
||||
self.listen = listen
|
||||
self.port = port
|
||||
self.is_running = False
|
||||
self.unix = None
|
||||
if unix and isinstance(unix, socket):
|
||||
self.unix = unix
|
||||
elif unix:
|
||||
self.unix = bind_unix_socket(str(unix))
|
||||
self._server_lock = asyncio.Lock()
|
||||
self._shutdown_lock = asyncio.Lock()
|
||||
|
||||
async def serve_forever(self, ready: Optional[asyncio.Event] = None) -> None:
|
||||
async with self._server_lock:
|
||||
if self.unix:
|
||||
self._http_server.add_socket(self.unix)
|
||||
else:
|
||||
self._http_server.listen(self.port, address=self.listen)
|
||||
|
||||
self.is_running = True
|
||||
if ready is not None:
|
||||
ready.set()
|
||||
|
||||
_LOGGER.debug("Webhook Server started.")
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
async with self._shutdown_lock:
|
||||
if not self.is_running:
|
||||
_LOGGER.debug("Webhook Server is already shut down. Returning")
|
||||
return
|
||||
self.is_running = False
|
||||
self._http_server.stop()
|
||||
await self._http_server.close_all_connections()
|
||||
_LOGGER.debug("Webhook Server stopped")
|
||||
|
||||
|
||||
class WebhookAppClass(tornado.web.Application):
|
||||
"""Application used in the Webserver"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
webhook_path: str,
|
||||
bot: "Bot",
|
||||
update_queue: asyncio.Queue,
|
||||
secret_token: Optional[str] = None,
|
||||
):
|
||||
self.shared_objects = {
|
||||
"bot": bot,
|
||||
"update_queue": update_queue,
|
||||
"secret_token": secret_token,
|
||||
}
|
||||
handlers = [(rf"{webhook_path}/?", TelegramHandler, self.shared_objects)]
|
||||
tornado.web.Application.__init__(self, handlers) # type: ignore
|
||||
|
||||
def log_request(self, handler: tornado.web.RequestHandler) -> None:
|
||||
"""Overrides the default implementation since we have our own logging setup."""
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
class TelegramHandler(tornado.web.RequestHandler):
|
||||
"""BaseHandler that processes incoming requests from Telegram"""
|
||||
|
||||
__slots__ = ("bot", "secret_token", "update_queue")
|
||||
|
||||
SUPPORTED_METHODS = ("POST",) # type: ignore[assignment]
|
||||
|
||||
def initialize(self, bot: "Bot", update_queue: asyncio.Queue, secret_token: str) -> None:
|
||||
"""Initialize for each request - that's the interface provided by tornado"""
|
||||
# pylint: disable=attribute-defined-outside-init
|
||||
self.bot = bot
|
||||
self.update_queue = update_queue
|
||||
self.secret_token = secret_token
|
||||
if secret_token:
|
||||
_LOGGER.debug(
|
||||
"The webhook server has a secret token, expecting it in incoming requests now"
|
||||
)
|
||||
|
||||
def set_default_headers(self) -> None:
|
||||
"""Sets default headers"""
|
||||
self.set_header("Content-Type", 'application/json; charset="utf-8"')
|
||||
|
||||
async def post(self) -> None:
|
||||
"""Handle incoming POST request"""
|
||||
_LOGGER.debug("Webhook triggered")
|
||||
self._validate_post()
|
||||
|
||||
json_string = self.request.body.decode()
|
||||
data = json.loads(json_string)
|
||||
self.set_status(HTTPStatus.OK)
|
||||
_LOGGER.debug("Webhook received data: %s", json_string)
|
||||
|
||||
try:
|
||||
update = Update.de_json(data, self.bot)
|
||||
except Exception as exc:
|
||||
_LOGGER.critical(
|
||||
"Something went wrong processing the data received from Telegram. "
|
||||
"Received data was *not* processed! Received data was: %r",
|
||||
data,
|
||||
exc_info=exc,
|
||||
)
|
||||
raise tornado.web.HTTPError(
|
||||
HTTPStatus.BAD_REQUEST, reason="Update could not be processed"
|
||||
) from exc
|
||||
|
||||
if update:
|
||||
_LOGGER.debug(
|
||||
"Received Update with ID %d on Webhook",
|
||||
# For some reason pylint thinks update is a general TelegramObject
|
||||
update.update_id, # pylint: disable=no-member
|
||||
)
|
||||
|
||||
# handle arbitrary callback data, if necessary
|
||||
if isinstance(self.bot, ExtBot):
|
||||
self.bot.insert_callback_data(update)
|
||||
|
||||
await self.update_queue.put(update)
|
||||
|
||||
def _validate_post(self) -> None:
|
||||
"""Only accept requests with content type JSON"""
|
||||
ct_header = self.request.headers.get("Content-Type", None)
|
||||
if ct_header != "application/json":
|
||||
raise tornado.web.HTTPError(HTTPStatus.FORBIDDEN)
|
||||
# verifying that the secret token is the one the user set when the user set one
|
||||
if self.secret_token is not None:
|
||||
token = self.request.headers.get("X-Telegram-Bot-Api-Secret-Token")
|
||||
if not token:
|
||||
_LOGGER.debug("Request did not include the secret token")
|
||||
raise tornado.web.HTTPError(
|
||||
HTTPStatus.FORBIDDEN, reason="Request did not include the secret token"
|
||||
)
|
||||
if token != self.secret_token:
|
||||
_LOGGER.debug("Request had the wrong secret token: %s", token)
|
||||
raise tornado.web.HTTPError(
|
||||
HTTPStatus.FORBIDDEN, reason="Request had the wrong secret token"
|
||||
)
|
||||
|
||||
def log_exception(
|
||||
self,
|
||||
typ: Optional[type[BaseException]],
|
||||
value: Optional[BaseException],
|
||||
tb: Optional[TracebackType],
|
||||
) -> None:
|
||||
"""Override the default logging and instead use our custom logging."""
|
||||
_LOGGER.debug(
|
||||
"%s - %s",
|
||||
self.request.remote_ip,
|
||||
"Exception in TelegramHandler",
|
||||
exc_info=(typ, value, tb) if typ and value and tb else value,
|
||||
)
|
||||
Reference in New Issue
Block a user