Full Support for Bot API 8.0 (#4566, #4568, #4570, #4571, #4574, #4576, #4572)

This commit is contained in:
Bibo-Joshi 2024-12-01 10:26:48 +01:00 committed by GitHub
parent 151123745e
commit ef1685c436
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1479 additions and 45 deletions

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-7.11-blue?logo=telegram
.. image:: https://img.shields.io/badge/Bot%20API-8.0-blue?logo=telegram
:target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API version
@ -81,7 +81,7 @@ After installing_ the library, be sure to check out the section on `working with
Telegram API support
~~~~~~~~~~~~~~~~~~~~
All types and methods of the Telegram Bot API **7.11** are natively supported by this library.
All types and methods of the Telegram Bot API **8.0** 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

View file

@ -25,6 +25,8 @@
- Used for sending documents
* - :meth:`~telegram.Bot.send_game`
- Used for sending a game
* - :meth:`~telegram.Bot.send_gift`
- Used for sending a gift
* - :meth:`~telegram.Bot.send_invoice`
- Used for sending an invoice
* - :meth:`~telegram.Bot.send_location`
@ -151,6 +153,8 @@
- Used for setting a chat title
* - :meth:`~telegram.Bot.set_chat_description`
- Used for setting the description of a chat
* - :meth:`~telegram.Bot.set_user_emoji_status`
- Used for setting the users status emoji
* - :meth:`~telegram.Bot.pin_chat_message`
- Used for pinning a message
* - :meth:`~telegram.Bot.unpin_chat_message`
@ -355,7 +359,7 @@
.. raw:: html
<details>
<summary>Miscellaneous</summary>
<summary>Payments and Stars</summary>
.. list-table::
:align: left
@ -363,18 +367,39 @@
* - :meth:`~telegram.Bot.create_invoice_link`
- Used to generate an HTTP link for an invoice
* - :meth:`~telegram.Bot.edit_user_star_subscription`
- Used for editing a user's star subscription
* - :meth:`~telegram.Bot.get_star_transactions`
- Used for obtaining the bot's Telegram Stars transactions
* - :meth:`~telegram.Bot.refund_star_payment`
- Used for refunding a payment in Telegram Stars
.. raw:: html
</details>
<br>
.. raw:: html
<details>
<summary>Miscellaneous</summary>
.. list-table::
:align: left
:widths: 1 4
* - :meth:`~telegram.Bot.close`
- Used for closing server instance when switching to another local server
* - :meth:`~telegram.Bot.log_out`
- Used for logging out from cloud Bot API server
* - :meth:`~telegram.Bot.get_file`
- Used for getting basic info about a file
* - :meth:`~telegram.Bot.get_available_gifts`
- Used for getting information about gifts available for sending
* - :meth:`~telegram.Bot.get_me`
- Used for getting basic information about the bot
* - :meth:`~telegram.Bot.get_star_transactions`
- Used for obtaining the bot's Telegram Stars transactions
* - :meth:`~telegram.Bot.refund_star_payment`
- Used for refunding a payment in Telegram Stars
* - :meth:`~telegram.Bot.save_prepared_inline_message`
- Used for storing a message to be sent by a user of a Mini App
.. raw:: html

View file

@ -93,7 +93,6 @@ Available Types
telegram.inputpaidmediaphoto
telegram.inputpaidmediavideo
telegram.inputpolloption
telegram.inputsticker
telegram.keyboardbutton
telegram.keyboardbuttonpolltype
telegram.keyboardbuttonrequestchat

View file

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

View file

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

View file

@ -42,3 +42,4 @@ To enable this option, send the ``/setinline`` command to `@BotFather <https://t
telegram.inputvenuemessagecontent
telegram.inputcontactmessagecontent
telegram.inputinvoicemessagecontent
telegram.preparedinlinemessage

View file

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

View file

@ -6,6 +6,9 @@ The following methods and objects allow your bot to handle stickers and sticker
.. toctree::
:titlesonly:
telegram.gift
telegram.gifts
telegram.inputsticker
telegram.maskposition
telegram.sticker
telegram.stickerset

View file

@ -95,3 +95,5 @@
.. |tg_stars| replace:: `Telegram Stars <https://t.me/BotNews/90>`__
.. |allow_paid_broadcast| replace:: Pass True to allow up to :tg-const:`telegram.constants.FloodLimit.PAID_MESSAGES_PER_SECOND` messages per second, ignoring `broadcasting limits <https://core.telegram.org/bots/faq#how-can-i-message-all-of-my-bot-39s-subscribers-at-once>`__ for a fee of 0.1 Telegram Stars per message. The relevant Stars will be withdrawn from the bot's balance.
.. |tz-naive-dtms| replace:: For timezone naive :obj:`datetime.datetime` objects, the default timezone of the bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is used.

View file

@ -101,6 +101,8 @@ __all__ = (
"GameHighScore",
"GeneralForumTopicHidden",
"GeneralForumTopicUnhidden",
"Gift",
"Gifts",
"Giveaway",
"GiveawayCompleted",
"GiveawayCreated",
@ -201,6 +203,7 @@ __all__ = (
"PollAnswer",
"PollOption",
"PreCheckoutQuery",
"PreparedInlineMessage",
"ProximityAlertTriggered",
"ReactionCount",
"ReactionType",
@ -373,6 +376,7 @@ from ._forumtopic import (
from ._games.callbackgame import CallbackGame
from ._games.game import Game
from ._games.gamehighscore import GameHighScore
from ._gifts import Gift, Gifts
from ._giveaway import Giveaway, GiveawayCompleted, GiveawayCreated, GiveawayWinners
from ._inline.inlinekeyboardbutton import InlineKeyboardButton
from ._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
@ -405,6 +409,7 @@ from ._inline.inputlocationmessagecontent import InputLocationMessageContent
from ._inline.inputmessagecontent import InputMessageContent
from ._inline.inputtextmessagecontent import InputTextMessageContent
from ._inline.inputvenuemessagecontent import InputVenueMessageContent
from ._inline.preparedinlinemessage import PreparedInlineMessage
from ._keyboardbutton import KeyboardButton
from ._keyboardbuttonpolltype import KeyboardButtonPollType
from ._keyboardbuttonrequest import KeyboardButtonRequestChat, KeyboardButtonRequestUsers

View file

@ -24,7 +24,7 @@ import contextlib
import copy
import pickle
from collections.abc import Sequence
from datetime import datetime
from datetime import datetime, timedelta
from types import TracebackType
from typing import (
TYPE_CHECKING,
@ -75,7 +75,9 @@ from telegram._files.videonote import VideoNote
from telegram._files.voice import Voice
from telegram._forumtopic import ForumTopic
from telegram._games.gamehighscore import GameHighScore
from telegram._gifts import Gift, Gifts
from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton
from telegram._inline.preparedinlinemessage import PreparedInlineMessage
from telegram._menubutton import MenuButton
from telegram._message import Message
from telegram._messageid import MessageId
@ -3641,6 +3643,65 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
api_kwargs=api_kwargs,
)
async def save_prepared_inline_message(
self,
user_id: int,
result: "InlineQueryResult",
allow_user_chats: Optional[bool] = None,
allow_bot_chats: Optional[bool] = None,
allow_group_chats: Optional[bool] = None,
allow_channel_chats: Optional[bool] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> PreparedInlineMessage:
"""Stores a message that can be sent by a user of a Mini App.
.. versionadded:: NEXT.VERSION
Args:
user_id (:obj:`int`): Unique identifier of the target user that can use the prepared
message.
result (:class:`telegram.InlineQueryResult`): The result to store.
allow_user_chats (:obj:`bool`, optional): Pass :obj:`True` if the message can be sent
to private chats with users
allow_bot_chats (:obj:`bool`, optional): Pass :obj:`True` if the message can be sent
to private chats with bots
allow_group_chats (:obj:`bool`, optional): Pass :obj:`True` if the message can be sent
to group and supergroup chats
allow_channel_chats (:obj:`bool`, optional): Pass :obj:`True` if the message can be
sent to channels
Returns:
:class:`telegram.PreparedInlineMessage`: On success, the prepared message is returned.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {
"user_id": user_id,
"result": result,
"allow_user_chats": allow_user_chats,
"allow_bot_chats": allow_bot_chats,
"allow_group_chats": allow_group_chats,
"allow_channel_chats": allow_channel_chats,
}
return PreparedInlineMessage.de_json( # type: ignore[return-value]
await self._post(
"savePreparedInlineMessage",
data,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
),
self,
)
async def get_user_profile_photos(
self,
user_id: int,
@ -3779,9 +3840,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
be unbanned, unix time. If user is banned for more than 366 days or less than 30
seconds from the current time they are considered to be banned forever. Applied
for supergroups and channels only.
For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is
used.
|tz-naive-dtms|
revoke_messages (:obj:`bool`, optional): Pass :obj:`True` to delete all messages from
the chat for the user that is being removed. If :obj:`False`, the user will be able
to see messages in the group that were sent before the user was removed.
@ -5415,9 +5474,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
will be lifted for the user, unix time. If user is restricted for more than 366
days or less than 30 seconds from the current time, they are considered to be
restricted forever.
For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is
used.
|tz-naive-dtms|
permissions (:class:`telegram.ChatPermissions`): An object for new user
permissions.
use_independent_chat_permissions (:obj:`bool`, optional): Pass :obj:`True` if chat
@ -5761,9 +5818,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
expire_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the link will
expire. Integer input will be interpreted as Unix timestamp.
For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is
used.
|tz-naive-dtms|
member_limit (:obj:`int`, optional): Maximum number of users that can be members of
the chat simultaneously after joining the chat via this invite link;
:tg-const:`telegram.constants.ChatInviteLinkLimit.MIN_MEMBER_LIMIT`-
@ -5840,9 +5895,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
Now also accepts :class:`telegram.ChatInviteLink` instances.
expire_date (:obj:`int` | :obj:`datetime.datetime`, optional): Date when the link will
expire.
For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is
used.
|tz-naive-dtms|
member_limit (:obj:`int`, optional): Maximum number of users that can be members of
the chat simultaneously after joining the chat via this invite link;
:tg-const:`telegram.constants.ChatInviteLinkLimit.MIN_MEMBER_LIMIT`-
@ -6176,6 +6229,56 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
api_kwargs=api_kwargs,
)
async def set_user_emoji_status(
self,
user_id: int,
emoji_status_custom_emoji_id: Optional[str] = None,
emoji_status_expiration_date: Optional[Union[int, datetime]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> bool:
"""Changes the emoji status for a given user that previously allowed the bot to manage
their emoji status via the Mini App method
`requestEmojiStatusAccess <https://core.telegram.org/bots/webapps#initializing-mini-apps>`_
.
.. versionadded:: NEXT.VERSION
Args:
user_id (:obj:`int`): Unique identifier of the target user
emoji_status_custom_emoji_id (:obj:`str`, optional): Custom emoji identifier of the
emoji status to set. Pass an empty string to remove the status.
emoji_status_expiration_date (Union[:obj:`int`, :obj:`datetime.datetime`], optional):
Expiration date of the emoji status, if any, as unix timestamp or
:class:`datetime.datetime` object.
|tz-naive-dtms|
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {
"user_id": user_id,
"emoji_status_custom_emoji_id": emoji_status_custom_emoji_id,
"emoji_status_expiration_date": emoji_status_expiration_date,
}
return await self._post(
"setUserEmojiStatus",
data,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def pin_chat_message(
self,
chat_id: Union[str, int],
@ -7127,9 +7230,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
:tg-const:`telegram.Poll.MIN_OPEN_PERIOD` and no more than
:tg-const:`telegram.Poll.MAX_OPEN_PERIOD` seconds in the future.
Can't be used together with :paramref:`open_period`.
For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is
used.
|tz-naive-dtms|
is_closed (:obj:`bool`, optional): Pass :obj:`True`, if the poll needs to be
immediately closed. This can be useful for poll preview.
disable_notification (:obj:`bool`, optional): |disable_notification|
@ -8024,6 +8125,8 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
send_phone_number_to_provider: Optional[bool] = None,
send_email_to_provider: Optional[bool] = None,
is_flexible: Optional[bool] = None,
subscription_period: Optional[Union[int, timedelta]] = None,
business_connection_id: Optional[str] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@ -8036,6 +8139,10 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
.. versionadded:: 20.0
Args:
business_connection_id (:obj:`str`, optional): |business_id_str|
For payments in |tg_stars| only.
.. versionadded:: NEXT.VERSION
title (:obj:`str`): Product name. :tg-const:`telegram.Invoice.MIN_TITLE_LENGTH`-
:tg-const:`telegram.Invoice.MAX_TITLE_LENGTH` characters.
description (:obj:`str`): Product description.
@ -8062,6 +8169,15 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
.. versionchanged:: 20.0
|sequenceargs|
subscription_period (:obj:`int` | :class:`datetime.timedelta`, optional): The time the
subscription will be active for before the next payment, either as number of
seconds or as :class:`datetime.timedelta` object. The currency must be set to
``XTR`` (Telegram Stars) if the parameter is used. Currently, it must always be
:tg-const:`telegram.constants.InvoiceLimit.SUBSCRIPTION_PERIOD` if specified. Any
number of subscriptions can be active for a given bot at the same time, including
multiple concurrent subscriptions from the same user.
.. versionadded:: NEXT.VERSION
max_tip_amount (:obj:`int`, optional): The maximum accepted amount for tips in the
*smallest units* of the currency (integer, **not** float/double). For example, for
a maximum tip of ``US$ 1.45`` pass ``max_tip_amount = 145``. See the ``exp``
@ -8127,6 +8243,12 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"is_flexible": is_flexible,
"send_phone_number_to_provider": send_phone_number_to_provider,
"send_email_to_provider": send_email_to_provider,
"subscription_period": (
subscription_period.total_seconds()
if isinstance(subscription_period, timedelta)
else subscription_period
),
"business_connection_id": business_connection_id,
}
return await self._post(
@ -9254,6 +9376,53 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
bot=self,
)
async def edit_user_star_subscription(
self,
user_id: int,
telegram_payment_charge_id: str,
is_canceled: bool,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> bool:
"""Allows the bot to cancel or re-enable extension of a subscription paid in Telegram
Stars.
.. versionadded:: NEXT.VERSION
Args:
user_id (:obj:`int`): Identifier of the user whose subscription will be edited.
telegram_payment_charge_id (:obj:`str`): Telegram payment identifier for the
subscription.
is_canceled (:obj:`bool`): Pass :obj:`True` to cancel extension of the user
subscription; the subscription must be active up to the end of the current
subscription period. Pass :obj:`False` to allow the user to re-enable a
subscription that was previously canceled by the bot.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {
"user_id": user_id,
"telegram_payment_charge_id": telegram_payment_charge_id,
"is_canceled": is_canceled,
}
return await self._post(
"editUserStartSubscription",
data,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def send_paid_media(
self,
chat_id: Union[str, int],
@ -9475,6 +9644,99 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
return ChatInviteLink.de_json(result, self) # type: ignore[return-value]
async def get_available_gifts(
self,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> Gifts:
"""Returns the list of gifts that can be sent by the bot to users.
Requires no parameters.
.. versionadded:: NEXT.VERSION
Returns:
:class:`telegram.Gifts`
Raises:
:class:`telegram.error.TelegramError`
"""
return Gifts.de_json( # type: ignore[return-value]
await self._post(
"getAvailableGifts",
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
)
async def send_gift(
self,
user_id: int,
gift_id: Union[str, Gift],
text: Optional[str] = None,
text_parse_mode: ODVInput[str] = DEFAULT_NONE,
text_entities: Optional[Sequence["MessageEntity"]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> bool:
"""Sends a gift to the given user.
The gift can't be converted to Telegram Stars by the user
.. versionadded:: NEXT.VERSION
Args:
user_id (:obj:`int`): Unique identifier of the target user that will receive the gift
gift_id (:obj:`str` | :class:`~telegram.Gift`): Identifier of the gift or a
:class:`~telegram.Gift` object
text (:obj:`str`, optional): Text that will be shown along with the gift;
0- :tg-const:`telegram.constants.GiftLimit.MAX_TEXT_LENGTH` characters
text_parse_mode (:obj:`str`, optional): Mode for parsing entities.
See :class:`telegram.constants.ParseMode` and
`formatting options <https://core.telegram.org/bots/api#formatting-options>`__ for
more details. Entities other than :attr:`~MessageEntity.BOLD`,
:attr:`~MessageEntity.ITALIC`, :attr:`~MessageEntity.UNDERLINE`,
:attr:`~MessageEntity.STRIKETHROUGH`, :attr:`~MessageEntity.SPOILER`, and
:attr:`~MessageEntity.CUSTOM_EMOJI` are ignored.
text_entities (Sequence[:class:`telegram.MessageEntity`], optional): A list of special
entities that appear in the gift text. It can be specified instead of
:paramref:`text_parse_mode`. Entities other than :attr:`~MessageEntity.BOLD`,
:attr:`~MessageEntity.ITALIC`, :attr:`~MessageEntity.UNDERLINE`,
:attr:`~MessageEntity.STRIKETHROUGH`, :attr:`~MessageEntity.SPOILER`, and
:attr:`~MessageEntity.CUSTOM_EMOJI` are ignored.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {
"user_id": user_id,
"gift_id": gift_id.id if isinstance(gift_id, Gift) else gift_id,
"text": text,
"text_parse_mode": text_parse_mode,
"text_entities": text_entities,
}
return await self._post(
"sendGift",
data,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
def to_dict(self, recursive: bool = True) -> JSONDict: # noqa: ARG002
"""See :meth:`telegram.TelegramObject.to_dict`."""
data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name}
@ -9531,6 +9793,8 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"""Alias for :meth:`send_chat_action`"""
answerInlineQuery = answer_inline_query
"""Alias for :meth:`answer_inline_query`"""
savePreparedInlineMessage = save_prepared_inline_message
"""Alias for :meth:`save_prepared_inline_message`"""
getUserProfilePhotos = get_user_profile_photos
"""Alias for :meth:`get_user_profile_photos`"""
getFile = get_file
@ -9615,6 +9879,8 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"""Alias for :meth:`set_chat_title`"""
setChatDescription = set_chat_description
"""Alias for :meth:`set_chat_description`"""
setUserEmojiStatus = set_user_emoji_status
"""Alias for :meth:`set_user_emoji_status`"""
pinChatMessage = pin_chat_message
"""Alias for :meth:`pin_chat_message`"""
unpinChatMessage = unpin_chat_message
@ -9729,9 +9995,15 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"""Alias for :meth:`refund_star_payment`"""
getStarTransactions = get_star_transactions
"""Alias for :meth:`get_star_transactions`"""
editUserStarSubscription = edit_user_star_subscription
"""Alias for :meth:`edit_user_star_subscription`"""
sendPaidMedia = send_paid_media
"""Alias for :meth:`send_paid_media`"""
createChatSubscriptionInviteLink = create_chat_subscription_invite_link
"""Alias for :meth:`create_chat_subscription_invite_link`"""
editChatSubscriptionInviteLink = edit_chat_subscription_invite_link
"""Alias for :meth:`edit_chat_subscription_invite_link`"""
getAvailableGifts = get_available_gifts
"""Alias for :meth:`get_available_gifts`"""
sendGift = send_gift
"""Alias for :meth:`send_gift`"""

View file

@ -44,6 +44,7 @@ if TYPE_CHECKING:
ChatMember,
Contact,
Document,
Gift,
InlineKeyboardMarkup,
InputMediaAudio,
InputMediaDocument,
@ -3436,6 +3437,46 @@ class _ChatBase(TelegramObject):
allow_paid_broadcast=allow_paid_broadcast,
)
async def send_gift(
self,
gift_id: Union[str, "Gift"],
text: Optional[str] = None,
text_parse_mode: ODVInput[str] = DEFAULT_NONE,
text_entities: Optional[Sequence["MessageEntity"]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> bool:
"""Shortcut for::
await bot.send_gift(user_id=update.effective_chat.id, *args, **kwargs )
For the documentation of the arguments, please see :meth:`telegram.Bot.send_gift`.
Caution:
Can only work, if the chat is a private chat, see :attr:`type`.
.. versionadded:: NEXT.VERSION
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return await self.get_bot().send_gift(
user_id=self.id,
gift_id=gift_id,
text=text,
text_parse_mode=text_parse_mode,
text_entities=text_entities,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
class Chat(_ChatBase):
"""This object represents a chat.

136
telegram/_gifts.py Normal file
View file

@ -0,0 +1,136 @@
#!/usr/bin/env python
# pylint: disable=redefined-builtin
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# Leandro Toledo de Souza <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 classes related to gifs sent by bots."""
from collections.abc import Sequence
from typing import TYPE_CHECKING, Optional
from telegram._files.sticker import Sticker
from telegram._telegramobject import TelegramObject
from telegram._utils.argumentparsing import parse_sequence_arg
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
from telegram import Bot
class Gift(TelegramObject):
"""This object represents a gift that can be sent by the bot.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal if their :attr:`id` is equal.
.. versionadded:: NEXT.VERSION
Args:
id (:obj:`str`): Unique identifier of the gift
sticker (:class:`~telegram.Sticker`): The sticker that represents the gift
star_count (:obj:`int`): The number of Telegram Stars that must be paid to send the sticker
total_count (:obj:`int`, optional): The total number of the gifts of this type that can be
sent; for limited gifts only
remaining_count (:obj:`int`, optional): The number of remaining gifts of this type that can
be sent; for limited gifts only
Attributes:
id (:obj:`str`): Unique identifier of the gift
sticker (:class:`~telegram.Sticker`): The sticker that represents the gift
star_count (:obj:`int`): The number of Telegram Stars that must be paid to send the sticker
total_count (:obj:`int`): Optional. The total number of the gifts of this type that can be
sent; for limited gifts only
remaining_count (:obj:`int`): Optional. The number of remaining gifts of this type that can
be sent; for limited gifts only
"""
__slots__ = ("id", "remaining_count", "star_count", "sticker", "total_count")
def __init__(
self,
id: str,
sticker: Sticker,
star_count: int,
total_count: Optional[int] = None,
remaining_count: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.id: str = id
self.sticker: Sticker = sticker
self.star_count: int = star_count
self.total_count: Optional[int] = total_count
self.remaining_count: Optional[int] = remaining_count
self._id_attrs = (self.id,)
self._freeze()
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Gift"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
data["sticker"] = Sticker.de_json(data.get("sticker"), bot)
return cls(**data)
class Gifts(TelegramObject):
"""This object represent a list of gifts.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal if their :attr:`gifts` are equal.
.. versionadded:: NEXT.VERSION
Args:
gifts (Sequence[:class:`Gift`]): The sequence of gifts
Attributes:
gifts (tuple[:class:`Gift`]): The sequence of gifts
"""
__slots__ = ("gifts",)
def __init__(
self,
gifts: Sequence[Gift],
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.gifts: tuple[Gift, ...] = parse_sequence_arg(gifts)
self._id_attrs = (self.gifts,)
self._freeze()
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optional["Gifts"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
data["gifts"] = Gift.de_list(data.get("gifts"), bot)
return cls(**data)

View file

@ -0,0 +1,83 @@
#!/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 an object that represents a Telegram Prepared inline Message."""
import datetime as dtm
from typing import TYPE_CHECKING, Optional
from telegram._telegramobject import TelegramObject
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
from telegram import Bot
class PreparedInlineMessage(TelegramObject):
"""Describes an inline message to be sent by a user of a Mini App.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`id` is equal.
.. versionadded:: NEXT.VERSION
Args:
id (:obj:`str`): Unique identifier of the prepared message
expiration_date (:class:`datetime.datetime`): Expiration date of the prepared message.
Expired prepared messages can no longer be used.
|datetime_localization|
Attributes:
id (:obj:`str`): Unique identifier of the prepared message
expiration_date (:class:`datetime.datetime`): Expiration date of the prepared message.
Expired prepared messages can no longer be used.
|datetime_localization|
"""
__slots__ = ("expiration_date", "id")
def __init__(
self,
id: str, # pylint: disable=redefined-builtin
expiration_date: dtm.datetime,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.id: str = id
self.expiration_date: dtm.datetime = expiration_date
self._id_attrs = (self.id,)
self._freeze()
@classmethod
def de_json(
cls, data: Optional[JSONDict], bot: Optional["Bot"] = None
) -> Optional["PreparedInlineMessage"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if data is None:
return None
# Get the local timezone from the bot if it has defaults
loc_tzinfo = extract_tzinfo_from_defaults(bot)
data["expiration_date"] = from_timestamp(data.get("expiration_date"), tzinfo=loc_tzinfo)
return super().de_json(data=data, bot=bot)

View file

@ -19,11 +19,12 @@
# pylint: disable=redefined-builtin
"""This module contains the classes for Telegram Stars transactions."""
import datetime as dtm
from collections.abc import Sequence
from datetime import datetime
from typing import TYPE_CHECKING, Final, Optional
from telegram import constants
from telegram._gifts import Gift
from telegram._paidmedia import PaidMedia
from telegram._telegramobject import TelegramObject
from telegram._user import User
@ -144,7 +145,7 @@ class RevenueWithdrawalStateSucceeded(RevenueWithdrawalState):
def __init__(
self,
date: datetime,
date: dtm.datetime,
url: str,
*,
api_kwargs: Optional[JSONDict] = None,
@ -152,7 +153,7 @@ class RevenueWithdrawalStateSucceeded(RevenueWithdrawalState):
super().__init__(type=RevenueWithdrawalState.SUCCEEDED, api_kwargs=api_kwargs)
with self._unfrozen():
self.date: datetime = date
self.date: dtm.datetime = date
self.url: str = url
self._id_attrs = (
self.type,
@ -328,30 +329,51 @@ class TransactionPartnerUser(TransactionPartner):
Args:
user (:class:`telegram.User`): Information about the user.
invoice_payload (:obj:`str`, optional): Bot-specified invoice payload.
subscription_period (:class:`datetime.timedelta`, optional): The duration of the paid
subscription
.. versionadded:: NEXT.VERSION
paid_media (Sequence[:class:`telegram.PaidMedia`], optional): Information about the paid
media bought by the user.
.. versionadded:: 21.5
paid_media_payload (:obj:`str`, optional): Optional. Bot-specified paid media payload.
paid_media_payload (:obj:`str`, optional): Bot-specified paid media payload.
.. versionadded:: 21.6
gift (:class:`telegram.Gift`, optional): The gift sent to the user by the bot
.. versionadded:: NEXT.VERSION
Attributes:
type (:obj:`str`): The type of the transaction partner,
always :tg-const:`telegram.TransactionPartner.USER`.
user (:class:`telegram.User`): Information about the user.
invoice_payload (:obj:`str`): Optional. Bot-specified invoice payload.
subscription_period (:class:`datetime.timedelta`): Optional. The duration of the paid
subscription
.. versionadded:: NEXT.VERSION
paid_media (tuple[:class:`telegram.PaidMedia`]): Optional. Information about the paid
media bought by the user.
.. versionadded:: 21.5
paid_media_payload (:obj:`str`): Optional. Optional. Bot-specified paid media payload.
paid_media_payload (:obj:`str`): Optional. Bot-specified paid media payload.
.. versionadded:: 21.6
gift (:class:`telegram.Gift`): Optional. The gift sent to the user by the bot
.. versionadded:: NEXT.VERSION
"""
__slots__ = ("invoice_payload", "paid_media", "paid_media_payload", "user")
__slots__ = (
"gift",
"invoice_payload",
"paid_media",
"paid_media_payload",
"subscription_period",
"user",
)
def __init__(
self,
@ -359,6 +381,8 @@ class TransactionPartnerUser(TransactionPartner):
invoice_payload: Optional[str] = None,
paid_media: Optional[Sequence[PaidMedia]] = None,
paid_media_payload: Optional[str] = None,
subscription_period: Optional[dtm.timedelta] = None,
gift: Optional[Gift] = None,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
@ -369,6 +393,9 @@ class TransactionPartnerUser(TransactionPartner):
self.invoice_payload: Optional[str] = invoice_payload
self.paid_media: Optional[tuple[PaidMedia, ...]] = parse_sequence_arg(paid_media)
self.paid_media_payload: Optional[str] = paid_media_payload
self.subscription_period: Optional[dtm.timedelta] = subscription_period
self.gift: Optional[Gift] = gift
self._id_attrs = (
self.type,
self.user,
@ -386,6 +413,12 @@ class TransactionPartnerUser(TransactionPartner):
data["user"] = User.de_json(data.get("user"), bot)
data["paid_media"] = PaidMedia.de_list(data.get("paid_media"), bot=bot)
data["subscription_period"] = (
dtm.timedelta(seconds=sp)
if (sp := data.get("subscription_period")) is not None
else None
)
data["gift"] = Gift.de_json(data.get("gift"), bot)
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
@ -496,7 +529,7 @@ class StarTransaction(TelegramObject):
self,
id: str,
amount: int,
date: datetime,
date: dtm.datetime,
source: Optional[TransactionPartner] = None,
receiver: Optional[TransactionPartner] = None,
*,
@ -505,7 +538,7 @@ class StarTransaction(TelegramObject):
super().__init__(api_kwargs=api_kwargs)
self.id: str = id
self.amount: int = amount
self.date: datetime = date
self.date: dtm.datetime = date
self.source: Optional[TransactionPartner] = source
self.receiver: Optional[TransactionPartner] = receiver

View file

@ -18,10 +18,12 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram SuccessfulPayment."""
import datetime as dtm
from typing import TYPE_CHECKING, Optional
from telegram._payment.orderinfo import OrderInfo
from telegram._telegramobject import TelegramObject
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
from telegram._utils.types import JSONDict
if TYPE_CHECKING:
@ -45,6 +47,17 @@ class SuccessfulPayment(TelegramObject):
it shows the number of digits past the decimal point for each currency
(2 for the majority of currencies).
invoice_payload (:obj:`str`): Bot-specified invoice payload.
subscription_expiration_date (:class:`datetime.datetime`, optional): Expiration date of the
subscription; for recurring payments only.
.. versionadded:: NEXT.VERSION
is_recurring (:obj:`bool`, optional): True, if the payment is for a subscription.
.. versionadded:: NEXT.VERSION
is_first_recurring (:obj:`bool`, optional): True, if the payment is the first payment of a
subscription.
.. versionadded:: NEXT.VERSION
shipping_option_id (:obj:`str`, optional): Identifier of the shipping option chosen by the
user.
order_info (:class:`telegram.OrderInfo`, optional): Order info provided by the user.
@ -61,6 +74,17 @@ class SuccessfulPayment(TelegramObject):
it shows the number of digits past the decimal point for each currency
(2 for the majority of currencies).
invoice_payload (:obj:`str`): Bot-specified invoice payload.
subscription_expiration_date (:class:`datetime.datetime`): Optional. Expiration
date of the subscription; for recurring payments only.
.. versionadded:: NEXT.VERSION
is_recurring (:obj:`bool`): Optional. True, if the payment is for a subscription.
.. versionadded:: NEXT.VERSION
is_first_recurring (:obj:`bool`): Optional. True, if the payment is the first payment of a
subscription.
.. versionadded:: NEXT.VERSION
shipping_option_id (:obj:`str`): Optional. Identifier of the shipping option chosen by the
user.
order_info (:class:`telegram.OrderInfo`): Optional. Order info provided by the user.
@ -72,9 +96,12 @@ class SuccessfulPayment(TelegramObject):
__slots__ = (
"currency",
"invoice_payload",
"is_first_recurring",
"is_recurring",
"order_info",
"provider_payment_charge_id",
"shipping_option_id",
"subscription_expiration_date",
"telegram_payment_charge_id",
"total_amount",
)
@ -88,6 +115,9 @@ class SuccessfulPayment(TelegramObject):
provider_payment_charge_id: str,
shipping_option_id: Optional[str] = None,
order_info: Optional[OrderInfo] = None,
subscription_expiration_date: Optional[dtm.datetime] = None,
is_recurring: Optional[bool] = None,
is_first_recurring: Optional[bool] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@ -99,6 +129,9 @@ class SuccessfulPayment(TelegramObject):
self.order_info: Optional[OrderInfo] = order_info
self.telegram_payment_charge_id: str = telegram_payment_charge_id
self.provider_payment_charge_id: str = provider_payment_charge_id
self.subscription_expiration_date: Optional[dtm.datetime] = subscription_expiration_date
self.is_recurring: Optional[bool] = is_recurring
self.is_first_recurring: Optional[bool] = is_first_recurring
self._id_attrs = (self.telegram_payment_charge_id, self.provider_payment_charge_id)
@ -116,4 +149,11 @@ class SuccessfulPayment(TelegramObject):
data["order_info"] = OrderInfo.de_json(data.get("order_info"), bot)
# Get the local timezone from the bot if it has defaults
loc_tzinfo = extract_tzinfo_from_defaults(bot)
data["subscription_expiration_date"] = from_timestamp(
data.get("subscription_expiration_date"), tzinfo=loc_tzinfo
)
return super().de_json(data=data, bot=bot)

View file

@ -636,6 +636,8 @@ class TelegramObject:
elif isinstance(value, datetime.datetime):
out[key] = to_timestamp(value)
elif isinstance(value, datetime.timedelta):
out[key] = value.total_seconds()
for key in pop_keys:
out.pop(key)

View file

@ -36,6 +36,7 @@ if TYPE_CHECKING:
Audio,
Contact,
Document,
Gift,
InlineKeyboardMarkup,
InputMediaAudio,
InputMediaDocument,
@ -1646,6 +1647,43 @@ class User(TelegramObject):
allow_paid_broadcast=allow_paid_broadcast,
)
async def send_gift(
self,
gift_id: Union[str, "Gift"],
text: Optional[str] = None,
text_parse_mode: ODVInput[str] = DEFAULT_NONE,
text_entities: Optional[Sequence["MessageEntity"]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
) -> bool:
"""Shortcut for::
await bot.send_gift( user_id=update.effective_user.id, *args, **kwargs )
For the documentation of the arguments, please see :meth:`telegram.Bot.send_gift`.
.. versionadded:: NEXT.VERSION
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return await self.get_bot().send_gift(
user_id=self.id,
gift_id=gift_id,
text=text,
text_parse_mode=text_parse_mode,
text_entities=text_entities,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def send_copy(
self,
from_chat_id: Union[str, int],

View file

@ -64,6 +64,7 @@ __all__ = [
"FloodLimit",
"ForumIconColor",
"ForumTopicLimit",
"GiftLimit",
"GiveawayLimit",
"InlineKeyboardButtonLimit",
"InlineKeyboardMarkupLimit",
@ -152,7 +153,7 @@ class _AccentColor(NamedTuple):
#: :data:`telegram.__bot_api_version_info__`.
#:
#: .. versionadded:: 20.0
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=11)
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=8, minor=0)
#: :obj:`str`: Telegram Bot API
#: version supported by this version of `python-telegram-bot`. Also available as
#: :data:`telegram.__bot_api_version__`.
@ -1224,6 +1225,21 @@ class ForumIconColor(IntEnum):
"""
class GiftLimit(IntEnum):
"""This enum contains limitations for :meth:`~telegram.Bot.send_gift`.
The enum members of this enumeration are instances of :class:`int` and can be treated as such.
.. versionadded:: NEXT.VERSION
"""
__slots__ = ()
MAX_TEXT_LENGTH = 255
""":obj:`int`: Maximum number of characters in a :obj:`str` passed as the
:paramref:`~telegram.Bot.send_gift.text` parameter of :meth:`~telegram.Bot.send_gift`.
"""
class GiveawayLimit(IntEnum):
"""This enum contains limitations for :class:`telegram.Giveaway` and related classes.
The enum members of this enumeration are instances of :class:`int` and can be treated as such.
@ -2902,6 +2918,13 @@ class InvoiceLimit(IntEnum):
.. versionadded:: 21.6
"""
SUBSCRIPTION_PERIOD = datetime.timedelta(days=30).total_seconds()
""":obj:`int`: The period of time for which the subscription is active before
the next payment, passed as :paramref:`~telegram.Bot.create_invoice_link.subscription_period`
parameter of :meth:`telegram.Bot.create_invoice_link`.
.. versionadded:: NEXT.VERSION
"""
class UserProfilePhotosLimit(IntEnum):

View file

@ -437,9 +437,7 @@ class CallbackDataCache:
Args:
time_cutoff (:obj:`float` | :obj:`datetime.datetime`, optional): Pass a UNIX timestamp
or a :obj:`datetime.datetime` to clear only entries which are older.
For timezone naive :obj:`datetime.datetime` objects, the default timezone of the
bot will be used, which is UTC unless :attr:`telegram.ext.Defaults.tzinfo` is
used.
|tz-naive-dtms|
"""
self.__clear(self._keyboard_data, time_cutoff=time_cutoff)

View file

@ -188,6 +188,7 @@ class Defaults:
"explanation_parse_mode",
"link_preview_options",
"parse_mode",
"text_parse_mode",
"protect_content",
"question_parse_mode",
):
@ -271,7 +272,8 @@ class Defaults:
@property
def text_parse_mode(self) -> Optional[str]:
""":obj:`str`: Optional. Alias for :attr:`parse_mode`, used for
the corresponding parameter of :class:`telegram.InputPollOption`.
the corresponding parameter of :class:`telegram.InputPollOption` and
:meth:`telegram.Bot.send_gift`.
.. versionadded:: 21.2
"""

View file

@ -20,7 +20,7 @@
"""This module contains an object that represents a Telegram Bot with convenience extensions."""
from collections.abc import Sequence
from copy import copy
from datetime import datetime
from datetime import datetime, timedelta
from typing import (
TYPE_CHECKING,
Any,
@ -57,6 +57,8 @@ from telegram import (
File,
ForumTopic,
GameHighScore,
Gift,
Gifts,
InlineKeyboardMarkup,
InlineQueryResultsButton,
InputMedia,
@ -69,6 +71,7 @@ from telegram import (
MessageId,
PhotoSize,
Poll,
PreparedInlineMessage,
ReactionType,
ReplyParameters,
SentWebAppMessage,
@ -979,6 +982,36 @@ class ExtBot(Bot, Generic[RLARGS]):
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
async def save_prepared_inline_message(
self,
user_id: int,
result: "InlineQueryResult",
allow_user_chats: Optional[bool] = None,
allow_bot_chats: Optional[bool] = None,
allow_group_chats: Optional[bool] = None,
allow_channel_chats: Optional[bool] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
rate_limit_args: Optional[RLARGS] = None,
) -> PreparedInlineMessage:
return await super().save_prepared_inline_message(
user_id=user_id,
result=result,
allow_user_chats=allow_user_chats,
allow_bot_chats=allow_bot_chats,
allow_group_chats=allow_group_chats,
allow_channel_chats=allow_channel_chats,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
async def answer_pre_checkout_query(
self,
pre_checkout_query_id: str,
@ -1171,6 +1204,8 @@ class ExtBot(Bot, Generic[RLARGS]):
send_phone_number_to_provider: Optional[bool] = None,
send_email_to_provider: Optional[bool] = None,
is_flexible: Optional[bool] = None,
subscription_period: Optional[Union[int, timedelta]] = None,
business_connection_id: Optional[str] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@ -1204,6 +1239,8 @@ class ExtBot(Bot, Generic[RLARGS]):
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
subscription_period=subscription_period,
business_connection_id=business_connection_id,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
@ -3385,6 +3422,30 @@ class ExtBot(Bot, Generic[RLARGS]):
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
async def set_user_emoji_status(
self,
user_id: int,
emoji_status_custom_emoji_id: Optional[str] = None,
emoji_status_expiration_date: Optional[Union[int, datetime]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
rate_limit_args: Optional[RLARGS] = None,
) -> bool:
return await super().set_user_emoji_status(
user_id=user_id,
emoji_status_custom_emoji_id=emoji_status_custom_emoji_id,
emoji_status_expiration_date=emoji_status_expiration_date,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
async def set_chat_menu_button(
self,
chat_id: Optional[int] = None,
@ -4255,6 +4316,30 @@ class ExtBot(Bot, Generic[RLARGS]):
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
async def edit_user_star_subscription(
self,
user_id: int,
telegram_payment_charge_id: str,
is_canceled: bool,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
rate_limit_args: Optional[RLARGS] = None,
) -> bool:
return await super().edit_user_star_subscription(
user_id=user_id,
telegram_payment_charge_id=telegram_payment_charge_id,
is_canceled=is_canceled,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
async def send_paid_media(
self,
chat_id: Union[str, int],
@ -4355,6 +4440,52 @@ class ExtBot(Bot, Generic[RLARGS]):
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
async def get_available_gifts(
self,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
rate_limit_args: Optional[RLARGS] = None,
) -> Gifts:
return await super().get_available_gifts(
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
async def send_gift(
self,
user_id: int,
gift_id: Union[str, Gift],
text: Optional[str] = None,
text_parse_mode: ODVInput[str] = DEFAULT_NONE,
text_entities: Optional[Sequence["MessageEntity"]] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
rate_limit_args: Optional[RLARGS] = None,
) -> bool:
return await super().send_gift(
user_id=user_id,
gift_id=gift_id,
text=text,
text_parse_mode=text_parse_mode,
text_entities=text_entities,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
# updated camelCase aliases
getMe = get_me
sendMessage = send_message
@ -4379,6 +4510,7 @@ class ExtBot(Bot, Generic[RLARGS]):
sendGame = send_game
sendChatAction = send_chat_action
answerInlineQuery = answer_inline_query
savePreparedInlineMessage = save_prepared_inline_message
getUserProfilePhotos = get_user_profile_photos
getFile = get_file
banChatMember = ban_chat_member
@ -4421,6 +4553,7 @@ class ExtBot(Bot, Generic[RLARGS]):
deleteChatPhoto = delete_chat_photo
setChatTitle = set_chat_title
setChatDescription = set_chat_description
setUserEmojiStatus = set_user_emoji_status
pinChatMessage = pin_chat_message
unpinChatMessage = unpin_chat_message
unpinAllChatMessages = unpin_all_chat_messages
@ -4478,6 +4611,9 @@ class ExtBot(Bot, Generic[RLARGS]):
replaceStickerInSet = replace_sticker_in_set
refundStarPayment = refund_star_payment
getStarTransactions = get_star_transactions
editUserStarSubscription = edit_user_star_subscription
createChatSubscriptionInviteLink = create_chat_subscription_invite_link
editChatSubscriptionInviteLink = edit_chat_subscription_invite_link
sendPaidMedia = send_paid_media
getAvailableGifts = get_available_gifts
sendGift = send_gift

View file

@ -0,0 +1,108 @@
#!/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 datetime as dtm
import pytest
from telegram import Location, PreparedInlineMessage
from telegram._utils.datetime import UTC, to_timestamp
from tests.auxil.slots import mro_slots
@pytest.fixture(scope="module")
def prepared_inline_message():
return PreparedInlineMessage(
PreparedInlineMessageTestBase.id,
PreparedInlineMessageTestBase.expiration_date,
)
class PreparedInlineMessageTestBase:
id = "some_uid"
expiration_date = dtm.datetime.now(tz=UTC).replace(microsecond=0)
class TestPreparedInlineMessageWithoutRequest(PreparedInlineMessageTestBase):
def test_slot_behaviour(self, prepared_inline_message):
inst = prepared_inline_message
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_expected_values(self, prepared_inline_message):
assert prepared_inline_message.id == self.id
assert prepared_inline_message.expiration_date == self.expiration_date
def test_de_json(self, prepared_inline_message):
json_dict = {
"id": self.id,
"expiration_date": to_timestamp(self.expiration_date),
}
new_prepared_inline_message = PreparedInlineMessage.de_json(json_dict, None)
assert isinstance(new_prepared_inline_message, PreparedInlineMessage)
assert new_prepared_inline_message.id == prepared_inline_message.id
assert (
new_prepared_inline_message.expiration_date == prepared_inline_message.expiration_date
)
def test_de_json_localization(self, offline_bot, raw_bot, tz_bot):
json_dict = {
"id": "some_uid",
"expiration_date": to_timestamp(self.expiration_date),
}
pim = PreparedInlineMessage.de_json(json_dict, offline_bot)
pim_raw = PreparedInlineMessage.de_json(json_dict, raw_bot)
pim_tz = PreparedInlineMessage.de_json(json_dict, tz_bot)
# comparing utcoffset because comparing tzinfo objects is not reliable
offset = pim_tz.expiration_date.utcoffset()
offset_tz = tz_bot.defaults.tzinfo.utcoffset(pim_tz.expiration_date.replace(tzinfo=None))
assert pim.expiration_date.tzinfo == UTC
assert pim_raw.expiration_date.tzinfo == UTC
assert offset_tz == offset
def test_to_dict(self, prepared_inline_message):
prepared_inline_message_dict = prepared_inline_message.to_dict()
assert isinstance(prepared_inline_message_dict, dict)
assert prepared_inline_message_dict["id"] == prepared_inline_message.id
assert prepared_inline_message_dict["expiration_date"] == to_timestamp(
self.expiration_date
)
def test_equality(self, prepared_inline_message):
a = prepared_inline_message
b = PreparedInlineMessage(self.id, self.expiration_date)
c = PreparedInlineMessage(self.id, self.expiration_date + dtm.timedelta(seconds=1))
d = PreparedInlineMessage("other_uid", self.expiration_date)
e = Location(123, 456)
assert a == b
assert hash(a) == hash(b)
assert a == c
assert hash(a) == hash(c)
assert a != d
assert hash(a) != hash(d)
assert a != e
assert hash(a) != hash(e)

View file

@ -17,6 +17,7 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
import asyncio
import datetime as dtm
import pytest
@ -122,10 +123,14 @@ class TestInvoiceWithoutRequest(InvoiceTestBase):
protect_content=True,
)
async def test_send_all_args_create_invoice_link(self, offline_bot, monkeypatch):
@pytest.mark.parametrize("subscription_period", [42, dtm.timedelta(seconds=42)])
async def test_send_all_args_create_invoice_link(
self, offline_bot, monkeypatch, subscription_period
):
async def make_assertion(*args, **_):
kwargs = args[1]
return all(kwargs[i] == i for i in kwargs)
sp = kwargs.pop("subscription_period") == 42
return all(kwargs[i] == i for i in kwargs) and sp
monkeypatch.setattr(offline_bot, "_post", make_assertion)
assert await offline_bot.create_invoice_link(
@ -149,6 +154,8 @@ class TestInvoiceWithoutRequest(InvoiceTestBase):
send_phone_number_to_provider="send_phone_number_to_provider",
send_email_to_provider="send_email_to_provider",
is_flexible="is_flexible",
business_connection_id="business_connection_id",
subscription_period=subscription_period,
)
async def test_send_object_as_provider_data(

View file

@ -16,9 +16,12 @@
#
# 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 datetime as dtm
import pytest
from telegram import OrderInfo, SuccessfulPayment
from telegram._utils.datetime import UTC, to_timestamp
from tests.auxil.slots import mro_slots
@ -32,6 +35,9 @@ def successful_payment():
SuccessfulPaymentTestBase.provider_payment_charge_id,
shipping_option_id=SuccessfulPaymentTestBase.shipping_option_id,
order_info=SuccessfulPaymentTestBase.order_info,
subscription_expiration_date=SuccessfulPaymentTestBase.subscription_expiration_date,
is_recurring=SuccessfulPaymentTestBase.is_recurring,
is_first_recurring=SuccessfulPaymentTestBase.is_first_recurring,
)
@ -43,6 +49,9 @@ class SuccessfulPaymentTestBase:
order_info = OrderInfo()
telegram_payment_charge_id = "telegram_payment_charge_id"
provider_payment_charge_id = "provider_payment_charge_id"
subscription_expiration_date = dtm.datetime.now(tz=UTC).replace(microsecond=0)
is_recurring = True
is_first_recurring = True
class TestSuccessfulPaymentWithoutRequest(SuccessfulPaymentTestBase):
@ -61,6 +70,9 @@ class TestSuccessfulPaymentWithoutRequest(SuccessfulPaymentTestBase):
"order_info": self.order_info.to_dict(),
"telegram_payment_charge_id": self.telegram_payment_charge_id,
"provider_payment_charge_id": self.provider_payment_charge_id,
"subscription_expiration_date": to_timestamp(self.subscription_expiration_date),
"is_recurring": self.is_recurring,
"is_first_recurring": self.is_first_recurring,
}
successful_payment = SuccessfulPayment.de_json(json_dict, offline_bot)
assert successful_payment.api_kwargs == {}
@ -72,6 +84,32 @@ class TestSuccessfulPaymentWithoutRequest(SuccessfulPaymentTestBase):
assert successful_payment.order_info == self.order_info
assert successful_payment.telegram_payment_charge_id == self.telegram_payment_charge_id
assert successful_payment.provider_payment_charge_id == self.provider_payment_charge_id
assert successful_payment.subscription_expiration_date == self.subscription_expiration_date
assert successful_payment.is_recurring == self.is_recurring
assert successful_payment.is_first_recurring == self.is_first_recurring
def test_de_json_localization(self, offline_bot, raw_bot, tz_bot):
json_dict = {
"invoice_payload": self.invoice_payload,
"currency": self.currency,
"total_amount": self.total_amount,
"telegram_payment_charge_id": self.telegram_payment_charge_id,
"provider_payment_charge_id": self.provider_payment_charge_id,
"subscription_expiration_date": to_timestamp(self.subscription_expiration_date),
}
successful_payment = SuccessfulPayment.de_json(json_dict, offline_bot)
successful_payment_raw = SuccessfulPayment.de_json(json_dict, raw_bot)
successful_payment_tz = SuccessfulPayment.de_json(json_dict, tz_bot)
# comparing utcoffsets because comparing timezones is unpredicatable
date_offset = successful_payment_tz.subscription_expiration_date.utcoffset()
tz_bot_offset = tz_bot.defaults.tzinfo.utcoffset(
successful_payment_tz.subscription_expiration_date.replace(tzinfo=None)
)
assert successful_payment_raw.subscription_expiration_date.tzinfo == UTC
assert successful_payment.subscription_expiration_date.tzinfo == UTC
assert date_offset == tz_bot_offset
def test_to_dict(self, successful_payment):
successful_payment_dict = successful_payment.to_dict()
@ -92,6 +130,13 @@ class TestSuccessfulPaymentWithoutRequest(SuccessfulPaymentTestBase):
successful_payment_dict["provider_payment_charge_id"]
== successful_payment.provider_payment_charge_id
)
assert successful_payment_dict["subscription_expiration_date"] == to_timestamp(
successful_payment.subscription_expiration_date
)
assert successful_payment_dict["is_recurring"] == successful_payment.is_recurring
assert (
successful_payment_dict["is_first_recurring"] == successful_payment.is_first_recurring
)
def test_equality(self):
a = SuccessfulPayment(

View file

@ -67,6 +67,7 @@ from telegram import (
MessageEntity,
Poll,
PollOption,
PreparedInlineMessage,
ReactionTypeCustomEmoji,
ReactionTypeEmoji,
ReplyParameters,
@ -2321,6 +2322,22 @@ class TestBotWithoutRequest:
obj = await offline_bot.get_star_transactions(offset=3)
assert isinstance(obj, StarTransactions)
async def test_edit_user_star_subscription(self, offline_bot, monkeypatch):
"""Can't properly test, so we only check that the correct values are passed"""
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
return (
request_data.parameters.get("user_id") == 42
and request_data.parameters.get("telegram_payment_charge_id")
== "telegram_payment_charge_id"
and request_data.parameters.get("is_canceled") is False
)
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.edit_user_star_subscription(
42, "telegram_payment_charge_id", False
)
async def test_create_chat_subscription_invite_link(
self,
monkeypatch,
@ -2336,6 +2353,39 @@ class TestBotWithoutRequest:
await offline_bot.create_chat_subscription_invite_link(1234, 2592000, 6)
@pytest.mark.parametrize(
"expiration_date", [dtm.datetime(2024, 1, 1), 1704067200], ids=["datetime", "timestamp"]
)
async def test_set_user_emoji_status_basic(self, offline_bot, monkeypatch, expiration_date):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
assert request_data.parameters.get("user_id") == 4242
assert (
request_data.parameters.get("emoji_status_custom_emoji_id")
== "emoji_status_custom_emoji_id"
)
assert request_data.parameters.get("emoji_status_expiration_date") == 1704067200
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
await offline_bot.set_user_emoji_status(
4242, "emoji_status_custom_emoji_id", expiration_date
)
async def test_set_user_emoji_status_default_timezone(self, tz_bot, monkeypatch):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
assert request_data.parameters.get("user_id") == 4242
assert (
request_data.parameters.get("emoji_status_custom_emoji_id")
== "emoji_status_custom_emoji_id"
)
assert request_data.parameters.get("emoji_status_expiration_date") == to_timestamp(
dtm.datetime(2024, 1, 1), tzinfo=tz_bot.defaults.tzinfo
)
monkeypatch.setattr(tz_bot.request, "post", make_assertion)
await tz_bot.set_user_emoji_status(
4242, "emoji_status_custom_emoji_id", dtm.datetime(2024, 1, 1)
)
class TestBotWithRequest:
"""
@ -2345,6 +2395,9 @@ class TestBotWithRequest:
is tested in `test_callbackdatacache`
"""
# get_available_gifts, send_gift are tested in `test_gift`.
# No need to duplicate here.
async def test_invalid_token_server_response(self):
with pytest.raises(InvalidToken, match="The token `12` was rejected by the server."):
async with ExtBot(token="12"):
@ -2841,6 +2894,15 @@ class TestBotWithRequest:
1234, results=inline_results, next_offset=42, current_offset=51
)
async def test_save_prepared_inline_message(self, bot, chat_id):
# We can't really check that the result is stored correctly, we just ensur ethat we get
# a proper return value
result = InlineQueryResultArticle(
id="some_id", title="title", input_message_content=InputTextMessageContent("text")
)
out = await bot.save_prepared_inline_message(chat_id, result, True, False, True, False)
assert isinstance(out, PreparedInlineMessage)
async def test_get_user_profile_photos(self, bot, chat_id):
user_profile_photos = await bot.get_user_profile_photos(chat_id)
assert user_profile_photos.photos[0][0].file_size == 5403

View file

@ -1311,6 +1311,28 @@ class TestChatWithoutRequest(ChatTestBase):
media="media", star_count=42, caption="stars", payload="payload"
)
async def test_instance_method_send_gift(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
return (
kwargs["user_id"] == chat.id
and kwargs["gift_id"] == "gift_id"
and kwargs["text"] == "text"
and kwargs["text_parse_mode"] == "text_parse_mode"
and kwargs["text_entities"] == "text_entities"
)
assert check_shortcut_signature(Chat.send_gift, Bot.send_gift, ["user_id"], [])
assert await check_shortcut_call(chat.send_gift, chat.get_bot(), "send_gift")
assert await check_defaults_handling(chat.send_gift, chat.get_bot())
monkeypatch.setattr(chat.get_bot(), "send_gift", make_assertion)
assert await chat.send_gift(
gift_id="gift_id",
text="text",
text_parse_mode="text_parse_mode",
text_entities="text_entities",
)
def test_mention_html(self):
chat = Chat(id=1, type="foo")
with pytest.raises(TypeError, match="Can not create a mention to a private group chat"):

267
tests/test_gifts.py Normal file
View file

@ -0,0 +1,267 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# Leandro Toledo de Souza <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/].
from collections.abc import Sequence
import pytest
from telegram import BotCommand, Gift, Gifts, MessageEntity, Sticker
from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram.request import RequestData
from tests.auxil.slots import mro_slots
@pytest.fixture
def gift(request):
return Gift(
id=GiftTestBase.id,
sticker=GiftTestBase.sticker,
star_count=GiftTestBase.star_count,
total_count=GiftTestBase.total_count,
remaining_count=GiftTestBase.remaining_count,
)
class GiftTestBase:
id = "some_id"
sticker = Sticker(
file_id="file_id",
file_unique_id="file_unique_id",
width=512,
height=512,
is_animated=False,
is_video=False,
type="regular",
)
star_count = 5
total_count = 10
remaining_count = 5
class TestGiftWithoutRequest(GiftTestBase):
def test_slot_behaviour(self, gift):
for attr in gift.__slots__:
assert getattr(gift, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(gift)) == len(set(mro_slots(gift))), "duplicate slot"
def test_de_json(self, offline_bot, gift):
json_dict = {
"id": self.id,
"sticker": self.sticker.to_dict(),
"star_count": self.star_count,
"total_count": self.total_count,
"remaining_count": self.remaining_count,
}
gift = Gift.de_json(json_dict, offline_bot)
assert gift.api_kwargs == {}
assert gift.id == self.id
assert gift.sticker == self.sticker
assert gift.star_count == self.star_count
assert gift.total_count == self.total_count
assert gift.remaining_count == self.remaining_count
assert Gift.de_json(None, offline_bot) is None
def test_to_dict(self, gift):
gift_dict = gift.to_dict()
assert isinstance(gift_dict, dict)
assert gift_dict["id"] == self.id
assert gift_dict["sticker"] == self.sticker.to_dict()
assert gift_dict["star_count"] == self.star_count
assert gift_dict["total_count"] == self.total_count
assert gift_dict["remaining_count"] == self.remaining_count
def test_equality(self, gift):
a = gift
b = Gift(self.id, self.sticker, self.star_count, self.total_count, self.remaining_count)
c = Gift(
"other_uid", self.sticker, self.star_count, self.total_count, self.remaining_count
)
d = BotCommand("start", "description")
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
@pytest.mark.parametrize(
"gift",
[
"gift_id",
Gift(
"gift_id",
Sticker("file_id", "file_unique_id", 512, 512, False, False, "regular"),
5,
10,
5,
),
],
ids=["string", "Gift"],
)
async def test_send_gift(self, offline_bot, gift, monkeypatch):
# We can't send actual gifts, so we just check that the correct parameters are passed
text_entities = [
MessageEntity(MessageEntity.TEXT_LINK, 0, 4, "url"),
MessageEntity(MessageEntity.BOLD, 5, 9),
]
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
user_id = request_data.parameters["user_id"] == "user_id"
gift_id = request_data.parameters["gift_id"] == "gift_id"
text = request_data.parameters["text"] == "text"
text_parse_mode = request_data.parameters["text_parse_mode"] == "text_parse_mode"
tes = request_data.parameters["text_entities"] == [
me.to_dict() for me in text_entities
]
return user_id and gift_id and text and text_parse_mode and tes
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.send_gift(
"user_id", gift, "text", text_parse_mode="text_parse_mode", text_entities=text_entities
)
@pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True)
@pytest.mark.parametrize(
("passed_value", "expected_value"),
[(DEFAULT_NONE, "Markdown"), ("HTML", "HTML"), (None, None)],
)
async def test_send_gift_default_parse_mode(
self, default_bot, monkeypatch, passed_value, expected_value
):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
return request_data.parameters.get("text_parse_mode") == expected_value
monkeypatch.setattr(default_bot.request, "post", make_assertion)
kwargs = {
"user_id": "user_id",
"gift_id": "gift_id",
}
if passed_value is not DEFAULT_NONE:
kwargs["text_parse_mode"] = passed_value
assert await default_bot.send_gift(**kwargs)
@pytest.fixture
def gifts(request):
return Gifts(gifts=GiftsTestBase.gifts)
class GiftsTestBase:
gifts: Sequence[Gift] = [
Gift(
id="id1",
sticker=Sticker(
file_id="file_id",
file_unique_id="file_unique_id",
width=512,
height=512,
is_animated=False,
is_video=False,
type="regular",
),
star_count=5,
total_count=5,
remaining_count=5,
),
Gift(
id="id2",
sticker=Sticker(
file_id="file_id",
file_unique_id="file_unique_id",
width=512,
height=512,
is_animated=False,
is_video=False,
type="regular",
),
star_count=6,
total_count=6,
remaining_count=6,
),
Gift(
id="id3",
sticker=Sticker(
file_id="file_id",
file_unique_id="file_unique_id",
width=512,
height=512,
is_animated=False,
is_video=False,
type="regular",
),
star_count=7,
total_count=7,
remaining_count=7,
),
]
class TestGiftsWithoutRequest(GiftsTestBase):
def test_slot_behaviour(self, gifts):
for attr in gifts.__slots__:
assert getattr(gifts, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(gifts)) == len(set(mro_slots(gifts))), "duplicate slot"
def test_de_json(self, offline_bot, gifts):
json_dict = {"gifts": [gift.to_dict() for gift in self.gifts]}
gifts = Gifts.de_json(json_dict, offline_bot)
assert gifts.api_kwargs == {}
assert gifts.gifts == tuple(self.gifts)
for de_json_gift, original_gift in zip(gifts.gifts, self.gifts):
assert de_json_gift.id == original_gift.id
assert de_json_gift.sticker == original_gift.sticker
assert de_json_gift.star_count == original_gift.star_count
assert de_json_gift.total_count == original_gift.total_count
assert de_json_gift.remaining_count == original_gift.remaining_count
assert Gifts.de_json(None, offline_bot) is None
def test_to_dict(self, gifts):
gifts_dict = gifts.to_dict()
assert isinstance(gifts_dict, dict)
assert gifts_dict["gifts"] == [gift.to_dict() for gift in self.gifts]
def test_equality(self, gifts):
a = gifts
b = Gifts(self.gifts)
c = Gifts(self.gifts[:2])
d = BotCommand("start", "description")
assert a == b
assert hash(a) == hash(b)
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
class TestGiftsWithRequest(GiftTestBase):
async def test_get_available_gifts(self, bot, chat_id):
# We don't control the available gifts, so we can not make any better assertions
assert isinstance(await bot.get_available_gifts(), Gifts)

View file

@ -24,7 +24,7 @@ import inspect
import logging
import re
from collections.abc import Sequence
from datetime import datetime
from datetime import datetime, timedelta
from types import FunctionType
from typing import Any
@ -67,6 +67,7 @@ DATETIME_REGEX = re.compile(
""",
re.VERBOSE,
)
TIMEDELTA_REGEX = re.compile(r"\w+_period$") # Parameter names ending with "_period"
log = logging.debug
@ -191,7 +192,18 @@ def check_param_type(
# If it's a class, we only accept datetime as the parameter
mapped_type = datetime if is_class else mapped_type | datetime
# 4) COMPLEX TYPES:
# 4) HANDLING TIMEDELTA:
elif re.search(TIMEDELTA_REGEX, ptb_param.name) and obj.__name__ in (
"TransactionPartnerUser",
"create_invoice_link",
):
# Currently we only support timedelta for `subscription_period` in `TransactionPartnerUser`
# and `create_invoice_link`.
# See https://github.com/python-telegram-bot/python-telegram-bot/issues/4575
log("Checking that `%s` is a timedelta!\n", ptb_param.name)
mapped_type = timedelta if is_class else mapped_type | timedelta
# 5) COMPLEX TYPES:
# Some types are too complicated, so we replace our annotation with a simpler type:
elif any(ptb_param.name in key for key in PTCE.COMPLEX_TYPES):
log("Converting `%s` to a simpler type!\n", ptb_param.name)
@ -199,7 +211,7 @@ def check_param_type(
if ptb_param.name == param_name and is_class is is_expected_class:
ptb_annotation = wrap_with_none(tg_parameter, exception_type, obj)
# 5) HANDLING DEFAULTS PARAMETERS:
# 6) HANDLING DEFAULTS PARAMETERS:
# Classes whose parameters are all ODVInput should be converted and checked.
elif obj.__name__ in PTCE.IGNORED_DEFAULTS_CLASSES:
log("Checking that `%s`'s param is ODVInput:\n", obj.__name__)

View file

@ -18,8 +18,7 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains exceptions to our API compared to the official API."""
from telegram import Animation, Audio, Document, PhotoSize, Sticker, Video, VideoNote, Voice
from telegram import Animation, Audio, Document, Gift, PhotoSize, Sticker, Video, VideoNote, Voice
from tests.test_official.helpers import _get_params_base
IGNORED_OBJECTS = ("ResponseParameters",)
@ -45,6 +44,7 @@ class ParamTypeCheckingExceptions:
"animation": Animation,
"voice": Voice,
"sticker": Sticker,
"gift_id": Gift,
}
# TODO: Look into merging this with COMPLEX_TYPES

View file

@ -24,6 +24,7 @@ import pytest
from telegram import (
Dice,
Gift,
PaidMediaPhoto,
PhotoSize,
RevenueWithdrawalState,
@ -32,6 +33,7 @@ from telegram import (
RevenueWithdrawalStateSucceeded,
StarTransaction,
StarTransactions,
Sticker,
TransactionPartner,
TransactionPartnerFragment,
TransactionPartnerOther,
@ -76,6 +78,22 @@ def transaction_partner_user():
)
],
paid_media_payload="payload",
subscription_period=datetime.timedelta(days=1),
gift=Gift(
id="some_id",
sticker=Sticker(
file_id="file_id",
file_unique_id="file_unique_id",
width=512,
height=512,
is_animated=False,
is_video=False,
type="regular",
),
star_count=5,
total_count=10,
remaining_count=5,
),
)
@ -515,6 +533,20 @@ class TestTransactionPartnerWithoutRequest(TransactionPartnerTestBase):
assert hash(c) != hash(f)
class TestTransactionPartnerUserWithoutRequest(TransactionPartnerTestBase):
def test_de_json_required(self, offline_bot):
json_dict = {
"user": transaction_partner_user().user.to_dict(),
}
tp = TransactionPartnerUser.de_json(json_dict, offline_bot)
assert tp.api_kwargs == {}
assert tp.user == transaction_partner_user().user
# This test is here mainly to check that the below cases work
assert tp.subscription_period is None
assert tp.gift is None
class RevenueWithdrawalStateTestBase:
date = datetime.datetime(2024, 1, 1, 0, 0, 0, 0, tzinfo=UTC)
url = "url"

View file

@ -720,3 +720,25 @@ class TestUserWithoutRequest(UserTestBase):
monkeypatch.setattr(user.get_bot(), "refund_star_payment", make_assertion)
assert await user.refund_star_payment(telegram_payment_charge_id=42)
async def test_instance_method_send_gift(self, monkeypatch, user):
async def make_assertion(*_, **kwargs):
return (
kwargs["user_id"] == user.id
and kwargs["gift_id"] == "gift_id"
and kwargs["text"] == "text"
and kwargs["text_parse_mode"] == "text_parse_mode"
and kwargs["text_entities"] == "text_entities"
)
assert check_shortcut_signature(user.send_gift, Bot.send_gift, ["user_id"], [])
assert await check_shortcut_call(user.send_gift, user.get_bot(), "send_gift")
assert await check_defaults_handling(user.send_gift, user.get_bot())
monkeypatch.setattr(user.get_bot(), "send_gift", make_assertion)
assert await user.send_gift(
gift_id="gift_id",
text="text",
text_parse_mode="text_parse_mode",
text_entities="text_entities",
)