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
|
|
@ -2,6 +2,7 @@ from .client import session
|
|||
from .client.bot import Bot
|
||||
from .dispatcher import filters, handler
|
||||
from .dispatcher.dispatcher import Dispatcher
|
||||
from .dispatcher.flags.flag import FlagGenerator
|
||||
from .dispatcher.middlewares.base import BaseMiddleware
|
||||
from .dispatcher.router import Router
|
||||
from .utils.magic_filter import MagicFilter
|
||||
|
|
@ -18,6 +19,7 @@ except ImportError: # pragma: no cover
|
|||
F = MagicFilter()
|
||||
html = _html_decoration
|
||||
md = _markdown_decoration
|
||||
flags = FlagGenerator()
|
||||
|
||||
__all__ = (
|
||||
"__api_version__",
|
||||
|
|
@ -34,7 +36,8 @@ __all__ = (
|
|||
"F",
|
||||
"html",
|
||||
"md",
|
||||
"flags",
|
||||
)
|
||||
|
||||
__version__ = "3.0.0b1"
|
||||
__api_version__ = "5.5"
|
||||
__version__ = "3.0.0b2"
|
||||
__api_version__ = "5.7"
|
||||
|
|
|
|||
|
|
@ -311,7 +311,9 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
if isinstance(file, str):
|
||||
file_id = file
|
||||
else:
|
||||
file_id = getattr(file, "file_id", None)
|
||||
# type is ignored in due to:
|
||||
# Incompatible types in assignment (expression has type "Optional[Any]", variable has type "str")
|
||||
file_id = getattr(file, "file_id", None) # type: ignore
|
||||
if file_id is None:
|
||||
raise TypeError("file can only be of the string or Downloadable type")
|
||||
|
||||
|
|
@ -533,6 +535,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
entities: Optional[List[MessageEntity]] = None,
|
||||
disable_web_page_preview: Optional[bool] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[
|
||||
|
|
@ -551,6 +554,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param entities: A JSON-serialized list of special entities that appear in message text, which can be specified instead of *parse_mode*
|
||||
:param disable_web_page_preview: Disables link previews for links in this message
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding and saving
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_, `custom reply keyboard <https://core.telegram.org/bots#keyboards>`_, instructions to remove reply keyboard or to force a reply from the user.
|
||||
|
|
@ -564,6 +568,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
entities=entities,
|
||||
disable_web_page_preview=disable_web_page_preview,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -576,6 +581,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
from_chat_id: Union[int, str],
|
||||
message_id: int,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
request_timeout: Optional[int] = None,
|
||||
) -> Message:
|
||||
"""
|
||||
|
|
@ -587,6 +593,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param from_chat_id: Unique identifier for the chat where the original message was sent (or channel username in the format :code:`@channelusername`)
|
||||
:param message_id: Message identifier in the chat specified in *from_chat_id*
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the forwarded message from forwarding and saving
|
||||
:param request_timeout: Request timeout
|
||||
:return: On success, the sent Message is returned.
|
||||
"""
|
||||
|
|
@ -595,6 +602,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
from_chat_id=from_chat_id,
|
||||
message_id=message_id,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
)
|
||||
return await self(call, request_timeout=request_timeout)
|
||||
|
||||
|
|
@ -607,6 +615,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
parse_mode: Optional[str] = UNSET,
|
||||
caption_entities: Optional[List[MessageEntity]] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[
|
||||
|
|
@ -626,6 +635,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param parse_mode: Mode for parsing entities in the new caption. See `formatting options <https://core.telegram.org/bots/api#formatting-options>`_ for more details.
|
||||
:param caption_entities: A JSON-serialized list of special entities that appear in the new caption, which can be specified instead of *parse_mode*
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding and saving
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_, `custom reply keyboard <https://core.telegram.org/bots#keyboards>`_, instructions to remove reply keyboard or to force a reply from the user.
|
||||
|
|
@ -640,6 +650,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -654,6 +665,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
parse_mode: Optional[str] = UNSET,
|
||||
caption_entities: Optional[List[MessageEntity]] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[
|
||||
|
|
@ -672,6 +684,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param parse_mode: Mode for parsing entities in the photo caption. See `formatting options <https://core.telegram.org/bots/api#formatting-options>`_ for more details.
|
||||
:param caption_entities: A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode*
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding and saving
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_, `custom reply keyboard <https://core.telegram.org/bots#keyboards>`_, instructions to remove reply keyboard or to force a reply from the user.
|
||||
|
|
@ -685,6 +698,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -703,6 +717,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
title: Optional[str] = None,
|
||||
thumb: Optional[Union[InputFile, str]] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[
|
||||
|
|
@ -726,6 +741,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param title: Track name
|
||||
:param thumb: Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass 'attach://<file_attach_name>' if the thumbnail was uploaded using multipart/form-data under <file_attach_name>. :ref:`More info on Sending Files » <sending-files>`
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding and saving
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_, `custom reply keyboard <https://core.telegram.org/bots#keyboards>`_, instructions to remove reply keyboard or to force a reply from the user.
|
||||
|
|
@ -743,6 +759,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
title=title,
|
||||
thumb=thumb,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -759,6 +776,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
caption_entities: Optional[List[MessageEntity]] = None,
|
||||
disable_content_type_detection: Optional[bool] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[
|
||||
|
|
@ -779,6 +797,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param caption_entities: A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode*
|
||||
:param disable_content_type_detection: Disables automatic server-side content type detection for files uploaded using multipart/form-data
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding and saving
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_, `custom reply keyboard <https://core.telegram.org/bots#keyboards>`_, instructions to remove reply keyboard or to force a reply from the user.
|
||||
|
|
@ -794,6 +813,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
caption_entities=caption_entities,
|
||||
disable_content_type_detection=disable_content_type_detection,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -813,6 +833,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
caption_entities: Optional[List[MessageEntity]] = None,
|
||||
supports_streaming: Optional[bool] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[
|
||||
|
|
@ -836,6 +857,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param caption_entities: A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode*
|
||||
:param supports_streaming: Pass :code:`True`, if the uploaded video is suitable for streaming
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding and saving
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_, `custom reply keyboard <https://core.telegram.org/bots#keyboards>`_, instructions to remove reply keyboard or to force a reply from the user.
|
||||
|
|
@ -854,6 +876,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
caption_entities=caption_entities,
|
||||
supports_streaming=supports_streaming,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -872,6 +895,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
parse_mode: Optional[str] = UNSET,
|
||||
caption_entities: Optional[List[MessageEntity]] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[
|
||||
|
|
@ -894,6 +918,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param parse_mode: Mode for parsing entities in the animation caption. See `formatting options <https://core.telegram.org/bots/api#formatting-options>`_ for more details.
|
||||
:param caption_entities: A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode*
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding and saving
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_, `custom reply keyboard <https://core.telegram.org/bots#keyboards>`_, instructions to remove reply keyboard or to force a reply from the user.
|
||||
|
|
@ -911,6 +936,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
parse_mode=parse_mode,
|
||||
caption_entities=caption_entities,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -926,6 +952,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
caption_entities: Optional[List[MessageEntity]] = None,
|
||||
duration: Optional[int] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[
|
||||
|
|
@ -945,6 +972,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param caption_entities: A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode*
|
||||
:param duration: Duration of the voice message in seconds
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding and saving
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_, `custom reply keyboard <https://core.telegram.org/bots#keyboards>`_, instructions to remove reply keyboard or to force a reply from the user.
|
||||
|
|
@ -959,6 +987,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
caption_entities=caption_entities,
|
||||
duration=duration,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -973,6 +1002,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
length: Optional[int] = None,
|
||||
thumb: Optional[Union[InputFile, str]] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[
|
||||
|
|
@ -991,6 +1021,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param length: Video width and height, i.e. diameter of the video message
|
||||
:param thumb: Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass 'attach://<file_attach_name>' if the thumbnail was uploaded using multipart/form-data under <file_attach_name>. :ref:`More info on Sending Files » <sending-files>`
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding and saving
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_, `custom reply keyboard <https://core.telegram.org/bots#keyboards>`_, instructions to remove reply keyboard or to force a reply from the user.
|
||||
|
|
@ -1004,6 +1035,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
length=length,
|
||||
thumb=thumb,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1015,6 +1047,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
chat_id: Union[int, str],
|
||||
media: List[Union[InputMediaAudio, InputMediaDocument, InputMediaPhoto, InputMediaVideo]],
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
request_timeout: Optional[int] = None,
|
||||
|
|
@ -1027,6 +1060,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param chat_id: Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)
|
||||
:param media: A JSON-serialized array describing messages to be sent, must include 2-10 items
|
||||
:param disable_notification: Sends messages `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent messages from forwarding and saving
|
||||
:param reply_to_message_id: If the messages are a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param request_timeout: Request timeout
|
||||
|
|
@ -1036,6 +1070,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
chat_id=chat_id,
|
||||
media=media,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
)
|
||||
|
|
@ -1051,6 +1086,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
heading: Optional[int] = None,
|
||||
proximity_alert_radius: Optional[int] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[
|
||||
|
|
@ -1071,6 +1107,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param heading: For live locations, a direction in which the user is moving, in degrees. Must be between 1 and 360 if specified.
|
||||
:param proximity_alert_radius: For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified.
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding and saving
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_, `custom reply keyboard <https://core.telegram.org/bots#keyboards>`_, instructions to remove reply keyboard or to force a reply from the user.
|
||||
|
|
@ -1086,6 +1123,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
heading=heading,
|
||||
proximity_alert_radius=proximity_alert_radius,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1177,6 +1215,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
google_place_id: Optional[str] = None,
|
||||
google_place_type: Optional[str] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[
|
||||
|
|
@ -1199,6 +1238,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param google_place_id: Google Places identifier of the venue
|
||||
:param google_place_type: Google Places type of the venue. (See `supported types <https://developers.google.com/places/web-service/supported_types>`_.)
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding and saving
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_, `custom reply keyboard <https://core.telegram.org/bots#keyboards>`_, instructions to remove reply keyboard or to force a reply from the user.
|
||||
|
|
@ -1216,6 +1256,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
google_place_id=google_place_id,
|
||||
google_place_type=google_place_type,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1230,6 +1271,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
last_name: Optional[str] = None,
|
||||
vcard: Optional[str] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[
|
||||
|
|
@ -1248,6 +1290,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param last_name: Contact's last name
|
||||
:param vcard: Additional data about the contact in the form of a `vCard <https://en.wikipedia.org/wiki/VCard>`_, 0-2048 bytes
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding and saving
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_, `custom reply keyboard <https://core.telegram.org/bots#keyboards>`_, instructions to remove keyboard or to force a reply from the user.
|
||||
|
|
@ -1261,6 +1304,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
last_name=last_name,
|
||||
vcard=vcard,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1283,6 +1327,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
close_date: Optional[Union[datetime.datetime, datetime.timedelta, int]] = None,
|
||||
is_closed: Optional[bool] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[
|
||||
|
|
@ -1309,6 +1354,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param close_date: Point in time (Unix timestamp) when the poll will be automatically closed. Must be at least 5 and no more than 600 seconds in the future. Can't be used together with *open_period*.
|
||||
:param is_closed: Pass :code:`True`, if the poll needs to be immediately closed. This can be useful for poll preview.
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding and saving
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_, `custom reply keyboard <https://core.telegram.org/bots#keyboards>`_, instructions to remove reply keyboard or to force a reply from the user.
|
||||
|
|
@ -1330,6 +1376,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
close_date=close_date,
|
||||
is_closed=is_closed,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1341,6 +1388,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
chat_id: Union[int, str],
|
||||
emoji: Optional[str] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[
|
||||
|
|
@ -1356,6 +1404,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param chat_id: Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)
|
||||
:param emoji: Emoji on which the dice throw animation is based. Currently, must be one of '🎲', '🎯', '🏀', '⚽', '🎳', or '🎰'. Dice can have values 1-6 for '🎲', '🎯' and '🎳', values 1-5 for '🏀' and '⚽', and values 1-64 for '🎰'. Defaults to '🎲'
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_, `custom reply keyboard <https://core.telegram.org/bots#keyboards>`_, instructions to remove reply keyboard or to force a reply from the user.
|
||||
|
|
@ -1366,6 +1415,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
chat_id=chat_id,
|
||||
emoji=emoji,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -1517,7 +1567,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
|
||||
Source: https://core.telegram.org/bots/api#unbanchatmember
|
||||
|
||||
:param chat_id: Unique identifier for the target group or username of the target supergroup or channel (in the format :code:`@username`)
|
||||
:param chat_id: Unique identifier for the target group or username of the target supergroup or channel (in the format :code:`@channelusername`)
|
||||
:param user_id: Unique identifier of the target user
|
||||
:param only_if_banned: Do nothing if the user is not banned
|
||||
:param request_timeout: Request timeout
|
||||
|
|
@ -2512,6 +2562,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
chat_id: Union[int, str],
|
||||
sticker: Union[InputFile, str],
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[
|
||||
|
|
@ -2520,13 +2571,14 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
request_timeout: Optional[int] = None,
|
||||
) -> Message:
|
||||
"""
|
||||
Use this method to send static .WEBP or `animated <https://telegram.org/blog/animated-stickers>`_ .TGS stickers. On success, the sent :class:`aiogram.types.message.Message` is returned.
|
||||
Use this method to send static .WEBP, `animated <https://telegram.org/blog/animated-stickers>`_ .TGS, or `video <https://telegram.org/blog/video-stickers-better-reactions>`_ .WEBM stickers. On success, the sent :class:`aiogram.types.message.Message` is returned.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#sendsticker
|
||||
|
||||
:param chat_id: Unique identifier for the target chat or username of the target channel (in the format :code:`@channelusername`)
|
||||
:param sticker: Sticker to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a .WEBP file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » <sending-files>`
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding and saving
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: Additional interface options. A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_, `custom reply keyboard <https://core.telegram.org/bots#keyboards>`_, instructions to remove reply keyboard or to force a reply from the user.
|
||||
|
|
@ -2537,6 +2589,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
chat_id=chat_id,
|
||||
sticker=sticker,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -2592,12 +2645,13 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
emojis: str,
|
||||
png_sticker: Optional[Union[InputFile, str]] = None,
|
||||
tgs_sticker: Optional[InputFile] = None,
|
||||
webm_sticker: Optional[InputFile] = None,
|
||||
contains_masks: Optional[bool] = None,
|
||||
mask_position: Optional[MaskPosition] = None,
|
||||
request_timeout: Optional[int] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Use this method to create a new sticker set owned by a user. The bot will be able to edit the sticker set thus created. You **must** use exactly one of the fields *png_sticker* or *tgs_sticker*. Returns :code:`True` on success.
|
||||
Use this method to create a new sticker set owned by a user. The bot will be able to edit the sticker set thus created. You **must** use exactly one of the fields *png_sticker*, *tgs_sticker*, or *webm_sticker*. Returns :code:`True` on success.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#createnewstickerset
|
||||
|
||||
|
|
@ -2606,7 +2660,8 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param title: Sticker set title, 1-64 characters
|
||||
:param emojis: One or more emoji corresponding to the sticker
|
||||
:param png_sticker: **PNG** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a *file_id* as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » <sending-files>`
|
||||
:param tgs_sticker: **TGS** animation with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/animated_stickers#technical-requirements <https://core.telegram.org/animated_stickers#technical-requirements>`_`https://core.telegram.org/animated_stickers#technical-requirements <https://core.telegram.org/animated_stickers#technical-requirements>`_ for technical requirements
|
||||
:param tgs_sticker: **TGS** animation with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/stickers#animated-sticker-requirements <https://core.telegram.org/stickers#animated-sticker-requirements>`_`https://core.telegram.org/stickers#animated-sticker-requirements <https://core.telegram.org/stickers#animated-sticker-requirements>`_ for technical requirements
|
||||
:param webm_sticker: **WEBM** video with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/stickers#video-sticker-requirements <https://core.telegram.org/stickers#video-sticker-requirements>`_`https://core.telegram.org/stickers#video-sticker-requirements <https://core.telegram.org/stickers#video-sticker-requirements>`_ for technical requirements
|
||||
:param contains_masks: Pass :code:`True`, if a set of mask stickers should be created
|
||||
:param mask_position: A JSON-serialized object for position where the mask should be placed on faces
|
||||
:param request_timeout: Request timeout
|
||||
|
|
@ -2619,6 +2674,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
emojis=emojis,
|
||||
png_sticker=png_sticker,
|
||||
tgs_sticker=tgs_sticker,
|
||||
webm_sticker=webm_sticker,
|
||||
contains_masks=contains_masks,
|
||||
mask_position=mask_position,
|
||||
)
|
||||
|
|
@ -2631,11 +2687,12 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
emojis: str,
|
||||
png_sticker: Optional[Union[InputFile, str]] = None,
|
||||
tgs_sticker: Optional[InputFile] = None,
|
||||
webm_sticker: Optional[InputFile] = None,
|
||||
mask_position: Optional[MaskPosition] = None,
|
||||
request_timeout: Optional[int] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Use this method to add a new sticker to a set created by the bot. You **must** use exactly one of the fields *png_sticker* or *tgs_sticker*. Animated stickers can be added to animated sticker sets and only to them. Animated sticker sets can have up to 50 stickers. Static sticker sets can have up to 120 stickers. Returns :code:`True` on success.
|
||||
Use this method to add a new sticker to a set created by the bot. You **must** use exactly one of the fields *png_sticker*, *tgs_sticker*, or *webm_sticker*. Animated stickers can be added to animated sticker sets and only to them. Animated sticker sets can have up to 50 stickers. Static sticker sets can have up to 120 stickers. Returns :code:`True` on success.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#addstickertoset
|
||||
|
||||
|
|
@ -2643,7 +2700,8 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param name: Sticker set name
|
||||
:param emojis: One or more emoji corresponding to the sticker
|
||||
:param png_sticker: **PNG** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a *file_id* as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » <sending-files>`
|
||||
:param tgs_sticker: **TGS** animation with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/animated_stickers#technical-requirements <https://core.telegram.org/animated_stickers#technical-requirements>`_`https://core.telegram.org/animated_stickers#technical-requirements <https://core.telegram.org/animated_stickers#technical-requirements>`_ for technical requirements
|
||||
:param tgs_sticker: **TGS** animation with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/stickers#animated-sticker-requirements <https://core.telegram.org/stickers#animated-sticker-requirements>`_`https://core.telegram.org/stickers#animated-sticker-requirements <https://core.telegram.org/stickers#animated-sticker-requirements>`_ for technical requirements
|
||||
:param webm_sticker: **WEBM** video with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/stickers#video-sticker-requirements <https://core.telegram.org/stickers#video-sticker-requirements>`_`https://core.telegram.org/stickers#video-sticker-requirements <https://core.telegram.org/stickers#video-sticker-requirements>`_ for technical requirements
|
||||
:param mask_position: A JSON-serialized object for position where the mask should be placed on faces
|
||||
:param request_timeout: Request timeout
|
||||
:return: Returns True on success.
|
||||
|
|
@ -2654,6 +2712,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
emojis=emojis,
|
||||
png_sticker=png_sticker,
|
||||
tgs_sticker=tgs_sticker,
|
||||
webm_sticker=webm_sticker,
|
||||
mask_position=mask_position,
|
||||
)
|
||||
return await self(call, request_timeout=request_timeout)
|
||||
|
|
@ -2707,13 +2766,13 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
request_timeout: Optional[int] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set for animated sticker sets only. Returns :code:`True` on success.
|
||||
Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set for animated sticker sets only. Video thumbnails can be set only for video sticker sets only. Returns :code:`True` on success.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#setstickersetthumb
|
||||
|
||||
:param name: Sticker set name
|
||||
:param user_id: User identifier of the sticker set owner
|
||||
:param thumb: A **PNG** image with the thumbnail, must be up to 128 kilobytes in size and have width and height exactly 100px, or a **TGS** animation with the thumbnail up to 32 kilobytes in size; see `https://core.telegram.org/animated_stickers#technical-requirements <https://core.telegram.org/animated_stickers#technical-requirements>`_`https://core.telegram.org/animated_stickers#technical-requirements <https://core.telegram.org/animated_stickers#technical-requirements>`_ for animated sticker technical requirements. Pass a *file_id* as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » <sending-files>`. Animated sticker set thumbnail can't be uploaded via HTTP URL.
|
||||
:param thumb: A **PNG** image with the thumbnail, must be up to 128 kilobytes in size and have width and height exactly 100px, or a **TGS** animation with the thumbnail up to 32 kilobytes in size; see `https://core.telegram.org/stickers#animated-sticker-requirements <https://core.telegram.org/stickers#animated-sticker-requirements>`_`https://core.telegram.org/stickers#animated-sticker-requirements <https://core.telegram.org/stickers#animated-sticker-requirements>`_ for animated sticker technical requirements, or a **WEBM** video with the thumbnail up to 32 kilobytes in size; see `https://core.telegram.org/stickers#video-sticker-requirements <https://core.telegram.org/stickers#video-sticker-requirements>`_`https://core.telegram.org/stickers#video-sticker-requirements <https://core.telegram.org/stickers#video-sticker-requirements>`_ for video sticker technical requirements. Pass a *file_id* as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » <sending-files>`. Animated sticker set thumbnails can't be uploaded via HTTP URL.
|
||||
:param request_timeout: Request timeout
|
||||
:return: Returns True on success.
|
||||
"""
|
||||
|
|
@ -2798,6 +2857,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
send_email_to_provider: Optional[bool] = None,
|
||||
is_flexible: Optional[bool] = None,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[InlineKeyboardMarkup] = None,
|
||||
|
|
@ -2831,6 +2891,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param send_email_to_provider: Pass :code:`True`, if user's email address should be sent to provider
|
||||
:param is_flexible: Pass :code:`True`, if the final price depends on the shipping method
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding and saving
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_. If empty, one 'Pay :code:`total price`' button will be shown. If not empty, the first button must be a Pay button.
|
||||
|
|
@ -2861,6 +2922,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
send_email_to_provider=send_email_to_provider,
|
||||
is_flexible=is_flexible,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
@ -2960,6 +3022,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
chat_id: int,
|
||||
game_short_name: str,
|
||||
disable_notification: Optional[bool] = None,
|
||||
protect_content: Optional[bool] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: Optional[bool] = None,
|
||||
reply_markup: Optional[InlineKeyboardMarkup] = None,
|
||||
|
|
@ -2973,6 +3036,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
:param chat_id: Unique identifier for the target chat
|
||||
:param game_short_name: Short name of the game, serves as the unique identifier for the game. Set up your games via `Botfather <https://t.me/botfather>`_.
|
||||
:param disable_notification: Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound.
|
||||
:param protect_content: Protects the contents of the sent message from forwarding and saving
|
||||
:param reply_to_message_id: If the message is a reply, ID of the original message
|
||||
:param allow_sending_without_reply: Pass :code:`True`, if the message should be sent even if the specified replied-to message is not found
|
||||
:param reply_markup: A JSON-serialized object for an `inline keyboard <https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating>`_. If empty, one 'Play game_title' button will be shown. If not empty, the first button must launch the game.
|
||||
|
|
@ -2983,6 +3047,7 @@ class Bot(ContextInstanceMixin["Bot"]):
|
|||
chat_id=chat_id,
|
||||
game_short_name=game_short_name,
|
||||
disable_notification=disable_notification,
|
||||
protect_content=protect_content,
|
||||
reply_to_message_id=reply_to_message_id,
|
||||
allow_sending_without_reply=allow_sending_without_reply,
|
||||
reply_markup=reply_markup,
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ from ..utils.backoff import Backoff, BackoffConfig
|
|||
from .event.bases import UNHANDLED, SkipHandler
|
||||
from .event.telegram import TelegramEventObserver
|
||||
from .fsm.middleware import FSMContextMiddleware
|
||||
from .fsm.storage.base import BaseStorage
|
||||
from .fsm.storage.memory import MemoryStorage
|
||||
from .fsm.storage.base import BaseEventIsolation, BaseStorage
|
||||
from .fsm.storage.memory import DisabledEventIsolation, MemoryStorage
|
||||
from .fsm.strategy import FSMStrategy
|
||||
from .middlewares.error import ErrorsMiddleware
|
||||
from .middlewares.user_context import UserContextMiddleware
|
||||
|
|
@ -35,7 +35,7 @@ class Dispatcher(Router):
|
|||
self,
|
||||
storage: Optional[BaseStorage] = None,
|
||||
fsm_strategy: FSMStrategy = FSMStrategy.USER_IN_CHAT,
|
||||
isolate_events: bool = False,
|
||||
events_isolation: Optional[BaseEventIsolation] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super(Dispatcher, self).__init__(**kwargs)
|
||||
|
|
@ -48,19 +48,22 @@ class Dispatcher(Router):
|
|||
)
|
||||
self.update.register(self._listen_update)
|
||||
|
||||
# Error handlers should works is out of all other functions and be registered before all other middlewares
|
||||
# Error handlers should work is out of all other functions and be registered before all others middlewares
|
||||
self.update.outer_middleware(ErrorsMiddleware(self))
|
||||
|
||||
# User context middleware makes small optimization for all other builtin
|
||||
# middlewares via caching the user and chat instances in the event context
|
||||
self.update.outer_middleware(UserContextMiddleware())
|
||||
|
||||
# FSM middleware should always be registered after User context middleware
|
||||
# because here is used context from previous step
|
||||
self.fsm = FSMContextMiddleware(
|
||||
storage=storage if storage else MemoryStorage(),
|
||||
strategy=fsm_strategy,
|
||||
isolate_events=isolate_events,
|
||||
events_isolation=events_isolation if events_isolation else DisabledEventIsolation(),
|
||||
)
|
||||
self.update.outer_middleware(self.fsm)
|
||||
self.shutdown.register(self.fsm.close)
|
||||
|
||||
self._running_lock = Lock()
|
||||
|
||||
|
|
@ -104,7 +107,7 @@ class Dispatcher(Router):
|
|||
finally:
|
||||
finish_time = loop.time()
|
||||
duration = (finish_time - start_time) * 1000
|
||||
loggers.dispatcher.info(
|
||||
loggers.event.info(
|
||||
"Update id=%s is %s. Duration %d ms by bot id=%d",
|
||||
update.update_id,
|
||||
"handled" if handled else "not handled",
|
||||
|
|
@ -213,11 +216,11 @@ class Dispatcher(Router):
|
|||
try:
|
||||
await bot(result)
|
||||
except TelegramAPIError as e:
|
||||
# In due to WebHook mechanism doesn't allows to get response for
|
||||
# In due to WebHook mechanism doesn't allow getting response for
|
||||
# requests called in answer to WebHook request.
|
||||
# Need to skip unsuccessful responses.
|
||||
# For debugging here is added logging.
|
||||
loggers.dispatcher.error("Failed to make answer: %s: %s", e.__class__.__name__, e)
|
||||
loggers.event.error("Failed to make answer: %s: %s", e.__class__.__name__, e)
|
||||
|
||||
async def _process_update(
|
||||
self, bot: Bot, update: Update, call_answer: bool = True, **kwargs: Any
|
||||
|
|
@ -238,7 +241,7 @@ class Dispatcher(Router):
|
|||
return response is not UNHANDLED
|
||||
|
||||
except Exception as e:
|
||||
loggers.dispatcher.exception(
|
||||
loggers.event.exception(
|
||||
"Cause exception while process update id=%d by bot id=%d\n%s: %s",
|
||||
update.update_id,
|
||||
bot.id,
|
||||
|
|
@ -282,7 +285,7 @@ class Dispatcher(Router):
|
|||
try:
|
||||
return await self.feed_update(bot, update, **kwargs)
|
||||
except Exception as e:
|
||||
loggers.dispatcher.exception(
|
||||
loggers.event.exception(
|
||||
"Cause exception while process update id=%d by bot id=%d\n%s: %s",
|
||||
update.update_id,
|
||||
bot.id,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Type,
|
|||
from magic_filter import MagicFilter
|
||||
|
||||
from aiogram.dispatcher.filters.base import BaseFilter
|
||||
from aiogram.dispatcher.flags.getter import extract_flags_from_object
|
||||
from aiogram.dispatcher.handler.base import BaseHandler
|
||||
|
||||
CallbackType = Callable[..., Awaitable[Any]]
|
||||
|
|
@ -71,6 +72,7 @@ class HandlerObject(CallableMixin):
|
|||
callback = inspect.unwrap(self.callback)
|
||||
if inspect.isclass(callback) and issubclass(callback, BaseHandler):
|
||||
self.awaitable = True
|
||||
self.flags.update(extract_flags_from_object(callback))
|
||||
|
||||
async def check(self, *args: Any, **kwargs: Any) -> Tuple[bool, Dict[str, Any]]:
|
||||
if not self.filters:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,21 @@
|
|||
from typing import Dict, Tuple, Type
|
||||
|
||||
from .base import BaseFilter
|
||||
from .chat_member_updated import (
|
||||
ADMINISTRATOR,
|
||||
CREATOR,
|
||||
IS_ADMIN,
|
||||
IS_MEMBER,
|
||||
IS_NOT_MEMBER,
|
||||
JOIN_TRANSITION,
|
||||
KICKED,
|
||||
LEAVE_TRANSITION,
|
||||
LEFT,
|
||||
MEMBER,
|
||||
PROMOTED_TRANSITION,
|
||||
RESTRICTED,
|
||||
ChatMemberUpdatedFilter,
|
||||
)
|
||||
from .command import Command, CommandObject
|
||||
from .content_types import ContentTypesFilter
|
||||
from .exception import ExceptionMessageFilter, ExceptionTypeFilter
|
||||
|
|
@ -19,6 +34,19 @@ __all__ = (
|
|||
"ExceptionTypeFilter",
|
||||
"StateFilter",
|
||||
"MagicData",
|
||||
"ChatMemberUpdatedFilter",
|
||||
"CREATOR",
|
||||
"ADMINISTRATOR",
|
||||
"MEMBER",
|
||||
"RESTRICTED",
|
||||
"LEFT",
|
||||
"KICKED",
|
||||
"IS_MEMBER",
|
||||
"IS_ADMIN",
|
||||
"PROMOTED_TRANSITION",
|
||||
"IS_NOT_MEMBER",
|
||||
"JOIN_TRANSITION",
|
||||
"LEAVE_TRANSITION",
|
||||
)
|
||||
|
||||
_ALL_EVENTS_FILTERS: Tuple[Type[BaseFilter], ...] = (MagicData,)
|
||||
|
|
@ -84,10 +112,12 @@ BUILTIN_FILTERS: Dict[str, Tuple[Type[BaseFilter], ...]] = {
|
|||
"my_chat_member": (
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
*_TELEGRAM_EVENTS_FILTERS,
|
||||
ChatMemberUpdatedFilter,
|
||||
),
|
||||
"chat_member": (
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
*_TELEGRAM_EVENTS_FILTERS,
|
||||
ChatMemberUpdatedFilter,
|
||||
),
|
||||
"chat_join_request": (
|
||||
*_ALL_EVENTS_FILTERS,
|
||||
|
|
|
|||
179
aiogram/dispatcher/filters/chat_member_updated.py
Normal file
179
aiogram/dispatcher/filters/chat_member_updated.py
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
from typing import Any, Dict, Optional, TypeVar, Union
|
||||
|
||||
from aiogram.dispatcher.filters import BaseFilter
|
||||
from aiogram.types import ChatMember, ChatMemberUpdated
|
||||
|
||||
MarkerT = TypeVar("MarkerT", bound="_MemberStatusMarker")
|
||||
MarkerGroupT = TypeVar("MarkerGroupT", bound="_MemberStatusGroupMarker")
|
||||
TransitionT = TypeVar("TransitionT", bound="_MemberStatusTransition")
|
||||
|
||||
|
||||
class _MemberStatusMarker:
|
||||
def __init__(self, name: str, *, is_member: Optional[bool] = None) -> None:
|
||||
self.name = name
|
||||
self.is_member = is_member
|
||||
|
||||
def __str__(self) -> str:
|
||||
result = self.name.upper()
|
||||
if self.is_member is not None:
|
||||
result = ("+" if self.is_member else "-") + result
|
||||
return result
|
||||
|
||||
def __pos__(self: MarkerT) -> MarkerT:
|
||||
return type(self)(name=self.name, is_member=True)
|
||||
|
||||
def __neg__(self: MarkerT) -> MarkerT:
|
||||
return type(self)(name=self.name, is_member=False)
|
||||
|
||||
def __or__(
|
||||
self, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"]
|
||||
) -> "_MemberStatusGroupMarker":
|
||||
if isinstance(other, _MemberStatusMarker):
|
||||
return _MemberStatusGroupMarker(self, other)
|
||||
if isinstance(other, _MemberStatusGroupMarker):
|
||||
return other | self
|
||||
raise TypeError(
|
||||
f"unsupported operand type(s) for |: {type(self).__name__!r} and {type(other).__name__!r}"
|
||||
)
|
||||
|
||||
__ror__ = __or__
|
||||
|
||||
def __rshift__(
|
||||
self, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"]
|
||||
) -> "_MemberStatusTransition":
|
||||
old = _MemberStatusGroupMarker(self)
|
||||
if isinstance(other, _MemberStatusMarker):
|
||||
return _MemberStatusTransition(old=old, new=_MemberStatusGroupMarker(other))
|
||||
if isinstance(other, _MemberStatusGroupMarker):
|
||||
return _MemberStatusTransition(old=old, new=other)
|
||||
raise TypeError(
|
||||
f"unsupported operand type(s) for >>: {type(self).__name__!r} and {type(other).__name__!r}"
|
||||
)
|
||||
|
||||
def __lshift__(
|
||||
self, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"]
|
||||
) -> "_MemberStatusTransition":
|
||||
new = _MemberStatusGroupMarker(self)
|
||||
if isinstance(other, _MemberStatusMarker):
|
||||
return _MemberStatusTransition(old=_MemberStatusGroupMarker(other), new=new)
|
||||
if isinstance(other, _MemberStatusGroupMarker):
|
||||
return _MemberStatusTransition(old=other, new=new)
|
||||
raise TypeError(
|
||||
f"unsupported operand type(s) for <<: {type(self).__name__!r} and {type(other).__name__!r}"
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.name, self.is_member))
|
||||
|
||||
def check(self, *, member: ChatMember) -> bool:
|
||||
if self.is_member is not None and member.is_member != self.is_member:
|
||||
return False
|
||||
return self.name == member.status
|
||||
|
||||
|
||||
class _MemberStatusGroupMarker:
|
||||
def __init__(self, *statuses: _MemberStatusMarker) -> None:
|
||||
if not statuses:
|
||||
raise ValueError("Member status group should have at least one status included")
|
||||
self.statuses = frozenset(statuses)
|
||||
|
||||
def __or__(
|
||||
self: MarkerGroupT, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"]
|
||||
) -> MarkerGroupT:
|
||||
if isinstance(other, _MemberStatusMarker):
|
||||
return type(self)(*self.statuses, other)
|
||||
elif isinstance(other, _MemberStatusGroupMarker):
|
||||
return type(self)(*self.statuses, *other.statuses)
|
||||
raise TypeError(
|
||||
f"unsupported operand type(s) for |: {type(self).__name__!r} and {type(other).__name__!r}"
|
||||
)
|
||||
|
||||
def __rshift__(
|
||||
self, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"]
|
||||
) -> "_MemberStatusTransition":
|
||||
if isinstance(other, _MemberStatusMarker):
|
||||
return _MemberStatusTransition(old=self, new=_MemberStatusGroupMarker(other))
|
||||
if isinstance(other, _MemberStatusGroupMarker):
|
||||
return _MemberStatusTransition(old=self, new=other)
|
||||
raise TypeError(
|
||||
f"unsupported operand type(s) for >>: {type(self).__name__!r} and {type(other).__name__!r}"
|
||||
)
|
||||
|
||||
def __lshift__(
|
||||
self, other: Union["_MemberStatusMarker", "_MemberStatusGroupMarker"]
|
||||
) -> "_MemberStatusTransition":
|
||||
if isinstance(other, _MemberStatusMarker):
|
||||
return _MemberStatusTransition(old=_MemberStatusGroupMarker(other), new=self)
|
||||
if isinstance(other, _MemberStatusGroupMarker):
|
||||
return _MemberStatusTransition(old=other, new=self)
|
||||
raise TypeError(
|
||||
f"unsupported operand type(s) for <<: {type(self).__name__!r} and {type(other).__name__!r}"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
result = " | ".join(map(str, sorted(self.statuses, key=str)))
|
||||
if len(self.statuses) != 1:
|
||||
return f"({result})"
|
||||
return result
|
||||
|
||||
def check(self, *, member: ChatMember) -> bool:
|
||||
for status in self.statuses:
|
||||
if status.check(member=member):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class _MemberStatusTransition:
|
||||
def __init__(self, *, old: _MemberStatusGroupMarker, new: _MemberStatusGroupMarker) -> None:
|
||||
self.old = old
|
||||
self.new = new
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.old} >> {self.new}"
|
||||
|
||||
def __invert__(self: TransitionT) -> TransitionT:
|
||||
return type(self)(old=self.new, new=self.old)
|
||||
|
||||
def check(self, *, old: ChatMember, new: ChatMember) -> bool:
|
||||
return self.old.check(member=old) and self.new.check(member=new)
|
||||
|
||||
|
||||
CREATOR = _MemberStatusMarker("creator")
|
||||
ADMINISTRATOR = _MemberStatusMarker("administrator")
|
||||
MEMBER = _MemberStatusMarker("member")
|
||||
RESTRICTED = _MemberStatusMarker("restricted")
|
||||
LEFT = _MemberStatusMarker("left")
|
||||
KICKED = _MemberStatusMarker("kicked")
|
||||
|
||||
IS_MEMBER = CREATOR | ADMINISTRATOR | MEMBER | +RESTRICTED
|
||||
IS_ADMIN = CREATOR | ADMINISTRATOR
|
||||
IS_NOT_MEMBER = LEFT | KICKED | -RESTRICTED
|
||||
|
||||
JOIN_TRANSITION = IS_NOT_MEMBER >> IS_MEMBER
|
||||
LEAVE_TRANSITION = ~JOIN_TRANSITION
|
||||
PROMOTED_TRANSITION = (MEMBER | RESTRICTED | LEFT | KICKED) >> ADMINISTRATOR
|
||||
|
||||
|
||||
class ChatMemberUpdatedFilter(BaseFilter):
|
||||
member_status_changed: Union[
|
||||
_MemberStatusMarker,
|
||||
_MemberStatusGroupMarker,
|
||||
_MemberStatusTransition,
|
||||
]
|
||||
"""Accepts the status transition or new status of the member (see usage in docs)"""
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
async def __call__(self, member_updated: ChatMemberUpdated) -> Union[bool, Dict[str, Any]]:
|
||||
old = member_updated.old_chat_member
|
||||
new = member_updated.new_chat_member
|
||||
rule = self.member_status_changed
|
||||
|
||||
if isinstance(rule, (_MemberStatusMarker, _MemberStatusGroupMarker)):
|
||||
return rule.check(member=new)
|
||||
if isinstance(rule, _MemberStatusTransition):
|
||||
return rule.check(old=old, new=new)
|
||||
|
||||
# Impossible variant in due to pydantic validation
|
||||
return False # pragma: no cover
|
||||
0
aiogram/dispatcher/flags/__init__.py
Normal file
0
aiogram/dispatcher/flags/__init__.py
Normal file
60
aiogram/dispatcher/flags/flag.py
Normal file
60
aiogram/dispatcher/flags/flag.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Any, Callable, Optional, Union, cast, overload
|
||||
|
||||
from magic_filter import AttrDict
|
||||
|
||||
from aiogram.dispatcher.flags.getter import extract_flags_from_object
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Flag:
|
||||
name: str
|
||||
value: Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FlagDecorator:
|
||||
flag: Flag
|
||||
|
||||
@classmethod
|
||||
def _with_flag(cls, flag: Flag) -> "FlagDecorator":
|
||||
return cls(flag)
|
||||
|
||||
def _with_value(self, value: Any) -> "FlagDecorator":
|
||||
new_flag = Flag(self.flag.name, value)
|
||||
return self._with_flag(new_flag)
|
||||
|
||||
@overload
|
||||
def __call__(self, value: Callable[..., Any]) -> Callable[..., Any]: # type: ignore
|
||||
pass
|
||||
|
||||
@overload
|
||||
def __call__(self, value: Any) -> "FlagDecorator":
|
||||
pass
|
||||
|
||||
@overload
|
||||
def __call__(self, **kwargs: Any) -> "FlagDecorator":
|
||||
pass
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
value: Optional[Any] = None,
|
||||
**kwargs: Any,
|
||||
) -> Union[Callable[..., Any], "FlagDecorator"]:
|
||||
if value and kwargs:
|
||||
raise ValueError("The arguments `value` and **kwargs can not be used together")
|
||||
|
||||
if value is not None and callable(value):
|
||||
value.aiogram_flag = {
|
||||
**extract_flags_from_object(value),
|
||||
self.flag.name: self.flag.value,
|
||||
}
|
||||
return cast(Callable[..., Any], value)
|
||||
return self._with_value(AttrDict(kwargs) if value is None else value)
|
||||
|
||||
|
||||
class FlagGenerator:
|
||||
def __getattr__(self, name: str) -> FlagDecorator:
|
||||
if name[0] == "_":
|
||||
raise AttributeError("Flag name must NOT start with underscore")
|
||||
return FlagDecorator(Flag(name, True))
|
||||
56
aiogram/dispatcher/flags/getter.py
Normal file
56
aiogram/dispatcher/flags/getter.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
from typing import TYPE_CHECKING, Any, Dict, Optional, Union, cast
|
||||
|
||||
from magic_filter import AttrDict, MagicFilter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aiogram.dispatcher.event.handler import HandlerObject
|
||||
|
||||
|
||||
def extract_flags_from_object(obj: Any) -> Dict[str, Any]:
|
||||
if not hasattr(obj, "aiogram_flag"):
|
||||
return {}
|
||||
return cast(Dict[str, Any], obj.aiogram_flag)
|
||||
|
||||
|
||||
def extract_flags(handler: Union["HandlerObject", Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract flags from handler or middleware context data
|
||||
|
||||
:param handler: handler object or data
|
||||
:return: dictionary with all handler flags
|
||||
"""
|
||||
if isinstance(handler, dict) and "handler" in handler:
|
||||
handler = handler["handler"]
|
||||
if not hasattr(handler, "flags"):
|
||||
return {}
|
||||
return handler.flags # type: ignore
|
||||
|
||||
|
||||
def get_flag(
|
||||
handler: Union["HandlerObject", Dict[str, Any]],
|
||||
name: str,
|
||||
*,
|
||||
default: Optional[Any] = None,
|
||||
) -> Any:
|
||||
"""
|
||||
Get flag by name
|
||||
|
||||
:param handler: handler object or data
|
||||
:param name: name of the flag
|
||||
:param default: default value (None)
|
||||
:return: value of the flag or default
|
||||
"""
|
||||
flags = extract_flags(handler)
|
||||
return flags.get(name, default)
|
||||
|
||||
|
||||
def check_flags(handler: Union["HandlerObject", Dict[str, Any]], magic: MagicFilter) -> Any:
|
||||
"""
|
||||
Check flags via magic filter
|
||||
|
||||
:param handler: handler object or data
|
||||
:param magic: instance of the magic
|
||||
:return: the result of magic filter check
|
||||
"""
|
||||
flags = extract_flags(handler)
|
||||
return magic.resolve(AttrDict(flags))
|
||||
|
|
@ -2,7 +2,12 @@ from typing import Any, Awaitable, Callable, Dict, Optional, cast
|
|||
|
||||
from aiogram import Bot
|
||||
from aiogram.dispatcher.fsm.context import FSMContext
|
||||
from aiogram.dispatcher.fsm.storage.base import DEFAULT_DESTINY, BaseStorage, StorageKey
|
||||
from aiogram.dispatcher.fsm.storage.base import (
|
||||
DEFAULT_DESTINY,
|
||||
BaseEventIsolation,
|
||||
BaseStorage,
|
||||
StorageKey,
|
||||
)
|
||||
from aiogram.dispatcher.fsm.strategy import FSMStrategy, apply_strategy
|
||||
from aiogram.dispatcher.middlewares.base import BaseMiddleware
|
||||
from aiogram.types import TelegramObject
|
||||
|
|
@ -12,12 +17,12 @@ class FSMContextMiddleware(BaseMiddleware):
|
|||
def __init__(
|
||||
self,
|
||||
storage: BaseStorage,
|
||||
events_isolation: BaseEventIsolation,
|
||||
strategy: FSMStrategy = FSMStrategy.USER_IN_CHAT,
|
||||
isolate_events: bool = True,
|
||||
) -> None:
|
||||
self.storage = storage
|
||||
self.strategy = strategy
|
||||
self.isolate_events = isolate_events
|
||||
self.events_isolation = events_isolation
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
|
|
@ -30,9 +35,8 @@ class FSMContextMiddleware(BaseMiddleware):
|
|||
data["fsm_storage"] = self.storage
|
||||
if context:
|
||||
data.update({"state": context, "raw_state": await context.get_state()})
|
||||
if self.isolate_events:
|
||||
async with self.storage.lock(bot=bot, key=context.key):
|
||||
return await handler(event, data)
|
||||
async with self.events_isolation.lock(bot=bot, key=context.key):
|
||||
return await handler(event, data)
|
||||
return await handler(event, data)
|
||||
|
||||
def resolve_event_context(
|
||||
|
|
@ -81,3 +85,7 @@ class FSMContextMiddleware(BaseMiddleware):
|
|||
destiny=destiny,
|
||||
),
|
||||
)
|
||||
|
||||
async def close(self) -> None:
|
||||
await self.storage.close()
|
||||
await self.events_isolation.close()
|
||||
|
|
|
|||
|
|
@ -24,19 +24,6 @@ class BaseStorage(ABC):
|
|||
Base class for all FSM storages
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
@asynccontextmanager
|
||||
async def lock(self, bot: Bot, key: StorageKey) -> AsyncGenerator[None, None]:
|
||||
"""
|
||||
Isolate events with lock.
|
||||
Will be used as context manager
|
||||
|
||||
:param bot: instance of the current bot
|
||||
:param key: storage key
|
||||
:return: An async generator
|
||||
"""
|
||||
yield None
|
||||
|
||||
@abstractmethod
|
||||
async def set_state(self, bot: Bot, key: StorageKey, state: StateType = None) -> None:
|
||||
"""
|
||||
|
|
@ -101,3 +88,22 @@ class BaseStorage(ABC):
|
|||
Close storage (database connection, file or etc.)
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BaseEventIsolation(ABC):
|
||||
@abstractmethod
|
||||
@asynccontextmanager
|
||||
async def lock(self, bot: Bot, key: StorageKey) -> AsyncGenerator[None, None]:
|
||||
"""
|
||||
Isolate events with lock.
|
||||
Will be used as context manager
|
||||
|
||||
:param bot: instance of the current bot
|
||||
:param key: storage key
|
||||
:return: An async generator
|
||||
"""
|
||||
yield None
|
||||
|
||||
@abstractmethod
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -2,18 +2,22 @@ from asyncio import Lock
|
|||
from collections import defaultdict
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, AsyncGenerator, DefaultDict, Dict, Optional
|
||||
from typing import Any, AsyncGenerator, DefaultDict, Dict, Hashable, Optional
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.dispatcher.fsm.state import State
|
||||
from aiogram.dispatcher.fsm.storage.base import BaseStorage, StateType, StorageKey
|
||||
from aiogram.dispatcher.fsm.storage.base import (
|
||||
BaseEventIsolation,
|
||||
BaseStorage,
|
||||
StateType,
|
||||
StorageKey,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemoryStorageRecord:
|
||||
data: Dict[str, Any] = field(default_factory=dict)
|
||||
state: Optional[str] = None
|
||||
lock: Lock = field(default_factory=Lock)
|
||||
|
||||
|
||||
class MemoryStorage(BaseStorage):
|
||||
|
|
@ -34,11 +38,6 @@ class MemoryStorage(BaseStorage):
|
|||
async def close(self) -> None:
|
||||
pass
|
||||
|
||||
@asynccontextmanager
|
||||
async def lock(self, bot: Bot, key: StorageKey) -> AsyncGenerator[None, None]:
|
||||
async with self.storage[key].lock:
|
||||
yield None
|
||||
|
||||
async def set_state(self, bot: Bot, key: StorageKey, state: StateType = None) -> None:
|
||||
self.storage[key].state = state.state if isinstance(state, State) else state
|
||||
|
||||
|
|
@ -50,3 +49,27 @@ class MemoryStorage(BaseStorage):
|
|||
|
||||
async def get_data(self, bot: Bot, key: StorageKey) -> Dict[str, Any]:
|
||||
return self.storage[key].data.copy()
|
||||
|
||||
|
||||
class DisabledEventIsolation(BaseEventIsolation):
|
||||
@asynccontextmanager
|
||||
async def lock(self, bot: Bot, key: StorageKey) -> AsyncGenerator[None, None]:
|
||||
yield
|
||||
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class SimpleEventIsolation(BaseEventIsolation):
|
||||
def __init__(self) -> None:
|
||||
# TODO: Unused locks cleaner is needed
|
||||
self._locks: DefaultDict[Hashable, Lock] = defaultdict(Lock)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lock(self, bot: Bot, key: StorageKey) -> AsyncGenerator[None, None]:
|
||||
lock = self._locks[key]
|
||||
async with lock:
|
||||
yield
|
||||
|
||||
async def close(self) -> None:
|
||||
self._locks.clear()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@ from aioredis import ConnectionPool, Redis
|
|||
|
||||
from aiogram import Bot
|
||||
from aiogram.dispatcher.fsm.state import State
|
||||
from aiogram.dispatcher.fsm.storage.base import DEFAULT_DESTINY, BaseStorage, StateType, StorageKey
|
||||
from aiogram.dispatcher.fsm.storage.base import (
|
||||
DEFAULT_DESTINY,
|
||||
BaseEventIsolation,
|
||||
BaseStorage,
|
||||
StateType,
|
||||
StorageKey,
|
||||
)
|
||||
|
||||
DEFAULT_REDIS_LOCK_KWARGS = {"timeout": 60}
|
||||
|
||||
|
|
@ -121,19 +127,12 @@ class RedisStorage(BaseStorage):
|
|||
redis = Redis(connection_pool=pool)
|
||||
return cls(redis=redis, **kwargs)
|
||||
|
||||
def create_isolation(self, **kwargs: Any) -> "RedisEventIsolation":
|
||||
return RedisEventIsolation(redis=self.redis, key_builder=self.key_builder, **kwargs)
|
||||
|
||||
async def close(self) -> None:
|
||||
await self.redis.close() # type: ignore
|
||||
|
||||
@asynccontextmanager
|
||||
async def lock(
|
||||
self,
|
||||
bot: Bot,
|
||||
key: StorageKey,
|
||||
) -> AsyncGenerator[None, None]:
|
||||
redis_key = self.key_builder.build(key, "lock")
|
||||
async with self.redis.lock(name=redis_key, **self.lock_kwargs):
|
||||
yield None
|
||||
|
||||
async def set_state(
|
||||
self,
|
||||
bot: Bot,
|
||||
|
|
@ -146,8 +145,8 @@ class RedisStorage(BaseStorage):
|
|||
else:
|
||||
await self.redis.set(
|
||||
redis_key,
|
||||
state.state if isinstance(state, State) else state, # type: ignore[arg-type]
|
||||
ex=self.state_ttl, # type: ignore[arg-type]
|
||||
cast(str, state.state if isinstance(state, State) else state),
|
||||
ex=self.state_ttl,
|
||||
)
|
||||
|
||||
async def get_state(
|
||||
|
|
@ -174,7 +173,7 @@ class RedisStorage(BaseStorage):
|
|||
await self.redis.set(
|
||||
redis_key,
|
||||
bot.session.json_dumps(data),
|
||||
ex=self.data_ttl, # type: ignore[arg-type]
|
||||
ex=self.data_ttl,
|
||||
)
|
||||
|
||||
async def get_data(
|
||||
|
|
@ -189,3 +188,43 @@ class RedisStorage(BaseStorage):
|
|||
if isinstance(value, bytes):
|
||||
value = value.decode("utf-8")
|
||||
return cast(Dict[str, Any], bot.session.json_loads(value))
|
||||
|
||||
|
||||
class RedisEventIsolation(BaseEventIsolation):
|
||||
def __init__(
|
||||
self,
|
||||
redis: Redis,
|
||||
key_builder: Optional[KeyBuilder] = None,
|
||||
lock_kwargs: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
if key_builder is None:
|
||||
key_builder = DefaultKeyBuilder()
|
||||
self.redis = redis
|
||||
self.key_builder = key_builder
|
||||
self.lock_kwargs = lock_kwargs or {}
|
||||
|
||||
@classmethod
|
||||
def from_url(
|
||||
cls,
|
||||
url: str,
|
||||
connection_kwargs: Optional[Dict[str, Any]] = None,
|
||||
**kwargs: Any,
|
||||
) -> "RedisEventIsolation":
|
||||
if connection_kwargs is None:
|
||||
connection_kwargs = {}
|
||||
pool = ConnectionPool.from_url(url, **connection_kwargs)
|
||||
redis = Redis(connection_pool=pool)
|
||||
return cls(redis=redis, **kwargs)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lock(
|
||||
self,
|
||||
bot: Bot,
|
||||
key: StorageKey,
|
||||
) -> AsyncGenerator[None, None]:
|
||||
redis_key = self.key_builder.build(key, "lock")
|
||||
async with self.redis.lock(name=redis_key, **self.lock_kwargs):
|
||||
yield None
|
||||
|
||||
async def close(self) -> None:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ class BaseRequestHandler(ABC):
|
|||
bot=bot, update=await request.json(loads=bot.session.json_loads)
|
||||
)
|
||||
)
|
||||
return web.json_response({})
|
||||
return web.json_response({}, dumps=bot.session.json_dumps)
|
||||
|
||||
async def _handle_request(self, bot: Bot, request: web.Request) -> web.Response:
|
||||
result = await self.dispatcher.feed_webhook_update(
|
||||
|
|
@ -143,8 +143,8 @@ class BaseRequestHandler(ABC):
|
|||
**self.data,
|
||||
)
|
||||
if result:
|
||||
return web.json_response(result)
|
||||
return web.json_response({})
|
||||
return web.json_response(result, dumps=bot.session.json_dumps)
|
||||
return web.json_response({}, dumps=bot.session.json_dumps)
|
||||
|
||||
async def handle(self, request: web.Request) -> web.Response:
|
||||
bot = await self.resolve_bot(request)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
|
||||
dispatcher = logging.getLogger("aiogram.dispatcher")
|
||||
event = logging.getLogger("aiogram.event")
|
||||
middlewares = logging.getLogger("aiogram.middlewares")
|
||||
webhook = logging.getLogger("aiogram.webhook")
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
|||
|
||||
class AddStickerToSet(TelegramMethod[bool]):
|
||||
"""
|
||||
Use this method to add a new sticker to a set created by the bot. You **must** use exactly one of the fields *png_sticker* or *tgs_sticker*. Animated stickers can be added to animated sticker sets and only to them. Animated sticker sets can have up to 50 stickers. Static sticker sets can have up to 120 stickers. Returns :code:`True` on success.
|
||||
Use this method to add a new sticker to a set created by the bot. You **must** use exactly one of the fields *png_sticker*, *tgs_sticker*, or *webm_sticker*. Animated stickers can be added to animated sticker sets and only to them. Animated sticker sets can have up to 50 stickers. Static sticker sets can have up to 120 stickers. Returns :code:`True` on success.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#addstickertoset
|
||||
"""
|
||||
|
|
@ -27,15 +27,18 @@ class AddStickerToSet(TelegramMethod[bool]):
|
|||
png_sticker: Optional[Union[InputFile, str]] = None
|
||||
"""**PNG** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a *file_id* as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » <sending-files>`"""
|
||||
tgs_sticker: Optional[InputFile] = None
|
||||
"""**TGS** animation with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/animated_stickers#technical-requirements <https://core.telegram.org/animated_stickers#technical-requirements>`_`https://core.telegram.org/animated_stickers#technical-requirements <https://core.telegram.org/animated_stickers#technical-requirements>`_ for technical requirements"""
|
||||
"""**TGS** animation with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/stickers#animated-sticker-requirements <https://core.telegram.org/stickers#animated-sticker-requirements>`_`https://core.telegram.org/stickers#animated-sticker-requirements <https://core.telegram.org/stickers#animated-sticker-requirements>`_ for technical requirements"""
|
||||
webm_sticker: Optional[InputFile] = None
|
||||
"""**WEBM** video with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/stickers#video-sticker-requirements <https://core.telegram.org/stickers#video-sticker-requirements>`_`https://core.telegram.org/stickers#video-sticker-requirements <https://core.telegram.org/stickers#video-sticker-requirements>`_ for technical requirements"""
|
||||
mask_position: Optional[MaskPosition] = None
|
||||
"""A JSON-serialized object for position where the mask should be placed on faces"""
|
||||
|
||||
def build_request(self, bot: Bot) -> Request:
|
||||
data: Dict[str, Any] = self.dict(exclude={"png_sticker", "tgs_sticker"})
|
||||
data: Dict[str, Any] = self.dict(exclude={"png_sticker", "tgs_sticker", "webm_sticker"})
|
||||
|
||||
files: Dict[str, InputFile] = {}
|
||||
prepare_file(data=data, files=files, name="png_sticker", value=self.png_sticker)
|
||||
prepare_file(data=data, files=files, name="tgs_sticker", value=self.tgs_sticker)
|
||||
prepare_file(data=data, files=files, name="webm_sticker", value=self.webm_sticker)
|
||||
|
||||
return Request(method="addStickerToSet", data=data, files=files)
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ class CopyMessage(TelegramMethod[MessageId]):
|
|||
"""A JSON-serialized list of special entities that appear in the new caption, which can be specified instead of *parse_mode*"""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
|||
|
||||
class CreateNewStickerSet(TelegramMethod[bool]):
|
||||
"""
|
||||
Use this method to create a new sticker set owned by a user. The bot will be able to edit the sticker set thus created. You **must** use exactly one of the fields *png_sticker* or *tgs_sticker*. Returns :code:`True` on success.
|
||||
Use this method to create a new sticker set owned by a user. The bot will be able to edit the sticker set thus created. You **must** use exactly one of the fields *png_sticker*, *tgs_sticker*, or *webm_sticker*. Returns :code:`True` on success.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#createnewstickerset
|
||||
"""
|
||||
|
|
@ -29,17 +29,20 @@ class CreateNewStickerSet(TelegramMethod[bool]):
|
|||
png_sticker: Optional[Union[InputFile, str]] = None
|
||||
"""**PNG** image with the sticker, must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a *file_id* as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » <sending-files>`"""
|
||||
tgs_sticker: Optional[InputFile] = None
|
||||
"""**TGS** animation with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/animated_stickers#technical-requirements <https://core.telegram.org/animated_stickers#technical-requirements>`_`https://core.telegram.org/animated_stickers#technical-requirements <https://core.telegram.org/animated_stickers#technical-requirements>`_ for technical requirements"""
|
||||
"""**TGS** animation with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/stickers#animated-sticker-requirements <https://core.telegram.org/stickers#animated-sticker-requirements>`_`https://core.telegram.org/stickers#animated-sticker-requirements <https://core.telegram.org/stickers#animated-sticker-requirements>`_ for technical requirements"""
|
||||
webm_sticker: Optional[InputFile] = None
|
||||
"""**WEBM** video with the sticker, uploaded using multipart/form-data. See `https://core.telegram.org/stickers#video-sticker-requirements <https://core.telegram.org/stickers#video-sticker-requirements>`_`https://core.telegram.org/stickers#video-sticker-requirements <https://core.telegram.org/stickers#video-sticker-requirements>`_ for technical requirements"""
|
||||
contains_masks: Optional[bool] = None
|
||||
"""Pass :code:`True`, if a set of mask stickers should be created"""
|
||||
mask_position: Optional[MaskPosition] = None
|
||||
"""A JSON-serialized object for position where the mask should be placed on faces"""
|
||||
|
||||
def build_request(self, bot: Bot) -> Request:
|
||||
data: Dict[str, Any] = self.dict(exclude={"png_sticker", "tgs_sticker"})
|
||||
data: Dict[str, Any] = self.dict(exclude={"png_sticker", "tgs_sticker", "webm_sticker"})
|
||||
|
||||
files: Dict[str, InputFile] = {}
|
||||
prepare_file(data=data, files=files, name="png_sticker", value=self.png_sticker)
|
||||
prepare_file(data=data, files=files, name="tgs_sticker", value=self.tgs_sticker)
|
||||
prepare_file(data=data, files=files, name="webm_sticker", value=self.webm_sticker)
|
||||
|
||||
return Request(method="createNewStickerSet", data=data, files=files)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ class ForwardMessage(TelegramMethod[Message]):
|
|||
"""Message identifier in the chat specified in *from_chat_id*"""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the forwarded message from forwarding and saving"""
|
||||
|
||||
def build_request(self, bot: Bot) -> Request:
|
||||
data: Dict[str, Any] = self.dict()
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ class SendAnimation(TelegramMethod[Message]):
|
|||
"""A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode*"""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ class SendAudio(TelegramMethod[Message]):
|
|||
"""Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass 'attach://<file_attach_name>' if the thumbnail was uploaded using multipart/form-data under <file_attach_name>. :ref:`More info on Sending Files » <sending-files>`"""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ class SendContact(TelegramMethod[Message]):
|
|||
"""Additional data about the contact in the form of a `vCard <https://en.wikipedia.org/wiki/VCard>`_, 0-2048 bytes"""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ class SendDice(TelegramMethod[Message]):
|
|||
"""Emoji on which the dice throw animation is based. Currently, must be one of '🎲', '🎯', '🏀', '⚽', '🎳', or '🎰'. Dice can have values 1-6 for '🎲', '🎯' and '🎳', values 1-5 for '🏀' and '⚽', and values 1-64 for '🎰'. Defaults to '🎲'"""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ class SendDocument(TelegramMethod[Message]):
|
|||
"""Disables automatic server-side content type detection for files uploaded using multipart/form-data"""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ class SendGame(TelegramMethod[Message]):
|
|||
"""Short name of the game, serves as the unique identifier for the game. Set up your games via `Botfather <https://t.me/botfather>`_."""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ class SendInvoice(TelegramMethod[Message]):
|
|||
"""Pass :code:`True`, if the final price depends on the shipping method"""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ class SendLocation(TelegramMethod[Message]):
|
|||
"""For live locations, a maximum distance for proximity alerts about approaching another chat member, in meters. Must be between 1 and 100000 if specified."""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ class SendMediaGroup(TelegramMethod[List[Message]]):
|
|||
"""A JSON-serialized array describing messages to be sent, must include 2-10 items"""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends messages `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent messages from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the messages are a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ class SendMessage(TelegramMethod[Message]):
|
|||
"""Disables link previews for links in this message"""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ class SendPhoto(TelegramMethod[Message]):
|
|||
"""A JSON-serialized list of special entities that appear in the caption, which can be specified instead of *parse_mode*"""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ class SendPoll(TelegramMethod[Message]):
|
|||
"""Pass :code:`True`, if the poll needs to be immediately closed. This can be useful for poll preview."""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ if TYPE_CHECKING:
|
|||
|
||||
class SendSticker(TelegramMethod[Message]):
|
||||
"""
|
||||
Use this method to send static .WEBP or `animated <https://telegram.org/blog/animated-stickers>`_ .TGS stickers. On success, the sent :class:`aiogram.types.message.Message` is returned.
|
||||
Use this method to send static .WEBP, `animated <https://telegram.org/blog/animated-stickers>`_ .TGS, or `video <https://telegram.org/blog/video-stickers-better-reactions>`_ .WEBM stickers. On success, the sent :class:`aiogram.types.message.Message` is returned.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#sendsticker
|
||||
"""
|
||||
|
|
@ -31,6 +31,8 @@ class SendSticker(TelegramMethod[Message]):
|
|||
"""Sticker to send. Pass a file_id as String to send a file that exists on the Telegram servers (recommended), pass an HTTP URL as a String for Telegram to get a .WEBP file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » <sending-files>`"""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ class SendVenue(TelegramMethod[Message]):
|
|||
"""Google Places type of the venue. (See `supported types <https://developers.google.com/places/web-service/supported_types>`_.)"""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ class SendVideo(TelegramMethod[Message]):
|
|||
"""Pass :code:`True`, if the uploaded video is suitable for streaming"""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ class SendVideoNote(TelegramMethod[Message]):
|
|||
"""Thumbnail of the file sent; can be ignored if thumbnail generation for the file is supported server-side. The thumbnail should be in JPEG format and less than 200 kB in size. A thumbnail's width and height should not exceed 320. Ignored if the file is not uploaded using multipart/form-data. Thumbnails can't be reused and can be only uploaded as a new file, so you can pass 'attach://<file_attach_name>' if the thumbnail was uploaded using multipart/form-data under <file_attach_name>. :ref:`More info on Sending Files » <sending-files>`"""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ class SendVoice(TelegramMethod[Message]):
|
|||
"""Duration of the voice message in seconds"""
|
||||
disable_notification: Optional[bool] = None
|
||||
"""Sends the message `silently <https://telegram.org/blog/channels-2-0#silent-messages>`_. Users will receive a notification with no sound."""
|
||||
protect_content: Optional[bool] = None
|
||||
"""Protects the contents of the sent message from forwarding and saving"""
|
||||
reply_to_message_id: Optional[int] = None
|
||||
"""If the message is a reply, ID of the original message"""
|
||||
allow_sending_without_reply: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
|||
|
||||
class SetStickerSetThumb(TelegramMethod[bool]):
|
||||
"""
|
||||
Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set for animated sticker sets only. Returns :code:`True` on success.
|
||||
Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set for animated sticker sets only. Video thumbnails can be set only for video sticker sets only. Returns :code:`True` on success.
|
||||
|
||||
Source: https://core.telegram.org/bots/api#setstickersetthumb
|
||||
"""
|
||||
|
|
@ -23,7 +23,7 @@ class SetStickerSetThumb(TelegramMethod[bool]):
|
|||
user_id: int
|
||||
"""User identifier of the sticker set owner"""
|
||||
thumb: Optional[Union[InputFile, str]] = None
|
||||
"""A **PNG** image with the thumbnail, must be up to 128 kilobytes in size and have width and height exactly 100px, or a **TGS** animation with the thumbnail up to 32 kilobytes in size; see `https://core.telegram.org/animated_stickers#technical-requirements <https://core.telegram.org/animated_stickers#technical-requirements>`_`https://core.telegram.org/animated_stickers#technical-requirements <https://core.telegram.org/animated_stickers#technical-requirements>`_ for animated sticker technical requirements. Pass a *file_id* as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » <sending-files>`. Animated sticker set thumbnail can't be uploaded via HTTP URL."""
|
||||
"""A **PNG** image with the thumbnail, must be up to 128 kilobytes in size and have width and height exactly 100px, or a **TGS** animation with the thumbnail up to 32 kilobytes in size; see `https://core.telegram.org/stickers#animated-sticker-requirements <https://core.telegram.org/stickers#animated-sticker-requirements>`_`https://core.telegram.org/stickers#animated-sticker-requirements <https://core.telegram.org/stickers#animated-sticker-requirements>`_ for animated sticker technical requirements, or a **WEBM** video with the thumbnail up to 32 kilobytes in size; see `https://core.telegram.org/stickers#video-sticker-requirements <https://core.telegram.org/stickers#video-sticker-requirements>`_`https://core.telegram.org/stickers#video-sticker-requirements <https://core.telegram.org/stickers#video-sticker-requirements>`_ for video sticker technical requirements. Pass a *file_id* as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. :ref:`More info on Sending Files » <sending-files>`. Animated sticker set thumbnails can't be uploaded via HTTP URL."""
|
||||
|
||||
def build_request(self, bot: Bot) -> Request:
|
||||
data: Dict[str, Any] = self.dict(exclude={"thumb"})
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class UnbanChatMember(TelegramMethod[bool]):
|
|||
__returning__ = bool
|
||||
|
||||
chat_id: Union[int, str]
|
||||
"""Unique identifier for the target group or username of the target supergroup or channel (in the format :code:`@username`)"""
|
||||
"""Unique identifier for the target group or username of the target supergroup or channel (in the format :code:`@channelusername`)"""
|
||||
user_id: int
|
||||
"""Unique identifier of the target user"""
|
||||
only_if_banned: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@ class BotCommand(MutableTelegramObject):
|
|||
"""
|
||||
|
||||
command: str
|
||||
"""Text of the command, 1-32 characters. Can contain only lowercase English letters, digits and underscores."""
|
||||
"""Text of the command; 1-32 characters. Can contain only lowercase English letters, digits and underscores."""
|
||||
description: str
|
||||
"""Description of the command, 3-256 characters."""
|
||||
"""Description of the command; 1-256 characters."""
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from .base import TelegramObject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .user import User
|
||||
|
||||
|
||||
class ChatMember(TelegramObject):
|
||||
"""
|
||||
|
|
@ -16,3 +22,48 @@ class ChatMember(TelegramObject):
|
|||
|
||||
Source: https://core.telegram.org/bots/api#chatmember
|
||||
"""
|
||||
|
||||
status: str
|
||||
"""..."""
|
||||
user: Optional[User] = None
|
||||
"""*Optional*. Information about the user"""
|
||||
is_anonymous: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the user's presence in the chat is hidden"""
|
||||
custom_title: Optional[str] = None
|
||||
"""*Optional*. Custom title for this user"""
|
||||
can_be_edited: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the bot is allowed to edit administrator privileges of that user"""
|
||||
can_manage_chat: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the administrator can access the chat event log, chat statistics, message statistics in channels, see channel members, see anonymous administrators in supergroups and ignore slow mode. Implied by any other administrator privilege"""
|
||||
can_delete_messages: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the administrator can delete messages of other users"""
|
||||
can_manage_voice_chats: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the administrator can manage voice chats"""
|
||||
can_restrict_members: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the administrator can restrict, ban or unban chat members"""
|
||||
can_promote_members: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the administrator can add new administrators with a subset of their own privileges or demote administrators that he has promoted, directly or indirectly (promoted by administrators that were appointed by the user)"""
|
||||
can_change_info: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the user is allowed to change the chat title, photo and other settings"""
|
||||
can_invite_users: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the user is allowed to invite new users to the chat"""
|
||||
can_post_messages: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the administrator can post in the channel; channels only"""
|
||||
can_edit_messages: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the administrator can edit messages of other users and can pin messages; channels only"""
|
||||
can_pin_messages: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the user is allowed to pin messages; groups and supergroups only"""
|
||||
is_member: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the user is a member of the chat at the moment of the request"""
|
||||
can_send_messages: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the user is allowed to send text messages, contacts, locations and venues"""
|
||||
can_send_media_messages: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the user is allowed to send audios, documents, photos, videos, video notes and voice notes"""
|
||||
can_send_polls: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the user is allowed to send polls"""
|
||||
can_send_other_messages: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the user is allowed to send animations, games, stickers and use inline bots"""
|
||||
can_add_web_page_previews: Optional[bool] = None
|
||||
"""*Optional*. :code:`True`, if the user is allowed to add web page previews to their messages"""
|
||||
until_date: Optional[Union[datetime.datetime, datetime.timedelta, int]] = None
|
||||
"""*Optional*. Date when restrictions will be lifted for this user; unix time. If 0, then the user is restricted forever"""
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class Message(TelegramObject):
|
|||
forward_from_message_id: Optional[int] = None
|
||||
"""*Optional*. For messages forwarded from channels, identifier of the original message in the channel"""
|
||||
forward_signature: Optional[str] = None
|
||||
"""*Optional*. For messages forwarded from channels, signature of the post author if present"""
|
||||
"""*Optional*. For forwarded messages that were originally sent in channels or by an anonymous chat administrator, signature of the message sender if present"""
|
||||
forward_sender_name: Optional[str] = None
|
||||
"""*Optional*. Sender's name for messages forwarded from users who disallow adding a link to their account in forwarded messages"""
|
||||
forward_date: Optional[int] = None
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class MessageEntity(MutableTelegramObject):
|
|||
"""
|
||||
|
||||
type: str
|
||||
"""Type of the entity. Can be 'mention' (:code:`@username`), 'hashtag' (:code:`#hashtag`), 'cashtag' (:code:`$USD`), 'bot_command' (:code:`/start@jobs_bot`), 'url' (:code:`https://telegram.org`), 'email' (:code:`do-not-reply@telegram.org`), 'phone_number' (:code:`+1-212-555-0123`), 'bold' (**bold text**), 'italic' (*italic text*), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users `without usernames <https://telegram.org/blog/edit#new-mentions>`_)"""
|
||||
"""Type of the entity. Currently, can be 'mention' (:code:`@username`), 'hashtag' (:code:`#hashtag`), 'cashtag' (:code:`$USD`), 'bot_command' (:code:`/start@jobs_bot`), 'url' (:code:`https://telegram.org`), 'email' (:code:`do-not-reply@telegram.org`), 'phone_number' (:code:`+1-212-555-0123`), 'bold' (**bold text**), 'italic' (*italic text*), 'underline' (underlined text), 'strikethrough' (strikethrough text), 'spoiler' (spoiler message), 'code' (monowidth string), 'pre' (monowidth block), 'text_link' (for clickable text URLs), 'text_mention' (for users `without usernames <https://telegram.org/blog/edit#new-mentions>`_)"""
|
||||
offset: int
|
||||
"""Offset in UTF-16 code units to the start of the entity"""
|
||||
length: int
|
||||
|
|
|
|||
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