[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
This commit is contained in:
Alex Root Junior 2022-04-19 22:03:24 +03:00 committed by GitHub
parent 286cf39c8a
commit 497436595d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
81 changed files with 1942 additions and 147 deletions

View file

@ -12,3 +12,4 @@ python:
path: .
extra_requirements:
- docs
- redis

1
CHANGES/890.feature.rst Normal file
View file

@ -0,0 +1 @@
Added full support of `Telegram Bot API 6.0 <https://core.telegram.org/bots/api-changelog#april-16-2022>`_

View file

@ -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

View file

@ -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_<bot username>'*. *<bot_username>* 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_<bot_username>"`. :code:`<bot_username>` 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 » <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 <https://core.telegram.org/bots/webapps>`_ 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

View file

@ -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,
},

View file

@ -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"]

View file

@ -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

View file

@ -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",

View file

@ -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 <https://core.telegram.org/bots/webapps>`_ 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)

View file

@ -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_<bot username>'*. *<bot_username>* 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_<bot_username>"`. :code:`<bot_username>` is case insensitive. 1-64 characters."""
title: str
"""Sticker set title, 1-64 characters"""
emojis: str

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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",

View file

@ -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"""

View file

@ -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

View file

@ -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

View file

@ -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=<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 <https://core.telegram.org/widgets/login>`_."""
callback_data: Optional[str] = None
"""*Optional*. Data to be sent in a `callback query <https://core.telegram.org/bots/api#callbackquery>`_ to the bot when button is pressed, 1-64 bytes"""
web_app: Optional[WebAppInfo] = None
"""*Optional*. Description of the `Web App <https://core.telegram.org/bots/webapps>`_ 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 <https://core.telegram.org/widgets/login>`_."""
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

View file

@ -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 <https://core.telegram.org/bots/webapps>`_ 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."""

View file

@ -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
"""

View file

@ -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*"""

View file

@ -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*"""

View file

@ -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 <https://core.telegram.org/bots/webapps>`_.
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`."""

View file

@ -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

View file

@ -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 <https://core.telegram.org/bots/webapps>`_ 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 <https://core.telegram.org/bots/api#inlinekeyboardmarkup>`_ attached to the message."""

View file

@ -26,6 +26,8 @@ class Sticker(TelegramObject):
"""Sticker height"""
is_animated: bool
""":code:`True`, if the sticker is `animated <https://telegram.org/blog/animated-stickers>`_"""
is_video: bool
""":code:`True`, if the sticker is a `video sticker <https://telegram.org/blog/video-stickers-better-reactions>`_"""
thumb: Optional[PhotoSize] = None
"""*Optional*. Sticker thumbnail in the .WEBP or .JPG format"""
emoji: Optional[str] = None

View file

@ -22,9 +22,11 @@ class StickerSet(TelegramObject):
"""Sticker set title"""
is_animated: bool
""":code:`True`, if the sticker set contains `animated stickers <https://telegram.org/blog/animated-stickers>`_"""
is_video: bool
""":code:`True`, if the sticker set contains `video stickers <https://telegram.org/blog/video-stickers-better-reactions>`_"""
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"""

View file

@ -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"""

View file

@ -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"""

View file

@ -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"""

View file

@ -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
"""

View file

@ -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"""

View file

@ -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"""

View file

@ -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"""

View file

@ -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
"""

View file

@ -0,0 +1,16 @@
from __future__ import annotations
from .base import TelegramObject
class WebAppData(TelegramObject):
"""
Contains data sent from a `Web App <https://core.telegram.org/bots/webapps>`_ 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."""

View file

@ -0,0 +1,14 @@
from __future__ import annotations
from .base import TelegramObject
class WebAppInfo(TelegramObject):
"""
Contains information about a `Web App <https://core.telegram.org/bots/webapps>`_.
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 <https://core.telegram.org/bots/webapps#initializing-web-apps>`_"""

View file

@ -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

129
aiogram/utils/web_app.py Normal file
View file

@ -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 users 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")

View file

@ -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(...)

View file

@ -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(...))

View file

@ -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(...))

View file

@ -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
========

View file

@ -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(...)

View file

@ -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(...)

View file

@ -0,0 +1,9 @@
#######################
ChatAdministratorRights
#######################
.. automodule:: aiogram.types.chat_administrator_rights
:members:
:member-order: bysource
:undoc-members: True

View file

@ -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
========

View file

@ -0,0 +1,9 @@
##########
MenuButton
##########
.. automodule:: aiogram.types.menu_button
:members:
:member-order: bysource
:undoc-members: True

View file

@ -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

View file

@ -0,0 +1,9 @@
#################
MenuButtonDefault
#################
.. automodule:: aiogram.types.menu_button_default
:members:
:member-order: bysource
:undoc-members: True

View file

@ -0,0 +1,9 @@
################
MenuButtonWebApp
################
.. automodule:: aiogram.types.menu_button_web_app
:members:
:member-order: bysource
:undoc-members: True

View file

@ -0,0 +1,9 @@
#################
SentWebAppMessage
#################
.. automodule:: aiogram.types.sent_web_app_message
:members:
:member-order: bysource
:undoc-members: True

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,9 @@
##################
VideoChatScheduled
##################
.. automodule:: aiogram.types.video_chat_scheduled
:members:
:member-order: bysource
:undoc-members: True

View file

@ -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

View file

@ -0,0 +1,9 @@
##########
WebAppData
##########
.. automodule:: aiogram.types.web_app_data
:members:
:member-order: bysource
:undoc-members: True

View file

@ -0,0 +1,9 @@
##########
WebAppInfo
##########
.. automodule:: aiogram.types.web_app_info
:members:
:member-order: bysource
:undoc-members: True

View file

@ -7,3 +7,4 @@ Utils
keyboard
i18n
chat_action
web_app

55
docs/utils/web_app.rst Normal file
View file

@ -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 <https://telegram.org/blog/notifications-bots#bot-revolution>`_
and `documentation <https://core.telegram.org/bots/webapps>`_.
`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

376
examples/web_app/demo.html Normal file
View file

@ -0,0 +1,376 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
<meta name="format-detection" content="telephone=no"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="MobileOptimized" content="176"/>
<meta name="HandheldFriendly" content="True"/>
<meta name="robots" content="noindex,nofollow"/>
<script src="https://telegram.org/js/telegram-web-app.js?1"></script>
<script>
function setThemeClass() {
document.documentElement.className = Telegram.WebApp.colorScheme;
}
Telegram.WebApp.onEvent('themeChanged', setThemeClass);
setThemeClass();
</script>
<style>
body {
font-family: sans-serif;
background-color: var(--tg-theme-bg-color, #ffffff);
color: var(--tg-theme-text-color, #222222);
font-size: 16px;
margin: 0;
padding: 0;
color-scheme: var(--tg-color-scheme);
}
a {
color: var(--tg-theme-link-color, #2678b6);
}
button {
display: block;
width: 100%;
font-size: 14px;
margin: 15px 0;
padding: 12px 20px;
border: none;
border-radius: 4px;
background-color: var(--tg-theme-button-color, #50a8eb);
color: var(--tg-theme-button-text-color, #ffffff);
cursor: pointer;
}
button[disabled] {
opacity: 0.6;
cursor: auto;
pointer-events: none;
}
button.close_btn {
/*position: fixed;*/
position: absolute;
left: 0;
right: 0;
bottom: 0;
border-radius: 0;
margin: 0;
padding: 16px 20px;
text-transform: uppercase;
}
section {
padding: 15px 15px 65px;
text-align: center;
}
p {
margin: 40px 0 15px;
}
ul {
text-align: left;
}
li {
color: var(--tg-theme-hint-color, #a8a8a8);
}
textarea {
width: 100%;
box-sizing: border-box;
padding: 7px;
}
pre {
background: rgba(0, 0, 0, .07);
border-radius: 4px;
padding: 4px;
margin: 7px 0;
word-break: break-all;
word-break: break-word;
white-space: pre-wrap;
text-align: left;
}
.dark pre {
background: rgba(255, 255, 255, .15);
}
.hint {
font-size: .8em;
color: var(--tg-theme-hint-color, #a8a8a8);
}
.ok {
color: green;
}
.err {
color: red;
}
#fixed_wrap {
position: fixed;
left: 0;
right: 0;
top: 0;
transform: translateY(100vh);
}
.viewport_border,
.viewport_stable_border {
position: fixed;
left: 0;
right: 0;
top: 0;
height: var(--tg-viewport-height, 100vh);
pointer-events: none;
}
.viewport_stable_border {
height: var(--tg-viewport-stable-height, 100vh);
}
.viewport_border:before,
.viewport_stable_border:before {
content: attr(text);
display: inline-block;
position: absolute;
background: gray;
right: 0;
top: 0;
font-size: 7px;
padding: 2px 4px;
vertical-align: top;
}
.viewport_stable_border:before {
background: green;
left: 0;
right: auto;
}
.viewport_border:after,
.viewport_stable_border:after {
content: '';
display: block;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
border: 2px dashed gray;
}
.viewport_stable_border:after {
border-color: green;
}
</style>
</head>
<body style="visibility: hidden;">
<section>
<button id="main_btn" onclick="sendMessage('');">Send «Hello, World!»</button>
<button id="with_webview_btn" onclick="sendMessage('', true);">
Send «Hello, World!» with inline webview button
</button>
<button onclick="webviewExpand();">Expand Webview</button>
<button onclick="toggleMainButton(this);">Hide Main Button</button>
<div id="btn_status" class="hint" style="display: none;">
</div>
<p>Test links:</p>
<ul>
<li><a id="regular_link" href="?nextpage=1">Regular link #1</a> (opens inside webview)</li>
<li><a href="https://telegram.org/" target="_blank">target="_blank" link</a> (opens outside
webview)
</li>
<li><a href="javascript:window.open('https://telegram.org/');">window.open() link</a>
(opens outside webview)
</li>
<li><a href="https://t.me/like">LikeBot t.me link</a> (opens inside Telegram app)</li>
<li><a href="tg://resolve?domain=vote">VoteBot tg:// link</a> (does not open)</li>
</ul>
<p>Test permissions:</p>
<ul>
<li><a href="javascript:;" onclick="return requestLocation(this);">Request Location</a>
<span></span></li>
<li><a href="javascript:;" onclick="return requestVideo(this);">Request Video</a>
<span></span></li>
<li><a href="javascript:;" onclick="return requestAudio(this);">Request Audio</a>
<span></span></li>
</ul>
<pre><code id="webview_data"></code></pre>
<div class="hint">
Data passed to webview.
<span id="webview_data_status" style="display: none;">Checking hash...</span>
</div>
<pre><code id="theme_data"></code></pre>
<div class="hint">
Theme params
</div>
</section>
<div class="viewport_border"></div>
<div class="viewport_stable_border"></div>
<script src="https://webappcontent.telegram.org/js/jquery.min.js"></script>
<script>
Telegram.WebApp.ready();
var initData = Telegram.WebApp.initData || '';
var initDataUnsafe = Telegram.WebApp.initDataUnsafe || {};
function sendMessage(msg_id, with_webview) {
if (!initDataUnsafe.query_id) {
alert('WebViewQueryId not defined');
return;
}
$('button').prop('disabled', true);
$('#btn_status').text('Sending...').removeClass('ok err').show();
$.ajax('/demo/sendMessage', {
type: 'POST',
data: {
_auth: initData,
msg_id: msg_id || '',
with_webview: !initDataUnsafe.receiver && with_webview ? 1 : 0
},
dataType: 'json',
success: function (result) {
$('button').prop('disabled', false);
if (result.response) {
if (result.response.ok) {
$('#btn_status').html('Message sent successfully!').addClass('ok').show();
} else {
$('#btn_status').text(result.response.description).addClass('err').show();
alert(result.response.description);
}
} else {
$('#btn_status').text('Unknown error').addClass('err').show();
alert('Unknown error');
}
},
error: function (xhr) {
$('button').prop('disabled', false);
$('#btn_status').text('Server error').addClass('err').show();
alert('Server error');
}
});
}
function webviewExpand() {
Telegram.WebApp.expand();
}
function webviewClose() {
Telegram.WebApp.close();
}
function requestLocation(el) {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function (position) {
$(el).next('span').html('(' + position.coords.latitude + ', ' + position.coords.longitude + ')').attr('class', 'ok');
});
} else {
$(el).next('span').html('Geolocation is not supported in this browser.').attr('class', 'err');
}
return false;
}
function requestVideo(el) {
if (navigator.mediaDevices) {
navigator.mediaDevices.getUserMedia({
audio: false,
video: true
}).then(function (stream) {
$(el).next('span').html('(Access granted)').attr('class', 'ok');
});
} else {
$(el).next('span').html('Media devices is not supported in this browser.').attr('class', 'err');
}
return false;
}
function requestAudio(el) {
if (navigator.mediaDevices) {
navigator.mediaDevices.getUserMedia({
audio: true,
video: false
}).then(function (stream) {
$(el).next('span').html('(Access granted)').attr('class', 'ok');
});
} else {
$(el).next('span').html('Media devices is not supported in this browser.').attr('class', 'err');
}
return false;
}
Telegram.WebApp.onEvent('themeChanged', function () {
$('#theme_data').html(JSON.stringify(Telegram.WebApp.themeParams, null, 2));
});
$('#main_btn').toggle(!!initDataUnsafe.query_id);
$('#with_webview_btn').toggle(!!initDataUnsafe.query_id && !initDataUnsafe.receiver);
// $('#data_btn').toggle(!initDataUnsafe.query_id || !initDataUnsafe.receiver);
$('#webview_data').html(JSON.stringify(initDataUnsafe, null, 2));
$('#theme_data').html(JSON.stringify(Telegram.WebApp.themeParams, null, 2));
$('#regular_link').attr('href', $('#regular_link').attr('href') + location.hash);
$('#text_field').focus();
if (initDataUnsafe.query_id && initData) {
$('#webview_data_status').show();
$.ajax('/demo/checkData', {
type: 'POST',
data: {_auth: initData},
dataType: 'json',
success: function (result) {
if (result.ok) {
$('#webview_data_status').html('Hash is correct').addClass('ok');
} else {
$('#webview_data_status').html(result.error).addClass('err');
}
},
error: function (xhr) {
$('#webview_data_status').html('Server error').addClass('err');
}
});
}
$('body').css('visibility', '');
Telegram.WebApp.MainButton
.setText('CLOSE WEBVIEW')
.show()
.onClick(function () {
webviewClose();
});
function toggleMainButton(el) {
var mainButton = Telegram.WebApp.MainButton;
if (mainButton.isVisible) {
mainButton.hide();
el.innerHTML = 'Show Main Button';
} else {
mainButton.show();
el.innerHTML = 'Hide Main Button';
}
}
function round(val, d) {
var k = Math.pow(10, d || 0);
return Math.round(val * k) / k;
}
function setViewportData() {
$('.viewport_border').attr('text', window.innerWidth + ' x ' + round(Telegram.WebApp.viewportHeight, 2));
$('.viewport_stable_border').attr('text', window.innerWidth + ' x ' + round(Telegram.WebApp.viewportStableHeight, 2) + ' | is_expanded: ' + (Telegram.WebApp.isExpanded ? 'true' : 'false'));
}
Telegram.WebApp.onEvent('viewportChanged', setViewportData);
setViewportData();
</script>
</body>
</html>
<!-- page generated in 1.11ms -->

View file

@ -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"))]
]
),
)

49
examples/web_app/main.py Normal file
View file

@ -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()

View file

@ -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})

View file

@ -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

View file

View file

View file

View file

@ -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

View file

@ -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

View file

@ -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",
)
],

View file

@ -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"),

View file

@ -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

View file

@ -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

View file

View file

@ -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],
],

View file

@ -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

View file

@ -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,

View file

@ -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",
)