diff --git a/README.md b/README.md index 24a6e7845..374e98b40 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 6a0265c3e..c070afee0 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -250,6 +250,7 @@ def __init__( self.deleted_business_messages_handlers = [] self.purchased_paid_media_handlers = [] self.managed_bot_handlers = [] + self.guest_message_handlers = [] self.custom_filters = {} self.state_handlers = [] @@ -728,6 +729,7 @@ def process_new_updates(self, updates: List[types.Update]): new_deleted_business_messages = None new_purchased_paid_media = None new_managed_bots = None + new_guest_messages = None for update in updates: if apihelper.ENABLE_MIDDLEWARE and not self.use_class_middlewares: @@ -815,6 +817,9 @@ def process_new_updates(self, updates: List[types.Update]): if update.managed_bot: if new_managed_bots is None: new_managed_bots = [] new_managed_bots.append(update.managed_bot) + if update.guest_message: + if new_guest_messages is None: new_guest_messages = [] + new_guest_messages.append(update.guest_message) if new_messages: self.process_new_messages(new_messages) @@ -864,6 +869,8 @@ def process_new_updates(self, updates: List[types.Update]): self.process_new_purchased_paid_media(new_purchased_paid_media) if new_managed_bots: self.process_new_managed_bot(new_managed_bots) + if new_guest_messages: + self.process_new_guest_message(new_guest_messages) def process_new_messages(self, new_messages): """ @@ -1011,6 +1018,12 @@ 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_new_guest_message(self, new_guest_messages): + """ + :meta private: + """ + self._notify_command_handlers(self.guest_message_handlers, new_guest_messages, 'guest_message') def process_middlewares(self, update): """ @@ -1601,7 +1614,7 @@ def leave_chat(self, chat_id: Union[int, str]) -> bool: return apihelper.leave_chat(self.token, chat_id) - def get_chat_administrators(self, chat_id: Union[int, str]) -> List[types.ChatMember]: + def get_chat_administrators(self, chat_id: Union[int, str], return_bots: Optional[bool]=None) -> List[types.ChatMember]: """ Use this method to get a list of administrators in a chat. On success, returns an Array of ChatMember objects that contains @@ -1611,10 +1624,16 @@ def get_chat_administrators(self, chat_id: Union[int, str]) -> List[types.ChatMe :param chat_id: Unique identifier for the target chat or username of the target supergroup or channel (in the format @channelusername) + :type chat_id: :obj:`int` or :obj:`str` + + :param return_bots: Pass True to additionally receive all bots that are administrators of the chat. + By default, bots other than the current bot are omitted. + :type return_bots: :obj:`bool` + :return: List made of ChatMember objects. :rtype: :obj:`list` of :class:`telebot.types.ChatMember` """ - result = apihelper.get_chat_administrators(self.token, chat_id) + result = apihelper.get_chat_administrators(self.token, chat_id, return_bots=return_bots) return [types.ChatMember.de_json(r) for r in result] @@ -1652,6 +1671,24 @@ def get_chat_member_count(self, chat_id: Union[int, str]) -> int: :rtype: :obj:`int` """ return apihelper.get_chat_member_count(self.token, chat_id) + + def get_user_personal_chat_messages(self, user_id: int, limit: int) -> List[types.Message]: + """ + Use this method to get the last messages from the personal chat (i.e., the chat currently added to their profile) of a given user. On success, an array of Message objects is returned. + + Telegram documentation: https://core.telegram.org/bots/api#getuserpersonalchatmessages + + :param user_id: Unique identifier for the target user + :type user_id: :obj:`int` + + :param limit: The maximum number of messages to return; 1-20 + :type limit: :obj:`int` + + :return: List of Message objects. + :rtype: :obj:`list` of :class:`telebot.types.Message` + """ + result = apihelper.get_user_personal_chat_messages(self.token, user_id, limit) + return [types.Message.de_json(r) for r in result] def set_chat_sticker_set(self, chat_id: Union[int, str], sticker_set_name: str) -> types.StickerSet: @@ -2144,6 +2181,52 @@ def delete_messages(self, chat_id: Union[int, str], message_ids: List[int]): :return: Returns True on success. """ return apihelper.delete_messages(self.token, chat_id, message_ids) + + def delete_message_reaction(self, chat_id: Union[int, str], message_id: int, user_id: Optional[int]=None, actor_chat_id: Optional[int]=None) -> bool: + """ + Use this method to remove a reaction from a message in a group or a supergroup chat. + The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#deletemessagereaction + + :param chat_id: Unique identifier for the target chat or username of the target supergroup (in the format @username) + :type chat_id: :obj:`int` or :obj:`str` + + :param message_id: Identifier of the target message + :type message_id: :obj:`int` + + :param user_id: Identifier of the user whose reaction will be removed, if the reaction was added by a user + :type user_id: :obj:`int` + + :param actor_chat_id: Identifier of the chat whose reaction will be removed, if the reaction was added by a chat + :type actor_chat_id: :obj:`int` + + :return: Returns True on success. + :rtype: :obj:`bool` + """ + return apihelper.delete_message_reaction(self.token, chat_id, message_id, user_id=user_id, actor_chat_id=actor_chat_id) + + def delete_all_message_reactions(self, chat_id: Union[int, str], user_id: Optional[int]=None, actor_chat_id: Optional[int]=None) -> bool: + """ + Use this method to remove up to 10000 recent reactions in a group or a supergroup chat added by a given user or chat. + The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#deleteallmessagereactions + + :param chat_id: Unique identifier for the target chat or username of the target supergroup (in the format @username) + :type chat_id: :obj:`int` or :obj:`str` + + :param user_id: Identifier of the user whose reactions will be removed, if the reactions were added by a user + :type user_id: :obj:`int` + + :param actor_chat_id: Identifier of the chat whose reactions will be removed, if the reactions were added by a chat + :type actor_chat_id: :obj:`int` + + :return: Returns True on success. + :rtype: :obj:`bool` + """ + return apihelper.delete_all_message_reactions(self.token, chat_id, user_id=user_id, actor_chat_id=actor_chat_id) + def forward_messages(self, chat_id: Union[str, int], from_chat_id: Union[str, int], message_ids: List[int], @@ -2564,6 +2647,92 @@ def send_photo( direct_messages_topic_id=direct_messages_topic_id, suggested_post_parameters=suggested_post_parameters ) ) + + def send_live_photo( + self, business_connection_id: Optional[str], chat_id: Union[int, str], + live_photo: Union[Any, str], photo: Union[Any, str], + caption: Optional[str]=None, parse_mode: Optional[str]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, show_caption_above_media: Optional[bool]=None, + has_spoiler: Optional[bool]=None, disable_notification: Optional[bool]=None, protect_content: Optional[bool]=None, + allow_paid_broadcast: Optional[bool]=None, message_effect_id: Optional[str]=None, + suggested_post_parameters: Optional[types.SuggestedPostParameters]=None, reply_parameters: Optional[types.ReplyParameters]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None) -> types.Message: + """ + Use this method to send live photos. On success, the sent Message is returned. + + Telegram documentation: https://core.telegram.org/bots/api#sendlivephoto + + :param business_connection_id: Unique identifier of the business connection on behalf of which the message will be sent + :type business_connection_id: :obj:`str` + + :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername) + :type chat_id: :obj:`int` or :obj:`str` + + :param live_photo: Live photo video to send. The video must be no longer than 10 seconds and must not exceed 10 MB in size. + Pass a file_id as String to send a video that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. + More information on Sending Files ». Sending live photos by a URL is currently unsupported. + :type live_photo: :obj:`str` or :class:`telebot.types.InputFile` + + :param photo: The static photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. + More information on Sending Files ». Sending live photos by a URL is currently unsupported. + :type photo: :obj:`str` or :class:`telebot.types.InputFile` + + :param caption: Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing + :type caption: :obj:`str` + + :param parse_mode: Mode for parsing entities in the video caption. See formatting options for more details. + :type parse_mode: :obj:`str` + + :param caption_entities: A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode + :type caption_entities: :obj:`list` of :class:`telebot.types.Message + + :param show_caption_above_media: Pass True, if the caption must be shown above the message media + :type show_caption_above_media: :obj:`bool` + + :param has_spoiler: Pass True if the video needs to be covered with a spoiler animation + :type has_spoiler: :obj:`bool` + + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :type disable_notification: :obj:`bool` + + :param protect_content: Protects the contents of the sent message from forwarding and saving + :type protect_content: :obj:`bool` + + :param allow_paid_broadcast: Pass 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. + :type allow_paid_broadcast: :obj:`bool` + + :param message_effect_id: Unique identifier of the message effect to be added to the message; for private chats only + :type message_effect_id: :obj:`str` + + :param suggested_post_parameters: A JSON-serialized object containing the parameters of the suggested post to send; for direct messages chats only. + If the message is sent as a reply to another suggested post, then that suggested post is automatically declined + :type suggested_post_parameters: :class:`telebot.types.SuggestedPostParameters` + + :param reply_parameters: Additional parameters for replies to messages + :type reply_parameters: :class:`telebot.types.ReplyParameters` + + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions + to remove a reply keyboard or to force a reply from the user. + :type reply_markup: :class:`telebot.types.InlineKeyboardMarkup` or :class:`telebot.types.ReplyKeyboardMarkup` or :class:`telebot.types.ReplyKeyboardRemove` + or :class:`telebot.types.ForceReply` + + :return: On success, the sent Message is returned. + :rtype: :class:`telebot.types.Message` + """ + parse_mode = self.parse_mode if (parse_mode is None) else parse_mode + disable_notification = self.disable_notification if (disable_notification is None) else disable_notification + protect_content = self.protect_content if (protect_content is None) else protect_content + + return types.Message.de_json( + apihelper.send_live_photo( + self.token, business_connection_id, chat_id, live_photo, photo, caption=caption, parse_mode=parse_mode, + caption_entities=caption_entities, show_caption_above_media=show_caption_above_media, has_spoiler=has_spoiler, disable_notification=disable_notification, + protect_content=protect_content, allow_paid_broadcast=allow_paid_broadcast, message_effect_id=message_effect_id, + suggested_post_parameters=suggested_post_parameters, reply_parameters=reply_parameters, reply_markup=reply_markup + ) + ) + def send_audio( self, chat_id: Union[int, str], audio: Union[Any, str], @@ -3659,7 +3828,8 @@ def send_media_group( self, chat_id: Union[int, str], media: List[Union[ types.InputMediaAudio, types.InputMediaDocument, - types.InputMediaPhoto, types.InputMediaVideo]], + types.InputMediaPhoto, types.InputMediaVideo, + types.InputMediaLivePhoto]], disable_notification: Optional[bool]=None, protect_content: Optional[bool]=None, reply_to_message_id: Optional[int]=None, # deprecated, for backward compatibility @@ -4263,7 +4433,7 @@ def send_message_draft( :param draft_id: Unique identifier of the message draft; must be non-zero. Changes of drafts with the same identifier are animated :type draft_id: :obj:`int` - :param text: Text of the message to be sent, 1-4096 characters after entities parsing + :param text: Text of the message to be sent, 0-4096 characters after entities parsing. Pass an empty text to show a “Thinking…” placeholder. :type text: :obj:`str` :param parse_mode: Mode for parsing entities in the message text. See formatting options for more details. @@ -5268,6 +5438,42 @@ def replace_managed_bot_token(self, user_id: int) -> str: """ return apihelper.replace_managed_bot_token(self.token, user_id) + def get_managed_bot_access_settings(self, user_id: int) -> types.BotAccessSettings: + """ + Use this method to get the access settings of a managed bot. Returns a BotAccessSettings object on success. + + Telegram documentation: https://core.telegram.org/bots/api#getmanagedbotaccesssettings + + :param user_id: User identifier of the managed bot whose access settings will be returned + :type user_id: :obj:`int` + + :return: Returns a BotAccessSettings object on success. + :rtype: :class:`telebot.types.BotAccessSettings` + """ + return types.BotAccessSettings.de_json( + apihelper.get_managed_bot_access_settings(self.token, user_id) + ) + + def set_managed_bot_access_settings(self, user_id: int, is_access_restricted: bool, added_user_ids: Optional[List[int]]=None) -> bool: + """ + Use this method to change the access settings of a managed bot. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#setmanagedbotaccesssettings + + :param user_id: User identifier of the managed bot whose access settings will be changed + :type user_id: :obj:`int` + + :param is_access_restricted: Pass True, if only selected users can access the bot. The bot's owner can always access it. + :type is_access_restricted: :obj:`bool` + + :param added_user_ids: A JSON-serialized list of up to 10 identifiers of users who will have access to the bot in addition to its owner. Ignored if is_access_restricted is false. + :type added_user_ids: :obj:`list` of :obj:`int` + + :return: True on success. + :rtype: :obj:`bool` + """ + return apihelper.set_managed_bot_access_settings(self.token, user_id, is_access_restricted, added_user_ids=added_user_ids) + def set_my_commands(self, commands: List[types.BotCommand], scope: Optional[types.BotCommandScope]=None, language_code: Optional[str]=None) -> bool: @@ -6121,7 +6327,9 @@ def send_poll( 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: + description_entities: Optional[List[types.MessageEntity]]=None, + members_only: Optional[bool]=None, + country_codes: Optional[List[str]]=None) -> types.Message: """ Use this method to send a native poll. On success, the sent Message is returned. @@ -6220,6 +6428,13 @@ def send_poll( :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 members_only: Pass True, if voting is limited to users who have been members of the chat where the poll is being sent for more than 24 hours; for channel chats only + :type members_only: :obj:`bool` + + :param country_codes: A JSON-serialized list of 0-12 two-letter ISO 3166-1 alpha-2 country codes indicating the countries from which users can vote in the poll; + for channel chats only. If omitted or empty, then users from any country can participate in the poll. + :type country_codes: :obj:`list` of :obj:`str` + :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` @@ -6302,7 +6517,8 @@ def send_poll( 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) + description=description, description_parse_mode=description_parse_mode, description_entities=description_entities, + members_only=members_only, country_codes=country_codes) ) @@ -6666,6 +6882,26 @@ def answer_callback_query( """ return apihelper.answer_callback_query( self.token, callback_query_id, text=text, show_alert=show_alert, url=url, cache_time=cache_time) + + + def answer_guest_query(self, guest_query_id: str, result: types.InlineQueryResultBase) -> types.SentGuestMessage: + """ + Use this method to reply to a received guest message. On success, a SentGuestMessage object is returned. + + Telegram documentation: https://core.telegram.org/bots/api#answerguestquery + + :param guest_query_id: Unique identifier for the query to be answered + :type guest_query_id: :obj:`str` + + :param result: A JSON-serialized object describing the message to be sent + :type result: :obj:`types.InlineQueryResult` + + :return: On success, a SentGuestMessage object is returned. + :rtype: :obj:`types.SentGuestMessage` + """ + return types.SentGuestMessage.de_json( + apihelper.answer_guest_query(self.token, guest_query_id, result) + ) def get_user_chat_boosts(self, chat_id: Union[int, str], user_id: int) -> types.UserChatBoosts: @@ -10499,6 +10735,54 @@ def register_managed_bot_handler(self, callback: Callable, func: Optional[Callab handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) self.add_managed_bot_handler(handler_dict) + def guest_message_handler(self, func=None, **kwargs): + """ + New guest message. The bot can use the field :field:`telebot.types.Message.is_guest` and the method answerGuestQuery to send a message in response. + + :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_guest_message_handler(handler_dict) + return handler + + return decorator + + def add_guest_message_handler(self, handler_dict): + """ + Adds a guest_message handler. + Note that you should use register_guest_message_handler to add guest_message_handler to the bot. + + :meta private: + + :param handler_dict: + :return: + """ + self.guest_message_handlers.append(handler_dict) + + def register_guest_message_handler(self, callback: Callable, func: Optional[Callable]=None, pass_bot: Optional[bool]=False, **kwargs): + """ + Registers guest message 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_guest_message_handler(handler_dict) + def add_custom_filter(self, custom_filter: Union[SimpleCustomFilter, AdvancedCustomFilter]): """ diff --git a/telebot/apihelper.py b/telebot/apihelper.py index 849537ad1..6e5366860 100644 --- a/telebot/apihelper.py +++ b/telebot/apihelper.py @@ -391,9 +391,11 @@ def leave_chat(token, chat_id): return _make_request(token, method_url, params=payload) -def get_chat_administrators(token, chat_id): +def get_chat_administrators(token, chat_id, return_bots=None): method_url = r'getChatAdministrators' payload = {'chat_id': chat_id} + if return_bots is not None: + payload['return_bots'] = return_bots return _make_request(token, method_url, params=payload) @@ -621,6 +623,53 @@ def send_photo( payload['suggested_post_parameters'] = suggested_post_parameters.to_json() return _make_request(token, method_url, params=payload, files=files, method='post') +def send_live_photo( + token, chat_id, live_photo, photo, + caption=None, parse_mode=None, caption_entities=None, show_caption_above_media=None, has_spoiler=None, + disable_notification=None, protect_content=None, reply_parameters=None, reply_markup=None, + business_connection_id=None, message_effect_id=None, allow_paid_broadcast=None, + direct_messages_topic_id=None, suggested_post_parameters=None): + method_url = r'sendLivePhoto' + files = {} + payload = {'chat_id': chat_id} + if util.is_string(live_photo): + payload['live_photo'] = live_photo + else: + files['live_photo'] = live_photo + if util.is_string(photo): + payload['photo'] = photo + else: + files['photo'] = photo + if caption: + payload['caption'] = caption + if parse_mode: + payload['parse_mode'] = parse_mode + if caption_entities: + payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities)) + if show_caption_above_media is not None: + payload['show_caption_above_media'] = show_caption_above_media + if has_spoiler is not None: + payload['has_spoiler'] = has_spoiler + if disable_notification is not None: + payload['disable_notification'] = disable_notification + if protect_content is not None: + payload['protect_content'] = protect_content + if reply_parameters is not None: + payload['reply_parameters'] = reply_parameters.to_json() + if reply_markup: + payload['reply_markup'] = _convert_markup(reply_markup) + if business_connection_id: + payload['business_connection_id'] = business_connection_id + if message_effect_id: + payload['message_effect_id'] = message_effect_id + if allow_paid_broadcast is not None: + payload['allow_paid_broadcast'] = allow_paid_broadcast + if direct_messages_topic_id is not None: + payload['direct_messages_topic_id'] = direct_messages_topic_id + if suggested_post_parameters is not None: + payload['suggested_post_parameters'] = suggested_post_parameters.to_json() + return _make_request(token, method_url, params=payload, files=files or None, method='post') + def send_paid_media( token, chat_id, star_count, media, caption=None, parse_mode=None, caption_entities=None, show_caption_above_media=None, @@ -1611,6 +1660,26 @@ def get_managed_bot_token(token, user_id): payload = {'user_id': user_id} return _make_request(token, method_url, params=payload , method='post') +def set_managed_bot_access_settings(token, user_id, is_access_restricted, added_user_ids=None): + method_url = 'setManagedBotAccessSettings' + payload = { + 'user_id': user_id, + 'is_access_restricted': is_access_restricted + } + if added_user_ids is not None: + payload['added_user_ids'] = json.dumps(added_user_ids) + return _make_request(token, method_url, params=payload , method='post') + +def get_user_personal_chat_messages(token, user_id, limit): + method_url = 'getUserPersonalChatMessages' + payload = {'user_id': user_id, 'limit': limit} + return _make_request(token, method_url, params=payload , method='post') + +def get_managed_bot_access_settings(token, user_id): + method_url = 'getManagedBotAccessSettings' + 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} @@ -2058,6 +2127,10 @@ def answer_callback_query(token, callback_query_id, text=None, show_alert=None, payload['cache_time'] = cache_time return _make_request(token, method_url, params=payload, method='post') +def answer_guest_query(token, guest_query_id, result): + method_url = 'answerGuestQuery' + payload = {'guest_query_id': guest_query_id, 'result': result.to_json()} + return _make_request(token, method_url, params=payload, method='post') def get_user_chat_boosts(token, chat_id, user_id): method_url = 'getUserChatBoosts' @@ -2557,7 +2630,7 @@ def send_poll( 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, 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): + correct_option_ids=None, description=None, description_parse_mode=None, description_entities=None, members_only=None, country_codes=None): method_url = r'sendPoll' payload = { 'chat_id': str(chat_id), @@ -2624,6 +2697,10 @@ def send_poll( 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)) + if members_only is not None: + payload['members_only'] = members_only + if country_codes is not None: + payload['country_codes'] = json.dumps(country_codes) return _make_request(token, method_url, params=payload) def create_forum_topic(token, chat_id, name, icon_color=None, icon_custom_emoji_id=None): @@ -2710,6 +2787,29 @@ def delete_messages(token, chat_id, message_ids): } return _make_request(token, method_url, params=payload) +def delete_message_reaction(token, chat_id, message_id, user_id=None, actor_chat_id=None): + method_url = 'deleteMessageReaction' + payload = { + 'chat_id': chat_id, + 'message_id': message_id + } + if user_id is not None: + payload['user_id'] = user_id + if actor_chat_id is not None: + payload['actor_chat_id'] = actor_chat_id + return _make_request(token, method_url, params=payload, method='post') + +def delete_all_message_reactions(token, chat_id, user_id=None, actor_chat_id=None): + method_url = 'deleteAllMessageReactions' + payload = { + 'chat_id': chat_id + } + if user_id is not None: + payload['user_id'] = user_id + if actor_chat_id is not None: + payload['actor_chat_id'] = actor_chat_id + return _make_request(token, method_url, params=payload, method='post') + def forward_messages(token, chat_id, from_chat_id, message_ids, disable_notification=None, message_thread_id=None, protect_content=None, direct_messages_topic_id=None): method_url = 'forwardMessages' diff --git a/telebot/async_telebot.py b/telebot/async_telebot.py index 99349a15c..2a69dc8d5 100644 --- a/telebot/async_telebot.py +++ b/telebot/async_telebot.py @@ -186,6 +186,7 @@ def __init__(self, token: str, parse_mode: Optional[str]=None, offset: Optional[ self.deleted_business_messages_handlers = [] self.purchased_paid_media_handlers = [] self.managed_bot_handlers = [] + self.guest_message_handlers = [] self.custom_filters = {} self.state_handlers = [] @@ -650,6 +651,7 @@ async def process_new_updates(self, updates: List[types.Update]): new_deleted_business_messages = None new_purchased_paid_media = None new_managed_bots = None + new_guest_messages = None for update in updates: @@ -726,6 +728,9 @@ async def process_new_updates(self, updates: List[types.Update]): if update.managed_bot: if new_managed_bots is None: new_managed_bots = [] new_managed_bots.append(update.managed_bot) + if update.guest_message: + if new_guest_messages is None: new_guest_messages = [] + new_guest_messages.append(update.guest_message) if new_messages: @@ -774,6 +779,8 @@ async def process_new_updates(self, updates: List[types.Update]): await self.process_new_purchased_paid_media(new_purchased_paid_media) if new_managed_bots: await self.process_new_managed_bots(new_managed_bots) + if new_guest_messages: + await self.process_new_guest_message(new_guest_messages) async def process_new_messages(self, new_messages): """ @@ -920,6 +927,12 @@ async def process_new_managed_bots(self, new_managed_bots): """ await self._process_updates(self.managed_bot_handlers, new_managed_bots, 'managed_bot') + async def process_new_guest_message(self, new_guest_messages): + """ + :meta private: + """ + await self._process_updates(self.guest_message_handlers, new_guest_messages, 'guest_message') + async def _get_middlewares(self, update_type): """ :meta private: @@ -2713,6 +2726,54 @@ def register_managed_bot_handler(self, callback: Callable, func: Optional[Callab handler_dict = self._build_handler_dict(callback, func=func, pass_bot=pass_bot, **kwargs) self.add_managed_bot_handler(handler_dict) + def guest_message_handler(self, func=None, **kwargs): + """ + New guest message. The bot can use the field :field:`telebot.types.Message.is_guest` and the method answerGuestQuery to send a message in response. + + :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_guest_message_handler(handler_dict) + return handler + + return decorator + + def add_guest_message_handler(self, handler_dict): + """ + Adds a guest_message handler. + Note that you should use register_guest_message_handler to add guest_message_handler to the bot. + + :meta private: + + :param handler_dict: + :return: + """ + self.guest_message_handlers.append(handler_dict) + + def register_guest_message_handler(self, callback: Callable, func: Optional[Callable]=None, pass_bot: Optional[bool]=False, **kwargs): + """ + Registers guest message 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_guest_message_handler(handler_dict) + @staticmethod def _build_handler_dict(handler, pass_bot=False, **filters): @@ -3129,7 +3190,7 @@ async def leave_chat(self, chat_id: Union[int, str]) -> bool: result = await asyncio_helper.leave_chat(self.token, chat_id) return result - async def get_chat_administrators(self, chat_id: Union[int, str]) -> List[types.ChatMember]: + async def get_chat_administrators(self, chat_id: Union[int, str], return_bots: Optional[bool]=None) -> List[types.ChatMember]: """ Use this method to get a list of administrators in a chat. On success, returns an Array of ChatMember objects that contains @@ -3139,10 +3200,16 @@ async def get_chat_administrators(self, chat_id: Union[int, str]) -> List[types. :param chat_id: Unique identifier for the target chat or username of the target supergroup or channel (in the format @channelusername) + :type chat_id: :obj:`int` or :obj:`str` + + :param return_bots: Pass True to additionally receive all bots that are administrators of the chat. + By default, bots other than the current bot are omitted. + :type return_bots: :obj:`bool` + :return: List made of ChatMember objects. :rtype: :obj:`list` of :class:`telebot.types.ChatMember` """ - result = await asyncio_helper.get_chat_administrators(self.token, chat_id) + result = await asyncio_helper.get_chat_administrators(self.token, chat_id, return_bots=return_bots) return [types.ChatMember.de_json(r) for r in result] @util.deprecated(deprecation_text="Use get_chat_member_count instead") @@ -3180,6 +3247,25 @@ async def get_chat_member_count(self, chat_id: Union[int, str]) -> int: """ result = await asyncio_helper.get_chat_member_count(self.token, chat_id) return result + + async def get_user_personal_chat_messages(self, user_id: int, limit: int) -> List[types.Message]: + """ + Use this method to get the last messages from the personal chat (i.e., the chat currently added to their profile) of a given user. On success, an array of Message objects is returned. + + Telegram documentation: https://core.telegram.org/bots/api#getuserpersonalchatmessages + + :param user_id: Unique identifier for the target user + :type user_id: :obj:`int` + + :param limit: The maximum number of messages to return; 1-20 + :type limit: :obj:`int` + + :return: An array of Message objects. + :rtype: :obj:`list` of :class:`telebot.types.Message` + """ + + result = await asyncio_helper.get_user_personal_chat_messages(self.token, user_id, limit) + return [types.Message.de_json(r) for r in result] async def set_chat_sticker_set(self, chat_id: Union[int, str], sticker_set_name: str) -> types.StickerSet: """ @@ -3744,6 +3830,51 @@ async def delete_messages(self, chat_id: Union[int, str], message_ids: List[int] """ return await asyncio_helper.delete_messages(self.token, chat_id, message_ids) + + async def delete_message_reaction(self, chat_id: Union[int, str], message_id: int, user_id: Optional[int]=None, actor_chat_id: Optional[int]=None) -> bool: + """ + Use this method to remove a reaction from a message in a group or a supergroup chat. + The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#deletemessagereaction + + :param chat_id: Unique identifier for the target chat or username of the target supergroup (in the format @username) + :type chat_id: :obj:`int` or :obj:`str` + + :param message_id: Identifier of the target message + :type message_id: :obj:`int` + + :param user_id: Identifier of the user whose reaction will be removed, if the reaction was added by a user + :type user_id: :obj:`int`, optional + + :param actor_chat_id: Identifier of the chat whose reaction will be removed, if the reaction was added by a chat + :type actor_chat_id: :obj:`int`, optional + + :return: Returns True on success. + :rtype: :obj:`bool` + """ + return await asyncio_helper.delete_message_reaction(self.token, chat_id, message_id, user_id, actor_chat_id) + + async def delete_all_message_reactions(self, chat_id: Union[int, str], user_id: Optional[int]=None, actor_chat_id: Optional[int]=None) -> bool: + """ + Use this method to remove up to 10000 recent reactions in a group or a supergroup chat added by a given user or chat. + The bot must have the 'can_delete_messages' administrator right in the chat. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#deleteallmessagereactions + + :param chat_id: Unique identifier for the target chat or username of the target supergroup (in the format @username) + :type chat_id: :obj:`int` or :obj:`str` + + :param user_id: Identifier of the user whose reactions will be removed, if the reactions were added by a user + :type user_id: :obj:`int`, optional + + :param actor_chat_id: Identifier of the chat whose reactions will be removed, if the reactions were added by a chat + :type actor_chat_id: :obj:`int`, optional + + :return: Returns True on success. + :rtype: :obj:`bool` + """ + return await asyncio_helper.delete_all_message_reactions(self.token, chat_id, user_id, actor_chat_id) async def forward_messages(self, chat_id: Union[str, int], from_chat_id: Union[str, int], message_ids: List[int], disable_notification: Optional[bool]=None, message_thread_id: Optional[int]=None, protect_content: Optional[bool]=None, @@ -4155,6 +4286,93 @@ async def send_photo( ) ) + async def send_live_photo( + self, business_connection_id: str, chat_id: Union[int, str], + live_photo: Union[Any, str], photo: Union[Any, str], + caption: Optional[str]=None, parse_mode: Optional[str]=None, + caption_entities: Optional[List[types.MessageEntity]]=None, + show_caption_above_media: Optional[bool]=None, has_spoiler: Optional[bool]=None, + disable_notification: Optional[bool]=None, protect_content: Optional[bool]=None, + allow_paid_broadcast: Optional[bool]=None, message_effect_id: Optional[str]=None, + suggested_post_parameters: Optional[types.SuggestedPostParameters]=None, reply_parameters: Optional[types.ReplyParameters]=None, + reply_markup: Optional[REPLY_MARKUP_TYPES]=None) -> types.Message: + """ + Use this method to send live photos. On success, the sent Message is returned. + + Telegram documentation: https://core.telegram.org/bots/api#sendlivephoto + + :param business_connection_id: Unique identifier of the business connection on behalf of which the message will be sent + :type business_connection_id: :obj:`str` + + :param chat_id: Unique identifier for the target chat or username of the target channel (in the format @channelusername) + :type chat_id: :obj:`int` or :obj:`str` + + :param live_photo: Live photo video to send. The video must be no longer than 10 seconds and must not exceed 10 MB in size. + Pass a file_id as String to send a video that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. + More information on Sending Files ». Sending live photos by a URL is currently unsupported. + :type live_photo: :obj:`str` or :class:`telebot.types.InputFile` + + :param photo: The static photo to send. Pass a file_id as String to send a photo that exists on the Telegram servers (recommended) or upload a new video using multipart/form-data. + More information on Sending Files ». Sending live photos by a URL is currently unsupported. + :type photo: :obj:`str` or :class:`telebot.types.InputFile` + + :param caption: Video caption (may also be used when resending videos by file_id), 0-1024 characters after entities parsing + :type caption: :obj:`str` + + :param parse_mode: Mode for parsing entities in the video caption. See formatting options for more details. + :type parse_mode: :obj:`str` + + :param caption_entities: A JSON-serialized list of special entities that appear in the caption, which can be specified instead of parse_mode + :type caption_entities: :obj:`list` of :class:`telebot.types.Message + + :param show_caption_above_media: Pass True, if the caption must be shown above the message media + :type show_caption_above_media: :obj:`bool` + + :param has_spoiler: Pass True if the video needs to be covered with a spoiler animation + :type has_spoiler: :obj:`bool` + + :param disable_notification: Sends the message silently. Users will receive a notification with no sound. + :type disable_notification: :obj:`bool` + + :param protect_content: Protects the contents of the sent message from forwarding and saving + :type protect_content: :obj:`bool` + + :param allow_paid_broadcast: Pass 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. + :type allow_paid_broadcast: :obj:`bool` + + :param message_effect_id: Unique identifier of the message effect to be added to the message; for private chats only + :type message_effect_id: :obj:`str` + + :param suggested_post_parameters: A JSON-serialized object containing the parameters of the suggested post to send; for direct messages chats only. + If the message is sent as a reply to another suggested post, then that suggested post is automatically declined + :type suggested_post_parameters: :class:`telebot.types.SuggestedPostParameters` + + :param reply_parameters: Additional parameters for replies to messages + :type reply_parameters: :class:`telebot.types.ReplyParameters` + + :param reply_markup: Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard, instructions + to remove a reply keyboard or to force a reply from the user. + :type reply_markup: :class:`telebot.types.InlineKeyboardMarkup` or :class:`telebot.types.ReplyKeyboardMarkup` or :class:`telebot.types.ReplyKeyboardRemove` + or :class:`telebot.types.ForceReply` + + :return: On success, the sent Message is returned. + :rtype: :class:`telebot.types.Message` + """ + disable_notification = self.disable_notification if (disable_notification is None) else disable_notification + protect_content = self.protect_content if (protect_content is None) else protect_content + + if reply_parameters and (reply_parameters.allow_sending_without_reply is None): + reply_parameters.allow_sending_without_reply = self.allow_sending_without_reply + + return types.Message.de_json( + await asyncio_helper.send_live_photo( + self.token, business_connection_id, chat_id, live_photo, photo, caption=caption, parse_mode=parse_mode, caption_entities=caption_entities, + show_caption_above_media=show_caption_above_media, has_spoiler=has_spoiler, disable_notification=disable_notification, protect_content=protect_content, + allow_paid_broadcast=allow_paid_broadcast, message_effect_id=message_effect_id, suggested_post_parameters=suggested_post_parameters, reply_parameters=reply_parameters, reply_markup=reply_markup + ) + ) + async def send_audio( self, chat_id: Union[int, str], audio: Union[Any, str], caption: Optional[str]=None, duration: Optional[int]=None, @@ -5254,7 +5472,8 @@ async def send_media_group( self, chat_id: Union[int, str], media: List[Union[ types.InputMediaAudio, types.InputMediaDocument, - types.InputMediaPhoto, types.InputMediaVideo]], + types.InputMediaPhoto, types.InputMediaVideo, + types.InputMediaLivePhoto]], disable_notification: Optional[bool]=None, protect_content: Optional[bool]=None, reply_to_message_id: Optional[int]=None, @@ -5859,7 +6078,7 @@ async def send_message_draft( :param draft_id: Unique identifier of the message draft; must be non-zero. Changes of drafts with the same identifier are animated :type draft_id: :obj:`int` - :param text: Text of the message to be sent, 1-4096 characters after entities parsing + :param text: Text of the message to be sent, 0-4096 characters after entities parsing. Pass an empty text to show a “Thinking…” placeholder. :type text: :obj:`str` :param parse_mode: Mode for parsing entities in the message text. See formatting options for more details. @@ -6826,6 +7045,43 @@ async def replace_managed_bot_token(self, user_id: int) -> str: """ return await asyncio_helper.replace_managed_bot_token(self.token, user_id) + async def get_managed_bot_access_settings(self, user_id: int) -> types.BotAccessSettings: + """ + Use this method to get the access settings of a managed bot. Returns a BotAccessSettings object on success. + + Telegram documentation: https://core.telegram.org/bots/api#getmanagedbotaccesssettings + + :param user_id: User identifier of the managed bot whose access settings will be returned + :type user_id: :obj:`int` + + :return: Returns a BotAccessSettings object on success. + :rtype: :class:`telebot.types.BotAccessSettings` + """ + return types.BotAccessSettings.de_json( + await asyncio_helper.get_managed_bot_access_settings(self.token, user_id) + ) + + async def set_managed_bot_access_settings(self, user_id: int, is_access_restricted: bool, + added_user_ids: Optional[List[int]]=None) -> bool: + """ + Use this method to change the access settings of a managed bot. Returns True on success. + + Telegram documentation: https://core.telegram.org/bots/api#setmanagedbotaccesssettings + + :param user_id: User identifier of the managed bot whose access settings will be changed + :type user_id: :obj:`int` + + :param is_access_restricted: Pass True, if only selected users can access the bot. The bot's owner can always access it. + :type is_access_restricted: :obj:`bool` + + :param added_user_ids: A JSON-serialized list of up to 10 identifiers of users who will have access to the bot in addition to its owner. Ignored if is_access_restricted is false. + :type added_user_ids: :obj:`list` of :obj:`int` + + :return: True on success. + :rtype: :obj:`bool` + """ + return await asyncio_helper.set_managed_bot_access_settings(self.token, user_id, is_access_restricted, added_user_ids) + async def set_my_commands(self, commands: List[types.BotCommand], scope: Optional[types.BotCommandScope]=None, language_code: Optional[str]=None) -> bool: @@ -7650,7 +7906,9 @@ async def send_poll( 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: + description_entities: Optional[List[types.MessageEntity]]=None, + members_only: Optional[bool]=None, + country_codes: Optional[List[str]]=None) -> types.Message: """ Use this method to send a native poll. On success, the sent Message is returned. @@ -7753,6 +8011,13 @@ async def send_poll( :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 members_only: Pass True, if voting is limited to users who have been members of the chat where the poll is being sent for more than 24 hours; for channel chats only + :type members_only: :obj:`bool` + + :param country_codes: A JSON-serialized list of 0-12 two-letter ISO 3166-1 alpha-2 country codes indicating the countries from which users can vote in the poll; + for channel chats only. If omitted or empty, then users from any country can participate in the poll. + :type country_codes: :obj:`list` of :obj:`str` + :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` @@ -7831,7 +8096,8 @@ async def send_poll( 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 + description_parse_mode=description_parse_mode, description_entities=description_entities, + members_only=members_only, country_codes=country_codes ) ) @@ -8177,12 +8443,23 @@ async def answer_callback_query( """ return await asyncio_helper.answer_callback_query(self.token, callback_query_id, text, show_alert, url, cache_time) -# getUserChatBoosts -# Use this method to get the list of boosts added to a chat by a user. Requires administrator rights in the chat. Returns a UserChatBoosts object. -# Parameter Type Required Description -# chat_id Integer or String Yes Unique identifier for the chat or username of the channel (in the format @channelusername) -# user_id Integer Yes Unique identifier of the target user + async def answer_guest_query(self, guest_query_id: str, result: types.InlineQueryResultBase) -> types.SentGuestMessage: + """ + Use this method to reply to a received guest message. On success, a SentGuestMessage object is returned. + + Telegram documentation: https://core.telegram.org/bots/api#answerguestquery + + :param guest_query_id: Unique identifier for the query to be answered + :type guest_query_id: :obj:`str` + + :param result: A JSON-serialized object describing the message to be sent + :type result: :obj:`types.InlineQueryResult` + + :return: On success, a SentGuestMessage object is returned. + :rtype: :obj:`types.SentGuestMessage` + """ + return types.SentGuestMessage.de_json(await asyncio_helper.answer_guest_query(self.token, guest_query_id, result)) async def get_user_chat_boosts(self, chat_id: Union[int, str], user_id: int) -> types.UserChatBoosts: """ diff --git a/telebot/asyncio_helper.py b/telebot/asyncio_helper.py index 7f00b319c..c343609c6 100644 --- a/telebot/asyncio_helper.py +++ b/telebot/asyncio_helper.py @@ -381,9 +381,11 @@ async def leave_chat(token, chat_id): return await _process_request(token, method_url, params=payload) -async def get_chat_administrators(token, chat_id): +async def get_chat_administrators(token, chat_id, return_bots=None): method_url = r'getChatAdministrators' payload = {'chat_id': chat_id} + if return_bots is not None: + payload['return_bots'] = return_bots return await _process_request(token, method_url, params=payload) @@ -636,6 +638,59 @@ async def send_photo( if suggested_post_parameters is not None: payload['suggested_post_parameters'] = suggested_post_parameters.to_json() return await _process_request(token, method_url, params=payload, files=files, method='post') + +async def send_live_photo( + token, chat_id, live_photo, photo, + caption=None, reply_markup=None, + parse_mode=None, disable_notification=None, timeout=None, + caption_entities=None, protect_content=None, + message_thread_id=None, has_spoiler=None,reply_parameters=None, + business_connection_id=None, message_effect_id=None, show_caption_above_media=None, allow_paid_broadcast=None, + direct_messages_topic_id=None, suggested_post_parameters=None): + method_url = r'sendLivePhoto' + payload = {'chat_id': chat_id} + files = {} + if util.is_string(live_photo): + payload['live_photo'] = live_photo + else: + files['live_photo'] = live_photo + if util.is_string(photo): + payload['photo'] = photo + else: + files['photo'] = photo + if caption: + payload['caption'] = caption + if reply_markup: + payload['reply_markup'] = await _convert_markup(reply_markup) + if parse_mode: + payload['parse_mode'] = parse_mode + if disable_notification is not None: + payload['disable_notification'] = disable_notification + if timeout: + payload['timeout'] = timeout + if caption_entities: + payload['caption_entities'] = json.dumps(types.MessageEntity.to_list_of_dicts(caption_entities)) + if reply_parameters is not None: + payload['reply_parameters'] = json.dumps(reply_parameters.to_dict()) + if protect_content is not None: + payload['protect_content'] = protect_content + if message_thread_id: + payload['message_thread_id'] = message_thread_id + if has_spoiler is not None: + payload['has_spoiler'] = has_spoiler + if business_connection_id: + payload['business_connection_id'] = business_connection_id + if message_effect_id: + payload['message_effect_id'] = message_effect_id + if show_caption_above_media is not None: + payload['show_caption_above_media'] = show_caption_above_media + if allow_paid_broadcast is not None: + payload['allow_paid_broadcast'] = allow_paid_broadcast + if direct_messages_topic_id is not None: + payload['direct_messages_topic_id'] = direct_messages_topic_id + if suggested_post_parameters is not None: + payload['suggested_post_parameters'] = suggested_post_parameters.to_json() + return await _process_request(token, method_url, params=payload, files=files, method='post') async def send_paid_media( token, chat_id, star_count, media, @@ -1623,6 +1678,26 @@ async def get_managed_bot_token(token, user_id): payload = {'user_id': user_id} return await _process_request(token, method_url, params=payload , method='post') +async def set_managed_bot_access_settings(token, user_id, is_access_restricted, added_user_ids=None): + method_url = 'setManagedBotAccessSettings' + payload = { + 'user_id': user_id, + 'is_access_restricted': is_access_restricted + } + if added_user_ids is not None: + payload['added_user_ids'] = json.dumps(added_user_ids) + return await _process_request(token, method_url, params=payload , method='post') + +async def get_user_personal_chat_messages(token, user_id, limit): + method_url = 'getUserPersonalChatMessages' + payload = {'user_id': user_id, 'limit': limit} + return await _process_request(token, method_url, params=payload , method='post') + +async def get_managed_bot_access_settings(token, user_id): + method_url = 'getManagedBotAccessSettings' + 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} @@ -2073,6 +2148,12 @@ async def answer_callback_query(token, callback_query_id, text=None, show_alert= payload['cache_time'] = cache_time return await _process_request(token, method_url, params=payload, method='post') + +async def answer_guest_query(token, guest_query_id, result): + method_url = 'answerGuestQuery' + payload = {'guest_query_id': guest_query_id, 'result': result.to_json()} + return await _process_request(token, method_url, params=payload, method='post') + async def get_user_chat_boosts(token, chat_id, user_id): method_url = 'getUserChatBoosts' payload = {'chat_id': chat_id, 'user_id': user_id} @@ -2537,7 +2618,7 @@ async def send_poll( 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, 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): + correct_option_ids=None, description=None, description_parse_mode=None, description_entities=None, members_only=None, country_codes=None): method_url = r'sendPoll' payload = { 'chat_id': str(chat_id), @@ -2605,6 +2686,10 @@ async def send_poll( 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)) + if members_only is not None: + payload['members_only'] = members_only + if country_codes is not None: + payload['country_codes'] = json.dumps(country_codes) return await _process_request(token, method_url, params=payload) @@ -2683,6 +2768,24 @@ async def delete_messages(token, chat_id, message_ids): } return await _process_request(token, method_url, params=payload) +async def delete_message_reaction(token, chat_id, message_id, user_id=None, actor_chat_id=None): + method_url = 'deleteMessageReaction' + payload = {'chat_id': chat_id, 'message_id': message_id} + if user_id is not None: + payload['user_id'] = user_id + if actor_chat_id is not None: + payload['actor_chat_id'] = actor_chat_id + return await _process_request(token, method_url, params=payload, method='post') + +async def delete_all_message_reactions(token, chat_id, user_id=None, actor_chat_id=None): + method_url = 'deleteAllMessageReactions' + payload = {'chat_id': chat_id} + if user_id is not None: + payload['user_id'] = user_id + if actor_chat_id is not None: + payload['actor_chat_id'] = actor_chat_id + return await _process_request(token, method_url, params=payload, method='post') + async def forward_messages(token, chat_id, from_chat_id, message_ids, disable_notification=None, message_thread_id=None, protect_content=None, direct_messages_topic_id=None): method_url = 'forwardMessages' diff --git a/telebot/types.py b/telebot/types.py index 84f8eef8b..33943161f 100644 --- a/telebot/types.py +++ b/telebot/types.py @@ -209,6 +209,9 @@ class Update(JsonDeserializable): :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` + :param guest_message: Optional. New guest message. The bot can use the field Message.guest_query_id and the method answerGuestQuery to send a message in response. + :type guest_message: :class:`telebot.types.Message` + :return: Instance of the class :rtype: :class:`telebot.types.Update` @@ -242,18 +245,19 @@ def de_json(cls, json_string): 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')) + guest_message = Message.de_json(obj.get('guest_message')) 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, managed_bot) + deleted_business_messages, purchased_paid_media, managed_bot, guest_message) 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, managed_bot): + deleted_business_messages, purchased_paid_media, managed_bot, guest_message): self.update_id: int = update_id self.message: Optional[Message] = message self.edited_message: Optional[Message] = edited_message @@ -279,6 +283,7 @@ def __init__(self, update_id, message, edited_message, channel_post, edited_chan 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 + self.guest_message: Optional[Message] = guest_message class ChatMemberUpdated(JsonDeserializable): """ @@ -507,6 +512,9 @@ class User(JsonDeserializable, Dictionaryable, JsonSerializable): getMe. :type can_read_all_group_messages: :obj:`bool` + :param supports_guest_queries: Optional. True, if the bot supports guest queries from chats it is not a member of. Returned only in getMe. + :type supports_guest_queries: :obj:`bool` + :param supports_inline_queries: Optional. True, if the bot supports inline queries. Returned only in getMe. :type supports_inline_queries: :obj:`bool` @@ -538,7 +546,8 @@ 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, can_manage_bots=None, **kwargs): + has_main_web_app=None, has_topics_enabled=None, allows_users_to_create_topics=None, can_manage_bots=None, + supports_guest_queries=None, **kwargs): self.id: int = id self.is_bot: bool = is_bot self.first_name: str = first_name @@ -555,6 +564,8 @@ def __init__(self, id, is_bot, first_name, last_name=None, username=None, langua 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 + self.supports_guest_queries: Optional[bool] = supports_guest_queries + @property def full_name(self) -> str: """ @@ -584,7 +595,8 @@ def to_dict(self): '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, - 'can_manage_bots': self.can_manage_bots + 'can_manage_bots': self.can_manage_bots, + 'supports_guest_queries': self.supports_guest_queries } @@ -998,6 +1010,11 @@ class Message(JsonDeserializable): :param date: Date the message was sent in Unix time :type date: :obj:`int` + :param guest_query_id: Optional. The unique identifier for the guest query. Use this identifier with the method answerGuestQuery + to send a response message. If non-empty, the message belongs to the chat where the guest bot was summoned, which may not coincide + with other existing bot chats sharing the same identifier. + :type guest_query_id: :obj:`str` + :param business_connection_id: Optional. Unique identifier of the business connection from which the message was received. If non-empty, the message belongs to a chat of the corresponding business account that is independent from any potential bot chat which might share the same identifier. :type business_connection_id: :obj:`str` @@ -1037,6 +1054,12 @@ class Message(JsonDeserializable): :param via_bot: Optional. Bot through which the message was sent :type via_bot: :class:`telebot.types.User` + :param guest_bot_caller_user: Optional. For a message sent by a guest bot, this is the user whose original message triggered the bot's response + :type guest_bot_caller_user: :class:`telebot.types.User` + + :param guest_bot_caller_chat: Optional. For a message sent by a guest bot, this is the chat whose original message triggered the bot's response + :type guest_bot_caller_chat: :class:`telebot.types.Chat` + :param edit_date: Optional. Date the message was last edited in Unix time :type edit_date: :obj:`int` @@ -1089,6 +1112,10 @@ class Message(JsonDeserializable): :param document: Optional. Message is a general file, information about the file :type document: :class:`telebot.types.Document` + :param live_photo: Optional. Message is a live photo, information about the live photo. + For backward compatibility, when this field is set, the photo field will also be set + :type live_photo: :class:`telebot.types.LivePhoto` + :param paid_media: Optional. Message contains paid media; information about the paid media :type paid_media: :class:`telebot.types.PaidMediaInfo` @@ -1638,7 +1665,14 @@ def de_json(cls, json_string): 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'] - + if 'guest_bot_caller_user' in obj: + opts['guest_bot_caller_user'] = User.de_json(obj['guest_bot_caller_user']) + if 'guest_bot_caller_chat' in obj: + opts['guest_bot_caller_chat'] = Chat.de_json(obj['guest_bot_caller_chat']) + if 'guest_query_id' in obj: + opts['guest_query_id'] = obj['guest_query_id'] + if 'live_photo' in obj: + opts['live_photo'] = LivePhoto.de_json(obj['live_photo']) return cls(message_id, from_user, date, chat, content_type, opts, json_string) @classmethod @@ -1781,6 +1815,10 @@ def __init__(self, message_id, from_user, date, chat, content_type, options, jso self.poll_option_added: Optional[PollOptionAdded] = None self.poll_option_deleted: Optional[PollOptionDeleted] = None self.reply_to_poll_option_id: Optional[str] = None + self.guest_bot_caller_user: Optional[User] = None + self.guest_bot_caller_chat: Optional[Chat] = None + self.guest_query_id: Optional[str] = None + self.live_photo: Optional[LivePhoto] = None for key in options: setattr(self, key, options[key]) @@ -3730,6 +3768,9 @@ class ChatMemberRestricted(ChatMember): :param can_add_web_page_previews: True, if the user is allowed to add web page previews to their messages :type can_add_web_page_previews: :obj:`bool` + :param can_react_to_messages: True, if the user is allowed to react to messages + :type can_react_to_messages: :obj:`bool` + :param can_change_info: True, if the user is allowed to change the chat title, photo and other settings :type can_change_info: :obj:`bool` @@ -3758,7 +3799,7 @@ def __init__(self, user, status, is_member, can_send_messages, can_send_audios, can_send_photos, can_send_videos, can_send_video_notes, can_send_voice_notes, can_send_polls, can_send_other_messages, can_add_web_page_previews, can_change_info, can_invite_users, can_pin_messages, can_manage_topics, - until_date=None, tag=None, can_edit_tag=None, **kwargs): + until_date=None, tag=None, can_edit_tag=None, can_react_to_messages=None, **kwargs): super().__init__(user, status, **kwargs) self.is_member: bool = is_member self.can_send_messages: bool = can_send_messages @@ -3778,6 +3819,7 @@ def __init__(self, user, status, is_member, can_send_messages, can_send_audios, self.until_date: Optional[int] = until_date self.tag: Optional[str] = tag self.can_edit_tag: Optional[bool] = can_edit_tag + self.can_react_to_messages: Optional[bool] = can_react_to_messages # noinspection PyUnresolvedReferences @@ -3861,6 +3903,9 @@ class ChatPermissions(JsonDeserializable, JsonSerializable, Dictionaryable): messages :type can_add_web_page_previews: :obj:`bool` + :param can_react_to_messages: Optional. True, if the user is allowed to react to messages. If omitted, defaults to the value of can_send_messages. + :type can_react_to_messages: :obj:`bool` + :param can_change_info: Optional. True, if the user is allowed to change the chat title, photo and other settings. Ignored in public supergroups :type can_change_info: :obj:`bool` @@ -3896,7 +3941,7 @@ def __init__(self, can_send_messages=None, can_send_media_messages=None,can_send can_send_voice_notes=None, can_send_polls=None, can_send_other_messages=None, can_add_web_page_previews=None, can_change_info=None, can_invite_users=None, can_pin_messages=None, - can_manage_topics=None, can_edit_tag=None, **kwargs): + can_manage_topics=None, can_edit_tag=None, can_react_to_messages=None, **kwargs): self.can_send_messages: Optional[bool] = can_send_messages self.can_send_polls: Optional[bool] = can_send_polls self.can_send_other_messages: Optional[bool] = can_send_other_messages @@ -3912,6 +3957,7 @@ def __init__(self, can_send_messages=None, can_send_media_messages=None,can_send self.can_send_videos: Optional[bool] = can_send_videos self.can_send_video_notes: Optional[bool] = can_send_video_notes self.can_send_voice_notes: Optional[bool] = can_send_voice_notes + self.can_react_to_messages: Optional[bool] = can_react_to_messages if kwargs.get("de_json", False) and can_send_media_messages is not None: # Telegram passes can_send_media_messages in Chat.permissions. Temporary created parameter "de_json" allows avoid @@ -3961,6 +4007,8 @@ def to_dict(self): json_dict['can_manage_topics'] = self.can_manage_topics if self.can_edit_tag is not None: json_dict['can_edit_tag'] = self.can_edit_tag + if self.can_react_to_messages is not None: + json_dict['can_react_to_messages'] = self.can_react_to_messages return json_dict @@ -7438,6 +7486,180 @@ def to_dict(self): return ret +class InputMediaLivePhoto(InputMedia): + """ + Represents a live photo to be sent. + + Telegram Documentation: https://core.telegram.org/bots/api#inputmedialivephoto + + :param media: Video of the live photo to send. Pass a file_id to send a file that exists on the Telegram servers (recommended) or pass “attach://” + to upload a new one using multipart/form-data under name. More information on Sending Files ». Sending live photos by a URL is currently unsupported. + :type media: :obj:`str` + + :param photo: The static photo to send. Pass a file_id to send a file that exists on the Telegram servers (recommended) or pass “attach://” to upload a new one + using multipart/form-data under name. More information on Sending Files ». Sending live photos by a URL is currently unsupported. + :type photo: :obj:`str` + + :param caption: Optional. Caption of the live photo to be sent, 0-1024 characters after entities parsing + :type caption: :obj:`str` + + :param parse_mode: Optional. Mode for parsing entities in the live photo caption. See formatting options for more details. + :type parse_mode: :obj:`str` + + :param caption_entities: Optional. List of special entities that appear in the caption, which can be specified instead of parse_mode + :type caption_entities: :obj:`list` of :class:`telebot.types.MessageEntity` + + :param show_caption_above_media: Optional. Pass True, if the caption must be shown above the message media + :type show_caption_above_media: :obj:`bool` + + :param has_spoiler: Optional. Pass True if the live photo needs to be covered with a spoiler animation + :type has_spoiler: :obj:`bool` + + :return: Instance of the class + :rtype: :class:`telebot.types.InputMediaLivePhoto` + """ + def __init__(self, media: str, photo: str, caption: Optional[str] = None, parse_mode: Optional[str] = None, + caption_entities: Optional[List[MessageEntity]] = None, show_caption_above_media: Optional[bool] = None, has_spoiler: Optional[bool] = None): + super(InputMediaLivePhoto, self).__init__(type="live_photo", media=media, caption=caption, parse_mode=parse_mode, caption_entities=caption_entities) + self.photo: str = photo + self.show_caption_above_media: Optional[bool] = show_caption_above_media + self.has_spoiler: Optional[bool] = has_spoiler + + def to_dict(self): + json_dict = super(InputMediaLivePhoto, self).to_dict() + json_dict['photo'] = self.photo + if self.show_caption_above_media is not None: + json_dict['show_caption_above_media'] = self.show_caption_above_media + if self.has_spoiler is not None: + json_dict['has_spoiler'] = self.has_spoiler + return json_dict + +class InputMediaLocation(InputMedia): + """ + Represents a location to be sent. + + Telegram Documentation: https://core.telegram.org/bots/api#inputmedialocation + + :param latitude: Latitude of the location + :type latitude: :obj:`float` + + :param longitude: Longitude of the location + :type longitude: :obj:`float` + + :param horizontal_accuracy: Optional. The radius of uncertainty for the location, measured in meters; 0-1500 + :type horizontal_accuracy: :obj:`float` + + :return: Instance of the class + :rtype: :class:`telebot.types.InputMediaLocation` + """ + def __init__(self, latitude: float, longitude: float, horizontal_accuracy: Optional[float] = None): + super(InputMediaLocation, self).__init__(type="location", media="") + self.latitude: float = latitude + self.longitude: float = longitude + self.horizontal_accuracy: Optional[float] = horizontal_accuracy + + def to_dict(self): + json_dict = super(InputMediaLocation, self).to_dict() + json_dict['latitude'] = self.latitude + json_dict['longitude'] = self.longitude + if self.horizontal_accuracy is not None: + json_dict['horizontal_accuracy'] = self.horizontal_accuracy + # Remove 'media' field as it's not used for location + json_dict.pop('media', None) + return json_dict + +class InputMediaSticker(InputMedia): + """ + Represents a sticker file to be sent. + + Telegram Documentation: https://core.telegram.org/bots/api#inputmediasticker + + :param media: File to send. Pass a file_id to send a file that exists on the Telegram servers (recommended), + pass an HTTP URL for Telegram to get a .WEBP sticker from the Internet, or pass “attach://” + to upload a new .WEBP, .TGS, or .WEBM sticker using multipart/form-data under name. More information + on Sending Files » + :type media: :obj:`str` + + :param emoji: Optional. Emoji associated with the sticker; only for just uploaded stickers + :type emoji: :obj:`str` + + :return: Instance of the class + :rtype: :class:`telebot.types.InputMediaSticker` + """ + def __init__(self, media: str, emoji: Optional[str] = None): + super(InputMediaSticker, self).__init__(type="sticker", media=media) + self.emoji: Optional[str] = emoji + + def to_dict(self): + json_dict = super(InputMediaSticker, self).to_dict() + if self.emoji: + json_dict['emoji'] = self.emoji + return json_dict + +class InputMediaVenue(InputMedia): + """ + Represents a venue to be sent. + + Telegram Documentation: https://core.telegram.org/bots/api#inputmediavenue + + :param latitude: Latitude of the location + :type latitude: :obj:`float` + + :param longitude: Longitude of the location + :type longitude: :obj:`float` + + :param title: Name of the venue + :type title: :obj:`str` + + :param address: Address of the venue + :type address: :obj:`str` + + :param foursquare_id: Optional. Foursquare identifier of the venue + :type foursquare_id: :obj:`str` + + :param foursquare_type: Optional. Foursquare type of the venue, if known. (For example, “arts_entertainment/default”, “arts_entertainment/aquarium” or “food/icecream”.) + :type foursquare_type: :obj:`str` + + :param google_place_id: Optional. Google Places identifier of the venue + :type google_place_id: :obj:`str` + + :param google_place_type: Optional. Google Places type of the venue. (See supported types.) + :type google_place_type: :obj:`str` + + :return: Instance of the class + :rtype: :class:`telebot.types.InputMediaVenue` + """ + def __init__(self, latitude: float, longitude: float, title: str, address: str, foursquare_id: Optional[str] = None, + foursquare_type: Optional[str] = None, google_place_id: Optional[str] = None, google_place_type: Optional[str] = None): + super(InputMediaVenue, self).__init__(type="venue", media="") + self.latitude: float = latitude + self.longitude: float = longitude + self.title: str = title + self.address: str = address + self.foursquare_id: Optional[str] = foursquare_id + self.foursquare_type: Optional[str] = foursquare_type + self.google_place_id: Optional[str] = google_place_id + self.google_place_type: Optional[str] = google_place_type + + def to_dict(self): + json_dict = super(InputMediaVenue, self).to_dict() + json_dict['latitude'] = self.latitude + json_dict['longitude'] = self.longitude + json_dict['title'] = self.title + json_dict['address'] = self.address + if self.foursquare_id: + json_dict['foursquare_id'] = self.foursquare_id + if self.foursquare_type: + json_dict['foursquare_type'] = self.foursquare_type + if self.google_place_id: + json_dict['google_place_id'] = self.google_place_id + if self.google_place_type: + json_dict['google_place_type'] = self.google_place_type + # Remove 'media' field as it's not used for venue + json_dict.pop('media', None) + return json_dict + + class PollOption(JsonDeserializable): """ This object contains information about one answer option in a poll. @@ -7453,6 +7675,9 @@ class PollOption(JsonDeserializable): :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 media: Optional. Media added to the poll option + :type media: :class:`telebot.types.PollMedia` + :param voter_count: Number of users that voted for this option :type voter_count: :obj:`int` @@ -7478,9 +7703,11 @@ def de_json(cls, json_string): 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']) + if 'media' in obj: + obj['media'] = PollMedia.de_json(obj['media']) return cls(**obj) - def __init__(self, text, persistent_id, voter_count = 0, text_entities=None, added_by_user=None, added_by_chat=None, addition_date=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, media=None, **kwargs): self.text: str = text self.persistent_id: str = persistent_id self.voter_count: int = voter_count @@ -7488,6 +7715,7 @@ def __init__(self, text, persistent_id, voter_count = 0, text_entities=None, add 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 + self.media: Optional[PollMedia] = media # Converted in _convert_poll_options # def to_json(self): # # send_poll Option is a simple string: https://core.telegram.org/bots/api#sendpoll @@ -7509,14 +7737,18 @@ class InputPollOption(JsonSerializable): :param text_entities: Optional. A JSON-serialized list of special entities that appear in the poll option text. It can be specified instead of text_parse_mode :type text_entities: :obj:`list` of :class:`telebot.types.MessageEntity` + :param media: Optional. Media added to the poll option + :type media: :class:`telebot.types.PollMedia` + :return: Instance of the class :rtype: :class:`telebot.types.PollOption` """ def __init__(self, text: str, text_parse_mode: Optional[str] = None, text_entities: Optional[List[MessageEntity]] = None, - **kwargs): + media: Optional[PollMedia] = None, **kwargs): self.text: str = text self.text_parse_mode: Optional[str] = text_parse_mode self.text_entities: Optional[List[MessageEntity]] = text_entities + self.media: Optional[PollMedia] = media def to_json(self): return json.dumps(self.to_dict()) @@ -7529,6 +7761,8 @@ def to_dict(self): json_dict["text_parse_mode"] = self.text_parse_mode if self.text_entities: json_dict['text_entities'] = [entity.to_dict() for entity in self.text_entities] + if self.media: + json_dict['media'] = self.media.to_dict() return json_dict @@ -7545,7 +7779,7 @@ class Poll(JsonDeserializable): :param question: Poll question, 1-300 characters :type question: :obj:`str` - :param options: List of poll options + :param options: A JSON-serialized list of 1-12 answer options :type options: :obj:`list` of :class:`telebot.types.PollOption` :param total_voter_count: Total number of users that voted in the poll @@ -7575,6 +7809,9 @@ class Poll(JsonDeserializable): :param explanation_entities: Optional. Special entities like usernames, URLs, bot commands, etc. that appear in the explanation :type explanation_entities: :obj:`list` of :class:`telebot.types.MessageEntity` + :param explanation_media: Optional. Media added to the quiz explanation + :type explanation_media: :class:`telebot.types.PollMedia` + :param open_period: Optional. Amount of time in seconds the poll will be active after creation :type open_period: :obj:`int` @@ -7587,12 +7824,21 @@ class Poll(JsonDeserializable): :param allows_revoting: True, if the poll allows to change the chosen answer options :type allows_revoting: :obj:`bool` + :param members_only: True if voting is limited to users who have been members of the chat where the poll was originally sent for more than 24 hours + :type members_only: :obj:`bool` + + :param country_codes: Optional. A list of two-letter ISO 3166-1 alpha-2 country codes indicating the countries from which users can vote in the poll. If omitted, then users from any country can participate in the poll. + :type country_codes: :obj:`list` of :obj:`str` + :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` + :param media: Optional. Media added to the poll description; for polls inside the Message object only + :type media: :class:`telebot.types.PollMedia` + :return: Instance of the class :rtype: :class:`telebot.types.Poll` """ @@ -7611,6 +7857,11 @@ def de_json(cls, json_string): obj['question_entities'] = Message.parse_entities(obj['question_entities']) if 'description_entities' in obj: obj['description_entities'] = Message.parse_entities(obj['description_entities']) + if 'media' in obj: + obj['media'] = PollMedia.de_json(obj['media']) + if 'explanation_media' in obj: + obj['explanation_media'] = PollMedia.de_json(obj['explanation_media']) + return cls(**obj) def __init__( @@ -7622,7 +7873,7 @@ def __init__( 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): + media: PollMedia = None, members_only: bool = None, country_codes: List[str] = None, explanation_media: PollMedia = None, **kwargs): self.id: str = poll_id self.question: str = question self.options: List[PollOption] = options @@ -7644,6 +7895,10 @@ def __init__( self.allows_revoting: bool = allows_revoting self.description: str = description self.description_entities: List[MessageEntity] = description_entities + self.media: PollMedia = media + self.explanation_media: PollMedia = explanation_media + self.members_only: bool = members_only + self.country_codes: List[str] = country_codes @property def correct_option_id(self) -> Optional[int]: @@ -9122,6 +9377,9 @@ class ExternalReplyInfo(JsonDeserializable): :param document: Optional. Message is a general file, information about the file :type document: :class:`Document` + :param live_photo: Optional. Message is a live photo, information about the live photo + :type live_photo: :class:`LivePhoto` + :param paid_media: Optional. Message is a paid media content :type paid_media: :class:`PaidMedia` @@ -9228,6 +9486,8 @@ def de_json(cls, json_string): obj['paid_media'] = PaidMediaInfo.de_json(obj['paid_media']) if 'checklist' in obj: obj['checklist'] = Checklist.de_json(obj['checklist']) + if 'live_photo' in obj: + obj['live_photo'] = LivePhoto.de_json(obj['live_photo']) return cls(**obj) def __init__( @@ -9241,7 +9501,7 @@ def __init__( giveaway_winners: Optional[GiveawayWinners]=None, invoice: Optional[Invoice]=None, location: Optional[Location]=None, poll: Optional[Poll]=None, venue: Optional[Venue]=None, paid_media: Optional[PaidMediaInfo]=None, - checklist: Optional[Checklist]=None, **kwargs) -> None: + live_photo: Optional[LivePhoto]=None, checklist: Optional[Checklist]=None, **kwargs) -> None: self.origin: MessageOrigin = origin self.chat: Optional[Chat] = chat self.message_id: Optional[int] = message_id @@ -9266,6 +9526,7 @@ def __init__( self.poll: Optional[Poll] = poll self.venue: Optional[Venue] = venue self.paid_media: Optional[PaidMediaInfo] = paid_media + self.live_photo: Optional[LivePhoto] = live_photo self.checklist: Optional[Checklist] = checklist @@ -11164,6 +11425,7 @@ class PaidMedia(JsonDeserializable, ABC): PaidMediaPreview PaidMediaPhoto PaidMediaVideo + PaidMediaLivePhoto Telegram documentation: https://core.telegram.org/bots/api#paidmedia @@ -11221,6 +11483,34 @@ def de_json(cls, json_string): return cls(**obj) +class PaidMediaLivePhoto(PaidMedia): + """ + The paid media is a live photo. + + Telegram documentation: https://core.telegram.org/bots/api#paidmedialivephoto + + :param type: Type of the paid media, always “live_photo” + :type type: :obj:`str` + + :param live_photo: The photo + :type live_photo: :class:`LivePhoto` + + :return: Instance of the class + :rtype: :class:`PaidMediaLivePhoto` + """ + + def __init__(self, type, live_photo, **kwargs): + self.type: str = type + self.live_photo: LivePhoto = live_photo + + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + obj['live_photo'] = LivePhoto.de_json(obj['live_photo']) + return cls(**obj) + + # noinspection PyShadowingBuiltins class PaidMediaPhoto(PaidMedia): """ @@ -11315,6 +11605,7 @@ class InputPaidMedia(Dictionaryable, JsonSerializable): This object describes the paid media to be sent. Currently, it can be one of InputPaidMediaPhoto InputPaidMediaVideo + InputPaidMediaLivePhoto Telegram documentation: https://core.telegram.org/bots/api#inputpaidmedia @@ -11369,6 +11660,37 @@ class InputPaidMediaPhoto(InputPaidMedia): def __init__(self, media: Union[str, InputFile], **kwargs): super().__init__(type='photo', media=media) +class InputPaidMediaLivePhoto(InputPaidMedia): + """ + The paid media to send is a live photo. + + Telegram documentation: https://core.telegram.org/bots/api#inputpaidmedialivephoto + + :param type: Type of the media, must be live_photo + :type type: :obj:`str` + + :param media: Video of the live photo to send. Pass a file_id to send a file that exists on the Telegram servers (recommended) or + pass “attach://” to upload a new one using multipart/form-data under name. More information on Sending Files ». + Sending live photos by a URL is currently unsupported. + :type media: :obj:`str` or :class:`telebot.types.InputFile` + + :param photo: The static photo to send. Pass a file_id to send a file that exists on the Telegram servers (recommended) or + pass “attach://” to upload a new one using multipart/form-data under name. More information on Sending Files ». + Sending live photos by a URL is currently unsupported. + :type photo: :obj:`str` or :class:`telebot.types.InputFile` + + :return: Instance of the class + :rtype: :class:`InputPaidMediaLivePhoto` + """ + def __init__(self, media: Union[str, InputFile], photo: Union[str, InputFile], **kwargs): + super().__init__(type='live_photo', media=media) + self.photo: Union[str, InputFile] = photo + + def to_dict(self): + data = super().to_dict() + data['photo'] = self.photo if service_utils.is_string(self.photo) else 'attach://{0}'.format(service_utils.generate_random_token()) + return data + class InputPaidMediaVideo(InputPaidMedia): """ @@ -14016,3 +14338,187 @@ def de_json(cls, json_string): 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 SentGuestMessage(JsonDeserializable): + """ + Describes an inline message sent by a guest bot. + + Telegram documentation: https://core.telegram.org/bots/api#sentguestmessage + + :param inline_message_id: Identifier of the sent inline message + :type inline_message_id: :obj:`str` + + :return: Instance of the class + :rtype: :class:`SentGuestMessage` + """ + def __init__(self, inline_message_id: str, **kwargs): + self.inline_message_id: str = inline_message_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 PollMedia(JsonDeserializable): + """ + This object describes the media attached to a poll option. At most one of the optional fields can be present in any given object. + + Telegram documentation: https://core.telegram.org/bots/api#pollmedia + + :param animation: Optional. Media is an animation, information about the animation + :type animation: :class:`Animation` + + :param audio: Optional. Media is an audio file, information about the file; currently, can't be received in a poll option + :type audio: :class:`Audio` + + :param document: Optional. Media is a general file, information about the file; currently, can't be received in a poll option + :type document: :class:`Document` + + :param live_photo: Optional. Media is a live photo, information about the live photo + :type live_photo: :class:`LivePhoto` + + :param location: Optional. Media is a shared location, information about the location + :type location: :class:`Location` + + :param photo: Optional. Media is a photo, available sizes of the photo + :type photo: :obj:`list` of :class:`PhotoSize` + + :param sticker: Optional. Media is a sticker, information about the sticker; currently, for poll options only + :type sticker: :class:`Sticker` + + :param venue: Optional. Media is a venue, information about the venue + :type venue: :class:`Venue` + + :param video: Optional. Media is a video, information about the video + :type video: :class:`Video` + + :return: Instance of the class + :rtype: :class:`PollMedia` + + """ + def __init__(self, animation: Optional[Animation] = None, audio: Optional[Audio] = None, document: Optional[Document] = None, + live_photo: Optional[LivePhoto] = None, location: Optional[Location] = None, photo: Optional[List[PhotoSize]] = None, + sticker: Optional[Sticker] = None, venue: Optional[Venue] = None, video: Optional[Video] = None, **kwargs): + self.animation: Optional[Animation] = animation + self.audio: Optional[Audio] = audio + self.document: Optional[Document] = document + self.live_photo: Optional[LivePhoto] = live_photo + self.location: Optional[Location] = location + self.photo: Optional[List[PhotoSize]] = photo + self.sticker: Optional[Sticker] = sticker + self.venue: Optional[Venue] = venue + self.video: Optional[Video] = video + + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + if 'animation' in obj: + obj['animation'] = Animation.de_json(obj['animation']) + if 'audio' in obj: + obj['audio'] = Audio.de_json(obj['audio']) + if 'document' in obj: + obj['document'] = Document.de_json(obj['document']) + if 'live_photo' in obj: + obj['live_photo'] = LivePhoto.de_json(obj['live_photo']) + if 'location' in obj: + obj['location'] = Location.de_json(obj['location']) + if 'photo' in obj: + obj['photo'] = [PhotoSize.de_json(photo) for photo in obj['photo']] + if 'sticker' in obj: + obj['sticker'] = Sticker.de_json(obj['sticker']) + if 'venue' in obj: + obj['venue'] = Venue.de_json(obj['venue']) + if 'video' in obj: + obj['video'] = Video.de_json(obj['video']) + return cls(**obj) + +# why not.. +InputPollMedia = Union[InputMediaAnimation, InputMediaAudio, InputMediaDocument, InputMediaLivePhoto, InputMediaLocation, InputMediaPhoto, InputMediaVenue, InputMediaVideo] + +InputPollOptionMedia = Union[InputMediaAnimation, InputMediaLivePhoto, InputMediaLocation, InputMediaPhoto, InputMediaSticker, InputMediaVenue, InputMediaVideo] + + +class LivePhoto(JsonDeserializable): + """ + This object represents a live photo. + + Telegram documentation: https://core.telegram.org/bots/api#livephoto + + :param photo: Optional. Available sizes of the corresponding static photo + :type photo: :obj:`list` of :class:`PhotoSize` + + :param file_id: Identifier for the video file which can be used to download or reuse the file + :type file_id: :obj:`str` + + :param file_unique_id: Unique identifier for the video file which is supposed to be the same over time and for different bots. Can't be used to download or reuse the file. + :type file_unique_id: :obj:`str` + + :param width: Video width as defined by the sender + :type width: :obj:`int` + + :param height: Video height as defined by the sender + :type height: :obj:`int` + + :param duration: Duration of the video in seconds as defined by the sender + :type duration: :obj:`int` + + :param mime_type: Optional. MIME type of the file as defined by the sender + :type mime_type: :obj:`str` + + :param file_size: Optional. File size in bytes. It can be bigger than 2^31 and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a signed 64-bit integer or double-precision float type are safe for storing this value. + :type file_size: :obj:`int` + + :return: Instance of the class + :rtype: :class:`LivePhoto` + """ + def __init__(self, file_id: str, file_unique_id: str, width: int, height: int, duration: int, photo: Optional[List[PhotoSize]] = None, + mime_type: Optional[str] = None, file_size: Optional[int] = None, **kwargs): + self.photo: Optional[List[PhotoSize]] = photo + self.file_id: str = file_id + self.file_unique_id: str = file_unique_id + self.width: int = width + self.height: int = height + self.duration: int = duration + self.mime_type: Optional[str] = mime_type + self.file_size: Optional[int] = file_size + + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + if 'photo' in obj: + obj['photo'] = [PhotoSize.de_json(photo) for photo in obj['photo']] + return cls(**obj) + + +class BotAccessSettings(JsonDeserializable): + """ + This object describes the access settings of a bot. + + Telegram documentation: https://core.telegram.org/bots/api#botaccesssettings + + :param is_access_restricted: True, if only selected users can access the bot. The bot's owner can always access it. + :type is_access_restricted: :obj:`bool` + + :param added_users: Optional. The list of other users who have access to the bot if the access is restricted + :type added_users: :obj:`list` of :class:`User` + + :return: Instance of the class + :rtype: :class:`BotAccessSettings` + """ + def __init__(self, is_access_restricted: bool, added_users: Optional[List[User]] = None, **kwargs): + self.is_access_restricted: bool = is_access_restricted + self.added_users: Optional[List[User]] = added_users + + @classmethod + def de_json(cls, json_string): + if json_string is None: return None + obj = cls.check_json(json_string) + if 'added_users' in obj: + obj['added_users'] = [User.de_json(user) for user in obj['added_users']] + return cls(**obj) + \ No newline at end of file diff --git a/telebot/util.py b/telebot/util.py index ffadfe8f5..4461929ce 100644 --- a/telebot/util.py +++ b/telebot/util.py @@ -53,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", "managed_bot", + "purchased_paid_media", "managed_bot", "guest_message" ] diff --git a/tests/test_handler_backends.py b/tests/test_handler_backends.py index 3ef28c15a..4981d9457 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, 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, 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, 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, None) def next_handler(message): diff --git a/tests/test_telebot.py b/tests/test_telebot.py index 9c7ed221f..e66a16f1f 100644 --- a/tests/test_telebot.py +++ b/tests/test_telebot.py @@ -555,7 +555,8 @@ def create_message_update(text): business_connection=None, edited_business_message=None, deleted_business_messages=None, - managed_bot=None) + managed_bot=None, + guest_message=message) def test_is_string_unicode(self): s1 = u'string'