diff --git a/.apiversion b/.apiversion index 592f36ef..c026ac82 100644 --- a/.apiversion +++ b/.apiversion @@ -1 +1 @@ -9.5 +9.6 diff --git a/.butcher/enums/ContentType.yml b/.butcher/enums/ContentType.yml index acdae0a1..8f7ec557 100644 --- a/.butcher/enums/ContentType.yml +++ b/.butcher/enums/ContentType.yml @@ -49,3 +49,4 @@ extract: - reply_to_checklist_task_id - suggested_post_info - is_paid_post + - reply_to_poll_option_id diff --git a/.butcher/methods/sendPoll/default.yml b/.butcher/methods/sendPoll/default.yml index 26ac273c..de1aec36 100644 --- a/.butcher/methods/sendPoll/default.yml +++ b/.butcher/methods/sendPoll/default.yml @@ -1,3 +1,4 @@ explanation_parse_mode: parse_mode question_parse_mode: parse_mode +description_parse_mode: parse_mode protect_content: protect_content diff --git a/.butcher/schema/schema.json b/.butcher/schema/schema.json index faa2aee0..da1046d5 100644 --- a/.butcher/schema/schema.json +++ b/.butcher/schema/schema.json @@ -1,7 +1,7 @@ { "api": { - "version": "9.5", - "release_date": "2026-03-01" + "version": "9.6", + "release_date": "2026-04-03" }, "items": [ { diff --git a/.butcher/types/PollOption/replace.yml b/.butcher/types/PollOption/replace.yml new file mode 100644 index 00000000..a2abf27d --- /dev/null +++ b/.butcher/types/PollOption/replace.yml @@ -0,0 +1,5 @@ +annotations: + addition_date: + parsed_type: + type: std + name: DateTime diff --git a/CHANGES/1793.misc.rst b/CHANGES/1793.misc.rst new file mode 100644 index 00000000..6733dfb6 --- /dev/null +++ b/CHANGES/1793.misc.rst @@ -0,0 +1,23 @@ +Updated to `Bot API 9.6 `_ + +**Managed Bots:** + +- Added :class:`aiogram.types.managed_bot_updated.ManagedBotUpdated` and :class:`aiogram.types.managed_bot_created.ManagedBotCreated` types +- Added :class:`aiogram.types.keyboard_button_request_managed_bot.KeyboardButtonRequestManagedBot` type and :code:`request_managed_bot` field to :class:`aiogram.types.keyboard_button.KeyboardButton` +- Added :class:`aiogram.types.prepared_keyboard_button.PreparedKeyboardButton` type +- Added :class:`aiogram.methods.get_managed_bot_token.GetManagedBotToken`, :class:`aiogram.methods.replace_managed_bot_token.ReplaceManagedBotToken`, and :class:`aiogram.methods.save_prepared_keyboard_button.SavePreparedKeyboardButton` methods +- Added :code:`managed_bot` field to :class:`aiogram.types.update.Update` +- Added :code:`managed_bot_created` field to :class:`aiogram.types.message.Message` +- Added :code:`can_manage_bots` field to :class:`aiogram.types.user.User` + +**Polls:** + +- Replaced :code:`correct_option_id` with :code:`correct_option_ids` in :class:`aiogram.types.poll.Poll` and :class:`aiogram.methods.send_poll.SendPoll` — quizzes now support multiple correct answers +- Added :code:`allows_revoting`, :code:`description`, :code:`description_entities` fields to :class:`aiogram.types.poll.Poll` +- Added :code:`persistent_id`, :code:`added_by_user`, :code:`added_by_chat`, :code:`addition_date` fields to :class:`aiogram.types.poll_option.PollOption` +- Added :code:`option_persistent_ids` field to :class:`aiogram.types.poll_answer.PollAnswer` +- Added :code:`allows_revoting`, :code:`shuffle_options`, :code:`allow_adding_options`, :code:`hide_results_until_closes`, :code:`description`, :code:`description_parse_mode`, :code:`description_entities` parameters to :class:`aiogram.methods.send_poll.SendPoll` +- Added :class:`aiogram.types.poll_option_added.PollOptionAdded` and :class:`aiogram.types.poll_option_deleted.PollOptionDeleted` types +- Added :code:`poll_option_added`, :code:`poll_option_deleted`, :code:`reply_to_poll_option_id` fields to :class:`aiogram.types.message.Message` +- Added :code:`poll_option_id` field to :class:`aiogram.types.reply_parameters.ReplyParameters` +- Maximum poll duration increased to 2,628,000 seconds (~30.4 days) diff --git a/README.rst b/README.rst index a04ff797..94c8da9f 100644 --- a/README.rst +++ b/README.rst @@ -52,7 +52,7 @@ Features - Asynchronous (`asyncio docs `_, :pep:`492`) - Has type hints (:pep:`484`) and can be used with `mypy `_ - Supports `PyPy `_ -- Supports `Telegram Bot API 9.5 `_ and gets fast updates to the latest versions of the Bot API +- Supports `Telegram Bot API 9.6 `_ and gets fast updates to the latest versions of the Bot API - Telegram Bot API integration code was `autogenerated `_ and can be easily re-generated when API gets updated - Updates router (Blueprints) - Has Finite State Machine diff --git a/aiogram/__meta__.py b/aiogram/__meta__.py index 00647de5..f60aa330 100644 --- a/aiogram/__meta__.py +++ b/aiogram/__meta__.py @@ -1,2 +1,2 @@ -__version__ = "3.26.0" -__api_version__ = "9.5" +__version__ = "3.27.0" +__api_version__ = "9.6" diff --git a/aiogram/client/bot.py b/aiogram/client/bot.py index 195c06dd..0afb0fc5 100644 --- a/aiogram/client/bot.py +++ b/aiogram/client/bot.py @@ -80,6 +80,7 @@ from ..methods import ( GetFile, GetForumTopicIconStickers, GetGameHighScores, + GetManagedBotToken, GetMe, GetMyCommands, GetMyDefaultAdministratorRights, @@ -110,11 +111,13 @@ from ..methods import ( RemoveUserVerification, ReopenForumTopic, ReopenGeneralForumTopic, + ReplaceManagedBotToken, ReplaceStickerInSet, RepostStory, RestrictChatMember, RevokeChatInviteLink, SavePreparedInlineMessage, + SavePreparedKeyboardButton, SendAnimation, SendAudio, SendChatAction, @@ -216,6 +219,7 @@ from ..types import ( InputProfilePhotoUnion, InputSticker, InputStoryContentUnion, + KeyboardButton, LabeledPrice, LinkPreviewOptions, MaskPosition, @@ -228,6 +232,7 @@ from ..types import ( PassportElementErrorUnion, Poll, PreparedInlineMessage, + PreparedKeyboardButton, ReactionTypeUnion, ReplyMarkupUnion, ReplyParameters, @@ -1797,6 +1802,46 @@ class Bot: call = GetMe() return await self(call, request_timeout=request_timeout) + async def get_managed_bot_token( + self, + user_id: int, + request_timeout: int | None = None, + ) -> str: + """ + Use this method to get the token of a managed bot. Returns the token as *String* on success. + + Source: https://core.telegram.org/bots/api#getmanagedbottoken + + :param user_id: User identifier of the managed bot whose token will be returned + :param request_timeout: Request timeout + :return: Returns the token as *String* on success. + """ + + call = GetManagedBotToken( + user_id=user_id, + ) + return await self(call, request_timeout=request_timeout) + + async def replace_managed_bot_token( + self, + user_id: int, + request_timeout: int | None = None, + ) -> str: + """ + Use this method to revoke the current token of a managed bot and generate a new one. Returns the new token as *String* on success. + + Source: https://core.telegram.org/bots/api#replacemanagedbottoken + + :param user_id: User identifier of the managed bot whose token will be replaced + :param request_timeout: Request timeout + :return: Returns the new token as *String* on success. + """ + + call = ReplaceManagedBotToken( + user_id=user_id, + ) + return await self(call, request_timeout=request_timeout) + async def get_my_commands( self, scope: BotCommandScopeUnion | None = None, @@ -3008,13 +3053,20 @@ class Bot: is_anonymous: bool | None = None, type: str | None = None, allows_multiple_answers: bool | None = None, - correct_option_id: int | None = None, + allows_revoting: bool | None = None, + shuffle_options: bool | None = None, + allow_adding_options: bool | None = None, + hide_results_until_closes: bool | None = None, + correct_option_ids: list[int] | None = None, explanation: str | None = None, explanation_parse_mode: str | Default | None = Default("parse_mode"), explanation_entities: list[MessageEntity] | None = None, open_period: int | None = None, close_date: DateTimeUnion | None = None, is_closed: bool | None = None, + description: str | None = None, + description_parse_mode: str | Default | None = Default("parse_mode"), + description_entities: list[MessageEntity] | None = None, disable_notification: bool | None = None, protect_content: bool | Default | None = Default("protect_content"), allow_paid_broadcast: bool | None = None, @@ -3040,13 +3092,20 @@ class Bot: :param is_anonymous: :code:`True`, if the poll needs to be anonymous, defaults to :code:`True` :param type: Poll type, 'quiz' or 'regular', defaults to 'regular' :param allows_multiple_answers: :code:`True`, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to :code:`False` - :param correct_option_id: 0-based identifier of the correct answer option, required for polls in quiz mode + :param allows_revoting: Pass :code:`True`, if the poll allows to change chosen answer options + :param shuffle_options: Pass :code:`True`, if the poll options must be shown in random order + :param allow_adding_options: Pass :code:`True`, if answer options can be added to the poll after creation + :param hide_results_until_closes: Pass :code:`True`, if poll results must be shown only after the poll closes + :param correct_option_ids: A JSON-serialized list of 0-based identifiers of the correct answer options, required for polls in quiz mode :param explanation: Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing :param explanation_parse_mode: Mode for parsing entities in the explanation. See `formatting options `_ for more details. :param explanation_entities: A JSON-serialized list of special entities that appear in the poll explanation. It can be specified instead of *explanation_parse_mode* - :param open_period: Amount of time in seconds the poll will be active after creation, 5-600. Can't be used together with *close_date*. - :param close_date: Point in time (Unix timestamp) when the poll will be automatically closed. Must be at least 5 and no more than 600 seconds in the future. Can't be used together with *open_period*. + :param open_period: Amount of time in seconds the poll will be active after creation, 5-2628000. Can't be used together with *close_date*. + :param close_date: Point in time (Unix timestamp) when the poll will be automatically closed. Must be at least 5 and no more than 2628000 seconds in the future. Can't be used together with *open_period*. :param is_closed: Pass :code:`True` if the poll needs to be immediately closed. This can be useful for poll preview. + :param description: Description of the poll to be sent, 0-1024 characters after entities parsing + :param description_parse_mode: Mode for parsing entities in the poll description. + :param description_entities: A JSON-serialized list of special entities that appear in the poll description :param disable_notification: Sends the message `silently `_. Users will receive a notification with no sound. :param protect_content: Protects the contents of the sent message from forwarding and saving :param allow_paid_broadcast: Pass :code:`True` to allow up to 1000 messages per second, ignoring `broadcasting limits `_ for a fee of 0.1 Telegram Stars per message. The relevant Stars will be withdrawn from the bot's balance @@ -3070,13 +3129,20 @@ class Bot: is_anonymous=is_anonymous, type=type, allows_multiple_answers=allows_multiple_answers, - correct_option_id=correct_option_id, + allows_revoting=allows_revoting, + shuffle_options=shuffle_options, + allow_adding_options=allow_adding_options, + hide_results_until_closes=hide_results_until_closes, + correct_option_ids=correct_option_ids, explanation=explanation, explanation_parse_mode=explanation_parse_mode, explanation_entities=explanation_entities, open_period=open_period, close_date=close_date, is_closed=is_closed, + description=description, + description_parse_mode=description_parse_mode, + description_entities=description_entities, disable_notification=disable_notification, protect_content=protect_content, allow_paid_broadcast=allow_paid_broadcast, @@ -4883,6 +4949,29 @@ class Bot: ) return await self(call, request_timeout=request_timeout) + async def save_prepared_keyboard_button( + self, + user_id: int, + button: KeyboardButton, + request_timeout: int | None = None, + ) -> PreparedKeyboardButton: + """ + Stores a keyboard button that can be used by a user within a Mini App. Returns a :class:`aiogram.types.prepared_keyboard_button.PreparedKeyboardButton` object. + + Source: https://core.telegram.org/bots/api#savepreparedkeyboardbutton + + :param user_id: Unique identifier of the target user that can use the button + :param button: A JSON-serialized object describing the button to be saved. The button must be of the type *request_users*, *request_chat*, or *request_managed_bot* + :param request_timeout: Request timeout + :return: Returns a :class:`aiogram.types.prepared_keyboard_button.PreparedKeyboardButton` object. + """ + + call = SavePreparedKeyboardButton( + user_id=user_id, + button=button, + ) + return await self(call, request_timeout=request_timeout) + async def send_gift( self, gift_id: str, diff --git a/aiogram/dispatcher/middlewares/user_context.py b/aiogram/dispatcher/middlewares/user_context.py index 844ddd96..ab600b3f 100644 --- a/aiogram/dispatcher/middlewares/user_context.py +++ b/aiogram/dispatcher/middlewares/user_context.py @@ -183,4 +183,8 @@ class UserContextMiddleware(BaseMiddleware): return EventContext( user=event.purchased_paid_media.from_user, ) + if event.managed_bot: + return EventContext( + user=event.managed_bot.user, + ) return EventContext() diff --git a/aiogram/dispatcher/router.py b/aiogram/dispatcher/router.py index 90a2362a..532018d5 100644 --- a/aiogram/dispatcher/router.py +++ b/aiogram/dispatcher/router.py @@ -85,6 +85,7 @@ class Router: router=self, event_name="purchased_paid_media", ) + self.managed_bot = TelegramEventObserver(router=self, event_name="managed_bot") self.errors = self.error = TelegramEventObserver(router=self, event_name="error") @@ -115,6 +116,7 @@ class Router: "edited_business_message": self.edited_business_message, "business_message": self.business_message, "purchased_paid_media": self.purchased_paid_media, + "managed_bot": self.managed_bot, "error": self.errors, } diff --git a/aiogram/enums/content_type.py b/aiogram/enums/content_type.py index b2a555d4..6c2aaa53 100644 --- a/aiogram/enums/content_type.py +++ b/aiogram/enums/content_type.py @@ -73,6 +73,9 @@ class ContentType(str, Enum): SUGGESTED_POST_DECLINED = "suggested_post_declined" SUGGESTED_POST_PAID = "suggested_post_paid" SUGGESTED_POST_REFUNDED = "suggested_post_refunded" + MANAGED_BOT_CREATED = "managed_bot_created" + POLL_OPTION_ADDED = "poll_option_added" + POLL_OPTION_DELETED = "poll_option_deleted" VIDEO_CHAT_SCHEDULED = "video_chat_scheduled" VIDEO_CHAT_STARTED = "video_chat_started" VIDEO_CHAT_ENDED = "video_chat_ended" diff --git a/aiogram/enums/update_type.py b/aiogram/enums/update_type.py index 2629264d..0534a664 100644 --- a/aiogram/enums/update_type.py +++ b/aiogram/enums/update_type.py @@ -31,3 +31,4 @@ class UpdateType(str, Enum): CHAT_JOIN_REQUEST = "chat_join_request" CHAT_BOOST = "chat_boost" REMOVED_CHAT_BOOST = "removed_chat_boost" + MANAGED_BOT = "managed_bot" diff --git a/aiogram/methods/__init__.py b/aiogram/methods/__init__.py index b4c93d6c..b59d32cb 100644 --- a/aiogram/methods/__init__.py +++ b/aiogram/methods/__init__.py @@ -62,6 +62,7 @@ from .get_custom_emoji_stickers import GetCustomEmojiStickers from .get_file import GetFile from .get_forum_topic_icon_stickers import GetForumTopicIconStickers from .get_game_high_scores import GetGameHighScores +from .get_managed_bot_token import GetManagedBotToken from .get_me import GetMe from .get_my_commands import GetMyCommands from .get_my_default_administrator_rights import GetMyDefaultAdministratorRights @@ -92,11 +93,13 @@ from .remove_my_profile_photo import RemoveMyProfilePhoto from .remove_user_verification import RemoveUserVerification from .reopen_forum_topic import ReopenForumTopic from .reopen_general_forum_topic import ReopenGeneralForumTopic +from .replace_managed_bot_token import ReplaceManagedBotToken from .replace_sticker_in_set import ReplaceStickerInSet from .repost_story import RepostStory from .restrict_chat_member import RestrictChatMember from .revoke_chat_invite_link import RevokeChatInviteLink from .save_prepared_inline_message import SavePreparedInlineMessage +from .save_prepared_keyboard_button import SavePreparedKeyboardButton from .send_animation import SendAnimation from .send_audio import SendAudio from .send_chat_action import SendChatAction @@ -231,6 +234,7 @@ __all__ = ( "GetForumTopicIconStickers", "GetGameHighScores", "GetMe", + "GetManagedBotToken", "GetMyCommands", "GetMyDefaultAdministratorRights", "GetMyDescription", @@ -261,12 +265,14 @@ __all__ = ( "ReopenForumTopic", "ReopenGeneralForumTopic", "ReplaceStickerInSet", + "ReplaceManagedBotToken", "RepostStory", "Request", "Response", "RestrictChatMember", "RevokeChatInviteLink", "SavePreparedInlineMessage", + "SavePreparedKeyboardButton", "SendAnimation", "SendAudio", "SendChatAction", diff --git a/aiogram/methods/get_managed_bot_token.py b/aiogram/methods/get_managed_bot_token.py new file mode 100644 index 00000000..7f656791 --- /dev/null +++ b/aiogram/methods/get_managed_bot_token.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from .base import TelegramMethod + + +class GetManagedBotToken(TelegramMethod[str]): + """ + Use this method to get the token of a managed bot. Returns the token as *String* on success. + + Source: https://core.telegram.org/bots/api#getmanagedbottoken + """ + + __returning__ = str + __api_method__ = "getManagedBotToken" + + user_id: int + """User identifier of the managed bot whose token will be returned""" + + if TYPE_CHECKING: + + def __init__( + __pydantic__self__, + *, + user_id: int, + **__pydantic_kwargs: Any, + ) -> None: + super().__init__( + user_id=user_id, + **__pydantic_kwargs, + ) diff --git a/aiogram/methods/replace_managed_bot_token.py b/aiogram/methods/replace_managed_bot_token.py new file mode 100644 index 00000000..9b68413b --- /dev/null +++ b/aiogram/methods/replace_managed_bot_token.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from .base import TelegramMethod + + +class ReplaceManagedBotToken(TelegramMethod[str]): + """ + Use this method to revoke the current token of a managed bot and generate a new one. Returns the new token as *String* on success. + + Source: https://core.telegram.org/bots/api#replacemanagedbottoken + """ + + __returning__ = str + __api_method__ = "replaceManagedBotToken" + + user_id: int + """User identifier of the managed bot whose token will be replaced""" + + if TYPE_CHECKING: + + def __init__( + __pydantic__self__, + *, + user_id: int, + **__pydantic_kwargs: Any, + ) -> None: + super().__init__( + user_id=user_id, + **__pydantic_kwargs, + ) diff --git a/aiogram/methods/save_prepared_keyboard_button.py b/aiogram/methods/save_prepared_keyboard_button.py new file mode 100644 index 00000000..3ba9f641 --- /dev/null +++ b/aiogram/methods/save_prepared_keyboard_button.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from ..types import KeyboardButton, PreparedKeyboardButton +from .base import TelegramMethod + + +class SavePreparedKeyboardButton(TelegramMethod[PreparedKeyboardButton]): + """ + Stores a keyboard button that can be used by a user within a Mini App. Returns a :class:`aiogram.types.prepared_keyboard_button.PreparedKeyboardButton` object. + + Source: https://core.telegram.org/bots/api#savepreparedkeyboardbutton + """ + + __returning__ = PreparedKeyboardButton + __api_method__ = "savePreparedKeyboardButton" + + user_id: int + """Unique identifier of the target user that can use the button""" + button: KeyboardButton + """A JSON-serialized object describing the button to be saved. The button must be of the type *request_users*, *request_chat*, or *request_managed_bot*""" + + if TYPE_CHECKING: + + def __init__( + __pydantic__self__, + *, + user_id: int, + button: KeyboardButton, + **__pydantic_kwargs: Any, + ) -> None: + super().__init__( + user_id=user_id, + button=button, + **__pydantic_kwargs, + ) diff --git a/aiogram/methods/send_poll.py b/aiogram/methods/send_poll.py index 01979ff1..ca7cf48e 100644 --- a/aiogram/methods/send_poll.py +++ b/aiogram/methods/send_poll.py @@ -47,8 +47,16 @@ class SendPoll(TelegramMethod[Message]): """Poll type, 'quiz' or 'regular', defaults to 'regular'""" allows_multiple_answers: bool | None = None """:code:`True`, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to :code:`False`""" - correct_option_id: int | None = None - """0-based identifier of the correct answer option, required for polls in quiz mode""" + allows_revoting: bool | None = None + """Pass :code:`True`, if the poll allows to change chosen answer options, defaults to :code:`False` for quizzes and to :code:`True` for regular polls""" + shuffle_options: bool | None = None + """Pass :code:`True`, if the poll options must be shown in random order""" + allow_adding_options: bool | None = None + """Pass :code:`True`, if answer options can be added to the poll after creation; not supported for anonymous polls and quizzes""" + hide_results_until_closes: bool | None = None + """Pass :code:`True`, if poll results must be shown only after the poll closes""" + correct_option_ids: list[int] | None = None + """A JSON-serialized list of monotonically increasing 0-based identifiers of the correct answer options, required for polls in quiz mode""" explanation: str | None = None """Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing""" explanation_parse_mode: str | Default | None = Default("parse_mode") @@ -56,11 +64,17 @@ class SendPoll(TelegramMethod[Message]): explanation_entities: list[MessageEntity] | None = None """A JSON-serialized list of special entities that appear in the poll explanation. It can be specified instead of *explanation_parse_mode*""" open_period: int | None = None - """Amount of time in seconds the poll will be active after creation, 5-600. Can't be used together with *close_date*.""" + """Amount of time in seconds the poll will be active after creation, 5-2628000. Can't be used together with *close_date*.""" close_date: DateTimeUnion | None = None - """Point in time (Unix timestamp) when the poll will be automatically closed. Must be at least 5 and no more than 600 seconds in the future. Can't be used together with *open_period*.""" + """Point in time (Unix timestamp) when the poll will be automatically closed. Must be at least 5 and no more than 2628000 seconds in the future. Can't be used together with *open_period*.""" is_closed: bool | None = None """Pass :code:`True` if the poll needs to be immediately closed. This can be useful for poll preview.""" + description: str | None = None + """Description of the poll to be sent, 0-1024 characters after entities parsing""" + description_parse_mode: str | Default | None = Default("parse_mode") + """Mode for parsing entities in the poll description. See `formatting options `_ for more details.""" + description_entities: list[MessageEntity] | None = None + """A JSON-serialized list of special entities that appear in the poll description, which can be specified instead of *description_parse_mode*""" disable_notification: bool | None = None """Sends the message `silently `_. Users will receive a notification with no sound.""" protect_content: bool | Default | None = Default("protect_content") @@ -101,13 +115,20 @@ class SendPoll(TelegramMethod[Message]): is_anonymous: bool | None = None, type: str | None = None, allows_multiple_answers: bool | None = None, - correct_option_id: int | None = None, + allows_revoting: bool | None = None, + shuffle_options: bool | None = None, + allow_adding_options: bool | None = None, + hide_results_until_closes: bool | None = None, + correct_option_ids: list[int] | None = None, explanation: str | None = None, explanation_parse_mode: str | Default | None = Default("parse_mode"), explanation_entities: list[MessageEntity] | None = None, open_period: int | None = None, close_date: DateTimeUnion | None = None, is_closed: bool | None = None, + description: str | None = None, + description_parse_mode: str | Default | None = Default("parse_mode"), + description_entities: list[MessageEntity] | None = None, disable_notification: bool | None = None, protect_content: bool | Default | None = Default("protect_content"), allow_paid_broadcast: bool | None = None, @@ -133,13 +154,20 @@ class SendPoll(TelegramMethod[Message]): is_anonymous=is_anonymous, type=type, allows_multiple_answers=allows_multiple_answers, - correct_option_id=correct_option_id, + allows_revoting=allows_revoting, + shuffle_options=shuffle_options, + allow_adding_options=allow_adding_options, + hide_results_until_closes=hide_results_until_closes, + correct_option_ids=correct_option_ids, explanation=explanation, explanation_parse_mode=explanation_parse_mode, explanation_entities=explanation_entities, open_period=open_period, close_date=close_date, is_closed=is_closed, + description=description, + description_parse_mode=description_parse_mode, + description_entities=description_entities, disable_notification=disable_notification, protect_content=protect_content, allow_paid_broadcast=allow_paid_broadcast, diff --git a/aiogram/types/__init__.py b/aiogram/types/__init__.py index 90e59515..0d0783bb 100644 --- a/aiogram/types/__init__.py +++ b/aiogram/types/__init__.py @@ -171,6 +171,7 @@ from .invoice import Invoice from .keyboard_button import KeyboardButton from .keyboard_button_poll_type import KeyboardButtonPollType from .keyboard_button_request_chat import KeyboardButtonRequestChat +from .keyboard_button_request_managed_bot import KeyboardButtonRequestManagedBot from .keyboard_button_request_user import KeyboardButtonRequestUser from .keyboard_button_request_users import KeyboardButtonRequestUsers from .labeled_price import LabeledPrice @@ -178,6 +179,8 @@ from .link_preview_options import LinkPreviewOptions from .location import Location from .location_address import LocationAddress from .login_url import LoginUrl +from .managed_bot_created import ManagedBotCreated +from .managed_bot_updated import ManagedBotUpdated from .mask_position import MaskPosition from .maybe_inaccessible_message import MaybeInaccessibleMessage from .maybe_inaccessible_message_union import MaybeInaccessibleMessageUnion @@ -232,8 +235,11 @@ from .photo_size import PhotoSize from .poll import Poll from .poll_answer import PollAnswer from .poll_option import PollOption +from .poll_option_added import PollOptionAdded +from .poll_option_deleted import PollOptionDeleted from .pre_checkout_query import PreCheckoutQuery from .prepared_inline_message import PreparedInlineMessage +from .prepared_keyboard_button import PreparedKeyboardButton from .proximity_alert_triggered import ProximityAlertTriggered from .reaction_count import ReactionCount from .reaction_type import ReactionType @@ -495,6 +501,7 @@ __all__ = ( "KeyboardButton", "KeyboardButtonPollType", "KeyboardButtonRequestChat", + "KeyboardButtonRequestManagedBot", "KeyboardButtonRequestUser", "KeyboardButtonRequestUsers", "LabeledPrice", @@ -502,6 +509,8 @@ __all__ = ( "Location", "LocationAddress", "LoginUrl", + "ManagedBotCreated", + "ManagedBotUpdated", "MaskPosition", "MaybeInaccessibleMessage", "MaybeInaccessibleMessageUnion", @@ -554,8 +563,11 @@ __all__ = ( "Poll", "PollAnswer", "PollOption", + "PollOptionAdded", + "PollOptionDeleted", "PreCheckoutQuery", "PreparedInlineMessage", + "PreparedKeyboardButton", "ProximityAlertTriggered", "ReactionCount", "ReactionType", diff --git a/aiogram/types/chat_join_request.py b/aiogram/types/chat_join_request.py index 899e567b..51be4d29 100644 --- a/aiogram/types/chat_join_request.py +++ b/aiogram/types/chat_join_request.py @@ -1783,7 +1783,7 @@ class ChatJoinRequest(TelegramObject): is_anonymous: bool | None = None, type: str | None = None, allows_multiple_answers: bool | None = None, - correct_option_id: int | None = None, + correct_option_ids: list[int] | None = None, explanation: str | None = None, explanation_parse_mode: str | Default | None = Default("parse_mode"), explanation_entities: list[MessageEntity] | None = None, @@ -1819,7 +1819,7 @@ class ChatJoinRequest(TelegramObject): :param is_anonymous: :code:`True`, if the poll needs to be anonymous, defaults to :code:`True` :param type: Poll type, 'quiz' or 'regular', defaults to 'regular' :param allows_multiple_answers: :code:`True`, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to :code:`False` - :param correct_option_id: 0-based identifier of the correct answer option, required for polls in quiz mode + :param correct_option_ids: A JSON-serialized list of 0-based identifiers of the correct answer options, required for polls in quiz mode :param explanation: Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing :param explanation_parse_mode: Mode for parsing entities in the explanation. See `formatting options `_ for more details. :param explanation_entities: A JSON-serialized list of special entities that appear in the poll explanation. It can be specified instead of *explanation_parse_mode* @@ -1852,7 +1852,7 @@ class ChatJoinRequest(TelegramObject): is_anonymous=is_anonymous, type=type, allows_multiple_answers=allows_multiple_answers, - correct_option_id=correct_option_id, + correct_option_ids=correct_option_ids, explanation=explanation, explanation_parse_mode=explanation_parse_mode, explanation_entities=explanation_entities, @@ -1881,7 +1881,7 @@ class ChatJoinRequest(TelegramObject): is_anonymous: bool | None = None, type: str | None = None, allows_multiple_answers: bool | None = None, - correct_option_id: int | None = None, + correct_option_ids: list[int] | None = None, explanation: str | None = None, explanation_parse_mode: str | Default | None = Default("parse_mode"), explanation_entities: list[MessageEntity] | None = None, @@ -1917,7 +1917,7 @@ class ChatJoinRequest(TelegramObject): :param is_anonymous: :code:`True`, if the poll needs to be anonymous, defaults to :code:`True` :param type: Poll type, 'quiz' or 'regular', defaults to 'regular' :param allows_multiple_answers: :code:`True`, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to :code:`False` - :param correct_option_id: 0-based identifier of the correct answer option, required for polls in quiz mode + :param correct_option_ids: A JSON-serialized list of 0-based identifiers of the correct answer options, required for polls in quiz mode :param explanation: Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing :param explanation_parse_mode: Mode for parsing entities in the explanation. See `formatting options `_ for more details. :param explanation_entities: A JSON-serialized list of special entities that appear in the poll explanation. It can be specified instead of *explanation_parse_mode* @@ -1950,7 +1950,7 @@ class ChatJoinRequest(TelegramObject): is_anonymous=is_anonymous, type=type, allows_multiple_answers=allows_multiple_answers, - correct_option_id=correct_option_id, + correct_option_ids=correct_option_ids, explanation=explanation, explanation_parse_mode=explanation_parse_mode, explanation_entities=explanation_entities, diff --git a/aiogram/types/chat_member_updated.py b/aiogram/types/chat_member_updated.py index b693f491..8fafddc0 100644 --- a/aiogram/types/chat_member_updated.py +++ b/aiogram/types/chat_member_updated.py @@ -924,7 +924,7 @@ class ChatMemberUpdated(TelegramObject): is_anonymous: bool | None = None, type: str | None = None, allows_multiple_answers: bool | None = None, - correct_option_id: int | None = None, + correct_option_ids: list[int] | None = None, explanation: str | None = None, explanation_parse_mode: str | Default | None = Default("parse_mode"), explanation_entities: list[MessageEntity] | None = None, @@ -960,7 +960,7 @@ class ChatMemberUpdated(TelegramObject): :param is_anonymous: :code:`True`, if the poll needs to be anonymous, defaults to :code:`True` :param type: Poll type, 'quiz' or 'regular', defaults to 'regular' :param allows_multiple_answers: :code:`True`, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to :code:`False` - :param correct_option_id: 0-based identifier of the correct answer option, required for polls in quiz mode + :param correct_option_ids: A JSON-serialized list of 0-based identifiers of the correct answer options, required for polls in quiz mode :param explanation: Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing :param explanation_parse_mode: Mode for parsing entities in the explanation. See `formatting options `_ for more details. :param explanation_entities: A JSON-serialized list of special entities that appear in the poll explanation. It can be specified instead of *explanation_parse_mode* @@ -993,7 +993,7 @@ class ChatMemberUpdated(TelegramObject): is_anonymous=is_anonymous, type=type, allows_multiple_answers=allows_multiple_answers, - correct_option_id=correct_option_id, + correct_option_ids=correct_option_ids, explanation=explanation, explanation_parse_mode=explanation_parse_mode, explanation_entities=explanation_entities, diff --git a/aiogram/types/inaccessible_message.py b/aiogram/types/inaccessible_message.py index 8b8a9f1f..9bcd0f02 100644 --- a/aiogram/types/inaccessible_message.py +++ b/aiogram/types/inaccessible_message.py @@ -1746,7 +1746,7 @@ class InaccessibleMessage(MaybeInaccessibleMessage): is_anonymous: bool | None = None, type: str | None = None, allows_multiple_answers: bool | None = None, - correct_option_id: int | None = None, + correct_option_ids: list[int] | None = None, explanation: str | None = None, explanation_parse_mode: str | Default | None = Default("parse_mode"), explanation_entities: list[MessageEntity] | None = None, @@ -1782,7 +1782,7 @@ class InaccessibleMessage(MaybeInaccessibleMessage): :param is_anonymous: :code:`True`, if the poll needs to be anonymous, defaults to :code:`True` :param type: Poll type, 'quiz' or 'regular', defaults to 'regular' :param allows_multiple_answers: :code:`True`, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to :code:`False` - :param correct_option_id: 0-based identifier of the correct answer option, required for polls in quiz mode + :param correct_option_ids: A JSON-serialized list of 0-based identifiers of the correct answer options, required for polls in quiz mode :param explanation: Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing :param explanation_parse_mode: Mode for parsing entities in the explanation. See `formatting options `_ for more details. :param explanation_entities: A JSON-serialized list of special entities that appear in the poll explanation. It can be specified instead of *explanation_parse_mode* @@ -1819,7 +1819,7 @@ class InaccessibleMessage(MaybeInaccessibleMessage): is_anonymous=is_anonymous, type=type, allows_multiple_answers=allows_multiple_answers, - correct_option_id=correct_option_id, + correct_option_ids=correct_option_ids, explanation=explanation, explanation_parse_mode=explanation_parse_mode, explanation_entities=explanation_entities, @@ -1848,7 +1848,7 @@ class InaccessibleMessage(MaybeInaccessibleMessage): is_anonymous: bool | None = None, type: str | None = None, allows_multiple_answers: bool | None = None, - correct_option_id: int | None = None, + correct_option_ids: list[int] | None = None, explanation: str | None = None, explanation_parse_mode: str | Default | None = Default("parse_mode"), explanation_entities: list[MessageEntity] | None = None, @@ -1883,7 +1883,7 @@ class InaccessibleMessage(MaybeInaccessibleMessage): :param is_anonymous: :code:`True`, if the poll needs to be anonymous, defaults to :code:`True` :param type: Poll type, 'quiz' or 'regular', defaults to 'regular' :param allows_multiple_answers: :code:`True`, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to :code:`False` - :param correct_option_id: 0-based identifier of the correct answer option, required for polls in quiz mode + :param correct_option_ids: A JSON-serialized list of 0-based identifiers of the correct answer options, required for polls in quiz mode :param explanation: Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing :param explanation_parse_mode: Mode for parsing entities in the explanation. See `formatting options `_ for more details. :param explanation_entities: A JSON-serialized list of special entities that appear in the poll explanation. It can be specified instead of *explanation_parse_mode* @@ -1919,7 +1919,7 @@ class InaccessibleMessage(MaybeInaccessibleMessage): is_anonymous=is_anonymous, type=type, allows_multiple_answers=allows_multiple_answers, - correct_option_id=correct_option_id, + correct_option_ids=correct_option_ids, explanation=explanation, explanation_parse_mode=explanation_parse_mode, explanation_entities=explanation_entities, diff --git a/aiogram/types/keyboard_button.py b/aiogram/types/keyboard_button.py index f0c4af22..ddc1fc05 100644 --- a/aiogram/types/keyboard_button.py +++ b/aiogram/types/keyboard_button.py @@ -9,6 +9,7 @@ from .base import MutableTelegramObject if TYPE_CHECKING: from .keyboard_button_poll_type import KeyboardButtonPollType from .keyboard_button_request_chat import KeyboardButtonRequestChat + from .keyboard_button_request_managed_bot import KeyboardButtonRequestManagedBot from .keyboard_button_request_user import KeyboardButtonRequestUser from .keyboard_button_request_users import KeyboardButtonRequestUsers from .web_app_info import WebAppInfo @@ -31,6 +32,8 @@ class KeyboardButton(MutableTelegramObject): """*Optional*. If specified, pressing the button will open a list of suitable users. Identifiers of selected users will be sent to the bot in a 'users_shared' service message. Available in private chats only.""" request_chat: KeyboardButtonRequestChat | None = None """*Optional*. If specified, pressing the button will open a list of suitable chats. Tapping on a chat will send its identifier to the bot in a 'chat_shared' service message. Available in private chats only.""" + request_managed_bot: KeyboardButtonRequestManagedBot | None = None + """*Optional*. If specified, pressing the button will request creation or selection of a managed bot. Available in private chats only.""" request_contact: bool | None = 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.""" request_location: bool | None = None @@ -59,6 +62,7 @@ class KeyboardButton(MutableTelegramObject): style: str | None = None, request_users: KeyboardButtonRequestUsers | None = None, request_chat: KeyboardButtonRequestChat | None = None, + request_managed_bot: KeyboardButtonRequestManagedBot | None = None, request_contact: bool | None = None, request_location: bool | None = None, request_poll: KeyboardButtonPollType | None = None, @@ -76,6 +80,7 @@ class KeyboardButton(MutableTelegramObject): style=style, request_users=request_users, request_chat=request_chat, + request_managed_bot=request_managed_bot, request_contact=request_contact, request_location=request_location, request_poll=request_poll, diff --git a/aiogram/types/keyboard_button_request_managed_bot.py b/aiogram/types/keyboard_button_request_managed_bot.py new file mode 100644 index 00000000..fac31bdd --- /dev/null +++ b/aiogram/types/keyboard_button_request_managed_bot.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from .base import TelegramObject + + +class KeyboardButtonRequestManagedBot(TelegramObject): + """ + This object defines the parameters for the creation of a managed bot. Information about the created bot will be shared with the bot using the update *managed_bot* and a :class:`aiogram.types.message.Message` with the field *managed_bot_created*. + + Source: https://core.telegram.org/bots/api#keyboardbuttonrequestmanagedbot + """ + + request_id: int + """Signed 32-bit identifier of the request. Must be unique within the message""" + suggested_name: str | None = None + """*Optional*. Suggested name for the bot""" + suggested_username: str | None = None + """*Optional*. Suggested username for the bot""" + + if TYPE_CHECKING: + + def __init__( + __pydantic__self__, + *, + request_id: int, + suggested_name: str | None = None, + suggested_username: str | None = None, + **__pydantic_kwargs: Any, + ) -> None: + super().__init__( + request_id=request_id, + suggested_name=suggested_name, + suggested_username=suggested_username, + **__pydantic_kwargs, + ) diff --git a/aiogram/types/managed_bot_created.py b/aiogram/types/managed_bot_created.py new file mode 100644 index 00000000..2620cbe6 --- /dev/null +++ b/aiogram/types/managed_bot_created.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from pydantic import Field + +from .base import TelegramObject + +if TYPE_CHECKING: + from .user import User + + +class ManagedBotCreated(TelegramObject): + """ + This object represents a service message about a bot created to be managed by the current bot. + + Source: https://core.telegram.org/bots/api#managedbotcreated + """ + + managed_bot: User = Field(alias="bot") + """Information about the bot. The bot's token can be fetched using the method getManagedBotToken.""" + + if TYPE_CHECKING: + + def __init__( + __pydantic__self__, + *, + bot: User, + **__pydantic_kwargs: Any, + ) -> None: + super().__init__( + bot=bot, + **__pydantic_kwargs, + ) diff --git a/aiogram/types/managed_bot_updated.py b/aiogram/types/managed_bot_updated.py new file mode 100644 index 00000000..276669c7 --- /dev/null +++ b/aiogram/types/managed_bot_updated.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from pydantic import Field + +from .base import TelegramObject + +if TYPE_CHECKING: + from .user import User + + +class ManagedBotUpdated(TelegramObject): + """ + This object represents the creation or token update of a managed bot. + + Source: https://core.telegram.org/bots/api#managedbotupdated + """ + + user: User + """The user who created or updated the managed bot""" + managed_bot: User = Field(alias="bot") + """Information about the managed bot. Token of the bot can be fetched using the method getManagedBotToken.""" + + if TYPE_CHECKING: + + def __init__( + __pydantic__self__, + *, + user: User, + bot: User, + **__pydantic_kwargs: Any, + ) -> None: + super().__init__( + user=user, + bot=bot, + **__pydantic_kwargs, + ) diff --git a/aiogram/types/message.py b/aiogram/types/message.py index 1b7f9677..37b887ba 100644 --- a/aiogram/types/message.py +++ b/aiogram/types/message.py @@ -90,6 +90,7 @@ if TYPE_CHECKING: from .labeled_price import LabeledPrice from .link_preview_options import LinkPreviewOptions from .location import Location + from .managed_bot_created import ManagedBotCreated from .maybe_inaccessible_message_union import MaybeInaccessibleMessageUnion from .media_union import MediaUnion from .message_auto_delete_timer_changed import MessageAutoDeleteTimerChanged @@ -100,6 +101,8 @@ if TYPE_CHECKING: from .passport_data import PassportData from .photo_size import PhotoSize from .poll import Poll + from .poll_option_added import PollOptionAdded + from .poll_option_deleted import PollOptionDeleted from .proximity_alert_triggered import ProximityAlertTriggered from .reaction_type_union import ReactionTypeUnion from .refunded_payment import RefundedPayment @@ -177,6 +180,8 @@ class Message(MaybeInaccessibleMessage): """*Optional*. For replies to a story, the original story""" reply_to_checklist_task_id: int | None = None """*Optional*. Identifier of the specific checklist task that is being replied to""" + reply_to_poll_option_id: str | None = None + """*Optional*. Persistent identifier of the specific poll option that is being replied to""" via_bot: User | None = None """*Optional*. Bot through which the message was sent""" edit_date: int | None = None @@ -327,8 +332,14 @@ class Message(MaybeInaccessibleMessage): """*Optional*. A giveaway with public winners was completed""" giveaway_completed: GiveawayCompleted | None = None """*Optional*. Service message: a giveaway without public winners was completed""" + managed_bot_created: ManagedBotCreated | None = None + """*Optional*. Service message: a bot was created to be managed by the current bot""" paid_message_price_changed: PaidMessagePriceChanged | None = None """*Optional*. Service message: the price for paid messages has changed in the chat""" + poll_option_added: PollOptionAdded | None = None + """*Optional*. Service message: an option was added to a poll""" + poll_option_deleted: PollOptionDeleted | None = None + """*Optional*. Service message: an option was removed from a poll""" suggested_post_approved: SuggestedPostApproved | None = None """*Optional*. Service message: a suggested post was approved""" suggested_post_approval_failed: SuggestedPostApprovalFailed | None = None @@ -413,6 +424,7 @@ class Message(MaybeInaccessibleMessage): quote: TextQuote | None = None, reply_to_story: Story | None = None, reply_to_checklist_task_id: int | None = None, + reply_to_poll_option_id: str | None = None, via_bot: User | None = None, edit_date: int | None = None, has_protected_content: bool | None = None, @@ -488,7 +500,10 @@ class Message(MaybeInaccessibleMessage): giveaway: Giveaway | None = None, giveaway_winners: GiveawayWinners | None = None, giveaway_completed: GiveawayCompleted | None = None, + managed_bot_created: ManagedBotCreated | None = None, paid_message_price_changed: PaidMessagePriceChanged | None = None, + poll_option_added: PollOptionAdded | None = None, + poll_option_deleted: PollOptionDeleted | None = None, suggested_post_approved: SuggestedPostApproved | None = None, suggested_post_approval_failed: SuggestedPostApprovalFailed | None = None, suggested_post_declined: SuggestedPostDeclined | None = None, @@ -533,6 +548,7 @@ class Message(MaybeInaccessibleMessage): quote=quote, reply_to_story=reply_to_story, reply_to_checklist_task_id=reply_to_checklist_task_id, + reply_to_poll_option_id=reply_to_poll_option_id, via_bot=via_bot, edit_date=edit_date, has_protected_content=has_protected_content, @@ -608,7 +624,10 @@ class Message(MaybeInaccessibleMessage): giveaway=giveaway, giveaway_winners=giveaway_winners, giveaway_completed=giveaway_completed, + managed_bot_created=managed_bot_created, paid_message_price_changed=paid_message_price_changed, + poll_option_added=poll_option_added, + poll_option_deleted=poll_option_deleted, suggested_post_approved=suggested_post_approved, suggested_post_approval_failed=suggested_post_approval_failed, suggested_post_declined=suggested_post_declined, @@ -764,6 +783,12 @@ class Message(MaybeInaccessibleMessage): return ContentType.GIFT_UPGRADE_SENT if self.paid_message_price_changed: return ContentType.PAID_MESSAGE_PRICE_CHANGED + if self.managed_bot_created: + return ContentType.MANAGED_BOT_CREATED + if self.poll_option_added: + return ContentType.POLL_OPTION_ADDED + if self.poll_option_deleted: + return ContentType.POLL_OPTION_DELETED if self.suggested_post_approved: return ContentType.SUGGESTED_POST_APPROVED if self.suggested_post_approval_failed: @@ -2444,7 +2469,7 @@ class Message(MaybeInaccessibleMessage): is_anonymous: bool | None = None, type: str | None = None, allows_multiple_answers: bool | None = None, - correct_option_id: int | None = None, + correct_option_ids: list[int] | None = None, explanation: str | None = None, explanation_parse_mode: str | Default | None = Default("parse_mode"), explanation_entities: list[MessageEntity] | None = None, @@ -2479,7 +2504,7 @@ class Message(MaybeInaccessibleMessage): :param is_anonymous: :code:`True`, if the poll needs to be anonymous, defaults to :code:`True` :param type: Poll type, 'quiz' or 'regular', defaults to 'regular' :param allows_multiple_answers: :code:`True`, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to :code:`False` - :param correct_option_id: 0-based identifier of the correct answer option, required for polls in quiz mode + :param correct_option_ids: A JSON-serialized list of 0-based identifiers of the correct answer options, required for polls in quiz mode :param explanation: Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing :param explanation_parse_mode: Mode for parsing entities in the explanation. See `formatting options `_ for more details. :param explanation_entities: A JSON-serialized list of special entities that appear in the poll explanation. It can be specified instead of *explanation_parse_mode* @@ -2515,7 +2540,7 @@ class Message(MaybeInaccessibleMessage): is_anonymous=is_anonymous, type=type, allows_multiple_answers=allows_multiple_answers, - correct_option_id=correct_option_id, + correct_option_ids=correct_option_ids, explanation=explanation, explanation_parse_mode=explanation_parse_mode, explanation_entities=explanation_entities, @@ -2540,7 +2565,7 @@ class Message(MaybeInaccessibleMessage): is_anonymous: bool | None = None, type: str | None = None, allows_multiple_answers: bool | None = None, - correct_option_id: int | None = None, + correct_option_ids: list[int] | None = None, explanation: str | None = None, explanation_parse_mode: str | Default | None = Default("parse_mode"), explanation_entities: list[MessageEntity] | None = None, @@ -2576,7 +2601,7 @@ class Message(MaybeInaccessibleMessage): :param is_anonymous: :code:`True`, if the poll needs to be anonymous, defaults to :code:`True` :param type: Poll type, 'quiz' or 'regular', defaults to 'regular' :param allows_multiple_answers: :code:`True`, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to :code:`False` - :param correct_option_id: 0-based identifier of the correct answer option, required for polls in quiz mode + :param correct_option_ids: A JSON-serialized list of 0-based identifiers of the correct answer options, required for polls in quiz mode :param explanation: Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters with at most 2 line feeds after entities parsing :param explanation_parse_mode: Mode for parsing entities in the explanation. See `formatting options `_ for more details. :param explanation_entities: A JSON-serialized list of special entities that appear in the poll explanation. It can be specified instead of *explanation_parse_mode* @@ -2613,7 +2638,7 @@ class Message(MaybeInaccessibleMessage): is_anonymous=is_anonymous, type=type, allows_multiple_answers=allows_multiple_answers, - correct_option_id=correct_option_id, + correct_option_ids=correct_option_ids, explanation=explanation, explanation_parse_mode=explanation_parse_mode, explanation_entities=explanation_entities, diff --git a/aiogram/types/poll.py b/aiogram/types/poll.py index d9eff35a..0921df0f 100644 --- a/aiogram/types/poll.py +++ b/aiogram/types/poll.py @@ -33,10 +33,12 @@ class Poll(TelegramObject): """Poll type, currently can be 'regular' or 'quiz'""" allows_multiple_answers: bool """:code:`True`, if the poll allows multiple answers""" + allows_revoting: bool + """:code:`True`, if voters can change their answer""" question_entities: list[MessageEntity] | None = None """*Optional*. Special entities that appear in the *question*. Currently, only custom emoji entities are allowed in poll questions""" - correct_option_id: int | None = None - """*Optional*. 0-based identifier of the correct answer option. Available only for polls in the quiz mode, which are closed, or was sent (not forwarded) by the bot or to the private chat with the bot.""" + correct_option_ids: list[int] | None = None + """*Optional*. 0-based identifiers of the correct answer options. Available only for polls in the quiz mode, which are closed, or was sent (not forwarded) by the bot or to the private chat with the bot.""" explanation: str | None = None """*Optional*. Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters""" explanation_entities: list[MessageEntity] | None = None @@ -45,6 +47,10 @@ class Poll(TelegramObject): """*Optional*. Amount of time in seconds the poll will be active after creation""" close_date: DateTime | None = None """*Optional*. Point in time (Unix timestamp) when the poll will be automatically closed""" + description: str | None = None + """*Optional*. Poll description""" + description_entities: list[MessageEntity] | None = None + """*Optional*. Special entities that appear in the *description*""" if TYPE_CHECKING: # DO NOT EDIT MANUALLY!!! @@ -61,12 +67,15 @@ class Poll(TelegramObject): is_anonymous: bool, type: str, allows_multiple_answers: bool, + allows_revoting: bool, question_entities: list[MessageEntity] | None = None, - correct_option_id: int | None = None, + correct_option_ids: list[int] | None = None, explanation: str | None = None, explanation_entities: list[MessageEntity] | None = None, open_period: int | None = None, close_date: DateTime | None = None, + description: str | None = None, + description_entities: list[MessageEntity] | None = None, **__pydantic_kwargs: Any, ) -> None: # DO NOT EDIT MANUALLY!!! @@ -82,11 +91,14 @@ class Poll(TelegramObject): is_anonymous=is_anonymous, type=type, allows_multiple_answers=allows_multiple_answers, + allows_revoting=allows_revoting, question_entities=question_entities, - correct_option_id=correct_option_id, + correct_option_ids=correct_option_ids, explanation=explanation, explanation_entities=explanation_entities, open_period=open_period, close_date=close_date, + description=description, + description_entities=description_entities, **__pydantic_kwargs, ) diff --git a/aiogram/types/poll_answer.py b/aiogram/types/poll_answer.py index 030b2379..fc73c9f5 100644 --- a/aiogram/types/poll_answer.py +++ b/aiogram/types/poll_answer.py @@ -24,27 +24,25 @@ class PollAnswer(TelegramObject): """*Optional*. The chat that changed the answer to the poll, if the voter is anonymous""" user: User | None = None """*Optional*. The user that changed the answer to the poll, if the voter isn't anonymous""" + option_persistent_ids: list[str] + """Persistent identifiers of the chosen options. May be empty if the vote was retracted.""" if TYPE_CHECKING: - # DO NOT EDIT MANUALLY!!! - # This section was auto-generated via `butcher` def __init__( __pydantic__self__, *, poll_id: str, option_ids: list[int], + option_persistent_ids: list[str], voter_chat: Chat | None = None, user: User | None = None, **__pydantic_kwargs: Any, ) -> None: - # DO NOT EDIT MANUALLY!!! - # This method was auto-generated via `butcher` - # Is needed only for type checking and IDE support without any additional plugins - super().__init__( poll_id=poll_id, option_ids=option_ids, + option_persistent_ids=option_persistent_ids, voter_chat=voter_chat, user=user, **__pydantic_kwargs, diff --git a/aiogram/types/poll_option.py b/aiogram/types/poll_option.py index 5a91ab3d..72cf544c 100644 --- a/aiogram/types/poll_option.py +++ b/aiogram/types/poll_option.py @@ -3,9 +3,12 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any from .base import TelegramObject +from .custom import DateTime if TYPE_CHECKING: + from .chat import Chat from .message_entity import MessageEntity + from .user import User class PollOption(TelegramObject): @@ -15,32 +18,42 @@ class PollOption(TelegramObject): Source: https://core.telegram.org/bots/api#polloption """ + persistent_id: str + """Unique identifier of the option, persistent across option additions and deletions""" text: str """Option text, 1-100 characters""" voter_count: int """Number of users that voted for this option""" text_entities: list[MessageEntity] | None = None """*Optional*. Special entities that appear in the option *text*. Currently, only custom emoji entities are allowed in poll option texts""" + added_by_user: User | None = None + """*Optional*. The user who added the option, if it was added after poll creation""" + added_by_chat: Chat | None = None + """*Optional*. The chat that added the option, if it was added after poll creation""" + addition_date: DateTime | None = None + """*Optional*. Point in time (Unix timestamp) when the option was added""" if TYPE_CHECKING: - # DO NOT EDIT MANUALLY!!! - # This section was auto-generated via `butcher` def __init__( __pydantic__self__, *, + persistent_id: str, text: str, voter_count: int, text_entities: list[MessageEntity] | None = None, + added_by_user: User | None = None, + added_by_chat: Chat | None = None, + addition_date: DateTime | None = None, **__pydantic_kwargs: Any, ) -> None: - # DO NOT EDIT MANUALLY!!! - # This method was auto-generated via `butcher` - # Is needed only for type checking and IDE support without any additional plugins - super().__init__( + persistent_id=persistent_id, text=text, voter_count=voter_count, text_entities=text_entities, + added_by_user=added_by_user, + added_by_chat=added_by_chat, + addition_date=addition_date, **__pydantic_kwargs, ) diff --git a/aiogram/types/poll_option_added.py b/aiogram/types/poll_option_added.py new file mode 100644 index 00000000..ec8ad140 --- /dev/null +++ b/aiogram/types/poll_option_added.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from .base import TelegramObject + +if TYPE_CHECKING: + from .maybe_inaccessible_message_union import MaybeInaccessibleMessageUnion + from .message_entity import MessageEntity + + +class PollOptionAdded(TelegramObject): + """ + This object represents a service message about an option added to a poll. + + Source: https://core.telegram.org/bots/api#polloptionadded + """ + + option_persistent_id: str + """Unique identifier of the added option""" + option_text: str + """Text of the added option, 1-100 characters""" + poll_message: MaybeInaccessibleMessageUnion | None = None + """*Optional*. The message containing the poll to which the option was added. Note that the :class:`aiogram.types.message.Message` object in this field will not contain the *reply_to_message* field even if it itself is a reply.""" + option_text_entities: list[MessageEntity] | None = None + """*Optional*. Special entities that appear in the option text. Currently, only custom emoji entities are allowed in poll option texts""" + + if TYPE_CHECKING: + + def __init__( + __pydantic__self__, + *, + option_persistent_id: str, + option_text: str, + poll_message: MaybeInaccessibleMessageUnion | None = None, + option_text_entities: list[MessageEntity] | None = None, + **__pydantic_kwargs: Any, + ) -> None: + super().__init__( + option_persistent_id=option_persistent_id, + option_text=option_text, + poll_message=poll_message, + option_text_entities=option_text_entities, + **__pydantic_kwargs, + ) diff --git a/aiogram/types/poll_option_deleted.py b/aiogram/types/poll_option_deleted.py new file mode 100644 index 00000000..10aca0cb --- /dev/null +++ b/aiogram/types/poll_option_deleted.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from .base import TelegramObject + +if TYPE_CHECKING: + from .maybe_inaccessible_message_union import MaybeInaccessibleMessageUnion + from .message_entity import MessageEntity + + +class PollOptionDeleted(TelegramObject): + """ + This object represents a service message about an option deleted from a poll. + + Source: https://core.telegram.org/bots/api#polloptiondeleted + """ + + option_persistent_id: str + """Unique identifier of the deleted option""" + option_text: str + """Text of the deleted option, 1-100 characters""" + poll_message: MaybeInaccessibleMessageUnion | None = None + """*Optional*. The message containing the poll from which the option was deleted. Note that the :class:`aiogram.types.message.Message` object in this field will not contain the *reply_to_message* field even if it itself is a reply.""" + option_text_entities: list[MessageEntity] | None = None + """*Optional*. Special entities that appear in the option text. Currently, only custom emoji entities are allowed in poll option texts""" + + if TYPE_CHECKING: + + def __init__( + __pydantic__self__, + *, + option_persistent_id: str, + option_text: str, + poll_message: MaybeInaccessibleMessageUnion | None = None, + option_text_entities: list[MessageEntity] | None = None, + **__pydantic_kwargs: Any, + ) -> None: + super().__init__( + option_persistent_id=option_persistent_id, + option_text=option_text, + poll_message=poll_message, + option_text_entities=option_text_entities, + **__pydantic_kwargs, + ) diff --git a/aiogram/types/prepared_keyboard_button.py b/aiogram/types/prepared_keyboard_button.py new file mode 100644 index 00000000..29c35013 --- /dev/null +++ b/aiogram/types/prepared_keyboard_button.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from .base import TelegramObject + + +class PreparedKeyboardButton(TelegramObject): + """ + This object represents a prepared keyboard button that allows bots to request users, chats, and managed bots from Mini Apps. + + Source: https://core.telegram.org/bots/api#preparedkeyboardbutton + """ + + id: str + """Unique identifier of the prepared button""" + + if TYPE_CHECKING: + + def __init__( + __pydantic__self__, + *, + id: str, + **__pydantic_kwargs: Any, + ) -> None: + super().__init__( + id=id, + **__pydantic_kwargs, + ) diff --git a/aiogram/types/reply_parameters.py b/aiogram/types/reply_parameters.py index 3ad9131d..3d9fd356 100644 --- a/aiogram/types/reply_parameters.py +++ b/aiogram/types/reply_parameters.py @@ -33,6 +33,8 @@ class ReplyParameters(TelegramObject): """*Optional*. Position of the quote in the original message in UTF-16 code units""" checklist_task_id: int | None = None """*Optional*. Identifier of the specific checklist task to be replied to""" + poll_option_id: str | None = None + """*Optional*. Persistent identifier of a specific poll option to reply to""" if TYPE_CHECKING: # DO NOT EDIT MANUALLY!!! @@ -51,6 +53,7 @@ class ReplyParameters(TelegramObject): quote_entities: list[MessageEntity] | None = None, quote_position: int | None = None, checklist_task_id: int | None = None, + poll_option_id: str | None = None, **__pydantic_kwargs: Any, ) -> None: # DO NOT EDIT MANUALLY!!! @@ -66,5 +69,6 @@ class ReplyParameters(TelegramObject): quote_entities=quote_entities, quote_position=quote_position, checklist_task_id=checklist_task_id, + poll_option_id=poll_option_id, **__pydantic_kwargs, ) diff --git a/aiogram/types/update.py b/aiogram/types/update.py index aab31339..b37bb0b4 100644 --- a/aiogram/types/update.py +++ b/aiogram/types/update.py @@ -15,6 +15,7 @@ if TYPE_CHECKING: from .chat_member_updated import ChatMemberUpdated from .chosen_inline_result import ChosenInlineResult from .inline_query import InlineQuery + from .managed_bot_updated import ManagedBotUpdated from .message import Message from .message_reaction_count_updated import MessageReactionCountUpdated from .message_reaction_updated import MessageReactionUpdated @@ -82,6 +83,8 @@ class Update(TelegramObject): """*Optional*. A chat boost was added or changed. The bot must be an administrator in the chat to receive these updates.""" removed_chat_boost: ChatBoostRemoved | None = None """*Optional*. A boost was removed from a chat. The bot must be an administrator in the chat to receive these updates.""" + managed_bot: ManagedBotUpdated | None = None + """*Optional*. A managed bot was created or its token was updated""" if TYPE_CHECKING: # DO NOT EDIT MANUALLY!!! @@ -114,6 +117,7 @@ class Update(TelegramObject): chat_join_request: ChatJoinRequest | None = None, chat_boost: ChatBoostUpdated | None = None, removed_chat_boost: ChatBoostRemoved | None = None, + managed_bot: ManagedBotUpdated | None = None, **__pydantic_kwargs: Any, ) -> None: # DO NOT EDIT MANUALLY!!! @@ -145,6 +149,7 @@ class Update(TelegramObject): chat_join_request=chat_join_request, chat_boost=chat_boost, removed_chat_boost=removed_chat_boost, + managed_bot=managed_bot, **__pydantic_kwargs, ) @@ -206,6 +211,8 @@ class Update(TelegramObject): return "business_message" if self.purchased_paid_media: return "purchased_paid_media" + if self.managed_bot: + return "managed_bot" raise UpdateTypeLookupError("Update does not contain any known event type.") diff --git a/aiogram/types/user.py b/aiogram/types/user.py index 7f03504c..8dadaea6 100644 --- a/aiogram/types/user.py +++ b/aiogram/types/user.py @@ -47,6 +47,8 @@ class User(TelegramObject): """*Optional*. :code:`True`, if the bot has forum topic mode enabled in private chats. Returned only in :class:`aiogram.methods.get_me.GetMe`.""" allows_users_to_create_topics: bool | None = None """*Optional*. :code:`True`, if the bot allows users to create and delete topics in private chats. Returned only in :class:`aiogram.methods.get_me.GetMe`.""" + can_manage_bots: bool | None = None + """*Optional*. :code:`True`, if the bot can create other bots it controls. Returned only in :class:`aiogram.methods.get_me.GetMe`.""" if TYPE_CHECKING: # DO NOT EDIT MANUALLY!!! @@ -70,6 +72,7 @@ class User(TelegramObject): has_main_web_app: bool | None = None, has_topics_enabled: bool | None = None, allows_users_to_create_topics: bool | None = None, + can_manage_bots: bool | None = None, **__pydantic_kwargs: Any, ) -> None: # DO NOT EDIT MANUALLY!!! @@ -92,6 +95,7 @@ class User(TelegramObject): has_main_web_app=has_main_web_app, has_topics_enabled=has_topics_enabled, allows_users_to_create_topics=allows_users_to_create_topics, + can_manage_bots=can_manage_bots, **__pydantic_kwargs, ) diff --git a/tests/test_api/test_methods/test_send_poll.py b/tests/test_api/test_methods/test_send_poll.py index 27288b51..a1ce197d 100644 --- a/tests/test_api/test_methods/test_send_poll.py +++ b/tests/test_api/test_methods/test_send_poll.py @@ -17,22 +17,23 @@ class TestSendPoll: id="QA", question="Q", options=[ - PollOption(text="A", voter_count=0), - PollOption(text="B", voter_count=0), + PollOption(persistent_id="1", text="A", voter_count=0), + PollOption(persistent_id="2", text="B", voter_count=0), ], is_closed=False, is_anonymous=False, type="quiz", allows_multiple_answers=False, + allows_revoting=False, total_voter_count=0, - correct_option_id=0, + correct_option_ids=[0], ), chat=Chat(id=42, type="private"), ), ) response: Message = await bot.send_poll( - chat_id=42, question="Q?", options=["A", "B"], correct_option_id=0, type="quiz" + chat_id=42, question="Q?", options=["A", "B"], correct_option_ids=[0], type="quiz" ) bot.get_request() assert response == prepare_result.result diff --git a/tests/test_api/test_methods/test_stop_poll.py b/tests/test_api/test_methods/test_stop_poll.py index 64d0406c..b2d4b8b1 100644 --- a/tests/test_api/test_methods/test_stop_poll.py +++ b/tests/test_api/test_methods/test_stop_poll.py @@ -11,13 +11,17 @@ class TestStopPoll: result=Poll( id="QA", question="Q", - options=[PollOption(text="A", voter_count=0), PollOption(text="B", voter_count=0)], + options=[ + PollOption(persistent_id="1", text="A", voter_count=0), + PollOption(persistent_id="2", text="B", voter_count=0), + ], is_closed=False, is_anonymous=False, type="quiz", allows_multiple_answers=False, + allows_revoting=False, total_voter_count=0, - correct_option_id=0, + correct_option_ids=[0], ), ) diff --git a/tests/test_api/test_types/test_message.py b/tests/test_api/test_types/test_message.py index 51d1790d..f1f2b06f 100644 --- a/tests/test_api/test_types/test_message.py +++ b/tests/test_api/test_types/test_message.py @@ -76,6 +76,7 @@ from aiogram.types import ( InputMediaPhoto, Invoice, Location, + ManagedBotCreated, MessageAutoDeleteTimerChanged, MessageEntity, PaidMediaInfo, @@ -85,6 +86,8 @@ from aiogram.types import ( PhotoSize, Poll, PollOption, + PollOptionAdded, + PollOptionDeleted, ProximityAlertTriggered, ReactionTypeCustomEmoji, RefundedPayment, @@ -426,15 +429,16 @@ TEST_MESSAGE_POLL = Message( id="QA", question="Q", options=[ - PollOption(text="A", voter_count=0), - PollOption(text="B", voter_count=0), + PollOption(persistent_id="1", text="A", voter_count=0), + PollOption(persistent_id="2", text="B", voter_count=0), ], is_closed=False, is_anonymous=False, type="quiz", allows_multiple_answers=False, + allows_revoting=False, total_voter_count=0, - correct_option_id=1, + correct_option_ids=[1], ), chat=Chat(id=42, type="private"), from_user=User(id=42, is_bot=False, first_name="Test"), @@ -858,6 +862,27 @@ TEST_MESSAGE_SUGGESTED_POST_REFUNDED = Message( from_user=User(id=42, is_bot=False, first_name="Test"), suggested_post_refunded=SuggestedPostRefunded(reason="post_deleted"), ) +TEST_MESSAGE_MANAGED_BOT_CREATED = 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"), + managed_bot_created=ManagedBotCreated(bot=User(id=100, is_bot=True, first_name="ManagedBot")), +) +TEST_MESSAGE_POLL_OPTION_ADDED = 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"), + poll_option_added=PollOptionAdded(option_persistent_id="1", option_text="New option"), +) +TEST_MESSAGE_POLL_OPTION_DELETED = 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"), + poll_option_deleted=PollOptionDeleted(option_persistent_id="1", option_text="Old option"), +) MESSAGES_AND_CONTENT_TYPES = [ [TEST_MESSAGE_TEXT, ContentType.TEXT], @@ -937,6 +962,9 @@ MESSAGES_AND_CONTENT_TYPES = [ [TEST_MESSAGE_SUGGESTED_POST_DECLINED, ContentType.SUGGESTED_POST_DECLINED], [TEST_MESSAGE_SUGGESTED_POST_PAID, ContentType.SUGGESTED_POST_PAID], [TEST_MESSAGE_SUGGESTED_POST_REFUNDED, ContentType.SUGGESTED_POST_REFUNDED], + [TEST_MESSAGE_MANAGED_BOT_CREATED, ContentType.MANAGED_BOT_CREATED], + [TEST_MESSAGE_POLL_OPTION_ADDED, ContentType.POLL_OPTION_ADDED], + [TEST_MESSAGE_POLL_OPTION_DELETED, ContentType.POLL_OPTION_DELETED], [TEST_MESSAGE_UNKNOWN, ContentType.UNKNOWN], ] @@ -1013,6 +1041,9 @@ MESSAGES_AND_COPY_METHODS = [ [TEST_MESSAGE_SUGGESTED_POST_DECLINED, None], [TEST_MESSAGE_SUGGESTED_POST_PAID, None], [TEST_MESSAGE_SUGGESTED_POST_REFUNDED, None], + [TEST_MESSAGE_MANAGED_BOT_CREATED, None], + [TEST_MESSAGE_POLL_OPTION_ADDED, None], + [TEST_MESSAGE_POLL_OPTION_DELETED, None], [TEST_MESSAGE_UNKNOWN, None], ] diff --git a/tests/test_dispatcher/test_dispatcher.py b/tests/test_dispatcher/test_dispatcher.py index ca7d4092..29c2b762 100644 --- a/tests/test_dispatcher/test_dispatcher.py +++ b/tests/test_dispatcher/test_dispatcher.py @@ -30,6 +30,7 @@ from aiogram.types import ( ChatMemberUpdated, ChosenInlineResult, InlineQuery, + ManagedBotUpdated, Message, MessageReactionCountUpdated, MessageReactionUpdated, @@ -380,15 +381,16 @@ class TestDispatcher: id="poll id", question="Q?", options=[ - PollOption(text="A1", voter_count=2), - PollOption(text="A2", voter_count=3), + PollOption(persistent_id="1", text="A1", voter_count=2), + PollOption(persistent_id="2", text="A2", voter_count=3), ], is_closed=False, is_anonymous=False, type="quiz", allows_multiple_answers=False, + allows_revoting=False, total_voter_count=0, - correct_option_id=1, + correct_option_ids=[1], ), ), False, @@ -402,6 +404,7 @@ class TestDispatcher: poll_id="poll id", user=User(id=42, is_bot=False, first_name="Test"), option_ids=[42], + option_persistent_ids=["1"], ), ), False, @@ -600,6 +603,18 @@ class TestDispatcher: False, True, ), + pytest.param( + "managed_bot", + Update( + update_id=42, + managed_bot=ManagedBotUpdated( + user=User(id=42, is_bot=False, first_name="Test"), + bot=User(id=100, is_bot=True, first_name="ManagedBot"), + ), + ), + False, + True, + ), ], ) async def test_listen_update( @@ -655,15 +670,16 @@ class TestDispatcher: id="poll id", question="Q?", options=[ - PollOption(text="A1", voter_count=2), - PollOption(text="A2", voter_count=3), + PollOption(persistent_id="1", text="A1", voter_count=2), + PollOption(persistent_id="2", text="A2", voter_count=3), ], is_closed=False, is_anonymous=False, type="quiz", allows_multiple_answers=False, + allows_revoting=False, total_voter_count=0, - correct_option_id=0, + correct_option_ids=[0], ), ) ) diff --git a/tests/test_handler/test_poll.py b/tests/test_handler/test_poll.py index b070874d..df432ad4 100644 --- a/tests/test_handler/test_poll.py +++ b/tests/test_handler/test_poll.py @@ -9,13 +9,14 @@ class TestShippingQueryHandler: event = Poll( id="query", question="Q?", - options=[PollOption(text="A1", voter_count=1)], + options=[PollOption(persistent_id="1", text="A1", voter_count=1)], is_closed=True, is_anonymous=False, type="quiz", allows_multiple_answers=False, + allows_revoting=False, total_voter_count=0, - correct_option_id=0, + correct_option_ids=[0], ) class MyHandler(PollHandler):