diff --git a/README.md b/README.md index 3e3bdfe2b..19343ee50 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@

A simple, but extensible Python implementation for the Telegram Bot API.

Both synchronous and asynchronous.

-##

Supported Bot API version: Supported Bot API version +##

Supported Bot API version: Supported Bot API version

Official documentation

Official ru documentation

diff --git a/telebot/__init__.py b/telebot/__init__.py index d43e0131c..6a0265c3e 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -249,6 +249,7 @@ def __init__( self.edited_business_message_handlers = [] self.deleted_business_messages_handlers = [] self.purchased_paid_media_handlers = [] + self.managed_bot_handlers = [] self.custom_filters = {} self.state_handlers = [] @@ -726,6 +727,7 @@ def process_new_updates(self, updates: List[types.Update]): new_edited_business_messages = None new_deleted_business_messages = None new_purchased_paid_media = None + new_managed_bots = None for update in updates: if apihelper.ENABLE_MIDDLEWARE and not self.use_class_middlewares: @@ -810,6 +812,9 @@ def process_new_updates(self, updates: List[types.Update]): if update.purchased_paid_media: if new_purchased_paid_media is None: new_purchased_paid_media = [] new_purchased_paid_media.append(update.purchased_paid_media) + if update.managed_bot: + if new_managed_bots is None: new_managed_bots = [] + new_managed_bots.append(update.managed_bot) if new_messages: self.process_new_messages(new_messages) @@ -857,6 +862,8 @@ def process_new_updates(self, updates: List[types.Update]): self.process_new_deleted_business_messages(new_deleted_business_messages) if new_purchased_paid_media: self.process_new_purchased_paid_media(new_purchased_paid_media) + if new_managed_bots: + self.process_new_managed_bot(new_managed_bots) def process_new_messages(self, new_messages): """ @@ -999,6 +1006,12 @@ def process_new_purchased_paid_media(self, new_purchased_paid_media): """ self._notify_command_handlers(self.purchased_paid_media_handlers, new_purchased_paid_media, 'purchased_paid_media') + def process_new_managed_bot(self, new_managed_bots): + """ + :meta private: + """ + self._notify_command_handlers(self.managed_bot_handlers, new_managed_bots, 'managed_bot') + def process_middlewares(self, update): """ :meta private: @@ -5227,6 +5240,33 @@ def get_business_connection(self, business_connection_id: str) -> types.Business apihelper.get_business_connection(self.token, business_connection_id) ) + def get_managed_bot_token(self, user_id: int) -> str: + """ + Use this method to get the token of a managed bot. Returns the token as String on success. + + Telegram documentation: https://core.telegram.org/bots/api#getmanagedbottoken + + :param user_id: User identifier of the managed bot whose token will be returned + :type user_id: :obj:`int` + + :return: Returns the token as String on success. + :rtype: :obj:`str` + """ + return apihelper.get_managed_bot_token(self.token, user_id) + + def replace_managed_bot_token(self, user_id: int) -> 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. + + Telegram documentation: https://core.telegram.org/bots/api#replacemanagedbottoken + + :param user_id: User identifier of the managed bot whose token will be replaced + :type user_id: :obj:`int` + + :return: Returns the new token as String on success. + :rtype: :obj:`str` + """ + return apihelper.replace_managed_bot_token(self.token, user_id) def set_my_commands(self, commands: List[types.BotCommand], scope: Optional[types.BotCommandScope]=None, @@ -6049,7 +6089,6 @@ def create_invoice_link(self, send_email_to_provider=send_email_to_provider, is_flexible=is_flexible ,subscription_period=subscription_period, business_connection_id=business_connection_id) - # noinspection PyShadowingBuiltins def send_poll( self, chat_id: Union[int, str], question: str, options: List[Union[str, types.InputPollOption]], @@ -6074,7 +6113,15 @@ def send_poll( question_parse_mode: Optional[str] = None, question_entities: Optional[List[types.MessageEntity]] = None, message_effect_id: Optional[str]=None, - allow_paid_broadcast: Optional[bool]=None) -> types.Message: + allow_paid_broadcast: Optional[bool]=None, + allows_revoting: Optional[bool]=None, + shuffle_options: Optional[bool]=None, + allow_adding_options: Optional[bool]=None, + hide_results_until_closes: Optional[bool]=None, + correct_option_ids: Optional[List[int]]=None, + description: Optional[str]=None, + description_parse_mode: Optional[str]=None, + description_entities: Optional[List[types.MessageEntity]]=None) -> types.Message: """ Use this method to send a native poll. On success, the sent Message is returned. @@ -6096,10 +6143,10 @@ def send_poll( :param type: Poll type, “quiz” or “regular”, defaults to “regular” :type type: :obj:`str` - :param allows_multiple_answers: True, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to False + :param allows_multiple_answers: True, if the poll allows multiple answers, defaults to False :type allows_multiple_answers: :obj:`bool` - :param correct_option_id: 0-based identifier of the correct answer option. Available only for polls in quiz mode, defaults to None + :param correct_option_id: Deprecated, use correct_option_ids instead. :type correct_option_id: :obj:`int` :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 @@ -6108,10 +6155,11 @@ def send_poll( :param explanation_parse_mode: Mode for parsing entities in the explanation. See formatting options for more details. :type explanation_parse_mode: :obj:`str` - :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 open_period: Amount of time in seconds the poll will be active after creation, 5-2628000. Can't be used together with close_date. :type open_period: :obj:`int` :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. :type close_date: :obj:`int` | :obj:`datetime` :param is_closed: Pass True, if the poll needs to be immediately closed. This can be useful for poll preview. @@ -6160,6 +6208,30 @@ def send_poll( of 0.1 Telegram Stars per message. The relevant Stars will be withdrawn from the bot's balance :type allow_paid_broadcast: :obj:`bool` + :param allows_revoting: Pass True, if the poll allows to change chosen answer options, defaults to False for quizzes and to True for regular polls + :type allows_revoting: :obj:`bool` + + :param shuffle_options: Pass True, if the poll options must be shown in random order + :type shuffle_options: :obj:`bool` + + :param allow_adding_options: Pass True, if answer options can be added to the poll after creation; not supported for anonymous polls and quizzes + :type allow_adding_options: :obj:`bool` + + :param hide_results_until_closes: Pass True, if poll results must be shown only after the poll closes + :type hide_results_until_closes: :obj:`bool` + + :param correct_option_ids: A JSON-serialized list of monotonically increasing 0-based identifiers of the correct answer options, required for polls in quiz mode + :type correct_option_ids: :obj:`list` of :obj:`int` + + :param description: Description of the poll to be sent, 0-1024 characters after entities parsing + :type description: :obj:`str` + + :param description_parse_mode: Mode for parsing entities in the poll description. See formatting options for more details. + :type description_parse_mode: :obj:`str` + + :param description_entities: A JSON-serialized list of special entities that appear in the poll description, which can be specified instead of description_parse_mode + :type description_entities: :obj:`list` of :obj:`MessageEntity` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -6192,6 +6264,7 @@ def send_poll( explanation_parse_mode = self.parse_mode if (explanation_parse_mode is None) else explanation_parse_mode question_parse_mode = self.parse_mode if (question_parse_mode is None) else question_parse_mode + description_parse_mode = self.parse_mode if (description_parse_mode is None) else description_parse_mode if options and (not isinstance(options[0], types.InputPollOption)): # show a deprecation warning @@ -6203,19 +6276,33 @@ def send_poll( options = [types.InputPollOption(option.text, text_entities=option.text_entities) for option in options] else: raise RuntimeError("Type of 'options' items is unknown. Options should be List[types.InputPollOption], other types are deprecated.") + + # handle deprecated correct_option_id parameter + if correct_option_id is not None: + if correct_option_ids is not None: + # show a conflict warning + logger.warning("Both 'correct_option_id' and 'correct_option_ids' parameters are set: use 'correct_option_ids' instead.") + else: + # convert correct_option_id to correct_option_ids + correct_option_ids = [correct_option_id] + logger.warning("The parameter 'correct_option_id' is deprecated, use 'correct_option_ids' instead.") + return types.Message.de_json( apihelper.send_poll( self.token, chat_id, question, options, is_anonymous=is_anonymous, type=type, allows_multiple_answers=allows_multiple_answers, - correct_option_id=correct_option_id, explanation=explanation, + explanation=explanation, explanation_parse_mode=explanation_parse_mode, open_period=open_period, close_date=close_date, is_closed=is_closed, disable_notification=disable_notification, reply_markup=reply_markup, timeout=timeout, explanation_entities=explanation_entities, protect_content=protect_content, message_thread_id=message_thread_id, reply_parameters=reply_parameters, business_connection_id=business_connection_id, question_parse_mode=question_parse_mode, question_entities=question_entities, - message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast) + message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + 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, + description=description, description_parse_mode=description_parse_mode, description_entities=description_entities) ) @@ -6796,10 +6883,10 @@ def send_gift(self, user_id: Optional[Union[str, int]] = None, gift_id: str=None :param text: Text that will be shown along with the gift; 0-255 characters :type text: :obj:`str` - :param text_parse_mode: Mode for parsing entities in the text. See formatting options for more details. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, and “custom_emoji” are ignored. + :param text_parse_mode: Mode for parsing entities in the text. See formatting options for more details. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, “custom_emoji”, and “date_time” are ignored. :type text_parse_mode: :obj:`str` - :param text_entities: A JSON-serialized list of special entities that appear in the gift text. It can be specified instead of text_parse_mode. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, and “custom_emoji” are ignored. + :param text_entities: A JSON-serialized list of special entities that appear in the gift text. It can be specified instead of text_parse_mode. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, “custom_emoji”, and “date_time” are ignored. :type text_entities: :obj:`list` of :obj:`types.MessageEntity` :return: Returns True on success. @@ -7503,10 +7590,10 @@ def gift_premium_subscription( :param text: Text that will be shown along with the service message about the subscription; 0-128 characters :type text: :obj:`str` - :param text_parse_mode: Mode for parsing entities in the text. See formatting options for more details. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, and “custom_emoji” are ignored. + :param text_parse_mode: Mode for parsing entities in the text. See formatting options for more details. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, “custom_emoji”, and “date_time” are ignored. :type text_parse_mode: :obj:`str` - :param text_entities: A JSON-serialized list of special entities that appear in the gift text. It can be specified instead of text_parse_mode. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, and “custom_emoji” are ignored. + :param text_entities: A JSON-serialized list of special entities that appear in the gift text. It can be specified instead of text_parse_mode. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, “custom_emoji”, and “date_time” are ignored. :type text_entities: :obj:`list` of :class:`telebot.types.MessageEntity` :return: Returns True on success. @@ -8127,6 +8214,25 @@ def save_prepared_inline_message( self.token, user_id, result, allow_user_chats=allow_user_chats, allow_bot_chats=allow_bot_chats, allow_group_chats=allow_group_chats, allow_channel_chats=allow_channel_chats) ) + + def save_prepared_keyboard_button(self, user_id: int, button: types.KeyboardButton) -> types.PreparedKeyboardButton: + """ + Use this method to store a keyboard button that can be used by a user within a Mini App. Returns a PreparedKeyboardButton object. + + Telegram Documentation: https://core.telegram.org/bots/api#savepreparedkeyboardbutton + + :param user_id: Unique identifier of the target user that can use the button + :type user_id: :obj:`int` + + :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 + :type button: :class:`telebot.types.KeyboardButton` + + :return: On success, a PreparedKeyboardButton object is returned. + :rtype: :class:`telebot.types.PreparedKeyboardButton` + """ + return types.PreparedKeyboardButton.de_json( + apihelper.save_prepared_keyboard_button(self.token, user_id, button) + ) def register_for_reply(self, message: types.Message, callback: Callable, *args, **kwargs) -> None: """ @@ -10346,6 +10452,53 @@ def register_deleted_business_messages_handler(self, callback: Callable, func: O handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) self.add_deleted_business_messages_handler(handler_dict) + def managed_bot_handler(self, func=None, **kwargs): + """ + Handles new incoming updates about managed bot. As a parameter to the decorator function, it passes :class:`telebot.types.ManagedBotUpdated` object. + + :param func: Function executed as a filter + :type func: :obj:`function` + + :param kwargs: Optional keyword arguments(custom filters) + :return: None + """ + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_managed_bot_handler(handler_dict) + return handler + + return decorator + + def add_managed_bot_handler(self, handler_dict): + """ + Adds a managed_bot handler. + Note that you should use register_managed_bot_handler to add managed_bot_handler to the bot. + + :meta private: + :param handler_dict: + :return: + """ + self.managed_bot_handlers.append(handler_dict) + + def register_managed_bot_handler(self, callback: Callable, func: Optional[Callable]=None, pass_bot: Optional[bool]=False, **kwargs): + """ + Registers managed bot handler. + + :param callback: function to be called + :type callback: :obj:`function` + + :param func: Function executed as a filter + :type func: :obj:`function` + + :param pass_bot: True if you need to pass TeleBot instance to handler(useful for separating handlers into different files) + :type pass_bot: :obj:`bool` + + :param kwargs: Optional keyword arguments(custom filters) + :return: None + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_managed_bot_handler(handler_dict) + def add_custom_filter(self, custom_filter: Union[SimpleCustomFilter, AdvancedCustomFilter]): """ diff --git a/telebot/apihelper.py b/telebot/apihelper.py index daee29314..fa04fc9b7 100644 --- a/telebot/apihelper.py +++ b/telebot/apihelper.py @@ -1606,6 +1606,16 @@ def get_business_connection(token, business_connection_id): payload = {'business_connection_id': business_connection_id} return _make_request(token, method_url, params=payload , method='post') +def get_managed_bot_token(token, user_id): + method_url = 'getManagedBotToken' + payload = {'user_id': user_id} + return _make_request(token, method_url, params=payload , method='post') + +def replace_managed_bot_token(token, user_id): + method_url = 'replaceManagedBotToken' + payload = {'user_id': user_id} + return _make_request(token, method_url, params=payload , method='post') + def delete_my_commands(token, scope=None, language_code=None): method_url = r'deleteMyCommands' payload = {} @@ -2486,6 +2496,12 @@ def save_prepared_inline_message(token, user_id, result: types.InlineQueryResult return _make_request(token, method_url, params=payload, method='post') +def save_prepared_keyboard_button(token, user_id, button): + method_url = 'savePreparedKeyboardButton' + payload = {'user_id': user_id, 'button': button.to_json()} + return _make_request(token, method_url, params=payload, method='post') + + def create_invoice_link(token, title, description, payload, provider_token, currency, prices, max_tip_amount=None, suggested_tip_amounts=None, provider_data=None, photo_url=None, photo_size=None, photo_width=None, photo_height=None, need_name=None, need_phone_number=None, @@ -2534,11 +2550,12 @@ def create_invoice_link(token, title, description, payload, provider_token, # noinspection PyShadowingBuiltins def send_poll( token, chat_id, question, options, - is_anonymous = None, type = None, allows_multiple_answers = None, correct_option_id = None, explanation = None, + is_anonymous = None, type = None, allows_multiple_answers = None, explanation = None, explanation_parse_mode=None, open_period = None, close_date = None, is_closed = None, disable_notification=False, reply_markup=None, timeout=None, explanation_entities=None, protect_content=None, message_thread_id=None, reply_parameters=None, business_connection_id=None, question_parse_mode=None, question_entities=None, message_effect_id=None, - allow_paid_broadcast=None): + allow_paid_broadcast=None, allows_revoting=None, shuffle_options=None, allow_adding_options=None, hide_results_until_closes=None, + correct_option_ids=None, description=None, description_parse_mode=None, description_entities=None): method_url = r'sendPoll' payload = { 'chat_id': str(chat_id), @@ -2552,8 +2569,6 @@ def send_poll( payload['type'] = type if allows_multiple_answers is not None: payload['allows_multiple_answers'] = allows_multiple_answers - if correct_option_id is not None: - payload['correct_option_id'] = correct_option_id if explanation: payload['explanation'] = explanation if explanation_parse_mode: @@ -2591,6 +2606,22 @@ def send_poll( payload['message_effect_id'] = message_effect_id if allow_paid_broadcast is not None: payload['allow_paid_broadcast'] = allow_paid_broadcast + if allows_revoting is not None: + payload['allows_revoting'] = allows_revoting + if shuffle_options is not None: + payload['shuffle_options'] = shuffle_options + if allow_adding_options is not None: + payload['allow_adding_options'] = allow_adding_options + if hide_results_until_closes is not None: + payload['hide_results_until_closes'] = hide_results_until_closes + if correct_option_ids is not None: + payload['correct_option_ids'] = json.dumps(correct_option_ids) + if description is not None: + payload['description'] = description + if description_parse_mode is not None: + payload['description_parse_mode'] = description_parse_mode + if description_entities is not None: + payload['description_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(description_entities)) return _make_request(token, method_url, params=payload) def create_forum_topic(token, chat_id, name, icon_color=None, icon_custom_emoji_id=None): diff --git a/telebot/async_telebot.py b/telebot/async_telebot.py index 981f5f4fc..54957cf6b 100644 --- a/telebot/async_telebot.py +++ b/telebot/async_telebot.py @@ -185,6 +185,7 @@ def __init__(self, token: str, parse_mode: Optional[str]=None, offset: Optional[ self.edited_business_message_handlers = [] self.deleted_business_messages_handlers = [] self.purchased_paid_media_handlers = [] + self.managed_bot_handlers = [] self.custom_filters = {} self.state_handlers = [] @@ -639,6 +640,7 @@ async def process_new_updates(self, updates: List[types.Update]): new_edited_business_messages = None new_deleted_business_messages = None new_purchased_paid_media = None + new_managed_bots = None for update in updates: @@ -712,6 +714,9 @@ async def process_new_updates(self, updates: List[types.Update]): if update.purchased_paid_media: if new_purchased_paid_media is None: new_purchased_paid_media = [] new_purchased_paid_media.append(update.purchased_paid_media) + if update.managed_bot: + if new_managed_bots is None: new_managed_bots = [] + new_managed_bots.append(update.managed_bot) if new_messages: @@ -758,6 +763,8 @@ async def process_new_updates(self, updates: List[types.Update]): await self.process_new_deleted_business_messages(new_deleted_business_messages) if new_purchased_paid_media: await self.process_new_purchased_paid_media(new_purchased_paid_media) + if new_managed_bots: + await self.process_new_managed_bots(new_managed_bots) async def process_new_messages(self, new_messages): """ @@ -898,6 +905,12 @@ async def process_new_purchased_paid_media(self, new_purchased_paid_media): """ await self._process_updates(self.purchased_paid_media_handlers, new_purchased_paid_media, 'purchased_paid_media') + async def process_new_managed_bots(self, new_managed_bots): + """ + :meta private: + """ + await self._process_updates(self.managed_bot_handlers, new_managed_bots, 'managed_bot') + async def _get_middlewares(self, update_type): """ :meta private: @@ -2644,6 +2657,54 @@ def register_deleted_business_messages_handler(self, callback: Callable, func: O handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) self.add_deleted_business_messages_handler(handler_dict) + def managed_bot_handler(self, func=None, **kwargs): + """ + Handles new incoming managed bot state. + + :param func: Function executed as a filter + :type func: :obj:`function` + + :param kwargs: Optional keyword arguments(custom filters) + :return: None + """ + def decorator(handler): + handler_dict = self._build_handler_dict(handler, func=func, **kwargs) + self.add_managed_bot_handler(handler_dict) + return handler + + return decorator + + def add_managed_bot_handler(self, handler_dict): + """ + Adds a managed_bot handler. + Note that you should use register_managed_bot_handler to add managed_bot_handler to the bot. + + :meta private: + """ + self.managed_bot_handlers.append(handler_dict) + + def register_managed_bot_handler(self, callback: Callable, func: Optional[Callable]=None, pass_bot: Optional[bool]=False, **kwargs): + """ + Registers managed bot handler. + + :param callback: function to be called + :type callback: :obj:`function` + + :param func: Function executed as a filter + :type func: :obj:`function` + + :param pass_bot: True if you need to pass TeleBot instance to handler(useful for separating handlers into different files) + :type pass_bot: :obj:`bool` + + :param kwargs: Optional keyword arguments(custom filters) + :type kwargs: :obj:`dict` + + :return: None + """ + handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) + self.add_managed_bot_handler(handler_dict) + + @staticmethod def _build_handler_dict(handler, pass_bot=False, **filters): """ @@ -3199,6 +3260,23 @@ async def save_prepared_inline_message(self, user_id: int, result: types.InlineQ result = await asyncio_helper.save_prepared_inline_message(self.token, user_id, result, allow_user_chats, allow_bot_chats, allow_group_chats, allow_channel_chats) return types.PreparedInlineMessage.de_json(result) + async def save_prepared_keyboard_button(self, user_id: int, button: types.KeyboardButton) -> types.PreparedKeyboardButton: + """ + Stores a keyboard button that can be used by a user within a Mini App. Returns a PreparedKeyboardButton object. + + Telegram Documentation: https://core.telegram.org/bots/api#savepreparedkeyboardbutton + + :param user_id: Unique identifier of the target user that can use the button + :type user_id: :obj:`int` + + :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 + :type button: :class:`telebot.types.KeyboardButton` + + :return: :class:`telebot.types.PreparedKeyboardButton` + """ + result = await asyncio_helper.save_prepared_keyboard_button(self.token, user_id, button) + return types.PreparedKeyboardButton.de_json(result) + async def get_chat_member(self, chat_id: Union[int, str], user_id: int) -> types.ChatMember: """ Use this method to get information about a member of a chat. Returns a ChatMember object on success. @@ -6708,6 +6786,34 @@ async def get_business_connection(self, business_connection_id: str) -> types.Bu result ) + async def get_managed_bot_token(self, user_id: int) -> str: + """ + Use this method to get the token of a managed bot. Returns the token as String on success. + + Telegram documentation: https://core.telegram.org/bots/api#getmanagedbottoken + + :param user_id: User identifier of the managed bot whose token will be returned + :type user_id: :obj:`int` + + :return: Returns the token as String on success. + :rtype: :obj:`str` + """ + return await asyncio_helper.get_managed_bot_token(self.token, user_id) + + async def replace_managed_bot_token(self, user_id: int) -> 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. + + Telegram documentation: https://core.telegram.org/bots/api#replacemanagedbottoken + + :param user_id: User identifier of the managed bot whose token will be replaced + :type user_id: :obj:`int` + + :return: Returns the new token as String on success. + :rtype: :obj:`str` + """ + return await asyncio_helper.replace_managed_bot_token(self.token, user_id) async def set_my_commands(self, commands: List[types.BotCommand], scope: Optional[types.BotCommandScope]=None, @@ -7500,6 +7606,7 @@ async def create_invoice_link(self, send_email_to_provider, is_flexible, subscription_period=subscription_period, business_connection_id=business_connection_id) return result + # noinspection PyShadowingBuiltins async def send_poll( self, chat_id: Union[int, str], question: str, options: List[Union[str, types.InputPollOption]], @@ -7524,7 +7631,15 @@ async def send_poll( question_parse_mode: Optional[str] = None, question_entities: Optional[List[types.MessageEntity]] = None, message_effect_id: Optional[str]=None, - allow_paid_broadcast: Optional[bool]=None) -> types.Message: + allow_paid_broadcast: Optional[bool]=None, + allows_revoting: Optional[bool]=None, + shuffle_options: Optional[bool]=None, + allow_adding_options: Optional[bool]=None, + hide_results_until_closes: Optional[bool]=None, + correct_option_ids: Optional[List[int]]=None, + description: Optional[str]=None, + description_parse_mode: Optional[str]=None, + description_entities: Optional[List[types.MessageEntity]]=None) -> types.Message: """ Use this method to send a native poll. On success, the sent Message is returned. @@ -7546,10 +7661,10 @@ async def send_poll( :param type: Poll type, “quiz” or “regular”, defaults to “regular” :type type: :obj:`str` - :param allows_multiple_answers: True, if the poll allows multiple answers, ignored for polls in quiz mode, defaults to False + :param allows_multiple_answers: True, if the poll allows multiple answers, defaults to False :type allows_multiple_answers: :obj:`bool` - :param correct_option_id: 0-based identifier of the correct answer option. Available only for polls in quiz mode, + :param correct_option_id: Deprecated, use correct_option_ids instead. defaults to None :type correct_option_id: :obj:`int` @@ -7560,10 +7675,11 @@ async def send_poll( :param explanation_parse_mode: Mode for parsing entities in the explanation. See formatting options for more details. :type explanation_parse_mode: :obj:`str` - :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 open_period: Amount of time in seconds the poll will be active after creation, 5-2628000. Can't be used together with close_date. :type open_period: :obj:`int` :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. :type close_date: :obj:`int` | :obj:`datetime` :param is_closed: Pass True, if the poll needs to be immediately closed. This can be useful for poll preview. @@ -7614,6 +7730,30 @@ async def send_poll( of 0.1 Telegram Stars per message. The relevant Stars will be withdrawn from the bot's balance :type allow_paid_broadcast: :obj:`bool` + :param allows_revoting: Pass True, if the poll allows to change chosen answer options, defaults to False for quizzes and to True for regular polls + :type allows_revoting: :obj:`bool` + + :param shuffle_options: Pass True, if the poll options must be shown in random order + :type shuffle_options: :obj:`bool` + + :param allow_adding_options: Pass True, if answer options can be added to the poll after creation; not supported for anonymous polls and quizzes + :type allow_adding_options: :obj:`bool` + + :param hide_results_until_closes: Pass True, if poll results must be shown only after the poll closes + :type hide_results_until_closes: :obj:`bool` + + :param correct_option_ids: A JSON-serialized list of monotonically increasing 0-based identifiers of the correct answer options, required for polls in quiz mode + :type correct_option_ids: :obj:`list` of :obj:`int` + + :param description: Description of the poll to be sent, 0-1024 characters after entities parsing + :type description: :obj:`str` + + :param description_parse_mode: Mode for parsing entities in the poll description. See formatting options for more details. + :type description_parse_mode: :obj:`str` + + :param description_entities: A JSON-serialized list of special entities that appear in the poll description, which can be specified instead of description_parse_mode + :type description_entities: :obj:`list` of :obj:`MessageEntity` + :return: On success, the sent Message is returned. :rtype: :obj:`types.Message` """ @@ -7622,6 +7762,7 @@ async def send_poll( explanation_parse_mode = self.parse_mode if (explanation_parse_mode is None) else explanation_parse_mode question_parse_mode = self.parse_mode if (question_parse_mode is None) else question_parse_mode + description_parse_mode = self.parse_mode if (description_parse_mode is None) else description_parse_mode if allow_sending_without_reply is not None: logger.warning("The parameter 'allow_sending_without_reply' is deprecated. Use 'reply_parameters' instead.") @@ -7656,17 +7797,32 @@ async def send_poll( options = [types.InputPollOption(option.text, text_entities=option.text_entities) for option in options] else: raise RuntimeError("Type of 'options' items is unknown. Options should be List[types.InputPollOption], other types are deprecated.") + + # handle deprecated correct_option_id parameter + if correct_option_id is not None: + if correct_option_ids is not None: + # show a conflict warning + logger.warning("Both 'correct_option_id' and 'correct_option_ids' parameters are set: use 'correct_option_ids' instead.") + else: + # convert correct_option_id to correct_option_ids + correct_option_ids = [correct_option_id] + logger.warning("The parameter 'correct_option_id' is deprecated, use 'correct_option_ids' instead.") return types.Message.de_json( await asyncio_helper.send_poll( self.token, chat_id, question, options, - is_anonymous, type, allows_multiple_answers, correct_option_id, + is_anonymous, type, allows_multiple_answers, explanation, explanation_parse_mode, open_period, close_date, is_closed, disable_notification, reply_markup, timeout, explanation_entities, protect_content, message_thread_id, reply_parameters, business_connection_id, question_parse_mode=question_parse_mode, question_entities=question_entities, - message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast)) + message_effect_id=message_effect_id, allow_paid_broadcast=allow_paid_broadcast, + 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, description=description, + description_parse_mode=description_parse_mode, description_entities=description_entities + ) + ) async def stop_poll( self, chat_id: Union[int, str], message_id: int, @@ -8254,10 +8410,10 @@ async def send_gift(self, user_id: Optional[Union[str, int]] = None, gift_id: st :param text: Text that will be shown along with the gift; 0-255 characters :type text: :obj:`str` - :param text_parse_mode: Mode for parsing entities in the text. See formatting options for more details. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, and “custom_emoji” are ignored. + :param text_parse_mode: Mode for parsing entities in the text. See formatting options for more details. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, “custom_emoji”, and “date_time” are ignored. :type text_parse_mode: :obj:`str` - :param text_entities: A JSON-serialized list of special entities that appear in the gift text. It can be specified instead of text_parse_mode. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, and “custom_emoji” are ignored. + :param text_entities: A JSON-serialized list of special entities that appear in the gift text. It can be specified instead of text_parse_mode. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, “custom_emoji”, and “date_time” are ignored. :type text_entities: :obj:`list` of :obj:`types.MessageEntity` :return: Returns True on success. @@ -9003,10 +9159,10 @@ async def gift_premium_subscription( :param text: Text that will be shown along with the service message about the subscription; 0-128 characters :type text: :obj:`str` - :param text_parse_mode: Mode for parsing entities in the text. See formatting options for more details. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, and “custom_emoji” are ignored. + :param text_parse_mode: Mode for parsing entities in the text. See formatting options for more details. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, “custom_emoji”, and “date_time” are ignored. :type text_parse_mode: :obj:`str` - :param text_entities: A JSON-serialized list of special entities that appear in the gift text. It can be specified instead of text_parse_mode. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, and “custom_emoji” are ignored. + :param text_entities: A JSON-serialized list of special entities that appear in the gift text. It can be specified instead of text_parse_mode. Entities other than “bold”, “italic”, “underline”, “strikethrough”, “spoiler”, “custom_emoji”, and “date_time” are ignored. :type text_entities: :obj:`list` of :class:`telebot.types.MessageEntity` :return: Returns True on success. diff --git a/telebot/asyncio_helper.py b/telebot/asyncio_helper.py index fd6d2bd3b..0ba07277c 100644 --- a/telebot/asyncio_helper.py +++ b/telebot/asyncio_helper.py @@ -439,7 +439,13 @@ async def save_prepared_inline_message(token, user_id, result: types.InlineQuery payload['allow_group_chats'] = allow_group_chats if allow_channel_chats is not None: payload['allow_channel_chats'] = allow_channel_chats - return await _process_request(token, method_url, params=payload) + return await _process_request(token, method_url, params=payload, method='post') + + +async def save_prepared_keyboard_button(token, user_id, button: types.KeyboardButton): + method_url = r'savePreparedKeyboardButton' + payload = {'user_id': user_id, 'button': button.to_json()} + return await _process_request(token, method_url, params=payload, method='post') async def get_chat_member(token, chat_id, user_id): @@ -529,7 +535,7 @@ async def send_checklist( if reply_parameters: payload['reply_parameters'] = reply_parameters.to_json() if reply_markup: - payload['reply_markup'] = _convert_markup(reply_markup) + payload['reply_markup'] = await _convert_markup(reply_markup) return await _process_request(token, method_url, params=payload) @@ -655,7 +661,7 @@ async def send_paid_media( if reply_parameters is not None: _payload['reply_parameters'] = reply_parameters.to_json() if reply_markup: - _payload['reply_markup'] = _convert_markup(reply_markup) + _payload['reply_markup'] = await _convert_markup(reply_markup) if business_connection_id: _payload['business_connection_id'] = business_connection_id if payload: @@ -1609,6 +1615,16 @@ async def get_business_connection(token, business_connection_id): payload = {'business_connection_id': business_connection_id} return await _process_request(token, method_url, params=payload , method='post') +async def get_managed_bot_token(token, user_id): + method_url = 'getManagedBotToken' + payload = {'user_id': user_id} + return await _process_request(token, method_url, params=payload , method='post') + +async def replace_managed_bot_token(token, user_id): + method_url = 'replaceManagedBotToken' + payload = {'user_id': user_id} + return await _process_request(token, method_url, params=payload , method='post') + async def delete_my_commands(token, scope=None, language_code=None): method_url = r'deleteMyCommands' payload = {} @@ -2507,16 +2523,16 @@ async def create_invoice_link(token, title, description, payload, provider_token return await _process_request(token, method_url, params=payload, method='post') - # noinspection PyShadowingBuiltins async def send_poll( token, chat_id, question, options, - is_anonymous = None, type = None, allows_multiple_answers = None, correct_option_id = None, + is_anonymous = None, type = None, allows_multiple_answers = None, explanation = None, explanation_parse_mode=None, open_period = None, close_date = None, is_closed = None, disable_notification=False, reply_markup=None, timeout=None, explanation_entities=None, protect_content=None, message_thread_id=None, reply_parameters=None,business_connection_id=None, question_parse_mode=None, question_entities=None, message_effect_id=None, - allow_paid_broadcast=None): + allow_paid_broadcast=None, allows_revoting=None, shuffle_options=None, allow_adding_options=None, hide_results_until_closes=None, + correct_option_ids=None, description=None, description_parse_mode=None, description_entities=None): method_url = r'sendPoll' payload = { 'chat_id': str(chat_id), @@ -2530,8 +2546,6 @@ async def send_poll( payload['type'] = type if allows_multiple_answers is not None: payload['allows_multiple_answers'] = allows_multiple_answers - if correct_option_id is not None: - payload['correct_option_id'] = correct_option_id if explanation is not None: payload['explanation'] = explanation if explanation_parse_mode is not None: @@ -2570,6 +2584,22 @@ async def send_poll( payload['message_effect_id'] = message_effect_id if allow_paid_broadcast is not None: payload['allow_paid_broadcast'] = allow_paid_broadcast + if allows_revoting is not None: + payload['allows_revoting'] = allows_revoting + if shuffle_options is not None: + payload['shuffle_options'] = shuffle_options + if allow_adding_options is not None: + payload['allow_adding_options'] = allow_adding_options + if hide_results_until_closes is not None: + payload['hide_results_until_closes'] = hide_results_until_closes + if correct_option_ids is not None: + payload['correct_option_ids'] = json.dumps(correct_option_ids) + if description is not None: + payload['description'] = description + if description_parse_mode is not None: + payload['description_parse_mode'] = description_parse_mode + if description_entities is not None: + payload['description_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(description_entities)) return await _process_request(token, method_url, params=payload) diff --git a/telebot/types.py b/telebot/types.py index c0fe4ca21..9a2ca255f 100644 --- a/telebot/types.py +++ b/telebot/types.py @@ -206,6 +206,9 @@ class Update(JsonDeserializable): :param deleted_business_messages: Optional. Service message: the chat connected to the business account was deleted :type deleted_business_messages: :class:`telebot.types.BusinessMessagesDeleted` + :param managed_bot: Optional. A new bot was created to be managed by the bot or token of a bot was changed + :type managed_bot: :class:`telebot.types.ManagedBotUpdated` + :return: Instance of the class :rtype: :class:`telebot.types.Update` @@ -238,18 +241,19 @@ def de_json(cls, json_string): edited_business_message = Message.de_json(obj.get('edited_business_message')) deleted_business_messages = BusinessMessagesDeleted.de_json(obj.get('deleted_business_messages')) purchased_paid_media = PaidMediaPurchased.de_json(obj.get('purchased_paid_media')) + managed_bot = ManagedBotUpdated.de_json(obj.get('managed_bot')) return cls(update_id, message, edited_message, channel_post, edited_channel_post, inline_query, chosen_inline_result, callback_query, shipping_query, pre_checkout_query, poll, poll_answer, my_chat_member, chat_member, chat_join_request, message_reaction, message_reaction_count, removed_chat_boost, chat_boost, business_connection, business_message, edited_business_message, - deleted_business_messages, purchased_paid_media) + deleted_business_messages, purchased_paid_media, managed_bot) def __init__(self, update_id, message, edited_message, channel_post, edited_channel_post, inline_query, chosen_inline_result, callback_query, shipping_query, pre_checkout_query, poll, poll_answer, my_chat_member, chat_member, chat_join_request, message_reaction, message_reaction_count, removed_chat_boost, chat_boost, business_connection, business_message, edited_business_message, - deleted_business_messages, purchased_paid_media): + deleted_business_messages, purchased_paid_media, managed_bot): self.update_id: int = update_id self.message: Optional[Message] = message self.edited_message: Optional[Message] = edited_message @@ -274,7 +278,7 @@ def __init__(self, update_id, message, edited_message, channel_post, edited_chan self.edited_business_message: Optional[Message] = edited_business_message self.deleted_business_messages: Optional[BusinessMessagesDeleted] = deleted_business_messages self.purchased_paid_media: Optional[PaidMediaPurchased] = purchased_paid_media - + self.managed_bot: Optional[ManagedBotUpdated] = managed_bot class ChatMemberUpdated(JsonDeserializable): """ @@ -518,6 +522,9 @@ class User(JsonDeserializable, Dictionaryable, JsonSerializable): :param allows_users_to_create_topics: Optional. True, if the bot allows users to create and delete topics in private chats. Returned only in getMe. :type allows_users_to_create_topics: :obj:`bool` + :param can_manage_bots: Optional. True, if other bots can be created to be controlled by the bot. Returned only in getMe. + :type can_manage_bots: :obj:`bool` + :return: Instance of the class :rtype: :class:`telebot.types.User` """ @@ -531,7 +538,7 @@ def de_json(cls, json_string): def __init__(self, id, is_bot, first_name, last_name=None, username=None, language_code=None, can_join_groups=None, can_read_all_group_messages=None, supports_inline_queries=None, is_premium=None, added_to_attachment_menu=None, can_connect_to_business=None, - has_main_web_app=None, has_topics_enabled=None, allows_users_to_create_topics=None, **kwargs): + has_main_web_app=None, has_topics_enabled=None, allows_users_to_create_topics=None, can_manage_bots=None, **kwargs): self.id: int = id self.is_bot: bool = is_bot self.first_name: str = first_name @@ -547,7 +554,7 @@ def __init__(self, id, is_bot, first_name, last_name=None, username=None, langua self.has_main_web_app: Optional[bool] = has_main_web_app self.has_topics_enabled: Optional[bool] = has_topics_enabled self.allows_users_to_create_topics: Optional[bool] = allows_users_to_create_topics - + self.can_manage_bots: Optional[bool] = can_manage_bots @property def full_name(self) -> str: """ @@ -576,7 +583,8 @@ def to_dict(self): 'can_connect_to_business': self.can_connect_to_business, 'has_main_web_app': self.has_main_web_app, 'has_topics_enabled': self.has_topics_enabled, - 'allows_users_to_create_topics': self.allows_users_to_create_topics + 'allows_users_to_create_topics': self.allows_users_to_create_topics, + 'can_manage_bots': self.can_manage_bots } @@ -1023,6 +1031,9 @@ class Message(JsonDeserializable): :param reply_to_checklist_task_id: Optional. Identifier of the specific checklist task that is being replied to :type reply_to_checklist_task_id: :obj:`str` + :param reply_to_poll_option_id: Optional. Persistent identifier of the specific poll option that is being replied to + :type reply_to_poll_option_id: :obj:`str` + :param via_bot: Optional. Bot through which the message was sent :type via_bot: :class:`telebot.types.User` @@ -1275,9 +1286,18 @@ class Message(JsonDeserializable): :param giveaway_completed: Optional. Service message: giveaway completed, without public winners :type giveaway_completed: :class:`telebot.types.GiveawayCompleted` + :param managed_bot_created: Optional. Service message: user created a bot that will be managed by the current bot + :type managed_bot_created: :class:`telebot.types.ManagedBotCreated` + :param paid_message_price_changed: Optional. Service message: the price for paid messages has changed in the chat :type paid_message_price_changed: :class:`telebot.types.PaidMessagePriceChanged` + :param poll_option_added: Optional. Service message: answer option was added to a poll + :type poll_option_added: :class:`telebot.types.PollOptionAdded` + + :param poll_option_deleted: Optional. Service message: answer option was deleted from a poll + :type poll_option_deleted: :class:`telebot.types.PollOptionDeleted` + :param suggested_post_approved: Optional. Service message: a suggested post was approved :type suggested_post_approved: :class:`telebot.types.SuggestedPostApproved @@ -1607,6 +1627,17 @@ def de_json(cls, json_string): if 'chat_owner_left' in obj: opts['chat_owner_left'] = ChatOwnerLeft.de_json(obj['chat_owner_left']) content_type = 'chat_owner_left' + if 'managed_bot_created' in obj: + opts['managed_bot_created'] = ManagedBotCreated.de_json(obj['managed_bot_created']) + content_type = 'managed_bot_created' + if 'poll_option_added' in obj: + opts['poll_option_added'] = PollOptionAdded.de_json(obj['poll_option_added']) + content_type = 'poll_option_added' + if 'poll_option_deleted' in obj: + opts['poll_option_deleted'] = PollOptionDeleted.de_json(obj['poll_option_deleted']) + content_type = 'poll_option_deleted' + if 'reply_to_poll_option_id' in obj: + opts['reply_to_poll_option_id'] = obj['reply_to_poll_option_id'] return cls(message_id, from_user, date, chat, content_type, opts, json_string) @@ -1746,6 +1777,10 @@ def __init__(self, message_id, from_user, date, chat, content_type, options, jso self.suggested_post_refunded: Optional[SuggestedPostRefunded] = None self.chat_owner_left: Optional[ChatOwnerLeft] = None self.chat_owner_changed: Optional[ChatOwnerChanged] = None + self.managed_bot_created: Optional[ManagedBotCreated] = None + self.poll_option_added: Optional[PollOptionAdded] = None + self.poll_option_deleted: Optional[PollOptionDeleted] = None + self.reply_to_poll_option_id: Optional[str] = None for key in options: setattr(self, key, options[key]) @@ -2987,6 +3022,11 @@ class KeyboardButton(Dictionaryable, JsonSerializable): send its identifier to the bot in a “chat_shared” service message. Available in private chats only. :type request_chat: :class:`telebot.types.KeyboardButtonRequestChat` + :param request_managed_bot: Optional. If specified, pressing the button will ask the user to create and share a bot + that will be managed by the current bot. Available for bots that enabled management of other bots in the @BotFather + Mini App. Available in private chats only. + :type request_managed_bot: :class:`telebot.types.KeyboardButtonRequestManagedBot` + :return: Instance of the class :rtype: :class:`telebot.types.KeyboardButton` """ @@ -2994,7 +3034,7 @@ def __init__(self, text: str, request_contact: Optional[bool]=None, request_location: Optional[bool]=None, request_poll: Optional[KeyboardButtonPollType]=None, web_app: Optional[WebAppInfo]=None, request_user: Optional[KeyboardButtonRequestUser]=None, request_chat: Optional[KeyboardButtonRequestChat]=None, request_users: Optional[KeyboardButtonRequestUsers]=None, - icon_custom_emoji_id: Optional[str]=None, style: Optional[str]=None, **kwargs): + icon_custom_emoji_id: Optional[str]=None, style: Optional[str]=None, request_managed_bot: Optional[KeyboardButtonRequestManagedBot]=None): self.text: str = text self.request_contact: Optional[bool] = request_contact self.request_location: Optional[bool] = request_location @@ -3004,6 +3044,7 @@ def __init__(self, text: str, request_contact: Optional[bool]=None, self.request_users: Optional[KeyboardButtonRequestUsers] = request_users self.icon_custom_emoji_id: Optional[str] = icon_custom_emoji_id self.style: Optional[str] = style + self.request_managed_bot: Optional[KeyboardButtonRequestManagedBot] = request_managed_bot if request_user is not None: log_deprecation_warning('The parameter "request_user" is deprecated, use "request_users" instead') if self.request_users is None: @@ -3032,6 +3073,8 @@ def to_dict(self): json_dict['icon_custom_emoji_id'] = self.icon_custom_emoji_id if self.style: json_dict['style'] = self.style + if self.request_managed_bot is not None: + json_dict['request_managed_bot'] = self.request_managed_bot.to_dict() return json_dict @@ -7402,14 +7445,26 @@ class PollOption(JsonDeserializable): Telegram Documentation: https://core.telegram.org/bots/api#polloption + :param persistent_id: Unique identifier of the option, persistent on option addition and deletion + :type persistent_id: :obj:`str` + :param text: Option text, 1-100 characters :type text: :obj:`str` + :param text_entities: Optional. Special entities that appear in the option text. Currently, only custom emoji entities are allowed in poll option texts + :type text_entities: :obj:`list` of :class:`telebot.types.MessageEntity` + :param voter_count: Number of users that voted for this option :type voter_count: :obj:`int` - :param text_entities: Optional. Special entities that appear in the option text. Currently, only custom emoji entities are allowed in poll option texts - :type text_entities: :obj:`list` of :class:`telebot.types.MessageEntity` + :param added_by_user: Optional. User who added the option; omitted if the option wasn't added by a user after poll creation + :type added_by_user: :class:`telebot.types.User` + + :param added_by_chat: Optional. Chat that added the option; omitted if the option wasn't added by a chat after poll creation + :type added_by_chat: :class:`telebot.types.Chat` + + :param addition_date: Optional. Point in time (Unix timestamp) when the option was added; omitted if the option existed in the original poll + :type addition_date: :obj:`int` :return: Instance of the class :rtype: :class:`telebot.types.PollOption` @@ -7420,12 +7475,20 @@ def de_json(cls, json_string): obj = cls.check_json(json_string, dict_copy=False) if 'text_entities' in obj: obj['text_entities'] = Message.parse_entities(obj['text_entities']) + if 'added_by_user' in obj: + obj['added_by_user'] = User.de_json(obj['added_by_user']) + if 'added_by_chat' in obj: + obj['added_by_chat'] = Chat.de_json(obj['added_by_chat']) return cls(**obj) - def __init__(self, text, voter_count = 0, text_entities=None, **kwargs): + def __init__(self, text, persistent_id, voter_count = 0, text_entities=None, added_by_user=None, added_by_chat=None, addition_date=None, **kwargs): self.text: str = text + self.persistent_id: str = persistent_id self.voter_count: int = voter_count self.text_entities: Optional[List[MessageEntity]] = text_entities + self.added_by_user: Optional[User] = added_by_user + self.added_by_chat: Optional[Chat] = added_by_chat + self.addition_date: Optional[int] = addition_date # Converted in _convert_poll_options # def to_json(self): # # send_poll Option is a simple string: https://core.telegram.org/bots/api#sendpoll @@ -7501,9 +7564,12 @@ class Poll(JsonDeserializable): :param allows_multiple_answers: True, if the poll allows multiple answers :type allows_multiple_answers: :obj:`bool` - :param correct_option_id: 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. + :param correct_option_id: Deprecated. Use correct_option_ids instead. :type correct_option_id: :obj:`int` + :param correct_option_ids: Optional. Array of 0-based identifiers of the correct answer options. Available only for polls in quiz mode which are closed or were sent (not forwarded) by the bot or to the private chat with the bot. + :type correct_option_ids: :obj:`list` of :obj:`int` + :param explanation: 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 :type explanation: :obj:`str` @@ -7519,6 +7585,15 @@ class Poll(JsonDeserializable): :param question_entities: Optional. Special entities that appear in the question. Currently, only custom emoji entities are allowed in poll questions :type question_entities: :obj:`list` of :class:`telebot.types.MessageEntity` + :param allows_revoting: True, if the poll allows to change the chosen answer options + :type allows_revoting: :obj:`bool` + + :param description: Optional. Description of the poll; for polls inside the Message object only + :type description: :obj:`str` + + :param description_entities: Optional. Special entities like usernames, URLs, bot commands, etc. that appear in the description + :type description_entities: :obj:`list` of :class:`telebot.types.MessageEntity` + :return: Instance of the class :rtype: :class:`telebot.types.Poll` """ @@ -7535,15 +7610,19 @@ def de_json(cls, json_string): obj['explanation_entities'] = Message.parse_entities(obj['explanation_entities']) if 'question_entities' in obj: obj['question_entities'] = Message.parse_entities(obj['question_entities']) + if 'description_entities' in obj: + obj['description_entities'] = Message.parse_entities(obj['description_entities']) return cls(**obj) def __init__( self, question: str, options: List[PollOption], poll_id: str = None, total_voter_count: int = None, is_closed: bool = None, is_anonymous: bool = None, - type: str = None, allows_multiple_answers: bool = None, correct_option_id: int = None, + type: str = None, allows_multiple_answers: bool = None, explanation: str = None, explanation_entities: List[MessageEntity] = None, open_period: int = None, close_date: int = None, poll_type: str = None, question_entities: List[MessageEntity] = None, + correct_option_ids: List[int] = None, allows_revoting: bool = None, + description: str = None, description_entities: List[MessageEntity] = None, **kwargs): self.id: str = poll_id self.question: str = question @@ -7557,12 +7636,22 @@ def __init__( if type is None: self.type: str = poll_type self.allows_multiple_answers: bool = allows_multiple_answers - self.correct_option_id: int = correct_option_id self.explanation: str = explanation self.explanation_entities: List[MessageEntity] = explanation_entities self.question_entities: List[MessageEntity] = question_entities self.open_period: int = open_period self.close_date: int = close_date + self.correct_option_ids: List[int] = correct_option_ids + self.allows_revoting: bool = allows_revoting + self.description: str = description + self.description_entities: List[MessageEntity] = description_entities + + @property + def correct_option_id(self) -> Optional[int]: + log_deprecation_warning("Poll: correct_option_id parameter is deprecated. Use correct_option_ids instead.") + if self.correct_option_ids and len(self.correct_option_ids) > 0: + return self.correct_option_ids[0] + return None def add(self, option): """ @@ -7595,6 +7684,9 @@ class PollAnswer(JsonSerializable, JsonDeserializable, Dictionaryable): :param option_ids: 0-based identifiers of answer options, chosen by the user. May be empty if the user retracted their vote. :type option_ids: :obj:`list` of :obj:`int` + :param option_persistent_ids: Persistent identifiers of the chosen answer options. May be empty if the vote was retracted. + :type option_persistent_ids: :obj:`list` of :obj:`str` + :return: Instance of the class :rtype: :class:`telebot.types.PollAnswer` """ @@ -7608,10 +7700,12 @@ def de_json(cls, json_string): obj['voter_chat'] = Chat.de_json(obj['voter_chat']) return cls(**obj) - def __init__(self, poll_id: str, option_ids: List[int], user: Optional[User] = None, voter_chat: Optional[Chat] = None, **kwargs): + def __init__(self, poll_id: str, option_ids: List[int], option_persistent_ids: List[str], + user: Optional[User] = None, voter_chat: Optional[Chat] = None, **kwargs): self.poll_id: str = poll_id self.user: Optional[User] = user self.option_ids: List[int] = option_ids + self.option_persistent_ids: List[str] = option_persistent_ids self.voter_chat: Optional[Chat] = voter_chat @@ -7620,14 +7714,11 @@ def to_json(self): def to_dict(self): # Left for backward compatibility, but with no support for voter_chat + logger.warning("PollAnswer.to_dict is deprecated and will be removed in future versions.") json_dict = { "poll_id": self.poll_id, - "option_ids": self.option_ids + "option_ids": self.option_ids, } - if self.user: - json_dict["user"] = self.user.to_dict() - if self.voter_chat: - json_dict["voter_chat"] = self.voter_chat return json_dict @@ -9555,7 +9646,7 @@ class TextQuote(JsonDeserializable): :param text: Text of the quoted part of a message that is replied to by the given message :type text: :obj:`str` - :param entities: Optional. Special entities that appear in the quote. Currently, only bold, italic, underline, strikethrough, spoiler, and custom_emoji entities are kept in quotes. + :param entities: Optional. Special entities that appear in the quote. Currently, only bold, italic, underline, strikethrough, spoiler, custom_emoji, and date_time entities are kept in quotes. :type entities: :obj:`list` of :class:`MessageEntity` :param position: Approximate quote position in the original message in UTF-16 code units as specified by the sender @@ -9603,13 +9694,18 @@ class ReplyParameters(JsonDeserializable, Dictionaryable, JsonSerializable): :param message_id: Identifier of the message that will be replied to in the current chat, or in the chat chat_id if it is specified :type message_id: :obj:`int` - :param chat_id: Optional. If the message to be replied to is from a different chat, unique identifier for the chat or username of the channel (in the format @channelusername) + :param chat_id: Optional. If the message to be replied to is from a different chat, unique identifier for the chat or username + of the channel (in the format @channelusername) :type chat_id: :obj:`int` or :obj:`str` - :param allow_sending_without_reply: Optional. Pass True if the message should be sent even if the specified message to be replied to is not found; can be used only for replies in the same chat and forum topic. + :param allow_sending_without_reply: Optional. Pass True if the message should be sent even if the specified message to be replied to is not found; + can be used only for replies in the same chat and forum topic. :type allow_sending_without_reply: :obj:`bool` - :param quote: Optional. Quoted part of the message to be replied to; 0-1024 characters after entities parsing. The quote must be an exact substring of the message to be replied to, including bold, italic, underline, strikethrough, spoiler, and custom_emoji entities. The message will fail to send if the quote isn't found in the original message. + :param quote: Optional. Quoted part of the message to be replied to; 0-1024 characters after entities parsing. + The quote must be an exact substring of the message to be replied to, including bold, italic, underline, + strikethrough, spoiler, custom_emoji, and date_time entities. The message will fail to send if the quote + isn't found in the original message. :type quote: :obj:`str` :param quote_parse_mode: Optional. Mode for parsing entities in the quote. See formatting options for more details. @@ -9624,6 +9720,9 @@ class ReplyParameters(JsonDeserializable, Dictionaryable, JsonSerializable): :param checklist_task_id: Optional. Optional. Identifier of the specific checklist task to be replied to :type checklist_task_id: :obj:`int` + :param poll_option_id: Optional. Persistent identifier of the specific poll option to be replied to + :type poll_option_id: :obj:`str` + :return: Instance of the class :rtype: :class:`ReplyParameters` """ @@ -9639,7 +9738,8 @@ def de_json(cls, json_string): def __init__(self, message_id: int, chat_id: Optional[Union[int, str]] = None, allow_sending_without_reply: Optional[bool] = None, quote: Optional[str] = None, quote_parse_mode: Optional[str] = None, quote_entities: Optional[List[MessageEntity]] = None, - quote_position: Optional[int] = None, checklist_task_id: Optional[int] = None, **kwargs) -> None: + quote_position: Optional[int] = None, checklist_task_id: Optional[int] = None, + poll_option_id: Optional[str] = None, **kwargs) -> None: self.message_id: int = message_id self.chat_id: Optional[Union[int, str]] = chat_id self.allow_sending_without_reply: Optional[bool] = allow_sending_without_reply @@ -9648,6 +9748,7 @@ def __init__(self, message_id: int, chat_id: Optional[Union[int, str]] = None, self.quote_entities: Optional[List[MessageEntity]] = quote_entities self.quote_position: Optional[int] = quote_position self.checklist_task_id: Optional[int] = checklist_task_id + self.poll_option_id: Optional[str] = poll_option_id def to_dict(self) -> dict: json_dict = { @@ -9667,6 +9768,8 @@ def to_dict(self) -> dict: json_dict['quote_position'] = self.quote_position if self.checklist_task_id is not None: json_dict['checklist_task_id'] = self.checklist_task_id + if self.poll_option_id is not None: + json_dict['poll_option_id'] = self.poll_option_id return json_dict def to_json(self) -> str: @@ -13031,7 +13134,8 @@ class InputChecklistTask(JsonSerializable): :param parse_mode: Optional. Mode for parsing entities in the text. See formatting options for more details. :type parse_mode: :obj:`str` - :param text_entities: Optional. List of special entities that appear in the text, which can be specified instead of parse_mode. Currently, only bold, italic, underline, strikethrough, spoiler, and custom_emoji entities are allowed. + :param text_entities: Optional. List of special entities that appear in the text, which can be specified instead of parse_mode. + Currently, only bold, italic, underline, strikethrough, spoiler, custom_emoji, and date_time entities are allowed. :type text_entities: :obj:`list` of :class:`MessageEntity` :return: Instance of the class @@ -13071,7 +13175,8 @@ class InputChecklist(JsonSerializable): :param parse_mode: Optional. Mode for parsing entities in the title. See formatting options for more details. :type parse_mode: :obj:`str` - :param title_entities: Optional. List of special entities that appear in the title, which can be specified instead of parse_mode. Currently, only bold, italic, underline, strikethrough, spoiler, and custom_emoji entities are allowed. + :param title_entities: Optional. List of special entities that appear in the title, which can be specified instead of parse_mode. + Currently, only bold, italic, underline, strikethrough, spoiler, custom_emoji, and date_time entities are allowed. :type title_entities: :obj:`list` of :class:`MessageEntity` :param tasks: List of 1-30 tasks in the checklist @@ -13709,3 +13814,207 @@ def de_json(cls, json_string): obj = cls.check_json(json_string) obj['audios'] = [Audio.de_json(audio) for audio in obj['audios']] return cls(**obj) + + +class KeyboardButtonRequestManagedBot(JsonSerializable): + """ + 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 Message with + the field managed_bot_created. + + Telegram documentation: https://core.telegram.org/bots/api#keyboardbuttonrequestmanagedbot + + :param request_id: Signed 32-bit identifier of the request. Must be unique within the message + :type request_id: :obj:`int` + + :param suggested_name: Optional. Suggested name for the bot + :type suggested_name: :obj:`str` + + :param suggested_username: Optional. Suggested username for the bot + :type suggested_username: :obj:`str` + + :return: Instance of the class + :rtype: :class:`KeyboardButtonRequestManagedBot` + """ + def __init__(self, request_id: int, suggested_name: Optional[str] = None, suggested_username: Optional[str] = None, **kwargs): + self.request_id: int = request_id + self.suggested_name: Optional[str] = suggested_name + self.suggested_username: Optional[str] = suggested_username + + def to_json(self): + return json.dumps(self.to_dict()) + + def to_dict(self): + data = { + 'request_id': self.request_id + } + if self.suggested_name: + data['suggested_name'] = self.suggested_name + if self.suggested_username: + data['suggested_username'] = self.suggested_username + return data + + +class ManagedBotCreated(JsonDeserializable): + """ + This object contains information about the bot that was created to be managed by the current bot. + + Telegram documentation: https://core.telegram.org/bots/api#managedbotcreated + + :param bot: Information about the bot. The bot's token can be fetched using the method getManagedBotToken. + :type bot: :class:`User` + + :return: Instance of the class + :rtype: :class:`ManagedBotCreated` + """ + def __init__(self, bot: User, **kwargs): + self.bot: User = bot + + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + obj['bot'] = User.de_json(obj['bot']) + return cls(**obj) + + +class ManagedBotUpdated(JsonDeserializable): + """ + This object contains information about the creation or token update of a bot that is managed by the current bot. + + Telegram documentation: https://core.telegram.org/bots/api#managedbotupdated + + :param user: User that created the bot + :type user: :class:`User` + + :param bot: Information about the bot. Token of the bot can be fetched using the method getManagedBotToken. + :type bot: :class:`User` + + :return: Instance of the class + :rtype: :class:`ManagedBotUpdated` + """ + def __init__(self, user: User, bot: User, **kwargs): + self.user: User = user + self.bot: User = bot + + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + obj['user'] = User.de_json(obj['user']) + obj['bot'] = User.de_json(obj['bot']) + + return cls(**obj) + + +class PreparedKeyboardButton(JsonDeserializable): + """ + Describes a keyboard button to be used by a user of a Mini App. + + Telegram documentation: https://core.telegram.org/bots/api#preparedkeyboardbutton + + :param id: Unique identifier of the keyboard button + :type id: :obj:`str` + + :return: Instance of the class + :rtype: :class:`PreparedKeyboardButton` + """ + def __init__(self, id: str, **kwargs): + self.id: str = id + + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + return cls(**obj) + + +class PollOptionAdded(JsonDeserializable): + """ + Describes a service message about an option added to a poll. + + Telegram documentation: https://core.telegram.org/bots/api#polloptionadded + + :param poll_message: Optional. Message containing the poll to which the option was added, if known. Note that the Message object in this field will not contain the reply_to_message field even if it itself is a reply. + :type poll_message: :class:`MaybeInaccessibleMessage` + + :param option_persistent_id: Unique identifier of the added option + :type option_persistent_id: :obj:`str` + + :param option_text: Option text + :type option_text: :obj:`str` + + :param option_text_entities: Optional. Special entities that appear in the option_text + :type option_text_entities: :obj:`list` of :class:`MessageEntity` + + :return: Instance of the class + :rtype: :class:`PollOptionAdded` + """ + def __init__(self, option_persistent_id: str, option_text: str, + poll_message: Optional[Union[InaccessibleMessage, Message]] = None, + option_text_entities: Optional[List[MessageEntity]] = None, **kwargs): + self.poll_message: Optional[Union[InaccessibleMessage, Message]] = poll_message + self.option_persistent_id: str = option_persistent_id + self.option_text: str = option_text + self.option_text_entities: Optional[List[MessageEntity]] = option_text_entities + + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + if 'poll_message' in obj: + # if date is 0, then the message is inaccessible, otherwise it is accessible + poll_message = obj['poll_message'] + if poll_message['date'] == 0: + obj['poll_message'] = InaccessibleMessage.de_json(poll_message) + else: + obj['poll_message'] = Message.de_json(poll_message) + if 'option_text_entities' in obj: + obj['option_text_entities'] = [MessageEntity.de_json(entity) for entity in obj['option_text_entities']] + return cls(**obj) + + +class PollOptionDeleted(JsonDeserializable): + """ + Describes a service message about an option deleted from a poll. + + Telegram documentation: https://core.telegram.org/bots/api#polloptiondeleted + + :param poll_message: Optional. Message containing the poll from which the option was deleted, if known. Note that the Message object in this field will not contain the reply_to_message field even if it itself is a reply. + :type poll_message: :class:`MaybeInaccessibleMessage` + + :param option_persistent_id: Unique identifier of the deleted option + :type option_persistent_id: :obj:`str` + + :param option_text: Option text + :type option_text: :obj:`str` + + :param option_text_entities: Optional. Special entities that appear in the option_text + :type option_text_entities: :obj:`list` of :class:`MessageEntity` + + :return: Instance of the class + :rtype: :class:`PollOptionDeleted` + """ + def __init__(self, option_persistent_id: str, option_text: str, + poll_message: Optional[Union[InaccessibleMessage, Message]] = None, + option_text_entities: Optional[List[MessageEntity]] = None, **kwargs): + self.poll_message: Optional[Union[InaccessibleMessage, Message]] = poll_message + self.option_persistent_id: str = option_persistent_id + self.option_text: str = option_text + self.option_text_entities: Optional[List[MessageEntity]] = option_text_entities + + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + if 'poll_message' in obj: + # if date is 0, then the message is inaccessible, otherwise it is accessible + poll_message = obj['poll_message'] + if poll_message['date'] == 0: + obj['poll_message'] = InaccessibleMessage.de_json(poll_message) + else: + obj['poll_message'] = Message.de_json(poll_message) + if 'option_text_entities' in obj: + obj['option_text_entities'] = [MessageEntity.de_json(entity) for entity in obj['option_text_entities']] + return cls(**obj) + diff --git a/telebot/util.py b/telebot/util.py index 71117c679..c0c4d2d3a 100644 --- a/telebot/util.py +++ b/telebot/util.py @@ -43,7 +43,8 @@ 'giveaway_created', 'giveaway_winners', 'giveaway_completed', 'boost_added', 'paid_message_price_changed', 'checklist_tasks_done', 'checklist_tasks_added', 'direct_message_price_changed', 'suggested_post_refunded', 'suggested_post_info', 'suggested_post_approved', 'suggested_post_approval_failed', 'suggested_post_declined', - 'suggested_post_paid', 'gift_upgrade_sent', 'chat_owner_left', 'chat_owner_changed' + 'suggested_post_paid', 'gift_upgrade_sent', 'chat_owner_left', 'chat_owner_changed', 'managed_bot_created', + 'poll_option_added', 'poll_option_deleted' ] #: All update types, should be used for allowed_updates parameter in polling. @@ -52,7 +53,7 @@ "callback_query", "shipping_query", "pre_checkout_query", "poll", "poll_answer", "my_chat_member", "chat_member", "chat_join_request", "message_reaction", "message_reaction_count", "removed_chat_boost", "chat_boost", "business_connection", "business_message", "edited_business_message", "deleted_business_messages", - "purchased_paid_media", + "purchased_paid_media", "managed_bot", ] diff --git a/tests/test_handler_backends.py b/tests/test_handler_backends.py index 4df760ebc..3ef28c15a 100644 --- a/tests/test_handler_backends.py +++ b/tests/test_handler_backends.py @@ -71,7 +71,7 @@ def update_type(message): chat_boost_removed = None return types.Update(1001234038283, message, edited_message, channel_post, edited_channel_post, inline_query, chosen_inline_result, callback_query, shipping_query, pre_checkout_query, poll, poll_answer, - my_chat_member, chat_member, chat_join_request, message_reaction, message_reaction_count, chat_boost, chat_boost_removed, None, None, None, None, None) + my_chat_member, chat_member, chat_join_request, message_reaction, message_reaction_count, chat_boost, chat_boost_removed, None, None, None, None, None, None) @pytest.fixture() @@ -95,7 +95,7 @@ def reply_to_message_update_type(reply_to_message): chat_boost_removed = None return types.Update(1001234038284, reply_to_message, edited_message, channel_post, edited_channel_post, inline_query, chosen_inline_result, callback_query, shipping_query, pre_checkout_query, - poll, poll_answer, my_chat_member, chat_member, chat_join_request, message_reaction, message_reaction_count, chat_boost, chat_boost_removed, None, None, None, None, None) + poll, poll_answer, my_chat_member, chat_member, chat_join_request, message_reaction, message_reaction_count, chat_boost, chat_boost_removed, None, None, None, None, None, None) def next_handler(message): diff --git a/tests/test_telebot.py b/tests/test_telebot.py index f395ff9c5..9c7ed221f 100644 --- a/tests/test_telebot.py +++ b/tests/test_telebot.py @@ -554,7 +554,8 @@ def create_message_update(text): business_message=None, business_connection=None, edited_business_message=None, - deleted_business_messages=None, ) + deleted_business_messages=None, + managed_bot=None) def test_is_string_unicode(self): s1 = u'string' diff --git a/tests/test_types.py b/tests/test_types.py index bbfae60f1..ba6211af7 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -198,19 +198,31 @@ def test_InlineQueryResultCachedPhoto_with_markup(): def test_json_poll_1(): - jsonstring = r'{"message_id": 395020,"from": {"id": 111,"is_bot": false,"first_name": "FN","last_name": "LN","username": "Badiboy","language_code": "ru"},"chat": {"id": 111,"first_name": "FN","last_name": "LN","username": "Badiboy","type": "private"},"date": 1587841239,"poll": {"id": "5272018969396510722","question": "Test poll 1","options": [{"text": "Answer 1","voter_count": 0},{"text": "Answer 2","voter_count": 0}],"total_voter_count": 0,"is_closed": false,"is_anonymous": true,"type": "regular","allows_multiple_answers": true}}' + jsonstring = r'{"message_id":2649246,"from":{"id":927266710,"is_bot":false,"first_name":"a","username":"b","language_code":"en"},"chat":{"id":1234,"first_name":"a","username":"b","type":"private"},"date":1775379138,"poll":{"id":"5373272187744556440","question":"Test","options":[{"persistent_id":"0","text":"1","voter_count":0},{"persistent_id":"1","text":"2","voter_count":0}],"total_voter_count":0,"is_closed":false,"is_anonymous":true,"allows_multiple_answers":false,"allows_revoting":false,"type":"quiz","correct_option_id":0,"correct_option_ids":[0]}}' msg = types.Message.de_json(jsonstring) assert msg.poll is not None assert isinstance(msg.poll, types.Poll) - assert msg.poll.id == '5272018969396510722' - assert msg.poll.question is not None - assert msg.poll.options is not None + assert msg.poll.id == '5373272187744556440' + assert msg.poll.question == 'Test' assert len(msg.poll.options) == 2 - assert msg.poll.allows_multiple_answers is True + assert msg.poll.options[0].text == '1' + assert msg.poll.options[1].text == '2' + assert msg.poll.options[0].voter_count == 0 + assert msg.poll.options[1].voter_count == 0 + assert msg.poll.options[0].persistent_id == '0' + assert msg.poll.options[1].persistent_id == '1' + assert msg.poll.total_voter_count == 0 + assert msg.poll.is_closed is False + assert msg.poll.is_anonymous is True + assert msg.poll.allows_multiple_answers is False + assert msg.poll.allows_revoting is False + assert msg.poll.type == 'quiz' + assert msg.poll.correct_option_id == 0 + assert msg.poll.correct_option_ids == [0] def test_json_poll_answer(): - jsonstring = r'{"poll_id": "5895675970559410186", "user": {"id": 329343347, "is_bot": false, "first_name": "Test", "username": "test_user", "last_name": "User", "language_code": "en"}, "option_ids": [1]}' + jsonstring = r'{"poll_id": "5895675970559410186", "option_persistent_ids": ["0"], "user": {"id": 329343347, "is_bot": false, "first_name": "Test", "username": "test_user", "last_name": "User", "language_code": "en"}, "option_ids": [1]}' __import__('pprint').pprint(__import__('json').loads(jsonstring)) poll_answer = types.PollAnswer.de_json(jsonstring) assert poll_answer.poll_id == '5895675970559410186'