From 8bb8fe7ad7d0dfdb395da3d89ff0ffa793d1c61b Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 17 Apr 2022 00:05:29 +0300 Subject: [PATCH 1/9] Base implementation --- aiogram/client/bot.py | 121 +++++- aiogram/dispatcher/dispatcher.py | 12 +- aiogram/dispatcher/webhook/aiohttp_server.py | 1 + aiogram/methods/__init__.py | 10 + aiogram/methods/answer_web_app_query.py | 31 ++ aiogram/methods/create_new_sticker_set.py | 2 +- aiogram/methods/get_chat_menu_button.py | 27 ++ .../get_my_default_administrator_rights.py | 27 ++ aiogram/methods/promote_chat_member.py | 4 +- aiogram/methods/set_chat_menu_button.py | 29 ++ .../set_my_default_administrator_rights.py | 29 ++ aiogram/types/__init__.py | 24 ++ aiogram/types/chat_administrator_rights.py | 39 ++ aiogram/types/chat_member.py | 4 +- aiogram/types/chat_member_administrator.py | 4 +- aiogram/types/inline_keyboard_button.py | 7 +- aiogram/types/keyboard_button.py | 12 +- aiogram/types/menu_button.py | 22 + aiogram/types/menu_button_commands.py | 21 + aiogram/types/menu_button_default.py | 21 + aiogram/types/menu_button_web_app.py | 25 ++ aiogram/types/message.py | 51 ++- aiogram/types/sent_web_app_message.py | 19 + aiogram/types/sticker.py | 2 + aiogram/types/sticker_set.py | 4 +- aiogram/types/video_chat_ended.py | 19 + .../types/video_chat_participants_invited.py | 19 + aiogram/types/video_chat_scheduled.py | 19 + aiogram/types/video_chat_started.py | 16 + aiogram/types/web_app_data.py | 21 + aiogram/types/web_app_info.py | 19 + aiogram/types/web_app_init_data.py | 19 + aiogram/types/web_app_user.py | 26 ++ aiogram/types/webhook_info.py | 2 + aiogram/utils/web_app.py | 79 ++++ docs/api/methods/answer_web_app_query.rst | 51 +++ docs/api/methods/get_chat_menu_button.rst | 44 ++ .../get_my_default_administrator_rights.rst | 44 ++ docs/api/methods/index.rst | 5 + docs/api/methods/set_chat_menu_button.rst | 51 +++ .../set_my_default_administrator_rights.rst | 51 +++ docs/api/types/chat_administrator_rights.rst | 9 + docs/api/types/index.rst | 16 +- docs/api/types/menu_button.rst | 9 + docs/api/types/menu_button_commands.rst | 9 + docs/api/types/menu_button_default.rst | 9 + docs/api/types/menu_button_web_app.rst | 9 + docs/api/types/sent_web_app_message.rst | 9 + docs/api/types/video_chat_ended.rst | 9 + .../types/video_chat_participants_invited.rst | 9 + docs/api/types/video_chat_scheduled.rst | 9 + docs/api/types/video_chat_started.rst | 9 + docs/api/types/web_app_data.rst | 9 + docs/api/types/web_app_info.rst | 9 + examples/web_app/demo.html | 376 ++++++++++++++++++ examples/web_app/handlers.py | 48 +++ examples/web_app/main.py | 49 +++ examples/web_app/routes.py | 64 +++ .../test_methods/test_answer_web_app_query.py | 34 ++ .../test_approve_chat_join_request.py | 15 +- .../test_methods/test_ban_chat_sender_chat.py | 15 +- .../test_decline_chat_join_request.py | 17 +- .../test_methods/test_get_chat_menu_button.py | 28 ++ ...est_get_my_default_administrator_rights.py | 28 ++ .../test_methods/test_set_chat_menu_button.py | 27 ++ ...est_set_my_default_administrator_rights.py | 27 ++ .../test_unban_chat_sender_chat.py | 15 +- tests/test_dispatcher/test_dispatcher.py | 4 +- 68 files changed, 1826 insertions(+), 78 deletions(-) create mode 100644 aiogram/methods/answer_web_app_query.py create mode 100644 aiogram/methods/get_chat_menu_button.py create mode 100644 aiogram/methods/get_my_default_administrator_rights.py create mode 100644 aiogram/methods/set_chat_menu_button.py create mode 100644 aiogram/methods/set_my_default_administrator_rights.py create mode 100644 aiogram/types/chat_administrator_rights.py create mode 100644 aiogram/types/menu_button.py create mode 100644 aiogram/types/menu_button_commands.py create mode 100644 aiogram/types/menu_button_default.py create mode 100644 aiogram/types/menu_button_web_app.py create mode 100644 aiogram/types/sent_web_app_message.py create mode 100644 aiogram/types/video_chat_ended.py create mode 100644 aiogram/types/video_chat_participants_invited.py create mode 100644 aiogram/types/video_chat_scheduled.py create mode 100644 aiogram/types/video_chat_started.py create mode 100644 aiogram/types/web_app_data.py create mode 100644 aiogram/types/web_app_info.py create mode 100644 aiogram/types/web_app_init_data.py create mode 100644 aiogram/types/web_app_user.py create mode 100644 aiogram/utils/web_app.py create mode 100644 docs/api/methods/answer_web_app_query.rst create mode 100644 docs/api/methods/get_chat_menu_button.rst create mode 100644 docs/api/methods/get_my_default_administrator_rights.rst create mode 100644 docs/api/methods/set_chat_menu_button.rst create mode 100644 docs/api/methods/set_my_default_administrator_rights.rst create mode 100644 docs/api/types/chat_administrator_rights.rst create mode 100644 docs/api/types/menu_button.rst create mode 100644 docs/api/types/menu_button_commands.rst create mode 100644 docs/api/types/menu_button_default.rst create mode 100644 docs/api/types/menu_button_web_app.rst create mode 100644 docs/api/types/sent_web_app_message.rst create mode 100644 docs/api/types/video_chat_ended.rst create mode 100644 docs/api/types/video_chat_participants_invited.rst create mode 100644 docs/api/types/video_chat_scheduled.rst create mode 100644 docs/api/types/video_chat_started.rst create mode 100644 docs/api/types/web_app_data.rst create mode 100644 docs/api/types/web_app_info.rst create mode 100644 examples/web_app/demo.html create mode 100644 examples/web_app/handlers.py create mode 100644 examples/web_app/main.py create mode 100644 examples/web_app/routes.py create mode 100644 tests/test_api/test_methods/test_answer_web_app_query.py mode change 100644 => 100755 tests/test_api/test_methods/test_approve_chat_join_request.py mode change 100644 => 100755 tests/test_api/test_methods/test_ban_chat_sender_chat.py mode change 100644 => 100755 tests/test_api/test_methods/test_decline_chat_join_request.py create mode 100644 tests/test_api/test_methods/test_get_chat_menu_button.py create mode 100644 tests/test_api/test_methods/test_get_my_default_administrator_rights.py create mode 100644 tests/test_api/test_methods/test_set_chat_menu_button.py create mode 100644 tests/test_api/test_methods/test_set_my_default_administrator_rights.py mode change 100644 => 100755 tests/test_api/test_methods/test_unban_chat_sender_chat.py diff --git a/aiogram/client/bot.py b/aiogram/client/bot.py index 94feb7b8..17241039 100644 --- a/aiogram/client/bot.py +++ b/aiogram/client/bot.py @@ -27,6 +27,7 @@ from ..methods import ( AnswerInlineQuery, AnswerPreCheckoutQuery, AnswerShippingQuery, + AnswerWebAppQuery, ApproveChatJoinRequest, BanChatMember, BanChatSenderChat, @@ -54,10 +55,12 @@ from ..methods import ( GetChatMember, GetChatMemberCount, GetChatMembersCount, + GetChatMenuButton, GetFile, GetGameHighScores, GetMe, GetMyCommands, + GetMyDefaultAdministratorRights, GetStickerSet, GetUpdates, GetUserProfilePhotos, @@ -89,12 +92,14 @@ from ..methods import ( SendVoice, SetChatAdministratorCustomTitle, SetChatDescription, + SetChatMenuButton, SetChatPermissions, SetChatPhoto, SetChatStickerSet, SetChatTitle, SetGameScore, SetMyCommands, + SetMyDefaultAdministratorRights, SetPassportDataErrors, SetStickerPositionInSet, SetStickerSetThumb, @@ -113,6 +118,7 @@ from ..types import ( BotCommand, BotCommandScope, Chat, + ChatAdministratorRights, ChatInviteLink, ChatMemberAdministrator, ChatMemberBanned, @@ -135,6 +141,7 @@ from ..types import ( InputMediaVideo, LabeledPrice, MaskPosition, + MenuButton, Message, MessageEntity, MessageId, @@ -142,6 +149,7 @@ from ..types import ( Poll, ReplyKeyboardMarkup, ReplyKeyboardRemove, + SentWebAppMessage, ShippingOption, StickerSet, Update, @@ -1618,7 +1626,7 @@ class Bot(ContextInstanceMixin["Bot"]): can_post_messages: Optional[bool] = None, can_edit_messages: Optional[bool] = None, can_delete_messages: Optional[bool] = None, - can_manage_voice_chats: Optional[bool] = None, + can_manage_video_chats: Optional[bool] = None, can_restrict_members: Optional[bool] = None, can_promote_members: Optional[bool] = None, can_change_info: Optional[bool] = None, @@ -1638,7 +1646,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param can_post_messages: Pass :code:`True`, if the administrator can create channel posts, channels only :param can_edit_messages: Pass :code:`True`, if the administrator can edit messages of other users and can pin messages, channels only :param can_delete_messages: Pass :code:`True`, if the administrator can delete messages of other users - :param can_manage_voice_chats: Pass :code:`True`, if the administrator can manage voice chats + :param can_manage_video_chats: Pass :code:`True`, if the administrator can manage video chats :param can_restrict_members: Pass :code:`True`, if the administrator can restrict, ban or unban chat members :param can_promote_members: Pass :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 him) :param can_change_info: Pass :code:`True`, if the administrator can change chat title, photo and other settings @@ -1647,6 +1655,7 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ + # TODO: deprecate voice call = PromoteChatMember( chat_id=chat_id, user_id=user_id, @@ -1655,7 +1664,7 @@ class Bot(ContextInstanceMixin["Bot"]): can_post_messages=can_post_messages, can_edit_messages=can_edit_messages, can_delete_messages=can_delete_messages, - can_manage_voice_chats=can_manage_voice_chats, + can_manage_video_chats=can_manage_video_chats, can_restrict_members=can_restrict_members, can_promote_members=can_promote_members, can_change_info=can_change_info, @@ -2344,6 +2353,88 @@ class Bot(ContextInstanceMixin["Bot"]): ) return await self(call, request_timeout=request_timeout) + async def set_chat_menu_button( + self, + chat_id: Optional[int] = None, + menu_button: Optional[MenuButton] = None, + request_timeout: Optional[int] = None, + ) -> bool: + """ + Use this method to change the bot's menu button in a private chat, or the default menu button. Returns :code:`True` on success. + + Source: https://core.telegram.org/bots/api#setchatmenubutton + + :param chat_id: Unique identifier for the target private chat. If not specified, default bot's menu button will be changed + :param menu_button: A JSON-serialized object for the new bot's menu button. Defaults to :class:`aiogram.types.menu_button_default.MenuButtonDefault` + :param request_timeout: Request timeout + :return: Returns True on success. + """ + call = SetChatMenuButton( + chat_id=chat_id, + menu_button=menu_button, + ) + return await self(call, request_timeout=request_timeout) + + async def get_chat_menu_button( + self, + chat_id: Optional[int] = None, + request_timeout: Optional[int] = None, + ) -> MenuButton: + """ + Use this method to get the current value of the bot's menu button in a private chat, or the default menu button. Returns :class:`aiogram.types.menu_button.MenuButton` on success. + + Source: https://core.telegram.org/bots/api#getchatmenubutton + + :param chat_id: Unique identifier for the target private chat. If not specified, default bot's menu button will be returned + :param request_timeout: Request timeout + :return: Returns MenuButton on success. + """ + call = GetChatMenuButton( + chat_id=chat_id, + ) + return await self(call, request_timeout=request_timeout) + + async def set_my_default_administrator_rights( + self, + rights: Optional[ChatAdministratorRights] = None, + for_channels: Optional[bool] = None, + request_timeout: Optional[int] = None, + ) -> bool: + """ + Use this method to change the default administrator rights requested by the bot when it's added as an administrator to groups or channels. These rights will be suggested to users, but they are are free to modify the list before adding the bot. Returns :code:`True` on success. + + Source: https://core.telegram.org/bots/api#setmydefaultadministratorrights + + :param rights: A JSON-serialized object describing new default administrator rights. If not specified, the default administrator rights will be cleared. + :param for_channels: Pass :code:`True` to change the default administrator rights of the bot in channels. Otherwise, the default administrator rights of the bot for groups and supergroups will be changed. + :param request_timeout: Request timeout + :return: Returns True on success. + """ + call = SetMyDefaultAdministratorRights( + rights=rights, + for_channels=for_channels, + ) + return await self(call, request_timeout=request_timeout) + + async def get_my_default_administrator_rights( + self, + for_channels: Optional[bool] = None, + request_timeout: Optional[int] = None, + ) -> ChatAdministratorRights: + """ + Use this method to get the current default administrator rights of the bot. Returns :class:`aiogram.types.chat_administrator_rights.ChatAdministratorRights` on success. + + Source: https://core.telegram.org/bots/api#getmydefaultadministratorrights + + :param for_channels: Pass :code:`True` to get default administrator rights of the bot in channels. Otherwise, default administrator rights of the bot for groups and supergroups will be returned. + :param request_timeout: Request timeout + :return: Returns ChatAdministratorRights on success. + """ + call = GetMyDefaultAdministratorRights( + for_channels=for_channels, + ) + return await self(call, request_timeout=request_timeout) + # ============================================================================================= # Group: Updating messages # Source: https://core.telegram.org/bots/api#updating-messages @@ -2656,7 +2747,7 @@ class Bot(ContextInstanceMixin["Bot"]): Source: https://core.telegram.org/bots/api#createnewstickerset :param user_id: User identifier of created sticker set owner - :param name: Short name of sticker set, to be used in :code:`t.me/addstickers/` URLs (e.g., *animals*). Can contain only english letters, digits and underscores. Must begin with a letter, can't contain consecutive underscores and must end in *'_by_'*. ** is case insensitive. 1-64 characters. + :param name: Short name of sticker set, to be used in :code:`t.me/addstickers/` URLs (e.g., *animals*). Can contain only english letters, digits and underscores. Must begin with a letter, can't contain consecutive underscores and must end in :code:`"_by_"`. :code:`` is case insensitive. 1-64 characters. :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 » ` @@ -2827,6 +2918,28 @@ class Bot(ContextInstanceMixin["Bot"]): ) return await self(call, request_timeout=request_timeout) + async def answer_web_app_query( + self, + web_app_query_id: str, + result: InlineQueryResult, + request_timeout: Optional[int] = None, + ) -> SentWebAppMessage: + """ + Use this method to set the result of an interaction with a `Web App `_ and send a corresponding message on behalf of the user to the chat from which the query originated. On success, a :class:`aiogram.types.sent_web_app_message.SentWebAppMessage` object is returned. + + Source: https://core.telegram.org/bots/api#answerwebappquery + + :param web_app_query_id: Unique identifier for the query to be answered + :param result: A JSON-serialized object describing the message to be sent + :param request_timeout: Request timeout + :return: On success, a SentWebAppMessage object is returned. + """ + call = AnswerWebAppQuery( + web_app_query_id=web_app_query_id, + result=result, + ) + return await self(call, request_timeout=request_timeout) + # ============================================================================================= # Group: Payments # Source: https://core.telegram.org/bots/api#payments diff --git a/aiogram/dispatcher/dispatcher.py b/aiogram/dispatcher/dispatcher.py index 9b84262c..f18deabf 100644 --- a/aiogram/dispatcher/dispatcher.py +++ b/aiogram/dispatcher/dispatcher.py @@ -80,20 +80,20 @@ class Dispatcher(Router): self.update.outer_middleware(self.fsm) self.shutdown.register(self.fsm.close) - self._data: Dict[str, Any] = {} + self.workflow_data: Dict[str, Any] = {} self._running_lock = Lock() def __getitem__(self, item: str) -> Any: - return self._data[item] + return self.workflow_data[item] def __setitem__(self, key: str, value: Any) -> None: - self._data[key] = value + self.workflow_data[key] = value def __delitem__(self, key: str) -> None: - del self._data[key] + del self.workflow_data[key] def get(self, key: str, /, default: Optional[Any] = None) -> Optional[Any]: - return self._data.get(key, default) + return self.workflow_data.get(key, default) @property def storage(self) -> BaseStorage: @@ -136,7 +136,7 @@ class Dispatcher(Router): self.update.trigger, update, { - **self._data, + **self.workflow_data, **kwargs, "bot": bot, }, diff --git a/aiogram/dispatcher/webhook/aiohttp_server.py b/aiogram/dispatcher/webhook/aiohttp_server.py index a8d084f8..eb3b2c5b 100644 --- a/aiogram/dispatcher/webhook/aiohttp_server.py +++ b/aiogram/dispatcher/webhook/aiohttp_server.py @@ -26,6 +26,7 @@ def setup_application(app: Application, dispatcher: Dispatcher, /, **kwargs: Any "app": app, "dispatcher": dispatcher, **kwargs, + **dispatcher.workflow_data, } async def on_startup(*a: Any, **kw: Any) -> None: # pragma: no cover diff --git a/aiogram/methods/__init__.py b/aiogram/methods/__init__.py index 085044ae..f7b75066 100644 --- a/aiogram/methods/__init__.py +++ b/aiogram/methods/__init__.py @@ -3,6 +3,7 @@ from .answer_callback_query import AnswerCallbackQuery from .answer_inline_query import AnswerInlineQuery from .answer_pre_checkout_query import AnswerPreCheckoutQuery from .answer_shipping_query import AnswerShippingQuery +from .answer_web_app_query import AnswerWebAppQuery from .approve_chat_join_request import ApproveChatJoinRequest from .ban_chat_member import BanChatMember from .ban_chat_sender_chat import BanChatSenderChat @@ -31,10 +32,12 @@ from .get_chat_administrators import GetChatAdministrators from .get_chat_member import GetChatMember from .get_chat_member_count import GetChatMemberCount from .get_chat_members_count import GetChatMembersCount +from .get_chat_menu_button import GetChatMenuButton from .get_file import GetFile from .get_game_high_scores import GetGameHighScores from .get_me import GetMe from .get_my_commands import GetMyCommands +from .get_my_default_administrator_rights import GetMyDefaultAdministratorRights from .get_sticker_set import GetStickerSet from .get_updates import GetUpdates from .get_user_profile_photos import GetUserProfilePhotos @@ -66,12 +69,14 @@ from .send_video_note import SendVideoNote from .send_voice import SendVoice from .set_chat_administrator_custom_title import SetChatAdministratorCustomTitle from .set_chat_description import SetChatDescription +from .set_chat_menu_button import SetChatMenuButton from .set_chat_permissions import SetChatPermissions from .set_chat_photo import SetChatPhoto from .set_chat_sticker_set import SetChatStickerSet from .set_chat_title import SetChatTitle from .set_game_score import SetGameScore from .set_my_commands import SetMyCommands +from .set_my_default_administrator_rights import SetMyDefaultAdministratorRights from .set_passport_data_errors import SetPassportDataErrors from .set_sticker_position_in_set import SetStickerPositionInSet from .set_sticker_set_thumb import SetStickerSetThumb @@ -150,6 +155,10 @@ __all__ = ( "SetMyCommands", "DeleteMyCommands", "GetMyCommands", + "SetChatMenuButton", + "GetChatMenuButton", + "SetMyDefaultAdministratorRights", + "GetMyDefaultAdministratorRights", "EditMessageText", "EditMessageCaption", "EditMessageMedia", @@ -165,6 +174,7 @@ __all__ = ( "DeleteStickerFromSet", "SetStickerSetThumb", "AnswerInlineQuery", + "AnswerWebAppQuery", "SendInvoice", "AnswerShippingQuery", "AnswerPreCheckoutQuery", diff --git a/aiogram/methods/answer_web_app_query.py b/aiogram/methods/answer_web_app_query.py new file mode 100644 index 00000000..3211ed38 --- /dev/null +++ b/aiogram/methods/answer_web_app_query.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict + +from ..types import InlineQueryResult, SentWebAppMessage +from .base import Request, TelegramMethod, prepare_parse_mode + +if TYPE_CHECKING: + from ..client.bot import Bot + + +class AnswerWebAppQuery(TelegramMethod[SentWebAppMessage]): + """ + Use this method to set the result of an interaction with a `Web App `_ and send a corresponding message on behalf of the user to the chat from which the query originated. On success, a :class:`aiogram.types.sent_web_app_message.SentWebAppMessage` object is returned. + + Source: https://core.telegram.org/bots/api#answerwebappquery + """ + + __returning__ = SentWebAppMessage + + web_app_query_id: str + """Unique identifier for the query to be answered""" + result: InlineQueryResult + """A JSON-serialized object describing the message to be sent""" + + def build_request(self, bot: Bot) -> Request: + data: Dict[str, Any] = self.dict() + prepare_parse_mode( + bot, data["result"], parse_mode_property="parse_mode", entities_property="entities" + ) + return Request(method="answerWebAppQuery", data=data) diff --git a/aiogram/methods/create_new_sticker_set.py b/aiogram/methods/create_new_sticker_set.py index 5c807963..5faab9ef 100644 --- a/aiogram/methods/create_new_sticker_set.py +++ b/aiogram/methods/create_new_sticker_set.py @@ -21,7 +21,7 @@ class CreateNewStickerSet(TelegramMethod[bool]): user_id: int """User identifier of created sticker set owner""" name: str - """Short name of sticker set, to be used in :code:`t.me/addstickers/` URLs (e.g., *animals*). Can contain only english letters, digits and underscores. Must begin with a letter, can't contain consecutive underscores and must end in *'_by_'*. ** is case insensitive. 1-64 characters.""" + """Short name of sticker set, to be used in :code:`t.me/addstickers/` URLs (e.g., *animals*). Can contain only english letters, digits and underscores. Must begin with a letter, can't contain consecutive underscores and must end in :code:`"_by_"`. :code:`` is case insensitive. 1-64 characters.""" title: str """Sticker set title, 1-64 characters""" emojis: str diff --git a/aiogram/methods/get_chat_menu_button.py b/aiogram/methods/get_chat_menu_button.py new file mode 100644 index 00000000..e2a97134 --- /dev/null +++ b/aiogram/methods/get_chat_menu_button.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional, Union + +from ..types import MenuButton, MenuButtonCommands, MenuButtonDefault, MenuButtonWebApp +from .base import Request, TelegramMethod + +if TYPE_CHECKING: + from ..client.bot import Bot + + +class GetChatMenuButton(TelegramMethod[MenuButton]): + """ + Use this method to get the current value of the bot's menu button in a private chat, or the default menu button. Returns :class:`aiogram.types.menu_button.MenuButton` on success. + + Source: https://core.telegram.org/bots/api#getchatmenubutton + """ + + __returning__ = Union[MenuButtonDefault, MenuButtonWebApp, MenuButtonCommands] + + chat_id: Optional[int] = None + """Unique identifier for the target private chat. If not specified, default bot's menu button will be returned""" + + def build_request(self, bot: Bot) -> Request: + data: Dict[str, Any] = self.dict() + + return Request(method="getChatMenuButton", data=data) diff --git a/aiogram/methods/get_my_default_administrator_rights.py b/aiogram/methods/get_my_default_administrator_rights.py new file mode 100644 index 00000000..53a8e494 --- /dev/null +++ b/aiogram/methods/get_my_default_administrator_rights.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional + +from ..types import ChatAdministratorRights +from .base import Request, TelegramMethod + +if TYPE_CHECKING: + from ..client.bot import Bot + + +class GetMyDefaultAdministratorRights(TelegramMethod[ChatAdministratorRights]): + """ + Use this method to get the current default administrator rights of the bot. Returns :class:`aiogram.types.chat_administrator_rights.ChatAdministratorRights` on success. + + Source: https://core.telegram.org/bots/api#getmydefaultadministratorrights + """ + + __returning__ = ChatAdministratorRights + + for_channels: Optional[bool] = None + """Pass :code:`True` to get default administrator rights of the bot in channels. Otherwise, default administrator rights of the bot for groups and supergroups will be returned.""" + + def build_request(self, bot: Bot) -> Request: + data: Dict[str, Any] = self.dict() + + return Request(method="getMyDefaultAdministratorRights", data=data) diff --git a/aiogram/methods/promote_chat_member.py b/aiogram/methods/promote_chat_member.py index f2d8374f..f346f979 100644 --- a/aiogram/methods/promote_chat_member.py +++ b/aiogram/methods/promote_chat_member.py @@ -31,8 +31,8 @@ class PromoteChatMember(TelegramMethod[bool]): """Pass :code:`True`, if the administrator can edit messages of other users and can pin messages, channels only""" can_delete_messages: Optional[bool] = None """Pass :code:`True`, if the administrator can delete messages of other users""" - can_manage_voice_chats: Optional[bool] = None - """Pass :code:`True`, if the administrator can manage voice chats""" + can_manage_video_chats: Optional[bool] = None # TODO: deprecate voice + """Pass :code:`True`, if the administrator can manage video chats""" can_restrict_members: Optional[bool] = None """Pass :code:`True`, if the administrator can restrict, ban or unban chat members""" can_promote_members: Optional[bool] = None diff --git a/aiogram/methods/set_chat_menu_button.py b/aiogram/methods/set_chat_menu_button.py new file mode 100644 index 00000000..6578ec4e --- /dev/null +++ b/aiogram/methods/set_chat_menu_button.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional + +from ..types import MenuButton +from .base import Request, TelegramMethod + +if TYPE_CHECKING: + from ..client.bot import Bot + + +class SetChatMenuButton(TelegramMethod[bool]): + """ + Use this method to change the bot's menu button in a private chat, or the default menu button. Returns :code:`True` on success. + + Source: https://core.telegram.org/bots/api#setchatmenubutton + """ + + __returning__ = bool + + chat_id: Optional[int] = None + """Unique identifier for the target private chat. If not specified, default bot's menu button will be changed""" + menu_button: Optional[MenuButton] = None + """A JSON-serialized object for the new bot's menu button. Defaults to :class:`aiogram.types.menu_button_default.MenuButtonDefault`""" + + def build_request(self, bot: Bot) -> Request: + data: Dict[str, Any] = self.dict() + + return Request(method="setChatMenuButton", data=data) diff --git a/aiogram/methods/set_my_default_administrator_rights.py b/aiogram/methods/set_my_default_administrator_rights.py new file mode 100644 index 00000000..84341180 --- /dev/null +++ b/aiogram/methods/set_my_default_administrator_rights.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional + +from ..types import ChatAdministratorRights +from .base import Request, TelegramMethod + +if TYPE_CHECKING: + from ..client.bot import Bot + + +class SetMyDefaultAdministratorRights(TelegramMethod[bool]): + """ + Use this method to change the default administrator rights requested by the bot when it's added as an administrator to groups or channels. These rights will be suggested to users, but they are are free to modify the list before adding the bot. Returns :code:`True` on success. + + Source: https://core.telegram.org/bots/api#setmydefaultadministratorrights + """ + + __returning__ = bool + + rights: Optional[ChatAdministratorRights] = None + """A JSON-serialized object describing new default administrator rights. If not specified, the default administrator rights will be cleared.""" + for_channels: Optional[bool] = None + """Pass :code:`True` to change the default administrator rights of the bot in channels. Otherwise, the default administrator rights of the bot for groups and supergroups will be changed.""" + + def build_request(self, bot: Bot) -> Request: + data: Dict[str, Any] = self.dict() + + return Request(method="setMyDefaultAdministratorRights", data=data) diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index ebf4c839..ef41a85b 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -13,6 +13,7 @@ from .bot_command_scope_default import BotCommandScopeDefault from .callback_game import CallbackGame from .callback_query import CallbackQuery from .chat import Chat +from .chat_administrator_rights import ChatAdministratorRights from .chat_invite_link import ChatInviteLink from .chat_join_request import ChatJoinRequest from .chat_location import ChatLocation @@ -81,6 +82,10 @@ from .labeled_price import LabeledPrice from .location import Location from .login_url import LoginUrl from .mask_position import MaskPosition +from .menu_button import MenuButton +from .menu_button_commands import MenuButtonCommands +from .menu_button_default import MenuButtonDefault +from .menu_button_web_app import MenuButtonWebApp from .message import ContentType, Message from .message_auto_delete_timer_changed import MessageAutoDeleteTimerChanged from .message_entity import MessageEntity @@ -107,6 +112,7 @@ from .proximity_alert_triggered import ProximityAlertTriggered from .reply_keyboard_markup import ReplyKeyboardMarkup from .reply_keyboard_remove import ReplyKeyboardRemove from .response_parameters import ResponseParameters +from .sent_web_app_message import SentWebAppMessage from .shipping_address import ShippingAddress from .shipping_option import ShippingOption from .shipping_query import ShippingQuery @@ -118,12 +124,18 @@ from .user import User from .user_profile_photos import UserProfilePhotos from .venue import Venue from .video import Video +from .video_chat_ended import VideoChatEnded +from .video_chat_participants_invited import VideoChatParticipantsInvited +from .video_chat_scheduled import VideoChatScheduled +from .video_chat_started import VideoChatStarted from .video_note import VideoNote from .voice import Voice from .voice_chat_ended import VoiceChatEnded from .voice_chat_participants_invited import VoiceChatParticipantsInvited from .voice_chat_scheduled import VoiceChatScheduled from .voice_chat_started import VoiceChatStarted +from .web_app_data import WebAppData +from .web_app_info import WebAppInfo from .webhook_info import WebhookInfo __all__ = ( @@ -155,14 +167,20 @@ __all__ = ( "Poll", "Location", "Venue", + "WebAppData", "ProximityAlertTriggered", "MessageAutoDeleteTimerChanged", "VoiceChatScheduled", "VoiceChatStarted", "VoiceChatEnded", "VoiceChatParticipantsInvited", + "VideoChatScheduled", + "VideoChatStarted", + "VideoChatEnded", + "VideoChatParticipantsInvited", "UserProfilePhotos", "File", + "WebAppInfo", "ReplyKeyboardMarkup", "KeyboardButton", "KeyboardButtonPollType", @@ -174,6 +192,7 @@ __all__ = ( "ForceReply", "ChatPhoto", "ChatInviteLink", + "ChatAdministratorRights", "ChatMember", "ChatMemberOwner", "ChatMemberAdministrator", @@ -194,6 +213,10 @@ __all__ = ( "BotCommandScopeChat", "BotCommandScopeChatAdministrators", "BotCommandScopeChatMember", + "MenuButton", + "MenuButtonCommands", + "MenuButtonWebApp", + "MenuButtonDefault", "ResponseParameters", "InputMedia", "InputMediaPhoto", @@ -234,6 +257,7 @@ __all__ = ( "InputContactMessageContent", "InputInvoiceMessageContent", "ChosenInlineResult", + "SentWebAppMessage", "LabeledPrice", "Invoice", "ShippingAddress", diff --git a/aiogram/types/chat_administrator_rights.py b/aiogram/types/chat_administrator_rights.py new file mode 100644 index 00000000..20f4b65c --- /dev/null +++ b/aiogram/types/chat_administrator_rights.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +from .base import TelegramObject + +if TYPE_CHECKING: + pass + + +class ChatAdministratorRights(TelegramObject): + """ + Represents the rights of an administrator in a chat. + + Source: https://core.telegram.org/bots/api#chatadministratorrights + """ + + is_anonymous: bool + """:code:`True`, if the user's presence in the chat is hidden""" + can_manage_chat: bool + """: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: bool + """:code:`True`, if the administrator can delete messages of other users""" + can_manage_video_chats: bool + """:code:`True`, if the administrator can manage video chats""" + can_restrict_members: bool + """:code:`True`, if the administrator can restrict, ban or unban chat members""" + can_promote_members: bool + """: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: bool + """:code:`True`, if the user is allowed to change the chat title, photo and other settings""" + can_invite_users: bool + """: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""" diff --git a/aiogram/types/chat_member.py b/aiogram/types/chat_member.py index b3d1419c..d430e0ce 100644 --- a/aiogram/types/chat_member.py +++ b/aiogram/types/chat_member.py @@ -37,8 +37,8 @@ class ChatMember(TelegramObject): """*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_manage_video_chats: Optional[bool] = None + """*Optional*. :code:`True`, if the administrator can manage video 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 diff --git a/aiogram/types/chat_member_administrator.py b/aiogram/types/chat_member_administrator.py index a27156f9..896033d2 100644 --- a/aiogram/types/chat_member_administrator.py +++ b/aiogram/types/chat_member_administrator.py @@ -29,8 +29,8 @@ class ChatMemberAdministrator(ChatMember): """: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: bool """:code:`True`, if the administrator can delete messages of other users""" - can_manage_voice_chats: bool - """:code:`True`, if the administrator can manage voice chats""" + can_manage_video_chats: bool + """:code:`True`, if the administrator can manage video chats""" can_restrict_members: bool """:code:`True`, if the administrator can restrict, ban or unban chat members""" can_promote_members: bool diff --git a/aiogram/types/inline_keyboard_button.py b/aiogram/types/inline_keyboard_button.py index b661339a..aeb546f1 100644 --- a/aiogram/types/inline_keyboard_button.py +++ b/aiogram/types/inline_keyboard_button.py @@ -7,6 +7,7 @@ from .base import MutableTelegramObject if TYPE_CHECKING: from .callback_game import CallbackGame from .login_url import LoginUrl + from .web_app_info import WebAppInfo class InlineKeyboardButton(MutableTelegramObject): @@ -20,10 +21,12 @@ class InlineKeyboardButton(MutableTelegramObject): """Label text on the button""" url: Optional[str] = None """*Optional*. HTTP or tg:// url to be opened when the button is pressed. Links :code:`tg://user?id=` can be used to mention a user by their ID without using a username, if this is allowed by their privacy settings.""" - login_url: Optional[LoginUrl] = None - """*Optional*. An HTTP URL used to automatically authorize the user. Can be used as a replacement for the `Telegram Login Widget `_.""" callback_data: Optional[str] = None """*Optional*. Data to be sent in a `callback query `_ to the bot when button is pressed, 1-64 bytes""" + web_app: Optional[WebAppInfo] = None + """*Optional*. Description of the `Web App `_ that will be launched when the user presses the button. The Web App will be able to send an arbitrary message on behalf of the user using the method :class:`aiogram.methods.answer_web_app_query.AnswerWebAppQuery`. Available only in private chats between a user and the bot.""" + login_url: Optional[LoginUrl] = None + """*Optional*. An HTTP URL used to automatically authorize the user. Can be used as a replacement for the `Telegram Login Widget `_.""" switch_inline_query: Optional[str] = None """*Optional*. If set, pressing the button will prompt the user to select one of their chats, open that chat and insert the bot's username and the specified inline query in the input field. Can be empty, in which case just the bot's username will be inserted.""" switch_inline_query_current_chat: Optional[str] = None diff --git a/aiogram/types/keyboard_button.py b/aiogram/types/keyboard_button.py index c4eed8d1..bf8b0258 100644 --- a/aiogram/types/keyboard_button.py +++ b/aiogram/types/keyboard_button.py @@ -6,6 +6,7 @@ from .base import MutableTelegramObject if TYPE_CHECKING: from .keyboard_button_poll_type import KeyboardButtonPollType + from .web_app_info import WebAppInfo class WebApp(MutableTelegramObject): @@ -19,15 +20,18 @@ class KeyboardButton(MutableTelegramObject): **Note:** *request_poll* option will only work in Telegram versions released after 23 January, 2020. Older clients will display *unsupported message*. + **Note:** *web_app* option will only work in Telegram versions released after 16 April, 2022. Older clients will display *unsupported message*. + Source: https://core.telegram.org/bots/api#keyboardbutton """ text: str """Text of the button. If none of the optional fields are used, it will be sent as a message when the button is pressed""" request_contact: Optional[bool] = None - """*Optional*. If :code:`True`, the user's phone number will be sent as a contact when the button is pressed. Available in private chats only""" + """*Optional*. If :code:`True`, the user's phone number will be sent as a contact when the button is pressed. Available in private chats only.""" request_location: Optional[bool] = None - """*Optional*. If :code:`True`, the user's current location will be sent when the button is pressed. Available in private chats only""" + """*Optional*. If :code:`True`, the user's current location will be sent when the button is pressed. Available in private chats only.""" request_poll: Optional[KeyboardButtonPollType] = None - """*Optional*. If specified, the user will be asked to create a poll and send it to the bot when the button is pressed. Available in private chats only""" - web_app: Optional[WebApp] = None + """*Optional*. If specified, the user will be asked to create a poll and send it to the bot when the button is pressed. Available in private chats only.""" + web_app: Optional[WebAppInfo] = None + """*Optional*. If specified, the described `Web App `_ will be launched when the button is pressed. The Web App will be able to send a 'web_app_data' service message. Available in private chats only.""" diff --git a/aiogram/types/menu_button.py b/aiogram/types/menu_button.py new file mode 100644 index 00000000..0a709dd6 --- /dev/null +++ b/aiogram/types/menu_button.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .base import TelegramObject + +if TYPE_CHECKING: + pass + + +class MenuButton(TelegramObject): + """ + This object describes the bot's menu button in a private chat. It should be one of + + - :class:`aiogram.types.menu_button_commands.MenuButtonCommands` + - :class:`aiogram.types.menu_button_web_app.MenuButtonWebApp` + - :class:`aiogram.types.menu_button_default.MenuButtonDefault` + + If a menu button other than :class:`aiogram.types.menu_button_default.MenuButtonDefault` is set for a private chat, then it is applied in the chat. Otherwise the default menu button is applied. By default, the menu button opens the list of bot commands. + + Source: https://core.telegram.org/bots/api#menubutton + """ diff --git a/aiogram/types/menu_button_commands.py b/aiogram/types/menu_button_commands.py new file mode 100644 index 00000000..18452b70 --- /dev/null +++ b/aiogram/types/menu_button_commands.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pydantic import Field + +from . import MenuButton + +if TYPE_CHECKING: + pass + + +class MenuButtonCommands(MenuButton): + """ + Represents a menu button, which opens the bot's list of commands. + + Source: https://core.telegram.org/bots/api#menubuttoncommands + """ + + type: str = Field("commands", const=True) + """Type of the button, must be *commands*""" diff --git a/aiogram/types/menu_button_default.py b/aiogram/types/menu_button_default.py new file mode 100644 index 00000000..f4b60ea6 --- /dev/null +++ b/aiogram/types/menu_button_default.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pydantic import Field + +from . import MenuButton + +if TYPE_CHECKING: + pass + + +class MenuButtonDefault(MenuButton): + """ + Describes that no specific value for the menu button was set. + + Source: https://core.telegram.org/bots/api#menubuttondefault + """ + + type: str = Field("default", const=True) + """Type of the button, must be *default*""" diff --git a/aiogram/types/menu_button_web_app.py b/aiogram/types/menu_button_web_app.py new file mode 100644 index 00000000..0de45b94 --- /dev/null +++ b/aiogram/types/menu_button_web_app.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from pydantic import Field + +from . import MenuButton + +if TYPE_CHECKING: + from .web_app_info import WebAppInfo + + +class MenuButtonWebApp(MenuButton): + """ + Represents a menu button, which launches a `Web App `_. + + Source: https://core.telegram.org/bots/api#menubuttonwebapp + """ + + type: str = Field("web_app", const=True) + """Type of the button, must be *web_app*""" + text: str + """Text on the button""" + web_app: WebAppInfo + """Description of the Web App that will be launched when the user presses the button. The Web App will be able to send an arbitrary message on behalf of the user using the method :class:`aiogram.methods.answer_web_app_query.AnswerWebAppQuery`.""" diff --git a/aiogram/types/message.py b/aiogram/types/message.py index d1ab7cbd..a70feb64 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -63,12 +63,13 @@ if TYPE_CHECKING: from .user import User from .venue import Venue from .video import Video + from .video_chat_ended import VideoChatEnded + from .video_chat_participants_invited import VideoChatParticipantsInvited + from .video_chat_scheduled import VideoChatScheduled + from .video_chat_started import VideoChatStarted from .video_note import VideoNote from .voice import Voice - from .voice_chat_ended import VoiceChatEnded - from .voice_chat_participants_invited import VoiceChatParticipantsInvited - from .voice_chat_scheduled import VoiceChatScheduled - from .voice_chat_started import VoiceChatStarted + from .web_app_data import WebAppData class _BaseMessage(TelegramObject): @@ -184,14 +185,16 @@ class _BaseMessage(TelegramObject): """*Optional*. Telegram Passport data""" proximity_alert_triggered: Optional[ProximityAlertTriggered] = None """*Optional*. Service message. A user in the chat triggered another user's proximity alert while sharing Live Location.""" - voice_chat_scheduled: Optional[VoiceChatScheduled] = None - """*Optional*. Service message: voice chat scheduled""" - voice_chat_started: Optional[VoiceChatStarted] = None - """*Optional*. Service message: voice chat started""" - voice_chat_ended: Optional[VoiceChatEnded] = None - """*Optional*. Service message: voice chat ended""" - voice_chat_participants_invited: Optional[VoiceChatParticipantsInvited] = None - """*Optional*. Service message: new participants invited to a voice chat""" + video_chat_scheduled: Optional[VideoChatScheduled] = None + """*Optional*. Service message: video chat scheduled""" + video_chat_started: Optional[VideoChatStarted] = None + """*Optional*. Service message: video chat started""" + video_chat_ended: Optional[VideoChatEnded] = None + """*Optional*. Service message: video chat ended""" + video_chat_participants_invited: Optional[VideoChatParticipantsInvited] = None + """*Optional*. Service message: new participants invited to a video chat""" + web_app_data: Optional[WebAppData] = None + """*Optional*. Service message: data sent by a Web App""" reply_markup: Optional[InlineKeyboardMarkup] = None """*Optional*. Inline keyboard attached to the message. :code:`login_url` buttons are represented as ordinary :code:`url` buttons.""" @@ -257,12 +260,16 @@ class Message(_BaseMessage): return ContentType.DICE if self.message_auto_delete_timer_changed: return ContentType.MESSAGE_AUTO_DELETE_TIMER_CHANGED - if self.voice_chat_started: - return ContentType.VOICE_CHAT_STARTED - if self.voice_chat_ended: - return ContentType.VOICE_CHAT_ENDED - if self.voice_chat_participants_invited: - return ContentType.VOICE_CHAT_PARTICIPANTS_INVITED + if self.video_chat_scheduled: + return ContentType.VIDEO_CHAT_SCHEDULED + if self.video_chat_started: + return ContentType.VIDEO_CHAT_STARTED + if self.video_chat_ended: + return ContentType.VIDEO_CHAT_ENDED + if self.video_chat_participants_invited: + return ContentType.VIDEO_CHAT_PARTICIPANTS_INVITED + if self.web_app_data: + return ContentType.WEB_APP_DATA return ContentType.UNKNOWN @@ -1899,9 +1906,11 @@ class ContentType(helper.Helper): POLL = helper.Item() # poll DICE = helper.Item() # dice MESSAGE_AUTO_DELETE_TIMER_CHANGED = helper.Item() # message_auto_delete_timer_changed - VOICE_CHAT_STARTED = helper.Item() # voice_chat_started - VOICE_CHAT_ENDED = helper.Item() # voice_chat_ended - VOICE_CHAT_PARTICIPANTS_INVITED = helper.Item() # voice_chat_participants_invited + VIDEO_CHAT_SCHEDULED = helper.Item() # video_chat_scheduled + VIDEO_CHAT_STARTED = helper.Item() # video_chat_started + VIDEO_CHAT_ENDED = helper.Item() # video_chat_ended + VIDEO_CHAT_PARTICIPANTS_INVITED = helper.Item() # video_chat_participants_invited + WEB_APP_DATA = helper.Item() # web_app_data UNKNOWN = helper.Item() # unknown ANY = helper.Item() # any diff --git a/aiogram/types/sent_web_app_message.py b/aiogram/types/sent_web_app_message.py new file mode 100644 index 00000000..dca1e5b7 --- /dev/null +++ b/aiogram/types/sent_web_app_message.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +from .base import TelegramObject + +if TYPE_CHECKING: + pass + + +class SentWebAppMessage(TelegramObject): + """ + Contains information about an inline message sent by a `Web App `_ on behalf of a user. + + Source: https://core.telegram.org/bots/api#sentwebappmessage + """ + + inline_message_id: Optional[str] = None + """*Optional*. Identifier of the sent inline message. Available only if there is an `inline keyboard `_ attached to the message.""" diff --git a/aiogram/types/sticker.py b/aiogram/types/sticker.py index 2379d547..979d7e37 100644 --- a/aiogram/types/sticker.py +++ b/aiogram/types/sticker.py @@ -26,6 +26,8 @@ class Sticker(TelegramObject): """Sticker height""" is_animated: bool """:code:`True`, if the sticker is `animated `_""" + is_video: bool + """:code:`True`, if the sticker is a `video sticker `_""" thumb: Optional[PhotoSize] = None """*Optional*. Sticker thumbnail in the .WEBP or .JPG format""" emoji: Optional[str] = None diff --git a/aiogram/types/sticker_set.py b/aiogram/types/sticker_set.py index d26d206d..3ed5055c 100644 --- a/aiogram/types/sticker_set.py +++ b/aiogram/types/sticker_set.py @@ -22,9 +22,11 @@ class StickerSet(TelegramObject): """Sticker set title""" is_animated: bool """:code:`True`, if the sticker set contains `animated stickers `_""" + is_video: bool + """:code:`True`, if the sticker set contains `video stickers `_""" contains_masks: bool """:code:`True`, if the sticker set contains masks""" stickers: List[Sticker] """List of all set stickers""" thumb: Optional[PhotoSize] = None - """*Optional*. Sticker set thumbnail in the .WEBP or .TGS format""" + """*Optional*. Sticker set thumbnail in the .WEBP, .TGS, or .WEBM format""" diff --git a/aiogram/types/video_chat_ended.py b/aiogram/types/video_chat_ended.py new file mode 100644 index 00000000..cb85a931 --- /dev/null +++ b/aiogram/types/video_chat_ended.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .base import TelegramObject + +if TYPE_CHECKING: + pass + + +class VideoChatEnded(TelegramObject): + """ + This object represents a service message about a video chat ended in the chat. + + Source: https://core.telegram.org/bots/api#videochatended + """ + + duration: int + """Video chat duration in seconds""" diff --git a/aiogram/types/video_chat_participants_invited.py b/aiogram/types/video_chat_participants_invited.py new file mode 100644 index 00000000..3361f8ee --- /dev/null +++ b/aiogram/types/video_chat_participants_invited.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, List + +from .base import TelegramObject + +if TYPE_CHECKING: + from .user import User + + +class VideoChatParticipantsInvited(TelegramObject): + """ + This object represents a service message about new members invited to a video chat. + + Source: https://core.telegram.org/bots/api#videochatparticipantsinvited + """ + + users: List[User] + """New members that were invited to the video chat""" diff --git a/aiogram/types/video_chat_scheduled.py b/aiogram/types/video_chat_scheduled.py new file mode 100644 index 00000000..1e41c6d4 --- /dev/null +++ b/aiogram/types/video_chat_scheduled.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .base import TelegramObject + +if TYPE_CHECKING: + pass + + +class VideoChatScheduled(TelegramObject): + """ + This object represents a service message about a video chat scheduled in the chat. + + Source: https://core.telegram.org/bots/api#videochatscheduled + """ + + start_date: int + """Point in time (Unix timestamp) when the video chat is supposed to be started by a chat administrator""" diff --git a/aiogram/types/video_chat_started.py b/aiogram/types/video_chat_started.py new file mode 100644 index 00000000..595e889e --- /dev/null +++ b/aiogram/types/video_chat_started.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .base import TelegramObject + +if TYPE_CHECKING: + pass + + +class VideoChatStarted(TelegramObject): + """ + This object represents a service message about a video chat started in the chat. Currently holds no information. + + Source: https://core.telegram.org/bots/api#videochatstarted + """ diff --git a/aiogram/types/web_app_data.py b/aiogram/types/web_app_data.py new file mode 100644 index 00000000..5a4b5b0a --- /dev/null +++ b/aiogram/types/web_app_data.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .base import TelegramObject + +if TYPE_CHECKING: + pass + + +class WebAppData(TelegramObject): + """ + Contains data sent from a `Web App `_ to the bot. + + Source: https://core.telegram.org/bots/api#webappdata + """ + + data: str + """The data. Be aware that a bad client can send arbitrary data in this field.""" + button_text: str + """Text of the *web_app* keyboard button, from which the Web App was opened. Be aware that a bad client can send arbitrary data in this field.""" diff --git a/aiogram/types/web_app_info.py b/aiogram/types/web_app_info.py new file mode 100644 index 00000000..f5d228f2 --- /dev/null +++ b/aiogram/types/web_app_info.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .base import TelegramObject + +if TYPE_CHECKING: + pass + + +class WebAppInfo(TelegramObject): + """ + Contains information about a `Web App `_. + + Source: https://core.telegram.org/bots/api#webappinfo + """ + + url: str + """An HTTPS URL of a Web App to be opened with additional data as specified in `Initializing Web Apps `_""" diff --git a/aiogram/types/web_app_init_data.py b/aiogram/types/web_app_init_data.py new file mode 100644 index 00000000..e27b12af --- /dev/null +++ b/aiogram/types/web_app_init_data.py @@ -0,0 +1,19 @@ +from typing import Optional + +from aiogram.types import TelegramObject +from aiogram.types.web_app_user import WebAppUser + + +class WebAppInitData(TelegramObject): + query_id: Optional[str] = None + """Optional. A unique identifier for the Web App session, required for sending messages via the answerWebAppQuery method.""" + user: Optional[WebAppUser] = None + """Optional. An object containing data about the current user.""" + receiver: Optional[WebAppUser] = None + """Optional. An object containing data about the chat partner of the current user in the chat where the bot was launched via the attachment menu. Returned only for Web Apps launched via the attachment menu.""" + start_param: Optional[str] = None + """Optional. The value of the startattach parameter, passed via link. Only returned for Web Apps when launched from the attachment menu via link. The value of the start_param parameter will also be passed in the GET-parameter tgWebAppStartParam, so the Web App can load the correct interface right away.""" + auth_date: int + """Unix time when the form was opened.""" + hash: str + """A hash of all passed parameters, which the bot server can use to check their validity.""" diff --git a/aiogram/types/web_app_user.py b/aiogram/types/web_app_user.py new file mode 100644 index 00000000..e5436adf --- /dev/null +++ b/aiogram/types/web_app_user.py @@ -0,0 +1,26 @@ +from typing import Optional + +from aiogram.types import TelegramObject + + +class WebAppUser(TelegramObject): + """ + This object contains the data of the Web App user. + + Source: https://core.telegram.org/bots/webapps#webappuser + """ + + id: int + """A unique identifier for the user or bot. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. It has at most 52 significant bits, so a 64-bit integer or a double-precision float type is safe for storing this identifier.""" + is_bot: Optional[bool] = None + """Optional. True, if this user is a bot. Returns in the receiver field only.""" + first_name: str + """First name of the user or bot.""" + last_name: Optional[str] = None + """Optional. Last name of the user or bot.""" + username: Optional[str] = None + """Optional. Username of the user or bot.""" + language_code: Optional[str] = None + """Optional. IETF language tag of the user's language. Returns in user field only.""" + photo_url: Optional[str] = None + """Optional. URL of the user’s profile photo. The photo can be in .jpeg or .svg formats. Only returned for Web Apps launched from the attachment menu.""" diff --git a/aiogram/types/webhook_info.py b/aiogram/types/webhook_info.py index 3b1a64a0..7738588d 100644 --- a/aiogram/types/webhook_info.py +++ b/aiogram/types/webhook_info.py @@ -25,6 +25,8 @@ class WebhookInfo(TelegramObject): """*Optional*. Unix time for the most recent error that happened when trying to deliver an update via webhook""" last_error_message: Optional[str] = None """*Optional*. Error message in human-readable format for the most recent error that happened when trying to deliver an update via webhook""" + last_synchronization_error_date: Optional[int] = None + """*Optional*. Unix time of the most recent error that happened when trying to synchronize available updates with Telegram datacenters""" max_connections: Optional[int] = None """*Optional*. Maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery""" allowed_updates: Optional[List[str]] = None diff --git a/aiogram/utils/web_app.py b/aiogram/utils/web_app.py new file mode 100644 index 00000000..bb26830e --- /dev/null +++ b/aiogram/utils/web_app.py @@ -0,0 +1,79 @@ +import hashlib +import hmac +import json +from operator import itemgetter +from typing import Any, Callable +from urllib.parse import parse_qsl + +from aiogram.types.web_app_init_data import WebAppInitData + + +def check_webapp_signature(token: str, init_data: str) -> bool: + """ + Check incoming WebApp init data signature + + Source: https://core.telegram.org/bots/webapps#validating-data-received-via-the-web-app + + :param token: + :param init_data: + :return: + """ + try: + parsed_data = dict(parse_qsl(init_data)) + except ValueError: + # Init data is not a valid query string + return False + if "hash" not in parsed_data: + # Hash is not present in init data + return False + hash_ = parsed_data.pop("hash") + + data_check_string = "\n".join( + f"{k}={v}" for k, v in sorted(parsed_data.items(), key=itemgetter(0)) + ) + secret_key = hmac.new(key=b"WebAppData", msg=token.encode(), digestmod=hashlib.sha256) + calculated_hash = hmac.new( + key=secret_key.digest(), msg=data_check_string.encode(), digestmod=hashlib.sha256 + ).hexdigest() + return calculated_hash == hash_ + + +def parse_init_data( + init_data: str, + *, + _loads: Callable[..., Any] = json.loads, +) -> WebAppInitData: + """ + Parse WebApp init data and return it as dict + + :param init_data: + :param _loads: + :return: + """ + result = {} + for key, value in parse_qsl(init_data): + if (value.startswith("[") and value.endswith("]")) or ( + value.startswith("{") and value.endswith("}") + ): + value = _loads(value) + result[key] = value + return WebAppInitData(**result) + + +def safe_parse_webapp_init_data( + token: str, + init_data: str, + *, + _loads: Callable[..., Any] = json.loads, +) -> WebAppInitData: + """ + Validate WebApp init data and return it as dict + + :param token: + :param init_data: + :param _loads: + :return: + """ + if check_webapp_signature(token, init_data): + return parse_init_data(init_data, _loads=_loads) + raise ValueError("Invalid init data signature") diff --git a/docs/api/methods/answer_web_app_query.rst b/docs/api/methods/answer_web_app_query.rst new file mode 100644 index 00000000..a608083f --- /dev/null +++ b/docs/api/methods/answer_web_app_query.rst @@ -0,0 +1,51 @@ +################# +answerWebAppQuery +################# + +Returns: :obj:`SentWebAppMessage` + +.. automodule:: aiogram.methods.answer_web_app_query + :members: + :member-order: bysource + :undoc-members: True + + +Usage +===== + +As bot method +------------- + +.. code-block:: + + result: SentWebAppMessage = await bot.answer_web_app_query(...) + + +Method as object +---------------- + +Imports: + +- :code:`from aiogram.methods.answer_web_app_query import AnswerWebAppQuery` +- alias: :code:`from aiogram.methods import AnswerWebAppQuery` + +In handlers with current bot +---------------------------- + +.. code-block:: python + + result: SentWebAppMessage = await AnswerWebAppQuery(...) + +With specific bot +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + result: SentWebAppMessage = await bot(AnswerWebAppQuery(...)) + +As reply into Webhook in handler +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + return AnswerWebAppQuery(...) diff --git a/docs/api/methods/get_chat_menu_button.rst b/docs/api/methods/get_chat_menu_button.rst new file mode 100644 index 00000000..8e3df76a --- /dev/null +++ b/docs/api/methods/get_chat_menu_button.rst @@ -0,0 +1,44 @@ +################# +getChatMenuButton +################# + +Returns: :obj:`MenuButton` + +.. automodule:: aiogram.methods.get_chat_menu_button + :members: + :member-order: bysource + :undoc-members: True + + +Usage +===== + +As bot method +------------- + +.. code-block:: + + result: MenuButton = await bot.get_chat_menu_button(...) + + +Method as object +---------------- + +Imports: + +- :code:`from aiogram.methods.get_chat_menu_button import GetChatMenuButton` +- alias: :code:`from aiogram.methods import GetChatMenuButton` + +In handlers with current bot +---------------------------- + +.. code-block:: python + + result: MenuButton = await GetChatMenuButton(...) + +With specific bot +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + result: MenuButton = await bot(GetChatMenuButton(...)) diff --git a/docs/api/methods/get_my_default_administrator_rights.rst b/docs/api/methods/get_my_default_administrator_rights.rst new file mode 100644 index 00000000..d73c54f8 --- /dev/null +++ b/docs/api/methods/get_my_default_administrator_rights.rst @@ -0,0 +1,44 @@ +############################### +getMyDefaultAdministratorRights +############################### + +Returns: :obj:`ChatAdministratorRights` + +.. automodule:: aiogram.methods.get_my_default_administrator_rights + :members: + :member-order: bysource + :undoc-members: True + + +Usage +===== + +As bot method +------------- + +.. code-block:: + + result: ChatAdministratorRights = await bot.get_my_default_administrator_rights(...) + + +Method as object +---------------- + +Imports: + +- :code:`from aiogram.methods.get_my_default_administrator_rights import GetMyDefaultAdministratorRights` +- alias: :code:`from aiogram.methods import GetMyDefaultAdministratorRights` + +In handlers with current bot +---------------------------- + +.. code-block:: python + + result: ChatAdministratorRights = await GetMyDefaultAdministratorRights(...) + +With specific bot +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + result: ChatAdministratorRights = await bot(GetMyDefaultAdministratorRights(...)) diff --git a/docs/api/methods/index.rst b/docs/api/methods/index.rst index 1a4143d0..6c778282 100644 --- a/docs/api/methods/index.rst +++ b/docs/api/methods/index.rst @@ -82,6 +82,10 @@ Available methods set_my_commands delete_my_commands get_my_commands + set_chat_menu_button + get_chat_menu_button + set_my_default_administrator_rights + get_my_default_administrator_rights Updating messages ================= @@ -118,6 +122,7 @@ Inline mode :maxdepth: 1 answer_inline_query + answer_web_app_query Payments ======== diff --git a/docs/api/methods/set_chat_menu_button.rst b/docs/api/methods/set_chat_menu_button.rst new file mode 100644 index 00000000..6a60fae9 --- /dev/null +++ b/docs/api/methods/set_chat_menu_button.rst @@ -0,0 +1,51 @@ +################# +setChatMenuButton +################# + +Returns: :obj:`bool` + +.. automodule:: aiogram.methods.set_chat_menu_button + :members: + :member-order: bysource + :undoc-members: True + + +Usage +===== + +As bot method +------------- + +.. code-block:: + + result: bool = await bot.set_chat_menu_button(...) + + +Method as object +---------------- + +Imports: + +- :code:`from aiogram.methods.set_chat_menu_button import SetChatMenuButton` +- alias: :code:`from aiogram.methods import SetChatMenuButton` + +In handlers with current bot +---------------------------- + +.. code-block:: python + + result: bool = await SetChatMenuButton(...) + +With specific bot +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + result: bool = await bot(SetChatMenuButton(...)) + +As reply into Webhook in handler +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + return SetChatMenuButton(...) diff --git a/docs/api/methods/set_my_default_administrator_rights.rst b/docs/api/methods/set_my_default_administrator_rights.rst new file mode 100644 index 00000000..c115568c --- /dev/null +++ b/docs/api/methods/set_my_default_administrator_rights.rst @@ -0,0 +1,51 @@ +############################### +setMyDefaultAdministratorRights +############################### + +Returns: :obj:`bool` + +.. automodule:: aiogram.methods.set_my_default_administrator_rights + :members: + :member-order: bysource + :undoc-members: True + + +Usage +===== + +As bot method +------------- + +.. code-block:: + + result: bool = await bot.set_my_default_administrator_rights(...) + + +Method as object +---------------- + +Imports: + +- :code:`from aiogram.methods.set_my_default_administrator_rights import SetMyDefaultAdministratorRights` +- alias: :code:`from aiogram.methods import SetMyDefaultAdministratorRights` + +In handlers with current bot +---------------------------- + +.. code-block:: python + + result: bool = await SetMyDefaultAdministratorRights(...) + +With specific bot +~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + result: bool = await bot(SetMyDefaultAdministratorRights(...)) + +As reply into Webhook in handler +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + return SetMyDefaultAdministratorRights(...) diff --git a/docs/api/types/chat_administrator_rights.rst b/docs/api/types/chat_administrator_rights.rst new file mode 100644 index 00000000..ef86eede --- /dev/null +++ b/docs/api/types/chat_administrator_rights.rst @@ -0,0 +1,9 @@ +####################### +ChatAdministratorRights +####################### + + +.. automodule:: aiogram.types.chat_administrator_rights + :members: + :member-order: bysource + :undoc-members: True diff --git a/docs/api/types/index.rst b/docs/api/types/index.rst index f9b1bfc2..abcfe205 100644 --- a/docs/api/types/index.rst +++ b/docs/api/types/index.rst @@ -40,14 +40,16 @@ Available types poll location venue + web_app_data proximity_alert_triggered message_auto_delete_timer_changed - voice_chat_scheduled - voice_chat_started - voice_chat_ended - voice_chat_participants_invited + video_chat_scheduled + video_chat_started + video_chat_ended + video_chat_participants_invited user_profile_photos file + web_app_info reply_keyboard_markup keyboard_button keyboard_button_poll_type @@ -59,6 +61,7 @@ Available types force_reply chat_photo chat_invite_link + chat_administrator_rights chat_member chat_member_owner chat_member_administrator @@ -79,6 +82,10 @@ Available types bot_command_scope_chat bot_command_scope_chat_administrators bot_command_scope_chat_member + menu_button + menu_button_commands + menu_button_web_app + menu_button_default response_parameters input_media input_media_photo @@ -135,6 +142,7 @@ Inline mode input_contact_message_content input_invoice_message_content chosen_inline_result + sent_web_app_message Payments ======== diff --git a/docs/api/types/menu_button.rst b/docs/api/types/menu_button.rst new file mode 100644 index 00000000..44eeb4c3 --- /dev/null +++ b/docs/api/types/menu_button.rst @@ -0,0 +1,9 @@ +########## +MenuButton +########## + + +.. automodule:: aiogram.types.menu_button + :members: + :member-order: bysource + :undoc-members: True diff --git a/docs/api/types/menu_button_commands.rst b/docs/api/types/menu_button_commands.rst new file mode 100644 index 00000000..66614b04 --- /dev/null +++ b/docs/api/types/menu_button_commands.rst @@ -0,0 +1,9 @@ +################## +MenuButtonCommands +################## + + +.. automodule:: aiogram.types.menu_button_commands + :members: + :member-order: bysource + :undoc-members: True diff --git a/docs/api/types/menu_button_default.rst b/docs/api/types/menu_button_default.rst new file mode 100644 index 00000000..f114387c --- /dev/null +++ b/docs/api/types/menu_button_default.rst @@ -0,0 +1,9 @@ +################# +MenuButtonDefault +################# + + +.. automodule:: aiogram.types.menu_button_default + :members: + :member-order: bysource + :undoc-members: True diff --git a/docs/api/types/menu_button_web_app.rst b/docs/api/types/menu_button_web_app.rst new file mode 100644 index 00000000..bf5c0806 --- /dev/null +++ b/docs/api/types/menu_button_web_app.rst @@ -0,0 +1,9 @@ +################ +MenuButtonWebApp +################ + + +.. automodule:: aiogram.types.menu_button_web_app + :members: + :member-order: bysource + :undoc-members: True diff --git a/docs/api/types/sent_web_app_message.rst b/docs/api/types/sent_web_app_message.rst new file mode 100644 index 00000000..1a7d2084 --- /dev/null +++ b/docs/api/types/sent_web_app_message.rst @@ -0,0 +1,9 @@ +################# +SentWebAppMessage +################# + + +.. automodule:: aiogram.types.sent_web_app_message + :members: + :member-order: bysource + :undoc-members: True diff --git a/docs/api/types/video_chat_ended.rst b/docs/api/types/video_chat_ended.rst new file mode 100644 index 00000000..aed8b7a9 --- /dev/null +++ b/docs/api/types/video_chat_ended.rst @@ -0,0 +1,9 @@ +############## +VideoChatEnded +############## + + +.. automodule:: aiogram.types.video_chat_ended + :members: + :member-order: bysource + :undoc-members: True diff --git a/docs/api/types/video_chat_participants_invited.rst b/docs/api/types/video_chat_participants_invited.rst new file mode 100644 index 00000000..9ca905bd --- /dev/null +++ b/docs/api/types/video_chat_participants_invited.rst @@ -0,0 +1,9 @@ +############################ +VideoChatParticipantsInvited +############################ + + +.. automodule:: aiogram.types.video_chat_participants_invited + :members: + :member-order: bysource + :undoc-members: True diff --git a/docs/api/types/video_chat_scheduled.rst b/docs/api/types/video_chat_scheduled.rst new file mode 100644 index 00000000..0d5f8c45 --- /dev/null +++ b/docs/api/types/video_chat_scheduled.rst @@ -0,0 +1,9 @@ +################## +VideoChatScheduled +################## + + +.. automodule:: aiogram.types.video_chat_scheduled + :members: + :member-order: bysource + :undoc-members: True diff --git a/docs/api/types/video_chat_started.rst b/docs/api/types/video_chat_started.rst new file mode 100644 index 00000000..5d59a22e --- /dev/null +++ b/docs/api/types/video_chat_started.rst @@ -0,0 +1,9 @@ +################ +VideoChatStarted +################ + + +.. automodule:: aiogram.types.video_chat_started + :members: + :member-order: bysource + :undoc-members: True diff --git a/docs/api/types/web_app_data.rst b/docs/api/types/web_app_data.rst new file mode 100644 index 00000000..1a94573f --- /dev/null +++ b/docs/api/types/web_app_data.rst @@ -0,0 +1,9 @@ +########## +WebAppData +########## + + +.. automodule:: aiogram.types.web_app_data + :members: + :member-order: bysource + :undoc-members: True diff --git a/docs/api/types/web_app_info.rst b/docs/api/types/web_app_info.rst new file mode 100644 index 00000000..b21f0aea --- /dev/null +++ b/docs/api/types/web_app_info.rst @@ -0,0 +1,9 @@ +########## +WebAppInfo +########## + + +.. automodule:: aiogram.types.web_app_info + :members: + :member-order: bysource + :undoc-members: True diff --git a/examples/web_app/demo.html b/examples/web_app/demo.html new file mode 100644 index 00000000..40726974 --- /dev/null +++ b/examples/web_app/demo.html @@ -0,0 +1,376 @@ + + + + + + + + + + + + + + + +
+ + + + + +

Test links:

+ +

Test permissions:

+ +
+
+ Data passed to webview. + +
+
+
+ Theme params +
+
+
+
+ + + + + diff --git a/examples/web_app/handlers.py b/examples/web_app/handlers.py new file mode 100644 index 00000000..745d6645 --- /dev/null +++ b/examples/web_app/handlers.py @@ -0,0 +1,48 @@ +from aiogram import Bot, F, Router +from aiogram.dispatcher.filters import Command +from aiogram.types import ( + InlineKeyboardButton, + InlineKeyboardMarkup, + MenuButtonWebApp, + Message, + WebAppInfo, +) + +my_router = Router() + + +@my_router.message(Command(commands=["start"])) +async def command_start(message: Message, bot: Bot, base_url: str): + await bot.set_chat_menu_button( + chat_id=message.chat.id, + menu_button=MenuButtonWebApp(text="Open Menu", web_app=WebAppInfo(url=f"{base_url}/demo")), + ) + await message.answer("""Hi!\nSend me any type of message to start.\nOr just send /webview""") + + +@my_router.message(Command(commands=["webview"])) +async def command_webview(message: Message, base_url: str): + await message.answer( + "Good. Now you can try to send it via Webview", + reply_markup=InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Open Webview", web_app=WebAppInfo(url=f"{base_url}/demo") + ) + ] + ] + ), + ) + + +@my_router.message(~F.message.via_bot) # Echo to all messages except messages via bot +async def echo_all(message: Message, base_url: str): + await message.answer( + "Test webview", + reply_markup=InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Open", web_app=WebAppInfo(url=f"{base_url}/demo"))] + ] + ), + ) diff --git a/examples/web_app/main.py b/examples/web_app/main.py new file mode 100644 index 00000000..9f5260ad --- /dev/null +++ b/examples/web_app/main.py @@ -0,0 +1,49 @@ +import logging +from os import getenv + +from aiohttp.web import run_app +from aiohttp.web_app import Application +from handlers import my_router +from routes import check_data_handler, demo_handler, send_message_handler + +from aiogram import Bot, Dispatcher +from aiogram.dispatcher.webhook.aiohttp_server import SimpleRequestHandler, setup_application +from aiogram.types import MenuButtonWebApp, WebAppInfo + +TELEGRAM_TOKEN = getenv("TELEGRAM_TOKEN") +APP_BASE_URL = getenv("APP_BASE_URL") + + +async def on_startup(bot: Bot, base_url: str): + await bot.set_webhook(f"{base_url}/webhook") + await bot.set_chat_menu_button( + menu_button=MenuButtonWebApp(text="Open Menu", web_app=WebAppInfo(url=f"{base_url}/demo")) + ) + + +def main(): + bot = Bot(token=TELEGRAM_TOKEN, parse_mode="HTML") + dispatcher = Dispatcher() + dispatcher["base_url"] = APP_BASE_URL + dispatcher.startup.register(on_startup) + + dispatcher.include_router(my_router) + + app = Application() + app["bot"] = bot + + app.router.add_get("/demo", demo_handler) + app.router.add_post("/demo/checkData", check_data_handler) + app.router.add_post("/demo/sendMessage", send_message_handler) + SimpleRequestHandler( + dispatcher=dispatcher, + bot=bot, + ).register(app, path="/webhook") + setup_application(app, dispatcher, bot=bot) + + run_app(app, host="127.0.0.1", port=8081) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + main() diff --git a/examples/web_app/routes.py b/examples/web_app/routes.py new file mode 100644 index 00000000..d8c6b697 --- /dev/null +++ b/examples/web_app/routes.py @@ -0,0 +1,64 @@ +from pathlib import Path + +from aiohttp.web_fileresponse import FileResponse +from aiohttp.web_request import Request +from aiohttp.web_response import json_response + +from aiogram import Bot +from aiogram.types import ( + InlineKeyboardButton, + InlineKeyboardMarkup, + InlineQueryResultArticle, + InputTextMessageContent, + WebAppInfo, +) +from aiogram.utils.web_app import check_webapp_signature, safe_parse_webapp_init_data + + +async def demo_handler(request: Request): + return FileResponse(Path(__file__).parent.resolve() / "demo.html") + + +async def check_data_handler(request: Request): + bot: Bot = request.app["bot"] + + data = await request.post() + if check_webapp_signature(bot.token, data["_auth"]): + return json_response({"ok": True}) + return json_response({"ok": False, "err": "Unauthorized"}, status=401) + + +async def send_message_handler(request: Request): + bot: Bot = request.app["bot"] + data = await request.post() + try: + web_app_init_data = safe_parse_webapp_init_data(token=bot.token, init_data=data["_auth"]) + except ValueError: + return json_response({"ok": False, "err": "Unauthorized"}, status=401) + + print(data) + reply_markup = None + if data["with_webview"] == "1": + reply_markup = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Open", + web_app=WebAppInfo(url=str(request.url.with_scheme("https"))), + ) + ] + ] + ) + await bot.answer_web_app_query( + web_app_query_id=web_app_init_data.query_id, + result=InlineQueryResultArticle( + id=web_app_init_data.query_id, + title="Demo", + input_message_content=InputTextMessageContent( + message_text="Hello, World!", + parse_mode=None, + ), + reply_markup=reply_markup, + ), + ) + return json_response({"ok": True}) diff --git a/tests/test_api/test_methods/test_answer_web_app_query.py b/tests/test_api/test_methods/test_answer_web_app_query.py new file mode 100644 index 00000000..c8865be2 --- /dev/null +++ b/tests/test_api/test_methods/test_answer_web_app_query.py @@ -0,0 +1,34 @@ +import pytest + +from aiogram.methods import AnswerWebAppQuery, Request +from aiogram.types import SentWebAppMessage +from tests.mocked_bot import MockedBot + + +@pytest.mark.skip +class TestAnswerWebAppQuery: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(AnswerWebAppQuery, ok=True, result=None) + + response: SentWebAppMessage = await AnswerWebAppQuery( + web_app_query_id=..., + result=..., + ) + request: Request = bot.get_request() + assert request.method == "answerWebAppQuery" + # assert request.data == {} + assert response == prepare_result.result + + @pytest.mark.asyncio + async def test_bot_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(AnswerWebAppQuery, ok=True, result=None) + + response: SentWebAppMessage = await bot.answer_web_app_query( + web_app_query_id=..., + result=..., + ) + request: Request = bot.get_request() + assert request.method == "answerWebAppQuery" + # assert request.data == {} + assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_approve_chat_join_request.py b/tests/test_api/test_methods/test_approve_chat_join_request.py old mode 100644 new mode 100755 index 52d6554a..1b9fa8f6 --- a/tests/test_api/test_methods/test_approve_chat_join_request.py +++ b/tests/test_api/test_methods/test_approve_chat_join_request.py @@ -1,20 +1,22 @@ import pytest -from aiogram.methods import ApproveChatJoinRequest, Request +from aiogram.api.methods import ApproveChatJoinRequest, Request from tests.mocked_bot import MockedBot +@pytest.mark.skip class TestApproveChatJoinRequest: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(ApproveChatJoinRequest, ok=True, result=True) + prepare_result = bot.add_result_for(ApproveChatJoinRequest, ok=True, result=None) response: bool = await ApproveChatJoinRequest( - chat_id=-42, - user_id=42, + chat_id=..., + user_id=..., ) request: Request = bot.get_request() assert request.method == "approveChatJoinRequest" + # assert request.data == {} assert response == prepare_result.result @pytest.mark.asyncio @@ -22,9 +24,10 @@ class TestApproveChatJoinRequest: prepare_result = bot.add_result_for(ApproveChatJoinRequest, ok=True, result=None) response: bool = await bot.approve_chat_join_request( - chat_id=-42, - user_id=42, + chat_id=..., + user_id=..., ) request: Request = bot.get_request() assert request.method == "approveChatJoinRequest" + # assert request.data == {} assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_ban_chat_sender_chat.py b/tests/test_api/test_methods/test_ban_chat_sender_chat.py old mode 100644 new mode 100755 index 1d832e87..7df5ae7c --- a/tests/test_api/test_methods/test_ban_chat_sender_chat.py +++ b/tests/test_api/test_methods/test_ban_chat_sender_chat.py @@ -1,17 +1,18 @@ import pytest -from aiogram.methods import BanChatSenderChat, Request +from aiogram.api.methods import BanChatSenderChat, Request from tests.mocked_bot import MockedBot +@pytest.mark.skip class TestBanChatSenderChat: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(BanChatSenderChat, ok=True, result=True) + prepare_result = bot.add_result_for(BanChatSenderChat, ok=True, result=None) response: bool = await BanChatSenderChat( - chat_id=-42, - sender_chat_id=-1337, + chat_id=..., + sender_chat_id=..., ) request: Request = bot.get_request() assert request.method == "banChatSenderChat" @@ -20,11 +21,11 @@ class TestBanChatSenderChat: @pytest.mark.asyncio async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(BanChatSenderChat, ok=True, result=True) + prepare_result = bot.add_result_for(BanChatSenderChat, ok=True, result=None) response: bool = await bot.ban_chat_sender_chat( - chat_id=-42, - sender_chat_id=-1337, + chat_id=..., + sender_chat_id=..., ) request: Request = bot.get_request() assert request.method == "banChatSenderChat" diff --git a/tests/test_api/test_methods/test_decline_chat_join_request.py b/tests/test_api/test_methods/test_decline_chat_join_request.py old mode 100644 new mode 100755 index e6e60ca5..adb9b80c --- a/tests/test_api/test_methods/test_decline_chat_join_request.py +++ b/tests/test_api/test_methods/test_decline_chat_join_request.py @@ -1,30 +1,33 @@ import pytest -from aiogram.methods import DeclineChatJoinRequest, Request +from aiogram.api.methods import DeclineChatJoinRequest, Request from tests.mocked_bot import MockedBot +@pytest.mark.skip class TestDeclineChatJoinRequest: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(DeclineChatJoinRequest, ok=True, result=True) + prepare_result = bot.add_result_for(DeclineChatJoinRequest, ok=True, result=None) response: bool = await DeclineChatJoinRequest( - chat_id=-42, - user_id=42, + chat_id=..., + user_id=..., ) request: Request = bot.get_request() assert request.method == "declineChatJoinRequest" + # assert request.data == {} assert response == prepare_result.result @pytest.mark.asyncio async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(DeclineChatJoinRequest, ok=True, result=True) + prepare_result = bot.add_result_for(DeclineChatJoinRequest, ok=True, result=None) response: bool = await bot.decline_chat_join_request( - chat_id=-42, - user_id=42, + chat_id=..., + user_id=..., ) request: Request = bot.get_request() assert request.method == "declineChatJoinRequest" + # assert request.data == {} assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_get_chat_menu_button.py b/tests/test_api/test_methods/test_get_chat_menu_button.py new file mode 100644 index 00000000..2b7803f4 --- /dev/null +++ b/tests/test_api/test_methods/test_get_chat_menu_button.py @@ -0,0 +1,28 @@ +import pytest + +from aiogram.methods import GetChatMenuButton, Request +from aiogram.types import MenuButton +from tests.mocked_bot import MockedBot + + +@pytest.mark.skip +class TestGetChatMenuButton: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(GetChatMenuButton, ok=True, result=None) + + response: MenuButton = await GetChatMenuButton() + request: Request = bot.get_request() + assert request.method == "getChatMenuButton" + # assert request.data == {} + assert response == prepare_result.result + + @pytest.mark.asyncio + async def test_bot_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(GetChatMenuButton, ok=True, result=None) + + response: MenuButton = await bot.get_chat_menu_button() + request: Request = bot.get_request() + assert request.method == "getChatMenuButton" + # assert request.data == {} + assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_get_my_default_administrator_rights.py b/tests/test_api/test_methods/test_get_my_default_administrator_rights.py new file mode 100644 index 00000000..9b633382 --- /dev/null +++ b/tests/test_api/test_methods/test_get_my_default_administrator_rights.py @@ -0,0 +1,28 @@ +import pytest + +from aiogram.methods import GetMyDefaultAdministratorRights, Request +from aiogram.types import ChatAdministratorRights +from tests.mocked_bot import MockedBot + + +@pytest.mark.skip +class TestGetMyDefaultAdministratorRights: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(GetMyDefaultAdministratorRights, ok=True, result=None) + + response: ChatAdministratorRights = await GetMyDefaultAdministratorRights() + request: Request = bot.get_request() + assert request.method == "getMyDefaultAdministratorRights" + # assert request.data == {} + assert response == prepare_result.result + + @pytest.mark.asyncio + async def test_bot_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(GetMyDefaultAdministratorRights, ok=True, result=None) + + response: ChatAdministratorRights = await bot.get_my_default_administrator_rights() + request: Request = bot.get_request() + assert request.method == "getMyDefaultAdministratorRights" + # assert request.data == {} + assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_set_chat_menu_button.py b/tests/test_api/test_methods/test_set_chat_menu_button.py new file mode 100644 index 00000000..d63bf4a1 --- /dev/null +++ b/tests/test_api/test_methods/test_set_chat_menu_button.py @@ -0,0 +1,27 @@ +import pytest + +from aiogram.api.methods import Request, SetChatMenuButton +from tests.mocked_bot import MockedBot + + +@pytest.mark.skip +class TestSetChatMenuButton: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(SetChatMenuButton, ok=True, result=None) + + response: bool = await SetChatMenuButton() + request: Request = bot.get_request() + assert request.method == "setChatMenuButton" + # assert request.data == {} + assert response == prepare_result.result + + @pytest.mark.asyncio + async def test_bot_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(SetChatMenuButton, ok=True, result=None) + + response: bool = await bot.set_chat_menu_button() + request: Request = bot.get_request() + assert request.method == "setChatMenuButton" + # assert request.data == {} + assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_set_my_default_administrator_rights.py b/tests/test_api/test_methods/test_set_my_default_administrator_rights.py new file mode 100644 index 00000000..c064fdc2 --- /dev/null +++ b/tests/test_api/test_methods/test_set_my_default_administrator_rights.py @@ -0,0 +1,27 @@ +import pytest + +from aiogram.api.methods import Request, SetMyDefaultAdministratorRights +from tests.mocked_bot import MockedBot + + +@pytest.mark.skip +class TestSetMyDefaultAdministratorRights: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(SetMyDefaultAdministratorRights, ok=True, result=None) + + response: bool = await SetMyDefaultAdministratorRights() + request: Request = bot.get_request() + assert request.method == "setMyDefaultAdministratorRights" + # assert request.data == {} + assert response == prepare_result.result + + @pytest.mark.asyncio + async def test_bot_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(SetMyDefaultAdministratorRights, ok=True, result=None) + + response: bool = await bot.set_my_default_administrator_rights() + request: Request = bot.get_request() + assert request.method == "setMyDefaultAdministratorRights" + # assert request.data == {} + assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_unban_chat_sender_chat.py b/tests/test_api/test_methods/test_unban_chat_sender_chat.py old mode 100644 new mode 100755 index 4f80a5e1..033fbad0 --- a/tests/test_api/test_methods/test_unban_chat_sender_chat.py +++ b/tests/test_api/test_methods/test_unban_chat_sender_chat.py @@ -1,17 +1,18 @@ import pytest -from aiogram.methods import Request, UnbanChatSenderChat +from aiogram.api.methods import Request, UnbanChatSenderChat from tests.mocked_bot import MockedBot +@pytest.mark.skip class TestUnbanChatSenderChat: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(UnbanChatSenderChat, ok=True, result=True) + prepare_result = bot.add_result_for(UnbanChatSenderChat, ok=True, result=None) response: bool = await UnbanChatSenderChat( - chat_id=-42, - sender_chat_id=-1337, + chat_id=..., + sender_chat_id=..., ) request: Request = bot.get_request() assert request.method == "unbanChatSenderChat" @@ -20,11 +21,11 @@ class TestUnbanChatSenderChat: @pytest.mark.asyncio async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(UnbanChatSenderChat, ok=True, result=True) + prepare_result = bot.add_result_for(UnbanChatSenderChat, ok=True, result=None) response: bool = await bot.unban_chat_sender_chat( - chat_id=-42, - sender_chat_id=-1337, + chat_id=..., + sender_chat_id=..., ) request: Request = bot.get_request() assert request.method == "unbanChatSenderChat" diff --git a/tests/test_dispatcher/test_dispatcher.py b/tests/test_dispatcher/test_dispatcher.py index 89d027b1..f501ed75 100644 --- a/tests/test_dispatcher/test_dispatcher.py +++ b/tests/test_dispatcher/test_dispatcher.py @@ -82,11 +82,11 @@ class TestDispatcher: assert dp.get("foo", 42) == 42 dp["foo"] = 1 - assert dp._data["foo"] == 1 + assert dp.workflow_data["foo"] == 1 assert dp["foo"] == 1 del dp["foo"] - assert "foo" not in dp._data + assert "foo" not in dp.workflow_data def test_storage_property(self, dispatcher: Dispatcher): assert dispatcher.storage is dispatcher.fsm.storage From 8a37958e8298a668d7074736606f08f8ad21c549 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 17 Apr 2022 02:03:18 +0300 Subject: [PATCH 2/9] Bump license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 872283c6..f9721b14 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017-2019 Alex Root Junior +Copyright (c) 2017-2022 Alex Root Junior Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software From e5644248f9bb602c477441d631a524e3ff859f1e Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 19 Apr 2022 18:45:17 +0300 Subject: [PATCH 3/9] Revert re-generated tests --- .../test_approve_chat_join_request.py | 15 ++++++--------- .../test_methods/test_ban_chat_sender_chat.py | 15 +++++++-------- .../test_decline_chat_join_request.py | 17 +++++++---------- .../test_methods/test_unban_chat_sender_chat.py | 15 +++++++-------- 4 files changed, 27 insertions(+), 35 deletions(-) diff --git a/tests/test_api/test_methods/test_approve_chat_join_request.py b/tests/test_api/test_methods/test_approve_chat_join_request.py index 1b9fa8f6..52d6554a 100755 --- a/tests/test_api/test_methods/test_approve_chat_join_request.py +++ b/tests/test_api/test_methods/test_approve_chat_join_request.py @@ -1,22 +1,20 @@ import pytest -from aiogram.api.methods import ApproveChatJoinRequest, Request +from aiogram.methods import ApproveChatJoinRequest, Request from tests.mocked_bot import MockedBot -@pytest.mark.skip class TestApproveChatJoinRequest: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(ApproveChatJoinRequest, ok=True, result=None) + prepare_result = bot.add_result_for(ApproveChatJoinRequest, ok=True, result=True) response: bool = await ApproveChatJoinRequest( - chat_id=..., - user_id=..., + chat_id=-42, + user_id=42, ) request: Request = bot.get_request() assert request.method == "approveChatJoinRequest" - # assert request.data == {} assert response == prepare_result.result @pytest.mark.asyncio @@ -24,10 +22,9 @@ class TestApproveChatJoinRequest: prepare_result = bot.add_result_for(ApproveChatJoinRequest, ok=True, result=None) response: bool = await bot.approve_chat_join_request( - chat_id=..., - user_id=..., + chat_id=-42, + user_id=42, ) request: Request = bot.get_request() assert request.method == "approveChatJoinRequest" - # assert request.data == {} assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_ban_chat_sender_chat.py b/tests/test_api/test_methods/test_ban_chat_sender_chat.py index 7df5ae7c..1d832e87 100755 --- a/tests/test_api/test_methods/test_ban_chat_sender_chat.py +++ b/tests/test_api/test_methods/test_ban_chat_sender_chat.py @@ -1,18 +1,17 @@ import pytest -from aiogram.api.methods import BanChatSenderChat, Request +from aiogram.methods import BanChatSenderChat, Request from tests.mocked_bot import MockedBot -@pytest.mark.skip class TestBanChatSenderChat: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(BanChatSenderChat, ok=True, result=None) + prepare_result = bot.add_result_for(BanChatSenderChat, ok=True, result=True) response: bool = await BanChatSenderChat( - chat_id=..., - sender_chat_id=..., + chat_id=-42, + sender_chat_id=-1337, ) request: Request = bot.get_request() assert request.method == "banChatSenderChat" @@ -21,11 +20,11 @@ class TestBanChatSenderChat: @pytest.mark.asyncio async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(BanChatSenderChat, ok=True, result=None) + prepare_result = bot.add_result_for(BanChatSenderChat, ok=True, result=True) response: bool = await bot.ban_chat_sender_chat( - chat_id=..., - sender_chat_id=..., + chat_id=-42, + sender_chat_id=-1337, ) request: Request = bot.get_request() assert request.method == "banChatSenderChat" diff --git a/tests/test_api/test_methods/test_decline_chat_join_request.py b/tests/test_api/test_methods/test_decline_chat_join_request.py index adb9b80c..e6e60ca5 100755 --- a/tests/test_api/test_methods/test_decline_chat_join_request.py +++ b/tests/test_api/test_methods/test_decline_chat_join_request.py @@ -1,33 +1,30 @@ import pytest -from aiogram.api.methods import DeclineChatJoinRequest, Request +from aiogram.methods import DeclineChatJoinRequest, Request from tests.mocked_bot import MockedBot -@pytest.mark.skip class TestDeclineChatJoinRequest: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(DeclineChatJoinRequest, ok=True, result=None) + prepare_result = bot.add_result_for(DeclineChatJoinRequest, ok=True, result=True) response: bool = await DeclineChatJoinRequest( - chat_id=..., - user_id=..., + chat_id=-42, + user_id=42, ) request: Request = bot.get_request() assert request.method == "declineChatJoinRequest" - # assert request.data == {} assert response == prepare_result.result @pytest.mark.asyncio async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(DeclineChatJoinRequest, ok=True, result=None) + prepare_result = bot.add_result_for(DeclineChatJoinRequest, ok=True, result=True) response: bool = await bot.decline_chat_join_request( - chat_id=..., - user_id=..., + chat_id=-42, + user_id=42, ) request: Request = bot.get_request() assert request.method == "declineChatJoinRequest" - # assert request.data == {} assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_unban_chat_sender_chat.py b/tests/test_api/test_methods/test_unban_chat_sender_chat.py index 033fbad0..4f80a5e1 100755 --- a/tests/test_api/test_methods/test_unban_chat_sender_chat.py +++ b/tests/test_api/test_methods/test_unban_chat_sender_chat.py @@ -1,18 +1,17 @@ import pytest -from aiogram.api.methods import Request, UnbanChatSenderChat +from aiogram.methods import Request, UnbanChatSenderChat from tests.mocked_bot import MockedBot -@pytest.mark.skip class TestUnbanChatSenderChat: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(UnbanChatSenderChat, ok=True, result=None) + prepare_result = bot.add_result_for(UnbanChatSenderChat, ok=True, result=True) response: bool = await UnbanChatSenderChat( - chat_id=..., - sender_chat_id=..., + chat_id=-42, + sender_chat_id=-1337, ) request: Request = bot.get_request() assert request.method == "unbanChatSenderChat" @@ -21,11 +20,11 @@ class TestUnbanChatSenderChat: @pytest.mark.asyncio async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(UnbanChatSenderChat, ok=True, result=None) + prepare_result = bot.add_result_for(UnbanChatSenderChat, ok=True, result=True) response: bool = await bot.unban_chat_sender_chat( - chat_id=..., - sender_chat_id=..., + chat_id=-42, + sender_chat_id=-1337, ) request: Request = bot.get_request() assert request.method == "unbanChatSenderChat" From c8ad7552a273a44db12e4049c21e0348fd620e87 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 19 Apr 2022 21:03:26 +0300 Subject: [PATCH 4/9] Fix tests, improved docs --- .readthedocs.yml | 1 + CHANGES/890.feature.rst | 1 + aiogram/types/__init__.py | 8 -- aiogram/types/video_chat_scheduled.py | 3 +- aiogram/types/voice_chat_ended.py | 14 --- .../types/voice_chat_participants_invited.py | 19 ---- aiogram/types/voice_chat_scheduled.py | 14 --- aiogram/types/voice_chat_started.py | 11 --- aiogram/types/web_app_init_data.py | 19 ---- aiogram/types/web_app_user.py | 26 ------ aiogram/utils/web_app.py | 86 +++++++++++++++---- docs/api/types/voice_chat_ended.rst | 9 -- .../types/voice_chat_participants_invited.rst | 9 -- docs/api/types/voice_chat_scheduled.rst | 9 -- docs/api/types/voice_chat_started.rst | 9 -- docs/utils/index.rst | 1 + docs/utils/web_app.rst | 55 ++++++++++++ .../test_methods/test_answer_web_app_query.py | 15 ++-- .../test_methods/test_get_chat_menu_button.py | 7 +- ...est_get_my_default_administrator_rights.py | 31 ++++++- .../test_methods/test_get_sticker_set.py | 4 + .../test_methods/test_send_sticker.py | 2 + .../test_methods/test_set_chat_menu_button.py | 7 +- ...est_set_my_default_administrator_rights.py | 7 +- tests/test_api/test_types/test_message.py | 53 ++++++++---- .../test_filters/test_chat_member_updated.py | 2 +- tests/test_utils/test_web_app.py | 80 +++++++++++++++++ 27 files changed, 296 insertions(+), 206 deletions(-) create mode 100644 CHANGES/890.feature.rst delete mode 100644 aiogram/types/voice_chat_ended.py delete mode 100644 aiogram/types/voice_chat_participants_invited.py delete mode 100644 aiogram/types/voice_chat_scheduled.py delete mode 100644 aiogram/types/voice_chat_started.py delete mode 100644 aiogram/types/web_app_init_data.py delete mode 100644 aiogram/types/web_app_user.py delete mode 100644 docs/api/types/voice_chat_ended.rst delete mode 100644 docs/api/types/voice_chat_participants_invited.rst delete mode 100644 docs/api/types/voice_chat_scheduled.rst delete mode 100644 docs/api/types/voice_chat_started.rst create mode 100644 docs/utils/web_app.rst create mode 100644 tests/test_utils/test_web_app.py diff --git a/.readthedocs.yml b/.readthedocs.yml index 1efe11cb..e03323e6 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -12,3 +12,4 @@ python: path: . extra_requirements: - docs + - redis diff --git a/CHANGES/890.feature.rst b/CHANGES/890.feature.rst new file mode 100644 index 00000000..10c60c05 --- /dev/null +++ b/CHANGES/890.feature.rst @@ -0,0 +1 @@ +Added full support of `Telegram Bot API 6.0 `_ diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index ef41a85b..ec04b855 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -130,10 +130,6 @@ from .video_chat_scheduled import VideoChatScheduled from .video_chat_started import VideoChatStarted from .video_note import VideoNote from .voice import Voice -from .voice_chat_ended import VoiceChatEnded -from .voice_chat_participants_invited import VoiceChatParticipantsInvited -from .voice_chat_scheduled import VoiceChatScheduled -from .voice_chat_started import VoiceChatStarted from .web_app_data import WebAppData from .web_app_info import WebAppInfo from .webhook_info import WebhookInfo @@ -170,10 +166,6 @@ __all__ = ( "WebAppData", "ProximityAlertTriggered", "MessageAutoDeleteTimerChanged", - "VoiceChatScheduled", - "VoiceChatStarted", - "VoiceChatEnded", - "VoiceChatParticipantsInvited", "VideoChatScheduled", "VideoChatStarted", "VideoChatEnded", diff --git a/aiogram/types/video_chat_scheduled.py b/aiogram/types/video_chat_scheduled.py index 1e41c6d4..c523fbff 100644 --- a/aiogram/types/video_chat_scheduled.py +++ b/aiogram/types/video_chat_scheduled.py @@ -1,5 +1,6 @@ from __future__ import annotations +from datetime import datetime from typing import TYPE_CHECKING from .base import TelegramObject @@ -15,5 +16,5 @@ class VideoChatScheduled(TelegramObject): Source: https://core.telegram.org/bots/api#videochatscheduled """ - start_date: int + start_date: datetime """Point in time (Unix timestamp) when the video chat is supposed to be started by a chat administrator""" diff --git a/aiogram/types/voice_chat_ended.py b/aiogram/types/voice_chat_ended.py deleted file mode 100644 index 12c705c9..00000000 --- a/aiogram/types/voice_chat_ended.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -from .base import TelegramObject - - -class VoiceChatEnded(TelegramObject): - """ - This object represents a service message about a voice chat ended in the chat. - - Source: https://core.telegram.org/bots/api#voicechatended - """ - - duration: int - """Voice chat duration in seconds""" diff --git a/aiogram/types/voice_chat_participants_invited.py b/aiogram/types/voice_chat_participants_invited.py deleted file mode 100644 index b24ef91d..00000000 --- a/aiogram/types/voice_chat_participants_invited.py +++ /dev/null @@ -1,19 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, List, Optional - -from .base import TelegramObject - -if TYPE_CHECKING: - from .user import User - - -class VoiceChatParticipantsInvited(TelegramObject): - """ - This object represents a service message about new members invited to a voice chat. - - Source: https://core.telegram.org/bots/api#voicechatparticipantsinvited - """ - - users: Optional[List[User]] = None - """*Optional*. New members that were invited to the voice chat""" diff --git a/aiogram/types/voice_chat_scheduled.py b/aiogram/types/voice_chat_scheduled.py deleted file mode 100644 index 37c6c7bd..00000000 --- a/aiogram/types/voice_chat_scheduled.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -from .base import TelegramObject - - -class VoiceChatScheduled(TelegramObject): - """ - This object represents a service message about a voice chat scheduled in the chat. - - Source: https://core.telegram.org/bots/api#voicechatscheduled - """ - - start_date: int - """Point in time (Unix timestamp) when the voice chat is supposed to be started by a chat administrator""" diff --git a/aiogram/types/voice_chat_started.py b/aiogram/types/voice_chat_started.py deleted file mode 100644 index 6ad45263..00000000 --- a/aiogram/types/voice_chat_started.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import annotations - -from .base import TelegramObject - - -class VoiceChatStarted(TelegramObject): - """ - This object represents a service message about a voice chat started in the chat. Currently holds no information. - - Source: https://core.telegram.org/bots/api#voicechatstarted - """ diff --git a/aiogram/types/web_app_init_data.py b/aiogram/types/web_app_init_data.py deleted file mode 100644 index e27b12af..00000000 --- a/aiogram/types/web_app_init_data.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Optional - -from aiogram.types import TelegramObject -from aiogram.types.web_app_user import WebAppUser - - -class WebAppInitData(TelegramObject): - query_id: Optional[str] = None - """Optional. A unique identifier for the Web App session, required for sending messages via the answerWebAppQuery method.""" - user: Optional[WebAppUser] = None - """Optional. An object containing data about the current user.""" - receiver: Optional[WebAppUser] = None - """Optional. An object containing data about the chat partner of the current user in the chat where the bot was launched via the attachment menu. Returned only for Web Apps launched via the attachment menu.""" - start_param: Optional[str] = None - """Optional. The value of the startattach parameter, passed via link. Only returned for Web Apps when launched from the attachment menu via link. The value of the start_param parameter will also be passed in the GET-parameter tgWebAppStartParam, so the Web App can load the correct interface right away.""" - auth_date: int - """Unix time when the form was opened.""" - hash: str - """A hash of all passed parameters, which the bot server can use to check their validity.""" diff --git a/aiogram/types/web_app_user.py b/aiogram/types/web_app_user.py deleted file mode 100644 index e5436adf..00000000 --- a/aiogram/types/web_app_user.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Optional - -from aiogram.types import TelegramObject - - -class WebAppUser(TelegramObject): - """ - This object contains the data of the Web App user. - - Source: https://core.telegram.org/bots/webapps#webappuser - """ - - id: int - """A unique identifier for the user or bot. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. It has at most 52 significant bits, so a 64-bit integer or a double-precision float type is safe for storing this identifier.""" - is_bot: Optional[bool] = None - """Optional. True, if this user is a bot. Returns in the receiver field only.""" - first_name: str - """First name of the user or bot.""" - last_name: Optional[str] = None - """Optional. Last name of the user or bot.""" - username: Optional[str] = None - """Optional. Username of the user or bot.""" - language_code: Optional[str] = None - """Optional. IETF language tag of the user's language. Returns in user field only.""" - photo_url: Optional[str] = None - """Optional. URL of the user’s profile photo. The photo can be in .jpeg or .svg formats. Only returned for Web Apps launched from the attachment menu.""" diff --git a/aiogram/utils/web_app.py b/aiogram/utils/web_app.py index bb26830e..9cce8e06 100644 --- a/aiogram/utils/web_app.py +++ b/aiogram/utils/web_app.py @@ -1,11 +1,56 @@ import hashlib import hmac import json +from datetime import datetime from operator import itemgetter -from typing import Any, Callable +from typing import Any, Callable, Optional from urllib.parse import parse_qsl -from aiogram.types.web_app_init_data import WebAppInitData +from aiogram.types import TelegramObject + + +class WebAppUser(TelegramObject): + """ + This object contains the data of the Web App user. + + Source: https://core.telegram.org/bots/webapps#webappuser + """ + + id: int + """A unique identifier for the user or bot. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. It has at most 52 significant bits, so a 64-bit integer or a double-precision float type is safe for storing this identifier.""" + is_bot: Optional[bool] = None + """True, if this user is a bot. Returns in the receiver field only.""" + first_name: str + """First name of the user or bot.""" + last_name: Optional[str] = None + """Last name of the user or bot.""" + username: Optional[str] = None + """Username of the user or bot.""" + language_code: Optional[str] = None + """IETF language tag of the user's language. Returns in user field only.""" + photo_url: Optional[str] = None + """URL of the user’s profile photo. The photo can be in .jpeg or .svg formats. Only returned for Web Apps launched from the attachment menu.""" + + +class WebAppInitData(TelegramObject): + """ + This object contains data that is transferred to the Web App when it is opened. It is empty if the Web App was launched from a keyboard button. + + Source: https://core.telegram.org/bots/webapps#webappinitdata + """ + + query_id: Optional[str] = None + """A unique identifier for the Web App session, required for sending messages via the answerWebAppQuery method.""" + user: Optional[WebAppUser] = None + """An object containing data about the current user.""" + receiver: Optional[WebAppUser] = None + """An object containing data about the chat partner of the current user in the chat where the bot was launched via the attachment menu. Returned only for Web Apps launched via the attachment menu.""" + start_param: Optional[str] = None + """The value of the startattach parameter, passed via link. Only returned for Web Apps when launched from the attachment menu via link. The value of the start_param parameter will also be passed in the GET-parameter tgWebAppStartParam, so the Web App can load the correct interface right away.""" + auth_date: datetime + """Unix time when the form was opened.""" + hash: str + """A hash of all passed parameters, which the bot server can use to check their validity.""" def check_webapp_signature(token: str, init_data: str) -> bool: @@ -14,13 +59,13 @@ def check_webapp_signature(token: str, init_data: str) -> bool: Source: https://core.telegram.org/bots/webapps#validating-data-received-via-the-web-app - :param token: - :param init_data: + :param token: bot Token + :param init_data: data from frontend to be validated :return: """ try: - parsed_data = dict(parse_qsl(init_data)) - except ValueError: + parsed_data = dict(parse_qsl(init_data, strict_parsing=True)) + except ValueError: # pragma: no cover # Init data is not a valid query string return False if "hash" not in parsed_data: @@ -38,16 +83,19 @@ def check_webapp_signature(token: str, init_data: str) -> bool: return calculated_hash == hash_ -def parse_init_data( +def parse_webapp_init_data( init_data: str, *, - _loads: Callable[..., Any] = json.loads, + loads: Callable[..., Any] = json.loads, ) -> WebAppInitData: """ - Parse WebApp init data and return it as dict + Parse WebApp init data and return it as WebAppInitData object - :param init_data: - :param _loads: + This method doesn't make any security check, so you shall not trust to this data, + use :code:`safe_parse_webapp_init_data` instead. + + :param init_data: data from frontend to be parsed + :param loads: :return: """ result = {} @@ -55,7 +103,7 @@ def parse_init_data( if (value.startswith("[") and value.endswith("]")) or ( value.startswith("{") and value.endswith("}") ): - value = _loads(value) + value = loads(value) result[key] = value return WebAppInitData(**result) @@ -64,16 +112,18 @@ def safe_parse_webapp_init_data( token: str, init_data: str, *, - _loads: Callable[..., Any] = json.loads, + loads: Callable[..., Any] = json.loads, ) -> WebAppInitData: """ - Validate WebApp init data and return it as dict + Validate raw WebApp init data and return it as WebAppInitData object - :param token: - :param init_data: - :param _loads: + Raise :type:`ValueError` when data is invalid + + :param token: bot token + :param init_data: data from frontend to be parsed and validated + :param loads: :return: """ if check_webapp_signature(token, init_data): - return parse_init_data(init_data, _loads=_loads) + return parse_webapp_init_data(init_data, loads=loads) raise ValueError("Invalid init data signature") diff --git a/docs/api/types/voice_chat_ended.rst b/docs/api/types/voice_chat_ended.rst deleted file mode 100644 index cce70b7c..00000000 --- a/docs/api/types/voice_chat_ended.rst +++ /dev/null @@ -1,9 +0,0 @@ -############## -VoiceChatEnded -############## - - -.. automodule:: aiogram.types.voice_chat_ended - :members: - :member-order: bysource - :undoc-members: True diff --git a/docs/api/types/voice_chat_participants_invited.rst b/docs/api/types/voice_chat_participants_invited.rst deleted file mode 100644 index 89a94fa9..00000000 --- a/docs/api/types/voice_chat_participants_invited.rst +++ /dev/null @@ -1,9 +0,0 @@ -############################ -VoiceChatParticipantsInvited -############################ - - -.. automodule:: aiogram.types.voice_chat_participants_invited - :members: - :member-order: bysource - :undoc-members: True diff --git a/docs/api/types/voice_chat_scheduled.rst b/docs/api/types/voice_chat_scheduled.rst deleted file mode 100644 index e63936e2..00000000 --- a/docs/api/types/voice_chat_scheduled.rst +++ /dev/null @@ -1,9 +0,0 @@ -################## -VoiceChatScheduled -################## - - -.. automodule:: aiogram.types.voice_chat_scheduled - :members: - :member-order: bysource - :undoc-members: True diff --git a/docs/api/types/voice_chat_started.rst b/docs/api/types/voice_chat_started.rst deleted file mode 100644 index c1a1964b..00000000 --- a/docs/api/types/voice_chat_started.rst +++ /dev/null @@ -1,9 +0,0 @@ -################ -VoiceChatStarted -################ - - -.. automodule:: aiogram.types.voice_chat_started - :members: - :member-order: bysource - :undoc-members: True diff --git a/docs/utils/index.rst b/docs/utils/index.rst index 4f6eccf5..a7a8474d 100644 --- a/docs/utils/index.rst +++ b/docs/utils/index.rst @@ -7,3 +7,4 @@ Utils keyboard i18n chat_action + web_app diff --git a/docs/utils/web_app.rst b/docs/utils/web_app.rst new file mode 100644 index 00000000..82e9cc42 --- /dev/null +++ b/docs/utils/web_app.rst @@ -0,0 +1,55 @@ +====== +WebApз +====== + +Telegram Bot API 6.0 announces a revolution in the development of chatbots using WebApp feature. + +You can read more details on it in the official `blog `_ +and `documentation `_. + +`aiogram` implements simple utils to remove headache with the data validation from Telegram WebApp on the backend side. + +Usage +===== + +For example from frontend you will pass :code:`application/x-www-form-urlencoded` POST request +with :code:`_auth` field in body and wants to return User info inside response as :code:`application/json` + +.. code-block:: python + + from aiogram.utils.web_app import safe_parse_webapp_init_data + from aiohttp.web_request import Request + from aiohttp.web_response import json_response + + async def check_data_handler(request: Request): + bot: Bot = request.app["bot"] + + data = await request.post() # application/x-www-form-urlencoded + try: + data = safe_parse_webapp_init_data(token=bot.token, init_data=data["_auth"]) + except ValueError: + return json_response({"ok": False, "err": "Unauthorized"}, status=401) + return json_response({"ok": True, "data": data.user.dict()}) + +Functions +========= + +.. autofunction:: aiogram.utils.web_app.check_webapp_signature + +.. autofunction:: aiogram.utils.web_app.parse_webapp_init_data + +.. autofunction:: aiogram.utils.web_app.safe_parse_webapp_init_data + + +Types +===== + +.. autoclass:: aiogram.utils.web_app.WebAppInitData + :members: + :member-order: bysource + :undoc-members: True + +.. autoclass:: aiogram.utils.web_app.WebAppUser + :members: + :member-order: bysource + :undoc-members: True diff --git a/tests/test_api/test_methods/test_answer_web_app_query.py b/tests/test_api/test_methods/test_answer_web_app_query.py index c8865be2..8d9848da 100644 --- a/tests/test_api/test_methods/test_answer_web_app_query.py +++ b/tests/test_api/test_methods/test_answer_web_app_query.py @@ -1,19 +1,18 @@ import pytest from aiogram.methods import AnswerWebAppQuery, Request -from aiogram.types import SentWebAppMessage +from aiogram.types import InlineQueryResult, SentWebAppMessage from tests.mocked_bot import MockedBot -@pytest.mark.skip class TestAnswerWebAppQuery: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(AnswerWebAppQuery, ok=True, result=None) + prepare_result = bot.add_result_for(AnswerWebAppQuery, ok=True, result=SentWebAppMessage()) response: SentWebAppMessage = await AnswerWebAppQuery( - web_app_query_id=..., - result=..., + web_app_query_id="test", + result=InlineQueryResult(), ) request: Request = bot.get_request() assert request.method == "answerWebAppQuery" @@ -22,11 +21,11 @@ class TestAnswerWebAppQuery: @pytest.mark.asyncio async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(AnswerWebAppQuery, ok=True, result=None) + prepare_result = bot.add_result_for(AnswerWebAppQuery, ok=True, result=SentWebAppMessage()) response: SentWebAppMessage = await bot.answer_web_app_query( - web_app_query_id=..., - result=..., + web_app_query_id="test", + result=InlineQueryResult(), ) request: Request = bot.get_request() assert request.method == "answerWebAppQuery" diff --git a/tests/test_api/test_methods/test_get_chat_menu_button.py b/tests/test_api/test_methods/test_get_chat_menu_button.py index 2b7803f4..a7c2fd37 100644 --- a/tests/test_api/test_methods/test_get_chat_menu_button.py +++ b/tests/test_api/test_methods/test_get_chat_menu_button.py @@ -1,15 +1,14 @@ import pytest from aiogram.methods import GetChatMenuButton, Request -from aiogram.types import MenuButton +from aiogram.types import MenuButton, MenuButtonDefault from tests.mocked_bot import MockedBot -@pytest.mark.skip class TestGetChatMenuButton: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(GetChatMenuButton, ok=True, result=None) + prepare_result = bot.add_result_for(GetChatMenuButton, ok=True, result=MenuButtonDefault()) response: MenuButton = await GetChatMenuButton() request: Request = bot.get_request() @@ -19,7 +18,7 @@ class TestGetChatMenuButton: @pytest.mark.asyncio async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(GetChatMenuButton, ok=True, result=None) + prepare_result = bot.add_result_for(GetChatMenuButton, ok=True, result=MenuButtonDefault()) response: MenuButton = await bot.get_chat_menu_button() request: Request = bot.get_request() diff --git a/tests/test_api/test_methods/test_get_my_default_administrator_rights.py b/tests/test_api/test_methods/test_get_my_default_administrator_rights.py index 9b633382..179b468d 100644 --- a/tests/test_api/test_methods/test_get_my_default_administrator_rights.py +++ b/tests/test_api/test_methods/test_get_my_default_administrator_rights.py @@ -5,11 +5,23 @@ from aiogram.types import ChatAdministratorRights from tests.mocked_bot import MockedBot -@pytest.mark.skip class TestGetMyDefaultAdministratorRights: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(GetMyDefaultAdministratorRights, ok=True, result=None) + prepare_result = bot.add_result_for( + GetMyDefaultAdministratorRights, + ok=True, + result=ChatAdministratorRights( + is_anonymous=False, + can_manage_chat=False, + can_delete_messages=False, + can_manage_video_chats=False, + can_restrict_members=False, + can_promote_members=False, + can_change_info=False, + can_invite_users=False, + ), + ) response: ChatAdministratorRights = await GetMyDefaultAdministratorRights() request: Request = bot.get_request() @@ -19,7 +31,20 @@ class TestGetMyDefaultAdministratorRights: @pytest.mark.asyncio async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(GetMyDefaultAdministratorRights, ok=True, result=None) + prepare_result = bot.add_result_for( + GetMyDefaultAdministratorRights, + ok=True, + result=ChatAdministratorRights( + is_anonymous=False, + can_manage_chat=False, + can_delete_messages=False, + can_manage_video_chats=False, + can_restrict_members=False, + can_promote_members=False, + can_change_info=False, + can_invite_users=False, + ), + ) response: ChatAdministratorRights = await bot.get_my_default_administrator_rights() request: Request = bot.get_request() diff --git a/tests/test_api/test_methods/test_get_sticker_set.py b/tests/test_api/test_methods/test_get_sticker_set.py index baed1d40..d778f1f7 100644 --- a/tests/test_api/test_methods/test_get_sticker_set.py +++ b/tests/test_api/test_methods/test_get_sticker_set.py @@ -16,6 +16,7 @@ class TestGetStickerSet: name="test", title="test", is_animated=False, + is_video=False, contains_masks=False, stickers=[ Sticker( @@ -23,6 +24,7 @@ class TestGetStickerSet: width=42, height=42, is_animated=False, + is_video=False, file_unique_id="file id", ) ], @@ -42,6 +44,7 @@ class TestGetStickerSet: name="test", title="test", is_animated=False, + is_video=False, contains_masks=False, stickers=[ Sticker( @@ -49,6 +52,7 @@ class TestGetStickerSet: width=42, height=42, is_animated=False, + is_video=False, file_unique_id="file id", ) ], diff --git a/tests/test_api/test_methods/test_send_sticker.py b/tests/test_api/test_methods/test_send_sticker.py index d356e8ae..239065eb 100644 --- a/tests/test_api/test_methods/test_send_sticker.py +++ b/tests/test_api/test_methods/test_send_sticker.py @@ -22,6 +22,7 @@ class TestSendSticker: width=42, height=42, is_animated=False, + is_video=False, file_unique_id="file id", ), chat=Chat(id=42, type="private"), @@ -45,6 +46,7 @@ class TestSendSticker: width=42, height=42, is_animated=False, + is_video=False, file_unique_id="file id", ), chat=Chat(id=42, type="private"), diff --git a/tests/test_api/test_methods/test_set_chat_menu_button.py b/tests/test_api/test_methods/test_set_chat_menu_button.py index d63bf4a1..97e2fa90 100644 --- a/tests/test_api/test_methods/test_set_chat_menu_button.py +++ b/tests/test_api/test_methods/test_set_chat_menu_button.py @@ -1,14 +1,13 @@ import pytest -from aiogram.api.methods import Request, SetChatMenuButton +from aiogram.methods import Request, SetChatMenuButton from tests.mocked_bot import MockedBot -@pytest.mark.skip class TestSetChatMenuButton: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(SetChatMenuButton, ok=True, result=None) + prepare_result = bot.add_result_for(SetChatMenuButton, ok=True, result=True) response: bool = await SetChatMenuButton() request: Request = bot.get_request() @@ -18,7 +17,7 @@ class TestSetChatMenuButton: @pytest.mark.asyncio async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(SetChatMenuButton, ok=True, result=None) + prepare_result = bot.add_result_for(SetChatMenuButton, ok=True, result=True) response: bool = await bot.set_chat_menu_button() request: Request = bot.get_request() diff --git a/tests/test_api/test_methods/test_set_my_default_administrator_rights.py b/tests/test_api/test_methods/test_set_my_default_administrator_rights.py index c064fdc2..4bd08822 100644 --- a/tests/test_api/test_methods/test_set_my_default_administrator_rights.py +++ b/tests/test_api/test_methods/test_set_my_default_administrator_rights.py @@ -1,14 +1,13 @@ import pytest -from aiogram.api.methods import Request, SetMyDefaultAdministratorRights +from aiogram.methods import Request, SetMyDefaultAdministratorRights from tests.mocked_bot import MockedBot -@pytest.mark.skip class TestSetMyDefaultAdministratorRights: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(SetMyDefaultAdministratorRights, ok=True, result=None) + prepare_result = bot.add_result_for(SetMyDefaultAdministratorRights, ok=True, result=True) response: bool = await SetMyDefaultAdministratorRights() request: Request = bot.get_request() @@ -18,7 +17,7 @@ class TestSetMyDefaultAdministratorRights: @pytest.mark.asyncio async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(SetMyDefaultAdministratorRights, ok=True, result=None) + prepare_result = bot.add_result_for(SetMyDefaultAdministratorRights, ok=True, result=True) response: bool = await bot.set_my_default_administrator_rights() request: Request = bot.get_request() diff --git a/tests/test_api/test_types/test_message.py b/tests/test_api/test_types/test_message.py index b2b66b77..fcf357c4 100644 --- a/tests/test_api/test_types/test_message.py +++ b/tests/test_api/test_types/test_message.py @@ -52,11 +52,13 @@ from aiogram.types import ( User, Venue, Video, + VideoChatEnded, + VideoChatParticipantsInvited, + VideoChatScheduled, + VideoChatStarted, VideoNote, Voice, - VoiceChatEnded, - VoiceChatParticipantsInvited, - VoiceChatStarted, + WebAppData, ) from aiogram.types.message import ContentType, Message @@ -122,6 +124,7 @@ TEST_MESSAGE_STICKER = Message( width=42, height=42, is_animated=False, + is_video=False, ), chat=Chat(id=42, type="private"), from_user=User(id=42, is_bot=False, first_name="Test"), @@ -318,29 +321,38 @@ TEST_MESSAGE_MESSAGE_AUTO_DELETE_TIMER_CHANGED = Message( message_auto_delete_timer_changed=MessageAutoDeleteTimerChanged(message_auto_delete_time=42), from_user=User(id=42, is_bot=False, first_name="Test"), ) -TEST_MESSAGE_VOICE_CHAT_STARTED = Message( +TEST_MESSAGE_VIDEO_CHAT_STARTED = Message( message_id=42, date=datetime.datetime.now(), chat=Chat(id=42, type="private"), from_user=User(id=42, is_bot=False, first_name="Test"), - voice_chat_started=VoiceChatStarted(), + video_chat_started=VideoChatStarted(), ) -TEST_MESSAGE_VOICE_CHAT_ENDED = Message( +TEST_MESSAGE_VIDEO_CHAT_ENDED = Message( message_id=42, date=datetime.datetime.now(), chat=Chat(id=42, type="private"), from_user=User(id=42, is_bot=False, first_name="Test"), - voice_chat_ended=VoiceChatEnded(duration=42), + video_chat_ended=VideoChatEnded(duration=42), ) -TEST_MESSAGE_VOICE_CHAT_PARTICIPANTS_INVITED = Message( +TEST_MESSAGE_VIDEO_CHAT_PARTICIPANTS_INVITED = Message( message_id=42, date=datetime.datetime.now(), chat=Chat(id=42, type="private"), from_user=User(id=42, is_bot=False, first_name="Test"), - voice_chat_participants_invited=VoiceChatParticipantsInvited( + video_chat_participants_invited=VideoChatParticipantsInvited( users=[User(id=69, is_bot=False, first_name="Test")] ), ) +TEST_MESSAGE_VIDEO_CHAT_SCHEDULED = Message( + message_id=42, + date=datetime.datetime.now(), + chat=Chat(id=42, type="private"), + from_user=User(id=42, is_bot=False, first_name="Test"), + video_chat_scheduled=VideoChatScheduled( + start_date=datetime.datetime.now(), + ), +) TEST_MESSAGE_DICE = Message( message_id=42, date=datetime.datetime.now(), @@ -348,6 +360,13 @@ TEST_MESSAGE_DICE = Message( dice=Dice(value=6, emoji="X"), from_user=User(id=42, is_bot=False, first_name="Test"), ) +TEST_MESSAGE_WEB_APP_DATA = Message( + message_id=42, + date=datetime.datetime.now(), + chat=Chat(id=42, type="private"), + web_app_data=WebAppData(data="test", button_text="Test"), + from_user=User(id=42, is_bot=False, first_name="Test"), +) TEST_MESSAGE_UNKNOWN = Message( message_id=42, date=datetime.datetime.now(), @@ -391,13 +410,15 @@ class TestMessage: TEST_MESSAGE_MESSAGE_AUTO_DELETE_TIMER_CHANGED, ContentType.MESSAGE_AUTO_DELETE_TIMER_CHANGED, ], - [TEST_MESSAGE_VOICE_CHAT_STARTED, ContentType.VOICE_CHAT_STARTED], - [TEST_MESSAGE_VOICE_CHAT_ENDED, ContentType.VOICE_CHAT_ENDED], + [TEST_MESSAGE_VIDEO_CHAT_SCHEDULED, ContentType.VIDEO_CHAT_SCHEDULED], + [TEST_MESSAGE_VIDEO_CHAT_STARTED, ContentType.VIDEO_CHAT_STARTED], + [TEST_MESSAGE_VIDEO_CHAT_ENDED, ContentType.VIDEO_CHAT_ENDED], [ - TEST_MESSAGE_VOICE_CHAT_PARTICIPANTS_INVITED, - ContentType.VOICE_CHAT_PARTICIPANTS_INVITED, + TEST_MESSAGE_VIDEO_CHAT_PARTICIPANTS_INVITED, + ContentType.VIDEO_CHAT_PARTICIPANTS_INVITED, ], [TEST_MESSAGE_DICE, ContentType.DICE], + [TEST_MESSAGE_WEB_APP_DATA, ContentType.WEB_APP_DATA], [TEST_MESSAGE_UNKNOWN, ContentType.UNKNOWN], ], ) @@ -535,9 +556,9 @@ class TestMessage: [TEST_MESSAGE_PASSPORT_DATA, None], [TEST_MESSAGE_POLL, SendPoll], [TEST_MESSAGE_MESSAGE_AUTO_DELETE_TIMER_CHANGED, None], - [TEST_MESSAGE_VOICE_CHAT_STARTED, None], - [TEST_MESSAGE_VOICE_CHAT_ENDED, None], - [TEST_MESSAGE_VOICE_CHAT_PARTICIPANTS_INVITED, None], + [TEST_MESSAGE_VIDEO_CHAT_STARTED, None], + [TEST_MESSAGE_VIDEO_CHAT_ENDED, None], + [TEST_MESSAGE_VIDEO_CHAT_PARTICIPANTS_INVITED, None], [TEST_MESSAGE_DICE, SendDice], [TEST_MESSAGE_UNKNOWN, None], ], diff --git a/tests/test_dispatcher/test_filters/test_chat_member_updated.py b/tests/test_dispatcher/test_filters/test_chat_member_updated.py index 63ee1245..dae0e985 100644 --- a/tests/test_dispatcher/test_filters/test_chat_member_updated.py +++ b/tests/test_dispatcher/test_filters/test_chat_member_updated.py @@ -320,7 +320,7 @@ class TestChatMemberUpdatedStatusFilter: "can_be_edited": True, "can_manage_chat": True, "can_delete_messages": True, - "can_manage_voice_chats": True, + "can_manage_video_chats": True, "can_restrict_members": True, "can_promote_members": True, "can_change_info": True, diff --git a/tests/test_utils/test_web_app.py b/tests/test_utils/test_web_app.py new file mode 100644 index 00000000..abebd909 --- /dev/null +++ b/tests/test_utils/test_web_app.py @@ -0,0 +1,80 @@ +import pytest + +from aiogram.utils.web_app import ( + WebAppInitData, + check_webapp_signature, + parse_webapp_init_data, + safe_parse_webapp_init_data, +) + + +class TestWebApp: + @pytest.mark.parametrize( + "token,case,result", + [ + [ + "42:TEST", + "auth_date=1650385342" + "&user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D" + "&query_id=test" + "&hash=46d2ea5e32911ec8d30999b56247654460c0d20949b6277af519e76271182803", + True, + ], + [ + "42:INVALID", + "auth_date=1650385342" + "&user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D" + "&query_id=test" + "&hash=46d2ea5e32911ec8d30999b56247654460c0d20949b6277af519e76271182803", + False, + ], + [ + "42:TEST", + "user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D&query_id=test&hash=test", + False, + ], + [ + "42:TEST", + "user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D&query_id=test", + False, + ], + ["42:TEST", "", False], + ["42:TEST", "test&foo=bar=baz", False], + ], + ) + def test_check_webapp_signature(self, token, case, result): + assert check_webapp_signature(token, case) is result + + def test_parse_web_app_init_data(self): + parsed = parse_webapp_init_data( + "auth_date=1650385342" + "&user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D" + "&query_id=test" + "&hash=46d2ea5e32911ec8d30999b56247654460c0d20949b6277af519e76271182803", + ) + assert isinstance(parsed, WebAppInitData) + assert parsed.user + assert parsed.user.first_name == "Test" + assert parsed.user.id == 42 + assert parsed.query_id == "test" + assert parsed.hash == "46d2ea5e32911ec8d30999b56247654460c0d20949b6277af519e76271182803" + assert parsed.auth_date.year == 2022 + + def test_valid_safe_parse_webapp_init_data(self): + assert safe_parse_webapp_init_data( + "42:TEST", + "auth_date=1650385342" + "&user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D" + "&query_id=test" + "&hash=46d2ea5e32911ec8d30999b56247654460c0d20949b6277af519e76271182803", + ) + + def test_invalid_safe_parse_webapp_init_data(self): + with pytest.raises(ValueError): + safe_parse_webapp_init_data( + "42:TOKEN", + "auth_date=1650385342" + "&user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D" + "&query_id=test" + "&hash=test", + ) From f03a738af879bf9d05f8579e43218a81de8cd3fb Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 19 Apr 2022 21:19:35 +0300 Subject: [PATCH 5/9] Remove TODO --- aiogram/client/bot.py | 1 - aiogram/methods/promote_chat_member.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/aiogram/client/bot.py b/aiogram/client/bot.py index 17241039..16453574 100644 --- a/aiogram/client/bot.py +++ b/aiogram/client/bot.py @@ -1655,7 +1655,6 @@ class Bot(ContextInstanceMixin["Bot"]): :param request_timeout: Request timeout :return: Returns True on success. """ - # TODO: deprecate voice call = PromoteChatMember( chat_id=chat_id, user_id=user_id, diff --git a/aiogram/methods/promote_chat_member.py b/aiogram/methods/promote_chat_member.py index f346f979..4fd1a0f2 100644 --- a/aiogram/methods/promote_chat_member.py +++ b/aiogram/methods/promote_chat_member.py @@ -31,7 +31,7 @@ class PromoteChatMember(TelegramMethod[bool]): """Pass :code:`True`, if the administrator can edit messages of other users and can pin messages, channels only""" can_delete_messages: Optional[bool] = None """Pass :code:`True`, if the administrator can delete messages of other users""" - can_manage_video_chats: Optional[bool] = None # TODO: deprecate voice + can_manage_video_chats: Optional[bool] = None """Pass :code:`True`, if the administrator can manage video chats""" can_restrict_members: Optional[bool] = None """Pass :code:`True`, if the administrator can restrict, ban or unban chat members""" From a869c54ecc55a3ef7fa13f2dac277528998567bb Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 19 Apr 2022 21:25:43 +0300 Subject: [PATCH 6/9] Removed unreachable code --- aiogram/dispatcher/filters/text.py | 8 +++----- aiogram/types/menu_button_commands.py | 5 ----- aiogram/types/menu_button_default.py | 5 ----- aiogram/types/message.py | 4 +--- aiogram/types/sent_web_app_message.py | 5 +---- aiogram/types/video_chat_scheduled.py | 4 ---- aiogram/types/video_chat_started.py | 5 ----- aiogram/types/web_app_data.py | 5 ----- aiogram/types/web_app_info.py | 5 ----- 9 files changed, 5 insertions(+), 41 deletions(-) diff --git a/aiogram/dispatcher/filters/text.py b/aiogram/dispatcher/filters/text.py index 3dd36fbd..09acb285 100644 --- a/aiogram/dispatcher/filters/text.py +++ b/aiogram/dispatcher/filters/text.py @@ -1,14 +1,12 @@ -from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Union +from typing import Any, Dict, Optional, Sequence, Union from pydantic import root_validator from aiogram.dispatcher.filters import BaseFilter from aiogram.types import CallbackQuery, InlineQuery, Message, Poll +from aiogram.utils.i18n.lazy_proxy import LazyProxy -if TYPE_CHECKING: - from aiogram.utils.i18n.lazy_proxy import LazyProxy - -TextType = Union[str, "LazyProxy"] +TextType = Union[str, LazyProxy] class Text(BaseFilter): diff --git a/aiogram/types/menu_button_commands.py b/aiogram/types/menu_button_commands.py index 18452b70..5f4e252b 100644 --- a/aiogram/types/menu_button_commands.py +++ b/aiogram/types/menu_button_commands.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - from pydantic import Field from . import MenuButton -if TYPE_CHECKING: - pass - class MenuButtonCommands(MenuButton): """ diff --git a/aiogram/types/menu_button_default.py b/aiogram/types/menu_button_default.py index f4b60ea6..13cd3a37 100644 --- a/aiogram/types/menu_button_default.py +++ b/aiogram/types/menu_button_default.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - from pydantic import Field from . import MenuButton -if TYPE_CHECKING: - pass - class MenuButtonDefault(MenuButton): """ diff --git a/aiogram/types/message.py b/aiogram/types/message.py index a70feb64..1fe8a4f5 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -72,7 +72,7 @@ if TYPE_CHECKING: from .web_app_data import WebAppData -class _BaseMessage(TelegramObject): +class Message(TelegramObject): """ This object represents a message. @@ -198,8 +198,6 @@ class _BaseMessage(TelegramObject): reply_markup: Optional[InlineKeyboardMarkup] = None """*Optional*. Inline keyboard attached to the message. :code:`login_url` buttons are represented as ordinary :code:`url` buttons.""" - -class Message(_BaseMessage): @property def content_type(self) -> str: if self.text: diff --git a/aiogram/types/sent_web_app_message.py b/aiogram/types/sent_web_app_message.py index dca1e5b7..7295382c 100644 --- a/aiogram/types/sent_web_app_message.py +++ b/aiogram/types/sent_web_app_message.py @@ -1,12 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import Optional from .base import TelegramObject -if TYPE_CHECKING: - pass - class SentWebAppMessage(TelegramObject): """ diff --git a/aiogram/types/video_chat_scheduled.py b/aiogram/types/video_chat_scheduled.py index c523fbff..541e988a 100644 --- a/aiogram/types/video_chat_scheduled.py +++ b/aiogram/types/video_chat_scheduled.py @@ -1,13 +1,9 @@ from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING from .base import TelegramObject -if TYPE_CHECKING: - pass - class VideoChatScheduled(TelegramObject): """ diff --git a/aiogram/types/video_chat_started.py b/aiogram/types/video_chat_started.py index 595e889e..a6f9aed0 100644 --- a/aiogram/types/video_chat_started.py +++ b/aiogram/types/video_chat_started.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - from .base import TelegramObject -if TYPE_CHECKING: - pass - class VideoChatStarted(TelegramObject): """ diff --git a/aiogram/types/web_app_data.py b/aiogram/types/web_app_data.py index 5a4b5b0a..6a108fef 100644 --- a/aiogram/types/web_app_data.py +++ b/aiogram/types/web_app_data.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - from .base import TelegramObject -if TYPE_CHECKING: - pass - class WebAppData(TelegramObject): """ diff --git a/aiogram/types/web_app_info.py b/aiogram/types/web_app_info.py index f5d228f2..9317fae9 100644 --- a/aiogram/types/web_app_info.py +++ b/aiogram/types/web_app_info.py @@ -1,12 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - from .base import TelegramObject -if TYPE_CHECKING: - pass - class WebAppInfo(TelegramObject): """ From 5d513b682c706e92cffe6fd8255711ffd3137665 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 19 Apr 2022 21:28:17 +0300 Subject: [PATCH 7/9] Changed type of `last_synchronization_error_date` --- aiogram/types/webhook_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiogram/types/webhook_info.py b/aiogram/types/webhook_info.py index 7738588d..a3ec68e5 100644 --- a/aiogram/types/webhook_info.py +++ b/aiogram/types/webhook_info.py @@ -25,7 +25,7 @@ class WebhookInfo(TelegramObject): """*Optional*. Unix time for the most recent error that happened when trying to deliver an update via webhook""" last_error_message: Optional[str] = None """*Optional*. Error message in human-readable format for the most recent error that happened when trying to deliver an update via webhook""" - last_synchronization_error_date: Optional[int] = None + last_synchronization_error_date: Optional[datetime.datetime] = None """*Optional*. Unix time of the most recent error that happened when trying to synchronize available updates with Telegram datacenters""" max_connections: Optional[int] = None """*Optional*. Maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery""" From f0a0fdce998c0ef3d8f24d588362c9200bbafb59 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 19 Apr 2022 21:33:34 +0300 Subject: [PATCH 8/9] Fixed wrongly cleaned code --- aiogram/dispatcher/filters/text.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/aiogram/dispatcher/filters/text.py b/aiogram/dispatcher/filters/text.py index 09acb285..cd7195dd 100644 --- a/aiogram/dispatcher/filters/text.py +++ b/aiogram/dispatcher/filters/text.py @@ -1,12 +1,14 @@ -from typing import Any, Dict, Optional, Sequence, Union +from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence, Union from pydantic import root_validator from aiogram.dispatcher.filters import BaseFilter from aiogram.types import CallbackQuery, InlineQuery, Message, Poll -from aiogram.utils.i18n.lazy_proxy import LazyProxy -TextType = Union[str, LazyProxy] +if TYPE_CHECKING: + from aiogram.utils.i18n.lazy_proxy import LazyProxy # NOQA + +TextType = Union[str, "LazyProxy"] class Text(BaseFilter): From 108ff00741c9cc51a177cf9adcac5bf043af038c Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 19 Apr 2022 22:09:43 +0300 Subject: [PATCH 9/9] Release 3.0.0-beta.3 --- .apiversion | 2 +- CHANGES.rst | 39 +++++++++++++++++++++++++++++++++++++++ CHANGES/865.bugfix.rst | 1 - CHANGES/873.misc.rst | 1 - CHANGES/874.misc.rst | 2 -- CHANGES/876.bugfix | 1 - CHANGES/882.misc.rst | 1 - CHANGES/883.misc.rst | 3 --- CHANGES/885.bugfix.rst | 1 - CHANGES/889.feature.rst | 1 - CHANGES/890.feature.rst | 1 - Makefile | 10 ++++++---- README.rst | 6 +++--- aiogram/__init__.py | 4 ++-- pyproject.toml | 2 +- 15 files changed, 52 insertions(+), 23 deletions(-) delete mode 100644 CHANGES/865.bugfix.rst delete mode 100644 CHANGES/873.misc.rst delete mode 100644 CHANGES/874.misc.rst delete mode 100644 CHANGES/876.bugfix delete mode 100644 CHANGES/882.misc.rst delete mode 100644 CHANGES/883.misc.rst delete mode 100644 CHANGES/885.bugfix.rst delete mode 100644 CHANGES/889.feature.rst delete mode 100644 CHANGES/890.feature.rst diff --git a/.apiversion b/.apiversion index 760606e1..e0ea36fe 100644 --- a/.apiversion +++ b/.apiversion @@ -1 +1 @@ -5.7 +6.0 diff --git a/CHANGES.rst b/CHANGES.rst index 9216b305..66602c32 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,45 @@ Changelog .. towncrier release notes start +3.0.0b3 (2022-04-19) +===================== + +Features +-------- + +- Added possibility to get command magic result as handler argument + `#889 `_ +- Added full support of `Telegram Bot API 6.0 `_ + `#890 `_ + + +Bugfixes +-------- + +- Added parsing of spoiler message entity + `#865 `_ +- Fixed default `parse_mode` for `Message.copy_to()` method. + `#876 `_ +- Fixed CallbackData factory parsing IntEnum's + `#885 `_ + + +Misc +---- + +- Added automated check that pull-request adds a changes description to **CHANGES** directory + `#873 `_ +- Changed :code:`Message.html_text` and :code:`Message.md_text` attributes behaviour when message has no text. + The empty string will be used instead of raising error. + `#874 `_ +- Used `redis-py` instead of `aioredis` package in due to this packages was merged into single one + `#882 `_ +- Solved common naming problem with middlewares that confusing too much developers + - now you can't see the `middleware` and `middlewares` attributes at the same point + because this functionality encapsulated to special interface. + `#883 `_ + + 3.0.0b2 (2022-02-19) ===================== diff --git a/CHANGES/865.bugfix.rst b/CHANGES/865.bugfix.rst deleted file mode 100644 index bab93962..00000000 --- a/CHANGES/865.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Added parsing of spoiler message entity diff --git a/CHANGES/873.misc.rst b/CHANGES/873.misc.rst deleted file mode 100644 index 170dca4d..00000000 --- a/CHANGES/873.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Added automated check that pull-request adds a changes description to **CHANGES** directory diff --git a/CHANGES/874.misc.rst b/CHANGES/874.misc.rst deleted file mode 100644 index d167c1d8..00000000 --- a/CHANGES/874.misc.rst +++ /dev/null @@ -1,2 +0,0 @@ -Changed :code:`Message.html_text` and :code:`Message.md_text` attributes behaviour when message has no text. -The empty string will be used instead of raising error. diff --git a/CHANGES/876.bugfix b/CHANGES/876.bugfix deleted file mode 100644 index b4fded2e..00000000 --- a/CHANGES/876.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed default `parse_mode` for `Message.copy_to()` method. diff --git a/CHANGES/882.misc.rst b/CHANGES/882.misc.rst deleted file mode 100644 index ca8b0ba4..00000000 --- a/CHANGES/882.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Used `redis-py` instead of `aioredis` package in due to this packages was merged into single one diff --git a/CHANGES/883.misc.rst b/CHANGES/883.misc.rst deleted file mode 100644 index 8496c902..00000000 --- a/CHANGES/883.misc.rst +++ /dev/null @@ -1,3 +0,0 @@ -Solved common naming problem with middlewares that confusing too much developers -- now you can't see the `middleware` and `middlewares` attributes at the same point -because this functionality encapsulated to special interface. diff --git a/CHANGES/885.bugfix.rst b/CHANGES/885.bugfix.rst deleted file mode 100644 index 6d1bb095..00000000 --- a/CHANGES/885.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed CallbackData factory parsing IntEnum's diff --git a/CHANGES/889.feature.rst b/CHANGES/889.feature.rst deleted file mode 100644 index 8e58304c..00000000 --- a/CHANGES/889.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added possibility to get command magic result as handler argument diff --git a/CHANGES/890.feature.rst b/CHANGES/890.feature.rst deleted file mode 100644 index 10c60c05..00000000 --- a/CHANGES/890.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added full support of `Telegram Bot API 6.0 `_ diff --git a/Makefile b/Makefile index 8d9abab8..37a2b2b1 100644 --- a/Makefile +++ b/Makefile @@ -139,8 +139,10 @@ towncrier-draft-github: towncrier build --draft | pandoc - -o dist/release.md .PHONY: prepare-release -prepare-release: bump towncrier-draft-github towncrier-build +prepare-release: bump towncrier-build -.PHONY: tag-release -tag-release: - git tag v$(poetry version -s) +.PHONY: release +release: + git add . + git commit -m "Release $(shell poetry version -s)" + git tag v$(shell poetry version -s) diff --git a/README.rst b/README.rst index 99b88c07..df29b800 100644 --- a/README.rst +++ b/README.rst @@ -13,7 +13,7 @@ aiogram |beta badge| :target: https://pypi.python.org/pypi/aiogram :alt: Supported python versions -.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-5.7-blue.svg?logo=telegram +.. image:: https://img.shields.io/badge/Telegram%20Bot%20API-6.0-blue.svg?logo=telegram :target: https://core.telegram.org/bots/api :alt: Telegram Bot API @@ -71,8 +71,8 @@ Features .. warning:: - It is strongly advised that you have prior experience working - with `asyncio `_ + It is strongly advised that you have prior experience working + with `asyncio `_ before beginning to use **aiogram**. If you have any questions, you can visit our community chats on Telegram: diff --git a/aiogram/__init__.py b/aiogram/__init__.py index d734cb83..50973dd6 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -39,5 +39,5 @@ __all__ = ( "flags", ) -__version__ = "3.0.0b2" -__api_version__ = "5.7" +__version__ = "3.0.0b3" +__api_version__ = "6.0" diff --git a/pyproject.toml b/pyproject.toml index ba67b272..8e85e24d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aiogram" -version = "3.0.0-beta.2" +version = "3.0.0-beta.3" description = "Modern and fully asynchronous framework for Telegram Bot API" authors = [ "Alex Root Junior ",