mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Bot API 5.7 and some new features (#834)
* Update API, added some new features * Fixed unknown chat_action value * Separate events from dispatcher messages * Disabled cache for I18n LazyProxy * Rework events isolation * Added chat member status changed filter, update Bot API 5.7, other small changes * Improve exceptions in chat member status filter * Fixed tests, covered flags and events isolation modules * Try to fix flake8 unused type ignore * Fixed linter error * Cover chat member updated filter * Cover chat action sender * Added docs for chat action util * Try to fix tests for python <= 3.9 * Fixed headers * Added docs for flags functionality * Added docs for chat_member_updated filter * Added change notes * Update dependencies and fix mypy checks * Bump version
This commit is contained in:
parent
ac7f2dc408
commit
7776cf9cf6
77 changed files with 2485 additions and 502 deletions
347
aiogram/utils/chat_action.py
Normal file
347
aiogram/utils/chat_action.py
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from asyncio import Event, Lock
|
||||
from contextlib import suppress
|
||||
from types import TracebackType
|
||||
from typing import Any, Awaitable, Callable, Dict, Optional, Type, Union
|
||||
|
||||
from aiogram import BaseMiddleware, Bot
|
||||
from aiogram.dispatcher.flags.getter import get_flag
|
||||
from aiogram.types import Message, TelegramObject
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
DEFAULT_INTERVAL = 5.0
|
||||
DEFAULT_INITIAL_SLEEP = 0.1
|
||||
|
||||
|
||||
class ChatActionSender:
|
||||
"""
|
||||
This utility helps to automatically send chat action until long actions is done
|
||||
to take acknowledge bot users the bot is doing something and not crashed.
|
||||
|
||||
Provides simply to use context manager.
|
||||
|
||||
Technically sender start background task with infinity loop which works
|
||||
until action will be finished and sends the `chat action <https://core.telegram.org/bots/api#sendchataction>`_
|
||||
every 5 seconds.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
chat_id: Union[str, int],
|
||||
action: str = "typing",
|
||||
interval: float = DEFAULT_INTERVAL,
|
||||
initial_sleep: float = DEFAULT_INITIAL_SLEEP,
|
||||
bot: Optional[Bot] = None,
|
||||
) -> None:
|
||||
"""
|
||||
:param chat_id: target chat id
|
||||
:param action: chat action type
|
||||
:param interval: interval between iterations
|
||||
:param initial_sleep: sleep before first iteration
|
||||
:param bot: instance of the bot, can be omitted from the context
|
||||
"""
|
||||
if bot is None:
|
||||
bot = Bot.get_current(False)
|
||||
|
||||
self.chat_id = chat_id
|
||||
self.action = action
|
||||
self.interval = interval
|
||||
self.initial_sleep = initial_sleep
|
||||
self.bot = bot
|
||||
|
||||
self._lock = Lock()
|
||||
self._close_event = Event()
|
||||
self._closed_event = Event()
|
||||
self._task: Optional[asyncio.Task[Any]] = None
|
||||
|
||||
@property
|
||||
def running(self) -> bool:
|
||||
return bool(self._task)
|
||||
|
||||
async def _wait(self, interval: float) -> None:
|
||||
with suppress(asyncio.TimeoutError):
|
||||
await asyncio.wait_for(self._close_event.wait(), interval)
|
||||
|
||||
async def _worker(self) -> None:
|
||||
logger.debug(
|
||||
"Started chat action %r sender in chat_id=%s via bot id=%d",
|
||||
self.action,
|
||||
self.chat_id,
|
||||
self.bot.id,
|
||||
)
|
||||
try:
|
||||
counter = 0
|
||||
await self._wait(self.initial_sleep)
|
||||
while not self._close_event.is_set():
|
||||
start = time.monotonic()
|
||||
logger.debug(
|
||||
"Sent chat action %r to chat_id=%s via bot %d (already sent actions %d)",
|
||||
self.action,
|
||||
self.chat_id,
|
||||
self.bot.id,
|
||||
counter,
|
||||
)
|
||||
await self.bot.send_chat_action(chat_id=self.chat_id, action=self.action)
|
||||
counter += 1
|
||||
|
||||
interval = self.interval - (time.monotonic() - start)
|
||||
await self._wait(interval)
|
||||
finally:
|
||||
logger.debug(
|
||||
"Finished chat action %r sender in chat_id=%s via bot id=%d",
|
||||
self.action,
|
||||
self.chat_id,
|
||||
self.bot.id,
|
||||
)
|
||||
self._closed_event.set()
|
||||
|
||||
async def _run(self) -> None:
|
||||
async with self._lock:
|
||||
self._close_event.clear()
|
||||
self._closed_event.clear()
|
||||
if self.running:
|
||||
raise RuntimeError("Already running")
|
||||
self._task = asyncio.create_task(self._worker())
|
||||
|
||||
async def _stop(self) -> None:
|
||||
async with self._lock:
|
||||
if not self.running:
|
||||
return
|
||||
if not self._close_event.is_set():
|
||||
self._close_event.set()
|
||||
await self._closed_event.wait()
|
||||
self._task = None
|
||||
|
||||
async def __aenter__(self) -> "ChatActionSender":
|
||||
await self._run()
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_value: Optional[BaseException],
|
||||
traceback: Optional[TracebackType],
|
||||
) -> Any:
|
||||
await self._stop()
|
||||
|
||||
@classmethod
|
||||
def typing(
|
||||
cls,
|
||||
chat_id: Union[int, str],
|
||||
bot: Optional[Bot] = None,
|
||||
interval: float = DEFAULT_INTERVAL,
|
||||
initial_sleep: float = DEFAULT_INITIAL_SLEEP,
|
||||
) -> "ChatActionSender":
|
||||
"""Create instance of the sender with `typing` action"""
|
||||
return cls(
|
||||
bot=bot,
|
||||
chat_id=chat_id,
|
||||
action="typing",
|
||||
interval=interval,
|
||||
initial_sleep=initial_sleep,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def upload_photo(
|
||||
cls,
|
||||
chat_id: Union[int, str],
|
||||
bot: Optional[Bot] = None,
|
||||
interval: float = DEFAULT_INTERVAL,
|
||||
initial_sleep: float = DEFAULT_INITIAL_SLEEP,
|
||||
) -> "ChatActionSender":
|
||||
"""Create instance of the sender with `upload_photo` action"""
|
||||
return cls(
|
||||
bot=bot,
|
||||
chat_id=chat_id,
|
||||
action="upload_photo",
|
||||
interval=interval,
|
||||
initial_sleep=initial_sleep,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def record_video(
|
||||
cls,
|
||||
chat_id: Union[int, str],
|
||||
bot: Optional[Bot] = None,
|
||||
interval: float = DEFAULT_INTERVAL,
|
||||
initial_sleep: float = DEFAULT_INITIAL_SLEEP,
|
||||
) -> "ChatActionSender":
|
||||
"""Create instance of the sender with `record_video` action"""
|
||||
return cls(
|
||||
bot=bot,
|
||||
chat_id=chat_id,
|
||||
action="record_video",
|
||||
interval=interval,
|
||||
initial_sleep=initial_sleep,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def upload_video(
|
||||
cls,
|
||||
chat_id: Union[int, str],
|
||||
bot: Optional[Bot] = None,
|
||||
interval: float = DEFAULT_INTERVAL,
|
||||
initial_sleep: float = DEFAULT_INITIAL_SLEEP,
|
||||
) -> "ChatActionSender":
|
||||
"""Create instance of the sender with `upload_video` action"""
|
||||
return cls(
|
||||
bot=bot,
|
||||
chat_id=chat_id,
|
||||
action="upload_video",
|
||||
interval=interval,
|
||||
initial_sleep=initial_sleep,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def record_voice(
|
||||
cls,
|
||||
chat_id: Union[int, str],
|
||||
bot: Optional[Bot] = None,
|
||||
interval: float = DEFAULT_INTERVAL,
|
||||
initial_sleep: float = DEFAULT_INITIAL_SLEEP,
|
||||
) -> "ChatActionSender":
|
||||
"""Create instance of the sender with `record_voice` action"""
|
||||
return cls(
|
||||
bot=bot,
|
||||
chat_id=chat_id,
|
||||
action="record_voice",
|
||||
interval=interval,
|
||||
initial_sleep=initial_sleep,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def upload_voice(
|
||||
cls,
|
||||
chat_id: Union[int, str],
|
||||
bot: Optional[Bot] = None,
|
||||
interval: float = DEFAULT_INTERVAL,
|
||||
initial_sleep: float = DEFAULT_INITIAL_SLEEP,
|
||||
) -> "ChatActionSender":
|
||||
"""Create instance of the sender with `upload_voice` action"""
|
||||
return cls(
|
||||
bot=bot,
|
||||
chat_id=chat_id,
|
||||
action="upload_voice",
|
||||
interval=interval,
|
||||
initial_sleep=initial_sleep,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def upload_document(
|
||||
cls,
|
||||
chat_id: Union[int, str],
|
||||
bot: Optional[Bot] = None,
|
||||
interval: float = DEFAULT_INTERVAL,
|
||||
initial_sleep: float = DEFAULT_INITIAL_SLEEP,
|
||||
) -> "ChatActionSender":
|
||||
"""Create instance of the sender with `upload_document` action"""
|
||||
return cls(
|
||||
bot=bot,
|
||||
chat_id=chat_id,
|
||||
action="upload_document",
|
||||
interval=interval,
|
||||
initial_sleep=initial_sleep,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def choose_sticker(
|
||||
cls,
|
||||
chat_id: Union[int, str],
|
||||
bot: Optional[Bot] = None,
|
||||
interval: float = DEFAULT_INTERVAL,
|
||||
initial_sleep: float = DEFAULT_INITIAL_SLEEP,
|
||||
) -> "ChatActionSender":
|
||||
"""Create instance of the sender with `choose_sticker` action"""
|
||||
return cls(
|
||||
bot=bot,
|
||||
chat_id=chat_id,
|
||||
action="choose_sticker",
|
||||
interval=interval,
|
||||
initial_sleep=initial_sleep,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def find_location(
|
||||
cls,
|
||||
chat_id: Union[int, str],
|
||||
bot: Optional[Bot] = None,
|
||||
interval: float = DEFAULT_INTERVAL,
|
||||
initial_sleep: float = DEFAULT_INITIAL_SLEEP,
|
||||
) -> "ChatActionSender":
|
||||
"""Create instance of the sender with `find_location` action"""
|
||||
return cls(
|
||||
bot=bot,
|
||||
chat_id=chat_id,
|
||||
action="find_location",
|
||||
interval=interval,
|
||||
initial_sleep=initial_sleep,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def record_video_note(
|
||||
cls,
|
||||
chat_id: Union[int, str],
|
||||
bot: Optional[Bot] = None,
|
||||
interval: float = DEFAULT_INTERVAL,
|
||||
initial_sleep: float = DEFAULT_INITIAL_SLEEP,
|
||||
) -> "ChatActionSender":
|
||||
"""Create instance of the sender with `record_video_note` action"""
|
||||
return cls(
|
||||
bot=bot,
|
||||
chat_id=chat_id,
|
||||
action="record_video_note",
|
||||
interval=interval,
|
||||
initial_sleep=initial_sleep,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def upload_video_note(
|
||||
cls,
|
||||
chat_id: Union[int, str],
|
||||
bot: Optional[Bot] = None,
|
||||
interval: float = DEFAULT_INTERVAL,
|
||||
initial_sleep: float = DEFAULT_INITIAL_SLEEP,
|
||||
) -> "ChatActionSender":
|
||||
"""Create instance of the sender with `upload_video_note` action"""
|
||||
return cls(
|
||||
bot=bot,
|
||||
chat_id=chat_id,
|
||||
action="upload_video_note",
|
||||
interval=interval,
|
||||
initial_sleep=initial_sleep,
|
||||
)
|
||||
|
||||
|
||||
class ChatActionMiddleware(BaseMiddleware):
|
||||
"""
|
||||
Helps to automatically use chat action sender for all message handlers
|
||||
"""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any],
|
||||
) -> Any:
|
||||
if not isinstance(event, Message):
|
||||
return await handler(event, data)
|
||||
bot = data["bot"]
|
||||
|
||||
chat_action = get_flag(data, "chat_action") or "typing"
|
||||
kwargs = {}
|
||||
if isinstance(chat_action, dict):
|
||||
if initial_sleep := chat_action.get("initial_sleep"):
|
||||
kwargs["initial_sleep"] = initial_sleep
|
||||
if interval := chat_action.get("interval"):
|
||||
kwargs["interval"] = interval
|
||||
if action := chat_action.get("action"):
|
||||
kwargs["action"] = action
|
||||
elif isinstance(chat_action, bool):
|
||||
kwargs["action"] = "typing"
|
||||
else:
|
||||
kwargs["action"] = chat_action
|
||||
async with ChatActionSender(bot=bot, chat_id=event.chat.id, **kwargs):
|
||||
return await handler(event, data)
|
||||
|
|
@ -16,7 +16,7 @@ def gettext(*args: Any, **kwargs: Any) -> str:
|
|||
|
||||
|
||||
def lazy_gettext(*args: Any, **kwargs: Any) -> LazyProxy:
|
||||
return LazyProxy(gettext, *args, **kwargs)
|
||||
return LazyProxy(gettext, *args, **kwargs, enable_cache=False)
|
||||
|
||||
|
||||
ngettext = gettext
|
||||
|
|
|
|||
|
|
@ -118,4 +118,6 @@ class I18n(ContextInstanceMixin["I18n"]):
|
|||
def lazy_gettext(
|
||||
self, singular: str, plural: Optional[str] = None, n: int = 1, locale: Optional[str] = None
|
||||
) -> LazyProxy:
|
||||
return LazyProxy(self.gettext, singular=singular, plural=plural, n=n, locale=locale)
|
||||
return LazyProxy(
|
||||
self.gettext, singular=singular, plural=plural, n=n, locale=locale, enable_cache=False
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,10 +2,14 @@ from abc import ABC, abstractmethod
|
|||
from typing import Any, Awaitable, Callable, Dict, Optional, Set, cast
|
||||
|
||||
try:
|
||||
from babel import Locale
|
||||
from babel import Locale, UnknownLocaleError
|
||||
except ImportError: # pragma: no cover
|
||||
Locale = None
|
||||
|
||||
class UnknownLocaleError(Exception): # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
from aiogram import BaseMiddleware, Router
|
||||
from aiogram.dispatcher.fsm.context import FSMContext
|
||||
from aiogram.types import TelegramObject, User
|
||||
|
|
@ -116,7 +120,11 @@ class SimpleI18nMiddleware(I18nMiddleware):
|
|||
event_from_user: Optional[User] = data.get("event_from_user", None)
|
||||
if event_from_user is None:
|
||||
return self.i18n.default_locale
|
||||
locale = Locale.parse(event_from_user.language_code, sep="-")
|
||||
try:
|
||||
locale = Locale.parse(event_from_user.language_code, sep="-")
|
||||
except UnknownLocaleError:
|
||||
return self.i18n.default_locale
|
||||
|
||||
if locale.language not in self.i18n.available_locales:
|
||||
return self.i18n.default_locale
|
||||
return cast(str, locale.language)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextvars
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Generic, Optional, TypeVar, cast, overload
|
||||
from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, TypeVar, cast, overload
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
|
|
@ -38,7 +38,7 @@ ContextInstance = TypeVar("ContextInstance")
|
|||
|
||||
|
||||
class ContextInstanceMixin(Generic[ContextInstance]):
|
||||
__context_instance: ClassVar[contextvars.ContextVar[ContextInstance]]
|
||||
__context_instance: contextvars.ContextVar[ContextInstance]
|
||||
|
||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||
super().__init_subclass__()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue