diff --git a/compiler/docs/compiler.py b/compiler/docs/compiler.py index fa9bddab16..fb53f7c712 100644 --- a/compiler/docs/compiler.py +++ b/compiler/docs/compiler.py @@ -309,6 +309,8 @@ def get_title_list(s: str) -> list: send_checklist edit_message_checklist mark_checklist_tasks_as_done + add_poll_option + delete_poll_option """, chats=""" Chats @@ -635,6 +637,8 @@ def get_title_list(s: str) -> list: UpgradedGift WebAppData MessageAutoDeleteTimerChanged + PollOptionAdded + PollOptionDeleted ChatBoostAdded ChatBackground Game @@ -656,7 +660,7 @@ def get_title_list(s: str) -> list: ReactionTypeCustomEmoji ReactionTypePaid Thumbnail - TranslatedText + FormattedText StrippedThumbnail SponsoredMessage Sticker @@ -668,6 +672,8 @@ def get_title_list(s: str) -> list: ChatOwnerChanged ChatHasProtectedContentToggled ChatHasProtectedContentDisableRequested + ManagedBotCreated + ManagedBotUpdated """, chat_topics=""" Chat Forum Topics @@ -706,6 +712,7 @@ def get_title_list(s: str) -> list: KeyboardButtonPollTypeQuiz KeyboardButtonRequestChat KeyboardButtonRequestUsers + KeyboardButtonRequestManagedBot ReplyKeyboardMarkup ReplyKeyboardRemove LoginUrl diff --git a/compiler/errors/source/400_BAD_REQUEST.tsv b/compiler/errors/source/400_BAD_REQUEST.tsv index 1bed17d8ba..b0ad8b89c1 100644 --- a/compiler/errors/source/400_BAD_REQUEST.tsv +++ b/compiler/errors/source/400_BAD_REQUEST.tsv @@ -9,10 +9,15 @@ ADMIN_RANK_EMOJI_NOT_ALLOWED An admin rank cannot contain emojis. ADMIN_RANK_INVALID The specified admin rank is invalid. ADMIN_RIGHTS_EMPTY The chatAdminRights constructor passed in keyboardButtonRequestPeer.peer_type.user_admin_rights has no rights set (i.e. flags is 0). AD_EXPIRED The ad has expired (too old or not found). +AI_COMPOSE_TASK_MISSING The text composition style is missing. ALBUM_PHOTOS_TOO_MANY You have uploaded too many profile photos, delete some before retrying. +ANSWER_X_MEDIA_TYPE_INVALID The option media type at index {value} is invalid. +ANONYMOUS_OPEN_INVALID allow_adding_options is not supported for anonymous polls and quizzes. API_ID_INVALID API ID invalid. API_ID_PUBLISHED_FLOOD This API id was published somewhere, you can't use it now. ARTICLE_TITLE_EMPTY The title of the article is empty. +ATTACH_MEDIA_EMPTY The attached media is empty. +ATTACH_MEDIA_TYPE_INVALID The attached_media is invalid. AUDIO_CONTENT_URL_EMPTY The remote URL specified in the content field is empty. AUDIO_TITLE_EMPTY An empty audio title was provided. AUTH_BYTES_INVALID The provided authorization is invalid. @@ -26,6 +31,7 @@ BALANCE_TOO_LOW The transaction cannot be completed because the current [Telegra BANK_CARD_NUMBER_INVALID The specified card number is invalid. BANNED_RIGHTS_INVALID You provided some invalid flags in the banned rights. BASE_PORT_LOC_INVALID The base port location is invalid +BIRTHDAY_ALREADY The user has already entered a birthday. BIRTHDAY_INVALID An invalid age was specified, must be between 0 and 150 years. BOOSTS_EMPTY No boost slots were specified. BOOSTS_REQUIRED The specified channel must first be [boosted by its users](https://core.telegram.org/api/boost) in order to perform this action. @@ -154,6 +160,7 @@ DATA_JSON_INVALID The provided JSON data is invalid. DATA_TOO_LONG Data too long. DATE_EMPTY Date empty. DC_ID_INVALID The provided DC ID is invalid. +DELETE_ANSWER_FORBIDDEN You cannot delete the specified option in this poll. DH_G_A_INVALID g_a invalid. DOCUMENT_INVALID The specified document is invalid. EFFECT_ID_INVALID The specified effect ID is invalid. @@ -262,6 +269,7 @@ INLINE_RESULT_EXPIRED The inline query expired. INPUT_CHATLIST_INVALID The specified folder is invalid. INPUT_CONSTRUCTOR_INVALID The specified TL constructor is invalid. INPUT_FETCH_ERROR An error occurred while parsing the provided TL constructor. +INPUT_FETCH_ERROR_X An error occurred while parsing the provided TL {value} constructor. INPUT_FETCH_FAIL An error occurred while parsing the provided TL constructor. INPUT_FILE_INVALID The specified [InputFile](https://core.telegram.org/type/InputFile) is invalid. INPUT_FILTER_INVALID The specified filter is invalid. @@ -294,6 +302,8 @@ LASTNAME_INVALID The last name is invalid. LIMIT_INVALID The provided limit is invalid. LINK_NOT_MODIFIED Discussion link not modified. LOCATION_INVALID The provided location is invalid. +MANAGER_INVALID The provided bot manager is invalid. Bot Management Mode should be enabled for the bot in the @BotFather Mini App. +MANAGER_PERMISSION_MISSING The bot manager permission is missing. Bot Management Mode should be enabled for the bot in the @BotFather Mini App. MAX_DATE_INVALID The specified maximum date is invalid. MAX_ID_INVALID The provided max ID is invalid. MAX_QTS_INVALID The specified max_qts is invalid. @@ -487,6 +497,8 @@ SLOWMODE_MULTI_MSGS_DISABLED Slowmode is enabled, you cannot forward multiple me SLUG_INVALID The specified invoice slug is invalid. SMSJOB_ID_INVALID The specified job ID is invalid. SMS_CODE_CREATE_FAILED An error occurred while creating the SMS code. +SOLUTION_MEDIA_EMPTY The solution_media is empty. +SOLUTION_MEDIA_TYPE_INVALID The solution_media is invalid. SRP_A_INVALID The specified inputCheckPasswordSRP.A value is invalid. SRP_ID_INVALID Invalid SRP ID provided. SRP_PASSWORD_CHANGED Password has changed. @@ -618,6 +630,7 @@ USERNAME_NOT_MODIFIED The username was not modified. USERNAME_NOT_OCCUPIED The provided username is not occupied. USERNAME_OCCUPIED The provided username is already occupied. USERNAME_PURCHASE_AVAILABLE The specified username can be purchased on https://fragment.com. +USERNAME_SUFFIX_MISSING Bot username must end in `bot`. USERPIC_UPLOAD_REQUIRED You must have a profile picture to publish your geolocation. USERS_TOO_FEW Not enough users (to create a chat, for example). USERS_TOO_MUCH The maximum number of users has been exceeded (to create a chat, for example). diff --git a/compiler/errors/source/406_NOT_ACCEPTABLE.tsv b/compiler/errors/source/406_NOT_ACCEPTABLE.tsv index bacf3b6c3f..0f8457e3bc 100644 --- a/compiler/errors/source/406_NOT_ACCEPTABLE.tsv +++ b/compiler/errors/source/406_NOT_ACCEPTABLE.tsv @@ -1,4 +1,5 @@ id message +AICOMPOSE_ERROR_OCCURED The composeTextWithAi method encountered an error. ALLOW_PAYMENT_REQUIRED This peer only accepts [paid messages](https://core.telegram.org/api/paid-messages): this error is only emitted for older layers without paid messages support, so the client must be updated in order to use paid messages. . API_GIFT_RESTRICTED_UPDATE_APP Please update the app to access the gift API. AUTH_KEY_DUPLICATED Concurrent usage of the current session from multiple connections was detected, the current session was invalidated by the server for security reasons! diff --git a/docs/source/releases/changes-in-this-fork.rst b/docs/source/releases/changes-in-this-fork.rst index 94fe41b541..0f4e5b01c6 100644 --- a/docs/source/releases/changes-in-this-fork.rst +++ b/docs/source/releases/changes-in-this-fork.rst @@ -31,6 +31,26 @@ Breaking Changes in this Fork Changes in this Fork ===================== ++------------------------+ +| Scheme layer used: 224 | ++------------------------+ + +- Added the methods :meth:`~pyrogram.Client.add_poll_option` and :meth:`~pyrogram.Client.delete_poll_option`. +- Added the field ``can_manage_bots`` to the class :obj:`~pyrogram.types.User`. +- Added the class :obj:`~pyrogram.types.KeyboardButtonRequestManagedBot` and the field ``request_managed_bot`` to the class :obj:`~pyrogram.types.KeyboardButton`. +- Added the class :obj:`~pyrogram.types.ManagedBotCreated` and the field ``managed_bot_created`` to the class :obj:`~pyrogram.types.Message`. +- Added updates about the creation of managed bots and the change of their token, represented by the class :obj:`~pyrogram.handlers.ManagedBotUpdateHandler`. +- Added support for quizzes with multiple correct answers. +- Added the fields ``correct_option_ids``, ``allows_revoting``, ``description`` to the class :obj:`~pyrogram.types.Poll`. +- Added the parameters ``correct_option_ids``, ``allows_multiple_answers``, ``allows_revoting``, ``shuffle_options``, ``allow_adding_options``, ``hide_results_until_closes``, ``description`` to the method :meth:`~pyrogram.Client.send_poll`. +- Added the field ``option_persistent_ids`` to the class :obj:`~pyrogram.types.PollAnswer`. +- Added the fields ``persistent_id``, ``addition_date``, ``added_by_user`` and ``added_by_chat`` to the class :obj:`~pyrogram.types.PollOption`. +- Added the classes :obj:`~pyrogram.types.PollOptionAdded` and :obj:`~pyrogram.types.PollOptionDeleted` and the fields ``poll_option_added``, ``poll_option_deleted`` to the class :obj:`~pyrogram.types.Message`. +- Added the field ``poll_option_id`` to the class :obj:`~pyrogram.types.ReplyParameters`, allowing to reply to a specific poll option. +- Added the field ``reply_to_poll_option_id`` to the class :obj:`~pyrogram.types.Message`. +- Added the method :meth:`~pyrogram.Client.send_message_draft` (contributed by @sudo-py-dev in `#231 `__). +- View `new and changed `__ `raw API methods `__. + +------------------------+ | Scheme layer used: 223 | +------------------------+ diff --git a/pyrogram/dispatcher.py b/pyrogram/dispatcher.py index 1bd0bf4a91..36ca03b744 100644 --- a/pyrogram/dispatcher.py +++ b/pyrogram/dispatcher.py @@ -41,6 +41,7 @@ ChatJoinRequestHandler, + ManagedBotUpdateHandler, DeletedMessagesHandler, UserStatusHandler, StoryHandler, @@ -66,6 +67,7 @@ UpdateBusinessBotCallbackQuery, UpdateBotBusinessConnect, UpdateBotPurchasedPaidMedia, + UpdateManagedBot, ) log = logging.getLogger(__name__) @@ -90,6 +92,7 @@ class Dispatcher: NEW_STORY_UPDATES = (UpdateStory,) BOT_BUSINESS_CONNECT_UPDATES = (UpdateBotBusinessConnect,) PURCHASED_PAID_MEDIA_UPDATES = (UpdateBotPurchasedPaidMedia,) + MANAGED_BOT_UPDATES = (UpdateManagedBot,) def __init__(self, client: "pyrogram.Client"): self.client = client @@ -151,7 +154,7 @@ async def inline_query_parser(update, users, chats): async def poll_parser(update, users, chats): return ( - pyrogram.types.Poll._parse_update( + await pyrogram.types.Poll._parse_update( self.client, update, users, chats ), PollHandler @@ -233,6 +236,12 @@ async def purchased_paid_media_parser(update, users, chats): PurchasedPaidMediaHandler ) + async def managed_bot_update_parser(update, users, chats): + return ( + pyrogram.types.ManagedBotUpdated._parse(self.client, update, users), + ManagedBotUpdateHandler + ) + self.update_parsers = { Dispatcher.NEW_MESSAGE_UPDATES: message_parser, Dispatcher.EDIT_MESSAGE_UPDATES: edited_message_parser, @@ -252,6 +261,7 @@ async def purchased_paid_media_parser(update, users, chats): Dispatcher.NEW_STORY_UPDATES: story_parser, Dispatcher.BOT_BUSINESS_CONNECT_UPDATES: bot_business_connect_parser, Dispatcher.PURCHASED_PAID_MEDIA_UPDATES: purchased_paid_media_parser, + Dispatcher.MANAGED_BOT_UPDATES: managed_bot_update_parser, } self.update_parsers = {key: value for key_tuple, value in self.update_parsers.items() for key in key_tuple} diff --git a/pyrogram/enums/message_entity_type.py b/pyrogram/enums/message_entity_type.py index 86ee8633b4..57a44e9f69 100644 --- a/pyrogram/enums/message_entity_type.py +++ b/pyrogram/enums/message_entity_type.py @@ -89,5 +89,14 @@ class MessageEntityType(AutoName): DATE_TIME = raw.types.MessageEntityFormattedDate "for formatted date and time (see ``unix_time`` and ``date_time_format``)" + DIFF_TYPE_INSERT = raw.types.MessageEntityDiffInsert + "Represents a change of a text: Addition of some text" + + DIFF_TYPE_REPLACE = raw.types.MessageEntityDiffReplace + "Represents a change of a text: Change of some text" + + DIFF_TYPE_DELETE = raw.types.MessageEntityDiffDelete + "Represents a change of a text: Removal of some text" + UNKNOWN = raw.types.MessageEntityUnknown "Unknown message entity type" diff --git a/pyrogram/enums/message_service_type.py b/pyrogram/enums/message_service_type.py index d69332be00..133291efd2 100644 --- a/pyrogram/enums/message_service_type.py +++ b/pyrogram/enums/message_service_type.py @@ -168,5 +168,14 @@ class MessageServiceType(AutoName): CHAT_HAS_PROTECTED_CONTENT_DISABLE_REQUESTED = auto() "Chat has_protected_content setting was requested to be disabled" + MANAGED_BOT_CREATED = auto() + "A bot managed by another bot was created by the user" + + POLL_OPTION_ADDED = auto() + "An option was added to a poll" + + POLL_OPTION_DELETED = auto() + "A message with information about a deleted poll option" + UNKNOWN = auto() "This service message is unsupported by the current version of Pyrogram" diff --git a/pyrogram/handlers/__init__.py b/pyrogram/handlers/__init__.py index b5af6f1bad..a4140fbda0 100644 --- a/pyrogram/handlers/__init__.py +++ b/pyrogram/handlers/__init__.py @@ -35,3 +35,4 @@ from .purchased_paid_media_handler import PurchasedPaidMediaHandler from .shipping_query_handler import ShippingQueryHandler from .story_handler import StoryHandler +from .managed_bot_update_handler import ManagedBotUpdateHandler diff --git a/pyrogram/handlers/managed_bot_update_handler.py b/pyrogram/handlers/managed_bot_update_handler.py new file mode 100644 index 0000000000..29302f7221 --- /dev/null +++ b/pyrogram/handlers/managed_bot_update_handler.py @@ -0,0 +1,62 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from typing import Any, Callable + +import pyrogram +from pyrogram.filters import Filter +from .handler import Handler + +CallbackFunc: Callable = Callable[ + [ + "pyrogram.Client", + pyrogram.types.ManagedBotUpdated + ], + Any +] + + +class ManagedBotUpdateHandler(Handler): + """The ManagedBotUpdate handler class. + Used to handle new managed bot creation updates. + + It is intended to be used with :meth:`~pyrogram.Client.add_handler`. + + For a nicer way to register this handler, have a look at the + :meth:`~pyrogram.Client.on_managed_bot` decorator. + + Parameters: + callback (``Callable``): + Pass a function that will be called when a new ManagedBotUpdated event arrives. It takes + *(client, managed_bot)* as positional arguments (look at the section below for a detailed + description). + + filters (:obj:`Filters`): + Pass one or more filters to allow only a subset of updates to be passed in your callback function. + + Other parameters: + client (:obj:`~pyrogram.Client`): + The Client itself, useful when you want to call other API methods inside the handler. + + managed_bot (:obj:`~pyrogram.types.ManagedBotUpdated`): + A new bot was created to be managed by the bot or token of a bot was changed. + + """ + + def __init__(self, callback: CallbackFunc, filters: Filter = None): + super().__init__(callback, filters) diff --git a/pyrogram/methods/decorators/__init__.py b/pyrogram/methods/decorators/__init__.py index 9759a20aef..1759bc2d40 100644 --- a/pyrogram/methods/decorators/__init__.py +++ b/pyrogram/methods/decorators/__init__.py @@ -35,6 +35,7 @@ from .on_pre_checkout_query import OnPreCheckoutQuery from .on_shipping_query import OnShippingQuery from .on_story import OnStory +from .on_managed_bot import OnManagedBot class Decorators( @@ -59,6 +60,7 @@ class Decorators( OnDisconnect, OnUserStatus, OnStory, + OnManagedBot, OnRawUpdate, ): pass diff --git a/pyrogram/methods/decorators/on_managed_bot.py b/pyrogram/methods/decorators/on_managed_bot.py new file mode 100644 index 0000000000..048b343ef0 --- /dev/null +++ b/pyrogram/methods/decorators/on_managed_bot.py @@ -0,0 +1,64 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from typing import Callable + +import pyrogram +from pyrogram.filters import Filter + + +class OnManagedBot: + def on_managed_bot( + self=None, + filters=None, + group: int = 0 + ) -> Callable: + """Decorator for handling new managed bot creation updates. + + This does the same thing as :meth:`~pyrogram.Client.add_handler` using the + :obj:`~pyrogram.handlers.ManagedBotUpdateHandler`. + + .. include:: /_includes/usable-by/bots.rst + + Parameters: + filters (:obj:`~pyrogram.filters`, *optional*): + Pass one or more filters to allow only a subset of callback queries to be passed + in your function. + + group (``int``, *optional*): + The group identifier, defaults to 0. + + """ + + def decorator(func: Callable) -> Callable: + if isinstance(self, pyrogram.Client): + self.add_handler(pyrogram.handlers.ManagedBotUpdateHandler(func, filters), group) + elif isinstance(self, Filter) or self is None: + if not hasattr(func, "handlers"): + func.handlers = [] + + func.handlers.append( + ( + pyrogram.handlers.ManagedBotUpdateHandler(func, self), + group if filters is None else filters + ) + ) + + return func + + return decorator diff --git a/pyrogram/methods/messages/__init__.py b/pyrogram/methods/messages/__init__.py index a9961350d5..0c44579569 100644 --- a/pyrogram/methods/messages/__init__.py +++ b/pyrogram/methods/messages/__init__.py @@ -77,6 +77,8 @@ from .send_checklist import SendChecklist from .edit_message_checklist import EditMessageChecklist from .mark_checklist_tasks_as_done import MarkChecklistTasksAsDone +from .add_poll_option import AddPollOption +from .delete_poll_option import DeletePollOption class Messages( CopyMediaGroup, @@ -140,5 +142,7 @@ class Messages( SendChecklist, EditMessageChecklist, MarkChecklistTasksAsDone, + AddPollOption, + DeletePollOption ): pass diff --git a/pyrogram/methods/messages/add_poll_option.py b/pyrogram/methods/messages/add_poll_option.py new file mode 100644 index 0000000000..4dc9926654 --- /dev/null +++ b/pyrogram/methods/messages/add_poll_option.py @@ -0,0 +1,84 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from typing import Union + +import pyrogram +from pyrogram import raw, types + + +class AddPollOption: + async def add_poll_option( + self: "pyrogram.Client", + chat_id: Union[int, str], + message_id: int, + option: "types.InputPollOption", + ) -> Union["types.Message", bool]: + """Adds an option to a poll. + + .. include:: /_includes/usable-by/users.rst + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + For your personal cloud (Saved Messages) you can simply use "me" or "self". + For a contact that exists in your Telegram address book you can use his phone number (str). + + message_id (``int``): + Identifier of the message containing the poll. + + option (:obj:`~pyrogram.types.InputPollOption`): + The new option. + + Returns: + :obj:`~pyrogram.types.Message` | ``bool``: On success, an edited message or a service message will be returned (when applicable), + otherwise, in case a message object couldn't be returned, True is returned. + + Example: + .. code-block:: python + + await app.add_poll_option( + chat_id, + message_id, + option="Seoul" + ) + + """ + if isinstance(option, str): + option = types.InputPollOption( + text=types.FormattedText( + text=option + ) + ) + r = await self.invoke( + raw.functions.messages.AddPollAnswer( + peer=await self.resolve_peer(chat_id), + msg_id=message_id, + answer=await option.write(self) + ) + ) + for i in r.updates: + if isinstance(i, (raw.types.UpdateNewMessage, raw.types.UpdateEditChannelMessage, raw.types.UpdateNewChannelMessage)): + return await types.Message._parse( + self, + i.message, + {i.id: i for i in r.users}, + {i.id: i for i in r.chats}, + replies=self.fetch_replies + ) + return True diff --git a/pyrogram/methods/messages/delete_poll_option.py b/pyrogram/methods/messages/delete_poll_option.py new file mode 100644 index 0000000000..aed989ae02 --- /dev/null +++ b/pyrogram/methods/messages/delete_poll_option.py @@ -0,0 +1,78 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from typing import Union + +import pyrogram +from pyrogram import raw, types + + +class DeletePollOption: + async def delete_poll_option( + self: "pyrogram.Client", + chat_id: Union[int, str], + message_id: int, + option_id: str, + ) -> Union["types.Message", bool]: + """Deletes an option from a poll. + + .. include:: /_includes/usable-by/users.rst + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + For your personal cloud (Saved Messages) you can simply use "me" or "self". + For a contact that exists in your Telegram address book you can use his phone number (str). + + message_id (``int``): + Identifier of the message containing the checklist. + + option_id (``str``): + Unique identifier of the option. + + Returns: + :obj:`~pyrogram.types.Message` | ``bool``: On success, an edited message or a service message will be returned (when applicable), + otherwise, in case a message object couldn't be returned, True is returned. + + Example: + .. code-block:: python + + await app.delete_poll_option( + chat_id, + message_id, + option_id="0" + ) + + """ + r = await self.invoke( + raw.functions.messages.DeletePollAnswer( + peer=await self.resolve_peer(chat_id), + msg_id=message_id, + option=option_id.encode("UTF-8"), + ) + ) + for i in r.updates: + if isinstance(i, (raw.types.UpdateNewMessage, raw.types.UpdateEditChannelMessage, raw.types.UpdateNewChannelMessage)): + return await types.Message._parse( + self, + i.message, + {i.id: i for i in r.users}, + {i.id: i for i in r.chats}, + replies=self.fetch_replies + ) + return True diff --git a/pyrogram/methods/messages/retract_vote.py b/pyrogram/methods/messages/retract_vote.py index c456f4d08b..ed7335e482 100644 --- a/pyrogram/methods/messages/retract_vote.py +++ b/pyrogram/methods/messages/retract_vote.py @@ -58,4 +58,18 @@ async def retract_vote( ) ) - return types.Poll._parse(self, r.updates[0]) + for i in r.updates: + if isinstance( + i, + ( + raw.types.MessageMediaPoll, + raw.types.UpdateMessagePoll + ) + ): + return await types.Poll._parse( + self, + i, + {i.id: i for i in r.users}, + {i.id: i for i in r.chats}, + ) + diff --git a/pyrogram/methods/messages/send_message.py b/pyrogram/methods/messages/send_message.py index 871edc95b7..df0f1adf3f 100644 --- a/pyrogram/methods/messages/send_message.py +++ b/pyrogram/methods/messages/send_message.py @@ -347,8 +347,8 @@ async def send_message( for entity in r.entities ] if r.entities else None, message_auto_delete_timer_changed=types.MessageAutoDeleteTimerChanged( - message_auto_delete_time=getattr(r, "ttl_period", None) - ), + message_auto_delete_time=r.ttl_period + ) if r.ttl_period else None, chat=types.Chat( id=peer_id, type=enums.ChatType.PRIVATE, diff --git a/pyrogram/methods/messages/send_poll.py b/pyrogram/methods/messages/send_poll.py index 32978779cf..97f480bbf0 100644 --- a/pyrogram/methods/messages/send_poll.py +++ b/pyrogram/methods/messages/send_poll.py @@ -22,6 +22,7 @@ import pyrogram from pyrogram import raw, utils, types, enums +from pyrogram.file_id import FileType log = logging.getLogger(__name__) @@ -30,20 +31,21 @@ class SendPoll: async def send_poll( self: "pyrogram.Client", chat_id: Union[int, str], - question: str, + question: "types.FormattedText", options: list["types.InputPollOption"], - question_parse_mode: "enums.ParseMode" = None, - question_entities: list["types.MessageEntity"] = None, is_anonymous: bool = True, type: "enums.PollType" = enums.PollType.REGULAR, allows_multiple_answers: bool = None, - correct_option_id: int = None, - explanation: str = None, - explanation_parse_mode: "enums.ParseMode" = None, - explanation_entities: list["types.MessageEntity"] = None, + allows_revoting: bool = None, + shuffle_options: bool = None, + allow_adding_options: bool = None, + hide_results_until_closes: bool = None, + correct_option_ids: list[int] = None, + explanation: "types.FormattedText" = None, open_period: int = None, close_date: datetime = None, is_closed: bool = None, + description: "types.FormattedText" = None, disable_notification: bool = None, protect_content: bool = None, allow_paid_broadcast: bool = None, @@ -60,9 +62,28 @@ async def send_poll( "types.ReplyKeyboardRemove", "types.ForceReply" ] = None, - reply_to_message_id: int = None + attached_media_animation: str = None, + attached_media_audio: str = None, + attached_media_document: str = None, + # messageLocation + attached_media_photo: str = None, + attached_media_sticker: str = None, + # messageVenue + attached_media_video: str = None, + attached_media_video_note: str = None, + attached_media_voice: str = None, + solution_media_animation: str = None, + solution_media_audio: str = None, + solution_media_document: str = None, + # messageLocation + solution_media_photo: str = None, + solution_media_sticker: str = None, + # messageVenue + solution_media_video: str = None, + solution_media_video_note: str = None, + solution_media_voice: str = None, ) -> "types.Message": - """Send a new poll. + """Send a native poll. .. include:: /_includes/usable-by/users-bots.rst @@ -73,7 +94,7 @@ async def send_poll( For your personal cloud (Saved Messages) you can simply use "me" or "self". For a contact that exists in your Telegram address book you can use his phone number (str). - question (``str``): + question (:obj:`~pyrogram.types.FormattedText`): Poll question. **Users**: 1-255 characters. **Bots**: 1-300 characters. @@ -81,13 +102,6 @@ async def send_poll( options (List of :obj:`~pyrogram.types.InputPollOption`): List of 2-12 poll answer options. - question_parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): - By default, texts are parsed using both Markdown and HTML styles. - You can combine both syntaxes together. - - question_entities (List of :obj:`~pyrogram.types.MessageEntity`): - List of special entities that appear in the poll question, which can be specified instead of *question_parse_mode*. - is_anonymous (``bool``, *optional*): True, if the poll needs to be anonymous. Defaults to True. @@ -97,37 +111,44 @@ async def send_poll( Defaults to :obj:`~pyrogram.enums.PollType.REGULAR`. allows_multiple_answers (``bool``, *optional*): - True, if the poll allows multiple answers, ignored for polls in quiz mode. + True, if the poll allows multiple answers. Defaults to False. - correct_option_id (``int``, *optional*): - 0-based identifier of the correct answer option, required for polls in quiz mode. + allows_revoting (``bool``, *optional*): + Pass True, if the poll allows to change chosen answer options, defaults to False for quizzes and to True for regular polls. - explanation (``str``, *optional*): - Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style - poll, 0-200 characters with at most 2 line feeds after entities parsing. + shuffle_options (``bool``, *optional*): + Pass True, if the poll options must be shown in random order. - explanation_parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): - By default, texts are parsed using both Markdown and HTML styles. - You can combine both syntaxes together. + allow_adding_options (``bool``, *optional*): + Pass True, if answer options can be added to the poll after creation; not supported for anonymous polls and quizzes. - explanation_entities (List of :obj:`~pyrogram.types.MessageEntity`): - List of special entities that appear in the poll explanation, which can be specified instead of - *explanation_parse_mode*. + hide_results_until_closes (``bool``, *optional*): + Pass True, if poll results must be shown only after the poll closes. + + correct_option_ids (List of ``int``, *optional*): + List of monotonically increasing 0-based identifiers of the correct answer options, required for polls in quiz mode. + + explanation (:obj:`~pyrogram.types.FormattedText`, *optional*): + Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style + poll, 0-200 characters with at most 2 line feeds after entities parsing. open_period (``int``, *optional*): - Amount of time in seconds the poll will be active after creation, 5-600. + Amount of time in seconds the poll will be active after creation, 5-2628000. Can't be used together with *close_date*. close_date (:py:obj:`~datetime.datetime`, *optional*): Point in time when the poll will be automatically closed. - Must be at least 5 and no more than 600 seconds in the future. + Must be at least 5 and no more than 2628000 seconds in the future. Can't be used together with *open_period*. is_closed (``bool``, *optional*): Pass True, if the poll needs to be immediately closed. This can be useful for poll preview. + description (:obj:`~pyrogram.types.FormattedText`, *optional*): + Description of the poll to be sent, 0-1024 characters after entities parsing. + disable_notification (``bool``, *optional*): Sends the message silently. Users will receive a notification with no sound. @@ -173,35 +194,54 @@ async def send_poll( Example: .. code-block:: python - from pyrogram.types import InputPollOption + from pyrogram import types await app.send_poll( chat_id=chat_id, - question="Is this a poll question?", + question=types.FormattedText( + text="Is this a poll question?" + ), options=[ - InputPollOption(text="Yes"), - InputPollOption(text="No"), - InputPollOption(text= "Maybe"), + types.InputPollOption( + text=types.FormattedText( + text="Yes" + ) + ), + types.InputPollOption( + text=types.FormattedText( + text="No" + ), + sticker="CAACAgIAAxkBAAENQDJp0TeBK91YmUIY_eGLV_OpyDwI2gACRRkAAt1kkUkycGu4PBSkQTsE" + ), + types.InputPollOption( + text=types.FormattedText( + text="Maybe" + ) + ), ] ) """ + if isinstance(question, str): + question = types.FormattedText(text=question) - if reply_to_message_id and reply_parameters: - raise ValueError( - "Parameters `reply_to_message_id` and `reply_parameters` are mutually " - "exclusive." - ) - - if reply_to_message_id is not None: - log.warning( - "This property is deprecated. " - "Please use reply_parameters instead" - ) - reply_parameters = types.ReplyParameters(message_id=reply_to_message_id) + if isinstance(explanation, str): + explanation = types.FormattedText(text=explanation) + + if isinstance(description, str): + description = types.FormattedText(text=description) - solution, solution_entities = (await utils.parse_text_entities( - self, explanation, explanation_parse_mode, explanation_entities - )).values() + answers = [] + for i, answer_ in enumerate(options): + if isinstance(answer_, str): + answer_ = types.InputPollOption( + text=types.FormattedText( + text=answer_ + ) + ) + answers.append(await answer_.write(self)) + + raw_description = await description.write(self, None) if description else None + solution = await explanation.write(self) if explanation else None reply_to = await utils._get_reply_message_parameters( self, @@ -209,50 +249,77 @@ async def send_poll( reply_parameters ) - question, question_entities = (await utils.parse_text_entities(self, question, question_parse_mode, question_entities)).values() - if not question_entities: - question_entities = [] - - answers = [] - for i, answer_ in enumerate(options): - if isinstance(answer_, str): - answer, answer_entities = answer_, [] - else: - answer, answer_entities = (await utils.parse_text_entities(self, answer_.text, answer_.text_parse_mode, answer_.text_entities)).values() - if not answer_entities: - answer_entities = [] - answers.append( - raw.types.PollAnswer( - text=raw.types.TextWithEntities( - text=answer, - entities=answer_entities - ), - option=bytes([i]) - ) - ) + if type == enums.PollType.QUIZ and allow_adding_options: + allow_adding_options = False + + if type == enums.PollType.QUIZ and len(correct_option_ids) > 1 and not allows_multiple_answers: + allows_multiple_answers = True + + attached_media = None + solution_media = None + + if attached_media_animation: + attached_media = utils.get_input_media_from_file_id(attached_media_animation, FileType.ANIMATION) + elif attached_media_audio: + attached_media = utils.get_input_media_from_file_id(attached_media_audio, FileType.AUDIO) + elif attached_media_document: + attached_media = utils.get_input_media_from_file_id(attached_media_document, FileType.DOCUMENT) + elif attached_media_photo: + attached_media = utils.get_input_media_from_file_id(attached_media_photo, FileType.PHOTO) + elif attached_media_sticker: + attached_media = utils.get_input_media_from_file_id(attached_media_sticker, FileType.STICKER) + elif attached_media_video: + attached_media = utils.get_input_media_from_file_id(attached_media_video, FileType.VIDEO) + elif attached_media_video_note: + attached_media = utils.get_input_media_from_file_id(attached_media_video_note, FileType.VIDEO_NOTE) + elif attached_media_voice: + attached_media = utils.get_input_media_from_file_id(attached_media_voice, FileType.VOICE) + + if solution_media_animation: + solution_media = utils.get_input_media_from_file_id(solution_media_animation, FileType.ANIMATION) + elif solution_media_audio: + solution_media = utils.get_input_media_from_file_id(solution_media_audio, FileType.AUDIO) + elif solution_media_document: + solution_media = utils.get_input_media_from_file_id(solution_media_document, FileType.DOCUMENT) + elif solution_media_photo: + solution_media = utils.get_input_media_from_file_id(solution_media_photo, FileType.PHOTO) + elif solution_media_sticker: + solution_media = utils.get_input_media_from_file_id(solution_media_sticker, FileType.STICKER) + elif solution_media_video: + solution_media = utils.get_input_media_from_file_id(solution_media_video, FileType.VIDEO) + elif solution_media_video_note: + solution_media = utils.get_input_media_from_file_id(solution_media_video_note, FileType.VIDEO_NOTE) + elif solution_media_voice: + solution_media = utils.get_input_media_from_file_id(solution_media_voice, FileType.VOICE) rpc = raw.functions.messages.SendMedia( peer=await self.resolve_peer(chat_id), media=raw.types.InputMediaPoll( poll=raw.types.Poll( id=self.rnd_id(), - question=raw.types.TextWithEntities( - text=question, - entities=question_entities - ), + hash=0, + question=await question.write(self), answers=answers, closed=is_closed, public_voters=not is_anonymous, multiple_choice=allows_multiple_answers, quiz=type == enums.PollType.QUIZ or False, close_period=open_period, - close_date=utils.datetime_to_timestamp(close_date) + close_date=utils.datetime_to_timestamp(close_date), + open_answers=allow_adding_options, + revoting_disabled=not allows_revoting, + shuffle_answers=shuffle_options, + hide_results_until_close=hide_results_until_closes, + # creator:flags.10?true ), - correct_answers=[bytes([correct_option_id])] if correct_option_id is not None else None, - solution=solution, - solution_entities=solution_entities or [] + correct_answers=correct_option_ids or None, + solution=solution.text if solution else None, + solution_entities=solution.entities if solution else None, + solution_media=solution_media, + attached_media=attached_media, ), - message="", + message=raw_description.text if raw_description else "", + entities=raw_description.entities if raw_description else None, silent=disable_notification, reply_to=reply_to, random_id=self.rnd_id(), diff --git a/pyrogram/methods/messages/stop_poll.py b/pyrogram/methods/messages/stop_poll.py index 9b5cd533ca..84d4cdf7ed 100644 --- a/pyrogram/methods/messages/stop_poll.py +++ b/pyrogram/methods/messages/stop_poll.py @@ -64,14 +64,15 @@ async def stop_poll( poll = (await self.get_messages( chat_id=chat_id, message_ids=message_id - )).poll + ))._raw.media.poll # TODO rpc = raw.functions.messages.EditMessage( peer=await self.resolve_peer(chat_id), id=message_id, media=raw.types.InputMediaPoll( poll=raw.types.Poll( - id=int(poll.id), + id=poll.id, + hash=poll.hash, closed=True, question=raw.types.TextWithEntities(text="", entities=[]), answers=[] @@ -99,5 +100,18 @@ async def stop_poll( # await session.stop() else: r = await self.invoke(rpc) + for i in r.updates: + if isinstance( + i, + ( + raw.types.MessageMediaPoll, + raw.types.UpdateMessagePoll + ) + ): + return await types.Poll._parse( + self, + i, + {i.id: i for i in r.users}, + {i.id: i for i in r.chats}, + ) - return types.Poll._parse(self, r.updates[0]) diff --git a/pyrogram/methods/messages/translate_text.py b/pyrogram/methods/messages/translate_text.py index 2f05035d33..7a91efee83 100644 --- a/pyrogram/methods/messages/translate_text.py +++ b/pyrogram/methods/messages/translate_text.py @@ -27,8 +27,9 @@ async def translate_message_text( self: "pyrogram.Client", to_language_code: str, chat_id: Union[int, str], - message_ids: Union[int, list[int]] - ) -> Union["types.TranslatedText", list["types.TranslatedText"]]: + message_ids: Union[int, list[int]], + tone: str = "", + ) -> Union["types.FormattedText", list["types.FormattedText"]]: """Extracts text or caption of the given message and translates it to the given language. If the current user is a Telegram Premium user, then text formatting is preserved. .. include:: /_includes/usable-by/users.rst @@ -44,8 +45,11 @@ async def translate_message_text( message_ids (``int`` | List of ``int``): Identifier or list of message identifiers of the target message. + tone (``str``, *optional*): + Tone of the translation. + Returns: - :obj:`~pyrogram.types.TranslatedText` | List of :obj:`~pyrogram.types.TranslatedText`: In case *message_ids* was not + :obj:`~pyrogram.types.FormattedText` | List of :obj:`~pyrogram.types.FormattedText`: In case *message_ids* was not a list, a single result is returned, otherwise a list of results is returned. Example: @@ -59,15 +63,16 @@ async def translate_message_text( raw.functions.messages.TranslateText( to_lang=to_language_code, peer=await self.resolve_peer(chat_id), - id=ids + id=ids, + tone=tone, ) ) return ( - types.TranslatedText._parse(self, r.result[0]) + types.FormattedText._parse(self, r.result[0]) if len(r.result) == 1 else [ - types.TranslatedText._parse(self, i) + types.FormattedText._parse(self, i) for i in r.result ] ) @@ -76,10 +81,9 @@ async def translate_message_text( async def translate_text( self: "pyrogram.Client", to_language_code: str, - text: str, - parse_mode: Optional["enums.ParseMode"] = None, - entities: list["types.MessageEntity"] = None - ) -> Union["types.TranslatedText", list["types.TranslatedText"]]: + text: "types.FormattedText", + tone: str = "", + ) -> Union["types.FormattedText", list["types.FormattedText"]]: """Translates a text to the given language. If the current user is a Telegram Premium user, then text formatting is preserved. .. include:: /_includes/usable-by/users.rst @@ -89,18 +93,14 @@ async def translate_text( Language code of the language to which the message is translated. Must be one of "af", "sq", "am", "ar", "hy", "az", "eu", "be", "bn", "bs", "bg", "ca", "ceb", "zh-CN", "zh", "zh-Hans", "zh-TW", "zh-Hant", "co", "hr", "cs", "da", "nl", "en", "eo", "et", "fi", "fr", "fy", "gl", "ka", "de", "el", "gu", "ht", "ha", "haw", "he", "iw", "hi", "hmn", "hu", "is", "ig", "id", "in", "ga", "it", "ja", "jv", "kn", "kk", "km", "rw", "ko", "ku", "ky", "lo", "la", "lv", "lt", "lb", "mk", "mg", "ms", "ml", "mt", "mi", "mr", "mn", "my", "ne", "no", "ny", "or", "ps", "fa", "pl", "pt", "pa", "ro", "ru", "sm", "gd", "sr", "st", "sn", "sd", "si", "sk", "sl", "so", "es", "su", "sw", "sv", "tl", "tg", "ta", "tt", "te", "th", "tr", "tk", "uk", "ur", "ug", "uz", "vi", "cy", "xh", "yi", "ji", "yo", "zu". - text (``str``): + text (:obj:`~pyrogram.types.FormattedText`): Text to translate. - parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): - By default, texts are parsed using both Markdown and HTML styles. - You can combine both syntaxes together. - - entities (List of :obj:`~pyrogram.types.MessageEntity`): - List of special entities that appear in message text, which can be specified instead of *parse_mode*. + tone (``str``, *optional*): + Tone of the translation. Returns: - :obj:`~pyrogram.types.TranslatedText` | List of :obj:`~pyrogram.types.TranslatedText`: In case *message_ids* was not + :obj:`~pyrogram.types.FormattedText` | List of :obj:`~pyrogram.types.FormattedText`: In case *message_ids* was not a list, a single result is returned, otherwise a list of results is returned. Example: @@ -108,32 +108,21 @@ async def translate_text( await app.translate_text("fa", "Pyrogram") """ - message, entities = ( - await utils.parse_text_entities( - self, - text, - parse_mode, - entities - ) - ).values() - + if isinstance(text, str): + text = types.FormattedText(text=text) r = await self.invoke( raw.functions.messages.TranslateText( to_lang=to_language_code, - text=[ - raw.types.TextWithEntities( - text=message, - entities=entities or [] - ) - ] + text=[await text.write(self)], + tone=tone, ) ) return ( - types.TranslatedText._parse(self, r.result[0]) + types.FormattedText._parse(self, r.result[0]) if len(r.result) == 1 else [ - types.TranslatedText._parse(self, i) + types.FormattedText._parse(self, i) for i in r.result ] ) diff --git a/pyrogram/methods/messages/vote_poll.py b/pyrogram/methods/messages/vote_poll.py index 756fa88298..41dda3e26d 100644 --- a/pyrogram/methods/messages/vote_poll.py +++ b/pyrogram/methods/messages/vote_poll.py @@ -60,7 +60,6 @@ async def vote_poll( message_ids=message_id )).poll options = [options] if not isinstance(options, list) else options - r = await self.invoke( raw.functions.messages.SendVote( peer=await self.resolve_peer(chat_id), @@ -68,5 +67,17 @@ async def vote_poll( options=[poll.options[option].data for option in options] ) ) - - return types.Poll._parse(self, r.updates[0]) + for i in r.updates: + if isinstance( + i, + ( + raw.types.MessageMediaPoll, + raw.types.UpdateMessagePoll + ) + ): + return await types.Poll._parse( + self, + i, + {i.id: i for i in r.users}, + {i.id: i for i in r.chats}, + ) diff --git a/pyrogram/types/bots_and_keyboards/__init__.py b/pyrogram/types/bots_and_keyboards/__init__.py index f6d0228a6b..e25465767d 100644 --- a/pyrogram/types/bots_and_keyboards/__init__.py +++ b/pyrogram/types/bots_and_keyboards/__init__.py @@ -40,6 +40,7 @@ ) from .keyboard_button_request_chat import KeyboardButtonRequestChat from .keyboard_button_request_users import KeyboardButtonRequestUsers +from .keyboard_button_request_managed_bot import KeyboardButtonRequestManagedBot from .login_url import LoginUrl from .menu_button import MenuButton from .menu_button_commands import MenuButtonCommands @@ -50,7 +51,8 @@ from .sent_web_app_message import SentWebAppMessage from .switch_inline_query_chosen_chat import SwitchInlineQueryChosenChat from .web_app_info import WebAppInfo - +from .managed_bot_created import ManagedBotCreated +from .managed_bot_updated import ManagedBotUpdated __all__ = [ "CallbackGame", @@ -66,6 +68,7 @@ "KeyboardButtonPollTypeQuiz", "KeyboardButtonRequestChat", "KeyboardButtonRequestUsers", + "KeyboardButtonRequestManagedBot", "ReplyKeyboardMarkup", "ReplyKeyboardRemove", "LoginUrl", @@ -85,4 +88,6 @@ "MenuButtonDefault", "SentWebAppMessage", "SwitchInlineQueryChosenChat", + "ManagedBotCreated", + "ManagedBotUpdated", ] diff --git a/pyrogram/types/bots_and_keyboards/keyboard_button.py b/pyrogram/types/bots_and_keyboards/keyboard_button.py index 787914610e..32db6806ef 100644 --- a/pyrogram/types/bots_and_keyboards/keyboard_button.py +++ b/pyrogram/types/bots_and_keyboards/keyboard_button.py @@ -49,6 +49,11 @@ class KeyboardButton(Object): If specified, pressing the button will open a list of suitable chats. Tapping on a chat will send its identifier to the bot in a “chat_shared” service message. Available in private chats only. + request_managed_bot (:obj:`~pyrogram.types.KeyboardButtonRequestManagedBot`, *optional*): + If specified, pressing the button will ask the user to create and share a bot that will be managed by the current bot. + Available for bots that enabled management of other bots in the @BotFather Mini App. + Available in private chats only. + request_contact (``bool``, *optional*): If True, the user's phone number will be sent as a contact when the button is pressed. Available in private chats only. @@ -77,6 +82,7 @@ def __init__( web_app: "types.WebAppInfo" = None, request_users: "types.KeyboardButtonRequestUsers" = None, request_chat: "types.KeyboardButtonRequestChat" = None, + request_managed_bot: "types.KeyboardButtonRequestManagedBot" = None, icon_custom_emoji_id: Optional[int] = None, style: "enums.ButtonStyle" = enums.ButtonStyle.DEFAULT ): @@ -89,6 +95,7 @@ def __init__( self.web_app = web_app self.request_users = request_users self.request_chat = request_chat + self.request_managed_bot = request_managed_bot self.icon_custom_emoji_id = icon_custom_emoji_id self.style = style @@ -212,6 +219,18 @@ def read(b): icon_custom_emoji_id=icon_custom_emoji_id ) + if isinstance(b.peer_type, raw.types.RequestPeerTypeCreateBot): + return KeyboardButton( + text=b.text, + style=button_style, + icon_custom_emoji_id=icon_custom_emoji_id, + request_managed_bot=types.KeyboardButtonRequestManagedBot( + request_id=b.button_id, + suggested_name=b.peer_type.suggested_name, + suggested_username=b.peer_type.suggested_username, + ) + ) + def write(self): if isinstance(self, str): return raw.types.KeyboardButton( @@ -299,6 +318,18 @@ def write(self): max_quantity=1, style=raw_style ) + elif self.request_managed_bot: + return raw.types.InputKeyboardButtonRequestPeer( + max_quantity=1, + style=raw_style, + text=self.text, + button_id=self.request_managed_bot.request_id, + peer_type=raw.types.RequestPeerTypeCreateBot( + bot_managed=True, + suggested_name=self.request_managed_bot.suggested_name, + suggested_username=self.request_managed_bot.suggested_username, + ), + ) else: return raw.types.KeyboardButton( text=self.text, diff --git a/pyrogram/types/bots_and_keyboards/keyboard_button_request_managed_bot.py b/pyrogram/types/bots_and_keyboards/keyboard_button_request_managed_bot.py new file mode 100644 index 0000000000..27bfc8dfc3 --- /dev/null +++ b/pyrogram/types/bots_and_keyboards/keyboard_button_request_managed_bot.py @@ -0,0 +1,46 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-2021 Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from pyrogram import types +from ..object import Object + + +class KeyboardButtonRequestManagedBot(Object): + """This object defines the parameters for the creation of a managed bot. + Information about the created bot will be shared with the bot using the update managed_bot and a Message with the field ``managed_bot_created``. + + Parameters: + request_id (``int``): + Signed 32-bit identifier of the request. Must be unique within the message + + suggested_name (``str``, *optional*): + Suggested name for the bot. + + suggested_username (``str``, *optional*): + Suggested username for the bot. + + """ + def __init__( + self, + request_id: int, + suggested_name: str = None, + suggested_username: str = None, + ): + self.request_id = request_id + self.suggested_name = suggested_name + self.suggested_username = suggested_username diff --git a/pyrogram/types/messages_and_media/translated_text.py b/pyrogram/types/bots_and_keyboards/managed_bot_created.py similarity index 55% rename from pyrogram/types/messages_and_media/translated_text.py rename to pyrogram/types/bots_and_keyboards/managed_bot_created.py index 16a8182aef..cfae775556 100644 --- a/pyrogram/types/messages_and_media/translated_text.py +++ b/pyrogram/types/bots_and_keyboards/managed_bot_created.py @@ -16,44 +16,34 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . -import pyrogram from pyrogram import raw, types - from ..object import Object -from .message import Str -class TranslatedText(Object): - """A translated text with entities. +class ManagedBotCreated(Object): + """This object represents a service message about the bot that was created to be managed by the current bot. Parameters: - text (``str``): - Translated text. + bot (:obj:`~pyrogram.types.User`, *optional*): + Information about the bot. The bot's token can be fetched using the method :obj:`~pyrogram.raw.functions.bots.CreateBot`. - entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): - Entities of the text. """ def __init__( self, *, - text: Str, - entities: list["types.MessageEntity"] = None + bot: "types.User", ): - self.text = text - self.entities = entities + super().__init__() + + self.bot = bot @staticmethod def _parse( client, - translate_result: "raw.types.TextWithEntities" - ) -> "TranslatedText": - entities = [ - types.MessageEntity._parse(client, entity, {}) - for entity in translate_result.entities - ] - entities = types.List(filter(lambda x: x is not None, entities)) - - return TranslatedText( - text=Str(translate_result.text).init(entities) or None, entities=entities or None + action: "raw.types.MessageActionManagedBotCreated", + users: dict, + ) -> "ManagedBotCreated": + return ManagedBotCreated( + bot=types.User._parse(client, users[action.bot_id]), ) diff --git a/pyrogram/types/bots_and_keyboards/managed_bot_updated.py b/pyrogram/types/bots_and_keyboards/managed_bot_updated.py new file mode 100644 index 0000000000..cacc80bd9a --- /dev/null +++ b/pyrogram/types/bots_and_keyboards/managed_bot_updated.py @@ -0,0 +1,56 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from pyrogram import raw, types +from ..object import Object +from ..update import Update + + +class ManagedBotUpdated(Object, Update): + """This object contains information about the creation or token update of a bot that is managed by the current bot. + + Parameters: + user (:obj:`~pyrogram.types.User`, *optional*): + User that created the bot. + + bot (:obj:`~pyrogram.types.User`, *optional*): + Information about the bot. The bot's token can be fetched using the method :obj:`~pyrogram.raw.functions.bots.CreateBot`. + + """ + + def __init__( + self, + *, + user: "types.User", + bot: "types.User", + ): + super().__init__() + + self.user = user + self.bot = bot + + @staticmethod + def _parse( + client, + update: "raw.types.UpdateManagedBot", + users: dict, + ) -> "ManagedBotUpdated": + return ManagedBotUpdated( + user=types.User._parse(client, users[update.user_id]), + bot=types.User._parse(client, users[update.bot_id]), + ) diff --git a/pyrogram/types/chat_topics/forum_topic.py b/pyrogram/types/chat_topics/forum_topic.py index d7acc46a72..99bdc4ecac 100644 --- a/pyrogram/types/chat_topics/forum_topic.py +++ b/pyrogram/types/chat_topics/forum_topic.py @@ -82,6 +82,9 @@ class ForumTopic(Object): unread_reaction_count (``int``, *optional*): Number of messages with unread reactions in the topic + unread_poll_vote_count (``int``, *optional*): + Number of messages with unread poll votes in the topic. + is_reduced_version (``bool``, *optional*): True, if this is a reduced version of the full topic information. If needed, full information can be fetched using :meth:`~pyrogram.Client.get_forum_topic`. @@ -109,6 +112,7 @@ def __init__( last_read_outbox_message_id: int = None, unread_mention_count: int = None, unread_reaction_count: int = None, + unread_poll_vote_count: int = None, is_reduced_version: bool = None ): @@ -133,6 +137,7 @@ def __init__( self.last_read_outbox_message_id = last_read_outbox_message_id self.unread_mention_count = unread_mention_count self.unread_reaction_count = unread_reaction_count + self.unread_poll_vote_count = unread_poll_vote_count self.is_reduced_version = is_reduced_version @@ -178,20 +183,21 @@ def _parse( message_thread_id=forum_topic.id, name=forum_topic.title, icon_color=forum_topic.icon_color, # TODO - icon_custom_emoji_id=getattr(forum_topic, "icon_emoji_id", None), - is_name_implicit=getattr(forum_topic, "title_missing", False), + icon_custom_emoji_id=forum_topic.icon_emoji_id, + is_name_implicit=forum_topic.title_missing, creation_date=utils.timestamp_to_datetime(forum_topic.date), creator=creator, - outgoing=getattr(forum_topic, "my", None), - is_closed=getattr(forum_topic, "closed", None), - is_hidden=getattr(forum_topic, "hidden", None), + outgoing=forum_topic.my, + is_closed=forum_topic.closed, + is_hidden=forum_topic.hidden, last_message=last_message, - is_pinned=getattr(forum_topic, "pinned", None), - unread_count=getattr(forum_topic, "unread_count", None), - last_read_inbox_message_id=getattr(forum_topic, "read_inbox_max_id", None), - last_read_outbox_message_id=getattr(forum_topic, "read_outbox_max_id", None), - unread_mention_count=getattr(forum_topic, "unread_mentions_count", None), - unread_reaction_count=getattr(forum_topic, "unread_reactions_count", None), + is_pinned=forum_topic.pinned, + unread_count=forum_topic.unread_count, + last_read_inbox_message_id=forum_topic.read_inbox_max_id, + last_read_outbox_message_id=forum_topic.read_outbox_max_id, + unread_mention_count=forum_topic.unread_mentions_count, + unread_reaction_count=forum_topic.unread_reactions_count, + unread_poll_vote_count=forum_topic.unread_poll_votes_count, # TODO: notify_settings: PeerNotifySettings, draft: DraftMessage - is_reduced_version=getattr(forum_topic, "short", None) + is_reduced_version=forum_topic.short, ) diff --git a/pyrogram/types/input_message_content/external_reply_info.py b/pyrogram/types/input_message_content/external_reply_info.py index cd560d93e2..18496e38fd 100644 --- a/pyrogram/types/input_message_content/external_reply_info.py +++ b/pyrogram/types/input_message_content/external_reply_info.py @@ -305,7 +305,7 @@ async def _parse( else: media = None elif isinstance(media, raw.types.MessageMediaPoll): - poll = types.Poll._parse(client, media) + poll = await types.Poll._parse(client, media, users, chats) media_type = enums.MessageMediaType.POLL elif isinstance(media, raw.types.MessageMediaDice): dice = types.Dice._parse(client, media) diff --git a/pyrogram/types/input_message_content/input_poll_option.py b/pyrogram/types/input_message_content/input_poll_option.py index 15bbb6c5ad..d789ed78de 100644 --- a/pyrogram/types/input_message_content/input_poll_option.py +++ b/pyrogram/types/input_message_content/input_poll_option.py @@ -19,7 +19,8 @@ from typing import Union import pyrogram -from pyrogram import raw, utils, types, enums +from pyrogram import raw, utils, types +from pyrogram.file_id import FileType from ..object import Object @@ -28,28 +29,61 @@ class InputPollOption(Object): """This object contains information about one answer option in a poll to send. Parameters: - text (``str``): + text (:obj:`~pyrogram.types.FormattedText`): Option text, 1-100 characters after entity parsing. + Only custom emoji entities are allowed to be added and only by Premium users. - text_parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): - By default, texts are parsed using both Markdown and HTML styles. - You can combine both syntaxes together. - Currently, only custom emoji entities are allowed to be added and only by Upgraded bots OR Premium users. + animation (``str``, *optional*): + Pass a file_id as string to send a photo that exists on the Telegram servers. - text_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): - List of special entities that appear in the poll option text, which can be specified instead of *text_parse_mode*. + photo (``str``, *optional*): + Pass a file_id as string to send a photo that exists on the Telegram servers. + + sticker (``str``, *optional*): + Pass a file_id as string to send a photo that exists on the Telegram servers. + + video (``str``, *optional*): + Pass a file_id as string to send a photo that exists on the Telegram servers. """ def __init__( self, *, - text: str, - text_parse_mode: "enums.ParseMode" = None, - text_entities: list["types.MessageEntity"] = None, + text: "types.FormattedText", + animation: str = None, + # messageLocation + photo: str = None, + sticker: str = None, + # messageVenue + video: str = None, ): super().__init__() self.text = text - self.text_parse_mode = text_parse_mode - self.text_entities = text_entities + self.animation = animation + # TODO + self.photo = photo + self.sticker = sticker + self.video = video + + async def write( + self, + client: "pyrogram.Client" + ) -> "raw.types.PollAnswer": + if isinstance(self.text, str): + self.text = types.FormattedText(text=self.text) + + media = None + if self.animation: + media = utils.get_input_media_from_file_id(self.animation, FileType.ANIMATION) + elif self.photo: + media = utils.get_input_media_from_file_id(self.photo, FileType.PHOTO) + elif self.sticker: + media = utils.get_input_media_from_file_id(self.sticker, FileType.STICKER) + elif self.video: + media = utils.get_input_media_from_file_id(self.video, FileType.VIDEO) + return raw.types.InputPollAnswer( + text=await self.text.write(client), + media=media + ) diff --git a/pyrogram/types/input_message_content/reply_parameters.py b/pyrogram/types/input_message_content/reply_parameters.py index d3d677ae10..92f1cb4b5c 100644 --- a/pyrogram/types/input_message_content/reply_parameters.py +++ b/pyrogram/types/input_message_content/reply_parameters.py @@ -62,6 +62,9 @@ class ReplyParameters(Object): direct_messages_topic_id (``int``, *optional*): Identifier of the direct messages topic to which the message will be sent; **required** if the message is sent to a direct messages chat; pass None if the chat is not a channel direct messages chat administered by the current user. + poll_option_id (``int``, *optional*): + Persistent identifier of the specific poll option to be replied to. + """ def __init__( @@ -77,6 +80,7 @@ def __init__( quote_position: int = None, checklist_task_id: int = None, direct_messages_topic_id: int = None, + poll_option_id: int = None, ): super().__init__() @@ -89,3 +93,4 @@ def __init__( self.quote_position = quote_position self.checklist_task_id = checklist_task_id self.direct_messages_topic_id = direct_messages_topic_id + self.poll_option_id = poll_option_id diff --git a/pyrogram/types/messages_and_media/__init__.py b/pyrogram/types/messages_and_media/__init__.py index bbf626322f..0f57430179 100644 --- a/pyrogram/types/messages_and_media/__init__.py +++ b/pyrogram/types/messages_and_media/__init__.py @@ -66,7 +66,7 @@ from .gifted_stars import GiftedStars from .message_effect import MessageEffect from .screenshot_taken import ScreenshotTaken -from .translated_text import TranslatedText +from .formatted_text import FormattedText from .message_auto_delete_timer_changed import MessageAutoDeleteTimerChanged from .write_access_allowed import WriteAccessAllowed from .paid_message_price_changed import PaidMessagePriceChanged @@ -80,6 +80,8 @@ from .chat_owner_changed import ChatOwnerChanged from .chat_has_protected_content_toggled import ChatHasProtectedContentToggled from .chat_has_protected_content_disable_requested import ChatHasProtectedContentDisableRequested +from .poll_option_added import PollOptionAdded +from .poll_option_deleted import PollOptionDeleted __all__ = [ "Animation", @@ -132,7 +134,7 @@ "WebPage", "WriteAccessAllowed", "ScreenshotTaken", - "TranslatedText", + "FormattedText", "PaidMessagePriceChanged", "PaidMessagesRefunded", "Checklist", @@ -145,4 +147,6 @@ "ChatOwnerChanged", "ChatHasProtectedContentToggled", "ChatHasProtectedContentDisableRequested", + "PollOptionAdded", + "PollOptionDeleted", ] diff --git a/pyrogram/types/messages_and_media/formatted_text.py b/pyrogram/types/messages_and_media/formatted_text.py new file mode 100644 index 0000000000..5ac1f8c730 --- /dev/null +++ b/pyrogram/types/messages_and_media/formatted_text.py @@ -0,0 +1,96 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +import pyrogram +from pyrogram import enums, raw, types, utils + +from ..object import Object +from .message import Str + + +class FormattedText(Object): + """A text with some entities. + + Parameters: + text (``str``): + The text. + + parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): + Parse mode of the text. + By default, texts are parsed using both Markdown and HTML styles. + You can combine both syntaxes together. + + entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): + Entities contained in the text. + Entities can be nested, but must not mutually intersect with each other. + :obj:`~pyrogram.enums.MessageEntityType.PRE`, :obj:`~pyrogram.enums.MessageEntityType.CODE` and :obj:`~pyrogram.enums.MessageEntityType.DATE_TIME` entities can't contain other entities. + :obj:`~pyrogram.enums.MessageEntityType.BLOCKQUOTE` entities can't contain other :obj:`~pyrogram.enums.MessageEntityType.BLOCKQUOTE` entities. + :obj:`~pyrogram.enums.MessageEntityType.BOLD`, :obj:`~pyrogram.enums.MessageEntityType.ITALIC`, :obj:`~pyrogram.enums.MessageEntityType.UNDERLINE`, :obj:`~pyrogram.enums.MessageEntityType.STRIKETHROUGH`, and :obj:`~pyrogram.enums.MessageEntityType.SPOILER` entities can contain and can be part of any other entities. + All other entities can't contain each other. + + """ + + def __init__( + self, + *, + text: Str, + parse_mode: "enums.ParseMode" = None, + entities: list["types.MessageEntity"] = None + ): + self.text = text + self.parse_mode = parse_mode + self.entities = entities + + @staticmethod + def _parse( + client, + result: "raw.types.TextWithEntities" + ) -> "FormattedText": + if not result.text: + return None + entities = [ + types.MessageEntity._parse( + client, + entity, + {} # there isn't a TEXT_MENTION entity available yet + ) + for entity in result.entities + ] + entities = types.List(filter(lambda x: x is not None, entities)) + return FormattedText( + text=Str(result.text).init(entities) or None, entities=entities or None + ) + + async def write( + self, + client: "pyrogram.Client", + VBL = [] + ) -> "raw.types.TextWithEntities": + message, entities = ( + await utils.parse_text_entities( + client, + self.text, + self.parse_mode, + self.entities + ) + ).values() + + return raw.types.TextWithEntities( + text=message, + entities=entities or VBL + ) diff --git a/pyrogram/types/messages_and_media/message.py b/pyrogram/types/messages_and_media/message.py index 5a51a7d587..1b53339bb7 100644 --- a/pyrogram/types/messages_and_media/message.py +++ b/pyrogram/types/messages_and_media/message.py @@ -128,6 +128,9 @@ class Message(Object, Update): reply_to_checklist_task_id (``int``, *optional*): Identifier of the specific checklist task that is being replied to. + reply_to_poll_option_id (``str``, *optional*): + Persistent identifier of the specific poll option that is being replied to. + via_bot (:obj:`~pyrogram.types.User`): The information of the bot that generated the message from an inline query of a user. @@ -358,9 +361,18 @@ class Message(Object, Update): giveaway_completed (:obj:`~pyrogram.types.GiveawayCompleted`, *optional*): Service message: a giveaway without public winners was completed + managed_bot_created (:obj:`~pyrogram.types.ManagedBotCreated`, *optional*): + Service message: user created a bot that will be managed by the current bot. + paid_message_price_changed (:obj:`~pyrogram.types.PaidMessagePriceChanged`, *optional*): Service message: the price for paid messages has changed in the chat. - + + poll_option_added (:obj:`~pyrogram.types.PollOptionAdded`, *optional*): + Service message: answer option was added to a poll + + poll_option_deleted (:obj:`~pyrogram.types.PollOptionDeleted`, *optional*): + Service message: answer option was deleted from a poll. + direct_message_price_changed (:obj:`~pyrogram.types.DirectMessagePriceChanged`, *optional*): Service message: the price for paid messages in the corresponding direct messages chat of a channel has changed. @@ -494,6 +506,7 @@ def __init__( quote: "types.TextQuote" = None, reply_to_story: "types.Story" = None, reply_to_checklist_task_id: int = None, + reply_to_poll_option_id: str = None, via_bot: "types.User" = None, edit_date: datetime = None, has_protected_content: bool = None, @@ -602,6 +615,9 @@ def __init__( contact_registered: "types.ContactRegistered" = None, chat_join_type: "enums.ChatJoinType" = None, screenshot_taken: "types.ScreenshotTaken" = None, + managed_bot_created: "types.ManagedBotCreated" = None, + poll_option_added: "types.PollOptionAdded" = None, + poll_option_deleted: "types.PollOptionDeleted" = None, _raw = None ): super().__init__(client) @@ -724,7 +740,11 @@ def __init__( self.checklist_tasks_done = checklist_tasks_done self.checklist_tasks_added = checklist_tasks_added self.reply_to_checklist_task_id = reply_to_checklist_task_id + self.reply_to_poll_option_id = reply_to_poll_option_id self.direct_messages_topic = direct_messages_topic + self.managed_bot_created = managed_bot_created + self.poll_option_added = poll_option_added + self.poll_option_deleted = poll_option_deleted self._raw = _raw @staticmethod @@ -839,6 +859,10 @@ async def _parse( checklist_tasks_done = None checklist_tasks_added = None + managed_bot_created = None + poll_option_added = None + poll_option_deleted = None + service_type = enums.MessageServiceType.UNKNOWN if isinstance(action, raw.types.MessageActionChatAddUser): @@ -1135,6 +1159,16 @@ async def _parse( service_type = enums.MessageServiceType.CHECKLIST_TASKS_ADDED checklist_tasks_added = types.ChecklistTasksAdded._parse(client, message, users, chats) + elif isinstance(action, raw.types.MessageActionPollAppendAnswer): + service_type = enums.MessageServiceType.POLL_OPTION_ADDED + poll_option_added = types.PollOptionAdded._parse(client, message) + elif isinstance(action, raw.types.MessageActionPollDeleteAnswer): + service_type = enums.MessageServiceType.POLL_OPTION_DELETED + poll_option_deleted = types.PollOptionDeleted._parse(client, message) + elif isinstance(action, raw.types.MessageActionManagedBotCreated): + service_type = enums.MessageServiceType.MANAGED_BOT_CREATED + managed_bot_created = types.ManagedBotCreated._parse(client, action, users) + parsed_message = Message( id=message.id, date=utils.timestamp_to_datetime(message.date), @@ -1190,6 +1224,9 @@ async def _parse( reactions=types.MessageReactions._parse(client, message.reactions) if message.reactions else None, checklist_tasks_done=checklist_tasks_done, checklist_tasks_added=checklist_tasks_added, + managed_bot_created=managed_bot_created, + poll_option_added=poll_option_added, + poll_option_deleted=poll_option_deleted, client=client ) @@ -1364,7 +1401,7 @@ async def _parse( if not web_page: media = None elif isinstance(media, raw.types.MessageMediaPoll): - poll = types.Poll._parse(client, media) + poll = await types.Poll._parse(client, media, users, chats) media_type = enums.MessageMediaType.POLL elif isinstance(media, raw.types.MessageMediaDice): dice = types.Dice._parse(client, media) @@ -1528,6 +1565,13 @@ async def _parse( if isinstance(message.reply_to, raw.types.MessageReplyHeader): parsed_message.reply_to_checklist_task_id = message.reply_to.todo_item_id + try: + # Assuming reply_to.poll_option is a bytes-like object + poll_option_id = message.reply_to.poll_option.decode("UTF-8") + except (UnicodeDecodeError, AttributeError): + # Equivalent to poll_option_id_.clear() + poll_option_id = None + parsed_message.reply_to_poll_option_id = poll_option_id parsed_message.reply_to_message_id = message.reply_to.reply_to_msg_id parsed_message.message_thread_id = message.reply_to.reply_to_top_id if message.reply_to.forum_topic: @@ -3234,34 +3278,57 @@ async def reply_photo( async def reply_poll( self, - question: str, + question: "types.FormattedText", options: list["types.InputPollOption"], - question_parse_mode: "enums.ParseMode" = None, - question_entities: list["types.MessageEntity"] = None, + quote: bool = None, is_anonymous: bool = True, type: "enums.PollType" = enums.PollType.REGULAR, allows_multiple_answers: bool = None, - correct_option_id: int = None, - explanation: str = None, - explanation_parse_mode: "enums.ParseMode" = None, - explanation_entities: list["types.MessageEntity"] = None, + allows_revoting: bool = None, + shuffle_options: bool = None, + allow_adding_options: bool = None, + hide_results_until_closes: bool = None, + correct_option_ids: list[int] = None, + explanation: "types.FormattedText" = None, open_period: int = None, close_date: datetime = None, is_closed: bool = None, - quote: bool = None, + description: "types.FormattedText" = None, disable_notification: bool = None, protect_content: bool = None, allow_paid_broadcast: bool = None, - message_effect_id: int = None, reply_parameters: "types.ReplyParameters" = None, + message_thread_id: int = None, + business_connection_id: str = None, send_as: Union[int, str] = None, schedule_date: datetime = None, + message_effect_id: int = None, reply_markup: Union[ "types.InlineKeyboardMarkup", "types.ReplyKeyboardMarkup", "types.ReplyKeyboardRemove", "types.ForceReply" ] = None, + attached_media_animation: str = None, + attached_media_audio: str = None, + attached_media_document: str = None, + # messageLocation + attached_media_photo: str = None, + attached_media_sticker: str = None, + # messageVenue + attached_media_video: str = None, + attached_media_video_note: str = None, + attached_media_voice: str = None, + solution_media_animation: str = None, + solution_media_audio: str = None, + solution_media_document: str = None, + # messageLocation + solution_media_photo: str = None, + solution_media_sticker: str = None, + # messageVenue + solution_media_video: str = None, + solution_media_video_note: str = None, + solution_media_voice: str = None, reply_to_message_id: int = None ) -> "Message": """Bound method *reply_poll* of :obj:`~pyrogram.types.Message`. @@ -3294,7 +3361,7 @@ async def reply_poll( Parameters: - question (``str``): + question (:obj:`~pyrogram.types.FormattedText`): Poll question. **Users**: 1-255 characters. **Bots**: 1-300 characters. @@ -3302,12 +3369,10 @@ async def reply_poll( options (List of :obj:`~pyrogram.types.InputPollOption`): List of 2-12 poll answer options. - question_parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): - By default, texts are parsed using both Markdown and HTML styles. - You can combine both syntaxes together. - - question_entities (List of :obj:`~pyrogram.types.MessageEntity`): - List of special entities that appear in the poll question, which can be specified instead of *question_parse_mode*. + quote (``bool``, *optional*): + If ``True``, the message will be sent as a reply to this message. + If *reply_to_message_id* is passed, this parameter will be ignored. + Defaults to ``True`` in group chats and ``False`` in private chats. is_anonymous (``bool``, *optional*): True, if the poll needs to be anonymous. @@ -3318,41 +3383,43 @@ async def reply_poll( Defaults to :obj:`~pyrogram.enums.PollType.REGULAR`. allows_multiple_answers (``bool``, *optional*): - True, if the poll allows multiple answers, ignored for polls in quiz mode. + True, if the poll allows multiple answers. Defaults to False. - correct_option_id (``int``, *optional*): - 0-based identifier of the correct answer option, required for polls in quiz mode. + allows_revoting (``bool``, *optional*): + Pass True, if the poll allows to change chosen answer options, defaults to False for quizzes and to True for regular polls. - explanation (``str``, *optional*): - Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style - poll, 0-200 characters with at most 2 line feeds after entities parsing. + shuffle_options (``bool``, *optional*): + Pass True, if the poll options must be shown in random order. - explanation_parse_mode (:obj:`~pyrogram.enums.ParseMode`, *optional*): - By default, texts are parsed using both Markdown and HTML styles. - You can combine both syntaxes together. + allow_adding_options (``bool``, *optional*): + Pass True, if answer options can be added to the poll after creation; not supported for anonymous polls and quizzes. + + hide_results_until_closes (``bool``, *optional*): + Pass True, if poll results must be shown only after the poll closes. + + correct_option_ids (List of ``int``, *optional*): + List of monotonically increasing 0-based identifiers of the correct answer options, required for polls in quiz mode. - explanation_entities (List of :obj:`~pyrogram.types.MessageEntity`): - List of special entities that appear in the poll explanation, which can be specified instead of - *parse_mode*. + explanation (:obj:`~pyrogram.types.FormattedText`, *optional*): + Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style + poll, 0-200 characters with at most 2 line feeds after entities parsing. open_period (``int``, *optional*): - Amount of time in seconds the poll will be active after creation, 5-600. + Amount of time in seconds the poll will be active after creation, 5-2628000. Can't be used together with *close_date*. close_date (:py:obj:`~datetime.datetime`, *optional*): Point in time when the poll will be automatically closed. - Must be at least 5 and no more than 600 seconds in the future. + Must be at least 5 and no more than 2628000 seconds in the future. Can't be used together with *open_period*. is_closed (``bool``, *optional*): Pass True, if the poll needs to be immediately closed. This can be useful for poll preview. - quote (``bool``, *optional*): - If ``True``, the message will be sent as a reply to this message. - If *reply_to_message_id* is passed, this parameter will be ignored. - Defaults to ``True`` in group chats and ``False`` in private chats. + description (:obj:`~pyrogram.types.FormattedText`, *optional*): + Description of the poll to be sent, 0-1024 characters after entities parsing. disable_notification (``bool``, *optional*): Sends the message silently. @@ -3364,12 +3431,15 @@ async def reply_poll( allow_paid_broadcast (``bool``, *optional*): Pass True to allow the message to ignore regular broadcast limits for a small fee; for bots only - message_effect_id (``int`` ``64-bit``, *optional*): - Unique identifier of the message effect to be added to the message; for private chats only. - reply_parameters (:obj:`~pyrogram.types.ReplyParameters`, *optional*): Description of the message to reply to + message_thread_id (``int``, *optional*): + If the message is in a thread, ID of the original message. + + business_connection_id (``str``, *optional*): + Unique identifier of the business connection on behalf of which the message will be sent. + send_as (``int`` | ``str``): Unique identifier (int) or username (str) of the chat or channel to send the message as. You can use this to send the message on behalf of a chat or channel where you have appropriate permissions. @@ -3380,6 +3450,9 @@ async def reply_poll( schedule_date (:py:obj:`~datetime.datetime`, *optional*): Date when the message will be automatically sent. + message_effect_id (``int`` ``64-bit``, *optional*): + Unique identifier of the message effect to be added to the message; for private chats only. + reply_markup (:obj:`~pyrogram.types.InlineKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardMarkup` | :obj:`~pyrogram.types.ReplyKeyboardRemove` | :obj:`~pyrogram.types.ForceReply`, *optional*): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. @@ -3390,7 +3463,19 @@ async def reply_poll( Raises: RPCError: In case of a Telegram RPC error. """ + if reply_to_message_id and reply_parameters: + raise ValueError( + "Parameters `reply_to_message_id` and `reply_parameters` are mutually " + "exclusive." + ) + if reply_to_message_id is not None: + log.warning( + "This property is deprecated. " + "Please use reply_parameters instead" + ) + reply_parameters = types.ReplyParameters(message_id=reply_to_message_id) + reply_to_message_id, reply_parameters = utils._get_reply_to_message_quote_ids( reply_parameters, self.id, @@ -3404,30 +3489,47 @@ async def reply_poll( chat_id=self.chat.id, question=question, options=options, - question_parse_mode=question_parse_mode, - question_entities=question_entities, is_anonymous=is_anonymous, type=type, allows_multiple_answers=allows_multiple_answers, - correct_option_id=correct_option_id, + allows_revoting=allows_revoting, + shuffle_options=shuffle_options, + allow_adding_options=allow_adding_options, + hide_results_until_closes=hide_results_until_closes, + correct_option_ids=correct_option_ids, explanation=explanation, - explanation_parse_mode=explanation_parse_mode, - explanation_entities=explanation_entities, open_period=open_period, close_date=close_date, is_closed=is_closed, + description=description, disable_notification=disable_notification, protect_content=protect_content, allow_paid_broadcast=allow_paid_broadcast, paid_message_star_count=(self and self.chat and self.chat.paid_message_star_count) or None, - message_effect_id=message_effect_id, reply_parameters=reply_parameters, message_thread_id=self.message_thread_id, business_connection_id=self.business_connection_id, send_as=send_as, schedule_date=schedule_date, - reply_to_message_id=reply_to_message_id, - reply_markup=reply_markup + message_effect_id=message_effect_id, + reply_markup=reply_markup, + attached_media_animation=attached_media_animation, + attached_media_audio=attached_media_audio, + attached_media_document=attached_media_document, + attached_media_photo=attached_media_photo, + attached_media_sticker=attached_media_sticker, + attached_media_video=attached_media_video, + attached_media_video_note=attached_media_video_note, + # TODO: https://t.me/c/1279877202/191075 + attached_media_voice=attached_media_voice, + solution_media_animation=solution_media_animation, + solution_media_audio=solution_media_audio, + solution_media_document=solution_media_document, + solution_media_photo=solution_media_photo, + solution_media_sticker=solution_media_sticker, + solution_media_video=solution_media_video, + solution_media_video_note=solution_media_video_note, + solution_media_voice=solution_media_voice, ) async def reply_sticker( @@ -5667,7 +5769,7 @@ async def download( async def vote( self, - option: int, + option: Union[int, list[int]] ) -> "types.Poll": """Bound method *vote* of :obj:`~pyrogram.types.Message`. @@ -5687,14 +5789,15 @@ async def vote( message.vote(6) Parameters: - option (``int``): - Index of the poll option you want to vote for (0 to 9). + option (``Int`` | List of ``int``): + Index or list of indexes (for multiple answers) of the poll option(s) you want to vote for (0 to 9). Returns: :obj:`~pyrogram.types.Poll`: On success, the poll with the chosen option is returned. Raises: RPCError: In case of a Telegram RPC error. + """ return await self._client.vote_poll( @@ -5898,7 +6001,7 @@ async def view(self, force_read: bool = True) -> bool: async def translate( self, to_language_code: str - ) -> "types.TranslatedText": + ) -> "types.FormattedText": """Bound method *translate* of :obj:`~pyrogram.types.Message`. Use as a shortcut for: @@ -5922,7 +6025,7 @@ async def translate( Must be one of "af", "sq", "am", "ar", "hy", "az", "eu", "be", "bn", "bs", "bg", "ca", "ceb", "zh-CN", "zh", "zh-Hans", "zh-TW", "zh-Hant", "co", "hr", "cs", "da", "nl", "en", "eo", "et", "fi", "fr", "fy", "gl", "ka", "de", "el", "gu", "ht", "ha", "haw", "he", "iw", "hi", "hmn", "hu", "is", "ig", "id", "in", "ga", "it", "ja", "jv", "kn", "kk", "km", "rw", "ko", "ku", "ky", "lo", "la", "lv", "lt", "lb", "mk", "mg", "ms", "ml", "mt", "mi", "mr", "mn", "my", "ne", "no", "ny", "or", "ps", "fa", "pl", "pt", "pa", "ro", "ru", "sm", "gd", "sr", "st", "sn", "sd", "si", "sk", "sl", "so", "es", "su", "sw", "sv", "tl", "tg", "ta", "tt", "te", "th", "tr", "tk", "uk", "ur", "ug", "uz", "vi", "cy", "xh", "yi", "ji", "yo", "zu". Returns: - :obj:`~pyrogram.types.TranslatedText`: The translated result is returned. + :obj:`~pyrogram.types.FormattedText`: The translated result is returned. Raises: RPCError: In case of a Telegram RPC error. diff --git a/pyrogram/types/messages_and_media/poll.py b/pyrogram/types/messages_and_media/poll.py index 55958913ac..040d8aa089 100644 --- a/pyrogram/types/messages_and_media/poll.py +++ b/pyrogram/types/messages_and_media/poll.py @@ -33,12 +33,9 @@ class Poll(Object, Update): id (``str``): Unique poll identifier. - question (``str``): + question (:obj:`~pyrogram.types.FormattedText`): Poll question, 1-255 characters. - question_entities (List of :obj:`~pyrogram.types.MessageEntity`): - Special entities that appear in the question. Currently, only custom emoji entities are allowed in poll questions. - options (List of :obj:`~pyrogram.types.PollOption`): List of poll options. @@ -48,36 +45,44 @@ class Poll(Object, Update): is_closed (``bool``): True, if the poll is closed. - is_anonymous (``bool``, *optional*): + is_anonymous (``bool``): True, if the poll is anonymous - type (:obj:`~pyrogram.enums.PollType`, *optional*): + type (:obj:`~pyrogram.enums.PollType`): Poll type. - allows_multiple_answers (``bool``, *optional*): + allows_multiple_answers (``bool``): True, if the poll allows multiple answers. + allows_revoting (``bool``): + True, if the poll allows to change the chosen answer options. + chosen_option_id (``int``, *optional*): 0-based index of the chosen option), None in case of no vote yet. - correct_option_id (``int``, *optional*): - 0-based identifier of the correct answer option. - Available only for polls in the quiz mode, which are closed, or was sent (not forwarded) by the bot or to - the private chat with the bot. + correct_option_ids (List of ``int``, *optional*): + Array of 0-based identifiers of the correct answer options. + Available only for polls in quiz mode which are closed or were sent (not forwarded) by the bot or to the private chat with the bot. - explanation (``str``, *optional*): + explanation (:obj:`~pyrogram.types.FormattedText`, *optional*): Text that is shown when a user chooses an incorrect answer or taps on the lamp icon in a quiz-style poll, 0-200 characters. - explanation_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): - Special entities like usernames, URLs, bot commands, etc. that appear in the explanation. - open_period (``int``, *optional*): Amount of time in seconds the poll will be active after creation. close_date (:py:obj:`~datetime.datetime`, *optional*): Point in time when the poll will be automatically closed. + has_open_answers (``bool``, *optional*): + Participants can suggest new options. + + description (:obj:`~pyrogram.types.FormattedText`, *optional*): + Description of the poll; for polls inside the Message object only. + + can_add_option (``bool``, *property*): + True, if an option can be added to the poll using :meth:`~pyrogram.Client.add_poll_option`. + """ def __init__( @@ -85,47 +90,63 @@ def __init__( *, client: "pyrogram.Client" = None, id: str, - question: Str, + question: "types.FormattedText", options: list["types.PollOption"], - question_entities: list["types.MessageEntity"] = None, total_voter_count: int, is_closed: bool, - is_anonymous: bool = None, - type: "enums.PollType" = None, - allows_multiple_answers: bool = None, + is_anonymous: bool, + type: "enums.PollType", + allows_multiple_answers: bool, + allows_revoting: bool, chosen_option_id: Optional[int] = None, - correct_option_id: Optional[int] = None, - explanation: Optional[str] = None, - explanation_entities: Optional[list["types.MessageEntity"]] = None, + correct_option_ids: Optional[list[int]] = None, + explanation: Optional["types.FormattedText"] = None, open_period: Optional[int] = None, close_date: Optional[datetime] = None, + has_open_answers: Optional[bool] = None, + description: Optional["types.FormattedText"] = None, ): super().__init__(client) self.id = id self.question = question self.options = options - self.question_entities = question_entities self.total_voter_count = total_voter_count self.is_closed = is_closed self.is_anonymous = is_anonymous self.type = type self.allows_multiple_answers = allows_multiple_answers + self.allows_revoting = allows_revoting self.chosen_option_id = chosen_option_id - self.correct_option_id = correct_option_id + self.correct_option_ids = correct_option_ids self.explanation = explanation - self.explanation_entities = explanation_entities self.open_period = open_period self.close_date = close_date + self.has_open_answers = has_open_answers + self.description = description @staticmethod - def _parse(client, media_poll: Union["raw.types.MessageMediaPoll", "raw.types.UpdateMessagePoll"]) -> "Poll": + async def _parse( + client, + media_poll: Union[ + "raw.types.MessageMediaPoll", + "raw.types.UpdateMessagePoll" + ], + users: dict, + chats: dict, + ) -> "Poll": poll: raw.types.Poll = media_poll.poll poll_results: raw.types.PollResults = media_poll.results results: list[raw.types.PollAnswerVoters] = poll_results.results - chosen_option_id = None - correct_option_id = None + persistent_id = "" + if isinstance(media_poll, raw.types.UpdateMessagePoll): + persistent_id = str(media_poll.poll_id) + if isinstance(media_poll, raw.types.MessageMediaPoll): + persistent_id = str(poll.id) + + chosen_option_id = [] + correct_option_ids = [] options = [] for i, answer in enumerate(poll.answers): @@ -136,65 +157,68 @@ def _parse(client, media_poll: Union["raw.types.MessageMediaPoll", "raw.types.Up voter_count = result.voters if result.chosen: - chosen_option_id = i + chosen_option_id.append(i) if result.correct: - correct_option_id = i + correct_option_ids.append(i) - entities = [ - types.MessageEntity._parse( - client, - entity, - {} # there isn't a TEXT_MENTION entity available yet - ) - for entity in (answer.text.entities or []) - ] - entities = types.List(filter(lambda x: x is not None, entities)) + added_by_peer = answer.added_by + user = None + voter_chat = None + + if added_by_peer: + if isinstance(added_by_peer, raw.types.PeerUser): + user = types.Chat._parse_user_chat(client, users[added_by_peer.user_id]) + + elif isinstance(added_by_peer, raw.types.PeerChat): + voter_chat = types.Chat._parse_chat_chat(client, chats[added_by_peer.chat_id]) + + else: + voter_chat = types.Chat._parse_channel_chat(client, chats[added_by_peer.channel_id]) options.append( types.PollOption( - text=Str(answer.text.text).init(entities), - text_entities=entities, + persistent_id=answer.option.decode("UTF-8"), + text=types.FormattedText._parse(client, answer.text), + # media:flags.0?MessageMedia voter_count=voter_count, data=answer.option, + added_by_user=user, + added_by_chat=voter_chat, + addition_date=utils.timestamp_to_datetime(answer.date), client=client ) ) - entities = [ - types.MessageEntity._parse( - client, - entity, - {} # there isn't a TEXT_MENTION entity available yet - ) - for entity in (poll.question.entities or []) - ] - entities = types.List(filter(lambda x: x is not None, entities)) + if getattr(media_poll, "attached_media", None): + attached_media = media_poll.attached_media + # TODO return Poll( - id=str(poll.id), - question=Str(poll.question.text).init(entities), + id=persistent_id, + question=types.FormattedText._parse( + client, + poll.question + ), options=options, - question_entities=entities, - total_voter_count=media_poll.results.total_voters, + total_voter_count=poll_results.total_voters, is_closed=poll.closed, is_anonymous=not poll.public_voters, type=enums.PollType.QUIZ if poll.quiz else enums.PollType.REGULAR, allows_multiple_answers=poll.multiple_choice, + allows_revoting=not poll.revoting_disabled, chosen_option_id=chosen_option_id, - correct_option_id=correct_option_id, - explanation=poll_results.solution, - explanation_entities=[ - types.MessageEntity._parse(client, i, {}) - for i in poll_results.solution_entities - ] if poll_results.solution_entities else None, + correct_option_ids=correct_option_ids, + explanation=types.FormattedText._parse(client, raw.types.TextWithEntities(text=poll_results.solution, entities=poll_results.solution_entities)), open_period=poll.close_period, close_date=utils.timestamp_to_datetime(poll.close_date), + has_open_answers=poll.open_answers, + description=None, #types.FormattedText._parse(client, ), client=client ) @staticmethod - def _parse_update( + async def _parse_update( client, update: Union["raw.types.UpdateMessagePoll", "raw.types.UpdateMessagePollVote"], users: dict, @@ -202,26 +226,29 @@ def _parse_update( ): if isinstance(update, raw.types.UpdateMessagePoll): if update.poll is not None: - return Poll._parse(client, update) + return await Poll._parse(client, update, users, chats) # TODO: FIXME! results = update.results.results - chosen_option_id = None - correct_option_id = None + chosen_option_id = [] + correct_option_ids = [] options = [] - question = "" + question = types.FormattedText( + text="" + ) for i, result in enumerate(results): if result.chosen: - chosen_option_id = i + chosen_option_id.append(i) if result.correct: - correct_option_id = i + correct_option_ids.append(i) options.append( types.PollOption( - text="", - text_entities=[], + persistent_id=result.option.decode("UTF-8"), + text=None, + # media:flags.0?MessageMedia voter_count=result.voters, data=result.option, client=client @@ -234,8 +261,13 @@ def _parse_update( options=options, total_voter_count=update.results.total_voters, is_closed=False, + is_anonymous=None, + type=None, # TODO + allows_multiple_answers=None, + allows_revoting=None, + has_open_answers=None, chosen_option_id=chosen_option_id, - correct_option_id=correct_option_id, + correct_option_ids=correct_option_ids, client=client ) @@ -280,3 +312,7 @@ async def stop( reply_markup=reply_markup, business_connection_id=business_connection_id ) + + @property + def can_add_option(self): + return self.has_open_answers and not self.is_closed and len(self.options) < 12 diff --git a/pyrogram/types/messages_and_media/poll_answer.py b/pyrogram/types/messages_and_media/poll_answer.py index 5362adde52..1fb652a7e5 100644 --- a/pyrogram/types/messages_and_media/poll_answer.py +++ b/pyrogram/types/messages_and_media/poll_answer.py @@ -41,6 +41,9 @@ class PollAnswer(Object, Update): option_ids (List of ``int``): 0-based identifiers of chosen answer options. May be empty if the vote was retracted. + option_persistent_ids (List of ``str``): + Persistent identifiers of the chosen answer options. May be empty if the vote was retracted. + """ def __init__( @@ -49,6 +52,7 @@ def __init__( client: "pyrogram.Client" = None, poll_id: str, option_ids: list[int], + option_persistent_ids: list[str], user: Optional["types.User"] = None, voter_chat: Optional["types.Chat"] = None, ): @@ -56,6 +60,7 @@ def __init__( self.poll_id = poll_id self.option_ids = option_ids + self.option_persistent_ids = option_persistent_ids self.user = user self.voter_chat = voter_chat @@ -69,6 +74,7 @@ def _parse_update( if isinstance(update, raw.types.UpdateMessagePollVote): user = None voter_chat = None + if isinstance(update.peer, raw.types.PeerUser): user = types.Chat._parse_user_chat(client, users[update.peer.user_id]) @@ -84,6 +90,10 @@ def _parse_update( "{:0>2x}".format(option[0]) for option in update.options ], + option_persistent_ids=[ + str(position) + for position in update.positions + ], user=user, voter_chat=voter_chat, client=client diff --git a/pyrogram/types/messages_and_media/poll_option.py b/pyrogram/types/messages_and_media/poll_option.py index 7c650eb8a1..c4090178f1 100644 --- a/pyrogram/types/messages_and_media/poll_option.py +++ b/pyrogram/types/messages_and_media/poll_option.py @@ -16,43 +16,60 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . +from datetime import datetime + import pyrogram from pyrogram import types from ..object import Object -from .message import Str class PollOption(Object): """Contains information about one answer option in a poll. Parameters: - text (``str``): + persistent_id (``str``): + Unique identifier of the option, persistent on option addition and deletion. + + text (:obj:`~pyrogram.types.FormattedText`): Option text, 1-100 characters. - - text_entities (List of :obj:`~pyrogram.types.MessageEntity`, *optional*): - Special entities that appear in the option text. Currently, only custom emoji entities are allowed in poll option texts. voter_count (``int``): Number of users that voted for this option. Equals to 0 until you vote. + added_by_user (:obj:`~pyrogram.types.User`, *optional*): + User who added the option; omitted if the option wasn't added by a user after poll creation. + + added_by_chat (:obj:`~pyrogram.types.Chat`, *optional*): + Chat that added the option; omitted if the option wasn't added by a chat after poll creation. + + addition_date (:py:obj:`~datetime.datetime`, *optional*): + Date the message was last edited. + data (``bytes``): The data this poll option is holding. + """ def __init__( self, *, client: "pyrogram.Client" = None, - text: Str, - text_entities: list["types.MessageEntity"], + persistent_id: str, + text: "types.FormattedText", voter_count: int, - data: bytes + data: bytes, + added_by_user: "types.User" = None, + added_by_chat: "types.Chat" = None, + addition_date: datetime = None, ): super().__init__(client) + self.persistent_id = persistent_id self.text = text - self.text_entities = text_entities self.voter_count = voter_count self.data = data + self.added_by_user = added_by_user + self.added_by_chat = added_by_chat + self.addition_date = addition_date diff --git a/pyrogram/types/messages_and_media/poll_option_added.py b/pyrogram/types/messages_and_media/poll_option_added.py new file mode 100644 index 0000000000..1f94f1bae4 --- /dev/null +++ b/pyrogram/types/messages_and_media/poll_option_added.py @@ -0,0 +1,62 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from pyrogram import raw, types +from ..object import Object + + +class PollOptionAdded(Object): + """This object represents a service message about an option added to a poll. + + Parameters: + poll_message (:obj:`~pyrogram.types.Message`, *optional*): + Message containing the poll to which the option was added, if known. + + option_persistent_id (``str``): + Unique identifier of the added option. + + option_text (:obj:`~pyrogram.types.FormattedText`): + Option text. + + """ + + def __init__( + self, + *, + option_persistent_id: str, + poll_message: "types.Message" = None, + option_text: "types.FormattedText" = None, + ): + super().__init__() + + self.option_persistent_id = option_persistent_id + self.poll_message = poll_message + self.option_text = option_text + + @staticmethod + def _parse( + client, + message: "raw.types.MessageService", + ) -> "PollOptionAdded": + action: "raw.types.MessageActionPollAppendAnswer" = message.action + answer: "raw.types.PollAnswer" = action.answer + return PollOptionAdded( + option_persistent_id=answer.option.decode("UTF-8"), + poll_message=None, + option_text=types.FormattedText._parse(client, answer.text) + ) diff --git a/pyrogram/types/messages_and_media/poll_option_deleted.py b/pyrogram/types/messages_and_media/poll_option_deleted.py new file mode 100644 index 0000000000..6aecc932d0 --- /dev/null +++ b/pyrogram/types/messages_and_media/poll_option_deleted.py @@ -0,0 +1,62 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from pyrogram import raw, types +from ..object import Object + + +class PollOptionDeleted(Object): + """This object represents a service message about an option deleted from a poll. + + Parameters: + poll_message (:obj:`~pyrogram.types.Message`, *optional*): + Message containing the poll to which the option was added, if known. + + option_persistent_id (``str``): + Unique identifier of the added option. + + option_text (:obj:`~pyrogram.types.FormattedText`): + Option text. + + """ + + def __init__( + self, + *, + option_persistent_id: str, + poll_message: "types.Message" = None, + option_text: "types.FormattedText" = None, + ): + super().__init__() + + self.option_persistent_id = option_persistent_id + self.poll_message = poll_message + self.option_text = option_text + + @staticmethod + def _parse( + client, + message: "raw.types.MessageService", + ) -> "PollOptionDeleted": + action: "raw.types.MessageActionPollDeleteAnswer" = message.action + answer: "raw.types.PollAnswer" = action.answer + return PollOptionDeleted( + option_persistent_id=answer.option.decode("UTF-8"), + poll_message=None, + option_text=types.FormattedText._parse(client, answer.text) + ) diff --git a/pyrogram/types/user_and_chats/chat.py b/pyrogram/types/user_and_chats/chat.py index 37b2314afb..be001237c9 100644 --- a/pyrogram/types/user_and_chats/chat.py +++ b/pyrogram/types/user_and_chats/chat.py @@ -21,9 +21,7 @@ from typing import Union, Optional, AsyncGenerator import pyrogram -from pyrogram import raw, enums -from pyrogram import types -from pyrogram import utils +from pyrogram import enums, raw, types, utils from pyrogram.errors import MessageIdsEmpty from ..object import Object @@ -254,6 +252,12 @@ class Chat(Object): first_profile_audio (:obj:`~pyrogram.types.Audio`, *optional*): For private chats, the first audio added to the profile of the user. + note (:obj:`~pyrogram.types.FormattedText`, *optional*): + Note added to the user's contact. + + uses_unofficial_app (``bool``, *optional*): + True, if the user uses an unofficial application that poses a security risk. + full_name (``str``, *property*): Full name of the other party in a private chat, for private chats and bots. OR, Title of the chat, for groups and channels. @@ -332,6 +336,8 @@ def __init__( has_automatic_translation: bool = None, is_direct_messages: bool = None, first_profile_audio: "types.Audio" = None, + note: "types.FormattedText" = None, + uses_unofficial_app: bool = None, _raw: Union[ "raw.types.ChatInvite", "raw.types.Channel", @@ -411,6 +417,8 @@ def __init__( self.has_automatic_translation = has_automatic_translation self.is_direct_messages = is_direct_messages self.first_profile_audio = first_profile_audio + self.note = note + self.uses_unofficial_app = uses_unofficial_app self._raw = _raw @staticmethod @@ -690,6 +698,9 @@ async def _parse_full(client, chat_full: Union[raw.types.messages.ChatFull, raw. parsed_chat.first_profile_audio = types.Audio._parse( client, doc, audio_attributes, file_name ) + if full_user.note: + parsed_chat.note = types.FormattedText._parse(client, full_user.note) + parsed_chat.uses_unofficial_app = bool(full_user.unofficial_security_risk) or None else: full_chat = chat_full.full_chat diff --git a/pyrogram/types/user_and_chats/dialog.py b/pyrogram/types/user_and_chats/dialog.py index 66c9a80110..2c1f4b8b73 100644 --- a/pyrogram/types/user_and_chats/dialog.py +++ b/pyrogram/types/user_and_chats/dialog.py @@ -41,6 +41,9 @@ class Dialog(Object): unread_reactions_count (``int``): Amount of unread messages containing a reaction in this dialog. + + unread_poll_vote_count (``int``): + Number of messages with unread poll votes in the topic. unread_mark (``bool``): True, if the dialog has the unread mark set. @@ -73,6 +76,7 @@ def __init__( unread_messages_count: int, unread_mentions_count: int, unread_reactions_count: int, + unread_poll_vote_count: int, unread_mark: bool, is_pinned: bool, chat_list: int, @@ -88,6 +92,7 @@ def __init__( self.unread_messages_count = unread_messages_count self.unread_mentions_count = unread_mentions_count self.unread_reactions_count = unread_reactions_count + self.unread_poll_vote_count = unread_poll_vote_count self.unread_mark = unread_mark self.is_pinned = is_pinned self.chat_list = chat_list @@ -104,6 +109,7 @@ def _parse(client, dialog: "raw.types.Dialog", messages, users, chats) -> "Dialo unread_messages_count=dialog.unread_count, unread_mentions_count=dialog.unread_mentions_count, unread_reactions_count=dialog.unread_reactions_count, + unread_poll_vote_count=dialog.unread_poll_vote_count, unread_mark=dialog.unread_mark, is_pinned=dialog.pinned, chat_list=getattr(dialog, "folder_id", None), diff --git a/pyrogram/types/user_and_chats/user.py b/pyrogram/types/user_and_chats/user.py index ce07c70890..ad1fb5d215 100644 --- a/pyrogram/types/user_and_chats/user.py +++ b/pyrogram/types/user_and_chats/user.py @@ -198,6 +198,9 @@ class User(Object, Update): allows_users_to_create_topics (``bool``, *optional*): True, if users can create and delete topics in the chat with the bot. Returned only in get_me. + can_manage_bots (``bool``, *optional*): + True, if other bots can be created to be controlled by the bot. Returned only in get_me. + mention (``str``, *property*): Generate a text mention for this user. You can use ``user.mention()`` to mention the user using their first name (styled using html), or @@ -257,6 +260,7 @@ def __init__( paid_message_star_count: int = None, has_topics_enabled: bool = None, allows_users_to_create_topics: bool = None, + can_manage_bots: bool = None, _raw: "raw.base.User" = None ): super().__init__(client) @@ -305,6 +309,7 @@ def __init__( self.paid_message_star_count = paid_message_star_count self.has_topics_enabled = has_topics_enabled self.allows_users_to_create_topics = allows_users_to_create_topics + self.can_manage_bots = can_manage_bots self._raw = _raw @property @@ -342,7 +347,7 @@ def _parse(client, user: "raw.base.User") -> Optional["User"]: active_usernames = types.List( [ types.Username._parse(u) - for u in getattr(user, "usernames", []) + for u in user.usernames or [] ] ) or None _tmp_username = None @@ -376,34 +381,29 @@ def _parse(client, user: "raw.base.User") -> Optional["User"]: photo=types.ChatPhoto._parse(client, user.photo, user.id, user.access_hash), restrictions=types.List([types.Restriction._parse(r) for r in user.restriction_reason]) or None, client=client, - restricts_new_chats=getattr(user, "contact_require_premium", None), + restricts_new_chats=user.contact_require_premium or None, active_usernames=active_usernames, - is_close_friend=getattr(user, "close_friend", None), - accent_color=types.ChatColor._parse(getattr(user, "color", None)), - profile_color=types.ChatColor._parse_profile_color(getattr(user, "profile_color", None)), - have_access=not bool(getattr(user, "min", False)), # apply_min_photo + is_close_friend=user.close_friend or None, + accent_color=types.ChatColor._parse(user.color), + profile_color=types.ChatColor._parse_profile_color(user.profile_color), + have_access=not bool(user.min or None), # apply_min_photo paid_message_star_count=user.send_paid_messages_stars, _raw=user ) if parsed_user.is_bot: - parsed_user.added_to_attachment_menu = getattr(user, "attach_menu_enabled", None) - parsed_user.can_be_added_to_attachment_menu = getattr(user, "bot_attach_menu", None) - parsed_user.can_join_groups = not bool(getattr(user, "bot_nochats", None)) - parsed_user.can_read_all_group_messages = getattr(user, "bot_chat_history", None) - parsed_user.inline_query_placeholder = getattr( - user, "bot_inline_placeholder", None - ) + parsed_user.added_to_attachment_menu = user.attach_menu_enabled or None + parsed_user.can_be_added_to_attachment_menu = user.bot_attach_menu or None + parsed_user.can_join_groups = not bool(user.bot_nochats or None) + parsed_user.can_read_all_group_messages = user.bot_chat_history or None + parsed_user.inline_query_placeholder = user.bot_inline_placeholder or None parsed_user.supports_inline_queries = bool(parsed_user.inline_query_placeholder) - parsed_user.inline_need_location = bool( - getattr(user, "bot_inline_geo", None) - ) - parsed_user.can_connect_to_business = bool( - getattr(user, "bot_business", None) - ) - parsed_user.has_main_web_app = bool(getattr(user, "bot_has_main_app", None)) - parsed_user.active_user_count = getattr(user, "bot_active_users", None) - parsed_user.has_topics_enabled = getattr(user, "bot_forum_view", None) - parsed_user.allows_users_to_create_topics = getattr(user, "bot_forum_can_manage_topics", None) + parsed_user.inline_need_location = user.bot_inline_geo or None + parsed_user.can_connect_to_business = user.bot_business or None + parsed_user.has_main_web_app = user.bot_has_main_app or None + parsed_user.active_user_count = user. bot_active_users or None + parsed_user.has_topics_enabled = user.bot_forum_view or None + parsed_user.allows_users_to_create_topics = user.bot_forum_can_manage_topics or None + parsed_user.can_manage_bots = user.bot_can_manage_bots or None # stories_hidden:flags2.3?true # stories_unavailable:flags2.4?true # stories_max_id:flags2.5?RecentStory @@ -411,9 +411,7 @@ def _parse(client, user: "raw.base.User") -> Optional["User"]: # bot_info_version:flags.14?int # bot_verification_icon:flags2.14?long if parsed_user.is_bot: # TODO - parsed_user.can_be_edited = bool( - getattr(user, "bot_can_edit", None) - ) + parsed_user.can_be_edited = user.bot_can_edit or None return parsed_user @staticmethod diff --git a/pyrogram/utils.py b/pyrogram/utils.py index 1d7b18b098..74da00d0f6 100644 --- a/pyrogram/utils.py +++ b/pyrogram/utils.py @@ -545,6 +545,8 @@ async def _get_reply_message_parameters( ) if reply_parameters.checklist_task_id: reply_to.todo_item_id = reply_parameters.checklist_task_id + if reply_parameters.poll_option_id: + reply_to.poll_option = reply_parameters.poll_option_id.encode("UTF-8") return reply_to