Co-authored-by: aelkheir <90580077+aelkheir@users.noreply.github.com>
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
This commit is contained in:
Harshil 2024-09-17 12:09:19 -04:00 committed by GitHub
parent 9248c539d0
commit 67a97ae5a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 668 additions and 81 deletions

View file

@ -11,7 +11,7 @@
:target: https://pypi.org/project/python-telegram-bot/ :target: https://pypi.org/project/python-telegram-bot/
:alt: Supported Python versions :alt: Supported Python versions
.. image:: https://img.shields.io/badge/Bot%20API-7.9-blue?logo=telegram .. image:: https://img.shields.io/badge/Bot%20API-7.10-blue?logo=telegram
:target: https://core.telegram.org/bots/api-changelog :target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API version :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 Telegram API support
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
All types and methods of the Telegram Bot API **7.9** are natively supported by this library. All types and methods of the Telegram Bot API **7.10** are natively supported by this library.
In addition, Bot API functionality not yet natively included can still be used as described `in our wiki <https://github.com/python-telegram-bot/python-telegram-bot/wiki/Bot-API-Forward-Compatibility>`_. In addition, Bot API functionality not yet natively included can still be used as described `in our wiki <https://github.com/python-telegram-bot/python-telegram-bot/wiki/Bot-API-Forward-Compatibility>`_.
Notable Features Notable Features

View file

@ -120,6 +120,7 @@ Available Types
telegram.paidmediainfo telegram.paidmediainfo
telegram.paidmediaphoto telegram.paidmediaphoto
telegram.paidmediapreview telegram.paidmediapreview
telegram.paidmediapurchased
telegram.paidmediavideo telegram.paidmediavideo
telegram.photosize telegram.photosize
telegram.poll telegram.poll

View file

@ -18,6 +18,7 @@ Handlers
telegram.ext.inlinequeryhandler telegram.ext.inlinequeryhandler
telegram.ext.messagehandler telegram.ext.messagehandler
telegram.ext.messagereactionhandler telegram.ext.messagereactionhandler
telegram.ext.paidmediapurchasedhandler
telegram.ext.pollanswerhandler telegram.ext.pollanswerhandler
telegram.ext.pollhandler telegram.ext.pollhandler
telegram.ext.precheckoutqueryhandler telegram.ext.precheckoutqueryhandler

View file

@ -0,0 +1,6 @@
PaidMediaPurchasedHandler
=========================
.. autoclass:: telegram.ext.PaidMediaPurchasedHandler
:members:
:show-inheritance:

View file

@ -0,0 +1,6 @@
PaidMediaPurchased
==================
.. autoclass:: telegram.PaidMediaPurchased
:members:
:show-inheritance:

View file

@ -180,6 +180,7 @@ __all__ = (
"PaidMediaInfo", "PaidMediaInfo",
"PaidMediaPhoto", "PaidMediaPhoto",
"PaidMediaPreview", "PaidMediaPreview",
"PaidMediaPurchased",
"PaidMediaVideo", "PaidMediaVideo",
"PassportData", "PassportData",
"PassportElementError", "PassportElementError",
@ -419,7 +420,14 @@ from ._messageorigin import (
MessageOriginUser, MessageOriginUser,
) )
from ._messagereactionupdated import MessageReactionCountUpdated, MessageReactionUpdated from ._messagereactionupdated import MessageReactionCountUpdated, MessageReactionUpdated
from ._paidmedia import PaidMedia, PaidMediaInfo, PaidMediaPhoto, PaidMediaPreview, PaidMediaVideo from ._paidmedia import (
PaidMedia,
PaidMediaInfo,
PaidMediaPhoto,
PaidMediaPreview,
PaidMediaPurchased,
PaidMediaVideo,
)
from ._passport.credentials import ( from ._passport.credentials import (
Credentials, Credentials,
DataCredentials, DataCredentials,

View file

@ -9193,6 +9193,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
reply_parameters: Optional["ReplyParameters"] = None, reply_parameters: Optional["ReplyParameters"] = None,
reply_markup: Optional[ReplyMarkup] = None, reply_markup: Optional[ReplyMarkup] = None,
business_connection_id: Optional[str] = None, business_connection_id: Optional[str] = None,
payload: Optional[str] = None,
*, *,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None, reply_to_message_id: Optional[int] = None,
@ -9211,9 +9212,15 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Telegram Star proceeds from this media will be credited to the chat's balance. Telegram Star proceeds from this media will be credited to the chat's balance.
Otherwise, they will be credited to the bot'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 star_count (:obj:`int`): The number of Telegram Stars that must be paid to buy access
to the media. to the media; :tg-const:`telegram.constants.InvoiceLimit.MIN_STAR_COUNT` -
:tg-const:`telegram.constants.InvoiceLimit.MAX_STAR_COUNT`.
media (Sequence[:class:`telegram.InputPaidMedia`]): A list describing the media to be media (Sequence[:class:`telegram.InputPaidMedia`]): A list describing the media to be
sent; up to :tg-const:`telegram.constants.MediaGroupLimit.MAX_MEDIA_LENGTH` items. sent; up to :tg-const:`telegram.constants.MediaGroupLimit.MAX_MEDIA_LENGTH` items.
payload (:obj:`str`, optional): Bot-defined paid media payload,
0-:tg-const:`telegram.constants.InvoiceLimit.MAX_PAYLOAD_LENGTH` bytes. This will
not be displayed to the user, use it for your internal processes.
.. versionadded:: NEXT.VERSION
caption (:obj:`str`, optional): Caption of the media to be sent, caption (:obj:`str`, optional): Caption of the media to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters. 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters.
parse_mode (:obj:`str`, optional): |parse_mode| parse_mode (:obj:`str`, optional): |parse_mode|
@ -9252,6 +9259,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"star_count": star_count, "star_count": star_count,
"media": media, "media": media,
"show_caption_above_media": show_caption_above_media, "show_caption_above_media": show_caption_above_media,
"payload": payload,
} }
return await self._send_message( return await self._send_message(

View file

@ -3350,6 +3350,7 @@ class _ChatBase(TelegramObject):
reply_parameters: Optional["ReplyParameters"] = None, reply_parameters: Optional["ReplyParameters"] = None,
reply_markup: Optional[ReplyMarkup] = None, reply_markup: Optional[ReplyMarkup] = None,
business_connection_id: Optional[str] = None, business_connection_id: Optional[str] = None,
payload: Optional[str] = None,
*, *,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None, reply_to_message_id: Optional[int] = None,
@ -3391,6 +3392,7 @@ class _ChatBase(TelegramObject):
pool_timeout=pool_timeout, pool_timeout=pool_timeout,
api_kwargs=api_kwargs, api_kwargs=api_kwargs,
business_connection_id=business_connection_id, business_connection_id=business_connection_id,
payload=payload,
) )

View file

@ -187,15 +187,22 @@ class ChatBoostSourceGiftCode(ChatBoostSource):
class ChatBoostSourceGiveaway(ChatBoostSource): class ChatBoostSourceGiveaway(ChatBoostSource):
""" """
The boost was obtained by the creation of a Telegram Premium giveaway. This boosts the chat 4 The boost was obtained by the creation of a Telegram Premium giveaway or a Telegram Star.
times for the duration of the corresponding Telegram Premium subscription. This boosts the chat 4 times for the duration of the corresponding Telegram Premium
subscription for Telegram Premium giveaways and :attr:`prize_star_count` / 500 times for
one year for Telegram Star giveaways.
.. versionadded:: 20.8 .. versionadded:: 20.8
Args: Args:
giveaway_message_id (:obj:`int`): Identifier of a message in the chat with the giveaway; giveaway_message_id (:obj:`int`): Identifier of a message in the chat with the giveaway;
the message could have been deleted already. May be 0 if the message isn't sent yet. the message could have been deleted already. May be 0 if the message isn't sent yet.
user (:class:`telegram.User`, optional): User that won the prize in the giveaway if any. user (:class:`telegram.User`, optional): User that won the prize in the giveaway if any;
for Telegram Premium giveaways only.
prize_star_count (:obj:`int`, optional): The number of Telegram Stars to be split between
giveaway winners; for Telegram Star giveaways only.
.. versionadded:: NEXT.VERSION
is_unclaimed (:obj:`bool`, optional): :obj:`True`, if the giveaway was completed, but is_unclaimed (:obj:`bool`, optional): :obj:`True`, if the giveaway was completed, but
there was no user to win the prize. there was no user to win the prize.
@ -205,17 +212,22 @@ class ChatBoostSourceGiveaway(ChatBoostSource):
giveaway_message_id (:obj:`int`): Identifier of a message in the chat with the giveaway; giveaway_message_id (:obj:`int`): Identifier of a message in the chat with the giveaway;
the message could have been deleted already. May be 0 if the message isn't sent yet. the message could have been deleted already. May be 0 if the message isn't sent yet.
user (:class:`telegram.User`): Optional. User that won the prize in the giveaway if any. user (:class:`telegram.User`): Optional. User that won the prize in the giveaway if any.
prize_star_count (:obj:`int`): Optional. The number of Telegram Stars to be split between
giveaway winners; for Telegram Star giveaways only.
.. versionadded:: NEXT.VERSION
is_unclaimed (:obj:`bool`): Optional. :obj:`True`, if the giveaway was completed, but is_unclaimed (:obj:`bool`): Optional. :obj:`True`, if the giveaway was completed, but
there was no user to win the prize. there was no user to win the prize.
""" """
__slots__ = ("giveaway_message_id", "is_unclaimed", "user") __slots__ = ("giveaway_message_id", "is_unclaimed", "prize_star_count", "user")
def __init__( def __init__(
self, self,
giveaway_message_id: int, giveaway_message_id: int,
user: Optional[User] = None, user: Optional[User] = None,
is_unclaimed: Optional[bool] = None, is_unclaimed: Optional[bool] = None,
prize_star_count: Optional[int] = None,
*, *,
api_kwargs: Optional[JSONDict] = None, api_kwargs: Optional[JSONDict] = None,
): ):
@ -224,6 +236,7 @@ class ChatBoostSourceGiveaway(ChatBoostSource):
with self._unfrozen(): with self._unfrozen():
self.giveaway_message_id: int = giveaway_message_id self.giveaway_message_id: int = giveaway_message_id
self.user: Optional[User] = user self.user: Optional[User] = user
self.prize_star_count: Optional[int] = prize_star_count
self.is_unclaimed: Optional[bool] = is_unclaimed self.is_unclaimed: Optional[bool] = is_unclaimed

View file

@ -56,8 +56,13 @@ class Giveaway(TelegramObject):
country codes indicating the countries from which eligible users for the giveaway must country codes indicating the countries from which eligible users for the giveaway must
come. If empty, then all users can participate in the giveaway. Users with a phone come. If empty, then all users can participate in the giveaway. Users with a phone
number that was bought on Fragment can always participate in giveaways. number that was bought on Fragment can always participate in giveaways.
prize_star_count (:obj:`int`, optional): The number of Telegram Stars to be split between
giveaway winners; for Telegram Star giveaways only.
.. versionadded:: NEXT.VERSION
premium_subscription_month_count (:obj:`int`, optional): The number of months the Telegram premium_subscription_month_count (:obj:`int`, optional): The number of months the Telegram
Premium subscription won from the giveaway will be active for. Premium subscription won from the giveaway will be active for; for Telegram Premium
giveaways only.
Attributes: Attributes:
chats (Sequence[:class:`telegram.Chat`]): The list of chats which the user must join to chats (Sequence[:class:`telegram.Chat`]): The list of chats which the user must join to
@ -75,8 +80,13 @@ class Giveaway(TelegramObject):
country codes indicating the countries from which eligible users for the giveaway must country codes indicating the countries from which eligible users for the giveaway must
come. If empty, then all users can participate in the giveaway. Users with a phone come. If empty, then all users can participate in the giveaway. Users with a phone
number that was bought on Fragment can always participate in giveaways. number that was bought on Fragment can always participate in giveaways.
prize_star_count (:obj:`int`): Optional. The number of Telegram Stars to be split between
giveaway winners; for Telegram Star giveaways only.
.. versionadded:: NEXT.VERSION
premium_subscription_month_count (:obj:`int`): Optional. The number of months the Telegram premium_subscription_month_count (:obj:`int`): Optional. The number of months the Telegram
Premium subscription won from the giveaway will be active for. Premium subscription won from the giveaway will be active for; for Telegram Premium
giveaways only.
""" """
__slots__ = ( __slots__ = (
@ -86,6 +96,7 @@ class Giveaway(TelegramObject):
"only_new_members", "only_new_members",
"premium_subscription_month_count", "premium_subscription_month_count",
"prize_description", "prize_description",
"prize_star_count",
"winner_count", "winner_count",
"winners_selection_date", "winners_selection_date",
) )
@ -100,6 +111,7 @@ class Giveaway(TelegramObject):
prize_description: Optional[str] = None, prize_description: Optional[str] = None,
country_codes: Optional[Sequence[str]] = None, country_codes: Optional[Sequence[str]] = None,
premium_subscription_month_count: Optional[int] = None, premium_subscription_month_count: Optional[int] = None,
prize_star_count: Optional[int] = None,
*, *,
api_kwargs: Optional[JSONDict] = None, api_kwargs: Optional[JSONDict] = None,
): ):
@ -113,6 +125,7 @@ class Giveaway(TelegramObject):
self.prize_description: Optional[str] = prize_description self.prize_description: Optional[str] = prize_description
self.country_codes: Tuple[str, ...] = parse_sequence_arg(country_codes) self.country_codes: Tuple[str, ...] = parse_sequence_arg(country_codes)
self.premium_subscription_month_count: Optional[int] = premium_subscription_month_count self.premium_subscription_month_count: Optional[int] = premium_subscription_month_count
self.prize_star_count: Optional[int] = prize_star_count
self._id_attrs = ( self._id_attrs = (
self.chats, self.chats,
@ -145,13 +158,28 @@ class Giveaway(TelegramObject):
class GiveawayCreated(TelegramObject): class GiveawayCreated(TelegramObject):
"""This object represents a service message about the creation of a scheduled giveaway. """This object represents a service message about the creation of a scheduled giveaway.
Currently holds no information.
Args:
prize_star_count (:obj:`int`, optional): The number of Telegram Stars to be
split between giveaway winners; for Telegram Star giveaways only.
.. versionadded:: NEXT.VERSION
Attributes:
prize_star_count (:obj:`int`): Optional. The number of Telegram Stars to be
split between giveaway winners; for Telegram Star giveaways only.
.. versionadded:: NEXT.VERSION
""" """
__slots__ = () __slots__ = ("prize_star_count",)
def __init__(self, *, api_kwargs: Optional[JSONDict] = None): def __init__(
self, prize_star_count: Optional[int] = None, *, api_kwargs: Optional[JSONDict] = None
):
super().__init__(api_kwargs=api_kwargs) super().__init__(api_kwargs=api_kwargs)
self.prize_star_count: Optional[int] = prize_star_count
self._freeze() self._freeze()
@ -173,6 +201,10 @@ class GiveawayWinners(TelegramObject):
winner_count (:obj:`int`): Total number of winners in the giveaway winner_count (:obj:`int`): Total number of winners in the giveaway
winners (Sequence[:class:`telegram.User`]): List of up to winners (Sequence[:class:`telegram.User`]): List of up to
:tg-const:`telegram.constants.GiveawayLimit.MAX_WINNERS` winners of the giveaway :tg-const:`telegram.constants.GiveawayLimit.MAX_WINNERS` winners of the giveaway
prize_star_count (:obj:`int`, optional): The number of Telegram Stars to be split between
giveaway winners; for Telegram Star giveaways only.
.. versionadded:: NEXT.VERSION
additional_chat_count (:obj:`int`, optional): The number of other chats the user had to additional_chat_count (:obj:`int`, optional): The number of other chats the user had to
join in order to be eligible for the giveaway join in order to be eligible for the giveaway
premium_subscription_month_count (:obj:`int`, optional): The number of months the Telegram premium_subscription_month_count (:obj:`int`, optional): The number of months the Telegram
@ -194,6 +226,10 @@ class GiveawayWinners(TelegramObject):
:tg-const:`telegram.constants.GiveawayLimit.MAX_WINNERS` winners of the giveaway :tg-const:`telegram.constants.GiveawayLimit.MAX_WINNERS` winners of the giveaway
additional_chat_count (:obj:`int`): Optional. The number of other chats the user had to additional_chat_count (:obj:`int`): Optional. The number of other chats the user had to
join in order to be eligible for the giveaway join in order to be eligible for the giveaway
prize_star_count (:obj:`int`): Optional. The number of Telegram Stars to be split between
giveaway winners; for Telegram Star giveaways only.
.. versionadded:: NEXT.VERSION
premium_subscription_month_count (:obj:`int`): Optional. The number of months the Telegram premium_subscription_month_count (:obj:`int`): Optional. The number of months the Telegram
Premium subscription won from the giveaway will be active for Premium subscription won from the giveaway will be active for
unclaimed_prize_count (:obj:`int`): Optional. Number of undistributed prizes unclaimed_prize_count (:obj:`int`): Optional. Number of undistributed prizes
@ -211,6 +247,7 @@ class GiveawayWinners(TelegramObject):
"only_new_members", "only_new_members",
"premium_subscription_month_count", "premium_subscription_month_count",
"prize_description", "prize_description",
"prize_star_count",
"unclaimed_prize_count", "unclaimed_prize_count",
"was_refunded", "was_refunded",
"winner_count", "winner_count",
@ -231,6 +268,7 @@ class GiveawayWinners(TelegramObject):
only_new_members: Optional[bool] = None, only_new_members: Optional[bool] = None,
was_refunded: Optional[bool] = None, was_refunded: Optional[bool] = None,
prize_description: Optional[str] = None, prize_description: Optional[str] = None,
prize_star_count: Optional[int] = None,
*, *,
api_kwargs: Optional[JSONDict] = None, api_kwargs: Optional[JSONDict] = None,
): ):
@ -247,6 +285,7 @@ class GiveawayWinners(TelegramObject):
self.only_new_members: Optional[bool] = only_new_members self.only_new_members: Optional[bool] = only_new_members
self.was_refunded: Optional[bool] = was_refunded self.was_refunded: Optional[bool] = was_refunded
self.prize_description: Optional[str] = prize_description self.prize_description: Optional[str] = prize_description
self.prize_star_count: Optional[int] = prize_star_count
self._id_attrs = ( self._id_attrs = (
self.chat, self.chat,
@ -295,21 +334,29 @@ class GiveawayCompleted(TelegramObject):
unclaimed_prize_count (:obj:`int`, optional): Number of undistributed prizes unclaimed_prize_count (:obj:`int`, optional): Number of undistributed prizes
giveaway_message (:class:`telegram.Message`, optional): Message with the giveaway that was giveaway_message (:class:`telegram.Message`, optional): Message with the giveaway that was
completed, if it wasn't deleted completed, if it wasn't deleted
is_star_giveaway (:obj:`bool`, optional): :obj:`True`, if the giveaway is a Telegram Star
giveaway. Otherwise, currently, the giveaway is a Telegram Premium giveaway.
.. versionadded:: NEXT.VERSION
Attributes: Attributes:
winner_count (:obj:`int`): Number of winners in the giveaway winner_count (:obj:`int`): Number of winners in the giveaway
unclaimed_prize_count (:obj:`int`): Optional. Number of undistributed prizes unclaimed_prize_count (:obj:`int`): Optional. Number of undistributed prizes
giveaway_message (:class:`telegram.Message`): Optional. Message with the giveaway that was giveaway_message (:class:`telegram.Message`): Optional. Message with the giveaway that was
completed, if it wasn't deleted completed, if it wasn't deleted
is_star_giveaway (:obj:`bool`): Optional. :obj:`True`, if the giveaway is a Telegram Star
giveaway. Otherwise, currently, the giveaway is a Telegram Premium giveaway.
.. versionadded:: NEXT.VERSION
""" """
__slots__ = ("giveaway_message", "unclaimed_prize_count", "winner_count") __slots__ = ("giveaway_message", "is_star_giveaway", "unclaimed_prize_count", "winner_count")
def __init__( def __init__(
self, self,
winner_count: int, winner_count: int,
unclaimed_prize_count: Optional[int] = None, unclaimed_prize_count: Optional[int] = None,
giveaway_message: Optional["Message"] = None, giveaway_message: Optional["Message"] = None,
is_star_giveaway: Optional[bool] = None,
*, *,
api_kwargs: Optional[JSONDict] = None, api_kwargs: Optional[JSONDict] = None,
): ):
@ -318,6 +365,7 @@ class GiveawayCompleted(TelegramObject):
self.winner_count: int = winner_count self.winner_count: int = winner_count
self.unclaimed_prize_count: Optional[int] = unclaimed_prize_count self.unclaimed_prize_count: Optional[int] = unclaimed_prize_count
self.giveaway_message: Optional[Message] = giveaway_message self.giveaway_message: Optional[Message] = giveaway_message
self.is_star_giveaway: Optional[bool] = is_star_giveaway
self._id_attrs = ( self._id_attrs = (
self.winner_count, self.winner_count,

View file

@ -24,6 +24,7 @@ from telegram import constants
from telegram._files.photosize import PhotoSize from telegram._files.photosize import PhotoSize
from telegram._files.video import Video from telegram._files.video import Video
from telegram._telegramobject import TelegramObject from telegram._telegramobject import TelegramObject
from telegram._user import User
from telegram._utils import enum from telegram._utils import enum
from telegram._utils.argumentparsing import parse_sequence_arg from telegram._utils.argumentparsing import parse_sequence_arg
from telegram._utils.types import JSONDict from telegram._utils.types import JSONDict
@ -288,3 +289,52 @@ class PaidMediaInfo(TelegramObject):
data["paid_media"] = PaidMedia.de_list(data.get("paid_media"), bot=bot) data["paid_media"] = PaidMedia.de_list(data.get("paid_media"), bot=bot)
return super().de_json(data=data, bot=bot) return super().de_json(data=data, bot=bot)
class PaidMediaPurchased(TelegramObject):
"""This object contains information about a paid media purchase.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`from_user` and :attr:`paid_media_payload` are equal.
Note:
In Python :keyword:`from` is a reserved word. Use :paramref:`from_user` instead.
.. versionadded:: NEXT.VERSION
Args:
from_user (:class:`telegram.User`): User who purchased the media.
paid_media_payload (:obj:`str`): Bot-specified paid media payload.
Attributes:
from_user (:class:`telegram.User`): User who purchased the media.
paid_media_payload (:obj:`str`): Bot-specified paid media payload.
"""
__slots__ = ("from_user", "paid_media_payload")
def __init__(
self,
from_user: "User",
paid_media_payload: str,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
super().__init__(api_kwargs=api_kwargs)
self.from_user: User = from_user
self.paid_media_payload: str = paid_media_payload
self._id_attrs = (self.from_user, self.paid_media_payload)
self._freeze()
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["PaidMediaPurchased"]:
data = cls._parse_data(data)
if not data:
return None
data["from_user"] = User.de_json(data=data.pop("from"), bot=bot)
return super().de_json(data=data, bot=bot)

View file

@ -328,6 +328,9 @@ class TransactionPartnerUser(TransactionPartner):
media bought by the user. media bought by the user.
.. versionadded:: 21.5 .. versionadded:: 21.5
paid_media_payload (:obj:`str`, optional): Optional. Bot-specified paid media payload.
.. versionadded:: NEXT.VERSION
Attributes: Attributes:
type (:obj:`str`): The type of the transaction partner, type (:obj:`str`): The type of the transaction partner,
@ -338,19 +341,20 @@ class TransactionPartnerUser(TransactionPartner):
media bought by the user. media bought by the user.
.. versionadded:: 21.5 .. versionadded:: 21.5
paid_media_payload (:obj:`str`): Optional. Optional. Bot-specified paid media payload.
.. versionadded:: NEXT.VERSION
""" """
__slots__ = ( __slots__ = ("invoice_payload", "paid_media", "paid_media_payload", "user")
"invoice_payload",
"paid_media",
"user",
)
def __init__( def __init__(
self, self,
user: "User", user: "User",
invoice_payload: Optional[str] = None, invoice_payload: Optional[str] = None,
paid_media: Optional[Sequence[PaidMedia]] = None, paid_media: Optional[Sequence[PaidMedia]] = None,
paid_media_payload: Optional[str] = None,
*, *,
api_kwargs: Optional[JSONDict] = None, api_kwargs: Optional[JSONDict] = None,
) -> None: ) -> None:
@ -360,6 +364,7 @@ class TransactionPartnerUser(TransactionPartner):
self.user: User = user self.user: User = user
self.invoice_payload: Optional[str] = invoice_payload self.invoice_payload: Optional[str] = invoice_payload
self.paid_media: Optional[Tuple[PaidMedia, ...]] = parse_sequence_arg(paid_media) self.paid_media: Optional[Tuple[PaidMedia, ...]] = parse_sequence_arg(paid_media)
self.paid_media_payload: Optional[str] = paid_media_payload
self._id_attrs = ( self._id_attrs = (
self.type, self.type,
self.user, self.user,

View file

@ -30,6 +30,7 @@ from telegram._choseninlineresult import ChosenInlineResult
from telegram._inline.inlinequery import InlineQuery from telegram._inline.inlinequery import InlineQuery
from telegram._message import Message from telegram._message import Message
from telegram._messagereactionupdated import MessageReactionCountUpdated, MessageReactionUpdated from telegram._messagereactionupdated import MessageReactionCountUpdated, MessageReactionUpdated
from telegram._paidmedia import PaidMediaPurchased
from telegram._payment.precheckoutquery import PreCheckoutQuery from telegram._payment.precheckoutquery import PreCheckoutQuery
from telegram._payment.shippingquery import ShippingQuery from telegram._payment.shippingquery import ShippingQuery
from telegram._poll import Poll, PollAnswer from telegram._poll import Poll, PollAnswer
@ -156,6 +157,11 @@ class Update(TelegramObject):
.. versionadded:: 21.1 .. versionadded:: 21.1
purchased_paid_media (:class:`telegram.PaidMediaPurchased`, optional): A user purchased
paid media with a non-empty payload sent by the bot in a non-channel chat.
.. versionadded:: NEXT.VERSION
Attributes: Attributes:
update_id (:obj:`int`): The update's unique identifier. Update identifiers start from a update_id (:obj:`int`): The update's unique identifier. Update identifiers start from a
@ -263,6 +269,11 @@ class Update(TelegramObject):
were deleted from a connected business account. were deleted from a connected business account.
.. versionadded:: 21.1 .. versionadded:: 21.1
purchased_paid_media (:class:`telegram.PaidMediaPurchased`): Optional. A user purchased
paid media with a non-empty payload sent by the bot in a non-channel chat.
.. versionadded:: NEXT.VERSION
""" """
__slots__ = ( __slots__ = (
@ -290,6 +301,7 @@ class Update(TelegramObject):
"poll", "poll",
"poll_answer", "poll_answer",
"pre_checkout_query", "pre_checkout_query",
"purchased_paid_media",
"removed_chat_boost", "removed_chat_boost",
"shipping_query", "shipping_query",
"update_id", "update_id",
@ -383,6 +395,13 @@ class Update(TelegramObject):
""":const:`telegram.constants.UpdateType.DELETED_BUSINESS_MESSAGES` """:const:`telegram.constants.UpdateType.DELETED_BUSINESS_MESSAGES`
.. versionadded:: 21.1""" .. versionadded:: 21.1"""
PURCHASED_PAID_MEDIA: Final[str] = constants.UpdateType.PURCHASED_PAID_MEDIA
""":const:`telegram.constants.UpdateType.PURCHASED_PAID_MEDIA`
.. versionadded:: NEXT.VERSION
"""
ALL_TYPES: Final[List[str]] = list(constants.UpdateType) ALL_TYPES: Final[List[str]] = list(constants.UpdateType)
"""List[:obj:`str`]: A list of all available update types. """List[:obj:`str`]: A list of all available update types.
@ -413,6 +432,7 @@ class Update(TelegramObject):
business_message: Optional[Message] = None, business_message: Optional[Message] = None,
edited_business_message: Optional[Message] = None, edited_business_message: Optional[Message] = None,
deleted_business_messages: Optional[BusinessMessagesDeleted] = None, deleted_business_messages: Optional[BusinessMessagesDeleted] = None,
purchased_paid_media: Optional[PaidMediaPurchased] = None,
*, *,
api_kwargs: Optional[JSONDict] = None, api_kwargs: Optional[JSONDict] = None,
): ):
@ -444,6 +464,7 @@ class Update(TelegramObject):
self.deleted_business_messages: Optional[BusinessMessagesDeleted] = ( self.deleted_business_messages: Optional[BusinessMessagesDeleted] = (
deleted_business_messages deleted_business_messages
) )
self.purchased_paid_media: Optional[PaidMediaPurchased] = purchased_paid_media
self._effective_user: Optional[User] = None self._effective_user: Optional[User] = None
self._effective_sender: Optional[Union[User, Chat]] = None self._effective_sender: Optional[Union[User, Chat]] = None
@ -475,6 +496,9 @@ class Update(TelegramObject):
This property now also considers :attr:`business_connection`, :attr:`business_message` This property now also considers :attr:`business_connection`, :attr:`business_message`
and :attr:`edited_business_message`. and :attr:`edited_business_message`.
.. versionchanged:: NEXT.VERSION
This property now also considers :attr:`purchased_paid_media`.
Example: Example:
* If :attr:`message` is present, this will give * If :attr:`message` is present, this will give
:attr:`telegram.Message.from_user`. :attr:`telegram.Message.from_user`.
@ -531,6 +555,9 @@ class Update(TelegramObject):
elif self.business_connection: elif self.business_connection:
user = self.business_connection.user user = self.business_connection.user
elif self.purchased_paid_media:
user = self.purchased_paid_media.from_user
self._effective_user = user self._effective_user = user
return user return user
@ -601,7 +628,8 @@ class Update(TelegramObject):
This is the case, if :attr:`inline_query`, This is the case, if :attr:`inline_query`,
:attr:`chosen_inline_result`, :attr:`callback_query` from inline messages, :attr:`chosen_inline_result`, :attr:`callback_query` from inline messages,
:attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll`, :attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll`,
:attr:`poll_answer`, or :attr:`business_connection` is present. :attr:`poll_answer`, :attr:`business_connection`, or :attr:`purchased_paid_media`
is present.
.. versionchanged:: 21.1 .. versionchanged:: 21.1
This property now also considers :attr:`business_message`, This property now also considers :attr:`business_message`,
@ -768,5 +796,8 @@ class Update(TelegramObject):
data["deleted_business_messages"] = BusinessMessagesDeleted.de_json( data["deleted_business_messages"] = BusinessMessagesDeleted.de_json(
data.get("deleted_business_messages"), bot data.get("deleted_business_messages"), bot
) )
data["purchased_paid_media"] = PaidMediaPurchased.de_json(
data.get("purchased_paid_media"), bot
)
return super().de_json(data=data, bot=bot) return super().de_json(data=data, bot=bot)

View file

@ -152,7 +152,7 @@ class _AccentColor(NamedTuple):
#: :data:`telegram.__bot_api_version_info__`. #: :data:`telegram.__bot_api_version_info__`.
#: #:
#: .. versionadded:: 20.0 #: .. versionadded:: 20.0
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=9) BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=10)
#: :obj:`str`: Telegram Bot API #: :obj:`str`: Telegram Bot API
#: version supported by this version of `python-telegram-bot`. Also available as #: version supported by this version of `python-telegram-bot`. Also available as
#: :data:`telegram.__bot_api_version__`. #: :data:`telegram.__bot_api_version__`.
@ -552,6 +552,42 @@ class AccentColor(Enum):
""" """
class BackgroundTypeType(StringEnum):
"""This enum contains the available types of :class:`telegram.BackgroundType`. The enum
members of this enumeration are instances of :class:`str` and can be treated as such.
.. versionadded:: 21.2
"""
__slots__ = ()
FILL = "fill"
""":obj:`str`: A :class:`telegram.BackgroundType` with fill background."""
WALLPAPER = "wallpaper"
""":obj:`str`: A :class:`telegram.BackgroundType` with wallpaper background."""
PATTERN = "pattern"
""":obj:`str`: A :class:`telegram.BackgroundType` with pattern background."""
CHAT_THEME = "chat_theme"
""":obj:`str`: A :class:`telegram.BackgroundType` with chat_theme background."""
class BackgroundFillType(StringEnum):
"""This enum contains the available types of :class:`telegram.BackgroundFill`. The enum
members of this enumeration are instances of :class:`str` and can be treated as such.
.. versionadded:: 21.2
"""
__slots__ = ()
SOLID = "solid"
""":obj:`str`: A :class:`telegram.BackgroundFill` with solid fill."""
GRADIENT = "gradient"
""":obj:`str`: A :class:`telegram.BackgroundFill` with gradient fill."""
FREEFORM_GRADIENT = "freeform_gradient"
""":obj:`str`: A :class:`telegram.BackgroundFill` with freeform_gradient fill."""
class BotCommandLimit(IntEnum): class BotCommandLimit(IntEnum):
"""This enum contains limitations for :class:`telegram.BotCommand` and """This enum contains limitations for :class:`telegram.BotCommand` and
:meth:`telegram.Bot.set_my_commands`. :meth:`telegram.Bot.set_my_commands`.
@ -833,6 +869,25 @@ class ChatLimit(IntEnum):
""" """
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:: 21.5
"""
__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, maximum amount the subscription can be set to."""
class BackgroundTypeLimit(IntEnum): class BackgroundTypeLimit(IntEnum):
"""This enum contains limitations for :class:`telegram.BackgroundTypeFill`, """This enum contains limitations for :class:`telegram.BackgroundTypeFill`,
:class:`telegram.BackgroundTypeWallpaper` and :class:`telegram.BackgroundTypePattern`. :class:`telegram.BackgroundTypeWallpaper` and :class:`telegram.BackgroundTypePattern`.
@ -2724,6 +2779,11 @@ class UpdateType(StringEnum):
.. versionadded:: 21.1 .. versionadded:: 21.1
""" """
PURCHASED_PAID_MEDIA = "purchased_paid_media"
""":obj:`str`: Updates with :attr:`telegram.Update.purchased_paid_media`.
.. versionadded:: NEXT.VERSION
"""
class InvoiceLimit(IntEnum): class InvoiceLimit(IntEnum):
@ -2795,6 +2855,8 @@ class InvoiceLimit(IntEnum):
:meth:`telegram.Bot.send_invoice`. :meth:`telegram.Bot.send_invoice`.
* :paramref:`~telegram.Bot.create_invoice_link.payload` parameter of * :paramref:`~telegram.Bot.create_invoice_link.payload` parameter of
:meth:`telegram.Bot.create_invoice_link`. :meth:`telegram.Bot.create_invoice_link`.
* :paramref:`~telegram.Bot.send_paid_media.payload` parameter of
:meth:`telegram.Bot.send_paid_media`.
""" """
MAX_TIP_AMOUNTS = 4 MAX_TIP_AMOUNTS = 4
""":obj:`int`: Maximum length of a :obj:`Sequence` passed as: """:obj:`int`: Maximum length of a :obj:`Sequence` passed as:
@ -2804,6 +2866,20 @@ class InvoiceLimit(IntEnum):
* :paramref:`~telegram.Bot.create_invoice_link.suggested_tip_amounts` parameter of * :paramref:`~telegram.Bot.create_invoice_link.suggested_tip_amounts` parameter of
:meth:`telegram.Bot.create_invoice_link`. :meth:`telegram.Bot.create_invoice_link`.
""" """
MIN_STAR_COUNT = 1
""":obj:`int`: Minimum amount of starts that must be paid to buy access to a paid media
passed as :paramref:`~telegram.Bot.send_paid_media.star_count` parameter of
:meth:`telegram.Bot.send_paid_media`.
.. versionadded:: NEXT.VERSION
"""
MAX_STAR_COUNT = 2500
""":obj:`int`: Maximum amount of starts that must be paid to buy access to a paid media
passed as :paramref:`~telegram.Bot.send_paid_media.star_count` parameter of
:meth:`telegram.Bot.send_paid_media`.
.. versionadded:: NEXT.VERSION
"""
class UserProfilePhotosLimit(IntEnum): class UserProfilePhotosLimit(IntEnum):
@ -3066,58 +3142,3 @@ class ReactionEmoji(StringEnum):
""":obj:`str`: Woman Shrugging""" """:obj:`str`: Woman Shrugging"""
POUTING_FACE = "😡" POUTING_FACE = "😡"
""":obj:`str`: Pouting face""" """:obj:`str`: Pouting face"""
class BackgroundTypeType(StringEnum):
"""This enum contains the available types of :class:`telegram.BackgroundType`. The enum
members of this enumeration are instances of :class:`str` and can be treated as such.
.. versionadded:: 21.2
"""
__slots__ = ()
FILL = "fill"
""":obj:`str`: A :class:`telegram.BackgroundType` with fill background."""
WALLPAPER = "wallpaper"
""":obj:`str`: A :class:`telegram.BackgroundType` with wallpaper background."""
PATTERN = "pattern"
""":obj:`str`: A :class:`telegram.BackgroundType` with pattern background."""
CHAT_THEME = "chat_theme"
""":obj:`str`: A :class:`telegram.BackgroundType` with chat_theme background."""
class BackgroundFillType(StringEnum):
"""This enum contains the available types of :class:`telegram.BackgroundFill`. The enum
members of this enumeration are instances of :class:`str` and can be treated as such.
.. versionadded:: 21.2
"""
__slots__ = ()
SOLID = "solid"
""":obj:`str`: A :class:`telegram.BackgroundFill` with solid fill."""
GRADIENT = "gradient"
""":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:: 21.5
"""
__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, maximum amount the subscription can be set to."""

View file

@ -48,6 +48,7 @@ __all__ = (
"JobQueue", "JobQueue",
"MessageHandler", "MessageHandler",
"MessageReactionHandler", "MessageReactionHandler",
"PaidMediaPurchasedHandler",
"PersistenceInput", "PersistenceInput",
"PicklePersistence", "PicklePersistence",
"PollAnswerHandler", "PollAnswerHandler",
@ -89,6 +90,7 @@ from ._handlers.conversationhandler import ConversationHandler
from ._handlers.inlinequeryhandler import InlineQueryHandler from ._handlers.inlinequeryhandler import InlineQueryHandler
from ._handlers.messagehandler import MessageHandler from ._handlers.messagehandler import MessageHandler
from ._handlers.messagereactionhandler import MessageReactionHandler from ._handlers.messagereactionhandler import MessageReactionHandler
from ._handlers.paidmediapurchasedhandler import PaidMediaPurchasedHandler
from ._handlers.pollanswerhandler import PollAnswerHandler from ._handlers.pollanswerhandler import PollAnswerHandler
from ._handlers.pollhandler import PollHandler from ._handlers.pollhandler import PollHandler
from ._handlers.precheckoutqueryhandler import PreCheckoutQueryHandler from ._handlers.precheckoutqueryhandler import PreCheckoutQueryHandler

View file

@ -4235,6 +4235,7 @@ class ExtBot(Bot, Generic[RLARGS]):
reply_parameters: Optional["ReplyParameters"] = None, reply_parameters: Optional["ReplyParameters"] = None,
reply_markup: Optional[ReplyMarkup] = None, reply_markup: Optional[ReplyMarkup] = None,
business_connection_id: Optional[str] = None, business_connection_id: Optional[str] = None,
payload: Optional[str] = None,
*, *,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE, allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None, reply_to_message_id: Optional[int] = None,
@ -4265,6 +4266,7 @@ class ExtBot(Bot, Generic[RLARGS]):
pool_timeout=pool_timeout, pool_timeout=pool_timeout,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args), api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
business_connection_id=business_connection_id, business_connection_id=business_connection_id,
payload=payload,
) )
async def create_chat_subscription_invite_link( async def create_chat_subscription_invite_link(

View file

@ -0,0 +1,95 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# 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 the PaidMediaPurchased class."""
from typing import Optional
from telegram import Update
from telegram._utils.defaultvalue import DEFAULT_TRUE
from telegram._utils.types import SCT, DVType
from telegram.ext._handlers.basehandler import BaseHandler
from telegram.ext._utils._update_parsing import parse_chat_id, parse_username
from telegram.ext._utils.types import CCT, RT, HandlerCallback
class PaidMediaPurchasedHandler(BaseHandler[Update, CCT, RT]):
"""Handler class to handle Telegram
:attr:`purchased paid media <telegram.Update.purchased_paid_media>`.
.. versionadded:: NEXT.VERSION
Args:
callback (:term:`coroutine function`): The callback function for this handler. Will be
called when :meth:`check_update` has determined that an update should be processed by
this handler. Callback signature::
async def callback(update: Update, context: CallbackContext)
user_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only
those which are from the specified user ID(s).
username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only
those which are from the specified username(s).
block (:obj:`bool`, optional): Determines whether the return value of the callback should
be awaited before processing the next handler in
:meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`.
.. seealso:: :wiki:`Concurrency`
Attributes:
callback (:term:`coroutine function`): The callback function for this handler.
block (:obj:`bool`): Determines whether the return value of the callback should be
awaited before processing the next handler in
:meth:`telegram.ext.Application.process_update`.
"""
__slots__ = (
"_user_ids",
"_usernames",
)
def __init__(
self: "PaidMediaPurchasedHandler[CCT, RT]",
callback: HandlerCallback[Update, CCT, RT],
user_id: Optional[SCT[int]] = None,
username: Optional[SCT[str]] = None,
block: DVType[bool] = DEFAULT_TRUE,
):
super().__init__(callback, block=block)
self._user_ids = parse_chat_id(user_id)
self._usernames = parse_username(username)
def check_update(self, update: object) -> bool:
"""Determines whether an update should be passed to this handler's :attr:`callback`.
Args:
update (:class:`telegram.Update` | :obj:`object`): Incoming update.
Returns:
:obj:`bool`
"""
if not isinstance(update, Update) or not update.purchased_paid_media:
return False
if not self._user_ids and not self._usernames:
return True
if update.purchased_paid_media.from_user.id in self._user_ids:
return True
return update.purchased_paid_media.from_user.username in self._usernames

View file

@ -72,7 +72,7 @@ complete and correct. To run it, export an environment variable first:
$ export TEST_OFFICIAL=true $ export TEST_OFFICIAL=true
and then run ``pytest tests/test_official.py``. Note: You need py 3.10+ to run this test. and then run ``pytest tests/test_official/test_official.py``. Note: You need py 3.10+ to run this test.
We also have another marker, ``@pytest.mark.dev``, which you can add to tests that you want to run selectively. We also have another marker, ``@pytest.mark.dev``, which you can add to tests that you want to run selectively.
Use as follows: Use as follows:

View file

@ -0,0 +1,169 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import asyncio
import datetime
import pytest
from telegram import (
Bot,
CallbackQuery,
Chat,
ChosenInlineResult,
Message,
PaidMediaPurchased,
PreCheckoutQuery,
ShippingQuery,
Update,
User,
)
from telegram._utils.datetime import UTC
from telegram.ext import CallbackContext, JobQueue, PaidMediaPurchasedHandler
from tests.auxil.slots import mro_slots
message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text")
params = [
{"message": message},
{"edited_message": message},
{"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)},
{"channel_post": message},
{"edited_channel_post": message},
{"chosen_inline_result": ChosenInlineResult("id", User(1, "", False), "")},
{"shipping_query": ShippingQuery("id", User(1, "", False), "", None)},
{"pre_checkout_query": PreCheckoutQuery("id", User(1, "", False), "", 0, "")},
{"callback_query": CallbackQuery(1, User(1, "", False), "chat")},
]
ids = (
"message",
"edited_message",
"callback_query",
"channel_post",
"edited_channel_post",
"chosen_inline_result",
"shipping_query",
"pre_checkout_query",
"callback_query_without_message",
)
@pytest.fixture(scope="class", params=params, ids=ids)
def false_update(request):
return Update(update_id=2, **request.param)
@pytest.fixture(scope="class")
def time():
return datetime.datetime.now(tz=UTC)
@pytest.fixture(scope="class")
def purchased_paid_media(bot):
bc = PaidMediaPurchased(
from_user=User(1, "name", username="user_a", is_bot=False),
paid_media_payload="payload",
)
bc.set_bot(bot)
return bc
@pytest.fixture
def purchased_paid_media_update(bot, purchased_paid_media):
return Update(0, purchased_paid_media=purchased_paid_media)
class TestPaidMediaPurchasedHandler:
test_flag = False
def test_slot_behaviour(self):
action = PaidMediaPurchasedHandler(self.callback)
for attr in action.__slots__:
assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot"
@pytest.fixture(autouse=True)
def _reset(self):
self.test_flag = False
async def callback(self, update, context):
self.test_flag = (
isinstance(context, CallbackContext)
and isinstance(context.bot, Bot)
and isinstance(update, Update)
and isinstance(context.update_queue, asyncio.Queue)
and isinstance(context.job_queue, JobQueue)
and isinstance(context.user_data, dict)
and isinstance(context.bot_data, dict)
and isinstance(
update.purchased_paid_media,
PaidMediaPurchased,
)
)
def test_with_user_id(self, purchased_paid_media_update):
handler = PaidMediaPurchasedHandler(self.callback, user_id=1)
assert handler.check_update(purchased_paid_media_update)
handler = PaidMediaPurchasedHandler(self.callback, user_id=[1])
assert handler.check_update(purchased_paid_media_update)
handler = PaidMediaPurchasedHandler(self.callback, user_id=2, username="@user_a")
assert handler.check_update(purchased_paid_media_update)
handler = PaidMediaPurchasedHandler(self.callback, user_id=2)
assert not handler.check_update(purchased_paid_media_update)
handler = PaidMediaPurchasedHandler(self.callback, user_id=[2])
assert not handler.check_update(purchased_paid_media_update)
def test_with_username(self, purchased_paid_media_update):
handler = PaidMediaPurchasedHandler(self.callback, username="user_a")
assert handler.check_update(purchased_paid_media_update)
handler = PaidMediaPurchasedHandler(self.callback, username="@user_a")
assert handler.check_update(purchased_paid_media_update)
handler = PaidMediaPurchasedHandler(self.callback, username=["user_a"])
assert handler.check_update(purchased_paid_media_update)
handler = PaidMediaPurchasedHandler(self.callback, username=["@user_a"])
assert handler.check_update(purchased_paid_media_update)
handler = PaidMediaPurchasedHandler(self.callback, user_id=1, username="@user_b")
assert handler.check_update(purchased_paid_media_update)
handler = PaidMediaPurchasedHandler(self.callback, username="user_b")
assert not handler.check_update(purchased_paid_media_update)
handler = PaidMediaPurchasedHandler(self.callback, username="@user_b")
assert not handler.check_update(purchased_paid_media_update)
handler = PaidMediaPurchasedHandler(self.callback, username=["user_b"])
assert not handler.check_update(purchased_paid_media_update)
handler = PaidMediaPurchasedHandler(self.callback, username=["@user_b"])
assert not handler.check_update(purchased_paid_media_update)
purchased_paid_media_update.purchased_paid_media.from_user._unfreeze()
purchased_paid_media_update.purchased_paid_media.from_user.username = None
assert not handler.check_update(purchased_paid_media_update)
def test_other_update_types(self, false_update):
handler = PaidMediaPurchasedHandler(self.callback)
assert not handler.check_update(false_update)
assert not handler.check_update(True)
async def test_context(self, app, purchased_paid_media_update):
handler = PaidMediaPurchasedHandler(callback=self.callback)
app.add_handler(handler)
async with app:
await app.process_update(purchased_paid_media_update)
assert self.test_flag

View file

@ -1299,6 +1299,7 @@ class TestChatWithoutRequest(ChatTestBase):
and kwargs["media"] == "media" and kwargs["media"] == "media"
and kwargs["star_count"] == 42 and kwargs["star_count"] == 42
and kwargs["caption"] == "stars" and kwargs["caption"] == "stars"
and kwargs["payload"] == "payload"
) )
assert check_shortcut_signature(Chat.send_paid_media, Bot.send_paid_media, ["chat_id"], []) assert check_shortcut_signature(Chat.send_paid_media, Bot.send_paid_media, ["chat_id"], [])
@ -1306,7 +1307,9 @@ class TestChatWithoutRequest(ChatTestBase):
assert await check_defaults_handling(chat.send_paid_media, chat.get_bot()) assert await check_defaults_handling(chat.send_paid_media, chat.get_bot())
monkeypatch.setattr(chat.get_bot(), "send_paid_media", make_assertion) monkeypatch.setattr(chat.get_bot(), "send_paid_media", make_assertion)
assert await chat.send_paid_media(media="media", star_count=42, caption="stars") assert await chat.send_paid_media(
media="media", star_count=42, caption="stars", payload="payload"
)
def test_mention_html(self): def test_mention_html(self):
chat = Chat(id=1, type="foo") chat = Chat(id=1, type="foo")

View file

@ -50,6 +50,7 @@ class ChatBoostDefaults:
user = User(1, "user", False) user = User(1, "user", False)
date = to_timestamp(datetime.datetime.utcnow()) date = to_timestamp(datetime.datetime.utcnow())
default_source = ChatBoostSourcePremium(user) default_source = ChatBoostSourcePremium(user)
prize_star_count = 99
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@ -91,6 +92,7 @@ def chat_boost_source_giveaway():
user=ChatBoostDefaults.user, user=ChatBoostDefaults.user,
giveaway_message_id=ChatBoostDefaults.giveaway_message_id, giveaway_message_id=ChatBoostDefaults.giveaway_message_id,
is_unclaimed=ChatBoostDefaults.is_unclaimed, is_unclaimed=ChatBoostDefaults.is_unclaimed,
prize_star_count=ChatBoostDefaults.prize_star_count,
) )

View file

@ -48,6 +48,7 @@ def giveaway():
premium_subscription_month_count=( premium_subscription_month_count=(
TestGiveawayWithoutRequest.premium_subscription_month_count TestGiveawayWithoutRequest.premium_subscription_month_count
), ),
prize_star_count=TestGiveawayWithoutRequest.prize_star_count,
) )
@ -60,6 +61,7 @@ class TestGiveawayWithoutRequest:
prize_description = "prize_description" prize_description = "prize_description"
country_codes = ["DE", "US"] country_codes = ["DE", "US"]
premium_subscription_month_count = 3 premium_subscription_month_count = 3
prize_star_count = 99
def test_slot_behaviour(self, giveaway): def test_slot_behaviour(self, giveaway):
for attr in giveaway.__slots__: for attr in giveaway.__slots__:
@ -76,6 +78,7 @@ class TestGiveawayWithoutRequest:
"prize_description": self.prize_description, "prize_description": self.prize_description,
"country_codes": self.country_codes, "country_codes": self.country_codes,
"premium_subscription_month_count": self.premium_subscription_month_count, "premium_subscription_month_count": self.premium_subscription_month_count,
"prize_star_count": self.prize_star_count,
} }
giveaway = Giveaway.de_json(json_dict, offline_bot) giveaway = Giveaway.de_json(json_dict, offline_bot)
@ -89,6 +92,7 @@ class TestGiveawayWithoutRequest:
assert giveaway.prize_description == self.prize_description assert giveaway.prize_description == self.prize_description
assert giveaway.country_codes == tuple(self.country_codes) assert giveaway.country_codes == tuple(self.country_codes)
assert giveaway.premium_subscription_month_count == self.premium_subscription_month_count assert giveaway.premium_subscription_month_count == self.premium_subscription_month_count
assert giveaway.prize_star_count == self.prize_star_count
assert Giveaway.de_json(None, offline_bot) is None assert Giveaway.de_json(None, offline_bot) is None
@ -102,6 +106,7 @@ class TestGiveawayWithoutRequest:
"prize_description": self.prize_description, "prize_description": self.prize_description,
"country_codes": self.country_codes, "country_codes": self.country_codes,
"premium_subscription_month_count": self.premium_subscription_month_count, "premium_subscription_month_count": self.premium_subscription_month_count,
"prize_star_count": self.prize_star_count,
} }
giveaway_raw = Giveaway.de_json(json_dict, raw_bot) giveaway_raw = Giveaway.de_json(json_dict, raw_bot)
@ -133,6 +138,7 @@ class TestGiveawayWithoutRequest:
giveaway_dict["premium_subscription_month_count"] giveaway_dict["premium_subscription_month_count"]
== self.premium_subscription_month_count == self.premium_subscription_month_count
) )
assert giveaway_dict["prize_star_count"] == self.prize_star_count
def test_equality(self, giveaway): def test_equality(self, giveaway):
a = giveaway a = giveaway
@ -164,15 +170,40 @@ class TestGiveawayWithoutRequest:
assert hash(a) != hash(e) assert hash(a) != hash(e)
@pytest.fixture(scope="module")
def giveaway_created():
return GiveawayCreated(
prize_star_count=TestGiveawayCreatedWithoutRequest.prize_star_count,
)
class TestGiveawayCreatedWithoutRequest: class TestGiveawayCreatedWithoutRequest:
def test_slot_behaviour(self): prize_star_count = 99
giveaway_created = GiveawayCreated()
def test_slot_behaviour(self, giveaway_created):
for attr in giveaway_created.__slots__: for attr in giveaway_created.__slots__:
assert getattr(giveaway_created, attr, "err") != "err", f"got extra slot '{attr}'" assert getattr(giveaway_created, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(giveaway_created)) == len( assert len(mro_slots(giveaway_created)) == len(
set(mro_slots(giveaway_created)) set(mro_slots(giveaway_created))
), "duplicate slot" ), "duplicate slot"
def test_de_json(self, bot):
json_dict = {
"prize_star_count": self.prize_star_count,
}
gac = GiveawayCreated.de_json(json_dict, bot)
assert gac.api_kwargs == {}
assert gac.prize_star_count == self.prize_star_count
assert Giveaway.de_json(None, bot) is None
def test_to_dict(self, giveaway_created):
gac_dict = giveaway_created.to_dict()
assert isinstance(gac_dict, dict)
assert gac_dict["prize_star_count"] == self.prize_star_count
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def giveaway_winners(): def giveaway_winners():
@ -190,6 +221,7 @@ def giveaway_winners():
additional_chat_count=TestGiveawayWinnersWithoutRequest.additional_chat_count, additional_chat_count=TestGiveawayWinnersWithoutRequest.additional_chat_count,
unclaimed_prize_count=TestGiveawayWinnersWithoutRequest.unclaimed_prize_count, unclaimed_prize_count=TestGiveawayWinnersWithoutRequest.unclaimed_prize_count,
was_refunded=TestGiveawayWinnersWithoutRequest.was_refunded, was_refunded=TestGiveawayWinnersWithoutRequest.was_refunded,
prize_star_count=TestGiveawayWinnersWithoutRequest.prize_star_count,
) )
@ -205,6 +237,7 @@ class TestGiveawayWinnersWithoutRequest:
only_new_members = True only_new_members = True
was_refunded = True was_refunded = True
prize_description = "prize_description" prize_description = "prize_description"
prize_star_count = 99
def test_slot_behaviour(self, giveaway_winners): def test_slot_behaviour(self, giveaway_winners):
for attr in giveaway_winners.__slots__: for attr in giveaway_winners.__slots__:
@ -226,6 +259,7 @@ class TestGiveawayWinnersWithoutRequest:
"only_new_members": self.only_new_members, "only_new_members": self.only_new_members,
"was_refunded": self.was_refunded, "was_refunded": self.was_refunded,
"prize_description": self.prize_description, "prize_description": self.prize_description,
"prize_star_count": self.prize_star_count,
} }
giveaway_winners = GiveawayWinners.de_json(json_dict, offline_bot) giveaway_winners = GiveawayWinners.de_json(json_dict, offline_bot)
@ -245,6 +279,7 @@ class TestGiveawayWinnersWithoutRequest:
assert giveaway_winners.only_new_members == self.only_new_members assert giveaway_winners.only_new_members == self.only_new_members
assert giveaway_winners.was_refunded == self.was_refunded assert giveaway_winners.was_refunded == self.was_refunded
assert giveaway_winners.prize_description == self.prize_description assert giveaway_winners.prize_description == self.prize_description
assert giveaway_winners.prize_star_count == self.prize_star_count
assert GiveawayWinners.de_json(None, offline_bot) is None assert GiveawayWinners.de_json(None, offline_bot) is None
@ -291,6 +326,7 @@ class TestGiveawayWinnersWithoutRequest:
assert giveaway_winners_dict["only_new_members"] == self.only_new_members assert giveaway_winners_dict["only_new_members"] == self.only_new_members
assert giveaway_winners_dict["was_refunded"] == self.was_refunded assert giveaway_winners_dict["was_refunded"] == self.was_refunded
assert giveaway_winners_dict["prize_description"] == self.prize_description assert giveaway_winners_dict["prize_description"] == self.prize_description
assert giveaway_winners_dict["prize_star_count"] == self.prize_star_count
def test_equality(self, giveaway_winners): def test_equality(self, giveaway_winners):
a = giveaway_winners a = giveaway_winners
@ -336,12 +372,14 @@ def giveaway_completed():
winner_count=TestGiveawayCompletedWithoutRequest.winner_count, winner_count=TestGiveawayCompletedWithoutRequest.winner_count,
unclaimed_prize_count=TestGiveawayCompletedWithoutRequest.unclaimed_prize_count, unclaimed_prize_count=TestGiveawayCompletedWithoutRequest.unclaimed_prize_count,
giveaway_message=TestGiveawayCompletedWithoutRequest.giveaway_message, giveaway_message=TestGiveawayCompletedWithoutRequest.giveaway_message,
is_star_giveaway=TestGiveawayCompletedWithoutRequest.is_star_giveaway,
) )
class TestGiveawayCompletedWithoutRequest: class TestGiveawayCompletedWithoutRequest:
winner_count = 42 winner_count = 42
unclaimed_prize_count = 4 unclaimed_prize_count = 4
is_star_giveaway = True
giveaway_message = Message( giveaway_message = Message(
message_id=1, message_id=1,
date=dtm.datetime.now(dtm.timezone.utc), date=dtm.datetime.now(dtm.timezone.utc),
@ -362,6 +400,7 @@ class TestGiveawayCompletedWithoutRequest:
"winner_count": self.winner_count, "winner_count": self.winner_count,
"unclaimed_prize_count": self.unclaimed_prize_count, "unclaimed_prize_count": self.unclaimed_prize_count,
"giveaway_message": self.giveaway_message.to_dict(), "giveaway_message": self.giveaway_message.to_dict(),
"is_star_giveaway": self.is_star_giveaway,
} }
giveaway_completed = GiveawayCompleted.de_json(json_dict, offline_bot) giveaway_completed = GiveawayCompleted.de_json(json_dict, offline_bot)
@ -370,6 +409,7 @@ class TestGiveawayCompletedWithoutRequest:
assert giveaway_completed.winner_count == self.winner_count assert giveaway_completed.winner_count == self.winner_count
assert giveaway_completed.unclaimed_prize_count == self.unclaimed_prize_count assert giveaway_completed.unclaimed_prize_count == self.unclaimed_prize_count
assert giveaway_completed.giveaway_message == self.giveaway_message assert giveaway_completed.giveaway_message == self.giveaway_message
assert giveaway_completed.is_star_giveaway == self.is_star_giveaway
assert GiveawayCompleted.de_json(None, offline_bot) is None assert GiveawayCompleted.de_json(None, offline_bot) is None
@ -380,6 +420,7 @@ class TestGiveawayCompletedWithoutRequest:
assert giveaway_completed_dict["winner_count"] == self.winner_count assert giveaway_completed_dict["winner_count"] == self.winner_count
assert giveaway_completed_dict["unclaimed_prize_count"] == self.unclaimed_prize_count assert giveaway_completed_dict["unclaimed_prize_count"] == self.unclaimed_prize_count
assert giveaway_completed_dict["giveaway_message"] == self.giveaway_message.to_dict() assert giveaway_completed_dict["giveaway_message"] == self.giveaway_message.to_dict()
assert giveaway_completed_dict["is_star_giveaway"] == self.is_star_giveaway
def test_equality(self, giveaway_completed): def test_equality(self, giveaway_completed):
a = giveaway_completed a = giveaway_completed
@ -387,6 +428,7 @@ class TestGiveawayCompletedWithoutRequest:
winner_count=self.winner_count, winner_count=self.winner_count,
unclaimed_prize_count=self.unclaimed_prize_count, unclaimed_prize_count=self.unclaimed_prize_count,
giveaway_message=self.giveaway_message, giveaway_message=self.giveaway_message,
is_star_giveaway=self.is_star_giveaway,
) )
c = GiveawayCompleted( c = GiveawayCompleted(
winner_count=self.winner_count + 30, winner_count=self.winner_count + 30,

View file

@ -236,7 +236,7 @@ def message(bot):
winner_count=5, winner_count=5,
) )
}, },
{"giveaway_created": GiveawayCreated()}, {"giveaway_created": GiveawayCreated(prize_star_count=99)},
{ {
"giveaway_winners": GiveawayWinners( "giveaway_winners": GiveawayWinners(
chat=Chat(1, Chat.CHANNEL), chat=Chat(1, Chat.CHANNEL),

View file

@ -27,8 +27,10 @@ from telegram import (
PaidMediaInfo, PaidMediaInfo,
PaidMediaPhoto, PaidMediaPhoto,
PaidMediaPreview, PaidMediaPreview,
PaidMediaPurchased,
PaidMediaVideo, PaidMediaVideo,
PhotoSize, PhotoSize,
User,
Video, Video,
) )
from telegram.constants import PaidMediaType from telegram.constants import PaidMediaType
@ -122,6 +124,14 @@ def paid_media_info():
) )
@pytest.fixture(scope="module")
def paid_media_purchased():
return PaidMediaPurchased(
from_user=PaidMediaPurchasedTestBase.from_user,
paid_media_payload=PaidMediaPurchasedTestBase.paid_media_payload,
)
class PaidMediaTestBase: class PaidMediaTestBase:
width = 640 width = 640
height = 480 height = 480
@ -323,3 +333,54 @@ class TestPaidMediaInfoWithoutRequest(PaidMediaInfoTestBase):
assert pmi1 != pmi3 assert pmi1 != pmi3
assert hash(pmi1) != hash(pmi3) assert hash(pmi1) != hash(pmi3)
class PaidMediaPurchasedTestBase:
from_user = User(1, "user", False)
paid_media_payload = "payload"
class TestPaidMediaPurchasedWithoutRequest(PaidMediaPurchasedTestBase):
def test_slot_behaviour(self, paid_media_purchased):
inst = paid_media_purchased
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_de_json(self, bot):
json_dict = {
"from": self.from_user.to_dict(),
"paid_media_payload": self.paid_media_payload,
}
pmp = PaidMediaPurchased.de_json(json_dict, bot)
pmp_none = PaidMediaPurchased.de_json(None, bot)
assert pmp.from_user == self.from_user
assert pmp.paid_media_payload == self.paid_media_payload
assert pmp.api_kwargs == {}
assert pmp_none is None
def test_to_dict(self, paid_media_purchased):
assert paid_media_purchased.to_dict() == {
"from": self.from_user.to_dict(),
"paid_media_payload": self.paid_media_payload,
}
def test_equality(self):
pmp1 = PaidMediaPurchased(
from_user=self.from_user,
paid_media_payload=self.paid_media_payload,
)
pmp2 = PaidMediaPurchased(
from_user=self.from_user,
paid_media_payload=self.paid_media_payload,
)
pmp3 = PaidMediaPurchased(
from_user=User(2, "user", False),
paid_media_payload="other",
)
assert pmp1 == pmp2
assert hash(pmp1) == hash(pmp2)
assert pmp1 != pmp3
assert hash(pmp1) != hash(pmp3)

View file

@ -74,6 +74,7 @@ def transaction_partner_user():
] ]
) )
], ],
paid_media_payload="payload",
) )

View file

@ -40,6 +40,7 @@ from telegram import (
Message, Message,
MessageReactionCountUpdated, MessageReactionCountUpdated,
MessageReactionUpdated, MessageReactionUpdated,
PaidMediaPurchased,
Poll, Poll,
PollAnswer, PollAnswer,
PollOption, PollOption,
@ -143,6 +144,11 @@ business_message = Message(
User(1, "", False), User(1, "", False),
) )
purchased_paid_media = PaidMediaPurchased(
from_user=User(1, "", False),
paid_media_payload="payload",
)
params = [ params = [
{"message": message}, {"message": message},
@ -178,6 +184,7 @@ params = [
{"deleted_business_messages": deleted_business_messages}, {"deleted_business_messages": deleted_business_messages},
{"business_message": business_message}, {"business_message": business_message},
{"edited_business_message": business_message}, {"edited_business_message": business_message},
{"purchased_paid_media": purchased_paid_media},
# Must be last to conform with `ids` below! # Must be last to conform with `ids` below!
{"callback_query": CallbackQuery(1, User(1, "", False), "chat")}, {"callback_query": CallbackQuery(1, User(1, "", False), "chat")},
] ]
@ -205,6 +212,7 @@ all_types = (
"deleted_business_messages", "deleted_business_messages",
"business_message", "business_message",
"edited_business_message", "edited_business_message",
"purchased_paid_media",
) )
ids = (*all_types, "callback_query_without_message") ids = (*all_types, "callback_query_without_message")
@ -290,6 +298,7 @@ class TestUpdateWithoutRequest(UpdateTestBase):
or update.poll is not None or update.poll is not None
or update.poll_answer is not None or update.poll_answer is not None
or update.business_connection is not None or update.business_connection is not None
or update.purchased_paid_media is not None
): ):
assert chat.id == 1 assert chat.id == 1
else: else:
@ -403,6 +412,7 @@ class TestUpdateWithoutRequest(UpdateTestBase):
or update.message_reaction_count is not None or update.message_reaction_count is not None
or update.deleted_business_messages is not None or update.deleted_business_messages is not None
or update.business_connection is not None or update.business_connection is not None
or update.purchased_paid_media is not None
): ):
assert eff_message.message_id == message.message_id assert eff_message.message_id == message.message_id
else: else: