From 7be5808136493ec5be8e570ac3b6ab1e6bdc411b Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Fri, 11 Feb 2022 15:21:38 +0200 Subject: [PATCH] Update API, added some new features --- aiogram/__init__.py | 3 + aiogram/client/bot.py | 57 ++++ aiogram/dispatcher/event/handler.py | 2 + aiogram/dispatcher/flags/__init__.py | 0 aiogram/dispatcher/flags/flags.py | 56 ++++ aiogram/dispatcher/flags/getter.py | 35 ++ aiogram/dispatcher/webhook/aiohttp_server.py | 6 +- aiogram/methods/copy_message.py | 2 + aiogram/methods/forward_message.py | 2 + aiogram/methods/send_animation.py | 2 + aiogram/methods/send_audio.py | 2 + aiogram/methods/send_contact.py | 2 + aiogram/methods/send_dice.py | 2 + aiogram/methods/send_document.py | 2 + aiogram/methods/send_game.py | 2 + aiogram/methods/send_invoice.py | 2 + aiogram/methods/send_location.py | 2 + aiogram/methods/send_media_group.py | 2 + aiogram/methods/send_message.py | 2 + aiogram/methods/send_photo.py | 2 + aiogram/methods/send_poll.py | 2 + aiogram/methods/send_sticker.py | 2 + aiogram/methods/send_venue.py | 2 + aiogram/methods/send_video.py | 2 + aiogram/methods/send_video_note.py | 2 + aiogram/methods/send_voice.py | 2 + aiogram/types/bot_command.py | 4 +- aiogram/types/message.py | 2 +- aiogram/types/message_entity.py | 2 +- aiogram/utils/chat_action.py | 298 ++++++++++++++++++ aiogram/utils/i18n/middleware.py | 12 +- poetry.lock | 46 ++- pyproject.toml | 1 + .../test_fsm/storage/test_redis.py | 2 - 34 files changed, 547 insertions(+), 17 deletions(-) create mode 100644 aiogram/dispatcher/flags/__init__.py create mode 100644 aiogram/dispatcher/flags/flags.py create mode 100644 aiogram/dispatcher/flags/getter.py create mode 100644 aiogram/utils/chat_action.py diff --git a/aiogram/__init__.py b/aiogram/__init__.py index a820ee6c..6ed65c76 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -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.flags 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,6 +36,7 @@ __all__ = ( "F", "html", "md", + "flags", ) __version__ = "3.0.0b1" diff --git a/aiogram/client/bot.py b/aiogram/client/bot.py index d6378215..554393cf 100644 --- a/aiogram/client/bot.py +++ b/aiogram/client/bot.py @@ -533,6 +533,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 +552,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 `_. 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 `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -564,6 +566,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 +579,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 +591,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 `_. 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 +600,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 +613,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 +633,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param parse_mode: Mode for parsing entities in the new caption. See `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 `_. 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 `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -640,6 +648,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 +663,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 +682,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param parse_mode: Mode for parsing entities in the photo caption. See `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 `_. 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 `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -685,6 +696,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 +715,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 +739,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://' if the thumbnail was uploaded using multipart/form-data under . :ref:`More info on Sending Files ยป ` :param disable_notification: Sends the message `silently `_. 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 `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -743,6 +757,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 +774,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 +795,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 `_. 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 `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -794,6 +811,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 +831,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 +855,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 `_. 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 `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -854,6 +874,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 +893,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 +916,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param parse_mode: Mode for parsing entities in the animation caption. See `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 `_. 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 `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -911,6 +934,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 +950,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 +970,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 `_. 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 `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -959,6 +985,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 +1000,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 +1019,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://' if the thumbnail was uploaded using multipart/form-data under . :ref:`More info on Sending Files ยป ` :param disable_notification: Sends the message `silently `_. 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 `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -1004,6 +1033,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 +1045,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 +1058,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 `_. 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 +1068,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 +1084,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 +1105,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 `_. 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 `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -1086,6 +1121,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 +1213,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 +1236,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 `_.) :param disable_notification: Sends the message `silently `_. 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 `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -1216,6 +1254,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 +1269,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 +1288,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 `_, 0-2048 bytes :param disable_notification: Sends the message `silently `_. 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 `_, `custom reply keyboard `_, instructions to remove keyboard or to force a reply from the user. @@ -1261,6 +1302,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 +1325,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 +1352,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 `_. 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 `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -1330,6 +1374,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 +1386,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 +1402,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 `_. 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 `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -1366,6 +1413,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, @@ -2512,6 +2560,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[ @@ -2527,6 +2576,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 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 ยป ` :param disable_notification: Sends the message `silently `_. 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 `_, `custom reply keyboard `_, instructions to remove reply keyboard or to force a reply from the user. @@ -2537,6 +2587,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, @@ -2798,6 +2849,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 +2883,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 `_. 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 `_. If empty, one 'Pay :code:`total price`' button will be shown. If not empty, the first button must be a Pay button. @@ -2861,6 +2914,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 +3014,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 +3028,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 `_. :param disable_notification: Sends the message `silently `_. 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 `_. If empty, one 'Play game_title' button will be shown. If not empty, the first button must launch the game. @@ -2983,6 +3039,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, diff --git a/aiogram/dispatcher/event/handler.py b/aiogram/dispatcher/event/handler.py index 7937d209..813ddf51 100644 --- a/aiogram/dispatcher/event/handler.py +++ b/aiogram/dispatcher/event/handler.py @@ -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: diff --git a/aiogram/dispatcher/flags/__init__.py b/aiogram/dispatcher/flags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aiogram/dispatcher/flags/flags.py b/aiogram/dispatcher/flags/flags.py new file mode 100644 index 00000000..8df2d9f1 --- /dev/null +++ b/aiogram/dispatcher/flags/flags.py @@ -0,0 +1,56 @@ +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 +class Flag: + name: str + value: Any + + +@dataclass +class FlagDecorator: + flag: Flag + + def with_value(self, value: Any) -> "FlagDecorator": + new_flag = Flag(self.flag.name, value) + return type(self)(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)) diff --git a/aiogram/dispatcher/flags/getter.py b/aiogram/dispatcher/flags/getter.py new file mode 100644 index 00000000..13568251 --- /dev/null +++ b/aiogram/dispatcher/flags/getter.py @@ -0,0 +1,35 @@ +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]: + 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: + flags = extract_flags(handler) + return flags.get(name, default) + + +def check_flags(handler: Union["HandlerObject", Dict[str, Any]], magic: MagicFilter) -> Any: + flags = extract_flags(handler) + return magic.resolve(AttrDict(flags)) diff --git a/aiogram/dispatcher/webhook/aiohttp_server.py b/aiogram/dispatcher/webhook/aiohttp_server.py index 105e0b38..a8d084f8 100644 --- a/aiogram/dispatcher/webhook/aiohttp_server.py +++ b/aiogram/dispatcher/webhook/aiohttp_server.py @@ -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) diff --git a/aiogram/methods/copy_message.py b/aiogram/methods/copy_message.py index 3c90baee..9a5cddae 100644 --- a/aiogram/methods/copy_message.py +++ b/aiogram/methods/copy_message.py @@ -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 `_. 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 diff --git a/aiogram/methods/forward_message.py b/aiogram/methods/forward_message.py index 6ea5b233..ec4e7623 100644 --- a/aiogram/methods/forward_message.py +++ b/aiogram/methods/forward_message.py @@ -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 `_. 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() diff --git a/aiogram/methods/send_animation.py b/aiogram/methods/send_animation.py index 1f0971f6..97b483a4 100644 --- a/aiogram/methods/send_animation.py +++ b/aiogram/methods/send_animation.py @@ -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 `_. 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 diff --git a/aiogram/methods/send_audio.py b/aiogram/methods/send_audio.py index 8b61e75b..16b24723 100644 --- a/aiogram/methods/send_audio.py +++ b/aiogram/methods/send_audio.py @@ -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://' if the thumbnail was uploaded using multipart/form-data under . :ref:`More info on Sending Files ยป `""" disable_notification: Optional[bool] = None """Sends the message `silently `_. 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 diff --git a/aiogram/methods/send_contact.py b/aiogram/methods/send_contact.py index 182654d2..d0cb4ccb 100644 --- a/aiogram/methods/send_contact.py +++ b/aiogram/methods/send_contact.py @@ -36,6 +36,8 @@ class SendContact(TelegramMethod[Message]): """Additional data about the contact in the form of a `vCard `_, 0-2048 bytes""" disable_notification: Optional[bool] = None """Sends the message `silently `_. 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 diff --git a/aiogram/methods/send_dice.py b/aiogram/methods/send_dice.py index be3d567c..dd844534 100644 --- a/aiogram/methods/send_dice.py +++ b/aiogram/methods/send_dice.py @@ -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 `_. 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 diff --git a/aiogram/methods/send_document.py b/aiogram/methods/send_document.py index 9fff7d5e..17f38dfb 100644 --- a/aiogram/methods/send_document.py +++ b/aiogram/methods/send_document.py @@ -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 `_. 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 diff --git a/aiogram/methods/send_game.py b/aiogram/methods/send_game.py index d4957707..ba430845 100644 --- a/aiogram/methods/send_game.py +++ b/aiogram/methods/send_game.py @@ -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 `_.""" disable_notification: Optional[bool] = None """Sends the message `silently `_. 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 diff --git a/aiogram/methods/send_invoice.py b/aiogram/methods/send_invoice.py index 88a1bc5a..95c61ef6 100644 --- a/aiogram/methods/send_invoice.py +++ b/aiogram/methods/send_invoice.py @@ -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 `_. 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 diff --git a/aiogram/methods/send_location.py b/aiogram/methods/send_location.py index 31385884..225f23eb 100644 --- a/aiogram/methods/send_location.py +++ b/aiogram/methods/send_location.py @@ -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 `_. 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 diff --git a/aiogram/methods/send_media_group.py b/aiogram/methods/send_media_group.py index 6a35e934..7d5b4502 100644 --- a/aiogram/methods/send_media_group.py +++ b/aiogram/methods/send_media_group.py @@ -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 `_. 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 diff --git a/aiogram/methods/send_message.py b/aiogram/methods/send_message.py index bd7bfee0..d0269317 100644 --- a/aiogram/methods/send_message.py +++ b/aiogram/methods/send_message.py @@ -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 `_. 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 diff --git a/aiogram/methods/send_photo.py b/aiogram/methods/send_photo.py index 82eb06ac..faf9353f 100644 --- a/aiogram/methods/send_photo.py +++ b/aiogram/methods/send_photo.py @@ -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 `_. 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 diff --git a/aiogram/methods/send_poll.py b/aiogram/methods/send_poll.py index 11e0eec2..85455b0d 100644 --- a/aiogram/methods/send_poll.py +++ b/aiogram/methods/send_poll.py @@ -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 `_. 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 diff --git a/aiogram/methods/send_sticker.py b/aiogram/methods/send_sticker.py index c4435e77..abbe3773 100644 --- a/aiogram/methods/send_sticker.py +++ b/aiogram/methods/send_sticker.py @@ -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 ยป `""" disable_notification: Optional[bool] = None """Sends the message `silently `_. 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 diff --git a/aiogram/methods/send_venue.py b/aiogram/methods/send_venue.py index dc62b2d0..df708ff8 100644 --- a/aiogram/methods/send_venue.py +++ b/aiogram/methods/send_venue.py @@ -44,6 +44,8 @@ class SendVenue(TelegramMethod[Message]): """Google Places type of the venue. (See `supported types `_.)""" disable_notification: Optional[bool] = None """Sends the message `silently `_. 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 diff --git a/aiogram/methods/send_video.py b/aiogram/methods/send_video.py index 6867f37b..4ed09cf1 100644 --- a/aiogram/methods/send_video.py +++ b/aiogram/methods/send_video.py @@ -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 `_. 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 diff --git a/aiogram/methods/send_video_note.py b/aiogram/methods/send_video_note.py index 99e3651a..7431d582 100644 --- a/aiogram/methods/send_video_note.py +++ b/aiogram/methods/send_video_note.py @@ -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://' if the thumbnail was uploaded using multipart/form-data under . :ref:`More info on Sending Files ยป `""" disable_notification: Optional[bool] = None """Sends the message `silently `_. 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 diff --git a/aiogram/methods/send_voice.py b/aiogram/methods/send_voice.py index c991c12f..e753e9e1 100644 --- a/aiogram/methods/send_voice.py +++ b/aiogram/methods/send_voice.py @@ -41,6 +41,8 @@ class SendVoice(TelegramMethod[Message]): """Duration of the voice message in seconds""" disable_notification: Optional[bool] = None """Sends the message `silently `_. 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 diff --git a/aiogram/types/bot_command.py b/aiogram/types/bot_command.py index 844abffb..1bf65eba 100644 --- a/aiogram/types/bot_command.py +++ b/aiogram/types/bot_command.py @@ -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.""" diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 81ea168a..24435025 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -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 diff --git a/aiogram/types/message_entity.py b/aiogram/types/message_entity.py index ddaac506..28226286 100644 --- a/aiogram/types/message_entity.py +++ b/aiogram/types/message_entity.py @@ -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 `_)""" + """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 `_)""" offset: int """Offset in UTF-16 code units to the start of the entity""" length: int diff --git a/aiogram/utils/chat_action.py b/aiogram/utils/chat_action.py new file mode 100644 index 00000000..d9ab1ee3 --- /dev/null +++ b/aiogram/utils/chat_action.py @@ -0,0 +1,298 @@ +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: + def __init__( + self, + *, + chat_id: Union[str, int], + action: str = "typing", + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INTERVAL, + bot: Optional[Bot] = None, + ) -> None: + 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._close_event = Event() + self._running = False + self._lock = Lock() + + 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 asyncio.sleep(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) + with suppress(asyncio.TimeoutError): + await asyncio.wait_for(self._close_event.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._running = False + + async def _run(self) -> None: + async with self._lock: + if self._running: + raise RuntimeError("Already running") + asyncio.create_task(self._worker()) + + def _stop(self) -> None: + if not self._close_event.is_set(): + self._close_event.set() + + 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: + self._stop() + + @classmethod + def typing( + cls, + bot: Bot, + chat_id: Union[int, str], + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + return cls( + bot=bot, + chat_id=chat_id, + action="typing", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def upload_photo( + cls, + bot: Bot, + chat_id: Union[int, str], + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + return cls( + bot=bot, + chat_id=chat_id, + action="upload_photo", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def record_video( + cls, + bot: Bot, + chat_id: Union[int, str], + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + return cls( + bot=bot, + chat_id=chat_id, + action="record_video", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def upload_video( + cls, + bot: Bot, + chat_id: Union[int, str], + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + return cls( + bot=bot, + chat_id=chat_id, + action="upload_video", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def record_voice( + cls, + bot: Bot, + chat_id: Union[int, str], + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + return cls( + bot=bot, + chat_id=chat_id, + action="record_voice", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def upload_voice( + cls, + bot: Bot, + chat_id: Union[int, str], + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + return cls( + bot=bot, + chat_id=chat_id, + action="upload_voice", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def upload_document( + cls, + bot: Bot, + chat_id: Union[int, str], + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + return cls( + bot=bot, + chat_id=chat_id, + action="upload_document", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def choose_sticker( + cls, + bot: Bot, + chat_id: Union[int, str], + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + return cls( + bot=bot, + chat_id=chat_id, + action="choose_sticker", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def find_location( + cls, + bot: Bot, + chat_id: Union[int, str], + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + return cls( + bot=bot, + chat_id=chat_id, + action="find_location", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def record_video_note( + cls, + bot: Bot, + chat_id: Union[int, str], + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + return cls( + bot=bot, + chat_id=chat_id, + action="record_video_note", + interval=interval, + initial_sleep=initial_sleep, + ) + + @classmethod + def upload_video_note( + cls, + bot: Bot, + chat_id: Union[int, str], + interval: float = DEFAULT_INTERVAL, + initial_sleep: float = DEFAULT_INITIAL_SLEEP, + ) -> "ChatActionSender": + return cls( + bot=bot, + chat_id=chat_id, + action="upload_video_note", + interval=interval, + initial_sleep=initial_sleep, + ) + + +class ChatActionMiddleware(BaseMiddleware): + 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") + 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) diff --git a/aiogram/utils/i18n/middleware.py b/aiogram/utils/i18n/middleware.py index 2f6a73c2..dabbfae8 100644 --- a/aiogram/utils/i18n/middleware.py +++ b/aiogram/utils/i18n/middleware.py @@ -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) diff --git a/poetry.lock b/poetry.lock index 3ed1df7c..8abb8e68 100644 --- a/poetry.lock +++ b/poetry.lock @@ -212,7 +212,7 @@ name = "certifi" version = "2021.10.8" description = "Python package for providing Mozilla's CA Bundle." category = "main" -optional = true +optional = false python-versions = "*" [[package]] @@ -917,7 +917,7 @@ pytest = ">=3.5" [[package]] name = "python-socks" -version = "2.0.0" +version = "2.0.2" description = "Core proxy (SOCKS4, SOCKS5, HTTP tunneling) functionality for Python" category = "main" optional = true @@ -974,6 +974,36 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +[[package]] +name = "sentry-sdk" +version = "1.5.3" +description = "Python client for Sentry (https://sentry.io)" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +certifi = "*" +urllib3 = ">=1.10.0" + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +beam = ["apache-beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +chalice = ["chalice (>=1.16.0)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +flask = ["flask (>=0.11)", "blinker (>=1.1)"] +httpx = ["httpx (>=0.16.0)"] +pure_eval = ["pure-eval", "executing", "asttokens"] +pyspark = ["pyspark (>=2.4.4)"] +quart = ["quart (>=0.16.1)", "blinker (>=1.1)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +tornado = ["tornado (>=5)"] + [[package]] name = "six" version = "1.16.0" @@ -1242,7 +1272,7 @@ name = "urllib3" version = "1.26.7" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" -optional = true +optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] @@ -1324,7 +1354,7 @@ redis = ["aioredis"] [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "27d602728b6dab256d184fbd44953030676edf320652ad828c6e1b4beaa80f8b" +content-hash = "678dbb9bbf362c01757842a977f7b0f0f65638a657e3599b6125f0f4e42f4b6c" [metadata.files] aiofiles = [ @@ -2003,8 +2033,8 @@ pytest-mypy = [ {file = "pytest_mypy-0.8.1-py3-none-any.whl", hash = "sha256:6e68e8eb7ceeb7d1c83a1590912f784879f037b51adfb9c17b95c6b2fc57466b"}, ] python-socks = [ - {file = "python-socks-2.0.0.tar.gz", hash = "sha256:7944dad882846ac73e5f79e180c841e3895ee058e16855b7e8fff24f4cd0b90b"}, - {file = "python_socks-2.0.0-py3-none-any.whl", hash = "sha256:aac65671cbd3b0eb55b20f8558c8de3894a315536aaab3ec0a7b9d46ff89c1bf"}, + {file = "python-socks-2.0.2.tar.gz", hash = "sha256:aa9b7a53e81ae6b6e3ada602761012e470ea1c4cbcd5548f99b3fc102dce4fca"}, + {file = "python_socks-2.0.2-py3-none-any.whl", hash = "sha256:faa46857c79a8bf7def2e904ac839fb56755d7ab76c4cad12a131a85fec07241"}, ] pytz = [ {file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"}, @@ -2100,6 +2130,10 @@ requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] +sentry-sdk = [ + {file = "sentry-sdk-1.5.3.tar.gz", hash = "sha256:141da032f0fa4c56f9af6b361fda57360af1789576285bd1944561f9c274f9c0"}, + {file = "sentry_sdk-1.5.3-py2.py3-none-any.whl", hash = "sha256:9aeff2a47f4038460296b920bf4d269284e8454e1c67547ee002ccafd9c2442b"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, diff --git a/pyproject.toml b/pyproject.toml index 297f02a4..b42260eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,6 +84,7 @@ toml = "^0.10.2" pre-commit = "^2.15.0" packaging = "^20.3" typing-extensions = "^3.7.4" +sentry-sdk = "^1.5.3" [tool.poetry.extras] diff --git a/tests/test_dispatcher/test_fsm/storage/test_redis.py b/tests/test_dispatcher/test_fsm/storage/test_redis.py index dcb71c3d..8914dc94 100644 --- a/tests/test_dispatcher/test_fsm/storage/test_redis.py +++ b/tests/test_dispatcher/test_fsm/storage/test_redis.py @@ -1,5 +1,3 @@ -from typing import Literal - import pytest from aiogram.dispatcher.fsm.storage.base import DEFAULT_DESTINY, StorageKey