diff --git a/README.rst b/README.rst index 6248bd5f73b..2a96b7112b9 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-9.2-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-9.3-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API version @@ -81,7 +81,7 @@ After installing_ the library, be sure to check out the section on `working with Telegram API support ~~~~~~~~~~~~~~~~~~~~ -All types and methods of the Telegram Bot API **9.2** are natively supported by this library. +All types and methods of the Telegram Bot API **9.3** are natively supported by this library. In addition, Bot API functionality not yet natively included can still be used as described `in our wiki `_. Notable Features diff --git a/changes/unreleased/5078.FoNwUYLbXQFRebTFhR6UPn.toml b/changes/unreleased/5078.FoNwUYLbXQFRebTFhR6UPn.toml new file mode 100644 index 00000000000..85221185c4c --- /dev/null +++ b/changes/unreleased/5078.FoNwUYLbXQFRebTFhR6UPn.toml @@ -0,0 +1,22 @@ +features = """ +Full Support for Bot API 9.3 + +.. warning:: + + Bot API 9.3 introduces a now required argument ``gift_id`` to ``UniqueGift``. For backward compatibility, the argument is currently still marked as optional in the signature and it's presence is enforced through a runtime check + In future versions, this argument will be made required in the signature as well. + Please make sure to update your code accordingly to avoid potential issues in the future. We recommend using keyword arguments when creating ``UniqueGift`` instances to ensure compatibility with future updates. +""" + +pull_requests = [ + { uid = "5086", author_uids = ["Bibo-Joshi"] }, + { uid = "5078", author_uid = "aelkheir", closes_threads = ["5077"] }, + { uid = "5079", author_uid = "aelkheir" }, + { uid = "5084", author_uids = ["Bibo-Joshi"] }, + { uid = "5085", author_uids = ["Bibo-Joshi"] }, + { uid = "5087", author_uids = ["Bibo-Joshi"] }, + { uid = "5091", author_uids = ["Bibo-Joshi"] }, + { uid = "5090", author_uids = ["Bibo-Joshi"] }, + { uid = "5089", author_uids = ["Bibo-Joshi"] }, + { uid = "5092", author_uids = ["Bibo-Joshi"] }, +] diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index 128fa26ac5c..7bb89f23dac 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -35,6 +35,8 @@ - Used for sending media grouped together * - :meth:`~telegram.Bot.send_message` - Used for sending text messages + * - :meth:`~telegram.Bot.send_message_draft` + - Used for streaming partial text messages * - :meth:`~telegram.Bot.send_paid_media` - Used for sending paid media to channels * - :meth:`~telegram.Bot.send_photo` @@ -443,6 +445,8 @@ - Used for setting the business accounts profile photo * - :meth:`~telegram.Bot.post_story` - Used for posting a story on behalf of business account. + * - :meth:`~telegram.Bot.repost_story` + - Used for reposting an existing story on behalf of business account. * - :meth:`~telegram.Bot.edit_story` - Used for editing business stories posted by the bot. * - :meth:`~telegram.Bot.convert_gift_to_stars` @@ -481,8 +485,12 @@ - Used for getting basic info about a file * - :meth:`~telegram.Bot.get_available_gifts` - Used for getting information about gifts available for sending + * - :meth:`~telegram.Bot.get_chat_gifts` + - Used for getting information about gifts owned and hosted by a chat * - :meth:`~telegram.Bot.get_me` - Used for getting basic information about the bot + * - :meth:`~telegram.Bot.get_user_gifts` + - Used for getting information about gifts owned and hosted by a user * - :meth:`~telegram.Bot.save_prepared_inline_message` - Used for storing a message to be sent by a user of a Mini App diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index f04e35df648..de76000019d 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -83,6 +83,7 @@ Available Types telegram.forumtopicreopened telegram.generalforumtopichidden telegram.generalforumtopicunhidden + telegram.giftbackground telegram.giftinfo telegram.giveaway telegram.giveawaycompleted @@ -181,6 +182,7 @@ Available Types telegram.telegramobject telegram.textquote telegram.uniquegift + telegram.uniquegiftcolors telegram.uniquegiftbackdrop telegram.uniquegiftbackdropcolors telegram.uniquegiftinfo @@ -190,6 +192,7 @@ Available Types telegram.user telegram.userchatboosts telegram.userprofilephotos + telegram.userrating telegram.usersshared telegram.venue telegram.video diff --git a/docs/source/telegram.giftbackground.rst b/docs/source/telegram.giftbackground.rst new file mode 100644 index 00000000000..c2785ff67b0 --- /dev/null +++ b/docs/source/telegram.giftbackground.rst @@ -0,0 +1,7 @@ +GiftBackground +============== + +.. autoclass:: telegram.GiftBackground + :members: + :show-inheritance: + diff --git a/docs/source/telegram.uniquegiftcolors.rst b/docs/source/telegram.uniquegiftcolors.rst new file mode 100644 index 00000000000..3e554abd8de --- /dev/null +++ b/docs/source/telegram.uniquegiftcolors.rst @@ -0,0 +1,7 @@ +UniqueGiftColors +================ + +.. autoclass:: telegram.UniqueGiftColors + :members: + :show-inheritance: + diff --git a/docs/source/telegram.userrating.rst b/docs/source/telegram.userrating.rst new file mode 100644 index 00000000000..f4a8db4a068 --- /dev/null +++ b/docs/source/telegram.userrating.rst @@ -0,0 +1,6 @@ +UserRating +========== + +.. autoclass:: telegram.UserRating + :members: + :show-inheritance: diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst index fe7b976f5b7..f1bad363716 100644 --- a/docs/substitutions/global.rst +++ b/docs/substitutions/global.rst @@ -30,7 +30,7 @@ .. |message_thread_id| replace:: Unique identifier for the target message thread of the forum topic. -.. |message_thread_id_arg| replace:: Unique identifier for the target message thread (topic) of the forum; for forum supergroups only. +.. |message_thread_id_arg| replace:: Unique identifier for the target message thread (topic) of a forum; for forum supergroups and private chats of bots with forum topic mode enabled only. .. |parse_mode| replace:: Mode for parsing entities. See :class:`telegram.constants.ParseMode` and `formatting options `__ for more details. diff --git a/src/telegram/__init__.py b/src/telegram/__init__.py index 0d77c81eeba..1342ab76194 100644 --- a/src/telegram/__init__.py +++ b/src/telegram/__init__.py @@ -111,6 +111,7 @@ "GeneralForumTopicHidden", "GeneralForumTopicUnhidden", "Gift", + "GiftBackground", "GiftInfo", "Gifts", "Giveaway", @@ -287,6 +288,7 @@ "UniqueGift", "UniqueGiftBackdrop", "UniqueGiftBackdropColors", + "UniqueGiftColors", "UniqueGiftInfo", "UniqueGiftModel", "UniqueGiftSymbol", @@ -294,6 +296,7 @@ "User", "UserChatBoosts", "UserProfilePhotos", + "UserRating", "UsersShared", "Venue", "Video", @@ -453,7 +456,7 @@ from ._games.callbackgame import CallbackGame from ._games.game import Game from ._games.gamehighscore import GameHighScore -from ._gifts import AcceptedGiftTypes, Gift, GiftInfo, Gifts +from ._gifts import AcceptedGiftTypes, Gift, GiftBackground, GiftInfo, Gifts from ._giveaway import Giveaway, GiveawayCompleted, GiveawayCreated, GiveawayWinners from ._inline.inlinekeyboardbutton import InlineKeyboardButton from ._inline.inlinekeyboardmarkup import InlineKeyboardMarkup @@ -597,6 +600,7 @@ UniqueGift, UniqueGiftBackdrop, UniqueGiftBackdropColors, + UniqueGiftColors, UniqueGiftInfo, UniqueGiftModel, UniqueGiftSymbol, @@ -604,6 +608,7 @@ from ._update import Update from ._user import User from ._userprofilephotos import UserProfilePhotos +from ._userrating import UserRating from ._videochat import ( VideoChatEnded, VideoChatParticipantsInvited, diff --git a/src/telegram/_bot.py b/src/telegram/_bot.py index 7552f90ed1a..324927ec9f6 100644 --- a/src/telegram/_bot.py +++ b/src/telegram/_bot.py @@ -1200,6 +1200,69 @@ async def delete_message( api_kwargs=api_kwargs, ) + async def send_message_draft( + self, + chat_id: int, + draft_id: int, + text: str, + message_thread_id: int | None = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + entities: Sequence["MessageEntity"] | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> bool: + """Use this method to stream a partial message to a user while the message is being + generated; supported only for bots with forum topic mode enabled. + + .. versionadded:: NEXT.VERSION + + Args: + chat_id (:obj:`int`): Unique identifier for the target private chat. + draft_id (:obj:`int`): Unique identifier of the message draft; must be non-zero. + Changes of drafts with the same identifier are animated. + text (:obj:`str`): Text of the message to be sent, + :tg-const:`telegram.constants.MessageLimit.MIN_TEXT_LENGTH`- + :tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters after + entities parsing. + parse_mode (:obj:`str`): |parse_mode| + entities (Sequence[:class:`telegram.MessageEntity`], optional): Sequence of special + entities that appear in message text, which can be specified instead of + :paramref:`parse_mode`. + + |sequenceargs| + message_thread_id (:obj:`int`, optional): Unique identifier for the target + message thread. + + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "draft_id": draft_id, + "text": text, + "entities": entities, + } + return await self._send_message( + "sendMessageDraft", + data, + message_thread_id=message_thread_id, + parse_mode=parse_mode, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def delete_messages( self, chat_id: int | str, @@ -1253,6 +1316,7 @@ async def forward_message( video_start_timestamp: int | None = None, direct_messages_topic_id: int | None = None, suggested_post_parameters: "SuggestedPostParameters | None" = None, + message_effect_id: str | None = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1298,6 +1362,10 @@ async def forward_message( forwarded to a direct messages chat. .. versionadded:: 22.4 + message_effect_id (:obj:`str`, optional): Unique identifier of the message effect to be + added to the message; only available when forwarding to private chats + + .. versionadded:: NEXT.VERSION Returns: :class:`telegram.Message`: On success, the sent Message is returned. @@ -1325,6 +1393,7 @@ async def forward_message( pool_timeout=pool_timeout, api_kwargs=api_kwargs, direct_messages_topic_id=direct_messages_topic_id, + message_effect_id=message_effect_id, ) async def forward_messages( @@ -3721,7 +3790,9 @@ async def send_chat_action( action(:obj:`str`): Type of action to broadcast. Choose one, depending on what the user is about to receive. For convenience look at the constants in :class:`telegram.constants.ChatAction`. - message_thread_id (:obj:`int`, optional): |message_thread_id_arg| + message_thread_id (:obj:`int`, optional): Unique identifier for the target message + thread or topic of a forum; for supergroups and private chats of bots with forum + topic mode enabled only .. versionadded:: 20.0 business_connection_id (:obj:`str`, optional): |business_id_str| @@ -5905,7 +5976,9 @@ async def promote_chat_member( can_invite_users (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can invite new users to the chat. can_restrict_members (:obj:`bool`, optional): Pass :obj:`True`, if the administrator - can restrict, ban or unban chat members, or access supergroup statistics. + can restrict, ban or unban chat members, or access supergroup statistics. For + backward compatibility, defaults to :obj:`True` for promotions of channel + administrators can_pin_messages (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can pin messages, for supergroups only. can_promote_members (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can @@ -8347,6 +8420,7 @@ async def copy_message( video_start_timestamp: int | None = None, direct_messages_topic_id: int | None = None, suggested_post_parameters: "SuggestedPostParameters | None" = None, + message_effect_id: str | None = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: int | None = None, @@ -8408,6 +8482,10 @@ async def copy_message( direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id| .. versionadded:: 22.4 + message_effect_id (:obj:`str`, optional): Unique identifier of the message effect to be + added to the message; only available when copying to private chats + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -8470,6 +8548,7 @@ async def copy_message( "direct_messages_topic_id": direct_messages_topic_id, "video_start_timestamp": video_start_timestamp, "suggested_post_parameters": suggested_post_parameters, + "message_effect_id": message_effect_id, } result = await self._post( @@ -8903,8 +8982,9 @@ async def edit_forum_topic( api_kwargs: JSONDict | None = None, ) -> bool: """ - Use this method to edit name and icon of a topic in a forum supergroup chat. The bot must - be an administrator in the chat for this to work and must have the + Use this method to edit name and icon of a topic in a forum supergroup chat or a private + chat with a user. In the case of a supergroup chat the bot must be an administrator in the + chat for this to work and must have the :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights, unless it is the creator of the topic. @@ -9046,7 +9126,8 @@ async def delete_forum_topic( ) -> bool: """ Use this method to delete a forum topic along with all its messages in a forum supergroup - chat. The bot must be an administrator in the chat for this to work and must have + chat or a private chat with a user. In the case of a supergroup chat the bot must be an + administrator in the chat for this to work and must have the :paramref:`~telegram.ChatAdministratorRights.can_delete_messages` administrator rights. .. versionadded:: 20.0 @@ -9088,10 +9169,11 @@ async def unpin_all_forum_topic_messages( api_kwargs: JSONDict | None = None, ) -> bool: """ - Use this method to clear the list of pinned messages in a forum topic. The bot must - be an administrator in the chat for this to work and must have - :paramref:`~telegram.ChatAdministratorRights.can_pin_messages` administrator rights - in the supergroup. + Use this method to clear the list of pinned messages in a forum topic in a forum supergroup + chat or a private chat with a user. In the case of a supergroup chat the bot must be an + administrator in the chat for this to work and must have the + :paramref:`~telegram.ChatAdministratorRights.can_pin_messages` administrator right in + the supergroup. .. versionadded:: 20.0 @@ -11575,6 +11657,227 @@ async def decline_suggested_post( api_kwargs=api_kwargs, ) + async def repost_story( + self, + business_connection_id: str, + from_chat_id: int, + from_story_id: int, + active_period: int, + post_to_chat_page: bool | None = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> Story: + """ + Reposts a story on behalf of a business account from another business account. + Both business accounts must be managed by the same bot, and the story on the source account + must have been posted (or reposted) by the bot. Requires the + :attr:`~telegram.BusinessBotRight.can_manage_stories` business bot right for both + business accounts. + + .. versionadded:: NEXT.VERSION + + Args: + business_connection_id (:obj:`str`): Unique identifier of the business + from_chat_id (:obj:`int`): Unique identifier of the chat which posted the story that + should be reposted + from_story_id (:obj:`int`): Unique identifier of the story that should be reposted + active_period (:obj:`int`): Period after which the story is moved to the archive, in + seconds; must be one of + :tg-const:`telegram.constants.StoryLimit.SIX_HOURS`, + :tg-const:`telegram.constants.StoryLimit.TWELVE_HOURS`, + :tg-const:`telegram.constants.StoryLimit.ONE_DAY`, or + :tg-const:`telegram.constants.StoryLimit.TWO_DAYS`. + post_to_chat_page (:obj:`bool`, optional): Pass :obj:`True` to keep the story + accessible after it expires. + protect_content (:obj:`bool`, optional): Pass :obj:`True` if the content of the story + must be protected from forwarding and screenshotting + + Returns: + :class:`telegram.Story` + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "business_connection_id": business_connection_id, + "from_chat_id": from_chat_id, + "from_story_id": from_story_id, + "active_period": active_period, + "post_to_chat_page": post_to_chat_page, + "protect_content": protect_content, + } + return Story.de_json( + data=await self._post( + "repostStory", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ), + bot=self, + ) + + async def get_user_gifts( + self, + user_id: int, + exclude_unlimited: bool | None = None, + exclude_limited_upgradable: bool | None = None, + exclude_limited_non_upgradable: bool | None = None, + exclude_from_blockchain: bool | None = None, + exclude_unique: bool | None = None, + sort_by_price: bool | None = None, + offset: str | None = None, + limit: int | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> OwnedGifts: + """Returns the gifts owned and hosted by a user. + + .. versionadded:: NEXT.VERSION + + user_id (:obj:`int`): Unique identifier of the user + exclude_unlimited (:obj:`bool`, optional): Pass :obj:`True` to exclude gifts that can be + purchased an unlimited number of times + exclude_limited_upgradable (:obj:`bool`, optional): Pass :obj:`True` to exclude gifts that + can be purchased a limited number of times and can be upgraded to unique + exclude_limited_non_upgradable (:obj:`bool`, optional): Pass :obj:`True` to exclude gifts + that can be purchased a limited number of times and can't be upgraded to unique + exclude_from_blockchain (:obj:`bool`, optional): Pass :obj:`True` to exclude gifts that + were assigned from the TON blockchain and can't be resold or transferred in Telegram + exclude_unique (:obj:`bool`, optional): Pass :obj:`True` to exclude unique gifts + sort_by_price (:obj:`bool`, optional): Pass :obj:`True` to sort results by gift price + instead of send date. Sorting is applied before pagination. + offset (:obj:`str`, optional): Offset of the first entry to return as received from the + previous request; use an empty string to get the first chunk of results + limit (:obj:`int`, optional): The maximum number of gifts to be returned; + :tg-const:`~telegram.constants.BusinessLimit.MIN_GIFT_RESULTS` - + :tg-const:`~telegram.constants.BusinessLimit.MAX_GIFT_RESLUTS`. + Defaults to :tg-const:`~telegram.constants.BusinessLimit.MAX_GIFT_RESLUTS` + + Returns: + :class:`telegram.OwnedGifts`: The owned gifts for the user. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "user_id": user_id, + "exclude_unlimited": exclude_unlimited, + "exclude_limited_upgradable": exclude_limited_upgradable, + "exclude_limited_non_upgradable": exclude_limited_non_upgradable, + "exclude_from_blockchain": exclude_from_blockchain, + "exclude_unique": exclude_unique, + "sort_by_price": sort_by_price, + "offset": offset, + "limit": limit, + } + + result = await self._post( + "getUserGifts", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return OwnedGifts.de_json(result, self) + + async def get_chat_gifts( + self, + chat_id: int | str, + exclude_unsaved: bool | None = None, + exclude_saved: bool | None = None, + exclude_unlimited: bool | None = None, + exclude_limited_upgradable: bool | None = None, + exclude_limited_non_upgradable: bool | None = None, + exclude_from_blockchain: bool | None = None, + exclude_unique: bool | None = None, + sort_by_price: bool | None = None, + offset: str | None = None, + limit: int | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> OwnedGifts: + """Use this method to get gifts owned by a chat. + + .. versionadded:: NEXT.VERSION + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + exclude_unsaved (:obj:`bool`, optional): Pass :obj:`True` to exclude gifts that aren't + saved to the chat's profile page. Always :obj:`True`, unless the bot has the + :attr:`~telegram.ChatAdministratorRights..can_post_messages` administrator right in the + channel. + exclude_saved (:obj:`bool`, optional): Pass :obj:`True` to exclude gifts that are saved to + the chat's profile page. Always :obj:`False`, unless the bot has the + :attr:`~telegram.ChatAdministratorRights..can_post_messages` administrator right in the + channel. + exclude_unlimited (:obj:`bool`, optional): Pass :obj:`True` to exclude gifts that can be + purchased an unlimited number of times + exclude_limited_upgradable (:obj:`bool`, optional): Pass :obj:`True` to exclude gifts that + can be purchased a limited number of times and can be upgraded to unique + exclude_limited_non_upgradable (:obj:`bool`, optional): Pass :obj:`True` to exclude gifts + that can be purchased a limited number of times and can't be upgraded to unique + exclude_from_blockchain (:obj:`bool`, optional): Pass :obj:`True` to exclude gifts that + were assigned from the TON blockchain and can't be resold or transferred in Telegram + exclude_unique (:obj:`bool`, optional): Pass :obj:`True` to exclude unique gifts + sort_by_price (:obj:`bool`, optional): Pass :obj:`True` to sort results by gift price + instead of send date. Sorting is applied before pagination. + offset (:obj:`str`, optional): Offset of the first entry to return as received from the + previous request; use an empty string to get the first chunk of results + limit (:obj:`int`, optional): The maximum number of gifts to be returned; + :tg-const:`~telegram.constants.BusinessLimit.MIN_GIFT_RESULTS` - + :tg-const:`~telegram.constants.BusinessLimit.MAX_GIFT_RESLUTS`. + Defaults to :tg-const:`~telegram.constants.BusinessLimit.MAX_GIFT_RESLUTS` + + Returns: + :class:`telegram.OwnedGifts`: The owned gifts for the chat. + + Raises: + :class:`telegram.error.TelegramError` + """ + + data: JSONDict = { + "chat_id": chat_id, + "exclude_unsaved": exclude_unsaved, + "exclude_saved": exclude_saved, + "exclude_unlimited": exclude_unlimited, + "exclude_limited_upgradable": exclude_limited_upgradable, + "exclude_limited_non_upgradable": exclude_limited_non_upgradable, + "exclude_from_blockchain": exclude_from_blockchain, + "exclude_unique": exclude_unique, + "sort_by_price": sort_by_price, + "offset": offset, + "limit": limit, + } + + result = await self._post( + "getChatGifts", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + return OwnedGifts.de_json(result, self) + def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """See :meth:`telegram.TelegramObject.to_dict`.""" data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name} @@ -11589,6 +11892,8 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`get_me`""" sendMessage = send_message """Alias for :meth:`send_message`""" + sendMessageDraft = send_message_draft + """Alias for :meth:`send_message_draft`""" deleteMessage = delete_message """Alias for :meth:`delete_message`""" deleteMessages = delete_messages @@ -11899,3 +12204,9 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`approve_suggested_post`""" declineSuggestedPost = decline_suggested_post """Alias for :meth:`decline_suggested_post`""" + repostStory = repost_story + """Alias for :meth:`repost_story`""" + getUserGifts = get_user_gifts + """Alias for :meth:`get_user_gifts`""" + getChatGifts = get_chat_gifts + """Alias for :meth:`get_chat_gifts`""" diff --git a/src/telegram/_callbackquery.py b/src/telegram/_callbackquery.py index 90c83a60de1..fdfc509cac8 100644 --- a/src/telegram/_callbackquery.py +++ b/src/telegram/_callbackquery.py @@ -874,6 +874,7 @@ async def copy_message( allow_paid_broadcast: bool | None = None, video_start_timestamp: int | None = None, suggested_post_parameters: "SuggestedPostParameters | None" = None, + message_effect_id: str | None = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: int | None = None, @@ -925,6 +926,7 @@ async def copy_message( show_caption_above_media=show_caption_above_media, allow_paid_broadcast=allow_paid_broadcast, suggested_post_parameters=suggested_post_parameters, + message_effect_id=message_effect_id, ) MAX_ANSWER_TEXT_LENGTH: Final[int] = ( diff --git a/src/telegram/_chat.py b/src/telegram/_chat.py index cddaac5c1cb..5c28fc48a60 100644 --- a/src/telegram/_chat.py +++ b/src/telegram/_chat.py @@ -67,9 +67,11 @@ Message, MessageEntity, MessageId, + OwnedGifts, PhotoSize, ReplyParameters, Sticker, + Story, SuggestedPostParameters, UserChatBoosts, Venue, @@ -1080,6 +1082,44 @@ async def send_message( suggested_post_parameters=suggested_post_parameters, ) + async def send_message_draft( + self, + draft_id: int, + text: str, + message_thread_id: int | None = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + entities: Sequence["MessageEntity"] | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> bool: + """Shortcut for:: + + await bot.send_message_draft(update.effective_chat.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message_draft`. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().send_message_draft( + chat_id=self.id, + draft_id=draft_id, + text=text, + message_thread_id=message_thread_id, + parse_mode=parse_mode, + entities=entities, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def delete_message( self, message_id: int, @@ -2323,6 +2363,7 @@ async def send_copy( video_start_timestamp: int | None = None, direct_messages_topic_id: int | None = None, suggested_post_parameters: "SuggestedPostParameters | None" = None, + message_effect_id: str | None = None, *, reply_to_message_id: int | None = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2368,6 +2409,7 @@ async def send_copy( allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, suggested_post_parameters=suggested_post_parameters, + message_effect_id=message_effect_id, ) async def copy_message( @@ -2387,6 +2429,7 @@ async def copy_message( video_start_timestamp: int | None = None, direct_messages_topic_id: int | None = None, suggested_post_parameters: "SuggestedPostParameters | None" = None, + message_effect_id: str | None = None, *, reply_to_message_id: int | None = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -2432,6 +2475,7 @@ async def copy_message( allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, suggested_post_parameters=suggested_post_parameters, + message_effect_id=message_effect_id, ) async def send_copies( @@ -2538,6 +2582,7 @@ async def forward_from( video_start_timestamp: int | None = None, direct_messages_topic_id: int | None = None, suggested_post_parameters: "SuggestedPostParameters | None" = None, + message_effect_id: str | None = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2574,6 +2619,7 @@ async def forward_from( message_thread_id=message_thread_id, direct_messages_topic_id=direct_messages_topic_id, suggested_post_parameters=suggested_post_parameters, + message_effect_id=message_effect_id, ) async def forward_to( @@ -2586,6 +2632,7 @@ async def forward_to( video_start_timestamp: int | None = None, direct_messages_topic_id: int | None = None, suggested_post_parameters: "SuggestedPostParameters | None" = None, + message_effect_id: str | None = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2623,6 +2670,7 @@ async def forward_to( message_thread_id=message_thread_id, direct_messages_topic_id=direct_messages_topic_id, suggested_post_parameters=suggested_post_parameters, + message_effect_id=message_effect_id, ) async def forward_messages_from( @@ -3854,6 +3902,99 @@ async def decline_suggested_post( api_kwargs=api_kwargs, ) + async def repost_story( + self, + business_connection_id: str, + from_story_id: int, + active_period: int, + post_to_chat_page: bool | None = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> "Story": + """Shortcut for:: + + await bot.repost_story( + from_chat_id=update.effective_chat.id, + *args, **kwargs + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.repost_story`. + + .. versionadded:: NEXT.VERSION + + Returns: + :class:`Story`: On success, :class:`Story` is returned. + + """ + return await self.get_bot().repost_story( + business_connection_id=business_connection_id, + from_chat_id=self.id, + from_story_id=from_story_id, + active_period=active_period, + post_to_chat_page=post_to_chat_page, + protect_content=protect_content, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_gifts( + self, + exclude_unsaved: bool | None = None, + exclude_saved: bool | None = None, + exclude_unlimited: bool | None = None, + exclude_limited_upgradable: bool | None = None, + exclude_limited_non_upgradable: bool | None = None, + exclude_from_blockchain: bool | None = None, + exclude_unique: bool | None = None, + sort_by_price: bool | None = None, + offset: str | None = None, + limit: int | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> "OwnedGifts": + """Shortcut for:: + + await bot.get_chat_gifts(chat_id=update.effective_chat.id) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_chat_gifts`. + + .. versionadded:: NEXT.VERSION + + Returns: + :class:`telegram.OwnedGifts`: On success, returns the gifts owned by the chat. + """ + return await self.get_bot().get_chat_gifts( + chat_id=self.id, + exclude_unsaved=exclude_unsaved, + exclude_saved=exclude_saved, + exclude_unlimited=exclude_unlimited, + exclude_limited_upgradable=exclude_limited_upgradable, + exclude_limited_non_upgradable=exclude_limited_non_upgradable, + exclude_from_blockchain=exclude_from_blockchain, + exclude_unique=exclude_unique, + sort_by_price=sort_by_price, + offset=offset, + limit=limit, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + class Chat(_ChatBase): """This object represents a chat. diff --git a/src/telegram/_chatfullinfo.py b/src/telegram/_chatfullinfo.py index c87a13c09c8..0c2b49c5b3e 100644 --- a/src/telegram/_chatfullinfo.py +++ b/src/telegram/_chatfullinfo.py @@ -30,6 +30,8 @@ from telegram._files.chatphoto import ChatPhoto from telegram._gifts import AcceptedGiftTypes from telegram._reaction import ReactionType +from telegram._uniquegift import UniqueGiftColors +from telegram._userrating import UserRating from telegram._utils.argumentparsing import ( de_json_optional, de_list_optional, @@ -232,6 +234,19 @@ class ChatFullInfo(_ChatBase): chat; for direct messages chats only. .. versionadded:: 22.4 + rating (:class:`telegram.UserRating`, optional): For private chats, the rating of the user + if any. + + .. versionadded:: NEXT.VERSION + unique_gift_colors (:class:`telegram.UniqueGiftColors`, optional): The color scheme based + on a unique gift that must be used for the chat's name, message replies and link + previews + + .. versionadded:: NEXT.VERSION + paid_message_star_count (:obj:`int`, optional): The number of Telegram Stars a general user + have to pay to send a message to the chat + + .. versionadded:: NEXT.VERSION Attributes: id (:obj:`int`): Unique identifier for this chat. @@ -404,6 +419,19 @@ class ChatFullInfo(_ChatBase): chat; for direct messages chats only. .. versionadded:: 22.4 + rating (:class:`telegram.UserRating`): Optional. For private chats, the rating of the user + if any. + + .. versionadded:: NEXT.VERSION + unique_gift_colors (:class:`telegram.UniqueGiftColors`): Optional. The color scheme based + on a unique gift that must be used for the chat's name, message replies and link + previews + + .. versionadded:: NEXT.VERSION + paid_message_star_count (:obj:`int`): Optional. The number of Telegram Stars a general user + have to pay to send a message to the chat + + .. versionadded:: NEXT.VERSION .. _accent colors: https://core.telegram.org/bots/api#accent-colors .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups @@ -440,6 +468,7 @@ class ChatFullInfo(_ChatBase): "linked_chat_id", "location", "max_reaction_count", + "paid_message_star_count", "parent_chat", "permissions", "personal_chat", @@ -447,7 +476,9 @@ class ChatFullInfo(_ChatBase): "pinned_message", "profile_accent_color_id", "profile_background_custom_emoji_id", + "rating", "sticker_set_name", + "unique_gift_colors", "unrestrict_boost_count", ) @@ -500,6 +531,9 @@ def __init__( can_send_paid_media: bool | None = None, is_direct_messages: bool | None = None, parent_chat: Chat | None = None, + rating: UserRating | None = None, + unique_gift_colors: UniqueGiftColors | None = None, + paid_message_star_count: int | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -563,6 +597,9 @@ def __init__( self.can_send_paid_media: bool | None = can_send_paid_media self.accepted_gift_types: AcceptedGiftTypes = accepted_gift_types self.parent_chat: Chat | None = parent_chat + self.rating: UserRating | None = rating + self.unique_gift_colors: UniqueGiftColors | None = unique_gift_colors + self.paid_message_star_count: int | None = paid_message_star_count @property def slow_mode_delay(self) -> int | dtm.timedelta | None: @@ -615,4 +652,9 @@ def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "ChatFullInfo": ) data["parent_chat"] = de_json_optional(data.get("parent_chat"), Chat, bot) + data["rating"] = de_json_optional(data.get("rating"), UserRating, bot) + data["unique_gift_colors"] = de_json_optional( + data.get("unique_gift_colors"), UniqueGiftColors, bot + ) + return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_checklists.py b/src/telegram/_checklists.py index cb509adb784..dc4012c884c 100644 --- a/src/telegram/_checklists.py +++ b/src/telegram/_checklists.py @@ -22,6 +22,7 @@ from collections.abc import Sequence from typing import TYPE_CHECKING, Optional +from telegram._chat import Chat from telegram._messageentity import MessageEntity from telegram._telegramobject import TelegramObject from telegram._user import User @@ -51,6 +52,10 @@ class ChecklistTask(TelegramObject): entities that appear in the task text. completed_by_user (:class:`telegram.User`, optional): User that completed the task; omitted if the task wasn't completed + completed_by_chat (:class:`telegram.Chat`, optional): Chat that completed the task; omitted + if the task wasn't completed by a chat + + .. versionadded:: NEXT.VERSION completion_date (:class:`datetime.datetime`, optional): Point in time when the task was completed; :attr:`~telegram.constants.ZERO_DATE` if the task wasn't completed @@ -64,6 +69,10 @@ class ChecklistTask(TelegramObject): entities that appear in the task text. completed_by_user (:class:`telegram.User`): Optional. User that completed the task; omitted if the task wasn't completed + completed_by_chat (:class:`telegram.Chat`): Optional. Chat that completed the task; omitted + if the task wasn't completed by a chat + + .. versionadded:: NEXT.VERSION completion_date (:class:`datetime.datetime`): Optional. Point in time when the task was completed; :attr:`~telegram.constants.ZERO_DATE` if the task wasn't completed @@ -72,6 +81,7 @@ class ChecklistTask(TelegramObject): """ __slots__ = ( + "completed_by_chat", "completed_by_user", "completion_date", "id", @@ -86,6 +96,7 @@ def __init__( text_entities: Sequence[MessageEntity] | None = None, completed_by_user: User | None = None, completion_date: dtm.datetime | None = None, + completed_by_chat: Chat | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -94,6 +105,7 @@ def __init__( self.text: str = text self.text_entities: tuple[MessageEntity, ...] = parse_sequence_arg(text_entities) self.completed_by_user: User | None = completed_by_user + self.completed_by_chat: Chat | None = completed_by_chat self.completion_date: dtm.datetime | None = completion_date self._id_attrs = (self.id,) @@ -114,6 +126,7 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChecklistTask" data["completion_date"] = from_timestamp(date, tzinfo=loc_tzinfo) data["completed_by_user"] = de_json_optional(data.get("completed_by_user"), User, bot) + data["completed_by_chat"] = de_json_optional(data.get("completed_by_chat"), Chat, bot) data["text_entities"] = de_list_optional(data.get("text_entities"), MessageEntity, bot) return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_forumtopic.py b/src/telegram/_forumtopic.py index b648b09679d..7c787ca8c60 100644 --- a/src/telegram/_forumtopic.py +++ b/src/telegram/_forumtopic.py @@ -38,6 +38,10 @@ class ForumTopic(TelegramObject): icon_color (:obj:`int`): Color of the topic icon in RGB format icon_custom_emoji_id (:obj:`str`, optional): Unique identifier of the custom emoji shown as the topic icon. + is_name_implicit (:obj:`bool`, optional): :obj:`True`, if the name of the topic wasn't + specified explicitly by its creator and likely needs to be changed by the bot. + + .. versionadded:: NEXT.VERSION Attributes: message_thread_id (:obj:`int`): Unique identifier of the forum topic @@ -45,9 +49,19 @@ class ForumTopic(TelegramObject): icon_color (:obj:`int`): Color of the topic icon in RGB format icon_custom_emoji_id (:obj:`str`): Optional. Unique identifier of the custom emoji shown as the topic icon. + is_name_implicit (:obj:`bool`): Optional. :obj:`True`, if the name of the topic wasn't + specified explicitly by its creator and likely needs to be changed by the bot. + + .. versionadded:: NEXT.VERSION """ - __slots__ = ("icon_color", "icon_custom_emoji_id", "message_thread_id", "name") + __slots__ = ( + "icon_color", + "icon_custom_emoji_id", + "is_name_implicit", + "message_thread_id", + "name", + ) def __init__( self, @@ -55,6 +69,7 @@ def __init__( name: str, icon_color: int, icon_custom_emoji_id: str | None = None, + is_name_implicit: bool | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -63,6 +78,7 @@ def __init__( self.name: str = name self.icon_color: int = icon_color self.icon_custom_emoji_id: str | None = icon_custom_emoji_id + self.is_name_implicit: bool | None = is_name_implicit self._id_attrs = (self.message_thread_id, self.name, self.icon_color) @@ -84,21 +100,30 @@ class ForumTopicCreated(TelegramObject): icon_color (:obj:`int`): Color of the topic icon in RGB format icon_custom_emoji_id (:obj:`str`, optional): Unique identifier of the custom emoji shown as the topic icon. + is_name_implicit (:obj:`bool`, optional): :obj:`True`, if the name of the topic wasn't + specified explicitly by its creator and likely needs to be changed by the bot. + + .. versionadded:: NEXT.VERSION Attributes: name (:obj:`str`): Name of the topic icon_color (:obj:`int`): Color of the topic icon in RGB format icon_custom_emoji_id (:obj:`str`): Optional. Unique identifier of the custom emoji shown as the topic icon. + is_name_implicit (:obj:`bool`): Optional. :obj:`True`, if the name of the topic wasn't + specified explicitly by its creator and likely needs to be changed by the bot. + + .. versionadded:: NEXT.VERSION """ - __slots__ = ("icon_color", "icon_custom_emoji_id", "name") + __slots__ = ("icon_color", "icon_custom_emoji_id", "is_name_implicit", "name") def __init__( self, name: str, icon_color: int, icon_custom_emoji_id: str | None = None, + is_name_implicit: bool | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -106,6 +131,7 @@ def __init__( self.name: str = name self.icon_color: int = icon_color self.icon_custom_emoji_id: str | None = icon_custom_emoji_id + self.is_name_implicit: bool | None = is_name_implicit self._id_attrs = (self.name, self.icon_color) diff --git a/src/telegram/_gifts.py b/src/telegram/_gifts.py index dd8fb1fb345..f704fc1bc13 100644 --- a/src/telegram/_gifts.py +++ b/src/telegram/_gifts.py @@ -34,6 +34,55 @@ from telegram import Bot +class GiftBackground(TelegramObject): + """This object describes the background of a gift. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal if their :attr:`center_color`, :attr:`edge_color` and :attr:`text_color` are + equal. + + .. versionadded:: NEXT.VERSION + + Args: + center_color (:obj:`int`): Center color of the background in RGB format. + edge_color (:obj:`int`): Edge color of the background in RGB format. + text_color (:obj:`int`): Text color of the background in RGB format. + + Attributes: + center_color (:obj:`int`): Center color of the background in RGB format. + edge_color (:obj:`int`): Edge color of the background in RGB format. + text_color (:obj:`int`): Text color of the background in RGB format. + + """ + + __slots__ = ( + "center_color", + "edge_color", + "text_color", + ) + + def __init__( + self, + center_color: int, + edge_color: int, + text_color: int, + *, + api_kwargs: JSONDict | None = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.center_color: int = center_color + self.edge_color: int = edge_color + self.text_color: int = text_color + + self._id_attrs = ( + self.center_color, + self.edge_color, + self.text_color, + ) + + self._freeze() + + class Gift(TelegramObject): """This object represents a gift that can be sent by the bot. @@ -59,6 +108,29 @@ class Gift(TelegramObject): published the gift. .. versionadded:: 22.4 + personal_total_count (:obj:`int`, optional): The total number of gifts of this type that + can be sent by the bot; for limited gifts only. + + .. versionadded:: NEXT.VERSION + personal_remaining_count (:obj:`int`, optional): The number of remaining gifts of this type + that can be sent by the bot; for limited gifts only. + + .. versionadded:: NEXT.VERSION + background (:class:`GiftBackground`, optional): Background of the gift. + + .. versionadded:: NEXT.VERSION + is_premium (:obj:`bool`, optional): :obj:`True`, if the gift can only be purchased by + Telegram Premium subscribers. + + .. versionadded:: NEXT.VERSION + has_colors (:obj:`bool`, optional): :obj:`True`, if the gift can be used (after being + upgraded) to customize a user's appearance. + + .. versionadded:: NEXT.VERSION + unique_gift_variant_count (:obj:`int`, optional): The total number of different unique + gifts that can be obtained by upgrading the gift. + + .. versionadded:: NEXT.VERSION Attributes: id (:obj:`str`): Unique identifier of the gift. @@ -77,16 +149,45 @@ class Gift(TelegramObject): published the gift. .. versionadded:: 22.4 + personal_total_count (:obj:`int`): Optional. The total number of gifts of this type that + can be sent by the bot; for limited gifts only. + + .. versionadded:: NEXT.VERSION + personal_remaining_count (:obj:`int`): Optional. The number of remaining gifts of this type + that can be sent by the bot; for limited gifts only. + + .. versionadded:: NEXT.VERSION + background (:class:`GiftBackground`): Optional. Background of the gift. + + .. versionadded:: NEXT.VERSION + is_premium (:obj:`bool`): Optional. :obj:`True`, if the gift can only be purchased by + Telegram Premium subscribers. + + .. versionadded:: NEXT.VERSION + has_colors (:obj:`bool`): Optional. :obj:`True`, if the gift can be used (after being + upgraded) to customize a user's appearance. + + .. versionadded:: NEXT.VERSION + unique_gift_variant_count (:obj:`int`): Optional. The total number of different unique + gifts that can be obtained by upgrading the gift. + + .. versionadded:: NEXT.VERSION """ __slots__ = ( + "background", + "has_colors", "id", + "is_premium", + "personal_remaining_count", + "personal_total_count", "publisher_chat", "remaining_count", "star_count", "sticker", "total_count", + "unique_gift_variant_count", "upgrade_star_count", ) @@ -99,6 +200,12 @@ def __init__( remaining_count: int | None = None, upgrade_star_count: int | None = None, publisher_chat: Chat | None = None, + personal_total_count: int | None = None, + personal_remaining_count: int | None = None, + background: GiftBackground | None = None, + is_premium: bool | None = None, + has_colors: bool | None = None, + unique_gift_variant_count: int | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -110,6 +217,12 @@ def __init__( self.remaining_count: int | None = remaining_count self.upgrade_star_count: int | None = upgrade_star_count self.publisher_chat: Chat | None = publisher_chat + self.personal_total_count: int | None = personal_total_count + self.personal_remaining_count: int | None = personal_remaining_count + self.background: GiftBackground | None = background + self.is_premium: bool | None = is_premium + self.has_colors: bool | None = has_colors + self.unique_gift_variant_count: int | None = unique_gift_variant_count self._id_attrs = (self.id,) @@ -122,6 +235,7 @@ def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Gift": data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot) data["publisher_chat"] = de_json_optional(data.get("publisher_chat"), Chat, bot) + data["background"] = de_json_optional(data.get("background"), GiftBackground, bot) return super().de_json(data=data, bot=bot) @@ -189,6 +303,14 @@ class GiftInfo(TelegramObject): appear in the text. is_private (:obj:`bool`, optional): :obj:`True`, if the sender and gift text are shown only to the gift receiver; otherwise, everyone will be able to see them. + is_upgrade_separate (:obj:`bool`, optional): :obj:`True`, if the gift's upgrade was + purchased after the gift was sent. + + .. versionadded:: NEXT.VERSION + unique_gift_number (:obj:`int`, optional): Unique number reserved for this gift when + upgraded. See the number field in :class:`~telegram.UniqueGift`. + + .. versionadded:: NEXT.VERSION Attributes: gift (:class:`Gift`): Information about the gift. @@ -206,6 +328,14 @@ class GiftInfo(TelegramObject): appear in the text. is_private (:obj:`bool`): Optional. :obj:`True`, if the sender and gift text are shown only to the gift receiver; otherwise, everyone will be able to see them. + is_upgrade_separate (:obj:`bool`): Optional. :obj:`True`, if the gift's upgrade was + purchased after the gift was sent. + + .. versionadded:: NEXT.VERSION + unique_gift_number (:obj:`int`): Optional. Unique number reserved for this gift when + upgraded. See the number field in :class:`~telegram.UniqueGift`. + + .. versionadded:: NEXT.VERSION """ @@ -215,9 +345,11 @@ class GiftInfo(TelegramObject): "entities", "gift", "is_private", + "is_upgrade_separate", "owned_gift_id", "prepaid_upgrade_star_count", "text", + "unique_gift_number", ) def __init__( @@ -230,6 +362,8 @@ def __init__( text: str | None = None, entities: Sequence[MessageEntity] | None = None, is_private: bool | None = None, + unique_gift_number: int | None = None, + is_upgrade_separate: bool | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -244,6 +378,8 @@ def __init__( self.text: str | None = text self.entities: tuple[MessageEntity, ...] = parse_sequence_arg(entities) self.is_private: bool | None = is_private + self.unique_gift_number: int | None = unique_gift_number + self.is_upgrade_separate: bool | None = is_upgrade_separate self._id_attrs = (self.gift,) @@ -319,9 +455,11 @@ class AcceptedGiftTypes(TelegramObject): Objects of this class are comparable in terms of equality. Two objects of this class are considered equal if their :attr:`unlimited_gifts`, :attr:`limited_gifts`, - :attr:`unique_gifts` and :attr:`premium_subscription` are equal. + :attr:`unique_gifts`, :attr:`premium_subscription` and :attr:`gifts_from_channels` are equal. .. versionadded:: 22.1 + .. versionchanged:: NEXT.VERSION + :attr:`gifts_from_channels` is now considered for equality checks. Args: unlimited_gifts (:class:`bool`): :obj:`True`, if unlimited regular gifts are accepted. @@ -330,6 +468,10 @@ class AcceptedGiftTypes(TelegramObject): to unique for free are accepted. premium_subscription (:class:`bool`): :obj:`True`, if a Telegram Premium subscription is accepted. + gifts_from_channels (:obj:`bool`): :obj:`True`, if transfers of unique gifts from channels + are accepted + + .. versionadded:: NEXT.VERSION Attributes: unlimited_gifts (:class:`bool`): :obj:`True`, if unlimited regular gifts are accepted. @@ -338,10 +480,15 @@ class AcceptedGiftTypes(TelegramObject): to unique for free are accepted. premium_subscription (:class:`bool`): :obj:`True`, if a Telegram Premium subscription is accepted. + gifts_from_channels (:obj:`bool`): :obj:`True`, if transfers of unique gifts from channels + are accepted + + .. versionadded:: NEXT.VERSION """ __slots__ = ( + "gifts_from_channels", "limited_gifts", "premium_subscription", "unique_gifts", @@ -354,6 +501,7 @@ def __init__( limited_gifts: bool, unique_gifts: bool, premium_subscription: bool, + gifts_from_channels: bool, *, api_kwargs: JSONDict | None = None, ): @@ -362,12 +510,14 @@ def __init__( self.limited_gifts: bool = limited_gifts self.unique_gifts: bool = unique_gifts self.premium_subscription: bool = premium_subscription + self.gifts_from_channels: bool = gifts_from_channels self._id_attrs = ( self.unlimited_gifts, self.limited_gifts, self.unique_gifts, self.premium_subscription, + self.gifts_from_channels, ) self._freeze() diff --git a/src/telegram/_message.py b/src/telegram/_message.py index bccb270898e..7ddeee1336c 100644 --- a/src/telegram/_message.py +++ b/src/telegram/_message.py @@ -496,12 +496,12 @@ class Message(MaybeInaccessibleMessage): reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. :paramref:`~telegram.InlineKeyboardButton.login_url` buttons are represented as ordinary url buttons. - is_topic_message (:obj:`bool`, optional): :obj:`True`, if the message is sent to a forum - topic. + is_topic_message (:obj:`bool`, optional): :obj:`True`, if the message is sent to a topic + in a forum supergroup or a private chat with the bot. .. versionadded:: 20.0 - message_thread_id (:obj:`int`, optional): Unique identifier of a message thread to which - the message belongs; for supergroups only. + message_thread_id (:obj:`int`, optional): Unique identifier of a message thread or forum + topic to which the message belongs; for supergroups and private chats only. .. versionadded:: 20.0 forum_topic_created (:class:`telegram.ForumTopicCreated`, optional): Service message: @@ -558,6 +558,10 @@ class Message(MaybeInaccessibleMessage): was sent or received .. versionadded:: 22.1 + gift_upgrade_sent (:class:`telegram.GiftInfo`, optional): Service message: upgrade of a + gift was purchased after the gift was sent + + .. versionadded:: NEXT.VERSION giveaway_created (:class:`telegram.GiveawayCreated`, optional): Service message: a scheduled giveaway was created @@ -898,12 +902,12 @@ class Message(MaybeInaccessibleMessage): reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. :paramref:`~telegram.InlineKeyboardButton.login_url` buttons are represented as ordinary url buttons. - is_topic_message (:obj:`bool`): Optional. :obj:`True`, if the message is sent to a forum - topic. + is_topic_message (:obj:`bool`): Optional. :obj:`True`, if the message is sent to a topic + in a forum supergroup or a private chat with the bot. .. versionadded:: 20.0 - message_thread_id (:obj:`int`): Optional. Unique identifier of a message thread to which - the message belongs; for supergroups only. + message_thread_id (:obj:`int`): Optional. Unique identifier of a message thread or forum + topic to which the message belongs; for supergroups and private chats only. .. versionadded:: 20.0 forum_topic_created (:class:`telegram.ForumTopicCreated`): Optional. Service message: @@ -957,6 +961,10 @@ class Message(MaybeInaccessibleMessage): was sent or received .. versionadded:: 22.1 + gift_upgrade_sent (:class:`telegram.GiftInfo`): Optional. Service message: upgrade of a + gift was purchased after the gift was sent + + .. versionadded:: NEXT.VERSION giveaway_created (:class:`telegram.GiveawayCreated`): Optional. Service message: a scheduled giveaway was created @@ -1125,6 +1133,7 @@ class Message(MaybeInaccessibleMessage): "general_forum_topic_hidden", "general_forum_topic_unhidden", "gift", + "gift_upgrade_sent", "giveaway", "giveaway_completed", "giveaway_created", @@ -1296,6 +1305,7 @@ def __init__( suggested_post_info: "SuggestedPostInfo | None" = None, suggested_post_approved: "SuggestedPostApproved | None" = None, suggested_post_approval_failed: "SuggestedPostApprovalFailed | None" = None, + gift_upgrade_sent: GiftInfo | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -1422,6 +1432,7 @@ def __init__( self.suggested_post_approval_failed: SuggestedPostApprovalFailed | None = ( suggested_post_approval_failed ) + self.gift_upgrade_sent: GiftInfo | None = gift_upgrade_sent self._effective_attachment = DEFAULT_NONE @@ -1637,6 +1648,7 @@ def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Message": data["suggested_post_approval_failed"] = de_json_optional( data.get("suggested_post_approval_failed"), SuggestedPostApprovalFailed, bot ) + data["gift_upgrade_sent"] = de_json_optional(data.get("gift_upgrade_sent"), GiftInfo, bot) api_kwargs = {} # This is a deprecated field that TG still returns for backwards compatibility @@ -1982,9 +1994,9 @@ def _parse_message_thread_id( return message_thread_id # self.message_thread_id can be used for send_*.param.message_thread_id only if the - # thread is a forum topic. It does not work if the thread is a chain of replies to a - # message in a normal group. In that case, self.message_thread_id is just the message_id - # of the first message in the chain. + # thread is a forum topic (in supergroups or private chats). It does not work if the + # thread is a chain of replies to a message in a normal group. In that case, + # self.message_thread_id is just the message_id of the first message in the chain. if not self.is_topic_message: return None @@ -2077,6 +2089,55 @@ async def reply_text( suggested_post_parameters=suggested_post_parameters, ) + async def reply_text_draft( + self, + draft_id: int, + text: str, + parse_mode: ODVInput[str] = DEFAULT_NONE, + entities: Sequence["MessageEntity"] | None = None, + message_thread_id: ODVInput[int] = DEFAULT_NONE, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> bool: + """Shortcut for:: + + await bot.send_message_draft( + update.effective_message.chat_id, + message_thread_id=update.effective_message.message_thread_id, + *args, + **kwargs, + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message_draft`. + + Note: + |reply_same_thread| + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + message_thread_id = self._parse_message_thread_id(self.chat_id, message_thread_id) + return await self.get_bot().send_message_draft( + chat_id=self.chat_id, + draft_id=draft_id, + text=text, + parse_mode=parse_mode, + entities=entities, + message_thread_id=message_thread_id, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def reply_markdown( self, text: str, @@ -3822,6 +3883,7 @@ async def forward( message_thread_id: int | None = None, video_start_timestamp: int | None = None, suggested_post_parameters: "SuggestedPostParameters | None" = None, + message_effect_id: str | None = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -3868,6 +3930,7 @@ async def forward( pool_timeout=pool_timeout, api_kwargs=api_kwargs, direct_messages_topic_id=self._extract_direct_messages_topic_id(), + message_effect_id=message_effect_id, ) async def copy( @@ -3885,6 +3948,7 @@ async def copy( allow_paid_broadcast: bool | None = None, video_start_timestamp: int | None = None, suggested_post_parameters: "SuggestedPostParameters | None" = None, + message_effect_id: str | None = None, *, reply_to_message_id: int | None = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -3935,6 +3999,7 @@ async def copy( allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), suggested_post_parameters=suggested_post_parameters, + message_effect_id=message_effect_id, ) async def reply_copy( @@ -3953,6 +4018,7 @@ async def reply_copy( allow_paid_broadcast: bool | None = None, video_start_timestamp: int | None = None, suggested_post_parameters: "SuggestedPostParameters | None" = None, + message_effect_id: str | None = None, *, reply_to_message_id: int | None = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -4017,6 +4083,7 @@ async def reply_copy( allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=self._extract_direct_messages_topic_id(), suggested_post_parameters=suggested_post_parameters, + message_effect_id=message_effect_id, ) async def reply_paid_media( diff --git a/src/telegram/_ownedgift.py b/src/telegram/_ownedgift.py index 0f1d0155e97..3011a966b17 100644 --- a/src/telegram/_ownedgift.py +++ b/src/telegram/_ownedgift.py @@ -186,6 +186,14 @@ class OwnedGiftRegular(OwnedGift): to Telegram Stars. prepaid_upgrade_star_count (:obj:`int`, optional): Number of Telegram Stars that were paid by the sender for the ability to upgrade the gift. + is_upgrade_separate (:obj:`bool`, optional): :obj:`True`, if the gift's upgrade was + purchased after the gift was sent; for gifts received on behalf of business accounts + + .. versionadded:: NEXT.VERSION + unique_gift_number (:obj:`int`, optional): Unique number reserved for this gift when + upgraded. See the number field in :class:`~telegram.UniqueGift` + + ... versionadded:: NEXT.VERSION Attributes: type (:obj:`str`): Type of the gift, always :attr:`~telegram.OwnedGift.REGULAR`. @@ -211,6 +219,14 @@ class OwnedGiftRegular(OwnedGift): to Telegram Stars. prepaid_upgrade_star_count (:obj:`int`): Optional. Number of Telegram Stars that were paid by the sender for the ability to upgrade the gift. + is_upgrade_separate (:obj:`bool`): Optional. :obj:`True`, if the gift's upgrade was + purchased after the gift was sent; for gifts received on behalf of business accounts + + .. versionadded:: NEXT.VERSION + unique_gift_number (:obj:`int`): Optional. Unique number reserved for this gift when + upgraded. See the number field in :class:`~telegram.UniqueGift` + + ... versionadded:: NEXT.VERSION """ @@ -221,11 +237,13 @@ class OwnedGiftRegular(OwnedGift): "gift", "is_private", "is_saved", + "is_upgrade_separate", "owned_gift_id", "prepaid_upgrade_star_count", "send_date", "sender_user", "text", + "unique_gift_number", "was_refunded", ) @@ -243,6 +261,8 @@ def __init__( was_refunded: bool | None = None, convert_star_count: int | None = None, prepaid_upgrade_star_count: int | None = None, + is_upgrade_separate: bool | None = None, + unique_gift_number: int | None = None, *, api_kwargs: JSONDict | None = None, ) -> None: @@ -261,6 +281,8 @@ def __init__( self.was_refunded: bool | None = was_refunded self.convert_star_count: int | None = convert_star_count self.prepaid_upgrade_star_count: int | None = prepaid_upgrade_star_count + self.is_upgrade_separate: bool | None = is_upgrade_separate + self.unique_gift_number: int | None = unique_gift_number self._id_attrs = (self.type, self.gift, self.send_date) diff --git a/src/telegram/_story.py b/src/telegram/_story.py index 1e3fb3d1ca1..1f1be6946cb 100644 --- a/src/telegram/_story.py +++ b/src/telegram/_story.py @@ -22,7 +22,8 @@ from telegram._chat import Chat from telegram._telegramobject import TelegramObject -from telegram._utils.types import JSONDict +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram._utils.types import JSONDict, ODVInput if TYPE_CHECKING: from telegram import Bot @@ -77,3 +78,46 @@ def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "Story": data["chat"] = Chat.de_json(data.get("chat", {}), bot) return super().de_json(data=data, bot=bot) + + async def repost( + self, + business_connection_id: str, + active_period: int, + post_to_chat_page: bool | None = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> "Story": + """Shortcut for:: + + await bot.repost_story( + from_chat_id=story.chat.id, + from_story_id=story.id, + *args, **kwargs + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.repost_story`. + + .. versionadded:: NEXT.VERSION + + Returns: + :class:`Story`: On success, :class:`Story` is returned. + + """ + return await self.get_bot().repost_story( + business_connection_id=business_connection_id, + from_chat_id=self.chat.id, + from_story_id=self.id, + active_period=active_period, + post_to_chat_page=post_to_chat_page, + protect_content=protect_content, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) diff --git a/src/telegram/_uniquegift.py b/src/telegram/_uniquegift.py index 4e48a8febed..860bc1e57af 100644 --- a/src/telegram/_uniquegift.py +++ b/src/telegram/_uniquegift.py @@ -27,7 +27,7 @@ from telegram._files.sticker import Sticker from telegram._telegramobject import TelegramObject from telegram._utils import enum -from telegram._utils.argumentparsing import de_json_optional +from telegram._utils.argumentparsing import de_json_optional, parse_sequence_arg from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp from telegram._utils.types import JSONDict @@ -35,6 +35,80 @@ from telegram import Bot +class UniqueGiftColors(TelegramObject): + """This object contains information about the color scheme for a user's name, message replies + and link previews based on a unique gift. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal if their :attr:`model_custom_emoji_id`, :attr:`symbol_custom_emoji_id`, + :attr:`light_theme_main_color`, :attr:`light_theme_other_colors`, + :attr:`dark_theme_main_color`, and :attr:`dark_theme_other_colors` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + model_custom_emoji_id (:obj:`str`): Custom emoji identifier of the unique gift's model. + symbol_custom_emoji_id (:obj:`str`): Custom emoji identifier of the unique gift's symbol. + light_theme_main_color (:obj:`int`): Main color used in light themes; RGB format. + light_theme_other_colors (Sequence[:obj:`int`]): List of 1-3 additional colors used in + light themes; RGB format. |sequenceclassargs| + dark_theme_main_color (:obj:`int`): Main color used in dark themes; RGB format. + dark_theme_other_colors (Sequence[:obj:`int`]): List of 1-3 additional colors used in dark + themes; RGB format. |sequenceclassargs| + + Attributes: + model_custom_emoji_id (:obj:`str`): Custom emoji identifier of the unique gift's model. + symbol_custom_emoji_id (:obj:`str`): Custom emoji identifier of the unique gift's symbol. + light_theme_main_color (:obj:`int`): Main color used in light themes; RGB format. + light_theme_other_colors (Tuple[:obj:`int`]): Tuple of 1-3 additional colors used in + light themes; RGB format. + dark_theme_main_color (:obj:`int`): Main color used in dark themes; RGB format. + dark_theme_other_colors (Tuple[:obj:`int`]): Tuple of 1-3 additional colors used in dark + themes; RGB format. + """ + + __slots__ = ( + "dark_theme_main_color", + "dark_theme_other_colors", + "light_theme_main_color", + "light_theme_other_colors", + "model_custom_emoji_id", + "symbol_custom_emoji_id", + ) + + def __init__( + self, + model_custom_emoji_id: str, + symbol_custom_emoji_id: str, + light_theme_main_color: int, + light_theme_other_colors: list[int], + dark_theme_main_color: int, + dark_theme_other_colors: list[int], + *, + api_kwargs: JSONDict | None = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.model_custom_emoji_id: str = model_custom_emoji_id + self.symbol_custom_emoji_id: str = symbol_custom_emoji_id + self.light_theme_main_color: int = light_theme_main_color + self.light_theme_other_colors: tuple[int, ...] = parse_sequence_arg( + light_theme_other_colors + ) + self.dark_theme_main_color: int = dark_theme_main_color + self.dark_theme_other_colors: tuple[int, ...] = parse_sequence_arg(dark_theme_other_colors) + + self._id_attrs = ( + self.model_custom_emoji_id, + self.symbol_custom_emoji_id, + self.light_theme_main_color, + self.light_theme_other_colors, + self.dark_theme_main_color, + self.dark_theme_other_colors, + ) + + self._freeze() + + class UniqueGiftModel(TelegramObject): """This object describes the model of a unique gift. @@ -260,6 +334,9 @@ class UniqueGift(TelegramObject): .. versionadded:: 22.1 Args: + gift_id (:obj:`str`): Identifier of the regular gift from which the gift was upgraded. + + .. versionadded:: NEXT.VERSION base_name (:obj:`str`): Human-readable name of the regular gift from which this unique gift was upgraded. name (:obj:`str`): Unique name of the gift. This name can be used @@ -273,8 +350,24 @@ class UniqueGift(TelegramObject): published the gift. .. versionadded:: 22.4 + is_premium (:obj:`bool`, optional): :obj:`True`, if the original regular gift was + exclusively purchaseable by Telegram Premium subscribers. + + .. versionadded:: NEXT.VERSION + is_from_blockchain (:obj:`bool`, optional): :obj:`True`, if the gift is assigned from the + TON blockchain and can't be resold or transferred in Telegram. + + .. versionadded:: NEXT.VERSION + colors (:class:`telegram.UniqueGiftColors`, optional): The color scheme that can be used + by the gift's owner for the chat's name, replies to messages and link previews; for + business account gifts and gifts that are currently on sale only. + + .. versionadded:: NEXT.VERSION Attributes: + gift_id (:obj:`str`): Identifier of the regular gift from which the gift was upgraded. + + .. versionadded:: NEXT.VERSION base_name (:obj:`str`): Human-readable name of the regular gift from which this unique gift was upgraded. name (:obj:`str`): Unique name of the gift. This name can be used @@ -288,12 +381,29 @@ class UniqueGift(TelegramObject): published the gift. .. versionadded:: 22.4 + is_premium (:obj:`bool`): Optional. :obj:`True`, if the original regular gift was + exclusively purchaseable by Telegram Premium subscribers. + + .. versionadded:: NEXT.VERSION + is_from_blockchain (:obj:`bool`): Optional. :obj:`True`, if the gift is assigned from the + TON blockchain and can't be resold or transferred in Telegram. + + .. versionadded:: NEXT.VERSION + colors (:class:`telegram.UniqueGiftColors`): Optional. The color scheme that can be used + by the gift's owner for the chat's name, replies to messages and link previews; for + business account gifts and gifts that are currently on sale only. + + .. versionadded:: NEXT.VERSION """ __slots__ = ( "backdrop", "base_name", + "colors", + "gift_id", + "is_from_blockchain", + "is_premium", "model", "name", "number", @@ -310,10 +420,21 @@ def __init__( symbol: UniqueGiftSymbol, backdrop: UniqueGiftBackdrop, publisher_chat: Chat | None = None, + # tags: deprecated NEXT.VERSION, bot api 9.3 + # temporarily optional to account for changed signature + gift_id: str | None = None, + is_from_blockchain: bool | None = None, + is_premium: bool | None = None, + colors: UniqueGiftColors | None = None, *, api_kwargs: JSONDict | None = None, ): + # tags: deprecated NEXT.VERSION, bot api 9.3 + if gift_id is None: + raise TypeError("`gift_id` is a required argument since Bot API 9.3") + super().__init__(api_kwargs=api_kwargs) + self.gift_id: str = gift_id self.base_name: str = base_name self.name: str = name self.number: int = number @@ -321,6 +442,9 @@ def __init__( self.symbol: UniqueGiftSymbol = symbol self.backdrop: UniqueGiftBackdrop = backdrop self.publisher_chat: Chat | None = publisher_chat + self.is_from_blockchain: bool | None = is_from_blockchain + self.is_premium: bool | None = is_premium + self.colors: UniqueGiftColors | None = colors self._id_attrs = ( self.base_name, @@ -342,6 +466,7 @@ def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "UniqueGift": data["symbol"] = de_json_optional(data.get("symbol"), UniqueGiftSymbol, bot) data["backdrop"] = de_json_optional(data.get("backdrop"), UniqueGiftBackdrop, bot) data["publisher_chat"] = de_json_optional(data.get("publisher_chat"), Chat, bot) + data["colors"] = de_json_optional(data.get("colors"), UniqueGiftColors, bot) return super().de_json(data=data, bot=bot) diff --git a/src/telegram/_user.py b/src/telegram/_user.py index 88597074416..e6cc62151a4 100644 --- a/src/telegram/_user.py +++ b/src/telegram/_user.py @@ -56,9 +56,11 @@ Message, MessageEntity, MessageId, + OwnedGifts, PhotoSize, ReplyParameters, Sticker, + Story, SuggestedPostParameters, UserChatBoosts, UserProfilePhotos, @@ -112,6 +114,10 @@ class User(TelegramObject): Returned only in :meth:`telegram.Bot.get_me`. .. versionadded:: 21.5 + has_topics_enabled (:obj:`bool`, optional): :obj:`True`, if the bot has forum topic mode + enabled in private chats. Returned only in :meth:`telegram.Bot.get_me`. + + .. versionadded:: NEXT.VERSION Attributes: id (:obj:`int`): Unique identifier for this user or bot. @@ -143,6 +149,10 @@ class User(TelegramObject): Returned only in :meth:`telegram.Bot.get_me`. .. versionadded:: 21.5 + has_topics_enabled (:obj:`bool`): Optional. :obj:`True`, if the bot has forum topic mode + enabled in private chats. Returned only in :meth:`telegram.Bot.get_me`. + + .. versionadded:: NEXT.VERSION .. |user_chat_id_note| replace:: This shortcuts build on the assumption that :attr:`User.id` coincides with the :attr:`Chat.id` of the private chat with the user. This has been the @@ -156,6 +166,7 @@ class User(TelegramObject): "can_read_all_group_messages", "first_name", "has_main_web_app", + "has_topics_enabled", "id", "is_bot", "is_premium", @@ -180,6 +191,7 @@ def __init__( added_to_attachment_menu: bool | None = None, can_connect_to_business: bool | None = None, has_main_web_app: bool | None = None, + has_topics_enabled: bool | None = None, *, api_kwargs: JSONDict | None = None, ): @@ -199,6 +211,7 @@ def __init__( self.added_to_attachment_menu: bool | None = added_to_attachment_menu self.can_connect_to_business: bool | None = can_connect_to_business self.has_main_web_app: bool | None = has_main_web_app + self.has_topics_enabled: bool | None = has_topics_enabled self._id_attrs = (self.id,) @@ -486,6 +499,49 @@ async def send_message( suggested_post_parameters=suggested_post_parameters, ) + async def send_message_draft( + self, + draft_id: int, + text: str, + message_thread_id: int | None = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + entities: Sequence["MessageEntity"] | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> bool: + """Shortcut for:: + + await bot.send_message_draft(update.effective_user.id, *args, **kwargs) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_message_draft`. + + Note: + |user_chat_id_note| + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + """ + return await self.get_bot().send_message_draft( + chat_id=self.id, + draft_id=draft_id, + text=text, + message_thread_id=message_thread_id, + parse_mode=parse_mode, + entities=entities, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def delete_message( self, message_id: int, @@ -1812,6 +1868,7 @@ async def send_copy( video_start_timestamp: int | None = None, direct_messages_topic_id: int | None = None, suggested_post_parameters: "SuggestedPostParameters | None" = None, + message_effect_id: str | None = None, *, reply_to_message_id: int | None = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1858,6 +1915,7 @@ async def send_copy( allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, suggested_post_parameters=suggested_post_parameters, + message_effect_id=message_effect_id, ) async def copy_message( @@ -1877,6 +1935,7 @@ async def copy_message( video_start_timestamp: int | None = None, direct_messages_topic_id: int | None = None, suggested_post_parameters: "SuggestedPostParameters | None" = None, + message_effect_id: str | None = None, *, reply_to_message_id: int | None = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -1923,6 +1982,7 @@ async def copy_message( allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, suggested_post_parameters=suggested_post_parameters, + message_effect_id=message_effect_id, ) async def send_copies( @@ -2029,6 +2089,7 @@ async def forward_from( video_start_timestamp: int | None = None, direct_messages_topic_id: int | None = None, suggested_post_parameters: "SuggestedPostParameters | None" = None, + message_effect_id: str | None = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2065,6 +2126,7 @@ async def forward_from( message_thread_id=message_thread_id, direct_messages_topic_id=direct_messages_topic_id, suggested_post_parameters=suggested_post_parameters, + message_effect_id=message_effect_id, ) async def forward_to( @@ -2077,6 +2139,7 @@ async def forward_to( video_start_timestamp: int | None = None, direct_messages_topic_id: int | None = None, suggested_post_parameters: "SuggestedPostParameters | None" = None, + message_effect_id: str | None = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -2114,6 +2177,7 @@ async def forward_to( message_thread_id=message_thread_id, direct_messages_topic_id=direct_messages_topic_id, suggested_post_parameters=suggested_post_parameters, + message_effect_id=message_effect_id, ) async def forward_messages_from( @@ -2475,3 +2539,92 @@ async def remove_verification( pool_timeout=pool_timeout, api_kwargs=api_kwargs, ) + + async def repost_story( + self, + business_connection_id: str, + from_story_id: int, + active_period: int, + post_to_chat_page: bool | None = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> "Story": + """Shortcut for:: + + await bot.repost_story( + from_chat_id=update.effective_user.id, + *args, **kwargs + ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.repost_story`. + + .. versionadded:: NEXT.VERSION + + Returns: + :class:`Story`: On success, :class:`Story` is returned. + + """ + return await self.get_bot().repost_story( + business_connection_id=business_connection_id, + from_chat_id=self.id, + from_story_id=from_story_id, + active_period=active_period, + post_to_chat_page=post_to_chat_page, + protect_content=protect_content, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def get_gifts( + self, + exclude_unlimited: bool | None = None, + exclude_limited_upgradable: bool | None = None, + exclude_limited_non_upgradable: bool | None = None, + exclude_from_blockchain: bool | None = None, + exclude_unique: bool | None = None, + sort_by_price: bool | None = None, + offset: str | None = None, + limit: int | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + ) -> "OwnedGifts": + """Shortcut for:: + + await bot.get_user_gifts(user_id=update.effective_user.id) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.get_user_gifts`. + + .. versionadded:: NEXT.VERSION + + Returns: + :class:`telegram.OwnedGifts`: On success, returns the gifts owned by the user. + """ + return await self.get_bot().get_user_gifts( + user_id=self.id, + exclude_unlimited=exclude_unlimited, + exclude_limited_upgradable=exclude_limited_upgradable, + exclude_limited_non_upgradable=exclude_limited_non_upgradable, + exclude_from_blockchain=exclude_from_blockchain, + exclude_unique=exclude_unique, + sort_by_price=sort_by_price, + offset=offset, + limit=limit, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) diff --git a/src/telegram/_userrating.py b/src/telegram/_userrating.py new file mode 100644 index 00000000000..edfb4ffdea8 --- /dev/null +++ b/src/telegram/_userrating.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2026 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. +"""This module contains an object that represents a Telegram user rating.""" + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class UserRating(TelegramObject): + """ + This object describes the rating of a user based on their Telegram Star spendings. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`level` and :attr:`rating` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + level (:obj:`int`): Current level of the user, indicating their reliability when purchasing + digital goods and services. A higher level suggests a more trustworthy customer; a + negative level is likely reason for concern. + rating (:obj:`int`): Numerical value of the user's rating; the higher the rating, the + better + current_level_rating (:obj:`int`): The rating value required to get the current level + next_level_rating (:obj:`int`, optional): The rating value required to get to the next + level; omitted if the maximum level was reached + + Attributes: + level (:obj:`int`): Current level of the user, indicating their reliability when purchasing + digital goods and services. A higher level suggests a more trustworthy customer; a + negative level is likely reason for concern. + rating (:obj:`int`): Numerical value of the user's rating; the higher the rating, the + better + current_level_rating (:obj:`int`): The rating value required to get the current level + next_level_rating (:obj:`int`): Optional. The rating value required to get to the next + level; omitted if the maximum level was reached + + """ + + __slots__ = ("current_level_rating", "level", "next_level_rating", "rating") + + def __init__( + self, + level: int, + rating: int, + current_level_rating: int, + next_level_rating: int | None = None, + *, + api_kwargs: JSONDict | None = None, + ) -> None: + super().__init__(api_kwargs=api_kwargs) + self.level: int = level + self.rating: int = rating + self.current_level_rating: int = current_level_rating + self.next_level_rating: int | None = next_level_rating + + self._id_attrs = (self.level, self.rating) + + self._freeze() diff --git a/src/telegram/constants.py b/src/telegram/constants.py index a4628b930b7..5d8dd790dea 100644 --- a/src/telegram/constants.py +++ b/src/telegram/constants.py @@ -176,7 +176,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=9, minor=2) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=9, minor=3) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -761,13 +761,23 @@ class BusinessLimit(IntEnum): """ MIN_GIFT_RESULTS = 1 """:obj:`int`: Minimum number of gifts to be returned. Relevant for - :paramref:`~telegram.Bot.get_business_account_gifts.limit` of - :meth:`telegram.Bot.get_business_account_gifts`. + + * :paramref:`~telegram.Bot.get_business_account_gifts.limit` of + :meth:`telegram.Bot.get_business_account_gifts`. + * :paramref:`~telegram.Bot.get_chat_gifts.limit` of + :meth:`telegram.Bot.get_chat_gifts`. + * :paramref:`~telegram.Bot.get_user_gifts.limit` of + :meth:`telegram.Bot.get_user_gifts`. """ MAX_GIFT_RESULTS = 100 """:obj:`int`: Maximum number of gifts to be returned. Relevant for - :paramref:`~telegram.Bot.get_business_account_gifts.limit` of - :meth:`telegram.Bot.get_business_account_gifts`. + + * :paramref:`~telegram.Bot.get_business_account_gifts.limit` of + :meth:`telegram.Bot.get_business_account_gifts`. + * :paramref:`~telegram.Bot.get_chat_gifts.limit` of + :meth:`telegram.Bot.get_chat_gifts`. + * :paramref:`~telegram.Bot.get_user_gifts.limit` of + :meth:`telegram.Bot.get_user_gifts`. """ MIN_STAR_COUNT = 1 """:obj:`int`: Minimum number of Telegram Stars to be transfered. Relevant for @@ -2010,6 +2020,8 @@ class MessageLimit(IntEnum): * :paramref:`~telegram.Bot.send_message.text` parameter of :meth:`telegram.Bot.send_message` * :paramref:`~telegram.Bot.edit_message_text.text` parameter of :meth:`telegram.Bot.edit_message_text` + * :paramref:`~telegram.Bot.send_message_draft.text` parameter of + :meth:`telegram.Bot.send_message_draft` """ CAPTION_LENGTH = 1024 """:obj:`int`: Maximum number of characters in a :obj:`str` passed as: @@ -2025,11 +2037,14 @@ class MessageLimit(IntEnum): """ # constants above this line are tested MIN_TEXT_LENGTH = 1 - """:obj:`int`: Minimum number of characters in a :obj:`str` passed as the - :paramref:`~telegram.InputTextMessageContent.message_text` parameter of - :class:`telegram.InputTextMessageContent` and the - :paramref:`~telegram.Bot.edit_message_text.text` parameter of - :meth:`telegram.Bot.edit_message_text`. + """:obj:`int`: Minimum number of characters in a :obj:`str` passed as: + + * :paramref:`~telegram.InputTextMessageContent.message_text` parameter of + :class:`telegram.InputTextMessageContent`. + * :paramref:`~telegram.Bot.edit_message_text.text` parameter of + :meth:`telegram.Bot.edit_message_text`. + * :paramref:`~telegram.Bot.send_message_draft.text` parameter of + :meth:`telegram.Bot.send_message_draft`. """ DEEP_LINK_LENGTH = 64 """:obj:`int`: Maximum number of characters for a deep link.""" @@ -2175,6 +2190,11 @@ class MessageType(StringEnum): .. versionadded:: 22.1 """ + GIFT_UPGRADE_SENT = "gift_upgrade_sent" + """:obj:`str`: Messages with :attr:`telegram.Message.gift_upgrade_sent`. + + .. versionadded:: NEXT.VERSION + """ GIVEAWAY = "giveaway" """:obj:`str`: Messages with :attr:`telegram.Message.giveaway`. @@ -3058,7 +3078,7 @@ class StoryAreaTypeType(StringEnum): """:obj:`str`: Type of :class:`telegram.StoryAreaTypeUniqueGift`.""" -class StoryLimit(StringEnum): +class StoryLimit(IntEnum): """This enum contains limitations for :meth:`~telegram.Bot.post_story` and :meth:`~telegram.Bot.edit_story`. The enum members of this enumeration are instances of :class:`int` and can be treated as such. @@ -3074,17 +3094,17 @@ class StoryLimit(StringEnum): :meth:`telegram.Bot.edit_story`. """ ACTIVITY_SIX_HOURS = 6 * 3600 - """:obj:`int`: Possible value for :paramref:`~telegram.Bot.post_story.caption`` parameter of - :meth:`telegram.Bot.post_story`.""" + """:obj:`int`: Possible value for :paramref:`~telegram.Bot.post_story.active_period`` parameter + of :meth:`telegram.Bot.post_story`.""" ACTIVITY_TWELVE_HOURS = 12 * 3600 - """:obj:`int`: Possible value for :paramref:`~telegram.Bot.post_story.caption`` parameter of - :meth:`telegram.Bot.post_story`.""" + """:obj:`int`: Possible value for :paramref:`~telegram.Bot.post_story.active_period`` parameter + of :meth:`telegram.Bot.post_story`.""" ACTIVITY_ONE_DAY = 86400 - """:obj:`int`: Possible value for :paramref:`~telegram.Bot.post_story.caption`` parameter of - :meth:`telegram.Bot.post_story`.""" + """:obj:`int`: Possible value for :paramref:`~telegram.Bot.post_story.active_period`` parameter + of :meth:`telegram.Bot.post_story`.""" ACTIVITY_TWO_DAYS = 2 * 86400 - """:obj:`int`: Possible value for :paramref:`~telegram.Bot.post_story.caption`` parameter of - :meth:`telegram.Bot.post_story`.""" + """:obj:`int`: Possible value for :paramref:`~telegram.Bot.post_story.active_period`` parameter + of :meth:`telegram.Bot.post_story`.""" class SuggestedPost(IntEnum): @@ -3510,7 +3530,7 @@ class InvoiceLimit(IntEnum): .. versionadded:: 21.6 """ - MAX_STAR_COUNT = 10000 + MAX_STAR_COUNT = 25000 """:obj:`int`: Maximum amount of starts that must be paid to buy access to a paid media passed as :paramref:`~telegram.Bot.send_paid_media.star_count` parameter of :meth:`telegram.Bot.send_paid_media`. @@ -3518,6 +3538,8 @@ class InvoiceLimit(IntEnum): .. versionadded:: 21.6 .. versionchanged:: 22.1 Bot API 9.0 changed the value to 10000. + .. versionchanged:: NEXT.VERSION + Bot API 9.3 changed the value to 25000. """ SUBSCRIPTION_PERIOD = dtm.timedelta(days=30).total_seconds() """:obj:`int`: The period of time for which the subscription is active before diff --git a/src/telegram/ext/_extbot.py b/src/telegram/ext/_extbot.py index 3ab1b017c5e..15cede6f899 100644 --- a/src/telegram/ext/_extbot.py +++ b/src/telegram/ext/_extbot.py @@ -831,6 +831,7 @@ async def copy_message( video_start_timestamp: int | None = None, direct_messages_topic_id: int | None = None, suggested_post_parameters: "SuggestedPostParameters | None" = None, + message_effect_id: str | None = None, *, reply_to_message_id: int | None = None, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, @@ -866,6 +867,7 @@ async def copy_message( allow_paid_broadcast=allow_paid_broadcast, direct_messages_topic_id=direct_messages_topic_id, suggested_post_parameters=suggested_post_parameters, + message_effect_id=message_effect_id, ) async def copy_messages( @@ -1776,6 +1778,7 @@ async def forward_message( video_start_timestamp: int | None = None, direct_messages_topic_id: int | None = None, suggested_post_parameters: "SuggestedPostParameters | None" = None, + message_effect_id: str | None = None, *, read_timeout: ODVInput[float] = DEFAULT_NONE, write_timeout: ODVInput[float] = DEFAULT_NONE, @@ -1799,6 +1802,7 @@ async def forward_message( pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), direct_messages_topic_id=direct_messages_topic_id, + message_effect_id=message_effect_id, ) async def forward_messages( @@ -3134,6 +3138,36 @@ async def send_message( suggested_post_parameters=suggested_post_parameters, ) + async def send_message_draft( + self, + chat_id: int, + draft_id: int, + text: str, + message_thread_id: int | None = None, + parse_mode: ODVInput[str] = DEFAULT_NONE, + entities: Sequence["MessageEntity"] | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + rate_limit_args: RLARGS | None = None, + ) -> bool: + return await super().send_message_draft( + chat_id=chat_id, + draft_id=draft_id, + text=text, + message_thread_id=message_thread_id, + parse_mode=parse_mode, + entities=entities, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + async def send_photo( self, chat_id: int | str, @@ -5278,9 +5312,116 @@ async def approve_suggested_post( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def repost_story( + self, + business_connection_id: str, + from_chat_id: int, + from_story_id: int, + active_period: int, + post_to_chat_page: bool | None = None, + protect_content: ODVInput[bool] = DEFAULT_NONE, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + rate_limit_args: RLARGS | None = None, + ) -> Story: + return await super().repost_story( + business_connection_id=business_connection_id, + from_chat_id=from_chat_id, + from_story_id=from_story_id, + active_period=active_period, + post_to_chat_page=post_to_chat_page, + protect_content=protect_content, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def get_user_gifts( + self, + user_id: int, + exclude_unlimited: bool | None = None, + exclude_limited_upgradable: bool | None = None, + exclude_limited_non_upgradable: bool | None = None, + exclude_from_blockchain: bool | None = None, + exclude_unique: bool | None = None, + sort_by_price: bool | None = None, + offset: str | None = None, + limit: int | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + rate_limit_args: RLARGS | None = None, + ) -> OwnedGifts: + return await super().get_user_gifts( + user_id=user_id, + exclude_unlimited=exclude_unlimited, + exclude_limited_upgradable=exclude_limited_upgradable, + exclude_limited_non_upgradable=exclude_limited_non_upgradable, + exclude_from_blockchain=exclude_from_blockchain, + exclude_unique=exclude_unique, + sort_by_price=sort_by_price, + offset=offset, + limit=limit, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + + async def get_chat_gifts( + self, + chat_id: int | str, + exclude_unsaved: bool | None = None, + exclude_saved: bool | None = None, + exclude_unlimited: bool | None = None, + exclude_limited_upgradable: bool | None = None, + exclude_limited_non_upgradable: bool | None = None, + exclude_from_blockchain: bool | None = None, + exclude_unique: bool | None = None, + sort_by_price: bool | None = None, + offset: str | None = None, + limit: int | None = None, + *, + read_timeout: ODVInput[float] = DEFAULT_NONE, + write_timeout: ODVInput[float] = DEFAULT_NONE, + connect_timeout: ODVInput[float] = DEFAULT_NONE, + pool_timeout: ODVInput[float] = DEFAULT_NONE, + api_kwargs: JSONDict | None = None, + rate_limit_args: RLARGS | None = None, + ) -> OwnedGifts: + return await super().get_chat_gifts( + chat_id=chat_id, + exclude_unsaved=exclude_unsaved, + exclude_saved=exclude_saved, + exclude_unlimited=exclude_unlimited, + exclude_limited_upgradable=exclude_limited_upgradable, + exclude_limited_non_upgradable=exclude_limited_non_upgradable, + exclude_from_blockchain=exclude_from_blockchain, + exclude_unique=exclude_unique, + sort_by_price=sort_by_price, + offset=offset, + limit=limit, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + ) + # updated camelCase aliases getMe = get_me sendMessage = send_message + sendMessageDraft = send_message_draft deleteMessage = delete_message deleteMessages = delete_messages forwardMessage = forward_message @@ -5436,3 +5577,6 @@ async def approve_suggested_post( getMyStarBalance = get_my_star_balance approveSuggestedPost = approve_suggested_post declineSuggestedPost = decline_suggested_post + repostStory = repost_story + getUserGifts = get_user_gifts + getChatGifts = get_chat_gifts diff --git a/src/telegram/ext/filters.py b/src/telegram/ext/filters.py index 548dba00313..fe933d130fb 100644 --- a/src/telegram/ext/filters.py +++ b/src/telegram/ext/filters.py @@ -1978,6 +1978,7 @@ def filter(self, update: Update) -> bool: or StatusUpdate.GENERAL_FORUM_TOPIC_HIDDEN.check_update(update) or StatusUpdate.GENERAL_FORUM_TOPIC_UNHIDDEN.check_update(update) or StatusUpdate.GIFT.check_update(update) + or StatusUpdate.GIFT_UPGRADE_SENT.check_update(update) or StatusUpdate.GIVEAWAY_COMPLETED.check_update(update) or StatusUpdate.GIVEAWAY_CREATED.check_update(update) or StatusUpdate.LEFT_CHAT_MEMBER.check_update(update) @@ -2188,6 +2189,18 @@ def filter(self, message: Message) -> bool: .. versionadded:: 22.1 """ + class _GiftUpgradeSent(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.gift_upgrade_sent) + + GIFT_UPGRADE_SENT = _GiftUpgradeSent(name="filters.StatusUpdate.GIFT_UPGRADE_SENT") + """Messages that contain :attr:`telegram.Message.gift_upgrade_sent`. + + .. versionadded:: NEXT.VERSION + """ + class _GiveawayCreated(MessageFilter): __slots__ = () diff --git a/tests/auxil/dummy_objects.py b/tests/auxil/dummy_objects.py index 0fbf738e916..84bba3aa45b 100644 --- a/tests/auxil/dummy_objects.py +++ b/tests/auxil/dummy_objects.py @@ -81,7 +81,11 @@ accent_color_id=1, max_reaction_count=1, accepted_gift_types=AcceptedGiftTypes( - unlimited_gifts=True, limited_gifts=True, unique_gifts=True, premium_subscription=True + unlimited_gifts=True, + limited_gifts=True, + unique_gifts=True, + premium_subscription=True, + gifts_from_channels=True, ), ), "ChatInviteLink": ChatInviteLink( diff --git a/tests/ext/test_filters.py b/tests/ext/test_filters.py index fdaa673f922..f568eb65a1a 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -1136,6 +1136,11 @@ def test_filters_status_update(self, update): assert filters.StatusUpdate.UNIQUE_GIFT.check_update(update) update.message.unique_gift = None + update.message.gift_upgrade_sent = "gift_upgrade_sent" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.GIFT_UPGRADE_SENT.check_update(update) + update.message.gift_upgrade_sent = None + update.message.paid_message_price_changed = "paid_message_price_changed" assert filters.StatusUpdate.ALL.check_update(update) assert filters.StatusUpdate.PAID_MESSAGE_PRICE_CHANGED.check_update(update) diff --git a/tests/test_bot.py b/tests/test_bot.py index 496be324dfe..eee17f7419d 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -65,6 +65,7 @@ MenuButtonWebApp, Message, MessageEntity, + OwnedGifts, Poll, PollOption, PreparedInlineMessage, @@ -1477,6 +1478,61 @@ async def make_assertion(*args, **_): "SoSecretToken", ) + async def test_send_message_draft(self, offline_bot, monkeypatch): + entities = [ + MessageEntity(MessageEntity.BOLD, 0, 3), + MessageEntity(MessageEntity.ITALIC, 5, 8), + ] + + async def make_assertions(*args, **kwargs): + params = kwargs.get("request_data").parameters + assert params.get("chat_id") == 123 + assert params.get("draft_id") == 1 + assert params.get("text") == "test test" + assert params.get("message_thread_id") == 9 + assert params.get("parse_mode") == "markdown" + assert params.get("entities") == [e.to_dict() for e in entities] + + return True + + monkeypatch.setattr(offline_bot.request, "post", make_assertions) + assert await offline_bot.send_message_draft( + chat_id=123, + draft_id=1, + text="test test", + message_thread_id=9, + parse_mode="markdown", + entities=entities, + ) + + @pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True) + @pytest.mark.parametrize( + ("passed_value", "expected_value"), + [(DEFAULT_NONE, "Markdown"), ("HTML", "HTML"), (None, None)], + ) + async def test_send_message_draft_default_parse_mode( + self, default_bot, monkeypatch, passed_value, expected_value + ): + async def make_assertion(url, request_data, *args, **kwargs): + assert request_data.parameters.get("parse_mode") == expected_value + return True + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + kwargs = { + "chat_id": 123, + "draft_id": 1, + "text": "test test", + "message_thread_id": 9, + "entities": [ + MessageEntity(MessageEntity.BOLD, 0, 3), + MessageEntity(MessageEntity.ITALIC, 5, 8), + ], + } + if passed_value is not DEFAULT_NONE: + kwargs["parse_mode"] = passed_value + + await default_bot.send_message_draft(**kwargs) + # TODO: Needs improvement. Need incoming shipping queries to test async def test_answer_shipping_query_ok(self, monkeypatch, offline_bot): # For now just test that our internals pass the correct data @@ -2712,6 +2768,72 @@ async def make_assertion(url, request_data: RequestData, *args, **kwargs): await offline_bot.decline_suggested_post(1234, 5678, "declined") + async def test_get_user_gifts_parameter_passing(self, offline_bot, monkeypatch): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + for param in ( + "user_id", + "exclude_unlimited", + "exclude_limited_upgradable", + "exclude_limited_non_upgradable", + "exclude_from_blockchain", + "exclude_unique", + "sort_by_price", + "offset", + "limit", + ): + assert request_data.parameters.get(param) == param + + return OwnedGifts(0, [], "null").to_dict() + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + + await offline_bot.get_user_gifts( + user_id="user_id", + exclude_unlimited="exclude_unlimited", + exclude_limited_upgradable="exclude_limited_upgradable", + exclude_limited_non_upgradable="exclude_limited_non_upgradable", + exclude_from_blockchain="exclude_from_blockchain", + exclude_unique="exclude_unique", + sort_by_price="sort_by_price", + offset="offset", + limit="limit", + ) + + async def test_get_chat_gifts_parameter_passing(self, offline_bot, monkeypatch): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + for param in ( + "chat_id", + "exclude_saved", + "exclude_unsaved", + "exclude_unlimited", + "exclude_limited_upgradable", + "exclude_limited_non_upgradable", + "exclude_from_blockchain", + "exclude_unique", + "sort_by_price", + "offset", + "limit", + ): + assert request_data.parameters.get(param) == param + + return OwnedGifts(0, [], "null").to_dict() + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + + await offline_bot.get_chat_gifts( + chat_id="chat_id", + exclude_saved="exclude_saved", + exclude_unsaved="exclude_unsaved", + exclude_unlimited="exclude_unlimited", + exclude_limited_upgradable="exclude_limited_upgradable", + exclude_limited_non_upgradable="exclude_limited_non_upgradable", + exclude_from_blockchain="exclude_from_blockchain", + exclude_unique="exclude_unique", + sort_by_price="sort_by_price", + offset="offset", + limit="limit", + ) + class TestBotWithRequest: """ @@ -4687,6 +4809,16 @@ async def test_get_my_star_balance(self, bot): assert isinstance(balance, StarAmount) assert balance.amount == 0 + async def test_get_user_gifts_basic(self, bot): + gifts = await bot.get_user_gifts(bot.bot.id) + assert isinstance(gifts, OwnedGifts) + assert gifts.total_count == 0 + + async def test_get_chat_gifts_basic(self, bot, chat_id): + gifts = await bot.get_chat_gifts(chat_id) + assert isinstance(gifts, OwnedGifts) + assert gifts.total_count == 0 + async def test_initialize_tracks_requests_and_bot_separately(self, offline_bot, monkeypatch): """Test that requests and bot user are initialized separately and only once.""" request_init_count = 0 diff --git a/tests/test_business_methods.py b/tests/test_business_methods.py index 4e44a84e37d..720e194adbb 100644 --- a/tests/test_business_methods.py +++ b/tests/test_business_methods.py @@ -214,7 +214,7 @@ async def make_assertion(*args, **kwargs): async def test_set_business_account_gift_settings(self, offline_bot, monkeypatch): show_gift_button = True - accepted_gift_types = AcceptedGiftTypes(True, True, True, True) + accepted_gift_types = AcceptedGiftTypes(True, True, True, True, True) async def make_assertion(*args, **kwargs): data = kwargs.get("request_data").json_parameters @@ -789,3 +789,55 @@ async def make_assertions(*args, **kwargs): reply_markup=reply_markup, ) assert isinstance(obj, Message) + + async def test_repost_story(self, offline_bot, monkeypatch): + """No way to test this without stories""" + + async def make_assertion(url, request_data, *args, **kwargs): + for param in ( + "business_connection_id", + "from_chat_id", + "from_story_id", + "active_period", + "post_to_chat_page", + "protect_content", + ): + assert request_data.parameters.get(param) == param + return Story(chat=Chat(id=1, type=Chat.PRIVATE), id=42).to_dict() + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + + story = await offline_bot.repost_story( + business_connection_id="business_connection_id", + from_chat_id="from_chat_id", + from_story_id="from_story_id", + active_period="active_period", + post_to_chat_page="post_to_chat_page", + protect_content="protect_content", + ) + assert story.chat.id == 1 + assert story.id == 42 + + @pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True) + @pytest.mark.parametrize( + ("passed_value", "expected_value"), + [(DEFAULT_NONE, True), (False, False), (None, None)], + ) + async def test_repost_story_default_protect_content( + self, default_bot, monkeypatch, passed_value, expected_value + ): + async def make_assertion(url, request_data, *args, **kwargs): + assert request_data.parameters.get("protect_content") == expected_value + return Story(chat=Chat(123, "private"), id=123).to_dict() + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + kwargs = { + "business_connection_id": self.bci, + "from_chat_id": 123, + "from_story_id": 456, + "active_period": dtm.timedelta(seconds=20), + } + if passed_value is not DEFAULT_NONE: + kwargs["protect_content"] = passed_value + + await default_bot.repost_story(**kwargs) diff --git a/tests/test_chat.py b/tests/test_chat.py index 7ab0c2f1d0f..9050825a36f 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -524,6 +524,21 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(chat.get_bot(), "send_message", make_assertion) assert await chat.send_message(text="test") + async def test_instance_method_send_message_draft(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["text"] == "test" + + assert check_shortcut_signature( + Chat.send_message_draft, Bot.send_message_draft, ["chat_id"], [] + ) + assert await check_shortcut_call( + chat.send_message_draft, chat.get_bot(), "send_message_draft" + ) + assert await check_defaults_handling(chat.send_message_draft, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_message_draft", make_assertion) + assert await chat.send_message_draft(draft_id=1, text="test") + async def test_instance_method_send_media_group(self, monkeypatch, chat): async def make_assertion(*_, **kwargs): return kwargs["chat_id"] == chat.id and kwargs["media"] == "test_media_group" @@ -1495,6 +1510,44 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(chat.get_bot(), "decline_suggested_post", make_assertion) assert await chat.decline_suggested_post(message_id="message_id", comment="comment") + async def test_instance_method_repost_story(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["from_chat_id"] == chat.id + + assert check_shortcut_signature( + Chat.repost_story, + Bot.repost_story, + [ + "from_chat_id", + ], + additional_kwargs=[], + ) + assert await check_shortcut_call( + chat.repost_story, + chat.get_bot(), + "repost_story", + shortcut_kwargs=["from_chat_id"], + ) + assert await check_defaults_handling(chat.repost_story, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "repost_story", make_assertion) + assert await chat.repost_story( + business_connection_id="bcid", + from_story_id=123, + active_period=3600, + ) + + async def test_instance_method_get_gifts(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id + + assert check_shortcut_signature(Chat.get_gifts, Bot.get_chat_gifts, ["chat_id"], []) + assert await check_shortcut_call(chat.get_gifts, chat.get_bot(), "get_chat_gifts") + assert await check_defaults_handling(chat.get_gifts, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "get_chat_gifts", make_assertion) + assert await chat.get_gifts() + def test_mention_html(self): chat = Chat(id=1, type="foo") with pytest.raises(TypeError, match="Can not create a mention to a private group chat"): diff --git a/tests/test_chatfullinfo.py b/tests/test_chatfullinfo.py index 79d55e2fa8b..86aaa62046d 100644 --- a/tests/test_chatfullinfo.py +++ b/tests/test_chatfullinfo.py @@ -33,6 +33,8 @@ Location, ReactionTypeCustomEmoji, ReactionTypeEmoji, + UniqueGiftColors, + UserRating, ) from telegram._gifts import AcceptedGiftTypes from telegram._utils.datetime import UTC, to_timestamp @@ -89,6 +91,9 @@ def chat_full_info(bot): can_send_paid_media=ChatFullInfoTestBase.can_send_paid_media, is_direct_messages=ChatFullInfoTestBase.is_direct_messages, parent_chat=ChatFullInfoTestBase.parent_chat, + rating=ChatFullInfoTestBase.rating, + unique_gift_colors=ChatFullInfoTestBase.unique_gift_colors, + paid_message_star_count=ChatFullInfoTestBase.paid_message_star_count, ) chat.set_bot(bot) chat._unfreeze() @@ -147,9 +152,19 @@ class ChatFullInfoTestBase: first_name = "first_name" last_name = "last_name" can_send_paid_media = True - accepted_gift_types = AcceptedGiftTypes(True, True, True, True) + accepted_gift_types = AcceptedGiftTypes(True, True, True, True, True) is_direct_messages = True parent_chat = Chat(4, "channel", "channel") + rating = UserRating(level=1, rating=2, current_level_rating=3, next_level_rating=4) + unique_gift_colors = UniqueGiftColors( + model_custom_emoji_id="model_custom_emoji_id", + symbol_custom_emoji_id="symbol_custom_emoji_id", + light_theme_main_color=0xFF5733, + light_theme_other_colors=[0x33FF57, 0x3357FF], + dark_theme_main_color=0xC70039, + dark_theme_other_colors=[0x900C3F, 0x581845], + ) + paid_message_star_count = 1234 class TestChatFullInfoWithoutRequest(ChatFullInfoTestBase): @@ -207,6 +222,9 @@ def test_de_json(self, offline_bot): "can_send_paid_media": self.can_send_paid_media, "is_direct_messages": self.is_direct_messages, "parent_chat": self.parent_chat.to_dict(), + "rating": self.rating.to_dict(), + "unique_gift_colors": self.unique_gift_colors.to_dict(), + "paid_message_star_count": self.paid_message_star_count, } cfi = ChatFullInfo.de_json(json_dict, offline_bot) @@ -258,6 +276,9 @@ def test_de_json(self, offline_bot): assert cfi.can_send_paid_media == self.can_send_paid_media assert cfi.is_direct_messages == self.is_direct_messages assert cfi.parent_chat == self.parent_chat + assert cfi.rating == self.rating + assert cfi.unique_gift_colors == self.unique_gift_colors + assert cfi.paid_message_star_count == self.paid_message_star_count def test_de_json_localization(self, offline_bot, raw_bot, tz_bot): json_dict = { @@ -341,6 +362,9 @@ def test_to_dict(self, chat_full_info): assert cfi_dict["max_reaction_count"] == cfi.max_reaction_count assert cfi_dict["is_direct_messages"] == cfi.is_direct_messages assert cfi_dict["parent_chat"] == cfi.parent_chat.to_dict() + assert cfi_dict["rating"] == cfi.rating.to_dict() + assert cfi_dict["unique_gift_colors"] == cfi.unique_gift_colors.to_dict() + assert cfi_dict["paid_message_star_count"] == cfi.paid_message_star_count def test_time_period_properties(self, PTB_TIMEDELTA, chat_full_info): cfi = chat_full_info diff --git a/tests/test_checklists.py b/tests/test_checklists.py index 96ab522d130..1d5b0fe86d1 100644 --- a/tests/test_checklists.py +++ b/tests/test_checklists.py @@ -21,6 +21,7 @@ import pytest from telegram import ( + Chat, Checklist, ChecklistTask, ChecklistTasksAdded, @@ -43,6 +44,7 @@ class ChecklistTaskTestBase: MessageEntity(type="italic", offset=5, length=2), ] completed_by_user = User(id=1, first_name="Test", last_name="User", is_bot=False) + completed_by_chat = Chat(id=-100, type=Chat.SUPERGROUP, title="Test Chat") completion_date = dtm.datetime.now(tz=UTC).replace(microsecond=0) @@ -53,6 +55,7 @@ def checklist_task(): text=ChecklistTaskTestBase.text, text_entities=ChecklistTaskTestBase.text_entities, completed_by_user=ChecklistTaskTestBase.completed_by_user, + completed_by_chat=ChecklistTaskTestBase.completed_by_chat, completion_date=ChecklistTaskTestBase.completion_date, ) @@ -72,6 +75,7 @@ def test_to_dict(self, checklist_task): assert clt_dict["text"] == self.text assert clt_dict["text_entities"] == [entity.to_dict() for entity in self.text_entities] assert clt_dict["completed_by_user"] == self.completed_by_user.to_dict() + assert clt_dict["completed_by_chat"] == self.completed_by_chat.to_dict() assert clt_dict["completion_date"] == to_timestamp(self.completion_date) def test_de_json(self, offline_bot): @@ -80,6 +84,7 @@ def test_de_json(self, offline_bot): "text": self.text, "text_entities": [entity.to_dict() for entity in self.text_entities], "completed_by_user": self.completed_by_user.to_dict(), + "completed_by_chat": self.completed_by_chat.to_dict(), "completion_date": to_timestamp(self.completion_date), } clt = ChecklistTask.de_json(json_dict, offline_bot) @@ -88,6 +93,7 @@ def test_de_json(self, offline_bot): assert clt.text == self.text assert clt.text_entities == tuple(self.text_entities) assert clt.completed_by_user == self.completed_by_user + assert clt.completed_by_chat == self.completed_by_chat assert clt.completion_date == self.completion_date assert clt.api_kwargs == {} diff --git a/tests/test_forum.py b/tests/test_forum.py index dc627eb8462..85f9b3a9db0 100644 --- a/tests/test_forum.py +++ b/tests/test_forum.py @@ -40,13 +40,20 @@ async def forum_topic_object(forum_group_id, emoji_id): return ForumTopic( message_thread_id=forum_group_id, - name=TEST_TOPIC_NAME, - icon_color=TEST_TOPIC_ICON_COLOR, + name=ForumTopicTestBase.TEST_TOPIC_NAME, + icon_color=ForumTopicTestBase.TEST_TOPIC_ICON_COLOR, icon_custom_emoji_id=emoji_id, + is_name_implicit=ForumTopicTestBase.is_name_implicit, ) -class TestForumTopicWithoutRequest: +class ForumTopicTestBase: + TEST_TOPIC_NAME = TEST_TOPIC_NAME + TEST_TOPIC_ICON_COLOR = TEST_TOPIC_ICON_COLOR + is_name_implicit = False + + +class TestForumTopicWithoutRequest(ForumTopicTestBase): def test_slot_behaviour(self, forum_topic_object): inst = forum_topic_object for attr in inst.__slots__: @@ -55,33 +62,37 @@ def test_slot_behaviour(self, forum_topic_object): async def test_expected_values(self, emoji_id, forum_group_id, forum_topic_object): assert forum_topic_object.message_thread_id == forum_group_id - assert forum_topic_object.icon_color == TEST_TOPIC_ICON_COLOR - assert forum_topic_object.name == TEST_TOPIC_NAME + assert forum_topic_object.icon_color == self.TEST_TOPIC_ICON_COLOR + assert forum_topic_object.name == self.TEST_TOPIC_NAME assert forum_topic_object.icon_custom_emoji_id == emoji_id + assert forum_topic_object.is_name_implicit == self.is_name_implicit def test_de_json(self, offline_bot, emoji_id, forum_group_id): json_dict = { "message_thread_id": forum_group_id, - "name": TEST_TOPIC_NAME, - "icon_color": TEST_TOPIC_ICON_COLOR, + "name": self.TEST_TOPIC_NAME, + "icon_color": self.TEST_TOPIC_ICON_COLOR, "icon_custom_emoji_id": emoji_id, + "is_name_implicit": self.is_name_implicit, } topic = ForumTopic.de_json(json_dict, offline_bot) assert topic.api_kwargs == {} assert topic.message_thread_id == forum_group_id - assert topic.icon_color == TEST_TOPIC_ICON_COLOR - assert topic.name == TEST_TOPIC_NAME + assert topic.icon_color == self.TEST_TOPIC_ICON_COLOR + assert topic.name == self.TEST_TOPIC_NAME assert topic.icon_custom_emoji_id == emoji_id + assert topic.is_name_implicit == self.is_name_implicit def test_to_dict(self, emoji_id, forum_group_id, forum_topic_object): topic_dict = forum_topic_object.to_dict() assert isinstance(topic_dict, dict) assert topic_dict["message_thread_id"] == forum_group_id - assert topic_dict["name"] == TEST_TOPIC_NAME - assert topic_dict["icon_color"] == TEST_TOPIC_ICON_COLOR + assert topic_dict["name"] == self.TEST_TOPIC_NAME + assert topic_dict["icon_color"] == self.TEST_TOPIC_ICON_COLOR assert topic_dict["icon_custom_emoji_id"] == emoji_id + assert topic_dict["is_name_implicit"] == self.is_name_implicit def test_equality(self, emoji_id, forum_group_id): a = ForumTopic( @@ -289,10 +300,20 @@ async def test_close_reopen_hide_unhide_general_forum_topic(self, bot, forum_gro @pytest.fixture(scope="module") def topic_created(): - return ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR) + return ForumTopicCreated( + name=ForumTopicCreatedTestBase.TEST_TOPIC_NAME, + icon_color=ForumTopicCreatedTestBase.TEST_TOPIC_ICON_COLOR, + is_name_implicit=ForumTopicCreatedTestBase.is_name_implicit, + ) + + +class ForumTopicCreatedTestBase: + TEST_TOPIC_NAME = TEST_TOPIC_NAME + TEST_TOPIC_ICON_COLOR = TEST_TOPIC_ICON_COLOR + is_name_implicit = False -class TestForumTopicCreatedWithoutRequest: +class TestForumTopicCreatedWithoutRequest(ForumTopicCreatedTestBase): def test_slot_behaviour(self, topic_created): for attr in topic_created.__slots__: assert getattr(topic_created, attr, "err") != "err", f"got extra slot '{attr}'" @@ -301,23 +322,30 @@ def test_slot_behaviour(self, topic_created): ) def test_expected_values(self, topic_created): - assert topic_created.icon_color == TEST_TOPIC_ICON_COLOR - assert topic_created.name == TEST_TOPIC_NAME + assert topic_created.icon_color == self.TEST_TOPIC_ICON_COLOR + assert topic_created.name == self.TEST_TOPIC_NAME + assert topic_created.is_name_implicit == self.is_name_implicit def test_de_json(self, offline_bot): - json_dict = {"icon_color": TEST_TOPIC_ICON_COLOR, "name": TEST_TOPIC_NAME} + json_dict = { + "icon_color": self.TEST_TOPIC_ICON_COLOR, + "name": self.TEST_TOPIC_NAME, + "is_name_implicit": self.is_name_implicit, + } action = ForumTopicCreated.de_json(json_dict, offline_bot) assert action.api_kwargs == {} - assert action.icon_color == TEST_TOPIC_ICON_COLOR - assert action.name == TEST_TOPIC_NAME + assert action.icon_color == self.TEST_TOPIC_ICON_COLOR + assert action.name == self.TEST_TOPIC_NAME + assert action.is_name_implicit == self.is_name_implicit def test_to_dict(self, topic_created): action_dict = topic_created.to_dict() assert isinstance(action_dict, dict) - assert action_dict["name"] == TEST_TOPIC_NAME - assert action_dict["icon_color"] == TEST_TOPIC_ICON_COLOR + assert action_dict["name"] == self.TEST_TOPIC_NAME + assert action_dict["icon_color"] == self.TEST_TOPIC_ICON_COLOR + assert action_dict["is_name_implicit"] == self.is_name_implicit def test_equality(self, emoji_id): a = ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR) diff --git a/tests/test_gifts.py b/tests/test_gifts.py index 8929b3cd6bd..1ddda67a6af 100644 --- a/tests/test_gifts.py +++ b/tests/test_gifts.py @@ -21,12 +21,77 @@ import pytest from telegram import BotCommand, Chat, Gift, GiftInfo, Gifts, MessageEntity, Sticker -from telegram._gifts import AcceptedGiftTypes +from telegram._gifts import AcceptedGiftTypes, GiftBackground from telegram._utils.defaultvalue import DEFAULT_NONE from telegram.request import RequestData from tests.auxil.slots import mro_slots +@pytest.fixture +def gift_background(): + return GiftBackground( + center_color=GiftBackgroundTestBase.center_color, + edge_color=GiftBackgroundTestBase.edge_color, + text_color=GiftBackgroundTestBase.text_color, + ) + + +class GiftBackgroundTestBase: + center_color = 0xFFFFFF + edge_color = 0x000000 + text_color = 0xFF0000 + + +class TestGiftBackgroundWithoutRequest(GiftBackgroundTestBase): + def test_slot_behaviour(self, gift_background): + for attr in gift_background.__slots__: + assert getattr(gift_background, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(gift_background)) == len(set(mro_slots(gift_background))), ( + "duplicate slot" + ) + + def test_de_json(self, offline_bot): + json_dict = { + "center_color": self.center_color, + "edge_color": self.edge_color, + "text_color": self.text_color, + } + gift_background = GiftBackground.de_json(json_dict, offline_bot) + assert gift_background.api_kwargs == {} + assert gift_background.center_color == self.center_color + assert gift_background.edge_color == self.edge_color + assert gift_background.text_color == self.text_color + + def test_to_dict(self, gift_background): + json_dict = gift_background.to_dict() + assert json_dict["center_color"] == self.center_color + assert json_dict["edge_color"] == self.edge_color + assert json_dict["text_color"] == self.text_color + + def test_equality(self, gift_background): + a = gift_background + b = GiftBackground( + self.center_color, + self.edge_color, + self.text_color, + ) + c = GiftBackground( + 0x000000, + self.edge_color, + self.text_color, + ) + d = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + @pytest.fixture def gift(request): return Gift( @@ -37,6 +102,12 @@ def gift(request): remaining_count=GiftTestBase.remaining_count, upgrade_star_count=GiftTestBase.upgrade_star_count, publisher_chat=GiftTestBase.publisher_chat, + personal_total_count=GiftTestBase.personal_total_count, + personal_remaining_count=GiftTestBase.personal_remaining_count, + background=GiftTestBase.background, + is_premium=GiftTestBase.is_premium, + has_colors=GiftTestBase.has_colors, + unique_gift_variant_count=GiftTestBase.unique_gift_variant_count, ) @@ -56,6 +127,12 @@ class GiftTestBase: remaining_count = 5 upgrade_star_count = 10 publisher_chat = Chat(1, Chat.PRIVATE) + personal_total_count = 37 + personal_remaining_count = 23 + background = GiftBackground(0xFFFFFF, 0x000000, 0xFF0000) + is_premium = True + has_colors = True + unique_gift_variant_count = 42 class TestGiftWithoutRequest(GiftTestBase): @@ -73,6 +150,12 @@ def test_de_json(self, offline_bot, gift): "remaining_count": self.remaining_count, "upgrade_star_count": self.upgrade_star_count, "publisher_chat": self.publisher_chat.to_dict(), + "personal_total_count": self.personal_total_count, + "personal_remaining_count": self.personal_remaining_count, + "background": self.background.to_dict(), + "is_premium": self.is_premium, + "has_colors": self.has_colors, + "unique_gift_variant_count": self.unique_gift_variant_count, } gift = Gift.de_json(json_dict, offline_bot) assert gift.api_kwargs == {} @@ -84,6 +167,12 @@ def test_de_json(self, offline_bot, gift): assert gift.remaining_count == self.remaining_count assert gift.upgrade_star_count == self.upgrade_star_count assert gift.publisher_chat == self.publisher_chat + assert gift.personal_total_count == self.personal_total_count + assert gift.personal_remaining_count == self.personal_remaining_count + assert gift.background == self.background + assert gift.is_premium == self.is_premium + assert gift.has_colors == self.has_colors + assert gift.unique_gift_variant_count == self.unique_gift_variant_count def test_to_dict(self, gift): gift_dict = gift.to_dict() @@ -96,6 +185,12 @@ def test_to_dict(self, gift): assert gift_dict["remaining_count"] == self.remaining_count assert gift_dict["upgrade_star_count"] == self.upgrade_star_count assert gift_dict["publisher_chat"] == self.publisher_chat.to_dict() + assert gift_dict["personal_total_count"] == self.personal_total_count + assert gift_dict["personal_remaining_count"] == self.personal_remaining_count + assert gift_dict["background"] == self.background.to_dict() + assert gift_dict["is_premium"] == self.is_premium + assert gift_dict["has_colors"] == self.has_colors + assert gift_dict["unique_gift_variant_count"] == self.unique_gift_variant_count def test_equality(self, gift): a = gift @@ -316,6 +411,8 @@ def gift_info(): text=GiftInfoTestBase.text, entities=GiftInfoTestBase.entities, is_private=GiftInfoTestBase.is_private, + is_upgrade_separate=GiftInfoTestBase.is_upgrade_separate, + unique_gift_number=GiftInfoTestBase.unique_gift_number, ) @@ -338,6 +435,8 @@ class GiftInfoTestBase: MessageEntity(MessageEntity.ITALIC, 5, 8), ) is_private = True + is_upgrade_separate = False + unique_gift_number = 42 class TestGiftInfoWithoutRequest(GiftInfoTestBase): @@ -356,6 +455,8 @@ def test_de_json(self, offline_bot): "text": self.text, "entities": [e.to_dict() for e in self.entities], "is_private": self.is_private, + "is_upgrade_separate": self.is_upgrade_separate, + "unique_gift_number": self.unique_gift_number, } gift_info = GiftInfo.de_json(json_dict, offline_bot) assert gift_info.api_kwargs == {} @@ -367,6 +468,8 @@ def test_de_json(self, offline_bot): assert gift_info.text == self.text assert gift_info.entities == self.entities assert gift_info.is_private == self.is_private + assert gift_info.is_upgrade_separate == self.is_upgrade_separate + assert gift_info.unique_gift_number == self.unique_gift_number def test_to_dict(self, gift_info): json_dict = gift_info.to_dict() @@ -378,6 +481,8 @@ def test_to_dict(self, gift_info): assert json_dict["text"] == self.text assert json_dict["entities"] == [e.to_dict() for e in self.entities] assert json_dict["is_private"] == self.is_private + assert json_dict["is_upgrade_separate"] == self.is_upgrade_separate + assert json_dict["unique_gift_number"] == self.unique_gift_number def test_parse_entity(self, gift_info): entity = MessageEntity(MessageEntity.BOLD, 0, 4) @@ -430,6 +535,7 @@ def accepted_gift_types(): limited_gifts=AcceptedGiftTypesTestBase.limited_gifts, unique_gifts=AcceptedGiftTypesTestBase.unique_gifts, premium_subscription=AcceptedGiftTypesTestBase.premium_subscription, + gifts_from_channels=AcceptedGiftTypesTestBase.gifts_from_channels, ) @@ -438,6 +544,7 @@ class AcceptedGiftTypesTestBase: limited_gifts = True unique_gifts = True premium_subscription = True + gifts_from_channels = False class TestAcceptedGiftTypesWithoutRequest(AcceptedGiftTypesTestBase): @@ -454,6 +561,7 @@ def test_de_json(self, offline_bot): "limited_gifts": self.limited_gifts, "unique_gifts": self.unique_gifts, "premium_subscription": self.premium_subscription, + "gifts_from_channels": self.gifts_from_channels, } accepted_gift_types = AcceptedGiftTypes.de_json(json_dict, offline_bot) assert accepted_gift_types.api_kwargs == {} @@ -461,6 +569,7 @@ def test_de_json(self, offline_bot): assert accepted_gift_types.limited_gifts == self.limited_gifts assert accepted_gift_types.unique_gifts == self.unique_gifts assert accepted_gift_types.premium_subscription == self.premium_subscription + assert accepted_gift_types.gifts_from_channels == self.gifts_from_channels def test_to_dict(self, accepted_gift_types): json_dict = accepted_gift_types.to_dict() @@ -468,17 +577,23 @@ def test_to_dict(self, accepted_gift_types): assert json_dict["limited_gifts"] == self.limited_gifts assert json_dict["unique_gifts"] == self.unique_gifts assert json_dict["premium_subscription"] == self.premium_subscription + assert json_dict["gifts_from_channels"] == self.gifts_from_channels def test_equality(self, accepted_gift_types): a = accepted_gift_types b = AcceptedGiftTypes( - self.unlimited_gifts, self.limited_gifts, self.unique_gifts, self.premium_subscription + self.unlimited_gifts, + self.limited_gifts, + self.unique_gifts, + self.premium_subscription, + self.gifts_from_channels, ) c = AcceptedGiftTypes( not self.unlimited_gifts, self.limited_gifts, self.unique_gifts, self.premium_subscription, + self.gifts_from_channels, ) d = BotCommand("start", "description") diff --git a/tests/test_message.py b/tests/test_message.py index 3bafe22d54f..6ca430ce05e 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -268,20 +268,21 @@ def message(bot): { "unique_gift": UniqueGiftInfo( gift=UniqueGift( - "human_readable_name", - "unique_name", - 2, - UniqueGiftModel( + gift_id="gift_id", + base_name="human_readable_name", + name="unique_name", + number=2, + model=UniqueGiftModel( "model_name", Sticker("file_id1", "file_unique_id1", 512, 512, False, False, "regular"), 10, ), - UniqueGiftSymbol( + symbol=UniqueGiftSymbol( "symbol_name", Sticker("file_id2", "file_unique_id2", 512, 512, True, True, "mask"), 20, ), - UniqueGiftBackdrop( + backdrop=UniqueGiftBackdrop( "backdrop_name", UniqueGiftBackdropColors(0x00FF00, 0xEE00FF, 0xAA22BB, 0x20FE8F), 30, @@ -420,6 +421,15 @@ def message(bot): send_date=dtm.datetime.utcnow(), ) }, + { + "gift_upgrade_sent": GiftInfo( + gift=Gift( + "gift_id", + Sticker("file_id", "file_unique_id", 512, 512, False, False, "regular"), + 5, + ) + ) + }, ], ids=[ "reply", @@ -510,6 +520,7 @@ def message(bot): "suggested_post_approved", "suggested_post_approval_failed", "suggested_post_info", + "gift_upgrade_sent", ], ) def message_params(bot, request): @@ -735,7 +746,8 @@ async def extract_message_thread_id(*args, **kwargs): message_thread_id = await method(*args, message_thread_id=None) assert message_thread_id is None - if bot_method_name == "send_chat_action": + # These methods do not accept `do_quote` as passed below + if bot_method_name in ["send_chat_action", "send_message_draft"]: return message_thread_id = await method( @@ -1831,6 +1843,37 @@ async def make_assertion(*_, **kwargs): message, message.reply_html, "send_message", ["test"], monkeypatch ) + async def test_reply_text_draft(self, monkeypatch, message): + async def make_assertion(*_, **kwargs): + id_ = kwargs["chat_id"] == message.chat_id + text = kwargs["text"] == "test" + return id_ and text + + assert check_shortcut_signature( + Message.reply_text_draft, + Bot.send_message_draft, + ["chat_id"], + [], + annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)}, + ) + assert await check_shortcut_call( + message.reply_text_draft, + message.get_bot(), + "send_message_draft", + skip_params=[""], + shortcut_kwargs=["chat_id"], + ) + assert await check_defaults_handling( + message.reply_text_draft, message.get_bot(), no_default_kwargs={"message_thread_id"} + ) + + monkeypatch.setattr(message.get_bot(), "send_message_draft", make_assertion) + assert await message.reply_text_draft(draft_id=1, text="test") + + await self.check_thread_id_parsing( + message, message.reply_text_draft, "send_message_draft", [1, "test"], monkeypatch + ) + async def test_reply_media_group(self, monkeypatch, message): async def make_assertion(*_, **kwargs): id_ = kwargs["chat_id"] == message.chat_id diff --git a/tests/test_ownedgift.py b/tests/test_ownedgift.py index 67ecfada2ee..b2ea34bcd6d 100644 --- a/tests/test_ownedgift.py +++ b/tests/test_ownedgift.py @@ -60,6 +60,7 @@ class OwnedGiftTestBase: star_count=5, ) unique_gift = UniqueGift( + gift_id="gift_id", base_name="human_readable", name="unique_name", number=10, @@ -96,6 +97,8 @@ class OwnedGiftTestBase: can_be_transferred = True transfer_star_count = 300 next_transfer_date = dtm.datetime.now(tz=UTC).replace(microsecond=0) + is_upgrade_separate = False + unique_gift_number = 37 class TestOwnedGiftWithoutRequest(OwnedGiftTestBase): @@ -183,6 +186,8 @@ def owned_gift_regular(): was_refunded=TestOwnedGiftRegularWithoutRequest.was_refunded, convert_star_count=TestOwnedGiftRegularWithoutRequest.convert_star_count, prepaid_upgrade_star_count=TestOwnedGiftRegularWithoutRequest.prepaid_upgrade_star_count, + is_upgrade_separate=TestOwnedGiftRegularWithoutRequest.is_upgrade_separate, + unique_gift_number=TestOwnedGiftRegularWithoutRequest.unique_gift_number, ) @@ -209,6 +214,8 @@ def test_de_json(self, offline_bot): "was_refunded": self.was_refunded, "convert_star_count": self.convert_star_count, "prepaid_upgrade_star_count": self.prepaid_upgrade_star_count, + "is_upgrade_separate": self.is_upgrade_separate, + "unique_gift_number": self.unique_gift_number, } ogr = OwnedGiftRegular.de_json(json_dict, offline_bot) assert ogr.gift == self.gift @@ -223,6 +230,8 @@ def test_de_json(self, offline_bot): assert ogr.was_refunded == self.was_refunded assert ogr.convert_star_count == self.convert_star_count assert ogr.prepaid_upgrade_star_count == self.prepaid_upgrade_star_count + assert ogr.is_upgrade_separate == self.is_upgrade_separate + assert ogr.unique_gift_number == self.unique_gift_number assert ogr.api_kwargs == {} def test_to_dict(self, owned_gift_regular): @@ -241,6 +250,8 @@ def test_to_dict(self, owned_gift_regular): assert json_dict["was_refunded"] == self.was_refunded assert json_dict["convert_star_count"] == self.convert_star_count assert json_dict["prepaid_upgrade_star_count"] == self.prepaid_upgrade_star_count + assert json_dict["is_upgrade_separate"] == self.is_upgrade_separate + assert json_dict["unique_gift_number"] == self.unique_gift_number def test_parse_entity(self, owned_gift_regular): entity = MessageEntity(MessageEntity.BOLD, 0, 4) @@ -393,6 +404,7 @@ class OwnedGiftsTestBase: ), OwnedGiftUnique( gift=UniqueGift( + gift_id="gift_id", base_name="human_readable", name="unique_name", number=10, diff --git a/tests/test_story.py b/tests/test_story.py index f29c5c857ae..7b786ffd68b 100644 --- a/tests/test_story.py +++ b/tests/test_story.py @@ -18,13 +18,20 @@ import pytest -from telegram import Chat, Story +from telegram import Bot, Chat, Story +from tests.auxil.bot_method_checks import ( + check_defaults_handling, + check_shortcut_call, + check_shortcut_signature, +) from tests.auxil.slots import mro_slots @pytest.fixture(scope="module") -def story(): - return Story(StoryTestBase.chat, StoryTestBase.id) +def story(bot): + story = Story(StoryTestBase.chat, StoryTestBase.id) + story.set_bot(bot) + return story class StoryTestBase: @@ -69,3 +76,32 @@ def test_equality(self): assert a != e assert hash(a) != hash(e) + + async def test_instance_method_repost(self, monkeypatch, story): + async def make_assertion(*_, **kwargs): + chat_id = kwargs["from_chat_id"] == story.chat.id + story_id = kwargs["from_story_id"] == story.id + return chat_id and story_id + + assert check_shortcut_signature( + Story.repost, + Bot.repost_story, + [ + "from_chat_id", + "from_story_id", + ], + additional_kwargs=[], + ) + assert await check_shortcut_call( + story.repost, + story.get_bot(), + "repost_story", + shortcut_kwargs=["from_chat_id", "from_story_id"], + ) + assert await check_defaults_handling(story.repost, story.get_bot()) + + monkeypatch.setattr(story.get_bot(), "repost_story", make_assertion) + assert await story.repost( + business_connection_id="bcid", + active_period=3600, + ) diff --git a/tests/test_uniquegift.py b/tests/test_uniquegift.py index 3a21ad3fca9..73c753beb51 100644 --- a/tests/test_uniquegift.py +++ b/tests/test_uniquegift.py @@ -28,6 +28,7 @@ UniqueGift, UniqueGiftBackdrop, UniqueGiftBackdropColors, + UniqueGiftColors, UniqueGiftInfo, UniqueGiftModel, UniqueGiftSymbol, @@ -37,9 +38,96 @@ from tests.auxil.slots import mro_slots +@pytest.fixture +def unique_gift_colors(): + return UniqueGiftColors( + model_custom_emoji_id=UniqueGiftColorsTestBase.model_custom_emoji_id, + symbol_custom_emoji_id=UniqueGiftColorsTestBase.symbol_custom_emoji_id, + light_theme_main_color=UniqueGiftColorsTestBase.light_theme_main_color, + light_theme_other_colors=UniqueGiftColorsTestBase.light_theme_other_colors, + dark_theme_main_color=UniqueGiftColorsTestBase.dark_theme_main_color, + dark_theme_other_colors=UniqueGiftColorsTestBase.dark_theme_other_colors, + ) + + +class UniqueGiftColorsTestBase: + model_custom_emoji_id = "model_emoji_id" + symbol_custom_emoji_id = "symbol_emoji_id" + light_theme_main_color = 0xFFFFFF + light_theme_other_colors = [0xAAAAAA, 0xBBBBBB] + dark_theme_main_color = 0x000000 + dark_theme_other_colors = [0x111111, 0x222222] + + +class TestUniqueGiftColorsWithoutRequest(UniqueGiftColorsTestBase): + def test_slot_behaviour(self, unique_gift_colors): + for attr in unique_gift_colors.__slots__: + assert getattr(unique_gift_colors, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(unique_gift_colors)) == len(set(mro_slots(unique_gift_colors))), ( + "duplicate slot" + ) + + def test_de_json(self, offline_bot): + json_dict = { + "model_custom_emoji_id": self.model_custom_emoji_id, + "symbol_custom_emoji_id": self.symbol_custom_emoji_id, + "light_theme_main_color": self.light_theme_main_color, + "light_theme_other_colors": self.light_theme_other_colors, + "dark_theme_main_color": self.dark_theme_main_color, + "dark_theme_other_colors": self.dark_theme_other_colors, + } + unique_gift_colors = UniqueGiftColors.de_json(json_dict, offline_bot) + assert unique_gift_colors.api_kwargs == {} + assert unique_gift_colors.model_custom_emoji_id == self.model_custom_emoji_id + assert unique_gift_colors.symbol_custom_emoji_id == self.symbol_custom_emoji_id + assert unique_gift_colors.light_theme_main_color == self.light_theme_main_color + assert unique_gift_colors.light_theme_other_colors == tuple(self.light_theme_other_colors) + assert unique_gift_colors.dark_theme_main_color == self.dark_theme_main_color + assert unique_gift_colors.dark_theme_other_colors == tuple(self.dark_theme_other_colors) + + def test_to_dict(self, unique_gift_colors): + json_dict = unique_gift_colors.to_dict() + assert json_dict["model_custom_emoji_id"] == self.model_custom_emoji_id + assert json_dict["symbol_custom_emoji_id"] == self.symbol_custom_emoji_id + assert json_dict["light_theme_main_color"] == self.light_theme_main_color + assert json_dict["light_theme_other_colors"] == self.light_theme_other_colors + assert json_dict["dark_theme_main_color"] == self.dark_theme_main_color + assert json_dict["dark_theme_other_colors"] == self.dark_theme_other_colors + + def test_equality(self, unique_gift_colors): + a = unique_gift_colors + b = UniqueGiftColors( + self.model_custom_emoji_id, + self.symbol_custom_emoji_id, + self.light_theme_main_color, + self.light_theme_other_colors, + self.dark_theme_main_color, + self.dark_theme_other_colors, + ) + c = UniqueGiftColors( + "other_model_emoji_id", + self.symbol_custom_emoji_id, + self.light_theme_main_color, + self.light_theme_other_colors, + self.dark_theme_main_color, + self.dark_theme_other_colors, + ) + d = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + @pytest.fixture def unique_gift(): return UniqueGift( + gift_id=UniqueGiftTestBase.gift_id, base_name=UniqueGiftTestBase.base_name, name=UniqueGiftTestBase.name, number=UniqueGiftTestBase.number, @@ -47,10 +135,14 @@ def unique_gift(): symbol=UniqueGiftTestBase.symbol, backdrop=UniqueGiftTestBase.backdrop, publisher_chat=UniqueGiftTestBase.publisher_chat, + is_premium=UniqueGiftTestBase.is_premium, + is_from_blockchain=UniqueGiftTestBase.is_from_blockchain, + colors=UniqueGiftTestBase.colors, ) class UniqueGiftTestBase: + gift_id = "gift_id" base_name = "human_readable" name = "unique_name" number = 10 @@ -70,6 +162,16 @@ class UniqueGiftTestBase: rarity_per_mille=30, ) publisher_chat = Chat(1, Chat.PRIVATE) + is_premium = False + is_from_blockchain = True + colors = UniqueGiftColors( + model_custom_emoji_id="M", + symbol_custom_emoji_id="S", + light_theme_main_color=0xFFFFFF, + light_theme_other_colors=[0xAAAAAA], + dark_theme_main_color=0x000000, + dark_theme_other_colors=[0x111111], + ) class TestUniqueGiftWithoutRequest(UniqueGiftTestBase): @@ -80,6 +182,7 @@ def test_slot_behaviour(self, unique_gift): def test_de_json(self, offline_bot): json_dict = { + "gift_id": self.gift_id, "base_name": self.base_name, "name": self.name, "number": self.number, @@ -87,6 +190,9 @@ def test_de_json(self, offline_bot): "symbol": self.symbol.to_dict(), "backdrop": self.backdrop.to_dict(), "publisher_chat": self.publisher_chat.to_dict(), + "is_premium": self.is_premium, + "is_from_blockchain": self.is_from_blockchain, + "colors": self.colors.to_dict(), } unique_gift = UniqueGift.de_json(json_dict, offline_bot) assert unique_gift.api_kwargs == {} @@ -98,11 +204,15 @@ def test_de_json(self, offline_bot): assert unique_gift.symbol == self.symbol assert unique_gift.backdrop == self.backdrop assert unique_gift.publisher_chat == self.publisher_chat + assert unique_gift.is_premium == self.is_premium + assert unique_gift.is_from_blockchain == self.is_from_blockchain + assert unique_gift.colors == self.colors def test_to_dict(self, unique_gift): gift_dict = unique_gift.to_dict() assert isinstance(gift_dict, dict) + assert gift_dict["gift_id"] == self.gift_id assert gift_dict["base_name"] == self.base_name assert gift_dict["name"] == self.name assert gift_dict["number"] == self.number @@ -110,26 +220,31 @@ def test_to_dict(self, unique_gift): assert gift_dict["symbol"] == self.symbol.to_dict() assert gift_dict["backdrop"] == self.backdrop.to_dict() assert gift_dict["publisher_chat"] == self.publisher_chat.to_dict() + assert gift_dict["is_premium"] == self.is_premium + assert gift_dict["is_from_blockchain"] == self.is_from_blockchain + assert gift_dict["colors"] == self.colors.to_dict() def test_equality(self, unique_gift): a = unique_gift b = UniqueGift( - self.base_name, - self.name, - self.number, - self.model, - self.symbol, - self.backdrop, - self.publisher_chat, + gift_id=self.gift_id, + base_name=self.base_name, + name=self.name, + number=self.number, + model=self.model, + symbol=self.symbol, + backdrop=self.backdrop, + publisher_chat=self.publisher_chat, ) c = UniqueGift( - "other_base_name", - self.name, - self.number, - self.model, - self.symbol, - self.backdrop, - self.publisher_chat, + gift_id=self.gift_id, + base_name="other_base_name", + name=self.name, + number=self.number, + model=self.model, + symbol=self.symbol, + backdrop=self.backdrop, + publisher_chat=self.publisher_chat, ) d = BotCommand("start", "description") @@ -142,6 +257,19 @@ def test_equality(self, unique_gift): assert a != d assert hash(a) != hash(d) + def test_gift_id_required_workaround(self): + # tags: deprecated NEXT.VERSION, bot api 9.3 + with pytest.raises(TypeError, match="`gift_id` is a required"): + UniqueGift( + base_name=self.base_name, + name=self.name, + number=self.number, + model=self.model, + symbol=self.symbol, + backdrop=self.backdrop, + publisher_chat=self.publisher_chat, + ) + @pytest.fixture def unique_gift_model(): @@ -402,20 +530,21 @@ def unique_gift_info(): class UniqueGiftInfoTestBase: gift = UniqueGift( - "human_readable_name", - "unique_name", - 10, - UniqueGiftModel( + gift_id="gift_id", + base_name="human_readable_name", + name="unique_name", + number=10, + model=UniqueGiftModel( name="model_name", sticker=Sticker("file_id1", "file_unique_id1", 512, 512, False, False, "regular"), rarity_per_mille=10, ), - UniqueGiftSymbol( + symbol=UniqueGiftSymbol( name="symbol_name", sticker=Sticker("file_id2", "file_unique_id2", 512, 512, True, True, "mask"), rarity_per_mille=20, ), - UniqueGiftBackdrop( + backdrop=UniqueGiftBackdrop( name="backdrop_name", colors=UniqueGiftBackdropColors(0x00FF00, 0xEE00FF, 0xAA22BB, 0x20FE8F), rarity_per_mille=2, diff --git a/tests/test_user.py b/tests/test_user.py index 490aa6052ec..dd8eaa46a63 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -44,6 +44,7 @@ def json_dict(): "added_to_attachment_menu": UserTestBase.added_to_attachment_menu, "can_connect_to_business": UserTestBase.can_connect_to_business, "has_main_web_app": UserTestBase.has_main_web_app, + "has_topics_enabled": UserTestBase.has_topics_enabled, } @@ -63,6 +64,7 @@ def user(bot): added_to_attachment_menu=UserTestBase.added_to_attachment_menu, can_connect_to_business=UserTestBase.can_connect_to_business, has_main_web_app=UserTestBase.has_main_web_app, + has_topics_enabled=UserTestBase.has_topics_enabled, ) user.set_bot(bot) user._unfreeze() @@ -83,6 +85,7 @@ class UserTestBase: added_to_attachment_menu = False can_connect_to_business = True has_main_web_app = False + has_topics_enabled = False class TestUserWithoutRequest(UserTestBase): @@ -108,6 +111,7 @@ def test_de_json(self, json_dict, offline_bot): assert user.added_to_attachment_menu == self.added_to_attachment_menu assert user.can_connect_to_business == self.can_connect_to_business assert user.has_main_web_app == self.has_main_web_app + assert user.has_topics_enabled == self.has_topics_enabled def test_to_dict(self, user): user_dict = user.to_dict() @@ -126,6 +130,7 @@ def test_to_dict(self, user): assert user_dict["added_to_attachment_menu"] == user.added_to_attachment_menu assert user_dict["can_connect_to_business"] == user.can_connect_to_business assert user_dict["has_main_web_app"] == user.has_main_web_app + assert user_dict["has_topics_enabled"] == user.has_topics_enabled def test_equality(self): a = User(self.id_, self.first_name, self.is_bot, self.last_name) @@ -231,6 +236,25 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(user.get_bot(), "send_message", make_assertion) assert await user.send_message("test") + async def test_instance_method_send_message_draft(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == user.id + and kwargs["draft_id"] == 123 + and kwargs["text"] == "test" + ) + + assert check_shortcut_signature( + User.send_message_draft, Bot.send_message_draft, ["chat_id"], [] + ) + assert await check_shortcut_call( + user.send_message_draft, user.get_bot(), "send_message_draft" + ) + assert await check_defaults_handling(user.send_message_draft, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_message_draft", make_assertion) + assert await user.send_message_draft(123, "test") + async def test_instance_method_send_photo(self, monkeypatch, user): async def make_assertion(*_, **kwargs): return kwargs["chat_id"] == user.id and kwargs["photo"] == "test_photo" @@ -805,3 +829,41 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(user.get_bot(), "remove_user_verification", make_assertion) assert await user.remove_verification() + + async def test_instance_method_repost_story(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["from_chat_id"] == user.id + + assert check_shortcut_signature( + User.repost_story, + Bot.repost_story, + [ + "from_chat_id", + ], + additional_kwargs=[], + ) + assert await check_shortcut_call( + user.repost_story, + user.get_bot(), + "repost_story", + shortcut_kwargs=["from_chat_id"], + ) + assert await check_defaults_handling(user.repost_story, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "repost_story", make_assertion) + assert await user.repost_story( + business_connection_id="bcid", + from_story_id=123, + active_period=3600, + ) + + async def test_instance_method_get_gifts(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return kwargs["user_id"] == user.id + + assert check_shortcut_signature(user.get_gifts, Bot.get_user_gifts, ["user_id"], []) + assert await check_shortcut_call(user.get_gifts, user.get_bot(), "get_user_gifts") + assert await check_defaults_handling(user.get_gifts, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "get_user_gifts", make_assertion) + assert await user.get_gifts() diff --git a/tests/test_userrating.py b/tests/test_userrating.py new file mode 100644 index 00000000000..effcdebc68b --- /dev/null +++ b/tests/test_userrating.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2026 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. + +import pytest + +from telegram import BotCommand, UserRating +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def user_rating(): + return UserRating( + level=UserRatingTestBase.level, + rating=UserRatingTestBase.rating, + current_level_rating=UserRatingTestBase.current_level_rating, + next_level_rating=UserRatingTestBase.next_level_rating, + ) + + +class UserRatingTestBase: + level = 2 + rating = 120 + current_level_rating = 100 + next_level_rating = 180 + + +class TestUserRatingWithoutRequest(UserRatingTestBase): + def test_slot_behaviour(self, user_rating): + for attr in user_rating.__slots__: + assert getattr(user_rating, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(user_rating)) == len(set(mro_slots(user_rating))), "duplicate slot" + + def test_de_json_with_next(self, offline_bot): + json_dict = { + "level": self.level, + "rating": self.rating, + "current_level_rating": self.current_level_rating, + "next_level_rating": self.next_level_rating, + } + ur = UserRating.de_json(json_dict, offline_bot) + assert ur.api_kwargs == {} + + assert ur.level == self.level + assert ur.rating == self.rating + assert ur.current_level_rating == self.current_level_rating + assert ur.next_level_rating == self.next_level_rating + + def test_de_json_no_optional(self, offline_bot): + json_dict = { + "level": self.level, + "rating": self.rating, + "current_level_rating": self.current_level_rating, + } + ur = UserRating.de_json(json_dict, offline_bot) + assert ur.api_kwargs == {} + + assert ur.level == self.level + assert ur.rating == self.rating + assert ur.current_level_rating == self.current_level_rating + assert ur.next_level_rating is None + + def test_to_dict(self, user_rating): + ur_dict = user_rating.to_dict() + + assert isinstance(ur_dict, dict) + assert ur_dict["level"] == user_rating.level + assert ur_dict["rating"] == user_rating.rating + assert ur_dict["current_level_rating"] == user_rating.current_level_rating + assert ur_dict["next_level_rating"] == user_rating.next_level_rating + + def test_equality(self): + a = UserRating(3, 200, 150, 300) + b = UserRating(3, 200, 100, None) + c = UserRating(3, 201, 150, 300) + d = UserRating(4, 200, 150, 300) + e = BotCommand("start", "description") + + assert a == b + assert hash(a) == hash(b) + + assert a != c + assert hash(a) != hash(c) + + assert a != d + assert hash(a) != hash(d) + + assert a != e + assert hash(a) != hash(e)