From 497436595d4c77d765c009d6a7c1210f13b8fea5 Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Tue, 19 Apr 2022 22:03:24 +0300 Subject: [PATCH] [3.x] Bot API 6.0 (#890) * Base implementation * Bump license * Revert re-generated tests * Fix tests, improved docs * Remove TODO * Removed unreachable code * Changed type of `last_synchronization_error_date` * Fixed wrongly cleaned code --- .readthedocs.yml | 1 + CHANGES/890.feature.rst | 1 + LICENSE | 2 +- aiogram/client/bot.py | 120 +++++- aiogram/dispatcher/dispatcher.py | 12 +- aiogram/dispatcher/filters/text.py | 2 +- 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 | 32 +- 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 | 16 + aiogram/types/menu_button_default.py | 16 + aiogram/types/menu_button_web_app.py | 25 ++ aiogram/types/message.py | 55 +-- aiogram/types/sent_web_app_message.py | 16 + 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 | 16 + aiogram/types/video_chat_started.py | 11 + 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_data.py | 16 + aiogram/types/web_app_info.py | 14 + aiogram/types/webhook_info.py | 2 + aiogram/utils/web_app.py | 129 ++++++ 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 + ...scheduled.rst => menu_button_commands.rst} | 4 +- 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 + ...ce_chat_ended.rst => video_chat_ended.rst} | 4 +- ...st => video_chat_participants_invited.rst} | 4 +- docs/api/types/video_chat_scheduled.rst | 9 + ...hat_started.rst => video_chat_started.rst} | 4 +- docs/api/types/web_app_data.rst | 9 + docs/api/types/web_app_info.rst | 9 + docs/utils/index.rst | 1 + docs/utils/web_app.rst | 55 +++ 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 | 33 ++ .../test_approve_chat_join_request.py | 0 .../test_methods/test_ban_chat_sender_chat.py | 0 .../test_decline_chat_join_request.py | 0 .../test_methods/test_get_chat_menu_button.py | 27 ++ ...est_get_my_default_administrator_rights.py | 53 +++ .../test_methods/test_get_sticker_set.py | 4 + .../test_methods/test_send_sticker.py | 2 + .../test_methods/test_set_chat_menu_button.py | 26 ++ ...est_set_my_default_administrator_rights.py | 26 ++ .../test_unban_chat_sender_chat.py | 0 tests/test_api/test_types/test_message.py | 53 ++- tests/test_dispatcher/test_dispatcher.py | 4 +- .../test_filters/test_chat_member_updated.py | 2 +- tests/test_utils/test_web_app.py | 80 ++++ 81 files changed, 1942 insertions(+), 147 deletions(-) create mode 100644 CHANGES/890.feature.rst 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 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 create mode 100644 aiogram/types/web_app_data.py create mode 100644 aiogram/types/web_app_info.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 rename docs/api/types/{voice_chat_scheduled.rst => menu_button_commands.rst} (60%) 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 rename docs/api/types/{voice_chat_ended.rst => video_chat_ended.rst} (61%) rename docs/api/types/{voice_chat_participants_invited.rst => video_chat_participants_invited.rst} (58%) create mode 100644 docs/api/types/video_chat_scheduled.rst rename docs/api/types/{voice_chat_started.rst => video_chat_started.rst} (60%) create mode 100644 docs/api/types/web_app_data.rst create mode 100644 docs/api/types/web_app_info.rst create mode 100644 docs/utils/web_app.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 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/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 diff --git a/aiogram/client/bot.py b/aiogram/client/bot.py index 94feb7b8..16453574 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 @@ -1655,7 +1663,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 +2352,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 +2746,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 +2917,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/filters/text.py b/aiogram/dispatcher/filters/text.py index 3dd36fbd..cd7195dd 100644 --- a/aiogram/dispatcher/filters/text.py +++ b/aiogram/dispatcher/filters/text.py @@ -6,7 +6,7 @@ from aiogram.dispatcher.filters import BaseFilter from aiogram.types import CallbackQuery, InlineQuery, Message, Poll if TYPE_CHECKING: - from aiogram.utils.i18n.lazy_proxy import LazyProxy + from aiogram.utils.i18n.lazy_proxy import LazyProxy # NOQA TextType = Union[str, "LazyProxy"] 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..4fd1a0f2 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 + """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..ec04b855 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,14 @@ 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 +163,16 @@ __all__ = ( "Poll", "Location", "Venue", + "WebAppData", "ProximityAlertTriggered", "MessageAutoDeleteTimerChanged", - "VoiceChatScheduled", - "VoiceChatStarted", - "VoiceChatEnded", - "VoiceChatParticipantsInvited", + "VideoChatScheduled", + "VideoChatStarted", + "VideoChatEnded", + "VideoChatParticipantsInvited", "UserProfilePhotos", "File", + "WebAppInfo", "ReplyKeyboardMarkup", "KeyboardButton", "KeyboardButtonPollType", @@ -174,6 +184,7 @@ __all__ = ( "ForceReply", "ChatPhoto", "ChatInviteLink", + "ChatAdministratorRights", "ChatMember", "ChatMemberOwner", "ChatMemberAdministrator", @@ -194,6 +205,10 @@ __all__ = ( "BotCommandScopeChat", "BotCommandScopeChatAdministrators", "BotCommandScopeChatMember", + "MenuButton", + "MenuButtonCommands", + "MenuButtonWebApp", + "MenuButtonDefault", "ResponseParameters", "InputMedia", "InputMediaPhoto", @@ -234,6 +249,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..5f4e252b --- /dev/null +++ b/aiogram/types/menu_button_commands.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from pydantic import Field + +from . import MenuButton + + +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..13cd3a37 --- /dev/null +++ b/aiogram/types/menu_button_default.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from pydantic import Field + +from . import MenuButton + + +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..1fe8a4f5 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -63,15 +63,16 @@ 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): +class Message(TelegramObject): """ This object represents a message. @@ -184,19 +185,19 @@ 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.""" - -class Message(_BaseMessage): @property def content_type(self) -> str: if self.text: @@ -257,12 +258,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 +1904,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..7295382c --- /dev/null +++ b/aiogram/types/sent_web_app_message.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import Optional + +from .base import TelegramObject + + +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..541e988a --- /dev/null +++ b/aiogram/types/video_chat_scheduled.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from datetime import datetime + +from .base import TelegramObject + + +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: datetime + """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..a6f9aed0 --- /dev/null +++ b/aiogram/types/video_chat_started.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from .base import TelegramObject + + +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/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_data.py b/aiogram/types/web_app_data.py new file mode 100644 index 00000000..6a108fef --- /dev/null +++ b/aiogram/types/web_app_data.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from .base import TelegramObject + + +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..9317fae9 --- /dev/null +++ b/aiogram/types/web_app_info.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from .base import TelegramObject + + +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/webhook_info.py b/aiogram/types/webhook_info.py index 3b1a64a0..a3ec68e5 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[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""" 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..9cce8e06 --- /dev/null +++ b/aiogram/utils/web_app.py @@ -0,0 +1,129 @@ +import hashlib +import hmac +import json +from datetime import datetime +from operator import itemgetter +from typing import Any, Callable, Optional +from urllib.parse import parse_qsl + +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: + """ + Check incoming WebApp init data signature + + Source: https://core.telegram.org/bots/webapps#validating-data-received-via-the-web-app + + :param token: bot Token + :param init_data: data from frontend to be validated + :return: + """ + try: + 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: + # 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_webapp_init_data( + init_data: str, + *, + loads: Callable[..., Any] = json.loads, +) -> WebAppInitData: + """ + Parse WebApp init data and return it as WebAppInitData object + + 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 = {} + 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 raw WebApp init data and return it as WebAppInitData object + + 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_webapp_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/voice_chat_scheduled.rst b/docs/api/types/menu_button_commands.rst similarity index 60% rename from docs/api/types/voice_chat_scheduled.rst rename to docs/api/types/menu_button_commands.rst index e63936e2..66614b04 100644 --- a/docs/api/types/voice_chat_scheduled.rst +++ b/docs/api/types/menu_button_commands.rst @@ -1,9 +1,9 @@ ################## -VoiceChatScheduled +MenuButtonCommands ################## -.. automodule:: aiogram.types.voice_chat_scheduled +.. 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/voice_chat_ended.rst b/docs/api/types/video_chat_ended.rst similarity index 61% rename from docs/api/types/voice_chat_ended.rst rename to docs/api/types/video_chat_ended.rst index cce70b7c..aed8b7a9 100644 --- a/docs/api/types/voice_chat_ended.rst +++ b/docs/api/types/video_chat_ended.rst @@ -1,9 +1,9 @@ ############## -VoiceChatEnded +VideoChatEnded ############## -.. automodule:: aiogram.types.voice_chat_ended +.. automodule:: aiogram.types.video_chat_ended :members: :member-order: bysource :undoc-members: True diff --git a/docs/api/types/voice_chat_participants_invited.rst b/docs/api/types/video_chat_participants_invited.rst similarity index 58% rename from docs/api/types/voice_chat_participants_invited.rst rename to docs/api/types/video_chat_participants_invited.rst index 89a94fa9..9ca905bd 100644 --- a/docs/api/types/voice_chat_participants_invited.rst +++ b/docs/api/types/video_chat_participants_invited.rst @@ -1,9 +1,9 @@ ############################ -VoiceChatParticipantsInvited +VideoChatParticipantsInvited ############################ -.. automodule:: aiogram.types.voice_chat_participants_invited +.. 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/voice_chat_started.rst b/docs/api/types/video_chat_started.rst similarity index 60% rename from docs/api/types/voice_chat_started.rst rename to docs/api/types/video_chat_started.rst index c1a1964b..5d59a22e 100644 --- a/docs/api/types/voice_chat_started.rst +++ b/docs/api/types/video_chat_started.rst @@ -1,9 +1,9 @@ ################ -VoiceChatStarted +VideoChatStarted ################ -.. automodule:: aiogram.types.voice_chat_started +.. 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/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/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..8d9848da --- /dev/null +++ b/tests/test_api/test_methods/test_answer_web_app_query.py @@ -0,0 +1,33 @@ +import pytest + +from aiogram.methods import AnswerWebAppQuery, Request +from aiogram.types import InlineQueryResult, SentWebAppMessage +from tests.mocked_bot import MockedBot + + +class TestAnswerWebAppQuery: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(AnswerWebAppQuery, ok=True, result=SentWebAppMessage()) + + response: SentWebAppMessage = await AnswerWebAppQuery( + web_app_query_id="test", + result=InlineQueryResult(), + ) + 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=SentWebAppMessage()) + + response: SentWebAppMessage = await bot.answer_web_app_query( + web_app_query_id="test", + result=InlineQueryResult(), + ) + 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 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 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 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..a7c2fd37 --- /dev/null +++ b/tests/test_api/test_methods/test_get_chat_menu_button.py @@ -0,0 +1,27 @@ +import pytest + +from aiogram.methods import GetChatMenuButton, Request +from aiogram.types import MenuButton, MenuButtonDefault +from tests.mocked_bot import MockedBot + + +class TestGetChatMenuButton: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(GetChatMenuButton, ok=True, result=MenuButtonDefault()) + + 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=MenuButtonDefault()) + + 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..179b468d --- /dev/null +++ b/tests/test_api/test_methods/test_get_my_default_administrator_rights.py @@ -0,0 +1,53 @@ +import pytest + +from aiogram.methods import GetMyDefaultAdministratorRights, Request +from aiogram.types import ChatAdministratorRights +from tests.mocked_bot import MockedBot + + +class TestGetMyDefaultAdministratorRights: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + 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() + 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=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() + assert request.method == "getMyDefaultAdministratorRights" + # assert request.data == {} + assert response == prepare_result.result 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 new file mode 100644 index 00000000..97e2fa90 --- /dev/null +++ b/tests/test_api/test_methods/test_set_chat_menu_button.py @@ -0,0 +1,26 @@ +import pytest + +from aiogram.methods import Request, SetChatMenuButton +from tests.mocked_bot import MockedBot + + +class TestSetChatMenuButton: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(SetChatMenuButton, ok=True, result=True) + + 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=True) + + 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..4bd08822 --- /dev/null +++ b/tests/test_api/test_methods/test_set_my_default_administrator_rights.py @@ -0,0 +1,26 @@ +import pytest + +from aiogram.methods import Request, SetMyDefaultAdministratorRights +from tests.mocked_bot import MockedBot + + +class TestSetMyDefaultAdministratorRights: + @pytest.mark.asyncio + async def test_method(self, bot: MockedBot): + prepare_result = bot.add_result_for(SetMyDefaultAdministratorRights, ok=True, result=True) + + 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=True) + + 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 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_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 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", + )