diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst index 3189de1c1d3..dee73ce4c79 100644 --- a/docs/source/inclusions/bot_methods.rst +++ b/docs/source/inclusions/bot_methods.rst @@ -25,6 +25,8 @@ - Used for sending documents * - :meth:`~telegram.Bot.send_game` - Used for sending a game + * - :meth:`~telegram.Bot.send_gift` + - Used for sending a gift * - :meth:`~telegram.Bot.send_invoice` - Used for sending an invoice * - :meth:`~telegram.Bot.send_location` @@ -369,6 +371,8 @@ - Used for logging out from cloud Bot API server * - :meth:`~telegram.Bot.get_file` - 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_me` - Used for getting basic information about the bot * - :meth:`~telegram.Bot.get_star_transactions` diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index bdeb7015985..22abbfb3867 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -93,7 +93,6 @@ Available Types telegram.inputpaidmediaphoto telegram.inputpaidmediavideo telegram.inputpolloption - telegram.inputsticker telegram.keyboardbutton telegram.keyboardbuttonpolltype telegram.keyboardbuttonrequestchat diff --git a/docs/source/telegram.gift.rst b/docs/source/telegram.gift.rst new file mode 100644 index 00000000000..e42cb720ac2 --- /dev/null +++ b/docs/source/telegram.gift.rst @@ -0,0 +1,6 @@ +Gift +==== + +.. autoclass:: telegram.Gift + :members: + :show-inheritance: diff --git a/docs/source/telegram.gifts.rst b/docs/source/telegram.gifts.rst new file mode 100644 index 00000000000..649522d0dce --- /dev/null +++ b/docs/source/telegram.gifts.rst @@ -0,0 +1,6 @@ +Gifts +===== + +.. autoclass:: telegram.Gifts + :members: + :show-inheritance: diff --git a/docs/source/telegram.stickers-tree.rst b/docs/source/telegram.stickers-tree.rst index 2ea687183c0..e45dcacb56b 100644 --- a/docs/source/telegram.stickers-tree.rst +++ b/docs/source/telegram.stickers-tree.rst @@ -6,6 +6,9 @@ The following methods and objects allow your bot to handle stickers and sticker .. toctree:: :titlesonly: + telegram.gift + telegram.gifts + telegram.inputsticker telegram.maskposition telegram.sticker telegram.stickerset diff --git a/telegram/__init__.py b/telegram/__init__.py index a4902d4d882..69f06976dd7 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -101,6 +101,8 @@ "GameHighScore", "GeneralForumTopicHidden", "GeneralForumTopicUnhidden", + "Gift", + "Gifts", "Giveaway", "GiveawayCompleted", "GiveawayCreated", @@ -373,6 +375,7 @@ from ._games.callbackgame import CallbackGame from ._games.game import Game from ._games.gamehighscore import GameHighScore +from ._gifts import Gift, Gifts from ._giveaway import Giveaway, GiveawayCompleted, GiveawayCreated, GiveawayWinners from ._inline.inlinekeyboardbutton import InlineKeyboardButton from ._inline.inlinekeyboardmarkup import InlineKeyboardMarkup diff --git a/telegram/_bot.py b/telegram/_bot.py index cc2ba38fb3a..f6a460ecece 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -75,6 +75,7 @@ from telegram._files.voice import Voice from telegram._forumtopic import ForumTopic from telegram._games.gamehighscore import GameHighScore +from telegram._gifts import Gift, Gifts from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton from telegram._menubutton import MenuButton from telegram._message import Message @@ -9475,6 +9476,99 @@ async def edit_chat_subscription_invite_link( return ChatInviteLink.de_json(result, self) # type: ignore[return-value] + async def get_available_gifts( + self, + *, + 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: Optional[JSONDict] = None, + ) -> Gifts: + """Returns the list of gifts that can be sent by the bot to users. + Requires no parameters. + + .. versionadded:: NEXT.VERSION + + Returns: + :class:`telegram.Gifts` + + Raises: + :class:`telegram.error.TelegramError` + """ + return Gifts.de_json( # type: ignore[return-value] + await self._post( + "getAvailableGifts", + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + ) + + async def send_gift( + self, + user_id: int, + gift_id: Union[str, Gift], + text: Optional[str] = None, + text_parse_mode: ODVInput[str] = DEFAULT_NONE, + text_entities: Optional[Sequence["MessageEntity"]] = 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: Optional[JSONDict] = None, + ) -> bool: + """Sends a gift to the given user. + The gift can't be converted to Telegram Stars by the user + + .. versionadded:: NEXT.VERSION + + Args: + user_id (:obj:`int`): Unique identifier of the target user that will receive the gift + gift_id (:obj:`str` | :class:`~telegram.Gift`): Identifier of the gift or a + :class:`~telegram.Gift` object + text (:obj:`str`, optional): Text that will be shown along with the gift; + 0- :tg-const:`telegram.constants.GiftLimit.MAX_TEXT_LENGTH` characters + text_parse_mode (:obj:`str`, optional): Mode for parsing entities. + See :class:`telegram.constants.ParseMode` and + `formatting options `__ for + more details. Entities other than :attr:`~MessageEntity.BOLD`, + :attr:`~MessageEntity.ITALIC`, :attr:`~MessageEntity.UNDERLINE`, + :attr:`~MessageEntity.STRIKETHROUGH`, :attr:`~MessageEntity.SPOILER`, and + :attr:`~MessageEntity.CUSTOM_EMOJI` are ignored. + text_entities (Sequence[:class:`telegram.MessageEntity`], optional): A list of special + entities that appear in the gift text. It can be specified instead of + :paramref:`text_parse_mode`. Entities other than :attr:`~MessageEntity.BOLD`, + :attr:`~MessageEntity.ITALIC`, :attr:`~MessageEntity.UNDERLINE`, + :attr:`~MessageEntity.STRIKETHROUGH`, :attr:`~MessageEntity.SPOILER`, and + :attr:`~MessageEntity.CUSTOM_EMOJI` are ignored. + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + + Raises: + :class:`telegram.error.TelegramError` + """ + data: JSONDict = { + "user_id": user_id, + "gift_id": gift_id.id if isinstance(gift_id, Gift) else gift_id, + "text": text, + "text_parse_mode": text_parse_mode, + "text_entities": text_entities, + } + return await self._post( + "sendGift", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + 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} @@ -9735,3 +9829,7 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`create_chat_subscription_invite_link`""" editChatSubscriptionInviteLink = edit_chat_subscription_invite_link """Alias for :meth:`edit_chat_subscription_invite_link`""" + getAvailableGifts = get_available_gifts + """Alias for :meth:`get_available_gifts`""" + sendGift = send_gift + """Alias for :meth:`send_gift`""" diff --git a/telegram/_chat.py b/telegram/_chat.py index bb0e24b1da5..08321fe25d0 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -44,6 +44,7 @@ ChatMember, Contact, Document, + Gift, InlineKeyboardMarkup, InputMediaAudio, InputMediaDocument, @@ -3436,6 +3437,46 @@ async def send_paid_media( allow_paid_broadcast=allow_paid_broadcast, ) + async def send_gift( + self, + gift_id: Union[str, "Gift"], + text: Optional[str] = None, + text_parse_mode: ODVInput[str] = DEFAULT_NONE, + text_entities: Optional[Sequence["MessageEntity"]] = 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: Optional[JSONDict] = None, + ) -> bool: + """Shortcut for:: + + await bot.send_gift(user_id=update.effective_chat.id, *args, **kwargs ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_gift`. + + Caution: + Can only work, if the chat is a private chat, see :attr:`type`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().send_gift( + user_id=self.id, + gift_id=gift_id, + text=text, + text_parse_mode=text_parse_mode, + text_entities=text_entities, + 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/telegram/_gifts.py b/telegram/_gifts.py new file mode 100644 index 00000000000..63055974c41 --- /dev/null +++ b/telegram/_gifts.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python +# pylint: disable=redefined-builtin +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 classes related to gifs sent by bots.""" +from collections.abc import Sequence +from typing import TYPE_CHECKING, Optional + +from telegram._files.sticker import Sticker +from telegram._telegramobject import TelegramObject +from telegram._utils.argumentparsing import parse_sequence_arg +from telegram._utils.types import JSONDict + +if TYPE_CHECKING: + from telegram import Bot + + +class Gift(TelegramObject): + """This object represents a gift that can be sent by the bot. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal if their :attr:`id` is equal. + + .. versionadded:: NEXT.VERSION + + Args: + id (:obj:`str`): Unique identifier of the gift + sticker (:class:`~telegram.Sticker`): The sticker that represents the gift + star_count (:obj:`int`): The number of Telegram Stars that must be paid to send the sticker + total_count (:obj:`int`, optional): The total number of the gifts of this type that can be + sent; for limited gifts only + remaining_count (:obj:`int`, optional): The number of remaining gifts of this type that can + be sent; for limited gifts only + + Attributes: + id (:obj:`str`): Unique identifier of the gift + sticker (:class:`~telegram.Sticker`): The sticker that represents the gift + star_count (:obj:`int`): The number of Telegram Stars that must be paid to send the sticker + total_count (:obj:`int`): Optional. The total number of the gifts of this type that can be + sent; for limited gifts only + remaining_count (:obj:`int`): Optional. The number of remaining gifts of this type that can + be sent; for limited gifts only + + """ + + __slots__ = ("id", "remaining_count", "star_count", "sticker", "total_count") + + def __init__( + self, + id: str, + sticker: Sticker, + star_count: int, + total_count: Optional[int] = None, + remaining_count: Optional[int] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.id: str = id + self.sticker: Sticker = sticker + self.star_count: int = star_count + self.total_count: Optional[int] = total_count + self.remaining_count: Optional[int] = remaining_count + + self._id_attrs = (self.id,) + + self._freeze() + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Gift"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["sticker"] = Sticker.de_json(data.get("sticker"), bot) + return cls(**data) + + +class Gifts(TelegramObject): + """This object represent a list of gifts. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal if their :attr:`gifts` are equal. + + .. versionadded:: NEXT.VERSION + + Args: + gifts (Sequence[:class:`Gift`]): The sequence of gifts + + Attributes: + gifts (tuple[:class:`Gift`]): The sequence of gifts + + """ + + __slots__ = ("gifts",) + + def __init__( + self, + gifts: Sequence[Gift], + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.gifts: tuple[Gift, ...] = parse_sequence_arg(gifts) + + self._id_attrs = (self.gifts,) + + self._freeze() + + @classmethod + def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Gifts"]: + """See :meth:`telegram.TelegramObject.de_json`.""" + data = cls._parse_data(data) + + if not data: + return None + + data["gifts"] = Gift.de_list(data.get("gifts"), bot) + return cls(**data) diff --git a/telegram/_payment/stars.py b/telegram/_payment/stars.py index a47d3b44ff8..e6c685497be 100644 --- a/telegram/_payment/stars.py +++ b/telegram/_payment/stars.py @@ -19,11 +19,12 @@ # pylint: disable=redefined-builtin """This module contains the classes for Telegram Stars transactions.""" +import datetime as dtm from collections.abc import Sequence -from datetime import datetime from typing import TYPE_CHECKING, Final, Optional from telegram import constants +from telegram._gifts import Gift from telegram._paidmedia import PaidMedia from telegram._telegramobject import TelegramObject from telegram._user import User @@ -144,7 +145,7 @@ class RevenueWithdrawalStateSucceeded(RevenueWithdrawalState): def __init__( self, - date: datetime, + date: dtm.datetime, url: str, *, api_kwargs: Optional[JSONDict] = None, @@ -152,7 +153,7 @@ def __init__( super().__init__(type=RevenueWithdrawalState.SUCCEEDED, api_kwargs=api_kwargs) with self._unfrozen(): - self.date: datetime = date + self.date: dtm.datetime = date self.url: str = url self._id_attrs = ( self.type, @@ -328,30 +329,51 @@ class TransactionPartnerUser(TransactionPartner): Args: user (:class:`telegram.User`): Information about the user. invoice_payload (:obj:`str`, optional): Bot-specified invoice payload. + subscription_period (:class:`datetime.timedelta`, optional): The duration of the paid + subscription + + .. versionadded:: NEXT.VERSION paid_media (Sequence[:class:`telegram.PaidMedia`], optional): Information about the paid media bought by the user. .. versionadded:: 21.5 - paid_media_payload (:obj:`str`, optional): Optional. Bot-specified paid media payload. + paid_media_payload (:obj:`str`, optional): Bot-specified paid media payload. .. versionadded:: 21.6 + gift (:class:`telegram.Gift`, optional): The gift sent to the user by the bot + + .. versionadded:: NEXT.VERSION Attributes: type (:obj:`str`): The type of the transaction partner, always :tg-const:`telegram.TransactionPartner.USER`. user (:class:`telegram.User`): Information about the user. invoice_payload (:obj:`str`): Optional. Bot-specified invoice payload. + subscription_period (:class:`datetime.timedelta`): Optional. The duration of the paid + subscription + + .. versionadded:: NEXT.VERSION paid_media (tuple[:class:`telegram.PaidMedia`]): Optional. Information about the paid media bought by the user. .. versionadded:: 21.5 - paid_media_payload (:obj:`str`): Optional. Optional. Bot-specified paid media payload. + paid_media_payload (:obj:`str`): Optional. Bot-specified paid media payload. .. versionadded:: 21.6 + gift (:class:`telegram.Gift`): Optional. The gift sent to the user by the bot + + .. versionadded:: NEXT.VERSION """ - __slots__ = ("invoice_payload", "paid_media", "paid_media_payload", "user") + __slots__ = ( + "gift", + "invoice_payload", + "paid_media", + "paid_media_payload", + "subscription_period", + "user", + ) def __init__( self, @@ -359,6 +381,8 @@ def __init__( invoice_payload: Optional[str] = None, paid_media: Optional[Sequence[PaidMedia]] = None, paid_media_payload: Optional[str] = None, + subscription_period: Optional[dtm.timedelta] = None, + gift: Optional[Gift] = None, *, api_kwargs: Optional[JSONDict] = None, ) -> None: @@ -369,6 +393,9 @@ def __init__( self.invoice_payload: Optional[str] = invoice_payload self.paid_media: Optional[tuple[PaidMedia, ...]] = parse_sequence_arg(paid_media) self.paid_media_payload: Optional[str] = paid_media_payload + self.subscription_period: Optional[dtm.timedelta] = subscription_period + self.gift: Optional[Gift] = gift + self._id_attrs = ( self.type, self.user, @@ -386,6 +413,12 @@ def de_json( data["user"] = User.de_json(data.get("user"), bot) data["paid_media"] = PaidMedia.de_list(data.get("paid_media"), bot=bot) + data["subscription_period"] = ( + dtm.timedelta(seconds=sp) + if (sp := data.get("subscription_period")) is not None + else None + ) + data["gift"] = Gift.de_json(data.get("gift"), bot) return super().de_json(data=data, bot=bot) # type: ignore[return-value] @@ -496,7 +529,7 @@ def __init__( self, id: str, amount: int, - date: datetime, + date: dtm.datetime, source: Optional[TransactionPartner] = None, receiver: Optional[TransactionPartner] = None, *, @@ -505,7 +538,7 @@ def __init__( super().__init__(api_kwargs=api_kwargs) self.id: str = id self.amount: int = amount - self.date: datetime = date + self.date: dtm.datetime = date self.source: Optional[TransactionPartner] = source self.receiver: Optional[TransactionPartner] = receiver diff --git a/telegram/_telegramobject.py b/telegram/_telegramobject.py index 6f5038da44b..1b29095e824 100644 --- a/telegram/_telegramobject.py +++ b/telegram/_telegramobject.py @@ -636,6 +636,8 @@ def to_dict(self, recursive: bool = True) -> JSONDict: elif isinstance(value, datetime.datetime): out[key] = to_timestamp(value) + elif isinstance(value, datetime.timedelta): + out[key] = value.total_seconds() for key in pop_keys: out.pop(key) diff --git a/telegram/_user.py b/telegram/_user.py index 980ce7e4991..2a4b4dc7a99 100644 --- a/telegram/_user.py +++ b/telegram/_user.py @@ -36,6 +36,7 @@ Audio, Contact, Document, + Gift, InlineKeyboardMarkup, InputMediaAudio, InputMediaDocument, @@ -1646,6 +1647,43 @@ async def send_poll( allow_paid_broadcast=allow_paid_broadcast, ) + async def send_gift( + self, + gift_id: Union[str, "Gift"], + text: Optional[str] = None, + text_parse_mode: ODVInput[str] = DEFAULT_NONE, + text_entities: Optional[Sequence["MessageEntity"]] = 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: Optional[JSONDict] = None, + ) -> bool: + """Shortcut for:: + + await bot.send_gift( user_id=update.effective_user.id, *args, **kwargs ) + + For the documentation of the arguments, please see :meth:`telegram.Bot.send_gift`. + + .. versionadded:: NEXT.VERSION + + Returns: + :obj:`bool`: On success, :obj:`True` is returned. + """ + return await self.get_bot().send_gift( + user_id=self.id, + gift_id=gift_id, + text=text, + text_parse_mode=text_parse_mode, + text_entities=text_entities, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + async def send_copy( self, from_chat_id: Union[str, int], diff --git a/telegram/constants.py b/telegram/constants.py index 10f887a938f..e92fd36ac04 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -64,6 +64,7 @@ "FloodLimit", "ForumIconColor", "ForumTopicLimit", + "GiftLimit", "GiveawayLimit", "InlineKeyboardButtonLimit", "InlineKeyboardMarkupLimit", @@ -1224,6 +1225,21 @@ class ForumIconColor(IntEnum): """ +class GiftLimit(IntEnum): + """This enum contains limitations for :meth:`~telegram.Bot.send_gift`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + MAX_TEXT_LENGTH = 255 + """:obj:`int`: Maximum number of characters in a :obj:`str` passed as the + :paramref:`~telegram.Bot.send_gift.text` parameter of :meth:`~telegram.Bot.send_gift`. + """ + + class GiveawayLimit(IntEnum): """This enum contains limitations for :class:`telegram.Giveaway` and related classes. The enum members of this enumeration are instances of :class:`int` and can be treated as such. diff --git a/telegram/ext/_defaults.py b/telegram/ext/_defaults.py index 93e7e3748b8..03191462252 100644 --- a/telegram/ext/_defaults.py +++ b/telegram/ext/_defaults.py @@ -188,6 +188,7 @@ def __init__( "explanation_parse_mode", "link_preview_options", "parse_mode", + "text_parse_mode", "protect_content", "question_parse_mode", ): @@ -271,7 +272,8 @@ def quote_parse_mode(self, _: object) -> NoReturn: @property def text_parse_mode(self) -> Optional[str]: """:obj:`str`: Optional. Alias for :attr:`parse_mode`, used for - the corresponding parameter of :class:`telegram.InputPollOption`. + the corresponding parameter of :class:`telegram.InputPollOption` and + :meth:`telegram.Bot.send_gift`. .. versionadded:: 21.2 """ diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index 66921e415df..dede8def27f 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -57,6 +57,8 @@ File, ForumTopic, GameHighScore, + Gift, + Gifts, InlineKeyboardMarkup, InlineQueryResultsButton, InputMedia, @@ -4355,6 +4357,52 @@ async def edit_chat_subscription_invite_link( api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), ) + async def get_available_gifts( + self, + *, + 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: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> Gifts: + return await super().get_available_gifts( + 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_gift( + self, + user_id: int, + gift_id: Union[str, Gift], + text: Optional[str] = None, + text_parse_mode: ODVInput[str] = DEFAULT_NONE, + text_entities: Optional[Sequence["MessageEntity"]] = 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: Optional[JSONDict] = None, + rate_limit_args: Optional[RLARGS] = None, + ) -> bool: + return await super().send_gift( + user_id=user_id, + gift_id=gift_id, + text=text, + text_parse_mode=text_parse_mode, + text_entities=text_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), + ) + # updated camelCase aliases getMe = get_me sendMessage = send_message @@ -4481,3 +4529,5 @@ async def edit_chat_subscription_invite_link( createChatSubscriptionInviteLink = create_chat_subscription_invite_link editChatSubscriptionInviteLink = edit_chat_subscription_invite_link sendPaidMedia = send_paid_media + getAvailableGifts = get_available_gifts + sendGift = send_gift diff --git a/tests/test_bot.py b/tests/test_bot.py index 8ff0dec8d7b..b2f8d884740 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2345,6 +2345,9 @@ class TestBotWithRequest: is tested in `test_callbackdatacache` """ + # get_available_gifts, send_gift are tested in `test_gift`. + # No need to duplicate here. + async def test_invalid_token_server_response(self): with pytest.raises(InvalidToken, match="The token `12` was rejected by the server."): async with ExtBot(token="12"): diff --git a/tests/test_chat.py b/tests/test_chat.py index 966e820162e..b2db4e36f76 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1311,6 +1311,28 @@ async def make_assertion(*_, **kwargs): media="media", star_count=42, caption="stars", payload="payload" ) + async def test_instance_method_send_gift(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["user_id"] == chat.id + and kwargs["gift_id"] == "gift_id" + and kwargs["text"] == "text" + and kwargs["text_parse_mode"] == "text_parse_mode" + and kwargs["text_entities"] == "text_entities" + ) + + assert check_shortcut_signature(Chat.send_gift, Bot.send_gift, ["user_id"], []) + assert await check_shortcut_call(chat.send_gift, chat.get_bot(), "send_gift") + assert await check_defaults_handling(chat.send_gift, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "send_gift", make_assertion) + assert await chat.send_gift( + gift_id="gift_id", + text="text", + text_parse_mode="text_parse_mode", + text_entities="text_entities", + ) + 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_gifts.py b/tests/test_gifts.py new file mode 100644 index 00000000000..e7e13c75cef --- /dev/null +++ b/tests/test_gifts.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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/]. +from collections.abc import Sequence + +import pytest + +from telegram import BotCommand, Gift, Gifts, MessageEntity, Sticker +from telegram._utils.defaultvalue import DEFAULT_NONE +from telegram.request import RequestData +from tests.auxil.slots import mro_slots + + +@pytest.fixture +def gift(request): + return Gift( + id=GiftTestBase.id, + sticker=GiftTestBase.sticker, + star_count=GiftTestBase.star_count, + total_count=GiftTestBase.total_count, + remaining_count=GiftTestBase.remaining_count, + ) + + +class GiftTestBase: + id = "some_id" + sticker = Sticker( + file_id="file_id", + file_unique_id="file_unique_id", + width=512, + height=512, + is_animated=False, + is_video=False, + type="regular", + ) + star_count = 5 + total_count = 10 + remaining_count = 5 + + +class TestGiftWithoutRequest(GiftTestBase): + def test_slot_behaviour(self, gift): + for attr in gift.__slots__: + assert getattr(gift, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(gift)) == len(set(mro_slots(gift))), "duplicate slot" + + def test_de_json(self, offline_bot, gift): + json_dict = { + "id": self.id, + "sticker": self.sticker.to_dict(), + "star_count": self.star_count, + "total_count": self.total_count, + "remaining_count": self.remaining_count, + } + gift = Gift.de_json(json_dict, offline_bot) + assert gift.api_kwargs == {} + + assert gift.id == self.id + assert gift.sticker == self.sticker + assert gift.star_count == self.star_count + assert gift.total_count == self.total_count + assert gift.remaining_count == self.remaining_count + + assert Gift.de_json(None, offline_bot) is None + + def test_to_dict(self, gift): + gift_dict = gift.to_dict() + + assert isinstance(gift_dict, dict) + assert gift_dict["id"] == self.id + assert gift_dict["sticker"] == self.sticker.to_dict() + assert gift_dict["star_count"] == self.star_count + assert gift_dict["total_count"] == self.total_count + assert gift_dict["remaining_count"] == self.remaining_count + + def test_equality(self, gift): + a = gift + b = Gift(self.id, self.sticker, self.star_count, self.total_count, self.remaining_count) + c = Gift( + "other_uid", self.sticker, self.star_count, self.total_count, self.remaining_count + ) + 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.mark.parametrize( + "gift", + [ + "gift_id", + Gift( + "gift_id", + Sticker("file_id", "file_unique_id", 512, 512, False, False, "regular"), + 5, + 10, + 5, + ), + ], + ids=["string", "Gift"], + ) + async def test_send_gift(self, offline_bot, gift, monkeypatch): + # We can't send actual gifts, so we just check that the correct parameters are passed + text_entities = [ + MessageEntity(MessageEntity.TEXT_LINK, 0, 4, "url"), + MessageEntity(MessageEntity.BOLD, 5, 9), + ] + + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + user_id = request_data.parameters["user_id"] == "user_id" + gift_id = request_data.parameters["gift_id"] == "gift_id" + text = request_data.parameters["text"] == "text" + text_parse_mode = request_data.parameters["text_parse_mode"] == "text_parse_mode" + tes = request_data.parameters["text_entities"] == [ + me.to_dict() for me in text_entities + ] + return user_id and gift_id and text and text_parse_mode and tes + + monkeypatch.setattr(offline_bot.request, "post", make_assertion) + assert await offline_bot.send_gift( + "user_id", gift, "text", text_parse_mode="text_parse_mode", text_entities=text_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_gift_default_parse_mode( + self, default_bot, monkeypatch, passed_value, expected_value + ): + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + return request_data.parameters.get("text_parse_mode") == expected_value + + monkeypatch.setattr(default_bot.request, "post", make_assertion) + kwargs = { + "user_id": "user_id", + "gift_id": "gift_id", + } + if passed_value is not DEFAULT_NONE: + kwargs["text_parse_mode"] = passed_value + + assert await default_bot.send_gift(**kwargs) + + +@pytest.fixture +def gifts(request): + return Gifts(gifts=GiftsTestBase.gifts) + + +class GiftsTestBase: + gifts: Sequence[Gift] = [ + Gift( + id="id1", + sticker=Sticker( + file_id="file_id", + file_unique_id="file_unique_id", + width=512, + height=512, + is_animated=False, + is_video=False, + type="regular", + ), + star_count=5, + total_count=5, + remaining_count=5, + ), + Gift( + id="id2", + sticker=Sticker( + file_id="file_id", + file_unique_id="file_unique_id", + width=512, + height=512, + is_animated=False, + is_video=False, + type="regular", + ), + star_count=6, + total_count=6, + remaining_count=6, + ), + Gift( + id="id3", + sticker=Sticker( + file_id="file_id", + file_unique_id="file_unique_id", + width=512, + height=512, + is_animated=False, + is_video=False, + type="regular", + ), + star_count=7, + total_count=7, + remaining_count=7, + ), + ] + + +class TestGiftsWithoutRequest(GiftsTestBase): + def test_slot_behaviour(self, gifts): + for attr in gifts.__slots__: + assert getattr(gifts, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(gifts)) == len(set(mro_slots(gifts))), "duplicate slot" + + def test_de_json(self, offline_bot, gifts): + json_dict = {"gifts": [gift.to_dict() for gift in self.gifts]} + gifts = Gifts.de_json(json_dict, offline_bot) + assert gifts.api_kwargs == {} + + assert gifts.gifts == tuple(self.gifts) + for de_json_gift, original_gift in zip(gifts.gifts, self.gifts): + assert de_json_gift.id == original_gift.id + assert de_json_gift.sticker == original_gift.sticker + assert de_json_gift.star_count == original_gift.star_count + assert de_json_gift.total_count == original_gift.total_count + assert de_json_gift.remaining_count == original_gift.remaining_count + + assert Gifts.de_json(None, offline_bot) is None + + def test_to_dict(self, gifts): + gifts_dict = gifts.to_dict() + + assert isinstance(gifts_dict, dict) + assert gifts_dict["gifts"] == [gift.to_dict() for gift in self.gifts] + + def test_equality(self, gifts): + a = gifts + b = Gifts(self.gifts) + c = Gifts(self.gifts[:2]) + 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) + + +class TestGiftsWithRequest(GiftTestBase): + async def test_get_available_gifts(self, bot, chat_id): + # We don't control the available gifts, so we can not make any better assertions + assert isinstance(await bot.get_available_gifts(), Gifts) diff --git a/tests/test_stars.py b/tests/test_stars.py index 12329b62e75..5fb7a3c4068 100644 --- a/tests/test_stars.py +++ b/tests/test_stars.py @@ -24,6 +24,7 @@ from telegram import ( Dice, + Gift, PaidMediaPhoto, PhotoSize, RevenueWithdrawalState, @@ -32,6 +33,7 @@ RevenueWithdrawalStateSucceeded, StarTransaction, StarTransactions, + Sticker, TransactionPartner, TransactionPartnerFragment, TransactionPartnerOther, @@ -76,6 +78,22 @@ def transaction_partner_user(): ) ], paid_media_payload="payload", + subscription_period=datetime.timedelta(days=1), + gift=Gift( + id="some_id", + sticker=Sticker( + file_id="file_id", + file_unique_id="file_unique_id", + width=512, + height=512, + is_animated=False, + is_video=False, + type="regular", + ), + star_count=5, + total_count=10, + remaining_count=5, + ), ) @@ -515,6 +533,20 @@ def test_equality(self, transaction_partner, offline_bot): assert hash(c) != hash(f) +class TestTransactionPartnerUserWithoutRequest(TransactionPartnerTestBase): + def test_de_json_required(self, offline_bot): + json_dict = { + "user": transaction_partner_user().user.to_dict(), + } + tp = TransactionPartnerUser.de_json(json_dict, offline_bot) + assert tp.api_kwargs == {} + assert tp.user == transaction_partner_user().user + + # This test is here mainly to check that the below cases work + assert tp.subscription_period is None + assert tp.gift is None + + class RevenueWithdrawalStateTestBase: date = datetime.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC) url = "url" diff --git a/tests/test_user.py b/tests/test_user.py index d8a6265f0cf..ede518f6f8f 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -720,3 +720,25 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(user.get_bot(), "refund_star_payment", make_assertion) assert await user.refund_star_payment(telegram_payment_charge_id=42) + + async def test_instance_method_send_gift(self, monkeypatch, user): + async def make_assertion(*_, **kwargs): + return ( + kwargs["user_id"] == user.id + and kwargs["gift_id"] == "gift_id" + and kwargs["text"] == "text" + and kwargs["text_parse_mode"] == "text_parse_mode" + and kwargs["text_entities"] == "text_entities" + ) + + assert check_shortcut_signature(user.send_gift, Bot.send_gift, ["user_id"], []) + assert await check_shortcut_call(user.send_gift, user.get_bot(), "send_gift") + assert await check_defaults_handling(user.send_gift, user.get_bot()) + + monkeypatch.setattr(user.get_bot(), "send_gift", make_assertion) + assert await user.send_gift( + gift_id="gift_id", + text="text", + text_parse_mode="text_parse_mode", + text_entities="text_entities", + ) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy