diff --git a/.readthedocs.yml b/.readthedocs.yml index 1efe11cb..e03323e6 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -12,3 +12,4 @@ python: path: . extra_requirements: - docs + - redis diff --git a/CHANGES/890.feature.rst b/CHANGES/890.feature.rst new file mode 100644 index 00000000..10c60c05 --- /dev/null +++ b/CHANGES/890.feature.rst @@ -0,0 +1 @@ +Added full support of `Telegram Bot API 6.0 `_ diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index ef41a85b..ec04b855 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -130,10 +130,6 @@ from .video_chat_scheduled import VideoChatScheduled from .video_chat_started import VideoChatStarted from .video_note import VideoNote from .voice import Voice -from .voice_chat_ended import VoiceChatEnded -from .voice_chat_participants_invited import VoiceChatParticipantsInvited -from .voice_chat_scheduled import VoiceChatScheduled -from .voice_chat_started import VoiceChatStarted from .web_app_data import WebAppData from .web_app_info import WebAppInfo from .webhook_info import WebhookInfo @@ -170,10 +166,6 @@ __all__ = ( "WebAppData", "ProximityAlertTriggered", "MessageAutoDeleteTimerChanged", - "VoiceChatScheduled", - "VoiceChatStarted", - "VoiceChatEnded", - "VoiceChatParticipantsInvited", "VideoChatScheduled", "VideoChatStarted", "VideoChatEnded", diff --git a/aiogram/types/video_chat_scheduled.py b/aiogram/types/video_chat_scheduled.py index 1e41c6d4..c523fbff 100644 --- a/aiogram/types/video_chat_scheduled.py +++ b/aiogram/types/video_chat_scheduled.py @@ -1,5 +1,6 @@ from __future__ import annotations +from datetime import datetime from typing import TYPE_CHECKING from .base import TelegramObject @@ -15,5 +16,5 @@ class VideoChatScheduled(TelegramObject): Source: https://core.telegram.org/bots/api#videochatscheduled """ - start_date: int + start_date: datetime """Point in time (Unix timestamp) when the video chat is supposed to be started by a chat administrator""" diff --git a/aiogram/types/voice_chat_ended.py b/aiogram/types/voice_chat_ended.py deleted file mode 100644 index 12c705c9..00000000 --- a/aiogram/types/voice_chat_ended.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -from .base import TelegramObject - - -class VoiceChatEnded(TelegramObject): - """ - This object represents a service message about a voice chat ended in the chat. - - Source: https://core.telegram.org/bots/api#voicechatended - """ - - duration: int - """Voice chat duration in seconds""" diff --git a/aiogram/types/voice_chat_participants_invited.py b/aiogram/types/voice_chat_participants_invited.py deleted file mode 100644 index b24ef91d..00000000 --- a/aiogram/types/voice_chat_participants_invited.py +++ /dev/null @@ -1,19 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, List, Optional - -from .base import TelegramObject - -if TYPE_CHECKING: - from .user import User - - -class VoiceChatParticipantsInvited(TelegramObject): - """ - This object represents a service message about new members invited to a voice chat. - - Source: https://core.telegram.org/bots/api#voicechatparticipantsinvited - """ - - users: Optional[List[User]] = None - """*Optional*. New members that were invited to the voice chat""" diff --git a/aiogram/types/voice_chat_scheduled.py b/aiogram/types/voice_chat_scheduled.py deleted file mode 100644 index 37c6c7bd..00000000 --- a/aiogram/types/voice_chat_scheduled.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -from .base import TelegramObject - - -class VoiceChatScheduled(TelegramObject): - """ - This object represents a service message about a voice chat scheduled in the chat. - - Source: https://core.telegram.org/bots/api#voicechatscheduled - """ - - start_date: int - """Point in time (Unix timestamp) when the voice chat is supposed to be started by a chat administrator""" diff --git a/aiogram/types/voice_chat_started.py b/aiogram/types/voice_chat_started.py deleted file mode 100644 index 6ad45263..00000000 --- a/aiogram/types/voice_chat_started.py +++ /dev/null @@ -1,11 +0,0 @@ -from __future__ import annotations - -from .base import TelegramObject - - -class VoiceChatStarted(TelegramObject): - """ - This object represents a service message about a voice chat started in the chat. Currently holds no information. - - Source: https://core.telegram.org/bots/api#voicechatstarted - """ diff --git a/aiogram/types/web_app_init_data.py b/aiogram/types/web_app_init_data.py deleted file mode 100644 index e27b12af..00000000 --- a/aiogram/types/web_app_init_data.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Optional - -from aiogram.types import TelegramObject -from aiogram.types.web_app_user import WebAppUser - - -class WebAppInitData(TelegramObject): - query_id: Optional[str] = None - """Optional. A unique identifier for the Web App session, required for sending messages via the answerWebAppQuery method.""" - user: Optional[WebAppUser] = None - """Optional. An object containing data about the current user.""" - receiver: Optional[WebAppUser] = None - """Optional. An object containing data about the chat partner of the current user in the chat where the bot was launched via the attachment menu. Returned only for Web Apps launched via the attachment menu.""" - start_param: Optional[str] = None - """Optional. The value of the startattach parameter, passed via link. Only returned for Web Apps when launched from the attachment menu via link. The value of the start_param parameter will also be passed in the GET-parameter tgWebAppStartParam, so the Web App can load the correct interface right away.""" - auth_date: int - """Unix time when the form was opened.""" - hash: str - """A hash of all passed parameters, which the bot server can use to check their validity.""" diff --git a/aiogram/types/web_app_user.py b/aiogram/types/web_app_user.py deleted file mode 100644 index e5436adf..00000000 --- a/aiogram/types/web_app_user.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Optional - -from aiogram.types import TelegramObject - - -class WebAppUser(TelegramObject): - """ - This object contains the data of the Web App user. - - Source: https://core.telegram.org/bots/webapps#webappuser - """ - - id: int - """A unique identifier for the user or bot. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. It has at most 52 significant bits, so a 64-bit integer or a double-precision float type is safe for storing this identifier.""" - is_bot: Optional[bool] = None - """Optional. True, if this user is a bot. Returns in the receiver field only.""" - first_name: str - """First name of the user or bot.""" - last_name: Optional[str] = None - """Optional. Last name of the user or bot.""" - username: Optional[str] = None - """Optional. Username of the user or bot.""" - language_code: Optional[str] = None - """Optional. IETF language tag of the user's language. Returns in user field only.""" - photo_url: Optional[str] = None - """Optional. URL of the user’s profile photo. The photo can be in .jpeg or .svg formats. Only returned for Web Apps launched from the attachment menu.""" diff --git a/aiogram/utils/web_app.py b/aiogram/utils/web_app.py index bb26830e..9cce8e06 100644 --- a/aiogram/utils/web_app.py +++ b/aiogram/utils/web_app.py @@ -1,11 +1,56 @@ import hashlib import hmac import json +from datetime import datetime from operator import itemgetter -from typing import Any, Callable +from typing import Any, Callable, Optional from urllib.parse import parse_qsl -from aiogram.types.web_app_init_data import WebAppInitData +from aiogram.types import TelegramObject + + +class WebAppUser(TelegramObject): + """ + This object contains the data of the Web App user. + + Source: https://core.telegram.org/bots/webapps#webappuser + """ + + id: int + """A unique identifier for the user or bot. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. It has at most 52 significant bits, so a 64-bit integer or a double-precision float type is safe for storing this identifier.""" + is_bot: Optional[bool] = None + """True, if this user is a bot. Returns in the receiver field only.""" + first_name: str + """First name of the user or bot.""" + last_name: Optional[str] = None + """Last name of the user or bot.""" + username: Optional[str] = None + """Username of the user or bot.""" + language_code: Optional[str] = None + """IETF language tag of the user's language. Returns in user field only.""" + photo_url: Optional[str] = None + """URL of the user’s profile photo. The photo can be in .jpeg or .svg formats. Only returned for Web Apps launched from the attachment menu.""" + + +class WebAppInitData(TelegramObject): + """ + This object contains data that is transferred to the Web App when it is opened. It is empty if the Web App was launched from a keyboard button. + + Source: https://core.telegram.org/bots/webapps#webappinitdata + """ + + query_id: Optional[str] = None + """A unique identifier for the Web App session, required for sending messages via the answerWebAppQuery method.""" + user: Optional[WebAppUser] = None + """An object containing data about the current user.""" + receiver: Optional[WebAppUser] = None + """An object containing data about the chat partner of the current user in the chat where the bot was launched via the attachment menu. Returned only for Web Apps launched via the attachment menu.""" + start_param: Optional[str] = None + """The value of the startattach parameter, passed via link. Only returned for Web Apps when launched from the attachment menu via link. The value of the start_param parameter will also be passed in the GET-parameter tgWebAppStartParam, so the Web App can load the correct interface right away.""" + auth_date: datetime + """Unix time when the form was opened.""" + hash: str + """A hash of all passed parameters, which the bot server can use to check their validity.""" def check_webapp_signature(token: str, init_data: str) -> bool: @@ -14,13 +59,13 @@ def check_webapp_signature(token: str, init_data: str) -> bool: Source: https://core.telegram.org/bots/webapps#validating-data-received-via-the-web-app - :param token: - :param init_data: + :param token: bot Token + :param init_data: data from frontend to be validated :return: """ try: - parsed_data = dict(parse_qsl(init_data)) - except ValueError: + parsed_data = dict(parse_qsl(init_data, strict_parsing=True)) + except ValueError: # pragma: no cover # Init data is not a valid query string return False if "hash" not in parsed_data: @@ -38,16 +83,19 @@ def check_webapp_signature(token: str, init_data: str) -> bool: return calculated_hash == hash_ -def parse_init_data( +def parse_webapp_init_data( init_data: str, *, - _loads: Callable[..., Any] = json.loads, + loads: Callable[..., Any] = json.loads, ) -> WebAppInitData: """ - Parse WebApp init data and return it as dict + Parse WebApp init data and return it as WebAppInitData object - :param init_data: - :param _loads: + This method doesn't make any security check, so you shall not trust to this data, + use :code:`safe_parse_webapp_init_data` instead. + + :param init_data: data from frontend to be parsed + :param loads: :return: """ result = {} @@ -55,7 +103,7 @@ def parse_init_data( if (value.startswith("[") and value.endswith("]")) or ( value.startswith("{") and value.endswith("}") ): - value = _loads(value) + value = loads(value) result[key] = value return WebAppInitData(**result) @@ -64,16 +112,18 @@ def safe_parse_webapp_init_data( token: str, init_data: str, *, - _loads: Callable[..., Any] = json.loads, + loads: Callable[..., Any] = json.loads, ) -> WebAppInitData: """ - Validate WebApp init data and return it as dict + Validate raw WebApp init data and return it as WebAppInitData object - :param token: - :param init_data: - :param _loads: + Raise :type:`ValueError` when data is invalid + + :param token: bot token + :param init_data: data from frontend to be parsed and validated + :param loads: :return: """ if check_webapp_signature(token, init_data): - return parse_init_data(init_data, _loads=_loads) + return parse_webapp_init_data(init_data, loads=loads) raise ValueError("Invalid init data signature") diff --git a/docs/api/types/voice_chat_ended.rst b/docs/api/types/voice_chat_ended.rst deleted file mode 100644 index cce70b7c..00000000 --- a/docs/api/types/voice_chat_ended.rst +++ /dev/null @@ -1,9 +0,0 @@ -############## -VoiceChatEnded -############## - - -.. automodule:: aiogram.types.voice_chat_ended - :members: - :member-order: bysource - :undoc-members: True diff --git a/docs/api/types/voice_chat_participants_invited.rst b/docs/api/types/voice_chat_participants_invited.rst deleted file mode 100644 index 89a94fa9..00000000 --- a/docs/api/types/voice_chat_participants_invited.rst +++ /dev/null @@ -1,9 +0,0 @@ -############################ -VoiceChatParticipantsInvited -############################ - - -.. automodule:: aiogram.types.voice_chat_participants_invited - :members: - :member-order: bysource - :undoc-members: True diff --git a/docs/api/types/voice_chat_scheduled.rst b/docs/api/types/voice_chat_scheduled.rst deleted file mode 100644 index e63936e2..00000000 --- a/docs/api/types/voice_chat_scheduled.rst +++ /dev/null @@ -1,9 +0,0 @@ -################## -VoiceChatScheduled -################## - - -.. automodule:: aiogram.types.voice_chat_scheduled - :members: - :member-order: bysource - :undoc-members: True diff --git a/docs/api/types/voice_chat_started.rst b/docs/api/types/voice_chat_started.rst deleted file mode 100644 index c1a1964b..00000000 --- a/docs/api/types/voice_chat_started.rst +++ /dev/null @@ -1,9 +0,0 @@ -################ -VoiceChatStarted -################ - - -.. automodule:: aiogram.types.voice_chat_started - :members: - :member-order: bysource - :undoc-members: True diff --git a/docs/utils/index.rst b/docs/utils/index.rst index 4f6eccf5..a7a8474d 100644 --- a/docs/utils/index.rst +++ b/docs/utils/index.rst @@ -7,3 +7,4 @@ Utils keyboard i18n chat_action + web_app diff --git a/docs/utils/web_app.rst b/docs/utils/web_app.rst new file mode 100644 index 00000000..82e9cc42 --- /dev/null +++ b/docs/utils/web_app.rst @@ -0,0 +1,55 @@ +====== +WebApз +====== + +Telegram Bot API 6.0 announces a revolution in the development of chatbots using WebApp feature. + +You can read more details on it in the official `blog `_ +and `documentation `_. + +`aiogram` implements simple utils to remove headache with the data validation from Telegram WebApp on the backend side. + +Usage +===== + +For example from frontend you will pass :code:`application/x-www-form-urlencoded` POST request +with :code:`_auth` field in body and wants to return User info inside response as :code:`application/json` + +.. code-block:: python + + from aiogram.utils.web_app import safe_parse_webapp_init_data + from aiohttp.web_request import Request + from aiohttp.web_response import json_response + + async def check_data_handler(request: Request): + bot: Bot = request.app["bot"] + + data = await request.post() # application/x-www-form-urlencoded + try: + data = safe_parse_webapp_init_data(token=bot.token, init_data=data["_auth"]) + except ValueError: + return json_response({"ok": False, "err": "Unauthorized"}, status=401) + return json_response({"ok": True, "data": data.user.dict()}) + +Functions +========= + +.. autofunction:: aiogram.utils.web_app.check_webapp_signature + +.. autofunction:: aiogram.utils.web_app.parse_webapp_init_data + +.. autofunction:: aiogram.utils.web_app.safe_parse_webapp_init_data + + +Types +===== + +.. autoclass:: aiogram.utils.web_app.WebAppInitData + :members: + :member-order: bysource + :undoc-members: True + +.. autoclass:: aiogram.utils.web_app.WebAppUser + :members: + :member-order: bysource + :undoc-members: True diff --git a/tests/test_api/test_methods/test_answer_web_app_query.py b/tests/test_api/test_methods/test_answer_web_app_query.py index c8865be2..8d9848da 100644 --- a/tests/test_api/test_methods/test_answer_web_app_query.py +++ b/tests/test_api/test_methods/test_answer_web_app_query.py @@ -1,19 +1,18 @@ import pytest from aiogram.methods import AnswerWebAppQuery, Request -from aiogram.types import SentWebAppMessage +from aiogram.types import InlineQueryResult, SentWebAppMessage from tests.mocked_bot import MockedBot -@pytest.mark.skip class TestAnswerWebAppQuery: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(AnswerWebAppQuery, ok=True, result=None) + prepare_result = bot.add_result_for(AnswerWebAppQuery, ok=True, result=SentWebAppMessage()) response: SentWebAppMessage = await AnswerWebAppQuery( - web_app_query_id=..., - result=..., + web_app_query_id="test", + result=InlineQueryResult(), ) request: Request = bot.get_request() assert request.method == "answerWebAppQuery" @@ -22,11 +21,11 @@ class TestAnswerWebAppQuery: @pytest.mark.asyncio async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(AnswerWebAppQuery, ok=True, result=None) + prepare_result = bot.add_result_for(AnswerWebAppQuery, ok=True, result=SentWebAppMessage()) response: SentWebAppMessage = await bot.answer_web_app_query( - web_app_query_id=..., - result=..., + web_app_query_id="test", + result=InlineQueryResult(), ) request: Request = bot.get_request() assert request.method == "answerWebAppQuery" diff --git a/tests/test_api/test_methods/test_get_chat_menu_button.py b/tests/test_api/test_methods/test_get_chat_menu_button.py index 2b7803f4..a7c2fd37 100644 --- a/tests/test_api/test_methods/test_get_chat_menu_button.py +++ b/tests/test_api/test_methods/test_get_chat_menu_button.py @@ -1,15 +1,14 @@ import pytest from aiogram.methods import GetChatMenuButton, Request -from aiogram.types import MenuButton +from aiogram.types import MenuButton, MenuButtonDefault from tests.mocked_bot import MockedBot -@pytest.mark.skip class TestGetChatMenuButton: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(GetChatMenuButton, ok=True, result=None) + prepare_result = bot.add_result_for(GetChatMenuButton, ok=True, result=MenuButtonDefault()) response: MenuButton = await GetChatMenuButton() request: Request = bot.get_request() @@ -19,7 +18,7 @@ class TestGetChatMenuButton: @pytest.mark.asyncio async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(GetChatMenuButton, ok=True, result=None) + prepare_result = bot.add_result_for(GetChatMenuButton, ok=True, result=MenuButtonDefault()) response: MenuButton = await bot.get_chat_menu_button() request: Request = bot.get_request() diff --git a/tests/test_api/test_methods/test_get_my_default_administrator_rights.py b/tests/test_api/test_methods/test_get_my_default_administrator_rights.py index 9b633382..179b468d 100644 --- a/tests/test_api/test_methods/test_get_my_default_administrator_rights.py +++ b/tests/test_api/test_methods/test_get_my_default_administrator_rights.py @@ -5,11 +5,23 @@ from aiogram.types import ChatAdministratorRights from tests.mocked_bot import MockedBot -@pytest.mark.skip class TestGetMyDefaultAdministratorRights: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(GetMyDefaultAdministratorRights, ok=True, result=None) + prepare_result = bot.add_result_for( + GetMyDefaultAdministratorRights, + ok=True, + result=ChatAdministratorRights( + is_anonymous=False, + can_manage_chat=False, + can_delete_messages=False, + can_manage_video_chats=False, + can_restrict_members=False, + can_promote_members=False, + can_change_info=False, + can_invite_users=False, + ), + ) response: ChatAdministratorRights = await GetMyDefaultAdministratorRights() request: Request = bot.get_request() @@ -19,7 +31,20 @@ class TestGetMyDefaultAdministratorRights: @pytest.mark.asyncio async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(GetMyDefaultAdministratorRights, ok=True, result=None) + prepare_result = bot.add_result_for( + GetMyDefaultAdministratorRights, + ok=True, + result=ChatAdministratorRights( + is_anonymous=False, + can_manage_chat=False, + can_delete_messages=False, + can_manage_video_chats=False, + can_restrict_members=False, + can_promote_members=False, + can_change_info=False, + can_invite_users=False, + ), + ) response: ChatAdministratorRights = await bot.get_my_default_administrator_rights() request: Request = bot.get_request() diff --git a/tests/test_api/test_methods/test_get_sticker_set.py b/tests/test_api/test_methods/test_get_sticker_set.py index baed1d40..d778f1f7 100644 --- a/tests/test_api/test_methods/test_get_sticker_set.py +++ b/tests/test_api/test_methods/test_get_sticker_set.py @@ -16,6 +16,7 @@ class TestGetStickerSet: name="test", title="test", is_animated=False, + is_video=False, contains_masks=False, stickers=[ Sticker( @@ -23,6 +24,7 @@ class TestGetStickerSet: width=42, height=42, is_animated=False, + is_video=False, file_unique_id="file id", ) ], @@ -42,6 +44,7 @@ class TestGetStickerSet: name="test", title="test", is_animated=False, + is_video=False, contains_masks=False, stickers=[ Sticker( @@ -49,6 +52,7 @@ class TestGetStickerSet: width=42, height=42, is_animated=False, + is_video=False, file_unique_id="file id", ) ], diff --git a/tests/test_api/test_methods/test_send_sticker.py b/tests/test_api/test_methods/test_send_sticker.py index d356e8ae..239065eb 100644 --- a/tests/test_api/test_methods/test_send_sticker.py +++ b/tests/test_api/test_methods/test_send_sticker.py @@ -22,6 +22,7 @@ class TestSendSticker: width=42, height=42, is_animated=False, + is_video=False, file_unique_id="file id", ), chat=Chat(id=42, type="private"), @@ -45,6 +46,7 @@ class TestSendSticker: width=42, height=42, is_animated=False, + is_video=False, file_unique_id="file id", ), chat=Chat(id=42, type="private"), diff --git a/tests/test_api/test_methods/test_set_chat_menu_button.py b/tests/test_api/test_methods/test_set_chat_menu_button.py index d63bf4a1..97e2fa90 100644 --- a/tests/test_api/test_methods/test_set_chat_menu_button.py +++ b/tests/test_api/test_methods/test_set_chat_menu_button.py @@ -1,14 +1,13 @@ import pytest -from aiogram.api.methods import Request, SetChatMenuButton +from aiogram.methods import Request, SetChatMenuButton from tests.mocked_bot import MockedBot -@pytest.mark.skip class TestSetChatMenuButton: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(SetChatMenuButton, ok=True, result=None) + prepare_result = bot.add_result_for(SetChatMenuButton, ok=True, result=True) response: bool = await SetChatMenuButton() request: Request = bot.get_request() @@ -18,7 +17,7 @@ class TestSetChatMenuButton: @pytest.mark.asyncio async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(SetChatMenuButton, ok=True, result=None) + prepare_result = bot.add_result_for(SetChatMenuButton, ok=True, result=True) response: bool = await bot.set_chat_menu_button() request: Request = bot.get_request() diff --git a/tests/test_api/test_methods/test_set_my_default_administrator_rights.py b/tests/test_api/test_methods/test_set_my_default_administrator_rights.py index c064fdc2..4bd08822 100644 --- a/tests/test_api/test_methods/test_set_my_default_administrator_rights.py +++ b/tests/test_api/test_methods/test_set_my_default_administrator_rights.py @@ -1,14 +1,13 @@ import pytest -from aiogram.api.methods import Request, SetMyDefaultAdministratorRights +from aiogram.methods import Request, SetMyDefaultAdministratorRights from tests.mocked_bot import MockedBot -@pytest.mark.skip class TestSetMyDefaultAdministratorRights: @pytest.mark.asyncio async def test_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(SetMyDefaultAdministratorRights, ok=True, result=None) + prepare_result = bot.add_result_for(SetMyDefaultAdministratorRights, ok=True, result=True) response: bool = await SetMyDefaultAdministratorRights() request: Request = bot.get_request() @@ -18,7 +17,7 @@ class TestSetMyDefaultAdministratorRights: @pytest.mark.asyncio async def test_bot_method(self, bot: MockedBot): - prepare_result = bot.add_result_for(SetMyDefaultAdministratorRights, ok=True, result=None) + prepare_result = bot.add_result_for(SetMyDefaultAdministratorRights, ok=True, result=True) response: bool = await bot.set_my_default_administrator_rights() request: Request = bot.get_request() diff --git a/tests/test_api/test_types/test_message.py b/tests/test_api/test_types/test_message.py index b2b66b77..fcf357c4 100644 --- a/tests/test_api/test_types/test_message.py +++ b/tests/test_api/test_types/test_message.py @@ -52,11 +52,13 @@ from aiogram.types import ( User, Venue, Video, + VideoChatEnded, + VideoChatParticipantsInvited, + VideoChatScheduled, + VideoChatStarted, VideoNote, Voice, - VoiceChatEnded, - VoiceChatParticipantsInvited, - VoiceChatStarted, + WebAppData, ) from aiogram.types.message import ContentType, Message @@ -122,6 +124,7 @@ TEST_MESSAGE_STICKER = Message( width=42, height=42, is_animated=False, + is_video=False, ), chat=Chat(id=42, type="private"), from_user=User(id=42, is_bot=False, first_name="Test"), @@ -318,29 +321,38 @@ TEST_MESSAGE_MESSAGE_AUTO_DELETE_TIMER_CHANGED = Message( message_auto_delete_timer_changed=MessageAutoDeleteTimerChanged(message_auto_delete_time=42), from_user=User(id=42, is_bot=False, first_name="Test"), ) -TEST_MESSAGE_VOICE_CHAT_STARTED = Message( +TEST_MESSAGE_VIDEO_CHAT_STARTED = Message( message_id=42, date=datetime.datetime.now(), chat=Chat(id=42, type="private"), from_user=User(id=42, is_bot=False, first_name="Test"), - voice_chat_started=VoiceChatStarted(), + video_chat_started=VideoChatStarted(), ) -TEST_MESSAGE_VOICE_CHAT_ENDED = Message( +TEST_MESSAGE_VIDEO_CHAT_ENDED = Message( message_id=42, date=datetime.datetime.now(), chat=Chat(id=42, type="private"), from_user=User(id=42, is_bot=False, first_name="Test"), - voice_chat_ended=VoiceChatEnded(duration=42), + video_chat_ended=VideoChatEnded(duration=42), ) -TEST_MESSAGE_VOICE_CHAT_PARTICIPANTS_INVITED = Message( +TEST_MESSAGE_VIDEO_CHAT_PARTICIPANTS_INVITED = Message( message_id=42, date=datetime.datetime.now(), chat=Chat(id=42, type="private"), from_user=User(id=42, is_bot=False, first_name="Test"), - voice_chat_participants_invited=VoiceChatParticipantsInvited( + video_chat_participants_invited=VideoChatParticipantsInvited( users=[User(id=69, is_bot=False, first_name="Test")] ), ) +TEST_MESSAGE_VIDEO_CHAT_SCHEDULED = Message( + message_id=42, + date=datetime.datetime.now(), + chat=Chat(id=42, type="private"), + from_user=User(id=42, is_bot=False, first_name="Test"), + video_chat_scheduled=VideoChatScheduled( + start_date=datetime.datetime.now(), + ), +) TEST_MESSAGE_DICE = Message( message_id=42, date=datetime.datetime.now(), @@ -348,6 +360,13 @@ TEST_MESSAGE_DICE = Message( dice=Dice(value=6, emoji="X"), from_user=User(id=42, is_bot=False, first_name="Test"), ) +TEST_MESSAGE_WEB_APP_DATA = Message( + message_id=42, + date=datetime.datetime.now(), + chat=Chat(id=42, type="private"), + web_app_data=WebAppData(data="test", button_text="Test"), + from_user=User(id=42, is_bot=False, first_name="Test"), +) TEST_MESSAGE_UNKNOWN = Message( message_id=42, date=datetime.datetime.now(), @@ -391,13 +410,15 @@ class TestMessage: TEST_MESSAGE_MESSAGE_AUTO_DELETE_TIMER_CHANGED, ContentType.MESSAGE_AUTO_DELETE_TIMER_CHANGED, ], - [TEST_MESSAGE_VOICE_CHAT_STARTED, ContentType.VOICE_CHAT_STARTED], - [TEST_MESSAGE_VOICE_CHAT_ENDED, ContentType.VOICE_CHAT_ENDED], + [TEST_MESSAGE_VIDEO_CHAT_SCHEDULED, ContentType.VIDEO_CHAT_SCHEDULED], + [TEST_MESSAGE_VIDEO_CHAT_STARTED, ContentType.VIDEO_CHAT_STARTED], + [TEST_MESSAGE_VIDEO_CHAT_ENDED, ContentType.VIDEO_CHAT_ENDED], [ - TEST_MESSAGE_VOICE_CHAT_PARTICIPANTS_INVITED, - ContentType.VOICE_CHAT_PARTICIPANTS_INVITED, + TEST_MESSAGE_VIDEO_CHAT_PARTICIPANTS_INVITED, + ContentType.VIDEO_CHAT_PARTICIPANTS_INVITED, ], [TEST_MESSAGE_DICE, ContentType.DICE], + [TEST_MESSAGE_WEB_APP_DATA, ContentType.WEB_APP_DATA], [TEST_MESSAGE_UNKNOWN, ContentType.UNKNOWN], ], ) @@ -535,9 +556,9 @@ class TestMessage: [TEST_MESSAGE_PASSPORT_DATA, None], [TEST_MESSAGE_POLL, SendPoll], [TEST_MESSAGE_MESSAGE_AUTO_DELETE_TIMER_CHANGED, None], - [TEST_MESSAGE_VOICE_CHAT_STARTED, None], - [TEST_MESSAGE_VOICE_CHAT_ENDED, None], - [TEST_MESSAGE_VOICE_CHAT_PARTICIPANTS_INVITED, None], + [TEST_MESSAGE_VIDEO_CHAT_STARTED, None], + [TEST_MESSAGE_VIDEO_CHAT_ENDED, None], + [TEST_MESSAGE_VIDEO_CHAT_PARTICIPANTS_INVITED, None], [TEST_MESSAGE_DICE, SendDice], [TEST_MESSAGE_UNKNOWN, None], ], diff --git a/tests/test_dispatcher/test_filters/test_chat_member_updated.py b/tests/test_dispatcher/test_filters/test_chat_member_updated.py index 63ee1245..dae0e985 100644 --- a/tests/test_dispatcher/test_filters/test_chat_member_updated.py +++ b/tests/test_dispatcher/test_filters/test_chat_member_updated.py @@ -320,7 +320,7 @@ class TestChatMemberUpdatedStatusFilter: "can_be_edited": True, "can_manage_chat": True, "can_delete_messages": True, - "can_manage_voice_chats": True, + "can_manage_video_chats": True, "can_restrict_members": True, "can_promote_members": True, "can_change_info": True, diff --git a/tests/test_utils/test_web_app.py b/tests/test_utils/test_web_app.py new file mode 100644 index 00000000..abebd909 --- /dev/null +++ b/tests/test_utils/test_web_app.py @@ -0,0 +1,80 @@ +import pytest + +from aiogram.utils.web_app import ( + WebAppInitData, + check_webapp_signature, + parse_webapp_init_data, + safe_parse_webapp_init_data, +) + + +class TestWebApp: + @pytest.mark.parametrize( + "token,case,result", + [ + [ + "42:TEST", + "auth_date=1650385342" + "&user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D" + "&query_id=test" + "&hash=46d2ea5e32911ec8d30999b56247654460c0d20949b6277af519e76271182803", + True, + ], + [ + "42:INVALID", + "auth_date=1650385342" + "&user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D" + "&query_id=test" + "&hash=46d2ea5e32911ec8d30999b56247654460c0d20949b6277af519e76271182803", + False, + ], + [ + "42:TEST", + "user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D&query_id=test&hash=test", + False, + ], + [ + "42:TEST", + "user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D&query_id=test", + False, + ], + ["42:TEST", "", False], + ["42:TEST", "test&foo=bar=baz", False], + ], + ) + def test_check_webapp_signature(self, token, case, result): + assert check_webapp_signature(token, case) is result + + def test_parse_web_app_init_data(self): + parsed = parse_webapp_init_data( + "auth_date=1650385342" + "&user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D" + "&query_id=test" + "&hash=46d2ea5e32911ec8d30999b56247654460c0d20949b6277af519e76271182803", + ) + assert isinstance(parsed, WebAppInitData) + assert parsed.user + assert parsed.user.first_name == "Test" + assert parsed.user.id == 42 + assert parsed.query_id == "test" + assert parsed.hash == "46d2ea5e32911ec8d30999b56247654460c0d20949b6277af519e76271182803" + assert parsed.auth_date.year == 2022 + + def test_valid_safe_parse_webapp_init_data(self): + assert safe_parse_webapp_init_data( + "42:TEST", + "auth_date=1650385342" + "&user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D" + "&query_id=test" + "&hash=46d2ea5e32911ec8d30999b56247654460c0d20949b6277af519e76271182803", + ) + + def test_invalid_safe_parse_webapp_init_data(self): + with pytest.raises(ValueError): + safe_parse_webapp_init_data( + "42:TOKEN", + "auth_date=1650385342" + "&user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D" + "&query_id=test" + "&hash=test", + )