README.md edited

This commit is contained in:
2024-12-06 10:45:08 +09:00
parent 09e4edee6b
commit 1aa387aa59
13921 changed files with 2057290 additions and 10 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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