From ee6b4295bb5b3a450aff47abd704f010e3f01489 Mon Sep 17 00:00:00 2001 From: poolitzer Date: Wed, 14 Aug 2024 11:17:05 +0200 Subject: [PATCH 01/14] Feat: API 7.9 --- README.rst | 4 +- telegram/__init__.py | 9 ++- telegram/_bot.py | 130 ++++++++++++++++++++++++++++++++++++- telegram/_chat.py | 2 + telegram/_chatmember.py | 15 ++++- telegram/_message.py | 34 +++++----- telegram/_payment/stars.py | 15 ++++- telegram/_reaction.py | 42 ++++++++++-- telegram/constants.py | 27 +++++++- telegram/ext/_extbot.py | 55 +++++++++++++++- tests/test_bot.py | 26 ++++++++ tests/test_reaction.py | 8 ++- 12 files changed, 333 insertions(+), 34 deletions(-) diff --git a/README.rst b/README.rst index b2de996d2ed..f41e1288f7a 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-7.8-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-7.9-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 **7.8** are natively supported by this library. +All types and methods of the Telegram Bot API **7.9** 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/telegram/__init__.py b/telegram/__init__.py index 5b52bf85c40..7b5803a3e9f 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -204,6 +204,7 @@ "ReactionType", "ReactionTypeCustomEmoji", "ReactionTypeEmoji", + "ReactionTypePaid", "RefundedPayment", "ReplyKeyboardMarkup", "ReplyKeyboardRemove", @@ -467,7 +468,13 @@ from ._payment.successfulpayment import SuccessfulPayment from ._poll import InputPollOption, Poll, PollAnswer, PollOption from ._proximityalerttriggered import ProximityAlertTriggered -from ._reaction import ReactionCount, ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji +from ._reaction import ( + ReactionCount, + ReactionType, + ReactionTypeCustomEmoji, + ReactionTypeEmoji, + ReactionTypePaid, +) from ._reply import ExternalReplyInfo, ReplyParameters, TextQuote from ._replykeyboardmarkup import ReplyKeyboardMarkup from ._replykeyboardremove import ReplyKeyboardRemove diff --git a/telegram/_bot.py b/telegram/_bot.py index d825a88789e..d9638990c34 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -18,6 +18,7 @@ # 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 Bot.""" + import asyncio import contextlib import copy @@ -8946,7 +8947,7 @@ async def set_message_reaction( """ Use this method to change the chosen reactions on a message. Service messages can't be reacted to. Automatically forwarded messages from a channel to its discussion group have - the same available reactions as messages in the channel. + the same available reactions as messages in the channel. Bots can't use paid reactions. .. versionadded:: 20.8 @@ -8959,7 +8960,8 @@ async def set_message_reaction( :class:`telegram.ReactionType` | :obj:`str`, optional): A list of reaction types to set on the message. Currently, as non-premium users, bots can set up to one reaction per message. A custom emoji reaction can be used if it is either - already present on the message or explicitly allowed by chat administrators. + already present on the message or explicitly allowed by chat administrators. Paid + reactions can't be used by bots. Tip: Passed :obj:`str` values will be converted to either @@ -9201,6 +9203,7 @@ async def send_paid_media( protect_content: ODVInput[bool] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, reply_markup: Optional[ReplyMarkup] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -9215,7 +9218,9 @@ async def send_paid_media( .. versionadded:: 21.4 Args: - chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| If the chat is a channel, all + Telegram Star proceeds from this media will be credited to the chat's balance. + Otherwise, they will be credited to the bot's balance. star_count (:obj:`int`): The number of Telegram Stars that must be paid to buy access to the media. media (Sequence[:class:`telegram.InputPaidMedia`]): A list describing the media to be @@ -9233,6 +9238,9 @@ async def send_paid_media( :class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional): Additional interface options. An object for an inline keyboard, custom reply keyboard, instructions to remove reply keyboard or to force a reply from the user. + business_connection_id (:obj:`str`, optional): |business_id_str| + + .. versionadded:: NEXT.VERSION Keyword Args: allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| @@ -9274,8 +9282,120 @@ async def send_paid_media( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, + ) + + async def create_chat_subscription_invite_link( + self, + chat_id: Union[str, int], + subscription_period: int, + subscription_price: int, + name: Optional[str] = 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, + ) -> ChatInviteLink: + """ + Use this method to create a `subscription invite link `_ for a channel chat. + The bot must have :attr:`telegram.ChatPermissions.can_invite_users` administrator + right. The link can be edited using the :meth:`edit_chat_subscription_invite_link` or + revoked using the :meth:`revoke_chat_invite_Link`. + + + .. versionadded:: NEXT.VERSION + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + subscription_period (:obj:`int`): The number of seconds the subscription will be + active for before the next payment. Currently, it must always be + :tg-const:`telegram.constants.ChatSubscriptionLimit.SUBSCRIPTION_PERIOD` (30 days). + subscription_price (:obj:`int`): The number of Telegram Stars a user must pay initially + and after each subsequent subscription period to be a member of the chat; + :tg-const:`telegram.constants.ChatSubscriptionLimit.MIN_PRICE`- + :tg-const:`telegram.constants.ChatSubscriptionLimit.MAX_PRICE`. + name (:obj:`str`, optional): Invite link name; + 0-:tg-const:`telegram.constants.ChatInviteLinkLimit.NAME_LENGTH` characters. + + Returns: + :class:`telegram.ChatInviteLink` + + Raises: + :class:`telegram.error.TelegramError` + + """ + data: JSONDict = { + "chat_id": chat_id, + "subscription_period": subscription_period, + "subscription_price": subscription_price, + "name": name, + } + + result = await self._post( + "createChatSubscriptionInviteLink", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, ) + return ChatInviteLink.de_json(result, self) # type: ignore[return-value] + + async def edit_chat_subscription_invite_link( + self, + chat_id: Union[str, int], + invite_link: Union[str, "ChatInviteLink"], + name: Optional[str] = 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, + ) -> ChatInviteLink: + """ + Use this method to edit a subscription invite link created by the bot. The bot must have + :attr:`telegram.ChatPermissions.can_invite_users` administrator right. + + .. versionadded:: NEXT.VERSION + + Args: + chat_id (:obj:`int` | :obj:`str`): |chat_id_channel| + invite_link (:obj:`str` | :obj:`telegram.ChatInviteLink`): The invite link to edit. + name (:obj:`str`, optional): Invite link name; + 0-:tg-const:`telegram.constants.ChatInviteLinkLimit.NAME_LENGTH` characters. + + Returns: + :class:`telegram.ChatInviteLink` + + Raises: + :class:`telegram.error.TelegramError` + + """ + link = invite_link.invite_link if isinstance(invite_link, ChatInviteLink) else invite_link + data: JSONDict = { + "chat_id": chat_id, + "invite_link": link, + "name": name, + } + + result = await self._post( + "editChatSubscriptionInviteLink", + data, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + return ChatInviteLink.de_json(result, self) # type: ignore[return-value] + 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} @@ -9532,3 +9652,7 @@ def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002 """Alias for :meth:`get_star_transactions`""" sendPaidMedia = send_paid_media """Alias for :meth:`send_paid_media`""" + createChatSubscriptionInviteLink = create_chat_subscription_invite_link + """Alias for :meth:`create_chat_subscription_invite_link`""" + editChatSubscriptionInviteLink = edit_chat_subscription_invite_link + """Alias for :meth:`edit_chat_subscription_invite_link`""" diff --git a/telegram/_chat.py b/telegram/_chat.py index 02d80c94714..92bf3cf6558 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -3274,6 +3274,7 @@ async def send_paid_media( protect_content: ODVInput[bool] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, reply_markup: Optional[ReplyMarkup] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -3314,6 +3315,7 @@ async def send_paid_media( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=api_kwargs, + business_connection_id=business_connection_id, ) diff --git a/telegram/_chatmember.py b/telegram/_chatmember.py index 0cc06bf5804..3c0a9e218c0 100644 --- a/telegram/_chatmember.py +++ b/telegram/_chatmember.py @@ -17,6 +17,7 @@ # 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 ChatMember.""" + import datetime from typing import TYPE_CHECKING, Dict, Final, Optional, Type @@ -391,24 +392,34 @@ class ChatMemberMember(ChatMember): Args: user (:class:`telegram.User`): Information about the user. + until_date (:class:`datetime.datetime`, optional): Date when the user's subscription will + expire. + + .. versionadded:: NEXT.VERSION Attributes: status (:obj:`str`): The member's status in the chat, always :tg-const:`telegram.ChatMember.MEMBER`. user (:class:`telegram.User`): Information about the user. + until_date (:class:`datetime.datetime`): Optional. Date when the user's subscription will + expire. + + .. versionadded:: NEXT.VERSION """ - __slots__ = () + __slots__ = ("until_date",) def __init__( self, user: User, + until_date: Optional[datetime.datetime] = None, *, api_kwargs: Optional[JSONDict] = None, ): super().__init__(status=ChatMember.MEMBER, user=user, api_kwargs=api_kwargs) - self._freeze() + with self._unfrozen(): + self.until_date: Optional[datetime.datetime] = until_date class ChatMemberRestricted(ChatMember): diff --git a/telegram/_message.py b/telegram/_message.py index 7d077a4d9a7..5e416ab7a50 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -280,15 +280,14 @@ class Message(MaybeInaccessibleMessage): Args: message_id (:obj:`int`): Unique message identifier inside this chat. - from_user (:class:`telegram.User`, optional): Sender of the message; empty for messages - sent to channels. For backward compatibility, this will contain a fake sender user in - non-channel chats, if the message was sent on behalf of a chat. - sender_chat (:class:`telegram.Chat`, optional): Sender of the message, sent on behalf of a - chat. For example, the channel itself for channel posts, the supergroup itself for - messages from anonymous group administrators, the linked channel for messages - automatically forwarded to the discussion group. For backward compatibility, - :attr:`from_user` contains a fake sender user in non-channel chats, if the message was - sent on behalf of a chat. + from_user (:class:`telegram.User`, optional): Sender of the message; may be empty for + messages sent to channels. For backward compatibility, if the message was sent on + behalf of a chat, the field contains a fake sender user in non-channel chats. + sender_chat (:class:`telegram.Chat`, optional): Sender of the message when sent on behalf + of a chat. For example, the supergroup itself for messages sent by its anonymous + administrators or a linked channel for messages automatically forwarded to the + channel's discussion group. For backward compatibility, if the message was sent on + behalf of a chat, the field from contains a fake sender user in non-channel chats. date (:class:`datetime.datetime`): Date the message was sent in Unix time. Converted to :class:`datetime.datetime`. @@ -591,15 +590,14 @@ class Message(MaybeInaccessibleMessage): Attributes: message_id (:obj:`int`): Unique message identifier inside this chat. - from_user (:class:`telegram.User`): Optional. Sender of the message; empty for messages - sent to channels. For backward compatibility, this will contain a fake sender user in - non-channel chats, if the message was sent on behalf of a chat. - sender_chat (:class:`telegram.Chat`): Optional. Sender of the message, sent on behalf of a - chat. For example, the channel itself for channel posts, the supergroup itself for - messages from anonymous group administrators, the linked channel for messages - automatically forwarded to the discussion group. For backward compatibility, - :attr:`from_user` contains a fake sender user in non-channel chats, if the message was - sent on behalf of a chat. + from_user (:class:`telegram.User`): Optional. Sender of the message; may be empty for + messages sent to channels. For backward compatibility, if the message was sent on + behalf of a chat, the field contains a fake sender user in non-channel chats. + sender_chat (:class:`telegram.Chat`): Optional. Sender of the message when sent on behalf + of a chat. For example, the supergroup itself for messages sent by its anonymous + administrators or a linked channel for messages automatically forwarded to the + channel's discussion group. For backward compatibility, if the message was sent on + behalf of a chat, the field from contains a fake sender user in non-channel chats. date (:class:`datetime.datetime`): Date the message was sent in Unix time. Converted to :class:`datetime.datetime`. diff --git a/telegram/_payment/stars.py b/telegram/_payment/stars.py index b176f2315fe..a2698ab0703 100644 --- a/telegram/_payment/stars.py +++ b/telegram/_payment/stars.py @@ -23,6 +23,7 @@ from typing import TYPE_CHECKING, Dict, Final, Optional, Sequence, Tuple, Type from telegram import constants +from telegram._paidmedia import PaidMedia from telegram._telegramobject import TelegramObject from telegram._user import User from telegram._utils import enum @@ -310,20 +311,30 @@ class TransactionPartnerUser(TransactionPartner): Args: user (:class:`telegram.User`): Information about the user. invoice_payload (:obj:`str`, optional): Bot-specified invoice payload. + paid_media (Sequence[:class:`telegram.PaidMedia`], optional): Information about the paid + media bought by the user. 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. + paid_media (Sequence[:class:`telegram.PaidMedia`]): Optional. Information about the paid + media bought by the user. + """ - __slots__ = ("invoice_payload", "user") + __slots__ = ( + "invoice_payload", + "paid_media", + "user", + ) def __init__( self, user: "User", invoice_payload: Optional[str] = None, + paid_media: Optional[Sequence[PaidMedia]] = None, *, api_kwargs: Optional[JSONDict] = None, ) -> None: @@ -332,6 +343,7 @@ def __init__( with self._unfrozen(): self.user: User = user self.invoice_payload: Optional[str] = invoice_payload + self.paid_media: Optional[Sequence[PaidMedia]] = paid_media self._id_attrs = ( self.type, self.user, @@ -347,6 +359,7 @@ def de_json( return None data["user"] = User.de_json(data.get("user"), bot) + data["paid_media"] = PaidMedia.de_list(data.get("paid_media"), bot=bot) return super().de_json(data=data, bot=bot) # type: ignore[return-value] diff --git a/telegram/_reaction.py b/telegram/_reaction.py index d1ba718f0d6..bf205c138fc 100644 --- a/telegram/_reaction.py +++ b/telegram/_reaction.py @@ -17,6 +17,7 @@ # 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 objects that represents a Telegram ReactionType.""" + from typing import TYPE_CHECKING, Final, Literal, Optional, Union from telegram import constants @@ -30,16 +31,22 @@ class ReactionType(TelegramObject): """Base class for Telegram ReactionType Objects. - There exist :class:`telegram.ReactionTypeEmoji` and :class:`telegram.ReactionTypeCustomEmoji`. + There exist :class:`telegram.ReactionTypeEmoji`, :class:`telegram.ReactionTypeCustomEmoji` + and :class:`telegram.ReactionTypePaid`. .. versionadded:: 20.8 + .. versionchanged:: NEXT.VERSION + + Added paid reaction. Args: type (:obj:`str`): Type of the reaction. Can be - :attr:`~telegram.ReactionType.EMOJI` or :attr:`~telegram.ReactionType.CUSTOM_EMOJI`. + :attr:`~telegram.ReactionType.EMOJI`, :attr:`~telegram.ReactionType.CUSTOM_EMOJI` or + :attr:`~telegram.ReactionType.PAID`. Attributes: type (:obj:`str`): Type of the reaction. Can be - :attr:`~telegram.ReactionType.EMOJI` or :attr:`~telegram.ReactionType.CUSTOM_EMOJI`. + :attr:`~telegram.ReactionType.EMOJI`, :attr:`~telegram.ReactionType.CUSTOM_EMOJI` or + :attr:`~telegram.ReactionType.PAID`. """ @@ -49,11 +56,16 @@ class ReactionType(TelegramObject): """:const:`telegram.constants.ReactionType.EMOJI`""" CUSTOM_EMOJI: Final[constants.ReactionType] = constants.ReactionType.CUSTOM_EMOJI """:const:`telegram.constants.ReactionType.CUSTOM_EMOJI`""" + PAID: Final[constants.ReactionType] = constants.ReactionType.PAID + """:const:`telegram.constants.ReactionType.PAID` + + .. versionadded:: NEXT.VERSION + """ def __init__( self, type: Union[ # pylint: disable=redefined-builtin - Literal["emoji", "custom_emoji"], constants.ReactionType + Literal["emoji", "custom_emoji", "paid"], constants.ReactionType ], *, api_kwargs: Optional[JSONDict] = None, @@ -152,6 +164,28 @@ def __init__( self._id_attrs = (self.custom_emoji_id,) +class ReactionTypePaid(ReactionType): + """ + The reaction is paid. + + .. versionadded:: NEXT.VERSION + + Attributes: + type (:obj:`str`): Type of the reaction, + always :tg-const:`telegram.ReactionType.PAID`. + """ + + __slots__ = () + + def __init__( + self, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(type=ReactionType.PAID, api_kwargs=api_kwargs) + self._freeze() + + class ReactionCount(TelegramObject): """This class represents a reaction added to a message along with the number of times it was added. diff --git a/telegram/constants.py b/telegram/constants.py index 1aba1f2a93a..c8328f6cc91 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -54,6 +54,7 @@ "ChatLimit", "ChatMemberStatus", "ChatPhotoSize", + "ChatSubscriptionLimit", "ChatType", "ContactLimit", "CustomEmojiStickerLimit", @@ -151,7 +152,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=8) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=9) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -2903,6 +2904,11 @@ class ReactionType(StringEnum): """:obj:`str`: A :class:`telegram.ReactionType` with a normal emoji.""" CUSTOM_EMOJI = "custom_emoji" """:obj:`str`: A :class:`telegram.ReactionType` with a custom emoji.""" + PAID = "paid" + """:obj:`str`: A :class:`telegram.ReactionType` with a paid reaction. + + .. versionadded:: NEXT.VERSION + """ class ReactionEmoji(StringEnum): @@ -3096,3 +3102,22 @@ class BackgroundFillType(StringEnum): """:obj:`str`: A :class:`telegram.BackgroundFill` with gradient fill.""" FREEFORM_GRADIENT = "freeform_gradient" """:obj:`str`: A :class:`telegram.BackgroundFill` with freeform_gradient fill.""" + + +class ChatSubscriptionLimit(IntEnum): + """This enum contains limitations for + :paramref:`telegram.Bot.create_chat_subscription_invite_link.subscription_period` and + :paramref:`telegram.Bot.create_chat_subscription_invite_link.subscription_price`. + The enum members of this enumeration are instances of :class:`int` and can be treated as such. + + .. versionadded:: NEXT.VERSION + """ + + __slots__ = () + + SUBSCRIPTION_PERIOD = 2592000 + """:obj:`int`: The number of seconds the subscription will be active.""" + MIN_PRICE = 1 + """:obj:`int`: Amount of stars a user pays, minimum amount the subscription can be set to.""" + MAX_PRICE = 2500 + """:obj:`int`: Amount of stars a user pays, maxcimum amount the subscription can be set to.""" diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index d85d8822de4..f5fea8097b4 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -4234,6 +4234,7 @@ async def send_paid_media( protect_content: ODVInput[bool] = DEFAULT_NONE, reply_parameters: Optional["ReplyParameters"] = None, reply_markup: Optional[ReplyMarkup] = None, + business_connection_id: Optional[str] = None, *, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, reply_to_message_id: Optional[int] = None, @@ -4263,6 +4264,57 @@ async def send_paid_media( connect_timeout=connect_timeout, pool_timeout=pool_timeout, api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), + business_connection_id=business_connection_id, + ) + + async def create_chat_subscription_invite_link( + self, + chat_id: Union[str, int], + subscription_period: int, + subscription_price: int, + name: Optional[str] = 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, + ) -> ChatInviteLink: + return await super().create_chat_subscription_invite_link( + chat_id=chat_id, + subscription_period=subscription_period, + subscription_price=subscription_price, + name=name, + 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 edit_chat_subscription_invite_link( + self, + chat_id: Union[str, int], + invite_link: Union[str, "ChatInviteLink"], + name: Optional[str] = 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, + ) -> ChatInviteLink: + return await super().edit_chat_subscription_invite_link( + chat_id=chat_id, + invite_link=invite_link, + name=name, + 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 @@ -4388,4 +4440,5 @@ async def send_paid_media( replaceStickerInSet = replace_sticker_in_set refundStarPayment = refund_star_payment getStarTransactions = get_star_transactions - sendPaidMedia = send_paid_media + createChatSubscriptionInviteLink = create_chat_subscription_invite_link + editChatSubscriptionInviteLink = edit_chat_subscription_invite_link diff --git a/tests/test_bot.py b/tests/test_bot.py index cdf00082d59..40c676528f9 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2265,6 +2265,21 @@ async def do_request(url, request_data: RequestData, *args, **kwargs): obj = await bot.get_star_transactions(offset=3) assert isinstance(obj, StarTransactions) + async def test_subscription_link( + self, + monkeypatch, + bot, + ): + # Since the chat invite link object does not say if the sub args are passed we can + # only check here + async def make_assertion(url, request_data: RequestData, *args, **kwargs): + assert request_data.parameters.get("subscription_period") == 2592000 + assert request_data.parameters.get("subscription_price") == 6 + + monkeypatch.setattr(bot.request, "post", make_assertion) + + await bot.create_chat_subscription_invite_link(1234, 2592000, 6) + class TestBotWithRequest: """ @@ -4261,3 +4276,14 @@ async def test_get_star_transactions(self, bot): transactions = await bot.get_star_transactions(limit=1) assert isinstance(transactions, StarTransactions) assert len(transactions.transactions) == 0 + + async def test_chat_subscription_links(self, bot, channel_id): + sub_link = await bot.create_chat_subscription_invite_link( + channel_id, "sub_name", 2592000, 1 + ) + assert sub_link.name == "sub_name" + + edited_link = await bot.edit_chat_subscription_invite_link( + chat_id=channel_id, invite_link=sub_link, name="sub_name_2" + ) + assert edited_link.name == "sub_name_2" diff --git a/tests/test_reaction.py b/tests/test_reaction.py index 6f3d3cb4621..31b74ddc34c 100644 --- a/tests/test_reaction.py +++ b/tests/test_reaction.py @@ -28,6 +28,7 @@ ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji, + ReactionTypePaid, ) from telegram.constants import ReactionEmoji from tests.auxil.slots import mro_slots @@ -48,6 +49,10 @@ def reaction_type_emoji(): return ReactionTypeEmoji(RTDefaults.normal_emoji) +def reaction_type_paid(): + return ReactionTypePaid() + + def make_json_dict(instance: ReactionType, include_optional_args: bool = False) -> dict: """Used to make the json dict which we use for testing de_json. Similar to iter_args()""" json_dict = {"type": instance.type} @@ -99,6 +104,7 @@ def reaction_type(request): [ reaction_type_custom_emoji, reaction_type_emoji, + reaction_type_paid, ], indirect=True, ) @@ -155,7 +161,7 @@ def test_to_dict(self, reaction_type): assert reaction_type_dict["type"] == reaction_type.type if reaction_type.type == ReactionType.EMOJI: assert reaction_type_dict["emoji"] == reaction_type.emoji - else: + elif reaction_type.type == ReactionType.CUSTOM_EMOJI: assert reaction_type_dict["custom_emoji_id"] == reaction_type.custom_emoji_id for slot in reaction_type.__slots__: # additional verification for the optional args From a58869a7c71107bc393f68e35e76202129e52531 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:48:42 +0200 Subject: [PATCH 02/14] Update telegram/_payment/stars.py Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com> --- telegram/_payment/stars.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/telegram/_payment/stars.py b/telegram/_payment/stars.py index a2698ab0703..8f5c38138ac 100644 --- a/telegram/_payment/stars.py +++ b/telegram/_payment/stars.py @@ -313,6 +313,8 @@ class TransactionPartnerUser(TransactionPartner): invoice_payload (:obj:`str`, optional): Bot-specified invoice payload. paid_media (Sequence[:class:`telegram.PaidMedia`], optional): Information about the paid media bought by the user. + + .. versionadded:: NEXT.VERSION Attributes: type (:obj:`str`): The type of the transaction partner, @@ -322,6 +324,7 @@ class TransactionPartnerUser(TransactionPartner): paid_media (Sequence[:class:`telegram.PaidMedia`]): Optional. Information about the paid media bought by the user. + .. versionadded:: NEXT.VERSION """ __slots__ = ( From 2d322afe559470af65cbaa2e12b0ed6f193139f0 Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:48:47 +0200 Subject: [PATCH 03/14] Update telegram/constants.py Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com> --- telegram/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegram/constants.py b/telegram/constants.py index c8328f6cc91..2867c6c7c4f 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -3120,4 +3120,4 @@ class ChatSubscriptionLimit(IntEnum): MIN_PRICE = 1 """:obj:`int`: Amount of stars a user pays, minimum amount the subscription can be set to.""" MAX_PRICE = 2500 - """:obj:`int`: Amount of stars a user pays, maxcimum amount the subscription can be set to.""" + """:obj:`int`: Amount of stars a user pays, maximum amount the subscription can be set to.""" From 39163b49d54561568c23cd19e3cedb11dcbc499e Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:27:14 +0200 Subject: [PATCH 04/14] Doc Fixes --- docs/source/telegram.at-tree.rst | 1 + docs/source/telegram.reactiontypepaid.rst | 6 ++++++ telegram/_bot.py | 15 +++++++-------- telegram/_reaction.py | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 docs/source/telegram.reactiontypepaid.rst diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst index 8d3238a27e4..bb6e4c81472 100644 --- a/docs/source/telegram.at-tree.rst +++ b/docs/source/telegram.at-tree.rst @@ -130,6 +130,7 @@ Available Types telegram.reactiontype telegram.reactiontypecustomemoji telegram.reactiontypeemoji + telegram.reactiontypepaid telegram.replykeyboardmarkup telegram.replykeyboardremove telegram.replyparameters diff --git a/docs/source/telegram.reactiontypepaid.rst b/docs/source/telegram.reactiontypepaid.rst new file mode 100644 index 00000000000..f6ed05af115 --- /dev/null +++ b/docs/source/telegram.reactiontypepaid.rst @@ -0,0 +1,6 @@ +ReactionTypePaid +================d + +.. autoclass:: telegram.ReactionTypePaid + :members: + :show-inheritance: diff --git a/telegram/_bot.py b/telegram/_bot.py index d9638990c34..1a4a4e7d64e 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -8180,7 +8180,7 @@ async def edit_forum_topic( ) -> 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 + 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. @@ -8448,7 +8448,7 @@ async def edit_general_forum_topic( ) -> bool: """ Use this method to edit the name of the 'General' topic in a forum supergroup chat. The bot - must be an administrator in the chat for this to work and must have + must be an administrator in the chat for this to work and must have the :attr:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights. .. versionadded:: 20.0 @@ -9213,7 +9213,7 @@ async def send_paid_media( pool_timeout: ODVInput[float] = DEFAULT_NONE, api_kwargs: Optional[JSONDict] = None, ) -> Message: - """Use this method to send paid media to channel chats. + """Use this method to send paid media. .. versionadded:: 21.4 @@ -9300,11 +9300,10 @@ async def create_chat_subscription_invite_link( ) -> ChatInviteLink: """ Use this method to create a `subscription invite link `_ for a channel chat. - The bot must have :attr:`telegram.ChatPermissions.can_invite_users` administrator - right. The link can be edited using the :meth:`edit_chat_subscription_invite_link` or - revoked using the :meth:`revoke_chat_invite_Link`. - + superchannels-star-reactions-subscriptions#star-subscriptions>`_ for a channel chat. + The bot must have the :attr:`~telegram.ChatPermissions.can_invite_users` administrator + right. The link can be edited using the :meth:`edit_chat_subscription_invite_link` or + revoked using the :meth:`revoke_chat_invite_link`. .. versionadded:: NEXT.VERSION diff --git a/telegram/_reaction.py b/telegram/_reaction.py index bf205c138fc..7ac36c9688f 100644 --- a/telegram/_reaction.py +++ b/telegram/_reaction.py @@ -32,7 +32,7 @@ class ReactionType(TelegramObject): """Base class for Telegram ReactionType Objects. There exist :class:`telegram.ReactionTypeEmoji`, :class:`telegram.ReactionTypeCustomEmoji` - and :class:`telegram.ReactionTypePaid`. + and :class:`telegram.ReactionTypePaid`. .. versionadded:: 20.8 .. versionchanged:: NEXT.VERSION From eef7862046d3e5a869b0c7a2d3b31e6e15dbeb3b Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:44:01 +0200 Subject: [PATCH 05/14] `Chat` shortcuts --- telegram/_chat.py | 75 ++++++++++++++++++++++++++++++++++++++ telegram/_payment/stars.py | 2 +- tests/test_chat.py | 48 ++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) diff --git a/telegram/_chat.py b/telegram/_chat.py index 92bf3cf6558..a73a504d8c5 100644 --- a/telegram/_chat.py +++ b/telegram/_chat.py @@ -2666,6 +2666,81 @@ async def revoke_invite_link( api_kwargs=api_kwargs, ) + async def create_subscription_invite_link( + self, + subscription_period: int, + subscription_price: int, + name: Optional[str] = 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, + ) -> "ChatInviteLink": + """Shortcut for:: + + await bot.create_chat_subscription_invite_link( + chat_id=update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.create_chat_subscription_invite_link`. + + .. versionadded:: NEXT.VERSION + + Returns: + :class:`telegram.ChatInviteLink` + """ + return await self.get_bot().create_chat_subscription_invite_link( + chat_id=self.id, + subscription_period=subscription_period, + subscription_price=subscription_price, + name=name, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + ) + + async def edit_subscription_invite_link( + self, + invite_link: Union[str, "ChatInviteLink"], + name: Optional[str] = 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, + ) -> "ChatInviteLink": + """Shortcut for:: + + await bot.edit_chat_subscription_invite_link( + chat_id=update.effective_chat.id, *args, **kwargs + ) + + For the documentation of the arguments, please see + :meth:`telegram.Bot.edit_chat_subscription_invite_link`. + + .. versionadded:: NEXT.VERSION + + Returns: + :class:`telegram.ChatInviteLink` + + """ + return await self.get_bot().edit_chat_subscription_invite_link( + chat_id=self.id, + invite_link=invite_link, + read_timeout=read_timeout, + write_timeout=write_timeout, + connect_timeout=connect_timeout, + pool_timeout=pool_timeout, + api_kwargs=api_kwargs, + name=name, + ) + async def approve_join_request( self, user_id: int, diff --git a/telegram/_payment/stars.py b/telegram/_payment/stars.py index 8f5c38138ac..aeb888e4823 100644 --- a/telegram/_payment/stars.py +++ b/telegram/_payment/stars.py @@ -313,7 +313,7 @@ class TransactionPartnerUser(TransactionPartner): invoice_payload (:obj:`str`, optional): Bot-specified invoice payload. paid_media (Sequence[:class:`telegram.PaidMedia`], optional): Information about the paid media bought by the user. - + .. versionadded:: NEXT.VERSION Attributes: diff --git a/tests/test_chat.py b/tests/test_chat.py index 682bdbe514a..28905934c3c 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -889,6 +889,54 @@ async def make_assertion(*_, **kwargs): monkeypatch.setattr(chat.get_bot(), "revoke_chat_invite_link", make_assertion) assert await chat.revoke_invite_link(invite_link=link) + async def test_create_subscription_invite_link(self, monkeypatch, chat): + async def make_assertion(*_, **kwargs): + return ( + kwargs["chat_id"] == chat.id + and kwargs["subscription_price"] == 42 + and kwargs["subscription_period"] == 42 + ) + + assert check_shortcut_signature( + Chat.create_subscription_invite_link, + Bot.create_chat_subscription_invite_link, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.create_subscription_invite_link, + chat.get_bot(), + "create_chat_subscription_invite_link", + ) + assert await check_defaults_handling(chat.create_subscription_invite_link, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "create_chat_subscription_invite_link", make_assertion) + assert await chat.create_subscription_invite_link( + subscription_price=42, subscription_period=42 + ) + + async def test_edit_subscription_invite_link(self, monkeypatch, chat): + link = "ThisIsALink" + + async def make_assertion(*_, **kwargs): + return kwargs["chat_id"] == chat.id and kwargs["invite_link"] == link + + assert check_shortcut_signature( + Chat.edit_subscription_invite_link, + Bot.edit_chat_subscription_invite_link, + ["chat_id"], + [], + ) + assert await check_shortcut_call( + chat.edit_subscription_invite_link, + chat.get_bot(), + "edit_chat_subscription_invite_link", + ) + assert await check_defaults_handling(chat.edit_subscription_invite_link, chat.get_bot()) + + monkeypatch.setattr(chat.get_bot(), "edit_chat_subscription_invite_link", make_assertion) + assert await chat.edit_subscription_invite_link(invite_link=link) + async def test_instance_method_get_menu_button(self, monkeypatch, chat): async def make_assertion(*_, **kwargs): return kwargs["chat_id"] == chat.id From 481bee159a9c41931fb7a2efbab1ac7311eb437b Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:57:37 +0200 Subject: [PATCH 06/14] Slightly extend tests --- tests/test_bot.py | 4 ++-- tests/test_chatmember.py | 6 ++++-- tests/test_stars.py | 12 ++++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/test_bot.py b/tests/test_bot.py index 40c676528f9..3d2ac8db6b9 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -2265,7 +2265,7 @@ async def do_request(url, request_data: RequestData, *args, **kwargs): obj = await bot.get_star_transactions(offset=3) assert isinstance(obj, StarTransactions) - async def test_subscription_link( + async def test_create_chat_subscription_invite_link( self, monkeypatch, bot, @@ -4277,7 +4277,7 @@ async def test_get_star_transactions(self, bot): assert isinstance(transactions, StarTransactions) assert len(transactions.transactions) == 0 - async def test_chat_subscription_links(self, bot, channel_id): + async def test_create_edit_chat_subscription_link(self, bot, channel_id): sub_link = await bot.create_chat_subscription_invite_link( channel_id, "sub_name", 2592000, 1 ) diff --git a/tests/test_chatmember.py b/tests/test_chatmember.py index b4eac51cfb3..4296fdd2723 100644 --- a/tests/test_chatmember.py +++ b/tests/test_chatmember.py @@ -101,7 +101,7 @@ def chat_member_administrator(): def chat_member_member(): - return ChatMemberMember(CMDefaults.user) + return ChatMemberMember(CMDefaults.user, until_date=CMDefaults.until_date) def chat_member_restricted(): @@ -230,7 +230,9 @@ def test_de_json_all_args(self, bot, chat_member_type): def test_de_json_chatmemberbanned_localization(self, chat_member_type, tz_bot, bot, raw_bot): # We only test two classes because the other three don't have datetimes in them. - if isinstance(chat_member_type, (ChatMemberBanned, ChatMemberRestricted)): + if isinstance( + chat_member_type, (ChatMemberBanned, ChatMemberRestricted, ChatMemberMember) + ): json_dict = make_json_dict(chat_member_type, include_optional_args=True) chatmember_raw = ChatMember.de_json(json_dict, raw_bot) chatmember_bot = ChatMember.de_json(json_dict, bot) diff --git a/tests/test_stars.py b/tests/test_stars.py index d3560af7d2f..10ed7e63b81 100644 --- a/tests/test_stars.py +++ b/tests/test_stars.py @@ -24,6 +24,8 @@ from telegram import ( Dice, + PaidMediaPhoto, + PhotoSize, RevenueWithdrawalState, RevenueWithdrawalStateFailed, RevenueWithdrawalStatePending, @@ -62,6 +64,16 @@ def withdrawal_state_pending(): def transaction_partner_user(): return TransactionPartnerUser( user=User(id=1, is_bot=False, first_name="first_name", username="username"), + invoice_payload="payload", + paid_media=[ + PaidMediaPhoto( + photo=[ + PhotoSize( + file_id="file_id", width=1, height=1, file_unique_id="file_unique_id" + ) + ] + ) + ], ) From 2dc0a3126dd8b4e44b164a2e9af64e3248a9c60a Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:04:28 +0200 Subject: [PATCH 07/14] Slight fix for TransactionPartnerUser --- telegram/_payment/stars.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telegram/_payment/stars.py b/telegram/_payment/stars.py index aeb888e4823..0baebd39d7f 100644 --- a/telegram/_payment/stars.py +++ b/telegram/_payment/stars.py @@ -321,7 +321,7 @@ class TransactionPartnerUser(TransactionPartner): always :tg-const:`telegram.TransactionPartner.USER`. user (:class:`telegram.User`): Information about the user. invoice_payload (:obj:`str`): Optional. Bot-specified invoice payload. - paid_media (Sequence[:class:`telegram.PaidMedia`]): Optional. Information about the paid + paid_media (Tuple[:class:`telegram.PaidMedia`]): Optional. Information about the paid media bought by the user. .. versionadded:: NEXT.VERSION @@ -346,7 +346,7 @@ def __init__( with self._unfrozen(): self.user: User = user self.invoice_payload: Optional[str] = invoice_payload - self.paid_media: Optional[Sequence[PaidMedia]] = paid_media + self.paid_media: Optional[Tuple[PaidMedia, ...]] = parse_sequence_arg(paid_media) self._id_attrs = ( self.type, self.user, From def75089e1747ac4ff34bb069d0b6e56ff521fea Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:18:50 +0200 Subject: [PATCH 08/14] try fixing some tests --- telegram/_reaction.py | 26 ++++++++++++++------------ telegram/ext/_extbot.py | 1 + 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/telegram/_reaction.py b/telegram/_reaction.py index 7ac36c9688f..55e8968e8eb 100644 --- a/telegram/_reaction.py +++ b/telegram/_reaction.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains objects that represents a Telegram ReactionType.""" -from typing import TYPE_CHECKING, Final, Literal, Optional, Union +from typing import TYPE_CHECKING, Dict, Final, Literal, Optional, Type, Union from telegram import constants from telegram._telegramobject import TelegramObject @@ -83,14 +83,20 @@ def de_json( """See :meth:`telegram.TelegramObject.de_json`.""" data = cls._parse_data(data) - if not data: + if data is None: + return None + + if not data and cls is ReactionType: return None - if cls is ReactionType and data.get("type") in [cls.EMOJI, cls.CUSTOM_EMOJI]: - reaction_type = data.pop("type") - if reaction_type == cls.EMOJI: - return ReactionTypeEmoji.de_json(data=data, bot=bot) - return ReactionTypeCustomEmoji.de_json(data=data, bot=bot) + _class_mapping: Dict[str, Type[ReactionType]] = { + cls.EMOJI: ReactionTypeEmoji, + cls.CUSTOM_EMOJI: ReactionTypeCustomEmoji, + cls.PAID: ReactionTypePaid, + } + + if cls is ReactionType and data.get("type") in _class_mapping: + return _class_mapping[data.pop("type")].de_json(data, bot) return super().de_json(data=data, bot=bot) @@ -177,11 +183,7 @@ class ReactionTypePaid(ReactionType): __slots__ = () - def __init__( - self, - *, - api_kwargs: Optional[JSONDict] = None, - ): + def __init__(self, *, api_kwargs: Optional[JSONDict] = None): super().__init__(type=ReactionType.PAID, api_kwargs=api_kwargs) self._freeze() diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py index f5fea8097b4..76b17bad02a 100644 --- a/telegram/ext/_extbot.py +++ b/telegram/ext/_extbot.py @@ -4442,3 +4442,4 @@ async def edit_chat_subscription_invite_link( getStarTransactions = get_star_transactions createChatSubscriptionInviteLink = create_chat_subscription_invite_link editChatSubscriptionInviteLink = edit_chat_subscription_invite_link + sendPaidMedia = send_paid_media From 89cbda0f09ae4fa635690655a2637b91f77ddd6b Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 19 Aug 2024 19:15:01 +0200 Subject: [PATCH 09/14] Doc fix --- telegram/_chatmember.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telegram/_chatmember.py b/telegram/_chatmember.py index 3c0a9e218c0..1eabaa14e56 100644 --- a/telegram/_chatmember.py +++ b/telegram/_chatmember.py @@ -395,7 +395,7 @@ class ChatMemberMember(ChatMember): until_date (:class:`datetime.datetime`, optional): Date when the user's subscription will expire. - .. versionadded:: NEXT.VERSION + .. versionadded:: NEXT.VERSION Attributes: status (:obj:`str`): The member's status in the chat, @@ -404,7 +404,7 @@ class ChatMemberMember(ChatMember): until_date (:class:`datetime.datetime`): Optional. Date when the user's subscription will expire. - .. versionadded:: NEXT.VERSION + .. versionadded:: NEXT.VERSION """ From 43f42f043ba97178f39a6a51164b9e787c5a57b5 Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 19 Aug 2024 19:51:45 +0200 Subject: [PATCH 10/14] Fix test_create_edit_chat_subscription_link by adding `subscription_channel_id` --- tests/auxil/ci_bots.py | 19 ++++++++++--------- tests/conftest.py | 5 +++++ tests/test_bot.py | 11 ++++++++--- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/tests/auxil/ci_bots.py b/tests/auxil/ci_bots.py index bdb25a2f0ee..069a65ccec9 100644 --- a/tests/auxil/ci_bots.py +++ b/tests/auxil/ci_bots.py @@ -29,15 +29,16 @@ # purposes than testing. FALLBACKS = ( "W3sidG9rZW4iOiAiNTc5Njk0NzE0OkFBRnBLOHc2emtrVXJENHhTZVl3RjNNTzhlLTRHcm1jeTdjIiwgInBheW1lbnRfc" - "HJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpRME5qWmxOekk1WWpKaSIsICJjaGF0X2 lkIjogIjY3NTY2N" - "jIyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTgzOD" - "AwNDU3NyIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIi wgIm5hbWUiOiAiUFRCIHRlc3RzIG" - "ZhbGxiYWNrIDEiLCAidXNlcm5hbWUiOiAiQHB0Yl9mYWxsYmFja18xX2JvdCJ9LCB7InRva2VuIjogIjU1ODE5NDA2Njp" - "BQUZ3RFBJRmx6R1VsQ2FXSHRUT0VYNFJGclg4dTlETXFmbyIsIC JwYXltZW50X3Byb3ZpZGVyX3Rva2VuIjogIjI4NDY" - "4NTA2MzpURVNUOllqRXdPRFF3TVRGbU5EY3kiLCAiY2hhdF9pZCI6ICI2NzU2NjYyMjQiLCAic3VwZXJfZ3JvdXBfaWQi" - "OiAiLTEwMDEyMjEyMTY4MzAiLCAiZm9ydW1fZ3 JvdXBfaWQiOiAiLTEwMDE4NTc4NDgzMTQiLCAiY2hhbm5lbF9pZCI6" - "ICJAcHl0aG9udGVsZWdyYW1ib3R0ZXN0cyIsICJuYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAyIiwgInVzZXJuYW1lI" - "jogIkBwdGJfZmFsbGJhY2tfMl9ib3QifV0=" + "HJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpRME5qWmxOekk1WWpKaSIsICJjaGF0X2lkIjogIjY3NTY2Nj" + "IyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTgzODA" + "wNDU3NyIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgIm5hbWUiOiAiUFRCIHRlc3RzIGZh" + "bGxiYWNrIDEiLCAidXNlcm5hbWUiOiAiQHB0Yl9mYWxsYmFja18xX2JvdCIsICJzdWJzY3JpcHRpb25fY2hhbm5lbF9pZ" + "CI6IC0xMDAyMjI5NjQ5MzAzfSwgeyJ0b2tlbiI6ICI1NTgxOTQwNjY6QUFGd0RQSUZsekdVbENhV0h0VE9FWDRSRnJYOH" + "U5RE1xZm8iLCAicGF5bWVudF9wcm92aWRlcl90b2tlbiI6ICIyODQ2ODUwNjM6VEVTVDpZakV3T0RRd01URm1ORGN5Iiw" + "gImNoYXRfaWQiOiAiNjc1NjY2MjI0IiwgInN1cGVyX2dyb3VwX2lkIjogIi0xMDAxMjIxMjE2ODMwIiwgImZvcnVtX2dy" + "b3VwX2lkIjogIi0xMDAxODU3ODQ4MzE0IiwgImNoYW5uZWxfaWQiOiAiQHB5dGhvbnRlbGVncmFtYm90dGVzdHMiLCAib" + "mFtZSI6ICJQVEIgdGVzdHMgZmFsbGJhY2sgMiIsICJ1c2VybmFtZSI6ICJAcHRiX2ZhbGxiYWNrXzJfYm90IiwgInN1Yn" + "NjcmlwdGlvbl9jaGFubmVsX2lkIjogLTEwMDIyMjk2NDkzMDN9XQ==" ) GITHUB_ACTION = os.getenv("GITHUB_ACTION", None) diff --git a/tests/conftest.py b/tests/conftest.py index dd553f9fe82..c721605bdb5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -206,6 +206,11 @@ def provider_token(bot_info): return bot_info["payment_provider_token"] +@pytest.fixture(scope="session") +def subscription_channel_id(bot_info): + return bot_info["subscription_channel_id"] + + @pytest.fixture async def app(bot_info): # We build a new bot each time so that we use `app` in a context manager without problems diff --git a/tests/test_bot.py b/tests/test_bot.py index 3d2ac8db6b9..30b4f085b64 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -4277,13 +4277,18 @@ async def test_get_star_transactions(self, bot): assert isinstance(transactions, StarTransactions) assert len(transactions.transactions) == 0 - async def test_create_edit_chat_subscription_link(self, bot, channel_id): + async def test_create_edit_chat_subscription_link( + self, bot, subscription_channel_id, channel_id + ): sub_link = await bot.create_chat_subscription_invite_link( - channel_id, "sub_name", 2592000, 1 + subscription_channel_id, + name="sub_name", + subscription_period=2592000, + subscription_price=1, ) assert sub_link.name == "sub_name" edited_link = await bot.edit_chat_subscription_invite_link( - chat_id=channel_id, invite_link=sub_link, name="sub_name_2" + chat_id=subscription_channel_id, invite_link=sub_link, name="sub_name_2" ) assert edited_link.name == "sub_name_2" From abe5359b63fe133daa4851ed103e69ec50c07a2b Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Mon, 19 Aug 2024 20:18:44 +0200 Subject: [PATCH 11/14] Improve test coverage --- tests/test_reaction.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_reaction.py b/tests/test_reaction.py index 31b74ddc34c..67ece80e3b0 100644 --- a/tests/test_reaction.py +++ b/tests/test_reaction.py @@ -118,6 +118,7 @@ def test_slot_behaviour(self, reaction_type): def test_de_json_required_args(self, bot, reaction_type): cls = reaction_type.__class__ assert cls.de_json(None, bot) is None + assert ReactionType.de_json({}, bot) is None json_dict = make_json_dict(reaction_type) const_reaction_type = ReactionType.de_json(json_dict, bot) From 4b7128ece02ebc2eebf2f552d471f45309a1901c Mon Sep 17 00:00:00 2001 From: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:41:05 +0200 Subject: [PATCH 12/14] Update docs/source/telegram.reactiontypepaid.rst Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com> --- docs/source/telegram.reactiontypepaid.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/telegram.reactiontypepaid.rst b/docs/source/telegram.reactiontypepaid.rst index f6ed05af115..f5035a1ba5b 100644 --- a/docs/source/telegram.reactiontypepaid.rst +++ b/docs/source/telegram.reactiontypepaid.rst @@ -1,5 +1,5 @@ ReactionTypePaid -================d +================ .. autoclass:: telegram.ReactionTypePaid :members: From 8eec37ecfc3d3daf0727b7179b750976daa7966c Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 21 Aug 2024 21:58:59 +0200 Subject: [PATCH 13/14] Update `ChatInviteLink` with new attributes --- telegram/_chatinvitelink.py | 26 ++++++++++++++++++++++++++ tests/test_bot.py | 6 +++++- tests/test_chatinvitelink.py | 10 ++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/telegram/_chatinvitelink.py b/telegram/_chatinvitelink.py index 43e7e8ab62d..1e1a0e1cf44 100644 --- a/telegram/_chatinvitelink.py +++ b/telegram/_chatinvitelink.py @@ -69,6 +69,16 @@ class ChatInviteLink(TelegramObject): created using this link. .. versionadded:: 13.8 + subscription_period (:obj:`int`, optional): The number of seconds the subscription will be + active for before the next payment. + + .. versionadded:: NEXT.VERSION + subscription_price (:obj:`int`, optional): The amount of Telegram Stars a user must pay + initially and after each subsequent subscription period to be a member of the chat + using the link. + + .. versionadded:: NEXT.VERSION + Attributes: invite_link (:obj:`str`): The invite link. If the link was created by another chat administrator, then the second part of the link will be replaced with ``'…'``. @@ -96,6 +106,15 @@ class ChatInviteLink(TelegramObject): created using this link. .. versionadded:: 13.8 + subscription_period (:obj:`int`): Optional. The number of seconds the subscription will be + active for before the next payment. + + .. versionadded:: NEXT.VERSION + subscription_price (:obj:`int`): Optional. The amount of Telegram Stars a user must pay + initially and after each subsequent subscription period to be a member of the chat + using the link. + + .. versionadded:: NEXT.VERSION """ @@ -109,6 +128,8 @@ class ChatInviteLink(TelegramObject): "member_limit", "name", "pending_join_request_count", + "subscription_period", + "subscription_price", ) def __init__( @@ -122,6 +143,8 @@ def __init__( member_limit: Optional[int] = None, name: Optional[str] = None, pending_join_request_count: Optional[int] = None, + subscription_period: Optional[int] = None, + subscription_price: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -140,6 +163,9 @@ def __init__( self.pending_join_request_count: Optional[int] = ( int(pending_join_request_count) if pending_join_request_count is not None else None ) + self.subscription_period: Optional[int] = subscription_period + self.subscription_price: Optional[int] = subscription_price + self._id_attrs = ( self.invite_link, self.creates_join_request, diff --git a/tests/test_bot.py b/tests/test_bot.py index 30b4f085b64..6b912c6582a 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -4284,11 +4284,15 @@ async def test_create_edit_chat_subscription_link( subscription_channel_id, name="sub_name", subscription_period=2592000, - subscription_price=1, + subscription_price=13, ) assert sub_link.name == "sub_name" + assert sub_link.subscription_period == 2592000 + assert sub_link.subscription_price == 13 edited_link = await bot.edit_chat_subscription_invite_link( chat_id=subscription_channel_id, invite_link=sub_link, name="sub_name_2" ) assert edited_link.name == "sub_name_2" + assert sub_link.subscription_period == 2592000 + assert sub_link.subscription_price == 13 diff --git a/tests/test_chatinvitelink.py b/tests/test_chatinvitelink.py index 1b95177d9d5..2a4007986b3 100644 --- a/tests/test_chatinvitelink.py +++ b/tests/test_chatinvitelink.py @@ -42,6 +42,8 @@ def invite_link(creator): member_limit=TestChatInviteLinkBase.member_limit, name=TestChatInviteLinkBase.name, pending_join_request_count=TestChatInviteLinkBase.pending_join_request_count, + subscription_period=TestChatInviteLinkBase.subscription_period, + subscription_price=TestChatInviteLinkBase.subscription_price, ) @@ -54,6 +56,8 @@ class TestChatInviteLinkBase: member_limit = 42 name = "LinkName" pending_join_request_count = 42 + subscription_period = 43 + subscription_price = 44 class TestChatInviteLinkWithoutRequest(TestChatInviteLinkBase): @@ -91,6 +95,8 @@ def test_de_json_all_args(self, bot, creator): "member_limit": self.member_limit, "name": self.name, "pending_join_request_count": str(self.pending_join_request_count), + "subscription_period": self.subscription_period, + "subscription_price": self.subscription_price, } invite_link = ChatInviteLink.de_json(json_dict, bot) @@ -106,6 +112,8 @@ def test_de_json_all_args(self, bot, creator): assert invite_link.member_limit == self.member_limit assert invite_link.name == self.name assert invite_link.pending_join_request_count == self.pending_join_request_count + assert invite_link.subscription_period == self.subscription_period + assert invite_link.subscription_price == self.subscription_price def test_de_json_localization(self, tz_bot, bot, raw_bot, creator): json_dict = { @@ -146,6 +154,8 @@ def test_to_dict(self, invite_link): assert invite_link_dict["member_limit"] == self.member_limit assert invite_link_dict["name"] == self.name assert invite_link_dict["pending_join_request_count"] == self.pending_join_request_count + assert invite_link_dict["subscription_period"] == self.subscription_period + assert invite_link_dict["subscription_price"] == self.subscription_price def test_equality(self): a = ChatInviteLink("link", User(1, "", False), True, True, True) From 15185568cfab4db0c5a1df200ca95a4dbc4b4d1c Mon Sep 17 00:00:00 2001 From: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com> Date: Wed, 21 Aug 2024 22:04:06 +0200 Subject: [PATCH 14/14] Add note on leaving out `name` argument --- telegram/_bot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/telegram/_bot.py b/telegram/_bot.py index 1a4a4e7d64e..7172344cb4b 100644 --- a/telegram/_bot.py +++ b/telegram/_bot.py @@ -9369,6 +9369,9 @@ async def edit_chat_subscription_invite_link( name (:obj:`str`, optional): Invite link name; 0-:tg-const:`telegram.constants.ChatInviteLinkLimit.NAME_LENGTH` characters. + Tip: + Omitting this argument removes the name of the invite link. + Returns: :class:`telegram.ChatInviteLink`