Full Support for Bot API 8.3 (#4676)

Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
Co-authored-by: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com>
This commit is contained in:
Poolitzer
2025-03-01 11:13:46 +01:00
committed by GitHub
parent b0d22acedb
commit b75948ede4
24 changed files with 516 additions and 54 deletions
+2 -2
View File
@@ -11,7 +11,7 @@
:target: https://pypi.org/project/python-telegram-bot/
:alt: Supported Python versions
.. image:: https://img.shields.io/badge/Bot%20API-8.2-blue?logo=telegram
.. image:: https://img.shields.io/badge/Bot%20API-8.3-blue?logo=telegram
:target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API version
@@ -81,7 +81,7 @@ After installing_ the library, be sure to check out the section on `working with
Telegram API support
~~~~~~~~~~~~~~~~~~~~
All types and methods of the Telegram Bot API **8.2** are natively supported by this library.
All types and methods of the Telegram Bot API **8.3** are natively supported by this library.
In addition, Bot API functionality not yet natively included can still be used as described `in our wiki <https://github.com/python-telegram-bot/python-telegram-bot/wiki/Bot-API-Forward-Compatibility>`_.
Notable Features
+1
View File
@@ -27,6 +27,7 @@ Your bot can accept payments from Telegram users. Please see the `introduction t
telegram.successfulpayment
telegram.transactionpartner
telegram.transactionpartneraffiliateprogram
telegram.transactionpartnerchat
telegram.transactionpartnerfragment
telegram.transactionpartnerother
telegram.transactionpartnertelegramads
@@ -0,0 +1,7 @@
TransactionPartnerChat
======================
.. autoclass:: telegram.TransactionPartnerChat
:members:
:show-inheritance:
:inherited-members: TransactionPartner
+2
View File
@@ -238,6 +238,7 @@ __all__ = (
"TextQuote",
"TransactionPartner",
"TransactionPartnerAffiliateProgram",
"TransactionPartnerChat",
"TransactionPartnerFragment",
"TransactionPartnerOther",
"TransactionPartnerTelegramAds",
@@ -275,6 +276,7 @@ from telegram._payment.stars.startransactions import StarTransaction, StarTransa
from telegram._payment.stars.transactionpartner import (
TransactionPartner,
TransactionPartnerAffiliateProgram,
TransactionPartnerChat,
TransactionPartnerFragment,
TransactionPartnerOther,
TransactionPartnerTelegramAds,
+46 -7
View File
@@ -1218,6 +1218,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
video_start_timestamp: Optional[int] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1242,6 +1243,10 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
original message was sent (or channel username in the format ``@channelusername``).
message_id (:obj:`int`): Message identifier in the chat specified in
:paramref:`from_chat_id`.
video_start_timestamp (:obj:`int`, optional): New start timestamp for the
forwarded video in the message
.. versionadded:: NEXT.VERSION
disable_notification (:obj:`bool`, optional): |disable_notification|
protect_content (:obj:`bool`, optional): |protect_content|
@@ -1260,6 +1265,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
"chat_id": chat_id,
"from_chat_id": from_chat_id,
"message_id": message_id,
"video_start_timestamp": video_start_timestamp,
}
return await self._send_message(
@@ -1955,6 +1961,8 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
message_effect_id: Optional[str] = None,
allow_paid_broadcast: Optional[bool] = None,
show_caption_above_media: Optional[bool] = None,
cover: Optional[FileInput] = None,
start_timestamp: Optional[int] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -2002,6 +2010,13 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
|time-period-input|
width (:obj:`int`, optional): Video width.
height (:obj:`int`, optional): Video height.
cover (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
optional): Cover for the video in the message. |fileinputnopath|
.. versionadded:: NEXT.VERSION
start_timestamp (:obj:`int`, optional): Start timestamp for the video in the message.
.. versionadded:: NEXT.VERSION
caption (:obj:`str`, optional): Video caption (may also be used when resending videos
by file_id), 0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH`
characters after entities parsing.
@@ -2088,6 +2103,8 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
"width": width,
"height": height,
"supports_streaming": supports_streaming,
"cover": self._parse_file_input(cover, attach=True) if cover else None,
"start_timestamp": start_timestamp,
"thumbnail": self._parse_file_input(thumbnail, attach=True) if thumbnail else None,
"has_spoiler": has_spoiler,
"show_caption_above_media": show_caption_above_media,
@@ -7977,6 +7994,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -7996,6 +8014,10 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
from_chat_id (:obj:`int` | :obj:`str`): Unique identifier for the chat where the
original message was sent (or channel username in the format ``@channelusername``).
message_id (:obj:`int`): Message identifier in the chat specified in from_chat_id.
video_start_timestamp (:obj:`int`, optional): New start timestamp for the
copied video in the message
.. versionadded:: NEXT.VERSION
caption (:obj:`str`, optional): New caption for media,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
entities parsing. If not specified, the original caption is kept.
@@ -8086,6 +8108,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"reply_parameters": reply_parameters,
"show_caption_above_media": show_caption_above_media,
"allow_paid_broadcast": allow_paid_broadcast,
"video_start_timestamp": video_start_timestamp,
}
result = await self._post(
@@ -9277,7 +9300,8 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
api_kwargs: Optional[JSONDict] = None,
) -> bool:
"""
Use this method to change the chosen reactions on a message. Service messages can't be
Use this method to change the chosen reactions on a message. Service messages of some types
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. Bots can't use paid reactions.
@@ -9807,7 +9831,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> Gifts:
"""Returns the list of gifts that can be sent by the bot to users.
"""Returns the list of gifts that can be sent by the bot to users and channel chats.
Requires no parameters.
.. versionadded:: 21.8
@@ -9831,12 +9855,13 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
async def send_gift(
self,
user_id: int,
gift_id: Union[str, Gift],
user_id: Optional[int] = None,
gift_id: Union[str, Gift] = None, # type: ignore
text: Optional[str] = None,
text_parse_mode: ODVInput[str] = DEFAULT_NONE,
text_entities: Optional[Sequence["MessageEntity"]] = None,
pay_for_upgrade: Optional[bool] = None,
chat_id: Optional[Union[str, int]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -9844,15 +9869,23 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> bool:
"""Sends a gift to the given user.
The gift can't be converted to Telegram Stars by the user
"""Sends a gift to the given user or channel chat.
The gift can't be converted to Telegram Stars by the receiver.
.. versionadded:: 21.8
Args:
user_id (:obj:`int`): Unique identifier of the target user that will receive the gift
user_id (:obj:`int`, optional): Required if :paramref:`chat_id` is not specified.
Unique identifier of the target user that will receive the gift.
.. versionchanged:: NEXT.VERSION
Now optional.
gift_id (:obj:`str` | :class:`~telegram.Gift`): Identifier of the gift or a
:class:`~telegram.Gift` object
chat_id (:obj:`int` | :obj:`str`, optional): Required if :paramref:`user_id`
is not specified. |chat_id_channel| It will receive the gift.
.. versionadded:: NEXT.VERSION
text (:obj:`str`, optional): Text that will be shown along with the gift;
0- :tg-const:`telegram.constants.GiftLimit.MAX_TEXT_LENGTH` characters
text_parse_mode (:obj:`str`, optional): Mode for parsing entities.
@@ -9879,6 +9912,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Raises:
:class:`telegram.error.TelegramError`
"""
# TODO: Remove when stability policy allows, tags: deprecated NEXT.VERSION
# also we should raise a deprecation warnung if anything is passed by
# position since it will be moved, not sure how
if gift_id is None:
raise TypeError("Missing required argument `gift_id`.")
data: JSONDict = {
"user_id": user_id,
"gift_id": gift_id.id if isinstance(gift_id, Gift) else gift_id,
@@ -9886,6 +9924,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"text_parse_mode": text_parse_mode,
"text_entities": text_entities,
"pay_for_upgrade": pay_for_upgrade,
"chat_id": chat_id,
}
return await self._post(
"sendGift",
+2
View File
@@ -831,6 +831,7 @@ class CallbackQuery(TelegramObject):
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: Optional[int] = None,
@@ -864,6 +865,7 @@ class CallbackQuery(TelegramObject):
chat_id=chat_id,
caption=caption,
parse_mode=parse_mode,
video_start_timestamp=video_start_timestamp,
caption_entities=caption_entities,
disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id,
+22 -2
View File
@@ -1940,6 +1940,8 @@ class _ChatBase(TelegramObject):
message_effect_id: Optional[str] = None,
allow_paid_broadcast: Optional[bool] = None,
show_caption_above_media: Optional[bool] = None,
cover: Optional[FileInput] = None,
start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1978,6 +1980,8 @@ class _ChatBase(TelegramObject):
parse_mode=parse_mode,
supports_streaming=supports_streaming,
thumbnail=thumbnail,
cover=cover,
start_timestamp=start_timestamp,
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
caption_entities=caption_entities,
@@ -2199,6 +2203,7 @@ class _ChatBase(TelegramObject):
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2225,6 +2230,7 @@ class _ChatBase(TelegramObject):
from_chat_id=from_chat_id,
message_id=message_id,
caption=caption,
video_start_timestamp=video_start_timestamp,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_notification=disable_notification,
@@ -2257,6 +2263,7 @@ class _ChatBase(TelegramObject):
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2283,6 +2290,7 @@ class _ChatBase(TelegramObject):
chat_id=chat_id,
message_id=message_id,
caption=caption,
video_start_timestamp=video_start_timestamp,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_notification=disable_notification,
@@ -2398,6 +2406,7 @@ class _ChatBase(TelegramObject):
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
video_start_timestamp: Optional[int] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2423,6 +2432,7 @@ class _ChatBase(TelegramObject):
chat_id=self.id,
from_chat_id=from_chat_id,
message_id=message_id,
video_start_timestamp=video_start_timestamp,
disable_notification=disable_notification,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -2440,6 +2450,7 @@ class _ChatBase(TelegramObject):
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
video_start_timestamp: Optional[int] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2466,6 +2477,7 @@ class _ChatBase(TelegramObject):
from_chat_id=self.id,
chat_id=chat_id,
message_id=message_id,
video_start_timestamp=video_start_timestamp,
disable_notification=disable_notification,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -3462,18 +3474,25 @@ class _ChatBase(TelegramObject):
await bot.send_gift(user_id=update.effective_chat.id, *args, **kwargs )
or::
await bot.send_gift(chat_id=update.effective_chat.id, *args, **kwargs )
For the documentation of the arguments, please see :meth:`telegram.Bot.send_gift`.
Caution:
Can only work, if the chat is a private chat, see :attr:`type`.
Will only work if the chat is a private or channel chat, see :attr:`type`.
.. versionadded:: 21.8
.. versionchanged:: NEXT.VERSION
Added support for channel chats.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return await self.get_bot().send_gift(
user_id=self.id,
gift_id=gift_id,
text=text,
text_parse_mode=text_parse_mode,
@@ -3484,6 +3503,7 @@ class _ChatBase(TelegramObject):
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
**{"chat_id" if self.type == Chat.CHANNEL else "user_id": self.id},
)
async def verify(
+9
View File
@@ -200,6 +200,9 @@ class ChatFullInfo(_ChatBase):
sent or forwarded to the channel chat. The field is available only for channel chats.
.. versionadded:: 21.4
can_send_gift (:obj:`bool`, optional): :obj:`True`, if gifts can be sent to the chat.
.. versionadded:: NEXT.VERSION
Attributes:
id (:obj:`int`): Unique identifier for this chat.
@@ -354,6 +357,9 @@ class ChatFullInfo(_ChatBase):
sent or forwarded to the channel chat. The field is available only for channel chats.
.. versionadded:: 21.4
can_send_gift (:obj:`bool`): Optional. :obj:`True`, if gifts can be sent to the chat.
.. versionadded:: NEXT.VERSION
.. _accent colors: https://core.telegram.org/bots/api#accent-colors
.. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups
@@ -369,6 +375,7 @@ class ChatFullInfo(_ChatBase):
"business_intro",
"business_location",
"business_opening_hours",
"can_send_gift",
"can_send_paid_media",
"can_set_sticker_set",
"custom_emoji_sticker_set_name",
@@ -445,6 +452,7 @@ class ChatFullInfo(_ChatBase):
linked_chat_id: Optional[int] = None,
location: Optional[ChatLocation] = None,
can_send_paid_media: Optional[bool] = None,
can_send_gift: Optional[bool] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -510,6 +518,7 @@ class ChatFullInfo(_ChatBase):
self.business_location: Optional[BusinessLocation] = business_location
self.business_opening_hours: Optional[BusinessOpeningHours] = business_opening_hours
self.can_send_paid_media: Optional[bool] = can_send_paid_media
self.can_send_gift: Optional[bool] = can_send_gift
@classmethod
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo":
+51 -1
View File
@@ -214,6 +214,13 @@ class InputPaidMediaVideo(InputPaidMedia):
Lastly you can pass an existing :class:`telegram.Video` object to send.
thumbnail (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
optional): |thumbdocstringnopath|
cover (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
optional): Cover for the video in the message. |fileinputnopath|
.. versionchanged:: NEXT.VERSION
start_timestamp (:obj:`int`, optional): Start timestamp for the video in the message
.. versionchanged:: NEXT.VERSION
width (:obj:`int`, optional): Video width.
height (:obj:`int`, optional): Video height.
duration (:obj:`int`, optional): Video duration in seconds.
@@ -225,6 +232,13 @@ class InputPaidMediaVideo(InputPaidMedia):
:tg-const:`telegram.constants.InputPaidMediaType.VIDEO`.
media (:obj:`str` | :class:`telegram.InputFile`): Video to send.
thumbnail (:class:`telegram.InputFile`): Optional. |thumbdocstringbase|
cover (:class:`telegram.InputFile`): Optional. Cover for the video in the message.
|fileinputnopath|
.. versionchanged:: NEXT.VERSION
start_timestamp (:obj:`int`): Optional. Start timestamp for the video in the message
.. versionchanged:: NEXT.VERSION
width (:obj:`int`): Optional. Video width.
height (:obj:`int`): Optional. Video height.
duration (:obj:`int`): Optional. Video duration in seconds.
@@ -232,7 +246,15 @@ class InputPaidMediaVideo(InputPaidMedia):
suitable for streaming.
"""
__slots__ = ("duration", "height", "supports_streaming", "thumbnail", "width")
__slots__ = (
"cover",
"duration",
"height",
"start_timestamp",
"supports_streaming",
"thumbnail",
"width",
)
def __init__(
self,
@@ -242,6 +264,8 @@ class InputPaidMediaVideo(InputPaidMedia):
height: Optional[int] = None,
duration: Optional[int] = None,
supports_streaming: Optional[bool] = None,
cover: Optional[FileInput] = None,
start_timestamp: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -264,6 +288,10 @@ class InputPaidMediaVideo(InputPaidMedia):
self.height: Optional[int] = height
self.duration: Optional[int] = duration
self.supports_streaming: Optional[bool] = supports_streaming
self.cover: Optional[Union[InputFile, str]] = (
parse_file_input(cover, attach=True, local_mode=True) if cover else None
)
self.start_timestamp: Optional[int] = start_timestamp
class InputMediaAnimation(InputMedia):
@@ -536,6 +564,13 @@ class InputMediaVideo(InputMedia):
optional): |thumbdocstringnopath|
.. versionadded:: 20.2
cover (:term:`file object` | :obj:`bytes` | :class:`pathlib.Path` | :obj:`str`, \
optional): Cover for the video in the message. |fileinputnopath|
.. versionchanged:: NEXT.VERSION
start_timestamp (:obj:`int`, optional): Start timestamp for the video in the message
.. versionchanged:: NEXT.VERSION
show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med|
.. versionadded:: 21.3
@@ -568,13 +603,22 @@ class InputMediaVideo(InputMedia):
show_caption_above_media (:obj:`bool`): Optional. |show_cap_above_med|
.. versionadded:: 21.3
cover (:class:`telegram.InputFile`): Optional. Cover for the video in the message.
|fileinputnopath|
.. versionchanged:: NEXT.VERSION
start_timestamp (:obj:`int`): Optional. Start timestamp for the video in the message
.. versionchanged:: NEXT.VERSION
"""
__slots__ = (
"cover",
"duration",
"has_spoiler",
"height",
"show_caption_above_media",
"start_timestamp",
"supports_streaming",
"thumbnail",
"width",
@@ -594,6 +638,8 @@ class InputMediaVideo(InputMedia):
has_spoiler: Optional[bool] = None,
thumbnail: Optional[FileInput] = None,
show_caption_above_media: Optional[bool] = None,
cover: Optional[FileInput] = None,
start_timestamp: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -625,6 +671,10 @@ class InputMediaVideo(InputMedia):
self.supports_streaming: Optional[bool] = supports_streaming
self.has_spoiler: Optional[bool] = has_spoiler
self.show_caption_above_media: Optional[bool] = show_caption_above_media
self.cover: Optional[Union[InputFile, str]] = (
parse_file_input(cover, attach=True, local_mode=True) if cover else None
)
self.start_timestamp: Optional[int] = start_timestamp
class InputMediaAudio(InputMedia):
+42 -2
View File
@@ -17,12 +17,17 @@
# 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 Video."""
from typing import Optional
from collections.abc import Sequence
from typing import TYPE_CHECKING, Optional
from telegram._files._basethumbedmedium import _BaseThumbedMedium
from telegram._files.photosize import PhotoSize
from telegram._utils.argumentparsing import de_list_optional, parse_sequence_arg
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
from telegram import Bot
class Video(_BaseThumbedMedium):
"""This object represents a video file.
@@ -48,6 +53,13 @@ class Video(_BaseThumbedMedium):
thumbnail (:class:`telegram.PhotoSize`, optional): Video thumbnail.
.. versionadded:: 20.2
cover (Sequence[:class:`telegram.PhotoSize`], optional): Available sizes of the cover of
the video in the message.
.. versionadded:: NEXT.VERSION
start_timestamp (:obj:`int`, optional): Timestamp in seconds from which the video
will play in the message
.. versionadded:: NEXT.VERSION
Attributes:
file_id (:obj:`str`): Identifier for this file, which can be used to download
@@ -64,9 +76,24 @@ class Video(_BaseThumbedMedium):
thumbnail (:class:`telegram.PhotoSize`): Optional. Video thumbnail.
.. versionadded:: 20.2
cover (tuple[:class:`telegram.PhotoSize`]): Optional, Available sizes of the cover of
the video in the message.
.. versionadded:: NEXT.VERSION
start_timestamp (:obj:`int`): Optional, Timestamp in seconds from which the video
will play in the message
.. versionadded:: NEXT.VERSION
"""
__slots__ = ("duration", "file_name", "height", "mime_type", "width")
__slots__ = (
"cover",
"duration",
"file_name",
"height",
"mime_type",
"start_timestamp",
"width",
)
def __init__(
self,
@@ -79,6 +106,8 @@ class Video(_BaseThumbedMedium):
file_size: Optional[int] = None,
file_name: Optional[str] = None,
thumbnail: Optional[PhotoSize] = None,
cover: Optional[Sequence[PhotoSize]] = None,
start_timestamp: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -97,3 +126,14 @@ class Video(_BaseThumbedMedium):
# Optional
self.mime_type: Optional[str] = mime_type
self.file_name: Optional[str] = file_name
self.cover: Optional[Sequence[PhotoSize]] = parse_sequence_arg(cover)
self.start_timestamp: Optional[int] = start_timestamp
@classmethod
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Video":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
data["cover"] = de_list_optional(data.get("cover"), PhotoSize, bot)
return super().de_json(data=data, bot=bot)
+10
View File
@@ -2592,6 +2592,8 @@ class Message(MaybeInaccessibleMessage):
message_effect_id: Optional[str] = None,
allow_paid_broadcast: Optional[bool] = None,
show_caption_above_media: Optional[bool] = None,
cover: Optional[FileInput] = None,
start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2661,6 +2663,8 @@ class Message(MaybeInaccessibleMessage):
message_thread_id=message_thread_id,
has_spoiler=has_spoiler,
thumbnail=thumbnail,
cover=cover,
start_timestamp=start_timestamp,
business_connection_id=self.business_connection_id,
message_effect_id=message_effect_id,
allow_paid_broadcast=allow_paid_broadcast,
@@ -3506,6 +3510,7 @@ class Message(MaybeInaccessibleMessage):
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
video_start_timestamp: Optional[int] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -3540,6 +3545,7 @@ class Message(MaybeInaccessibleMessage):
chat_id=chat_id,
from_chat_id=self.chat_id,
message_id=self.message_id,
video_start_timestamp=video_start_timestamp,
disable_notification=disable_notification,
protect_content=protect_content,
message_thread_id=message_thread_id,
@@ -3563,6 +3569,7 @@ class Message(MaybeInaccessibleMessage):
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -3593,6 +3600,7 @@ class Message(MaybeInaccessibleMessage):
from_chat_id=self.chat_id,
message_id=self.message_id,
caption=caption,
video_start_timestamp=video_start_timestamp,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_notification=disable_notification,
@@ -3625,6 +3633,7 @@ class Message(MaybeInaccessibleMessage):
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -3675,6 +3684,7 @@ class Message(MaybeInaccessibleMessage):
from_chat_id=from_chat_id,
message_id=message_id,
caption=caption,
video_start_timestamp=video_start_timestamp,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_notification=disable_notification,
@@ -23,6 +23,7 @@ from collections.abc import Sequence
from typing import TYPE_CHECKING, Final, Optional
from telegram import constants
from telegram._chat import Chat
from telegram._gifts import Gift
from telegram._paidmedia import PaidMedia
from telegram._telegramobject import TelegramObject
@@ -43,6 +44,7 @@ class TransactionPartner(TelegramObject):
transactions. Currently, it can be one of:
* :class:`TransactionPartnerUser`
* :class:`TransactionPartnerChat`
* :class:`TransactionPartnerAffiliateProgram`
* :class:`TransactionPartnerFragment`
* :class:`TransactionPartnerTelegramAds`
@@ -54,6 +56,9 @@ class TransactionPartner(TelegramObject):
.. versionadded:: 21.4
..versionchanged:: NEXT.VERSION
Added :class:`TransactionPartnerChat`
Args:
type (:obj:`str`): The type of the transaction partner.
@@ -68,6 +73,11 @@ class TransactionPartner(TelegramObject):
.. versionadded:: 21.9
"""
CHAT: Final[str] = constants.TransactionPartnerType.CHAT
""":const:`telegram.constants.TransactionPartnerType.CHAT`
.. versionadded:: NEXT.VERSION
"""
FRAGMENT: Final[str] = constants.TransactionPartnerType.FRAGMENT
""":const:`telegram.constants.TransactionPartnerType.FRAGMENT`"""
OTHER: Final[str] = constants.TransactionPartnerType.OTHER
@@ -103,6 +113,7 @@ class TransactionPartner(TelegramObject):
_class_mapping: dict[str, type[TransactionPartner]] = {
cls.AFFILIATE_PROGRAM: TransactionPartnerAffiliateProgram,
cls.CHAT: TransactionPartnerChat,
cls.FRAGMENT: TransactionPartnerFragment,
cls.USER: TransactionPartnerUser,
cls.TELEGRAM_ADS: TransactionPartnerTelegramAds,
@@ -171,6 +182,60 @@ class TransactionPartnerAffiliateProgram(TransactionPartner):
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
class TransactionPartnerChat(TransactionPartner):
"""Describes a transaction with a chat.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`chat` are equal.
.. versionadded:: NEXT.VERSION
Args:
chat (:class:`telegram.Chat`): Information about the chat.
gift (:class:`telegram.Gift`, optional): The gift sent to the chat by the bot.
Attributes:
type (:obj:`str`): The type of the transaction partner,
always :tg-const:`telegram.TransactionPartner.CHAT`.
chat (:class:`telegram.Chat`): Information about the chat.
gift (:class:`telegram.Gift`): Optional. The gift sent to the user by the bot.
"""
__slots__ = (
"chat",
"gift",
)
def __init__(
self,
chat: Chat,
gift: Optional[Gift] = None,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
super().__init__(type=TransactionPartner.CHAT, api_kwargs=api_kwargs)
with self._unfrozen():
self.chat: Chat = chat
self.gift: Optional[Gift] = gift
self._id_attrs = (
self.type,
self.chat,
)
@classmethod
def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "TransactionPartnerChat":
"""See :meth:`telegram.TransactionPartner.de_json`."""
data = cls._parse_data(data)
data["chat"] = de_json_optional(data.get("chat"), Chat, bot)
data["gift"] = de_json_optional(data.get("gift"), Gift, bot)
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
class TransactionPartnerFragment(TransactionPartner):
"""Describes a withdrawal transaction with Fragment.
+14 -1
View File
@@ -1328,6 +1328,8 @@ class User(TelegramObject):
message_effect_id: Optional[str] = None,
allow_paid_broadcast: Optional[bool] = None,
show_caption_above_media: Optional[bool] = None,
cover: Optional[FileInput] = None,
start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1369,6 +1371,8 @@ class User(TelegramObject):
parse_mode=parse_mode,
supports_streaming=supports_streaming,
thumbnail=thumbnail,
cover=cover,
start_timestamp=start_timestamp,
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
caption_entities=caption_entities,
@@ -1670,7 +1674,7 @@ class User(TelegramObject):
) -> bool:
"""Shortcut for::
await bot.send_gift( user_id=update.effective_user.id, *args, **kwargs )
await bot.send_gift(user_id=update.effective_user.id, *args, **kwargs )
For the documentation of the arguments, please see :meth:`telegram.Bot.send_gift`.
@@ -1680,6 +1684,7 @@ class User(TelegramObject):
:obj:`bool`: On success, :obj:`True` is returned.
"""
return await self.get_bot().send_gift(
chat_id=None,
user_id=self.id,
gift_id=gift_id,
text=text,
@@ -1707,6 +1712,7 @@ class User(TelegramObject):
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1734,6 +1740,7 @@ class User(TelegramObject):
from_chat_id=from_chat_id,
message_id=message_id,
caption=caption,
video_start_timestamp=video_start_timestamp,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_notification=disable_notification,
@@ -1766,6 +1773,7 @@ class User(TelegramObject):
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1793,6 +1801,7 @@ class User(TelegramObject):
chat_id=chat_id,
message_id=message_id,
caption=caption,
video_start_timestamp=video_start_timestamp,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_notification=disable_notification,
@@ -1908,6 +1917,7 @@ class User(TelegramObject):
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
video_start_timestamp: Optional[int] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1933,6 +1943,7 @@ class User(TelegramObject):
chat_id=self.id,
from_chat_id=from_chat_id,
message_id=message_id,
video_start_timestamp=video_start_timestamp,
disable_notification=disable_notification,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -1950,6 +1961,7 @@ class User(TelegramObject):
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
video_start_timestamp: Optional[int] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1976,6 +1988,7 @@ class User(TelegramObject):
from_chat_id=self.id,
chat_id=chat_id,
message_id=message_id,
video_start_timestamp=video_start_timestamp,
disable_notification=disable_notification,
read_timeout=read_timeout,
write_timeout=write_timeout,
+10 -2
View File
@@ -155,7 +155,7 @@ class _AccentColor(NamedTuple):
#: :data:`telegram.__bot_api_version_info__`.
#:
#: .. versionadded:: 20.0
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=8, minor=2)
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=8, minor=3)
#: :obj:`str`: Telegram Bot API
#: version supported by this version of `python-telegram-bot`. Also available as
#: :data:`telegram.__bot_api_version__`.
@@ -1236,9 +1236,12 @@ class GiftLimit(IntEnum):
__slots__ = ()
MAX_TEXT_LENGTH = 255
MAX_TEXT_LENGTH = 128
""":obj:`int`: Maximum number of characters in a :obj:`str` passed as the
:paramref:`~telegram.Bot.send_gift.text` parameter of :meth:`~telegram.Bot.send_gift`.
.. versionchanged:: NEXT.VERSION
Updated Value to 128 based on Bot API 8.3
"""
@@ -2659,6 +2662,11 @@ class TransactionPartnerType(StringEnum):
.. versionadded:: 21.9
"""
CHAT = "chat"
""":obj:`str`: Transaction with a chat.
.. versionadded:: NEXT.VERSION
"""
FRAGMENT = "fragment"
""":obj:`str`: Withdrawal transaction with Fragment."""
OTHER = "other"
+12 -2
View File
@@ -815,6 +815,7 @@ class ExtBot(Bot, Generic[RLARGS]):
reply_parameters: Optional["ReplyParameters"] = None,
show_caption_above_media: Optional[bool] = None,
allow_paid_broadcast: Optional[bool] = None,
video_start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -831,6 +832,7 @@ class ExtBot(Bot, Generic[RLARGS]):
from_chat_id=from_chat_id,
message_id=message_id,
caption=caption,
video_start_timestamp=video_start_timestamp,
parse_mode=parse_mode,
caption_entities=caption_entities,
disable_notification=disable_notification,
@@ -1752,6 +1754,7 @@ class ExtBot(Bot, Generic[RLARGS]):
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
video_start_timestamp: Optional[int] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1764,6 +1767,7 @@ class ExtBot(Bot, Generic[RLARGS]):
chat_id=chat_id,
from_chat_id=from_chat_id,
message_id=message_id,
video_start_timestamp=video_start_timestamp,
disable_notification=disable_notification,
protect_content=protect_content,
message_thread_id=message_thread_id,
@@ -3240,6 +3244,8 @@ class ExtBot(Bot, Generic[RLARGS]):
message_effect_id: Optional[str] = None,
allow_paid_broadcast: Optional[bool] = None,
show_caption_above_media: Optional[bool] = None,
cover: Optional[FileInput] = None,
start_timestamp: Optional[int] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -3270,6 +3276,8 @@ class ExtBot(Bot, Generic[RLARGS]):
business_connection_id=business_connection_id,
has_spoiler=has_spoiler,
thumbnail=thumbnail,
cover=cover,
start_timestamp=start_timestamp,
filename=filename,
reply_parameters=reply_parameters,
read_timeout=read_timeout,
@@ -4468,12 +4476,13 @@ class ExtBot(Bot, Generic[RLARGS]):
async def send_gift(
self,
user_id: int,
gift_id: Union[str, Gift],
user_id: Optional[int] = None,
gift_id: Union[str, Gift] = None, # type: ignore
text: Optional[str] = None,
text_parse_mode: ODVInput[str] = DEFAULT_NONE,
text_entities: Optional[Sequence["MessageEntity"]] = None,
pay_for_upgrade: Optional[bool] = None,
chat_id: Optional[Union[str, int]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -4484,6 +4493,7 @@ class ExtBot(Bot, Generic[RLARGS]):
) -> bool:
return await super().send_gift(
user_id=user_id,
chat_id=chat_id,
gift_id=gift_id,
text=text,
text_parse_mode=text_parse_mode,
+24 -2
View File
@@ -58,6 +58,8 @@ def input_media_video(class_thumb_file):
parse_mode=InputMediaVideoTestBase.parse_mode,
caption_entities=InputMediaVideoTestBase.caption_entities,
thumbnail=class_thumb_file,
cover=class_thumb_file,
start_timestamp=InputMediaVideoTestBase.start_timestamp,
supports_streaming=InputMediaVideoTestBase.supports_streaming,
has_spoiler=InputMediaVideoTestBase.has_spoiler,
show_caption_above_media=InputMediaVideoTestBase.show_caption_above_media,
@@ -130,6 +132,8 @@ def input_paid_media_video(class_thumb_file):
return InputPaidMediaVideo(
media=InputMediaVideoTestBase.media,
thumbnail=class_thumb_file,
cover=class_thumb_file,
start_timestamp=InputMediaVideoTestBase.start_timestamp,
width=InputMediaVideoTestBase.width,
height=InputMediaVideoTestBase.height,
duration=InputMediaVideoTestBase.duration,
@@ -144,6 +148,7 @@ class InputMediaVideoTestBase:
width = 3
height = 4
duration = 5
start_timestamp = 3
parse_mode = "HTML"
supports_streaming = True
caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)]
@@ -169,6 +174,8 @@ class TestInputMediaVideoWithoutRequest(InputMediaVideoTestBase):
assert input_media_video.caption_entities == tuple(self.caption_entities)
assert input_media_video.supports_streaming == self.supports_streaming
assert isinstance(input_media_video.thumbnail, InputFile)
assert isinstance(input_media_video.cover, InputFile)
assert input_media_video.start_timestamp == self.start_timestamp
assert input_media_video.has_spoiler == self.has_spoiler
assert input_media_video.show_caption_above_media == self.show_caption_above_media
@@ -194,6 +201,8 @@ class TestInputMediaVideoWithoutRequest(InputMediaVideoTestBase):
input_media_video_dict["show_caption_above_media"]
== input_media_video.show_caption_above_media
)
assert input_media_video_dict["cover"] == input_media_video.cover
assert input_media_video_dict["start_timestamp"] == input_media_video.start_timestamp
def test_with_video(self, video):
# fixture found in test_video
@@ -214,10 +223,13 @@ class TestInputMediaVideoWithoutRequest(InputMediaVideoTestBase):
def test_with_local_files(self):
input_media_video = InputMediaVideo(
data_file("telegram.mp4"), thumbnail=data_file("telegram.jpg")
data_file("telegram.mp4"),
thumbnail=data_file("telegram.jpg"),
cover=data_file("telegram.jpg"),
)
assert input_media_video.media == data_file("telegram.mp4").as_uri()
assert input_media_video.thumbnail == data_file("telegram.jpg").as_uri()
assert input_media_video.cover == data_file("telegram.jpg").as_uri()
def test_type_enum_conversion(self):
# Since we have a lot of different test classes for all the input media types, we test this
@@ -565,6 +577,8 @@ class TestInputPaidMediaVideoWithoutRequest(InputMediaVideoTestBase):
assert input_paid_media_video.duration == self.duration
assert input_paid_media_video.supports_streaming == self.supports_streaming
assert isinstance(input_paid_media_video.thumbnail, InputFile)
assert isinstance(input_paid_media_video.cover, InputFile)
assert input_paid_media_video.start_timestamp == self.start_timestamp
def test_to_dict(self, input_paid_media_video):
input_paid_media_video_dict = input_paid_media_video.to_dict()
@@ -578,6 +592,11 @@ class TestInputPaidMediaVideoWithoutRequest(InputMediaVideoTestBase):
== input_paid_media_video.supports_streaming
)
assert input_paid_media_video_dict["thumbnail"] == input_paid_media_video.thumbnail
assert input_paid_media_video_dict["cover"] == input_paid_media_video.cover
assert (
input_paid_media_video_dict["start_timestamp"]
== input_paid_media_video.start_timestamp
)
def test_with_video(self, video):
# fixture found in test_video
@@ -596,10 +615,13 @@ class TestInputPaidMediaVideoWithoutRequest(InputMediaVideoTestBase):
def test_with_local_files(self):
input_paid_media_video = InputPaidMediaVideo(
data_file("telegram.mp4"), thumbnail=data_file("telegram.jpg")
data_file("telegram.mp4"),
thumbnail=data_file("telegram.jpg"),
cover=data_file("telegram.jpg"),
)
assert input_paid_media_video.media == data_file("telegram.mp4").as_uri()
assert input_paid_media_video.thumbnail == data_file("telegram.jpg").as_uri()
assert input_paid_media_video.cover == data_file("telegram.jpg").as_uri()
@pytest.fixture(scope="module")
+16 -1
View File
@@ -46,6 +46,8 @@ class VideoTestBase:
mime_type = "video/mp4"
supports_streaming = True
file_name = "telegram.mp4"
start_timestamp = 3
cover = (PhotoSize("file_id", "unique_id", 640, 360, file_size=0),)
thumb_width = 180
thumb_height = 320
thumb_file_size = 1767
@@ -92,6 +94,8 @@ class TestVideoWithoutRequest(VideoTestBase):
"mime_type": self.mime_type,
"file_size": self.file_size,
"file_name": self.file_name,
"start_timestamp": self.start_timestamp,
"cover": [photo_size.to_dict() for photo_size in self.cover],
}
json_video = Video.de_json(json_dict, offline_bot)
assert json_video.api_kwargs == {}
@@ -104,6 +108,8 @@ class TestVideoWithoutRequest(VideoTestBase):
assert json_video.mime_type == self.mime_type
assert json_video.file_size == self.file_size
assert json_video.file_name == self.file_name
assert json_video.start_timestamp == self.start_timestamp
assert json_video.cover == self.cover
def test_to_dict(self, video):
video_dict = video.to_dict()
@@ -223,7 +229,9 @@ class TestVideoWithoutRequest(VideoTestBase):
class TestVideoWithRequest(VideoTestBase):
@pytest.mark.parametrize("duration", [dtm.timedelta(seconds=5), 5])
async def test_send_all_args(self, bot, chat_id, video_file, video, thumb_file, duration):
async def test_send_all_args(
self, bot, chat_id, video_file, video, thumb_file, photo_file, duration
):
message = await bot.send_video(
chat_id,
video_file,
@@ -236,6 +244,8 @@ class TestVideoWithRequest(VideoTestBase):
height=video.height,
parse_mode="Markdown",
thumbnail=thumb_file,
cover=photo_file,
start_timestamp=self.start_timestamp,
has_spoiler=True,
show_caption_above_media=True,
)
@@ -256,6 +266,11 @@ class TestVideoWithRequest(VideoTestBase):
assert message.video.thumbnail.width == self.thumb_width
assert message.video.thumbnail.height == self.thumb_height
assert message.video.start_timestamp == self.start_timestamp
assert isinstance(message.video.cover, tuple)
assert isinstance(message.video.cover[0], PhotoSize)
assert message.video.file_name == self.file_name
assert message.has_protected_content
assert message.has_media_spoiler
@@ -22,12 +22,14 @@ import pytest
from telegram import (
AffiliateInfo,
Chat,
Gift,
PaidMediaVideo,
RevenueWithdrawalStatePending,
Sticker,
TransactionPartner,
TransactionPartnerAffiliateProgram,
TransactionPartnerChat,
TransactionPartnerFragment,
TransactionPartnerOther,
TransactionPartnerTelegramAds,
@@ -95,6 +97,10 @@ class TransactionPartnerTestBase:
amount=42,
)
request_count = 42
chat = Chat(
id=3,
type=Chat.CHANNEL,
)
class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase):
@@ -123,6 +129,7 @@ class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase):
("telegram_ads", TransactionPartnerTelegramAds),
("telegram_api", TransactionPartnerTelegramApi),
("other", TransactionPartnerOther),
("chat", TransactionPartnerChat),
],
)
def test_subclass(self, offline_bot, tp_type, subclass):
@@ -450,3 +457,58 @@ class TestTransactionPartnerTelegramApiWithoutRequest(TransactionPartnerTestBase
assert a != d
assert hash(a) != hash(d)
@pytest.fixture
def transaction_partner_chat():
return TransactionPartnerChat(
chat=TransactionPartnerTestBase.chat,
gift=TransactionPartnerTestBase.gift,
)
class TestTransactionPartnerChatWithoutRequest(TransactionPartnerTestBase):
type = TransactionPartnerType.CHAT
def test_slot_behaviour(self, transaction_partner_chat):
inst = transaction_partner_chat
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, offline_bot):
json_dict = {
"chat": self.chat.to_dict(),
"gift": self.gift.to_dict(),
}
tp = TransactionPartnerChat.de_json(json_dict, offline_bot)
assert tp.api_kwargs == {}
assert tp.type == "chat"
assert tp.chat == self.chat
assert tp.gift == self.gift
def test_to_dict(self, transaction_partner_chat):
json_dict = transaction_partner_chat.to_dict()
assert json_dict["type"] == self.type
assert json_dict["chat"] == self.chat.to_dict()
assert json_dict["gift"] == self.gift.to_dict()
def test_equality(self, transaction_partner_chat):
a = transaction_partner_chat
b = TransactionPartnerChat(
chat=self.chat,
gift=self.gift,
)
c = TransactionPartnerChat(
chat=Chat(id=1, type=Chat.CHANNEL),
)
d = Chat(id=1, type=Chat.CHANNEL)
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
+3
View File
@@ -351,6 +351,9 @@ def build_kwargs(
allow_sending_without_reply=manually_passed_value,
quote_parse_mode=manually_passed_value,
)
# TODO remove when gift_id isnt marked as optional anymore, tags: deprecated NEXT.VERSION
elif name == "gift_id":
kws[name] = "GIFT-ID"
return kws
+2
View File
@@ -1617,6 +1617,7 @@ class TestBotWithoutRequest:
== [MessageEntity(MessageEntity.BOLD, 0, 4).to_dict()],
data["protect_content"] is True,
data["message_thread_id"] == 1,
data["video_start_timestamp"] == 999,
]
):
pytest.fail("I got wrong parameters in post")
@@ -1628,6 +1629,7 @@ class TestBotWithoutRequest:
from_chat_id=chat_id,
message_id=media_message.message_id,
caption=caption,
video_start_timestamp=999,
caption_entities=[MessageEntity(MessageEntity.BOLD, 0, 4)],
parse_mode=ParseMode.HTML,
reply_to_message_id=media_message.message_id,
+32 -4
View File
@@ -1312,7 +1312,7 @@ class TestChatWithoutRequest(ChatTestBase):
)
async def test_instance_method_send_gift(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
async def make_assertion_private(*_, **kwargs):
return (
kwargs["user_id"] == chat.id
and kwargs["gift_id"] == "gift_id"
@@ -1321,11 +1321,39 @@ class TestChatWithoutRequest(ChatTestBase):
and kwargs["text_entities"] == "text_entities"
)
assert check_shortcut_signature(Chat.send_gift, Bot.send_gift, ["user_id"], [])
assert await check_shortcut_call(chat.send_gift, chat.get_bot(), "send_gift")
async def make_assertion_channel(*_, **kwargs):
return (
kwargs["chat_id"] == chat.id
and kwargs["gift_id"] == "gift_id"
and kwargs["text"] == "text"
and kwargs["text_parse_mode"] == "text_parse_mode"
and kwargs["text_entities"] == "text_entities"
)
# TODO discuss if better way exists
# tags: deprecated NEXT.VERSION
with pytest.raises(
Exception,
match="Default for argument gift_id does not match the default of the Bot method.",
):
assert check_shortcut_signature(
Chat.send_gift, Bot.send_gift, ["user_id", "chat_id"], []
)
assert await check_shortcut_call(
chat.send_gift, chat.get_bot(), "send_gift", ["user_id", "chat_id"]
)
assert await check_defaults_handling(chat.send_gift, chat.get_bot())
monkeypatch.setattr(chat.get_bot(), "send_gift", make_assertion)
monkeypatch.setattr(chat.get_bot(), "send_gift", make_assertion_private)
chat.type = chat.PRIVATE
assert await chat.send_gift(
gift_id="gift_id",
text="text",
text_parse_mode="text_parse_mode",
text_entities="text_entities",
)
monkeypatch.setattr(chat.get_bot(), "send_gift", make_assertion_channel)
chat.type = chat.CHANNEL
assert await chat.send_gift(
gift_id="gift_id",
text="text",
+37
View File
@@ -164,6 +164,43 @@ class TestGiftWithoutRequest(GiftTestBase):
pay_for_upgrade=True,
)
@pytest.mark.parametrize("id_name", ["user_id", "chat_id"])
async def test_send_gift_user_chat_id(self, offline_bot, gift, monkeypatch, id_name):
# Only here because we have to temporarily mark gift_id as optional.
# tags: deprecated NEXT.VERSION
# We can't send actual gifts, so we just check that the correct parameters are passed
text_entities = [
MessageEntity(MessageEntity.TEXT_LINK, 0, 4, "url"),
MessageEntity(MessageEntity.BOLD, 5, 9),
]
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
received_id = request_data.parameters[id_name] == id_name
gift_id = request_data.parameters["gift_id"] == "some_id"
text = request_data.parameters["text"] == "text"
text_parse_mode = request_data.parameters["text_parse_mode"] == "text_parse_mode"
tes = request_data.parameters["text_entities"] == [
me.to_dict() for me in text_entities
]
pay_for_upgrade = request_data.parameters["pay_for_upgrade"] is True
return received_id and gift_id and text and text_parse_mode and tes and pay_for_upgrade
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.send_gift(
gift_id=gift,
text="text",
text_parse_mode="text_parse_mode",
text_entities=text_entities,
pay_for_upgrade=True,
**{id_name: id_name},
)
async def test_send_gift_without_gift_id(self, offline_bot):
with pytest.raises(TypeError, match="Missing required argument `gift_id`."):
await offline_bot.send_gift()
@pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True)
@pytest.mark.parametrize(
("passed_value", "expected_value"),
+33 -26
View File
@@ -19,7 +19,7 @@
"""This module contains exceptions to our API compared to the official API."""
import datetime as dtm
from telegram import Animation, Audio, Document, Gift, PhotoSize, Sticker, Video, VideoNote, Voice
from telegram import Animation, Audio, Document, PhotoSize, Sticker, Video, VideoNote, Voice
from tests.test_official.helpers import _get_params_base
IGNORED_OBJECTS = ("ResponseParameters",)
@@ -47,7 +47,8 @@ class ParamTypeCheckingExceptions:
"animation": Animation,
"voice": Voice,
"sticker": Sticker,
"gift_id": Gift,
# TODO: Deprecated and will be corrected (and readded) in next major bot API release:
# "gift_id": Gift,
},
"(delete|set)_sticker.*": {
"sticker$": Sticker,
@@ -72,36 +73,40 @@ class ParamTypeCheckingExceptions:
("keyboard", True): "KeyboardButton", # + sequence[sequence[str]]
("reaction", False): "ReactionType", # + str
("options", False): "InputPollOption", # + str
# TODO: Deprecated and will be corrected (and removed) in next major PTB version:
# TODO: Deprecated and will be corrected (and removed) in next bot api release
("file_hashes", True): "list[str]",
}
# Special cases for other parameters that accept more types than the official API, and are
# too complex to compare/predict with official API
# structure: class/method_name: {param_name: reduced form of annotation}
COMPLEX_TYPES = (
{ # (param_name, is_class (i.e appears in a class?)): reduced form of annotation
"send_poll": {"correct_option_id": int}, # actual: Literal
"get_file": {
"file_id": str, # actual: Union[str, objs_with_file_id_attr]
},
r"\w+invite_link": {
"invite_link": str, # actual: Union[str, ChatInviteLink]
},
"send_invoice|create_invoice_link": {
"provider_data": str, # actual: Union[str, obj]
},
"InlineKeyboardButton": {
"callback_data": str, # actual: Union[str, obj]
},
"Input(Paid)?Media.*": {
"media": str, # actual: Union[str, InputMedia*, FileInput]
},
"EncryptedPassportElement": {
"data": str, # actual: Union[IdDocumentData, PersonalDetails, ResidentialAddress]
},
}
)
COMPLEX_TYPES = {
"send_poll": {"correct_option_id": int}, # actual: Literal
"get_file": {
"file_id": str, # actual: Union[str, objs_with_file_id_attr]
},
r"\w+invite_link": {
"invite_link": str, # actual: Union[str, ChatInviteLink]
},
"send_invoice|create_invoice_link": {
"provider_data": str, # actual: Union[str, obj]
},
"InlineKeyboardButton": {
"callback_data": str, # actual: Union[str, obj]
},
"Input(Paid)?Media.*": {
"media": str, # actual: Union[str, InputMedia*, FileInput]
# see also https://github.com/tdlib/telegram-bot-api/issues/707
"thumbnail": str, # actual: Union[str, FileInput]
"cover": str, # actual: Union[str, FileInput]
},
"EncryptedPassportElement": {
"data": str, # actual: Union[IdDocumentData, PersonalDetails, ResidentialAddress]
},
# TODO: Deprecated and will be corrected (and removed) in next major PTB
# version:
"send_gift": {"gift_id": str}, # actual: Non optional
}
# param names ignored in the param type checking in classes for the `tg.Defaults` case.
IGNORED_DEFAULTS_PARAM_NAMES = {
@@ -198,6 +203,8 @@ IGNORED_PARAM_REQUIREMENTS = {
"send_venue": {"latitude", "longitude", "title", "address"},
"send_contact": {"phone_number", "first_name"},
# ---->
# here for backwards compatibility. Todo: remove on next bot api release
"send_gift": {"gift_id"},
}
+12 -2
View File
@@ -731,8 +731,18 @@ class TestUserWithoutRequest(UserTestBase):
and kwargs["text_entities"] == "text_entities"
)
assert check_shortcut_signature(user.send_gift, Bot.send_gift, ["user_id"], [])
assert await check_shortcut_call(user.send_gift, user.get_bot(), "send_gift")
# TODO discuss if better way exists
# tags: deprecated NEXT.VERSION
with pytest.raises(
Exception,
match="Default for argument gift_id does not match the default of the Bot method.",
):
assert check_shortcut_signature(
user.send_gift, Bot.send_gift, ["user_id", "chat_id"], []
)
assert await check_shortcut_call(
user.send_gift, user.get_bot(), "send_gift", ["chat_id", "user_id"]
)
assert await check_defaults_handling(user.send_gift, user.get_bot())
monkeypatch.setattr(user.get_bot(), "send_gift", make_assertion)