Full Support for Bot API 10.0 (#5229)

Co-authored-by: Harshil <37377066+harshil21@users.noreply.github.com>
Co-authored-by: poolitzer <github@poolitzer.eu>
Co-authored-by: Phil Bazun <Phil9lne@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Abdelrahman Elkheir
2026-06-09 18:49:15 +03:00
committed by GitHub
parent ab2996713d
commit 0fb5678180
64 changed files with 5375 additions and 264 deletions
+2 -2
View File
@@ -87,14 +87,14 @@ jobs:
.test_report_optionals_junit.xml
- name: Submit coverage
uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
env_vars: OS,PYTHON
name: ${{ matrix.os }}-${{ matrix.python-version }}
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload test results to Codecov
uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
if: ${{ !cancelled() }}
with:
files: .test_report_no_optionals_junit.xml,.test_report_optionals_junit.xml
+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-9.6-blue?logo=telegram
.. image:: https://img.shields.io/badge/Bot%20API-10.0-blue?logo=telegram
:target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API version
@@ -77,7 +77,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 **9.6** are natively supported by this library.
All types and methods of the Telegram Bot API **10.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
@@ -1,4 +1,21 @@
features = "Full Support for Bot API 9.6"
features = """
Full Support for Bot API 9.6
.. warning::
- Bot API 9.6 replaces the field ``correct_option_id`` of ``Poll`` with the new field ``correct_option_ids``. The field ``correct_option_id`` is still present in PTB for backward compatibility, but it will be removed in future releases.
- Bot API 9.6 replaces the argument ``correct_option_id`` of ``Bot.send_poll`` with the new argument ``correct_option_ids``. The argument ``correct_option_id`` is still present in PTB for backward compatibility, but it will be removed in future releases.
- Bot API 9.6 introduces a now required argument ``persistent_id`` to ``PollOption``. For backward compatibility, the argument is currently still marked as optional in the signature and its presence is enforced through a runtime check. In future versions, this argument will be made required in the signature as well.
- Bot API 9.6 introduces a now required argument ``option_persistent_ids`` to ``PollAnswer``. For backward compatibility, the argument is currently still marked as optional in the signature and its presence is enforced through a runtime check. In future versions, this argument will be made required in the signature as well.
- Bot API 9.6 introduces a now required argument ``allows_revoting`` to ``Poll``. For backward compatibility, the argument is currently still marked as optional in the signature and its presence is enforced through a runtime check. In future versions, this argument will be made required in the signature as well.
Please make sure to update your code accordingly to avoid potential issues in the future. We recommend using keyword arguments to ensure compatibility with future updates.
"""
pull_requests = [
{ uid = "5196", author_uid = "harshil21" },
@@ -0,0 +1,33 @@
features = """
Full Support for Bot API 10.0
.. warning::
- Bot API 10.0 introduces a now required argument ``members_only`` to ``Poll``. For backward compatibility, the argument is currently still marked as optional in the signature and its presence is enforced through a runtime check. In future versions, this argument will be made required in the signature as well.
Please make sure to update your code accordingly to avoid potential issues in the future. We recommend using keyword arguments to ensure compatibility with future updates.
"""
deprecations = """
* Deprecated passing the ``filename`` parameter positionally to the classes:
* ``InputMediaAnimation``
* ``InputMediaAudio``
* ``InputMediaPhoto``
* ``InputMediaDocument``
* ``InputMediaVideo``
Please pass ``filename`` as a keyword argument instead, as this parameter will become keyword-only in the future.
* Deprecated ``InputPollOption.de_json``. The class ``InputPollOption`` is input only and its ``de_json`` method will be removed in future versions. The Bot API 10.0 ``media`` field of ``InputPollOption`` will not be included for deserialization.
"""
pull_requests = [
{ uid = "5229", author_uid = "aelkheir", closes_threads = ["5228"] },
{ uid = "5230", author_uid = "harshil21" },
{ uid = "5235", author_uid = "harshil21" },
{ uid = "5238", author_uid = "harshil21" },
{ uid = "5232", author_uid = "aelkheir" },
{ uid = "5232", author_uid = ["Poolitzer", "Phil9l", "harshil21", "aelkheir"] },
]
+1
View File
@@ -52,6 +52,7 @@ PRIVATE_BASE_CLASSES = {
"_BaseMedium": "TelegramObject",
"_CredentialsBase": "TelegramObject",
"_ChatBase": "TelegramObject",
"_BaseInputMedia": "TelegramObject",
}
+14
View File
@@ -41,6 +41,8 @@
- Used for sending paid media to channels
* - :meth:`~telegram.Bot.send_photo`
- Used for sending photos
* - :meth:`~telegram.Bot.send_live_photo`
- Used for sending live photos
* - :meth:`~telegram.Bot.send_poll`
- Used for sending polls
* - :meth:`~telegram.Bot.send_sticker`
@@ -80,6 +82,8 @@
- Used for answering the callback query
* - :meth:`~telegram.Bot.answer_inline_query`
- Used for answering the inline query
* - :meth:`~telegram.Bot.answer_guest_query`
- Used for replying to a received guest message
* - :meth:`~telegram.Bot.answer_pre_checkout_query`
- Used for answering a pre checkout query
* - :meth:`~telegram.Bot.answer_shipping_query`
@@ -104,6 +108,10 @@
- Used for stopping the running poll
* - :meth:`~telegram.Bot.set_message_reaction`
- Used for setting reactions on messages
* - :meth:`~telegram.Bot.delete_message_reaction`
- Used for deleting reactions on messages
* - :meth:`~telegram.Bot.delete_all_message_reactions`
- Used for deleting all reactions by a chat or user
.. raw:: html
@@ -167,6 +175,8 @@
- Used for unpinning a message
* - :meth:`~telegram.Bot.unpin_all_chat_messages`
- Used for unpinning all pinned chat messages
* - :meth:`~telegram.Bot.get_user_personal_chat_messages`
- Used for obtaining the personal chat messages of a user
* - :meth:`~telegram.Bot.get_user_profile_audios`
- Used for obtaining user's profile audios
* - :meth:`~telegram.Bot.get_user_profile_photos`
@@ -237,6 +247,10 @@
- Used for obtaining the menu button of a private chat or the default menu button
* - :meth:`~telegram.Bot.set_chat_menu_button`
- Used for setting the menu button of a private chat or the default menu button
* - :meth:`~telegram.Bot.set_managed_bot_access_settings`
- Used for changing the access settings of a managed bot
* - :meth:`~telegram.Bot.get_managed_bot_access_settings`
- Used for obtaining the access settings of a managed bot
* - :meth:`~telegram.Bot.set_my_description`
- Used for setting the description of the bot
* - :meth:`~telegram.Bot.get_my_description`
+12
View File
@@ -8,6 +8,7 @@ Available Types
telegram.animation
telegram.audio
telegram.birthdate
telegram.botaccesssettings
telegram.botcommand
telegram.botcommandscope
telegram.botcommandscopeallchatadministrators
@@ -101,15 +102,22 @@ Available Types
telegram.inputmediaanimation
telegram.inputmediaaudio
telegram.inputmediadocument
telegram.inputmedialivephoto
telegram.inputmedialocation
telegram.inputmediaphoto
telegram.inputmediasticker
telegram.inputmediavenue
telegram.inputmediavideo
telegram.inputpaidmedia
telegram.inputpaidmedialivephoto
telegram.inputpaidmediaphoto
telegram.inputpaidmediavideo
telegram.inputpollmedia
telegram.inputprofilephoto
telegram.inputprofilephotoanimated
telegram.inputprofilephotostatic
telegram.inputpolloption
telegram.inputpolloptionmedia
telegram.inputstorycontent
telegram.inputstorycontentphoto
telegram.inputstorycontentvideo
@@ -119,6 +127,7 @@ Available Types
telegram.keyboardbuttonrequestmanagedbot
telegram.keyboardbuttonrequestusers
telegram.linkpreviewoptions
telegram.livephoto
telegram.location
telegram.locationaddress
telegram.loginurl
@@ -146,6 +155,7 @@ Available Types
telegram.ownedgiftunique
telegram.paidmedia
telegram.paidmediainfo
telegram.paidmedialivephoto
telegram.paidmediaphoto
telegram.paidmediapreview
telegram.paidmediapurchased
@@ -154,6 +164,7 @@ Available Types
telegram.photosize
telegram.poll
telegram.pollanswer
telegram.pollmedia
telegram.polloptionadded
telegram.polloptiondeleted
telegram.preparedkeyboardbutton
@@ -166,6 +177,7 @@ Available Types
telegram.replykeyboardmarkup
telegram.replykeyboardremove
telegram.replyparameters
telegram.sentguestmessage
telegram.sentwebappmessage
telegram.shareduser
telegram.story
@@ -0,0 +1,6 @@
BotAccessSettings
=================
.. autoclass:: telegram.BotAccessSettings
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
InputMediaLivePhoto
===================
.. autoclass:: telegram.InputMediaLivePhoto
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
InputMediaLocation
==================
.. autoclass:: telegram.InputMediaLocation
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
InputMediaSticker
=================
.. autoclass:: telegram.InputMediaSticker
:members:
:show-inheritance:
+6
View File
@@ -0,0 +1,6 @@
InputMediaVenue
===============
.. autoclass:: telegram.InputMediaVenue
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
InputPaidMediaLivePhoto
======================
.. autoclass:: telegram.InputPaidMediaLivePhoto
:members:
:show-inheritance:
+6
View File
@@ -0,0 +1,6 @@
InputPollMedia
==============
.. versionadded:: NEXT.VERSION
.. autoclass:: telegram.InputPollMedia
@@ -0,0 +1,6 @@
InputPollOptionMedia
====================
.. versionadded:: NEXT.VERSION
.. autoclass:: telegram.InputPollOptionMedia
+6
View File
@@ -0,0 +1,6 @@
LivePhoto
=========
.. autoclass:: telegram.LivePhoto
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
PaidMediaLivePhoto
==================
.. autoclass:: telegram.PaidMediaLivePhoto
:members:
:show-inheritance:
+6
View File
@@ -0,0 +1,6 @@
PollMedia
=========
.. autoclass:: telegram.PollMedia
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
SentGuestMessage
================
.. autoclass:: telegram.SentGuestMessage
:members:
:show-inheritance:
+24
View File
@@ -35,6 +35,7 @@ __all__ = (
"BackgroundTypeWallpaper",
"Birthdate",
"Bot",
"BotAccessSettings",
"BotCommand",
"BotCommandScope",
"BotCommandScopeAllChatAdministrators",
@@ -157,13 +158,20 @@ __all__ = (
"InputMediaAnimation",
"InputMediaAudio",
"InputMediaDocument",
"InputMediaLivePhoto",
"InputMediaLocation",
"InputMediaPhoto",
"InputMediaSticker",
"InputMediaVenue",
"InputMediaVideo",
"InputMessageContent",
"InputPaidMedia",
"InputPaidMediaLivePhoto",
"InputPaidMediaPhoto",
"InputPaidMediaVideo",
"InputPollMedia",
"InputPollOption",
"InputPollOptionMedia",
"InputProfilePhoto",
"InputProfilePhotoAnimated",
"InputProfilePhotoStatic",
@@ -181,6 +189,7 @@ __all__ = (
"KeyboardButtonRequestUsers",
"LabeledPrice",
"LinkPreviewOptions",
"LivePhoto",
"Location",
"LocationAddress",
"LoginUrl",
@@ -210,6 +219,7 @@ __all__ = (
"OwnedGifts",
"PaidMedia",
"PaidMediaInfo",
"PaidMediaLivePhoto",
"PaidMediaPhoto",
"PaidMediaPreview",
"PaidMediaPurchased",
@@ -231,6 +241,7 @@ __all__ = (
"PhotoSize",
"Poll",
"PollAnswer",
"PollMedia",
"PollOption",
"PollOptionAdded",
"PollOptionDeleted",
@@ -254,6 +265,7 @@ __all__ = (
"RevenueWithdrawalStateSucceeded",
"SecureData",
"SecureValue",
"SentGuestMessage",
"SentWebAppMessage",
"SharedUser",
"ShippingAddress",
@@ -348,6 +360,7 @@ from telegram._payment.stars.transactionpartner import (
from . import _version, constants, error, helpers, request, warnings
from ._birthdate import Birthdate
from ._bot import Bot
from ._botaccesssettings import BotAccessSettings
from ._botcommand import BotCommand
from ._botcommandscope import (
BotCommandScope,
@@ -435,11 +448,18 @@ from ._files.inputmedia import (
InputMediaAnimation,
InputMediaAudio,
InputMediaDocument,
InputMediaLivePhoto,
InputMediaLocation,
InputMediaPhoto,
InputMediaSticker,
InputMediaVenue,
InputMediaVideo,
InputPaidMedia,
InputPaidMediaLivePhoto,
InputPaidMediaPhoto,
InputPaidMediaVideo,
InputPollMedia,
InputPollOptionMedia,
)
from ._files.inputprofilephoto import (
InputProfilePhoto,
@@ -447,6 +467,7 @@ from ._files.inputprofilephoto import (
InputProfilePhotoStatic,
)
from ._files.inputsticker import InputSticker
from ._files.livephoto import LivePhoto
from ._files.location import Location
from ._files.photosize import PhotoSize
from ._files.sticker import MaskPosition, Sticker, StickerSet
@@ -529,6 +550,7 @@ from ._ownedgift import OwnedGift, OwnedGiftRegular, OwnedGifts, OwnedGiftUnique
from ._paidmedia import (
PaidMedia,
PaidMediaInfo,
PaidMediaLivePhoto,
PaidMediaPhoto,
PaidMediaPreview,
PaidMediaPurchased,
@@ -579,6 +601,7 @@ from ._poll import (
InputPollOption,
Poll,
PollAnswer,
PollMedia,
PollOption,
PollOptionAdded,
PollOptionDeleted,
@@ -595,6 +618,7 @@ from ._reaction import (
from ._reply import ExternalReplyInfo, ReplyParameters, TextQuote
from ._replykeyboardmarkup import ReplyKeyboardMarkup
from ._replykeyboardremove import ReplyKeyboardRemove
from ._sentguestmessage import SentGuestMessage
from ._sentwebappmessage import SentWebAppMessage
from ._shared import ChatShared, SharedUser, UsersShared
from ._story import Story
+518 -51
View File
@@ -47,7 +47,8 @@ except ImportError:
serialization = None # type: ignore[assignment]
CRYPTO_INSTALLED = False
from telegram._botcommand import BotCommand # pylint: disable=ungrouped-imports
from telegram._botaccesssettings import BotAccessSettings # pylint: disable=ungrouped-imports
from telegram._botcommand import BotCommand
from telegram._botcommandscope import BotCommandScope
from telegram._botdescription import BotDescription, BotShortDescription
from telegram._botname import BotName
@@ -65,6 +66,7 @@ from telegram._files.contact import Contact
from telegram._files.document import Document
from telegram._files.file import File
from telegram._files.inputmedia import InputMedia, InputPaidMedia
from telegram._files.livephoto import LivePhoto
from telegram._files.location import Location
from telegram._files.photosize import PhotoSize
from telegram._files.sticker import MaskPosition, Sticker, StickerSet
@@ -89,6 +91,7 @@ from telegram._poll import InputPollOption, Poll
from telegram._preparedkeyboardbutton import PreparedKeyboardButton
from telegram._reaction import ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji
from telegram._reply import ReplyParameters
from telegram._sentguestmessage import SentGuestMessage
from telegram._sentwebappmessage import SentWebAppMessage
from telegram._story import Story
from telegram._telegramobject import TelegramObject
@@ -126,8 +129,10 @@ if TYPE_CHECKING:
InputFile,
InputMediaAudio,
InputMediaDocument,
InputMediaLivePhoto,
InputMediaPhoto,
InputMediaVideo,
InputPollMedia,
InputProfilePhoto,
InputSticker,
InputStoryContent,
@@ -1209,7 +1214,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
self,
chat_id: int,
draft_id: int,
text: str,
text: str | None = None,
message_thread_id: int | None = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
entities: Sequence["MessageEntity"] | None = None,
@@ -1221,7 +1226,9 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
api_kwargs: JSONDict | None = None,
) -> bool:
"""Use this method to stream a partial message to a user while the message is being
generated.
generated. Note that the streamed draft is ephemeral and acts as a temporary 30-second
preview - once the output is finalized, you must call :meth:`~Bot.send_message` with
the complete message to persist it in the user's chat.
.. versionadded:: 22.6
@@ -1233,19 +1240,21 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
chat_id (:obj:`int`): Unique identifier for the target private chat.
draft_id (:obj:`int`): Unique identifier of the message draft; must be non-zero.
Changes of drafts with the same identifier are animated.
text (:obj:`str`): Text of the message to be sent,
:tg-const:`telegram.constants.MessageLimit.MIN_TEXT_LENGTH`-
:tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters after
entities parsing.
text (:obj:`str`, optional): Text of the message to be sent,
0-:tg-const:`telegram.constants.MessageLimit.MAX_TEXT_LENGTH` characters after
entities parsing. Pass an empty text to show a "Thinking..." placeholder.
.. versionchanged:: NEXT.VERSION
Bot API 10.0 now makes this an optional parameter.
message_thread_id (:obj:`int`, optional): Unique identifier for the target
message thread.
parse_mode (:obj:`str`): |parse_mode|
entities (Sequence[:class:`telegram.MessageEntity`], optional): Sequence of special
entities that appear in message text, which can be specified instead of
:paramref:`parse_mode`.
|sequenceargs|
message_thread_id (:obj:`int`, optional): Unique identifier for the target
message thread.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
@@ -2839,7 +2848,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
self,
chat_id: int | str,
media: Sequence[
"InputMediaAudio | InputMediaDocument | InputMediaPhoto | InputMediaVideo"
"InputMediaAudio | InputMediaDocument | InputMediaPhoto | InputMediaVideo | InputMediaLivePhoto" # noqa: E501 # pylint: disable=line-too-long
],
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
@@ -2878,8 +2887,8 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
media (Sequence[:class:`telegram.InputMediaAudio`,\
:class:`telegram.InputMediaDocument`, :class:`telegram.InputMediaPhoto`,\
:class:`telegram.InputMediaVideo`]): An array
describing messages to be sent, must include
:class:`telegram.InputMediaVideo`, :class:`telegram.InputMediaLivePhoto`]): An
array describing messages to be sent, must include
:tg-const:`telegram.constants.MediaGroupLimit.MIN_MEDIA_LENGTH`-
:tg-const:`telegram.constants.MediaGroupLimit.MAX_MEDIA_LENGTH` items.
@@ -3677,7 +3686,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
async def send_game(
self,
chat_id: int,
chat_id: int | str,
game_short_name: str,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
reply_markup: "InlineKeyboardMarkup | None" = None,
@@ -3699,7 +3708,9 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
"""Use this method to send a game.
Args:
chat_id (:obj:`int`): Unique identifier for the target chat.
chat_id (:obj:`int`): Unique identifier for the target chat or username of the
target bot in the format ``@username``. Games can't be sent to channel direct
messages chats and channel chats.
game_short_name (:obj:`str`): Short name of the game, serves as the unique identifier
for the game. Set up your games via `@BotFather <https://t.me/BotFather>`_.
disable_notification (:obj:`bool`, optional): |disable_notification|
@@ -4658,12 +4669,12 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
api_kwargs: JSONDict | None = None,
) -> "Message | bool":
"""
Use this method to edit animation, audio, document, photo, or video messages, or to add
media to text messages. If a message
Use this method to edit animation, audio, document, live photo, photo, or video messages,
or to add media to text messages. If a message
is part of a message album, then it can be edited only to an audio for audio albums, only
to a document for document albums and to a photo or a video otherwise. When an inline
message is edited, a new file can't be uploaded; use a previously uploaded file via its
:attr:`~telegram.File.file_id` or specify a URL.
to a document for document albums and to a photo, live photo, or a video otherwise.
When an inline message is edited, a new file can't be uploaded; use a previously
uploaded file via its :attr:`~telegram.File.file_id` or specify a URL.
Note:
* |editreplymarkup|
@@ -5125,6 +5136,7 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
async def get_chat_administrators(
self,
chat_id: str | int,
return_bots: bool | None = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -5140,18 +5152,21 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
return_bots (:obj:`bool`, optional): Pass :obj:`True` to additionally receive all bots
that are administrators of the chat. By default, bots other than the current bot
are omitted.
.. versionadded:: NEXT.VERSION
Returns:
tuple[:class:`telegram.ChatMember`]: On success, returns a tuple of ``ChatMember``
objects that contains information about all chat administrators except
other bots. If the chat is a group or a supergroup and no administrators were
appointed, only the creator will be returned.
objects that contains information about all chat administrators.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {"chat_id": chat_id}
data: JSONDict = {"chat_id": chat_id, "return_bots": return_bots}
result = await self._post(
"getChatAdministrators",
data,
@@ -5848,6 +5863,51 @@ class Bot(TelegramObject, contextlib.AbstractAsyncContextManager["Bot"]):
return SentWebAppMessage.de_json(api_result, self)
async def answer_guest_query(
self,
guest_query_id: str,
result: "InlineQueryResult",
*,
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: JSONDict | None = None,
) -> SentGuestMessage:
"""Use this method to reply to a received guest message.
.. versionadded:: NEXT.VERSION
Args:
guest_query_id (:obj:`str`): Unique identifier for the query to be answered.
result (:class:`telegram.InlineQueryResult`): An object describing the message to be
sent.
Returns:
:class:`telegram.SentGuestMessage`: On success, a
:class:`telegram.SentGuestMessage` is returned.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {
"guest_query_id": guest_query_id,
"result": self._insert_defaults_for_ilq_results(result),
}
api_result = await self._post(
"answerGuestQuery",
data,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
return SentGuestMessage.de_json(api_result, self)
async def restrict_chat_member(
self,
chat_id: str | int,
@@ -7624,9 +7684,13 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
hide_results_until_closes: bool | None = None,
correct_option_ids: CorrectOptionIds | None = None,
description: str | None = None,
description_parse_mode: str | None = None,
description_parse_mode: ODVInput[str] | None = None,
description_entities: Sequence["MessageEntity"] | None = None,
shuffle_options: bool | None = None,
members_only: bool | None = None,
country_codes: Sequence[str] | None = None,
explanation_media: "InputPollMedia | None" = None,
media: "InputPollMedia | None" = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: int | None = None,
@@ -7769,6 +7833,27 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
shuffle_options (:obj:`bool`, optional): :obj:`True`, if the poll options must be
shown in random order
.. versionadded:: NEXT.VERSION
members_only (:obj:`bool`, optional): :obj:`True`, if voting is limited to users who
have been members of the chat where the poll is being sent for more than
:tg-const:`telegram.Poll.MIN_MEMBERSHIP_HOURS` hours; for channel chats only
.. versionadded:: NEXT.VERSION
country_codes (Sequence[:obj:`str`], optional): A list of
0-:tg-const:`telegram.constants.PollLimit.MAX_COUNTRY_CODES` two-letter
``ISO 3166-1 alpha-2`` country codes indicating the countries from which users can
vote in the poll; for channel chats only. Use ``"FT"`` as a country code to allow
users with anonymous numbers to vote. If omitted or empty, then users from any
country can participate in the poll.
.. versionadded:: NEXT.VERSION
explanation_media (:class:`telegram.InputPollMedia`, optional): Media added to the quiz
explanation
.. versionadded:: NEXT.VERSION
media (:class:`telegram.InputPollMedia`, optional): Media added to the poll
description.
.. versionadded:: NEXT.VERSION
Keyword Args:
@@ -7837,6 +7922,10 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
"close_date": close_date,
"question_parse_mode": question_parse_mode,
"question_entities": question_entities,
"members_only": members_only,
"country_codes": country_codes,
"explanation_media": explanation_media,
"media": media,
}
return await self._send_message(
@@ -7912,7 +8001,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
async def send_checklist(
self,
business_connection_id: str,
chat_id: int,
chat_id: int | str,
checklist: InputChecklist,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
@@ -7934,20 +8023,14 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
.. versionadded:: 22.3
Args:
business_connection_id (:obj:`str`):
|business_id_str|
chat_id (:obj:`int`):
Unique identifier for the target chat.
checklist (:class:`telegram.InputChecklist`):
The checklist to send.
disable_notification (:obj:`bool`, optional):
|disable_notification|
protect_content (:obj:`bool`, optional):
|protect_content|
message_effect_id (:obj:`str`, optional):
|message_effect_id|
reply_parameters (:class:`telegram.ReplyParameters`, optional):
|reply_parameters|
business_connection_id (:obj:`str`): |business_id_str|
chat_id (:obj:`int`): Unique identifier for the target chat or username of the target
bot in the format ``@username``.
checklist (:class:`telegram.InputChecklist`): The checklist to send.
disable_notification (:obj:`bool`, optional): |disable_notification|
protect_content (:obj:`bool`, optional): |protect_content|
message_effect_id (:obj:`str`, optional): |message_effect_id|
reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters|
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional):
An object for an inline keyboard
@@ -7992,7 +8075,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
async def edit_message_checklist(
self,
business_connection_id: str,
chat_id: int,
chat_id: int | str,
message_id: int,
checklist: InputChecklist,
reply_markup: "InlineKeyboardMarkup | None" = None,
@@ -8009,14 +8092,11 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
.. versionadded:: 22.3
Args:
business_connection_id (:obj:`str`):
|business_id_str|
chat_id (:obj:`int`):
Unique identifier for the target chat.
message_id (:obj:`int`):
Unique identifier for the target message.
checklist (:class:`telegram.InputChecklist`):
The new checklist.
business_connection_id (:obj:`str`): |business_id_str|
chat_id (:obj:`int`): Unique identifier for the target chat or username of the target
bot in the format ``@username``.
message_id (:obj:`int`): Unique identifier for the target message.
checklist (:class:`telegram.InputChecklist`): The new checklist.
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional):
An object for the new inline keyboard for the message.
@@ -8745,7 +8825,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Args:
chat_id (:obj:`int`, optional): Unique identifier for the target private chat. If not
specified, default bot's menu button will be changed
specified, the bot's default menu button will be changed
menu_button (:class:`telegram.MenuButton`, optional): An object for the new bot's menu
button. Defaults to :class:`telegram.MenuButtonDefault`.
@@ -8785,7 +8865,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
Args:
chat_id (:obj:`int`, optional): Unique identifier for the target private chat. If not
specified, default bot's menu button will be returned.
specified, the bot's default menu button will be returned.
Returns:
:class:`telegram.MenuButton`: On success, the current menu button is returned.
@@ -11123,6 +11203,145 @@ CHAT_ACTIVITY_TIMEOUT` seconds.
api_kwargs=api_kwargs,
)
async def get_managed_bot_access_settings(
self,
user_id: int,
*,
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: JSONDict | None = None,
) -> BotAccessSettings:
"""
Use this method to get the access settings of a managed bot.
.. versionadded:: NEXT.VERSION
Args:
user_id (:obj:`int`): User identifier of the managed bot whose access settings will be
returned.
Returns:
:class:`telegram.BotAccessSettings`: The access settings of the managed bot.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {
"user_id": user_id,
}
return BotAccessSettings.de_json(
await self._post(
"getManagedBotAccessSettings",
data,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
),
self,
)
async def set_managed_bot_access_settings(
self,
user_id: int,
is_access_restricted: bool,
added_user_ids: Sequence[int] | None = 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: JSONDict | None = None,
) -> bool:
"""
Use this method to change the access settings of a managed bot.
.. versionadded:: NEXT.VERSION
Args:
user_id (:obj:`int`): User identifier of the managed bot whose access settings will be
changed.
is_access_restricted (:obj:`bool`): Pass :obj:`True`, if only selected users can access
the bot. The bot's owner can always access it.
added_user_ids (Sequence[:obj:`int`], optional): A list of up to
:tg-const:`telegram.constants.ManagedBotAccessLimit.MAX_ALLOWED_USERS`
identifiers of users who will have access to the bot in addition to its owner.
Ignored if :paramref:`is_access_restricted` is :obj:`False`.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {
"user_id": user_id,
"is_access_restricted": is_access_restricted,
"added_user_ids": added_user_ids,
}
return await self._post(
"setManagedBotAccessSettings",
data,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def get_user_personal_chat_messages(
self,
user_id: int,
limit: int,
*,
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: JSONDict | None = None,
) -> tuple[Message, ...]:
"""
Use this method to get the last messages from the personal chat (i.e., the chat currently
added to their profile) of a given user.
.. versionadded:: NEXT.VERSION
Args:
user_id (:obj:`int`): Unique identifier of the target user.
limit (:obj:`int`): The maximum number of messages to return;
:tg-const:`telegram.constants.PersonalChatMessagesLimit.MIN_LIMIT`-
:tg-const:`telegram.constants.PersonalChatMessagesLimit.MAX_LIMIT`.
Returns:
tuple[:class:`telegram.Message`, ...]: On success, a tuple of
:class:`telegram.Message` objects is returned.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {"user_id": user_id, "limit": limit}
return Message.de_list(
await self._post(
"getUserPersonalChatMessages",
data,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
),
self,
)
async def send_paid_media(
self,
chat_id: str | int,
@@ -12283,6 +12502,240 @@ CHAT_ACTIVITY_TIMEOUT` seconds.
self,
)
async def send_live_photo(
self,
chat_id: int | str,
live_photo: "FileInput | LivePhoto",
photo: "FileInput | PhotoSize",
business_connection_id: str | None = None,
message_thread_id: int | None = None,
direct_messages_topic_id: int | None = None,
caption: str | None = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
caption_entities: Sequence["MessageEntity"] | None = None,
show_caption_above_media: bool | None = None,
has_spoiler: bool | None = None,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
allow_paid_broadcast: bool | None = None,
message_effect_id: str | None = None,
suggested_post_parameters: "SuggestedPostParameters | None" = None,
reply_parameters: "ReplyParameters | None" = None,
reply_markup: "ReplyMarkup | None" = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: int | None = None,
filename: str | None = 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: JSONDict | None = None,
) -> Message:
"""
Use this method to send live photos.
.. seealso:: :wiki:`Working with Files and Media <Working-with-Files-and-Media>`
.. versionadded:: NEXT.VERSION
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
live_photo (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | \
:obj:`bytes` | :class:`pathlib.Path` | :class:`telegram.LivePhoto`): Live photo
video to send. Pass a ``file_id`` to send a file that exists on the Telegram
servers (recommended). |uploadinputnopath| Sending live photos by a URL is
currently unsupported. Lastly you can pass an existing
:class:`telegram.LivePhoto` object to send.
Caution:
* The video must be at most 10MB in size.
* The video duration must not exceed 10 seconds.
* If you pass a :class:`telegram.LivePhoto`, its
:attr:`~telegram.LivePhoto.photo` field will not be considered, use
:paramref:`photo` to specify the photo to send.
photo (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
| :class:`pathlib.Path` | :class:`telegram.PhotoSize`): The static photo to send.
Pass a ``file_id`` to send a file that exists on the Telegram servers (recommended)
. |uploadinputnopath| Sending live photos by a URL is currently unsupported.
Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
business_connection_id (:obj:`str`, optional): |business_id_str|
message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
direct_messages_topic_id (:obj:`int`, optional): |direct_messages_topic_id|
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.
parse_mode (:obj:`str`, optional): |parse_mode|
caption_entities (Sequence[:class:`telegram.MessageEntity`], optional):
|caption_entities|
show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med|
has_spoiler (:obj:`bool`, optional): Pass :obj:`True` if the video needs to be covered
with a spoiler animation.
disable_notification (:obj:`bool`, optional): |disable_notification|
protect_content (:obj:`bool`, optional): |protect_content|
allow_paid_broadcast (:obj:`bool`, optional): |allow_paid_broadcast|
message_effect_id (:obj:`str`, optional): |message_effect_id|
suggested_post_parameters (:class:`telegram.SuggestedPostParameters`, optional):
|suggested_post_parameters|
reply_parameters (:class:`telegram.ReplyParameters`, optional): |reply_parameters|
reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \
:class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional):
Additional interface options. An object for an inline keyboard, custom reply
keyboard, instructions to remove reply keyboard or to force a reply from the user.
Keyword Args:
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience
parameter for
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
Mutually exclusive with :paramref:`reply_parameters`, which this is a convenience
parameter for.
filename (:obj:`str`, optional): Custom file name for :paramref:`photo`, when
uploading a new file. Convenience parameter, useful e.g. when sending files
generated by the :obj:`tempfile` module.
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {
"chat_id": chat_id,
"live_photo": self._parse_file_input(live_photo, LivePhoto),
"photo": self._parse_file_input(photo, PhotoSize, filename=filename),
"has_spoiler": has_spoiler,
"show_caption_above_media": show_caption_above_media,
}
return await self._send_message(
"sendLivePhoto",
data,
disable_notification=disable_notification,
reply_markup=reply_markup,
protect_content=protect_content,
message_thread_id=message_thread_id,
caption=caption,
parse_mode=parse_mode,
caption_entities=caption_entities,
reply_parameters=reply_parameters,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
message_effect_id=message_effect_id,
allow_paid_broadcast=allow_paid_broadcast,
direct_messages_topic_id=direct_messages_topic_id,
suggested_post_parameters=suggested_post_parameters,
allow_sending_without_reply=allow_sending_without_reply,
reply_to_message_id=reply_to_message_id,
)
async def delete_message_reaction(
self,
chat_id: int | str,
message_id: int,
user_id: int | None = None,
actor_chat_id: int | None = 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: JSONDict | None = None,
) -> bool:
"""
Use this method to remove a reaction from a message in a group or a supergroup chat.
The bot must have the :attr:`~telegram.ChatMemberAdministrator.can_delete_messages`
administrator right in the chat.
.. versionadded:: NEXT.VERSION
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_group|
message_id (:obj:`int`): Identifier of the target message.
user_id (:obj:`int`, optional): Identifier of the user whose reaction will be removed,
if the reaction were added by a user.
actor_chat_id (:obj:`int`, optional): Identifier of the chat whose reaction will be
removed, if the reaction were added by a chat.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {
"chat_id": chat_id,
"message_id": message_id,
"user_id": user_id,
"actor_chat_id": actor_chat_id,
}
return await self._post(
"deleteMessageReaction",
data,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def delete_all_message_reactions(
self,
chat_id: int | str,
user_id: int | None = None,
actor_chat_id: int | None = 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: JSONDict | None = None,
) -> bool:
"""
Use this method to remove up to ``10000`` recent reactions in a group or a supergroup chat
added by a given user or chat. The bot must have the
:attr:`~telegram.ChatMemberAdministrator.can_delete_messages` administrator right in the
chat.
.. versionadded:: NEXT.VERSION
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_group|
user_id (:obj:`int`, optional): Identifier of the user whose reactions will be removed,
if the reactions were added by a user.
actor_chat_id (:obj:`int`, optional): Identifier of the chat whose reactions will be
removed, if the reactions were added by a chat.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
Raises:
:class:`telegram.error.TelegramError`
"""
data: JSONDict = {
"chat_id": chat_id,
"user_id": user_id,
"actor_chat_id": actor_chat_id,
}
return await self._post(
"deleteAllMessageReactions",
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}
@@ -12399,6 +12852,8 @@ CHAT_ACTIVITY_TIMEOUT` seconds.
"""Alias for :meth:`answer_pre_checkout_query`"""
answerWebAppQuery = answer_web_app_query
"""Alias for :meth:`answer_web_app_query`"""
answerGuestQuery = answer_guest_query
"""Alias for :meth:`answer_guest_query`"""
restrictChatMember = restrict_chat_member
"""Alias for :meth:`restrict_chat_member`"""
promoteChatMember = promote_chat_member
@@ -12629,3 +13084,15 @@ CHAT_ACTIVITY_TIMEOUT` seconds.
"""Alias for :meth:`replace_managed_bot_token`"""
savePreparedKeyboardButton = save_prepared_keyboard_button
"""Alias for :meth:`save_prepared_keyboard_button`"""
sendLivePhoto = send_live_photo
"""Alias for :meth:`send_live_photo`"""
getManagedBotAccessSettings = get_managed_bot_access_settings
"""Alias for :meth:`get_managed_bot_access_settings`"""
setManagedBotAccessSettings = set_managed_bot_access_settings
"""Alias for :meth:`set_managed_bot_access_settings`"""
getUserPersonalChatMessages = get_user_personal_chat_messages
"""Alias for :meth:`get_user_personal_chat_messages`"""
deleteMessageReaction = delete_message_reaction
"""Alias for :meth:`delete_message_reaction`"""
deleteAllMessageReactions = delete_all_message_reactions
"""Alias for :meth:`delete_all_message_reactions`"""
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2026
# 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 Bot Access Settings."""
from collections.abc import Sequence
from typing import TYPE_CHECKING
from telegram._telegramobject import TelegramObject
from telegram._user import User
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 BotAccessSettings(TelegramObject):
"""
This object describes the access settings of a bot.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`is_access_restricted` and :attr:`added_users` are equal.
.. versionadded:: NEXT.VERSION
Args:
is_access_restricted (:obj:`bool`): :obj:`True`, if only selected users can access the bot.
The bot's owner can always access it.
added_users (Sequence[:class:`telegram.User`], optional): The list of other users who
have access to the bot if the access is restricted.
Attributes:
is_access_restricted (:obj:`bool`): :obj:`True`, if only selected users can access the bot.
The bot's owner can always access it.
added_users (Sequence[:class:`telegram.User`]): Optional. The list of other users who
have access to the bot if the access is restricted.
"""
__slots__ = ("added_users", "is_access_restricted")
def __init__(
self,
is_access_restricted: bool,
added_users: Sequence[User] | None = None,
*,
api_kwargs: JSONDict | None = None,
):
super().__init__(api_kwargs=api_kwargs)
self.is_access_restricted: bool = is_access_restricted
self.added_users: tuple[User, ...] = parse_sequence_arg(added_users)
self._id_attrs = (self.is_access_restricted, self.added_users)
self._freeze()
@classmethod
def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "BotAccessSettings":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
data["added_users"] = de_list_optional(data.get("added_users"), User, bot)
return super().de_json(data=data, bot=bot)
+165 -3
View File
@@ -58,12 +58,15 @@ if TYPE_CHECKING:
InputChecklist,
InputMediaAudio,
InputMediaDocument,
InputMediaLivePhoto,
InputMediaPhoto,
InputMediaVideo,
InputPaidMedia,
InputPollMedia,
InputPollOption,
LabeledPrice,
LinkPreviewOptions,
LivePhoto,
Location,
Message,
MessageEntity,
@@ -311,6 +314,7 @@ class _ChatBase(TelegramObject):
async def get_administrators(
self,
return_bots: bool | None = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -334,6 +338,7 @@ class _ChatBase(TelegramObject):
"""
return await self.get_bot().get_chat_administrators(
chat_id=self.id,
return_bots=return_bots,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1088,7 +1093,7 @@ class _ChatBase(TelegramObject):
async def send_message_draft(
self,
draft_id: int,
text: str,
text: str | None = None,
message_thread_id: int | None = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
entities: Sequence["MessageEntity"] | None = None,
@@ -1105,6 +1110,9 @@ class _ChatBase(TelegramObject):
For the documentation of the arguments, please see :meth:`telegram.Bot.send_message_draft`.
.. versionchanged:: NEXT.VERSION
Bot API 10.0 makes the ``text`` argument optional.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
@@ -1190,7 +1198,7 @@ class _ChatBase(TelegramObject):
async def send_media_group(
self,
media: Sequence[
"InputMediaAudio | InputMediaDocument | InputMediaPhoto | InputMediaVideo"
"InputMediaAudio | InputMediaDocument | InputMediaPhoto | InputMediaVideo | InputMediaLivePhoto" # noqa: E501 # pylint: disable=line-too-long
],
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
@@ -1349,6 +1357,76 @@ class _ChatBase(TelegramObject):
suggested_post_parameters=suggested_post_parameters,
)
async def send_live_photo(
self,
live_photo: "FileInput | LivePhoto",
photo: "FileInput | PhotoSize",
business_connection_id: str | None = None,
message_thread_id: int | None = None,
direct_messages_topic_id: int | None = None,
caption: str | None = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
caption_entities: Sequence["MessageEntity"] | None = None,
show_caption_above_media: bool | None = None,
has_spoiler: bool | None = None,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
allow_paid_broadcast: bool | None = None,
message_effect_id: str | None = None,
suggested_post_parameters: "SuggestedPostParameters | None" = None,
reply_parameters: "ReplyParameters | None" = None,
reply_markup: "ReplyMarkup | None" = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: int | None = None,
filename: str | None = 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: JSONDict | None = None,
) -> "Message":
"""Shortcut for::
await bot.send_live_photo(update.effective_chat.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_live_photo`.
.. versionadded:: NEXT.VERSION
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
return await self.get_bot().send_live_photo(
chat_id=self.id,
live_photo=live_photo,
photo=photo,
business_connection_id=business_connection_id,
message_thread_id=message_thread_id,
direct_messages_topic_id=direct_messages_topic_id,
caption=caption,
parse_mode=parse_mode,
caption_entities=caption_entities,
show_caption_above_media=show_caption_above_media,
has_spoiler=has_spoiler,
disable_notification=disable_notification,
protect_content=protect_content,
allow_paid_broadcast=allow_paid_broadcast,
message_effect_id=message_effect_id,
suggested_post_parameters=suggested_post_parameters,
reply_parameters=reply_parameters,
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
reply_to_message_id=reply_to_message_id,
filename=filename,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def send_contact(
self,
phone_number: str | None = None,
@@ -2303,8 +2381,12 @@ class _ChatBase(TelegramObject):
allow_adding_options: bool | None = None,
hide_results_until_closes: bool | None = None,
description: str | None = None,
description_parse_mode: str | None = None,
description_parse_mode: ODVInput[str] | None = None,
description_entities: Sequence["MessageEntity"] | None = None,
members_only: bool | None = None,
country_codes: Sequence[str] | None = None,
explanation_media: "InputPollMedia | None" = None,
media: "InputPollMedia | None" = None,
*,
reply_to_message_id: int | None = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2363,6 +2445,10 @@ class _ChatBase(TelegramObject):
description_entities=description_entities,
hide_results_until_closes=hide_results_until_closes,
allow_adding_options=allow_adding_options,
members_only=members_only,
country_codes=country_codes,
explanation_media=explanation_media,
media=media,
)
async def send_copy(
@@ -4049,6 +4135,82 @@ class _ChatBase(TelegramObject):
api_kwargs=api_kwargs,
)
async def delete_reaction(
self,
message_id: int,
user_id: int | None = None,
actor_chat_id: int | None = 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: JSONDict | None = None,
) -> bool:
"""
Shortcut for::
await bot.delete_message_reaction(chat_id=update.effective_chat.id, *args, **kwargs)
For the documentation of the arguments, please see
:meth:`telegram.Bot.delete_message_reaction`.
.. versionadded:: NEXT.VERSION
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return await self.get_bot().delete_message_reaction(
chat_id=self.id,
message_id=message_id,
user_id=user_id,
actor_chat_id=actor_chat_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def delete_all_reactions(
self,
user_id: int | None = None,
actor_chat_id: int | None = 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: JSONDict | None = None,
) -> bool:
"""
Shortcut for::
await bot.delete_all_message_reactions(
chat_id=update.effective_chat.id,
*args,
**kwargs
)
For the documentation of the arguments, please see
:meth:`telegram.Bot.delete_all_message_reactions`.
.. versionadded:: NEXT.VERSION
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return await self.get_bot().delete_all_message_reactions(
chat_id=self.id,
user_id=user_id,
actor_chat_id=actor_chat_id,
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.
+1 -1
View File
@@ -245,7 +245,7 @@ class ChatFullInfo(_ChatBase):
.. versionadded:: 22.6
paid_message_star_count (:obj:`int`, optional): The number of Telegram Stars a general user
have to pay to send a message to the chat
has to pay to send a message to the chat
.. versionadded:: 22.6
first_profile_audio (:obj:`telegram.Audio`, optional): For private chats, the first audio
+19
View File
@@ -515,8 +515,13 @@ class ChatMemberRestricted(ChatMember):
.. versionadded:: 20.1
can_edit_tag (:obj:`bool`): :obj:`True`, if the user is allowed to edit their own tag.
If omitted, defaults to the value of :attr:`can_pin_messages`.
.. versionadded:: 22.7
can_react_to_messages (:obj:`bool`): :obj:`True`, if the user is allowed to react to
messages.
.. versionadded:: NEXT.VERSION
tag (:obj:`str`, optional): Tag of the member.
.. versionadded:: 22.7
@@ -571,8 +576,13 @@ class ChatMemberRestricted(ChatMember):
.. versionadded:: 20.1
can_edit_tag (:obj:`bool`): :obj:`True`, if the user is allowed to edit their own tag.
If omitted, defaults to the value of :attr:`can_pin_messages`.
.. versionadded:: 22.7
can_react_to_messages (:obj:`bool`): :obj:`True`, if the user is allowed to react to
messages.
.. versionadded:: NEXT.VERSION
tag (:obj:`str`): Optional. Tag of the member.
.. versionadded:: 22.7
@@ -586,6 +596,7 @@ class ChatMemberRestricted(ChatMember):
"can_invite_users",
"can_manage_topics",
"can_pin_messages",
"can_react_to_messages",
"can_send_audios",
"can_send_documents",
"can_send_messages",
@@ -621,10 +632,17 @@ class ChatMemberRestricted(ChatMember):
can_send_voice_notes: bool,
can_edit_tag: bool,
tag: str | None = None,
# tags: NEXT.VERSION
# temporarily optional to make it not breaking
can_react_to_messages: bool | None = None,
*,
api_kwargs: JSONDict | None = None,
):
super().__init__(status=ChatMember.RESTRICTED, user=user, api_kwargs=api_kwargs)
if can_react_to_messages is None:
raise TypeError("`can_react_to_messages` is required and cannot be None")
with self._unfrozen():
self.is_member: bool = is_member
self.can_change_info: bool = can_change_info
@@ -643,6 +661,7 @@ class ChatMemberRestricted(ChatMember):
self.can_send_video_notes: bool = can_send_video_notes
self.can_send_voice_notes: bool = can_send_voice_notes
self.can_edit_tag: bool = can_edit_tag
self.can_react_to_messages: bool = can_react_to_messages
self.tag: str | None = tag
+2 -2
View File
@@ -79,11 +79,11 @@ class ChatOwnerLeft(TelegramObject):
.. versionadded:: 22.7
Args:
new_owner (:class:`telegram.User`, optional): The user which will be the new owner of the
new_owner (:class:`telegram.User`, optional): The user who will become the new owner of the
chat if the previous owner does not return to the chat
Attributes:
new_owner (:class:`telegram.User`): Optional. The user which will be the new owner of the
new_owner (:class:`telegram.User`): Optional. The user who will become the new owner of the
chat if the previous owner does not return to the chat
"""
+16 -1
View File
@@ -36,7 +36,7 @@ class ChatPermissions(TelegramObject):
:attr:`can_change_info`, :attr:`can_invite_users`, :attr:`can_pin_messages`,
:attr:`can_send_audios`, :attr:`can_send_documents`, :attr:`can_send_photos`,
:attr:`can_send_videos`, :attr:`can_send_video_notes`, :attr:`can_send_voice_notes`,
:attr:`can_manage_topics` and :attr:`can_edit_tag` are equal.
:attr:`can_manage_topics`, :attr:`can_edit_tag`, and :attr:`can_react_to_messages` are equal.
.. versionchanged:: 20.0
:attr:`can_manage_topics` is considered as well when comparing objects of
@@ -50,6 +50,9 @@ class ChatPermissions(TelegramObject):
.. versionchanged:: 22.7
:attr:`can_edit_tag` is considered as well when comparing objects of
this type in terms of equality.
.. versionchanged:: NEXT.VERSION
:attr:`can_react_to_messages` is considered as well when comparing objects of
this type in terms of equality.
Note:
@@ -100,6 +103,10 @@ class ChatPermissions(TelegramObject):
tag.
.. versionadded:: 22.7
can_react_to_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to react
to messages. If omitted, defaults to the value of :attr:`can_send_messages`.
.. versionadded:: NEXT.VERSION
Attributes:
can_send_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send text
@@ -145,6 +152,10 @@ class ChatPermissions(TelegramObject):
tag.
.. versionadded:: 22.7
can_react_to_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to react
to messages. If omitted, defaults to the value of :attr:`can_send_messages`.
.. versionadded:: NEXT.VERSION
"""
@@ -155,6 +166,7 @@ class ChatPermissions(TelegramObject):
"can_invite_users",
"can_manage_topics",
"can_pin_messages",
"can_react_to_messages",
"can_send_audios",
"can_send_documents",
"can_send_messages",
@@ -183,6 +195,7 @@ class ChatPermissions(TelegramObject):
can_send_video_notes: bool | None = None,
can_send_voice_notes: bool | None = None,
can_edit_tag: bool | None = None,
can_react_to_messages: bool | None = None,
*,
api_kwargs: JSONDict | None = None,
):
@@ -203,6 +216,7 @@ class ChatPermissions(TelegramObject):
self.can_send_video_notes: bool | None = can_send_video_notes
self.can_send_voice_notes: bool | None = can_send_voice_notes
self.can_edit_tag: bool | None = can_edit_tag
self.can_react_to_messages: bool | None = can_react_to_messages
self._id_attrs = (
self.can_send_messages,
@@ -220,6 +234,7 @@ class ChatPermissions(TelegramObject):
self.can_send_video_notes,
self.can_send_voice_notes,
self.can_edit_tag,
self.can_react_to_messages,
)
self._freeze()
+528 -55
View File
@@ -16,7 +16,8 @@
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""Base class for Telegram InputMedia Objects."""
"""Base classes for Telegram InputMedia, InputPaidMedia, InputPollMedia
and InputPollOptionMedia Objects."""
import datetime as dtm
from collections.abc import Sequence
@@ -28,6 +29,7 @@ from telegram._files.audio import Audio
from telegram._files.document import Document
from telegram._files.inputfile import InputFile
from telegram._files.photosize import PhotoSize
from telegram._files.sticker import Sticker
from telegram._files.video import Video
from telegram._messageentity import MessageEntity
from telegram._telegramobject import TelegramObject
@@ -37,17 +39,47 @@ from telegram._utils.datetime import get_timedelta_value
from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram._utils.files import parse_file_input
from telegram._utils.types import JSONDict, ODVInput, TimePeriod
from telegram.constants import InputMediaType
from telegram._utils.warnings import warn
from telegram.constants import BaseInputMediaType
from telegram.warnings import PTBDeprecationWarning
if TYPE_CHECKING:
from telegram._utils.types import FileInput
MediaType: TypeAlias = Animation | Audio | Document | PhotoSize | Video
class InputMedia(TelegramObject):
class _BaseInputMedia(TelegramObject):
"""
Base class for Telegram InputMedia Objects.
Base class for objects representing the various input media types.
Args:
media_type (:obj:`str`): Type of media that the instance represents.
Attributes:
type (:obj:`str`): Type of media that the instance represents.
"""
__slots__ = ("type",)
def __init__(
self,
media_type: str,
*,
api_kwargs: JSONDict | None = None,
):
super().__init__(api_kwargs=api_kwargs)
self.type: str = enum.get_member(constants.BaseInputMediaType, media_type, media_type)
class InputMedia(_BaseInputMedia):
"""
This object represents the content of a media message to be sent. It should be one of:
* :class:`telegram.InputMediaAnimation`
* :class:`telegram.InputMediaAudio`
* :class:`telegram.InputMediaDocument`
* :class:`telegram.InputMediaLivePhoto`
* :class:`telegram.InputMediaPhoto`
* :class:`telegram.InputMediaVideo`
.. versionchanged:: 20.0
Added arguments and attributes :attr:`type`, :attr:`media`, :attr:`caption`,
@@ -85,7 +117,7 @@ class InputMedia(TelegramObject):
"""
__slots__ = ("caption", "caption_entities", "media", "parse_mode", "type")
__slots__ = ("caption", "caption_entities", "media", "parse_mode")
def __init__(
self,
@@ -97,14 +129,12 @@ class InputMedia(TelegramObject):
*,
api_kwargs: JSONDict | None = None,
):
super().__init__(api_kwargs=api_kwargs)
self.type: str = enum.get_member(constants.InputMediaType, media_type, media_type)
self.media: str | InputFile = media
self.caption: str | None = caption
self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
self.parse_mode: ODVInput[str] = parse_mode
self._freeze()
super().__init__(media_type=media_type, api_kwargs=api_kwargs)
with self._unfrozen():
self.media: str | InputFile = media
self.caption: str | None = caption
self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
self.parse_mode: ODVInput[str] = parse_mode
@staticmethod
def _parse_thumbnail_input(thumbnail: "FileInput | None") -> str | InputFile | None:
@@ -123,6 +153,7 @@ class InputPaidMedia(TelegramObject):
* :class:`telegram.InputPaidMediaPhoto`
* :class:`telegram.InputPaidMediaVideo`
* :class:`telegram.InputPaidMediaLivePhoto`
.. seealso:: :wiki:`Working with Files and Media <Working-with-Files-and-Media>`
@@ -142,6 +173,11 @@ class InputPaidMedia(TelegramObject):
""":const:`telegram.constants.InputPaidMediaType.PHOTO`"""
VIDEO: Final[str] = constants.InputPaidMediaType.VIDEO
""":const:`telegram.constants.InputPaidMediaType.VIDEO`"""
LIVE_PHOTO: Final[str] = constants.InputPaidMediaType.LIVE_PHOTO
""":const:`telegram.constants.InputPaidMediaType.LIVE_PHOTO`
.. versionadded:: NEXT.VERSION
"""
__slots__ = ("media", "type")
@@ -300,6 +336,51 @@ class InputPaidMediaVideo(InputPaidMedia):
return get_timedelta_value(self._duration, attribute="duration")
class InputPaidMediaLivePhoto(InputPaidMedia):
"""
The paid media to send is a live photo.
.. seealso:: :wiki:`Working with Files and Media <Working-with-Files-and-Media>`
.. versionadded:: NEXT.VERSION
Args:
media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
| :class:`pathlib.Path` | :class:`~telegram.Video`): Video of the live photo to send.
Pass a ``file_id`` to send a file that exists on the Telegram servers (recommended).
|uploadinputnopath| Sending live photos by a URL is currently unsupported. Lastly you
can pass an existing :class:`telegram.Video` object to send.
photo (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`pathlib.Path` | :class:`~telegram.PhotoSize`): Photo of the live photo to send.
Pass a ``file_id`` to send a file that exists on the Telegram servers (recommended).
|uploadinputnopath| Sending live photos by a URL is currently unsupported.
Lastly you can pass an existing :class:`telegram.PhotoSize` object to send.
Attributes:
type (:obj:`str`): Type of the media, always
:tg-const:`telegram.constants.InputPaidMediaType.LIVE_PHOTO`.
media (:obj:`str` | :class:`telegram.InputFile`): Video of the live photo to send.
|fileinputnopath|
photo (:obj:`str` | :class:`telegram.InputFile`): Photo of the live photo to send.
|fileinputnopath|
"""
__slots__ = ("photo",)
def __init__(
self,
media: "FileInput | Video",
photo: "FileInput | PhotoSize",
*,
api_kwargs: JSONDict | None = None,
):
media = parse_file_input(media, tg_type=Video, attach=True, local_mode=True)
photo = parse_file_input(photo, tg_type=PhotoSize, attach=True, local_mode=True)
super().__init__(type=InputPaidMedia.LIVE_PHOTO, media=media, api_kwargs=api_kwargs)
with self._unfrozen():
self.photo: str | InputFile = photo
class InputMediaAnimation(InputMedia):
"""Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent.
@@ -320,11 +401,12 @@ class InputMediaAnimation(InputMedia):
.. versionchanged:: 13.2
Accept :obj:`bytes` as input.
filename (:obj:`str`, optional): Custom file name for the animation, when uploading a
new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module.
filename_depr (:obj:`str`, optional): Positional placeholder for keyword only parameter
:paramref:`filename`. For backward compatibility.
.. versionadded:: 13.1
.. versionadded:: NEXT.VERSION
.. deprecated:: NEXT.VERSION
This parameter is deprecated, use :paramref:`filename` instead.
caption (:obj:`str`, optional): Caption of the animation to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
after entities parsing.
@@ -353,8 +435,17 @@ class InputMediaAnimation(InputMedia):
.. versionadded:: 21.3
Keyword Args:
filename (:obj:`str`, optional): Custom file name for the animation, when uploading a
new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module.
.. versionadded:: 13.1
.. versionchanged:: NEXT.VERSION
This parameter is now keyword-only.
Attributes:
type (:obj:`str`): :tg-const:`telegram.constants.InputMediaType.ANIMATION`.
type (:obj:`str`): :tg-const:`telegram.constants.BaseInputMediaType.ANIMATION`.
media (:obj:`str` | :class:`telegram.InputFile`): Animation to send.
caption (:obj:`str`): Optional. Caption of the animation to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
@@ -403,13 +494,28 @@ class InputMediaAnimation(InputMedia):
height: int | None = None,
duration: TimePeriod | None = None,
caption_entities: Sequence[MessageEntity] | None = None,
filename: str | None = None,
# tag: deprecated NEXT.VERSION
filename_depr: str | None = None,
# -
has_spoiler: bool | None = None,
thumbnail: "FileInput | None" = None,
show_caption_above_media: bool | None = None,
*,
filename: str | None = None,
api_kwargs: JSONDict | None = None,
):
if filename_depr is not None and filename is not None:
raise ValueError("`filename_depr` and `filename` are mutually exclusive.")
if filename_depr is not None:
warn(
PTBDeprecationWarning(
"NEXT.VERSION",
"Positional passing of `filename` or keyword usage of `filename_depr`"
" is deprecated. `filename` will become a keyword-only argument.",
),
stacklevel=2,
)
if isinstance(media, Animation):
width = media.width if width is None else width
height = media.height if height is None else height
@@ -418,10 +524,13 @@ class InputMediaAnimation(InputMedia):
else:
# We use local_mode=True because we don't have access to the actual setting and want
# things to work in local mode.
media = parse_file_input(media, filename=filename, attach=True, local_mode=True)
effective_filename = filename_depr or filename
media = parse_file_input(
media, filename=effective_filename, attach=True, local_mode=True
)
super().__init__(
InputMediaType.ANIMATION,
BaseInputMediaType.ANIMATION,
media,
caption,
caption_entities,
@@ -453,11 +562,12 @@ class InputMediaPhoto(InputMedia):
.. versionchanged:: 13.2
Accept :obj:`bytes` as input.
filename (:obj:`str`, optional): Custom file name for the photo, when uploading a
new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module.
filename_depr (:obj:`str`, optional): Positional placeholder for keyword only parameter
:paramref:`filename`. For backward compatibility.
.. versionadded:: 13.1
.. versionadded:: NEXT.VERSION
.. deprecated:: NEXT.VERSION
This parameter is deprecated, use :paramref:`filename` instead.
caption (:obj:`str`, optional ): Caption of the photo to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
entities parsing.
@@ -474,8 +584,17 @@ class InputMediaPhoto(InputMedia):
.. versionadded:: 21.3
Keyword Args:
filename (:obj:`str`, optional): Custom file name for the photo, when uploading a
new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module.
.. versionadded:: 13.1
.. versionchanged:: NEXT.VERSION
This parameter is now keyword-only.
Attributes:
type (:obj:`str`): :tg-const:`telegram.constants.InputMediaType.PHOTO`.
type (:obj:`str`): :tg-const:`telegram.constants.BaseInputMediaType.PHOTO`.
media (:obj:`str` | :class:`telegram.InputFile`): Photo to send.
caption (:obj:`str`): Optional. Caption of the photo to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
@@ -507,17 +626,35 @@ class InputMediaPhoto(InputMedia):
caption: str | None = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
caption_entities: Sequence[MessageEntity] | None = None,
filename: str | None = None,
# tag: deprecated NEXT.VERSION
filename_depr: str | None = None,
# -
has_spoiler: bool | None = None,
show_caption_above_media: bool | None = None,
*,
filename: str | None = None,
api_kwargs: JSONDict | None = None,
):
if filename_depr is not None and filename is not None:
raise ValueError("`filename_depr` and `filename` are mutually exclusive.")
if filename_depr is not None:
warn(
PTBDeprecationWarning(
"NEXT.VERSION",
"Positional passing of `filename` or keyword usage of `filename_depr`"
" is deprecated. `filename` will become a keyword-only argument.",
),
stacklevel=2,
)
# We use local_mode=True because we don't have access to the actual setting and want
# things to work in local mode.
media = parse_file_input(media, PhotoSize, filename=filename, attach=True, local_mode=True)
effective_filename = filename_depr or filename
media = parse_file_input(
media, PhotoSize, filename=effective_filename, attach=True, local_mode=True
)
super().__init__(
InputMediaType.PHOTO,
BaseInputMediaType.PHOTO,
media,
caption,
caption_entities,
@@ -553,11 +690,12 @@ class InputMediaVideo(InputMedia):
.. versionchanged:: 13.2
Accept :obj:`bytes` as input.
filename (:obj:`str`, optional): Custom file name for the video, when uploading a
new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module.
filename_depr (:obj:`str`, optional): Positional placeholder for keyword only parameter
:paramref:`filename`. For backward compatibility.
.. versionadded:: 13.1
.. versionadded:: NEXT.VERSION
.. deprecated:: NEXT.VERSION
This parameter is deprecated, use :paramref:`filename` instead.
caption (:obj:`str`, optional): Caption of the video to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
entities parsing.
@@ -594,8 +732,17 @@ class InputMediaVideo(InputMedia):
.. versionadded:: 21.3
Keyword Args:
filename (:obj:`str`, optional): Custom file name for the video, when uploading a
new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module.
.. versionadded:: 13.1
.. versionchanged:: NEXT.VERSION
This parameter is now keyword-only.
Attributes:
type (:obj:`str`): :tg-const:`telegram.constants.InputMediaType.VIDEO`.
type (:obj:`str`): :tg-const:`telegram.constants.BaseInputMediaType.VIDEO`.
media (:obj:`str` | :class:`telegram.InputFile`): Video file to send.
caption (:obj:`str`): Optional. Caption of the video to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
@@ -656,15 +803,30 @@ class InputMediaVideo(InputMedia):
supports_streaming: bool | None = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
caption_entities: Sequence[MessageEntity] | None = None,
filename: str | None = None,
# tag: deprecated NEXT.VERSION
filename_depr: str | None = None,
# -
has_spoiler: bool | None = None,
thumbnail: "FileInput | None" = None,
show_caption_above_media: bool | None = None,
cover: "FileInput | None" = None,
start_timestamp: int | None = None,
*,
filename: str | None = None,
api_kwargs: JSONDict | None = None,
):
if filename_depr is not None and filename is not None:
raise ValueError("`filename_depr` and `filename` are mutually exclusive.")
if filename_depr is not None:
warn(
PTBDeprecationWarning(
"NEXT.VERSION",
"Positional passing of `filename` or keyword usage of `filename_depr`"
" is deprecated. `filename` will become a keyword-only argument.",
),
stacklevel=2,
)
if isinstance(media, Video):
width = width if width is not None else media.width
height = height if height is not None else media.height
@@ -673,10 +835,13 @@ class InputMediaVideo(InputMedia):
else:
# We use local_mode=True because we don't have access to the actual setting and want
# things to work in local mode.
media = parse_file_input(media, filename=filename, attach=True, local_mode=True)
effective_filename = filename_depr or filename
media = parse_file_input(
media, filename=effective_filename, attach=True, local_mode=True
)
super().__init__(
InputMediaType.VIDEO,
BaseInputMediaType.VIDEO,
media,
caption,
caption_entities,
@@ -701,6 +866,156 @@ class InputMediaVideo(InputMedia):
return get_timedelta_value(self._duration, attribute="duration")
class InputMediaLocation(_BaseInputMedia):
"""Represents a location to be sent.
.. versionadded:: NEXT.VERSION
Args:
latitude (:obj:`float`): Latitude of the location.
longitude (:obj:`float`): Longitude of the location.
horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location,
measured in meters; 0-:tg-const:`telegram.Location.HORIZONTAL_ACCURACY`.
Attributes:
type (:obj:`str`): :tg-const:`telegram.constants.BaseInputMediaType.LOCATION`.
latitude (:obj:`float`): Latitude of the location.
longitude (:obj:`float`): Longitude of the location.
horizontal_accuracy (:obj:`float`): Optional. The radius of uncertainty for the location,
measured in meters; 0-:tg-const:`telegram.Location.HORIZONTAL_ACCURACY`.
"""
__slots__ = ("horizontal_accuracy", "latitude", "longitude")
def __init__(
self,
latitude: float,
longitude: float,
horizontal_accuracy: float | None = None,
*,
api_kwargs: JSONDict | None = None,
):
super().__init__(media_type=BaseInputMediaType.LOCATION, api_kwargs=api_kwargs)
with self._unfrozen():
self.latitude: float = latitude
self.longitude: float = longitude
self.horizontal_accuracy: float | None = horizontal_accuracy
class InputMediaVenue(_BaseInputMedia):
"""Represents a venue to be sent.
.. versionadded:: NEXT.VERSION
Args:
latitude (:obj:`float`): Latitude of the location.
longitude (:obj:`float`): Longitude of the location.
title (:obj:`str`): Name of the venue.
address (:obj:`str`): Address of the venue.
foursquare_id (:obj:`str`, optional): Foursquare identifier of the venue.
foursquare_type (:obj:`str`, optional): Foursquare type of the venue, if known. (For
example, ``arts_entertainment/default``, ``arts_entertainment/aquarium``
or ``food/icecream``).
google_place_id (:obj:`str`, optional): Google Places identifier of the venue.
google_place_type (:obj:`str`, optional): Google Places type of the venue. (See\
`supported types <https://developers.google.com/places/web-service/supported_types>`__)
Attributes:
type (:obj:`str`): :tg-const:`telegram.constants.BaseInputMediaType.VENUE`.
latitude (:obj:`float`): Latitude of the location.
longitude (:obj:`float`): Longitude of the location.
title (:obj:`str`): Name of the venue.
address (:obj:`str`): Address of the venue.
foursquare_id (:obj:`str`): Optional. Foursquare identifier of the venue.
foursquare_type (:obj:`str`): Optional. Foursquare type of the venue, if known. (For
example, ``arts_entertainment/default``, ``arts_entertainment/aquarium``
or ``food/icecream``).
google_place_id (:obj:`str`): Optional. Google Places identifier of the venue.
google_place_type (:obj:`str`): Optional. Google Places type of the venue. (See\
`supported types <https://developers.google.com/places/web-service/supported_types>`__)
"""
__slots__ = (
"address",
"foursquare_id",
"foursquare_type",
"google_place_id",
"google_place_type",
"latitude",
"longitude",
"title",
)
def __init__(
self,
latitude: float,
longitude: float,
title: str,
address: str,
foursquare_id: str | None = None,
foursquare_type: str | None = None,
google_place_id: str | None = None,
google_place_type: str | None = None,
*,
api_kwargs: JSONDict | None = None,
):
super().__init__(media_type=BaseInputMediaType.VENUE, api_kwargs=api_kwargs)
with self._unfrozen():
self.latitude: float = latitude
self.longitude: float = longitude
self.title: str = title
self.address: str = address
self.foursquare_id: str | None = foursquare_id
self.foursquare_type: str | None = foursquare_type
self.google_place_id: str | None = google_place_id
self.google_place_type: str | None = google_place_type
class InputMediaSticker(_BaseInputMedia):
"""Represents a sticker file to be sent.
.. seealso:: :wiki:`Working with Files and Media <Working-with-Files-and-Media>`
.. versionadded:: NEXT.VERSION
Args:
media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` | \
:class:`pathlib.Path` | :class:`telegram.Sticker`): File to send. |fileinputnopath|
Lastly you can pass an existing :class:`telegram.Sticker` object to send.
emoji (:obj:`str`, optional): Emoji associated with the sticker; only for just uploaded
stickers.
Keyword Args:
filename (:obj:`str`, optional): Custom file name for the sticker, when uploading a
new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module.
Attributes:
type (:obj:`str`): :tg-const:`telegram.constants.BaseInputMediaType.STICKER`.
media (:obj:`str` | :class:`telegram.InputFile`): Sticker file to send.
emoji (:obj:`str`): Optional. Emoji associated with the sticker; only for just uploaded
stickers.
"""
__slots__ = ("emoji", "media")
def __init__(
self,
media: "FileInput | Sticker",
emoji: str | None = None,
*,
filename: str | None = None,
api_kwargs: JSONDict | None = None,
):
media = parse_file_input(media, Sticker, filename=filename, attach=True, local_mode=True)
super().__init__(media_type=BaseInputMediaType.STICKER, api_kwargs=api_kwargs)
with self._unfrozen():
self.media: str | InputFile = media
self.emoji: str | None = emoji
class InputMediaAudio(InputMedia):
"""Represents an audio file to be treated as music to be sent.
@@ -721,11 +1036,12 @@ class InputMediaAudio(InputMedia):
.. versionchanged:: 13.2
Accept :obj:`bytes` as input.
filename (:obj:`str`, optional): Custom file name for the audio, when uploading a
new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module.
filename_depr (:obj:`str`, optional): Positional placeholder for keyword only parameter
:paramref:`filename`. For backward compatibility.
.. versionadded:: 13.1
.. versionadded:: NEXT.VERSION
.. deprecated:: NEXT.VERSION
This parameter is deprecated, use :paramref:`filename` instead.
caption (:obj:`str`, optional): Caption of the audio to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
entities parsing.
@@ -748,8 +1064,17 @@ class InputMediaAudio(InputMedia):
.. versionadded:: 20.2
Keyword Args:
filename (:obj:`str`, optional): Custom file name for the audio, when uploading a
new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module.
.. versionadded:: 13.1
.. versionchanged:: NEXT.VERSION
This parameter is now keyword-only.
Attributes:
type (:obj:`str`): :tg-const:`telegram.constants.InputMediaType.AUDIO`.
type (:obj:`str`): :tg-const:`telegram.constants.BaseInputMediaType.AUDIO`.
media (:obj:`str` | :class:`telegram.InputFile`): Audio file to send.
caption (:obj:`str`): Optional. Caption of the audio to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
@@ -786,11 +1111,26 @@ class InputMediaAudio(InputMedia):
performer: str | None = None,
title: str | None = None,
caption_entities: Sequence[MessageEntity] | None = None,
filename: str | None = None,
# tag: deprecated NEXT.VERSION
filename_depr: str | None = None,
# -
thumbnail: "FileInput | None" = None,
*,
filename: str | None = None,
api_kwargs: JSONDict | None = None,
):
if filename_depr is not None and filename is not None:
raise ValueError("`filename_depr` and `filename` are mutually exclusive.")
if filename_depr is not None:
warn(
PTBDeprecationWarning(
"NEXT.VERSION",
"Positional passing of `filename` or keyword usage of `filename_depr`"
" is deprecated. `filename` will become a keyword-only argument.",
),
stacklevel=2,
)
if isinstance(media, Audio):
duration = duration if duration is not None else media._duration
performer = media.performer if performer is None else performer
@@ -799,10 +1139,13 @@ class InputMediaAudio(InputMedia):
else:
# We use local_mode=True because we don't have access to the actual setting and want
# things to work in local mode.
media = parse_file_input(media, filename=filename, attach=True, local_mode=True)
effective_filename = filename_depr or filename
media = parse_file_input(
media, filename=effective_filename, attach=True, local_mode=True
)
super().__init__(
InputMediaType.AUDIO,
BaseInputMediaType.AUDIO,
media,
caption,
caption_entities,
@@ -835,11 +1178,12 @@ class InputMediaDocument(InputMedia):
.. versionchanged:: 13.2
Accept :obj:`bytes` as input.
filename (:obj:`str`, optional): Custom file name for the document, when uploading a
new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module.
filename_depr (:obj:`str`, optional): Positional placeholder for keyword only parameter
:paramref:`filename`. For backward compatibility.
.. versionadded:: 13.1
.. versionadded:: NEXT.VERSION
.. deprecated:: NEXT.VERSION
This parameter is deprecated, use :paramref:`filename` instead.
caption (:obj:`str`, optional): Caption of the document to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
entities parsing.
@@ -857,8 +1201,17 @@ class InputMediaDocument(InputMedia):
.. versionadded:: 20.2
Keyword Args:
filename (:obj:`str`, optional): Custom file name for the document, when uploading a
new file. Convenience parameter, useful e.g. when sending files generated by the
:obj:`tempfile` module.
.. versionadded:: 13.1
.. versionchanged:: NEXT.VERSION
This parameter is now keyword-only.
Attributes:
type (:obj:`str`): :tg-const:`telegram.constants.InputMediaType.DOCUMENT`.
type (:obj:`str`): :tg-const:`telegram.constants.BaseInputMediaType.DOCUMENT`.
media (:obj:`str` | :class:`telegram.InputFile`): File to send.
caption (:obj:`str`): Optional. Caption of the document to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
@@ -887,17 +1240,35 @@ class InputMediaDocument(InputMedia):
parse_mode: ODVInput[str] = DEFAULT_NONE,
disable_content_type_detection: bool | None = None,
caption_entities: Sequence[MessageEntity] | None = None,
filename: str | None = None,
# tag: deprecated NEXT.VERSION
filename_depr: str | None = None,
# -
thumbnail: "FileInput | None" = None,
*,
filename: str | None = None,
api_kwargs: JSONDict | None = None,
):
if filename_depr is not None and filename is not None:
raise ValueError("`filename_depr` and `filename` are mutually exclusive.")
if filename_depr is not None:
warn(
PTBDeprecationWarning(
"NEXT.VERSION",
"Positional passing of `filename` or keyword usage of `filename_depr`"
" is deprecated. `filename` will become a keyword-only argument.",
),
stacklevel=2,
)
# We use local_mode=True because we don't have access to the actual setting and want
# things to work in local mode.
media = parse_file_input(media, Document, filename=filename, attach=True, local_mode=True)
effective_filename = filename_depr or filename
media = parse_file_input(
media, Document, filename=effective_filename, attach=True, local_mode=True
)
super().__init__(
InputMediaType.DOCUMENT,
BaseInputMediaType.DOCUMENT,
media,
caption,
caption_entities,
@@ -907,3 +1278,105 @@ class InputMediaDocument(InputMedia):
with self._unfrozen():
self.thumbnail: str | InputFile | None = self._parse_thumbnail_input(thumbnail)
self.disable_content_type_detection: bool | None = disable_content_type_detection
class InputMediaLivePhoto(InputMedia):
"""Represents a live photo to be sent.
.. seealso:: :wiki:`Working with Files and Media <Working-with-Files-and-Media>`
.. versionadded:: NEXT.VERSION
Args:
media (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
| :class:`pathlib.Path` | :class:`~telegram.Video`): Video of the live photo to send.
Pass a ``file_id`` to send a file that exists on the Telegram servers (recommended).
|uploadinputnopath| Sending live photos by a URL is currently unsupported. Lastly
you can pass an existing :class:`telegram.Video` object to send.
photo (:obj:`str` | :term:`file object` | :class:`~telegram.InputFile` | :obj:`bytes` \
| :class:`pathlib.Path` | :class:`~telegram.PhotoSize`): The static photo to send.
Pass a ``file_id`` to send a file that exists on the Telegram servers (recommended).
|uploadinputnopath| Sending live photos by a URL is currently unsupported. Lastly
you can pass an existing :class:`telegram.PhotoSize` object to send.
caption (:obj:`str`, optional): Caption of the live photo to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after
entities parsing.
parse_mode (:obj:`str`, optional): |parse_mode|
caption_entities (Sequence[:class:`telegram.MessageEntity`], optional): |caption_entities|
show_caption_above_media (:obj:`bool`, optional): Pass |show_cap_above_med|
has_spoiler (:obj:`bool`, optional): Pass :obj:`True`, if the video needs to be covered
with a spoiler animation.
Attributes:
type (:obj:`str`): :tg-const:`telegram.constants.BaseInputMediaType.LIVE_PHOTO`.
media (:obj:`str` | :class:`telegram.InputFile`): Video of the live photo to send.
photo (:obj:`str` | :class:`telegram.InputFile`): The static photo to send.
caption (:obj:`str`): Optional. Caption of the live photo to be sent,
0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters
after entities parsing.
parse_mode (:obj:`str`): Optional. |parse_mode|
caption_entities (tuple[:class:`telegram.MessageEntity`]): Optional. |captionentitiesattr|
show_caption_above_media (:obj:`bool`): Optional. |show_cap_above_med|
has_spoiler (:obj:`bool`): Optional. :obj:`True`, if the video is covered with a
spoiler animation.
"""
__slots__ = ("has_spoiler", "photo", "show_caption_above_media")
def __init__(
self,
media: "FileInput | Video",
photo: "FileInput | PhotoSize",
caption: str | None = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
caption_entities: Sequence[MessageEntity] | None = None,
show_caption_above_media: bool | None = None,
has_spoiler: bool | None = None,
*,
api_kwargs: JSONDict | None = None,
):
media = parse_file_input(media, tg_type=Video, attach=True, local_mode=True)
photo = parse_file_input(photo, tg_type=PhotoSize, attach=True, local_mode=True)
super().__init__(
BaseInputMediaType.LIVE_PHOTO,
media,
caption,
caption_entities,
parse_mode,
api_kwargs=api_kwargs,
)
with self._unfrozen():
self.photo: str | InputFile = photo
self.show_caption_above_media: bool | None = show_caption_above_media
self.has_spoiler: bool | None = has_spoiler
InputPollMedia: TypeAlias = (
InputMediaAnimation
| InputMediaAudio
| InputMediaDocument
| InputMediaLivePhoto
| InputMediaLocation
| InputMediaPhoto
| InputMediaVenue
| InputMediaVideo
)
"""Type alias for InputPollMedia objects.
versionadded:: NEXT.VERSION
"""
InputPollOptionMedia: TypeAlias = (
InputMediaAnimation
| InputMediaLivePhoto
| InputMediaLocation
| InputMediaPhoto
| InputMediaSticker
| InputMediaVenue
| InputMediaVideo
)
"""Type alias for InputPollOptionMedia objects.
.. versionadded:: NEXT.VERSION
"""
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2026
# 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 LivePhoto."""
from collections.abc import Sequence
from typing import TYPE_CHECKING
from telegram._files._basemedium import _BaseMedium
from telegram._files.photosize import PhotoSize
from telegram._utils.argumentparsing import de_list_optional, parse_sequence_arg, to_timedelta
from telegram._utils.types import JSONDict, TimePeriod
if TYPE_CHECKING:
import datetime as dtm
from telegram import Bot
class LivePhoto(_BaseMedium):
"""
This object represents a live photo.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`file_unique_id` is equal.
.. versionadded:: NEXT.VERSION
Args:
file_id (:obj:`str`): Identifier for the video file which can be used to download or reuse
the file.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
width (:obj:`int`): Video width as defined by the sender.
height (:obj:`int`): Video height as defined by the sender.
duration (:obj:`int` | :class:`datetime.timedelta`): Duration of the video
in seconds as defined by the sender.
photo (Sequence[:obj:`telegram.PhotoSize`], optional): Available sizes of the corresponding
static photo.
mime_type (:obj:`str`, optional): MIME type of a file as defined by the sender.
file_size (:obj:`int`, optional): File size in bytes.
Attributes:
file_id (:obj:`str`): Identifier for the video file which can be used to download or reuse
the file.
file_unique_id (:obj:`str`): Unique identifier for this file, which
is supposed to be the same over time and for different bots.
Can't be used to download or reuse the file.
width (:obj:`int`): Video width as defined by the sender.
height (:obj:`int`): Video height as defined by the sender.
duration (:class:`datetime.timedelta`): Duration of the video
in seconds as defined by the sender.
photo (tuple[:obj:`telegram.PhotoSize`]): Optional. Available sizes of the corresponding
static photo.
mime_type (:obj:`str`): Optional. MIME type of a file as defined by the sender.
file_size (:obj:`int`): Optional. File size in bytes.
"""
__slots__ = (
"duration",
"height",
"mime_type",
"photo",
"width",
)
def __init__(
self,
file_id: str,
file_unique_id: str,
width: int,
height: int,
duration: TimePeriod,
photo: Sequence[PhotoSize] | None = None,
mime_type: str | None = None,
file_size: int | None = None,
*,
api_kwargs: JSONDict | None = None,
):
super().__init__(
file_id=file_id,
file_unique_id=file_unique_id,
file_size=file_size,
api_kwargs=api_kwargs,
)
with self._unfrozen():
# Required
self.width: int = width
self.height: int = height
self.duration: dtm.timedelta = to_timedelta(duration)
# Optional
self.photo: Sequence[PhotoSize] | None = parse_sequence_arg(photo)
self.mime_type: str | None = mime_type
@classmethod
def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "LivePhoto":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
data["photo"] = de_list_optional(data.get("photo"), PhotoSize, bot)
return super().de_json(data=data, bot=bot)
+1 -1
View File
@@ -31,7 +31,7 @@ class ForceReply(TelegramObject):
the user (act as if the user has selected the bot's message and tapped 'Reply'). This can be
extremely useful if you want to create user-friendly step-by-step interfaces without having
to sacrifice `privacy mode <https://core.telegram.org/bots/features#privacy-mode>`_. Not
supported in channels and for messages sent on behalf of a Telegram Business account.
supported in channels and for messages sent on behalf of a user account.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`selective` is equal.
+237 -5
View File
@@ -37,6 +37,7 @@ from telegram._files.animation import Animation
from telegram._files.audio import Audio
from telegram._files.contact import Contact
from telegram._files.document import Document
from telegram._files.livephoto import LivePhoto
from telegram._files.location import Location
from telegram._files.photosize import PhotoSize
from telegram._files.sticker import Sticker
@@ -109,17 +110,21 @@ if TYPE_CHECKING:
GiveawayCompleted,
GiveawayCreated,
GiveawayWinners,
InlineQueryResult,
InputMedia,
InputMediaAudio,
InputMediaDocument,
InputMediaLivePhoto,
InputMediaPhoto,
InputMediaVideo,
InputPaidMedia,
InputPollMedia,
InputPollOption,
LabeledPrice,
MessageId,
MessageOrigin,
ReactionType,
SentGuestMessage,
SuggestedPostApprovalFailed,
SuggestedPostApproved,
SuggestedPostDeclined,
@@ -707,6 +712,26 @@ class Message(MaybeInaccessibleMessage):
managed_bot_created (:class:`telegram.ManagedBotCreated`, optional): Service message: user
created a bot that will be managed by the current bot.
.. versionadded:: NEXT.VERSION
guest_bot_caller_user (:class:`telegram.User`, optional): For a message sent by a guest
bot, this is the user whose original message triggered the bot's response.
.. versionadded:: NEXT.VERSION
guest_bot_caller_chat (:class:`telegram.Chat`, optional): For a message sent by a guest
bot, this is the chat whose original message triggered the bot's response.
.. versionadded:: NEXT.VERSION
guest_query_id (:obj:`str`, optional): The unique identifier for the guest query. Use this
identifier with the method :meth:`telegram.Bot.answer_guest_query` to send a response
message. If non-empty, the message belongs to the chat where the guest bot was
summoned, which may not coincide with other existing bot chats sharing the same
identifier.
.. versionadded:: NEXT.VERSION
live_photo (:class:`telegram.LivePhoto`, optional): Message is a live photo, information
about the live photo. For backward compatibility, when this field is set, the photo
field will also be set.
.. versionadded:: NEXT.VERSION
Attributes:
@@ -1139,6 +1164,26 @@ class Message(MaybeInaccessibleMessage):
managed_bot_created (:class:`telegram.ManagedBotCreated`): Optional. Service message: user
created a bot that will be managed by the current bot.
.. versionadded:: NEXT.VERSION
guest_bot_caller_user (:class:`telegram.User`): Optional. For a message sent by a guest
bot, this is the user whose original message triggered the bot's response.
.. versionadded:: NEXT.VERSION
guest_bot_caller_chat (:class:`telegram.Chat`): Optional. For a message sent by a guest
bot, this is the chat whose original message triggered the bot's response.
.. versionadded:: NEXT.VERSION
guest_query_id (:obj:`str`): Optional. The unique identifier for the guest query. Use this
identifier with the method :meth:`telegram.Bot.answer_guest_query` to send a response
message. If non-empty, the message belongs to the chat where the guest bot was
summoned, which may not coincide with other existing bot chats sharing the same
identifier.
.. versionadded:: NEXT.VERSION
live_photo (:class:`telegram.LivePhoto`): Optional. Message is a live photo, information
about the live photo. For backward compatibility, when this field is set, the photo
field will also be set.
.. versionadded:: NEXT.VERSION
.. |custom_emoji_no_md1_support| replace:: Since custom emoji entities are not supported by
@@ -1201,6 +1246,9 @@ class Message(MaybeInaccessibleMessage):
"giveaway_created",
"giveaway_winners",
"group_chat_created",
"guest_bot_caller_chat",
"guest_bot_caller_user",
"guest_query_id",
"has_media_spoiler",
"has_protected_content",
"invoice",
@@ -1210,6 +1258,7 @@ class Message(MaybeInaccessibleMessage):
"is_topic_message",
"left_chat_member",
"link_preview_options",
"live_photo",
"location",
"managed_bot_created",
"media_group_id",
@@ -1380,6 +1429,10 @@ class Message(MaybeInaccessibleMessage):
poll_option_deleted: PollOptionDeleted | None = None,
reply_to_poll_option_id: str | None = None,
managed_bot_created: ManagedBotCreated | None = None,
guest_bot_caller_user: User | None = None,
guest_bot_caller_chat: Chat | None = None,
guest_query_id: str | None = None,
live_photo: LivePhoto | None = None,
*,
api_kwargs: JSONDict | None = None,
):
@@ -1514,6 +1567,10 @@ class Message(MaybeInaccessibleMessage):
self.poll_option_deleted: PollOptionDeleted | None = poll_option_deleted
self.reply_to_poll_option_id: str | None = reply_to_poll_option_id
self.managed_bot_created: ManagedBotCreated | None = managed_bot_created
self.guest_bot_caller_user: User | None = guest_bot_caller_user
self.guest_bot_caller_chat: Chat | None = guest_bot_caller_chat
self.guest_query_id: str | None = guest_query_id
self.live_photo: LivePhoto | None = live_photo
self._effective_attachment = DEFAULT_NONE
@@ -1743,6 +1800,13 @@ class Message(MaybeInaccessibleMessage):
data["managed_bot_created"] = de_json_optional(
data.get("managed_bot_created"), ManagedBotCreated, bot
)
data["guest_bot_caller_user"] = de_json_optional(
data.get("guest_bot_caller_user"), User, bot
)
data["guest_bot_caller_chat"] = de_json_optional(
data.get("guest_bot_caller_chat"), Chat, bot
)
data["live_photo"] = de_json_optional(data.get("live_photo"), LivePhoto, bot)
api_kwargs = {}
# This is a deprecated field that TG still returns for backwards compatibility
@@ -1774,6 +1838,7 @@ class Message(MaybeInaccessibleMessage):
| Document
| Game
| Invoice
| LivePhoto
| Location
| PassportData
| Sequence[PhotoSize]
@@ -1798,6 +1863,7 @@ class Message(MaybeInaccessibleMessage):
* :class:`telegram.Animation`
* :class:`telegram.Game`
* :class:`telegram.Invoice`
* :class:`telegram.LivePhoto`
* :class:`telegram.Location`
* :class:`telegram.PassportData`
* list[:class:`telegram.PhotoSize`]
@@ -2186,7 +2252,7 @@ class Message(MaybeInaccessibleMessage):
async def reply_text_draft(
self,
draft_id: int,
text: str,
text: str | None = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
entities: Sequence["MessageEntity"] | None = None,
message_thread_id: ODVInput[int] = DEFAULT_NONE,
@@ -2213,6 +2279,9 @@ class Message(MaybeInaccessibleMessage):
.. versionadded:: 22.6
.. versionchanged:: NEXT.VERSION
Bot API 10.0 makes the ``text`` argument optional.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
@@ -2485,7 +2554,7 @@ class Message(MaybeInaccessibleMessage):
async def reply_media_group(
self,
media: Sequence[
"InputMediaAudio | InputMediaDocument | InputMediaPhoto | InputMediaVideo"
"InputMediaAudio | InputMediaDocument | InputMediaPhoto | InputMediaVideo | InputMediaLivePhoto" # noqa: E501 # pylint: disable=line-too-long
],
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
@@ -2646,6 +2715,87 @@ class Message(MaybeInaccessibleMessage):
suggested_post_parameters=suggested_post_parameters,
)
async def reply_live_photo(
self,
live_photo: "FileInput | LivePhoto",
photo: "FileInput | PhotoSize",
caption: str | None = None,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
reply_markup: "ReplyMarkup | None" = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
caption_entities: Sequence["MessageEntity"] | None = None,
show_caption_above_media: bool | None = None,
has_spoiler: bool | None = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: ODVInput[int] = DEFAULT_NONE,
reply_parameters: "ReplyParameters | None" = None,
message_effect_id: str | None = None,
allow_paid_broadcast: bool | None = None,
suggested_post_parameters: "SuggestedPostParameters | None" = None,
*,
reply_to_message_id: int | None = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
filename: str | None = None,
do_quote: bool | (_ReplyKwargs | None) = 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: JSONDict | None = None,
) -> "Message":
"""Shortcut for::
await bot.send_live_photo(
update.effective_message.chat_id,
message_thread_id=update.effective_message.message_thread_id,
business_connection_id=self.business_connection_id,
direct_messages_topic_id=self.direct_messages_topic.topic_id,
*args,
**kwargs,
)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_live_photo`.
.. versionadded:: NEXT.VERSION
Keyword Args:
do_quote (:obj:`bool` | :obj:`dict`, optional): |do_quote|
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
chat_id, effective_reply_parameters = await self._parse_quote_arguments(
do_quote, reply_to_message_id, reply_parameters, allow_sending_without_reply
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_live_photo(
chat_id=chat_id,
live_photo=live_photo,
photo=photo,
caption=caption,
disable_notification=disable_notification,
reply_parameters=effective_reply_parameters,
reply_markup=reply_markup,
parse_mode=parse_mode,
caption_entities=caption_entities,
filename=filename,
protect_content=protect_content,
message_thread_id=message_thread_id,
has_spoiler=has_spoiler,
show_caption_above_media=show_caption_above_media,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
business_connection_id=self.business_connection_id,
message_effect_id=message_effect_id,
allow_paid_broadcast=allow_paid_broadcast,
direct_messages_topic_id=self._extract_direct_messages_topic_id(),
suggested_post_parameters=suggested_post_parameters,
)
async def reply_audio(
self,
audio: "FileInput | Audio",
@@ -3535,8 +3685,12 @@ class Message(MaybeInaccessibleMessage):
allow_adding_options: bool | None = None,
hide_results_until_closes: bool | None = None,
description: str | None = None,
description_parse_mode: str | None = None,
description_parse_mode: ODVInput[str] | None = None,
description_entities: Sequence["MessageEntity"] | None = None,
members_only: bool | None = None,
country_codes: Sequence[str] | None = None,
explanation_media: "InputPollMedia | None" = None,
media: "InputPollMedia | None" = None,
*,
reply_to_message_id: int | None = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -3615,6 +3769,10 @@ class Message(MaybeInaccessibleMessage):
description_entities=description_entities,
hide_results_until_closes=hide_results_until_closes,
allow_adding_options=allow_adding_options,
members_only=members_only,
country_codes=country_codes,
explanation_media=explanation_media,
media=media,
)
async def reply_dice(
@@ -3733,7 +3891,7 @@ class Message(MaybeInaccessibleMessage):
)
return await self.get_bot().send_checklist(
business_connection_id=self.business_connection_id,
chat_id=chat_id, # type: ignore[arg-type]
chat_id=chat_id,
checklist=checklist,
disable_notification=disable_notification,
reply_parameters=effective_reply_parameters,
@@ -3845,7 +4003,7 @@ class Message(MaybeInaccessibleMessage):
)
message_thread_id = self._parse_message_thread_id(chat_id, message_thread_id)
return await self.get_bot().send_game(
chat_id=chat_id, # type: ignore[arg-type]
chat_id=chat_id,
game_short_name=game_short_name,
disable_notification=disable_notification,
reply_parameters=effective_reply_parameters,
@@ -5220,6 +5378,80 @@ class Message(MaybeInaccessibleMessage):
api_kwargs=api_kwargs,
)
async def delete_reaction(
self,
user_id: int | None = None,
actor_chat_id: int | None = 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: JSONDict | None = None,
) -> bool:
"""Shortcut for::
await bot.delete_message_reaction(
chat_id=message.chat_id,
message_id=message.message_id,
*args, **kwargs
)
For the documentation of the arguments, please see
:meth:`telegram.Bot.delete_message_reaction`.
.. versionadded:: NEXT.VERSION
Returns:
:obj:`bool` On success, :obj:`True` is returned.
"""
return await self.get_bot().delete_message_reaction(
chat_id=self.chat_id,
message_id=self.message_id,
user_id=user_id,
actor_chat_id=actor_chat_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def answer_guest_query(
self,
result: "InlineQueryResult",
*,
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: JSONDict | None = None,
) -> "SentGuestMessage":
"""Shortcut for::
await bot.answer_guest_query(
self.guest_query_id,
*args, **kwargs,
)
For the documentation of the arguments, please see :meth:`telegram.Bot.answer_guest_query`.
.. versionadded:: NEXT.VERSION
Returns:
:class:`telegram.SentGuestMessage`: On success, a
:class:`telegram.SentGuestMessage` is returned.
"""
return await self.get_bot().answer_guest_query(
guest_query_id=self.guest_query_id,
result=result,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
def parse_entity(self, entity: MessageEntity) -> str:
"""Returns the text from a given :class:`telegram.MessageEntity`.
+45
View File
@@ -23,6 +23,7 @@ from collections.abc import Sequence
from typing import TYPE_CHECKING, Final
from telegram import constants
from telegram._files.livephoto import LivePhoto
from telegram._files.photosize import PhotoSize
from telegram._files.video import Video
from telegram._telegramobject import TelegramObject
@@ -68,6 +69,8 @@ class PaidMedia(TelegramObject):
""":const:`telegram.constants.PaidMediaType.PHOTO`"""
VIDEO: Final[str] = constants.PaidMediaType.VIDEO
""":const:`telegram.constants.PaidMediaType.VIDEO`"""
LIVE_PHOTO: Final[str] = constants.PaidMediaType.LIVE_PHOTO
""":const:`telegram.constants.PaidMediaType.LIVE_PHOTO`"""
def __init__(
self,
@@ -100,6 +103,7 @@ class PaidMedia(TelegramObject):
cls.PREVIEW: PaidMediaPreview,
cls.PHOTO: PaidMediaPhoto,
cls.VIDEO: PaidMediaVideo,
cls.LIVE_PHOTO: PaidMediaLivePhoto,
}
if cls is PaidMedia and data.get("type") in _class_mapping:
@@ -251,6 +255,47 @@ class PaidMediaVideo(PaidMedia):
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
class PaidMediaLivePhoto(PaidMedia):
"""
The paid media is a live photo.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`live_photo` are equal.
.. versionadded:: NEXT.VERSION
Args:
type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.LIVE_PHOTO`
live_photo (:class:`telegram.LivePhoto`): The photo.
Attributes:
type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.LIVE_PHOTO`
live_photo (:class:`telegram.LivePhoto`): The photo.
"""
__slots__ = ("live_photo",)
def __init__(
self,
live_photo: LivePhoto,
*,
api_kwargs: JSONDict | None = None,
) -> None:
super().__init__(type=PaidMedia.LIVE_PHOTO, api_kwargs=api_kwargs)
with self._unfrozen():
self.live_photo: LivePhoto = live_photo
self._id_attrs = (self.type, self.live_photo)
@classmethod
def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PaidMediaLivePhoto":
data = cls._parse_data(data)
data["live_photo"] = de_json_optional(data.get("live_photo"), LivePhoto, bot)
return super().de_json(data=data, bot=bot) # type: ignore[return-value]
class PaidMediaInfo(TelegramObject):
"""
Describes the paid media added to a message.
+240 -13
View File
@@ -24,6 +24,15 @@ from typing import TYPE_CHECKING, Final
from telegram import constants
from telegram._chat import Chat
from telegram._files.animation import Animation
from telegram._files.audio import Audio
from telegram._files.document import Document
from telegram._files.livephoto import LivePhoto
from telegram._files.location import Location
from telegram._files.photosize import PhotoSize
from telegram._files.sticker import Sticker
from telegram._files.venue import Venue
from telegram._files.video import Video
from telegram._messageentity import MessageEntity
from telegram._telegramobject import TelegramObject
from telegram._user import User
@@ -46,7 +55,122 @@ from telegram._utils.warnings import warn
from telegram.warnings import PTBDeprecationWarning
if TYPE_CHECKING:
from telegram import Bot, MaybeInaccessibleMessage
from telegram import Bot, InputPollOptionMedia, MaybeInaccessibleMessage
class PollMedia(TelegramObject):
"""
At most one of the optional fields can be present in any given object.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if all of their attributes are equal.
.. versionadded:: NEXT.VERSION
Args:
animation (:class:`telegram.Animation`, optional): Media is an animation, information about
the animation
audio (:class:`telegram.Audio`, optional): Media is an audio file, information about the
file; currently, can't be received in a poll option
document (:class:`telegram.Document`, optional): Media is a general file, information about
the file; currently, can't be received in a poll option
live_photo (:class:`telegram.LivePhoto`, optional): Media is a live photo, information
about the live photo
location (:class:`telegram.Location`, optional): Media is a shared location, information
about the location
photo (Sequence[:class:`telegram.PhotoSize`], optional): Media is a photo, available sizes
of the photo
sticker (:class:`telegram.Sticker`, optional): Media is a sticker, information about the
sticker; currently, for poll options only
venue (:class:`telegram.Venue`, optional): Media is a venue, information about the venue
video (:class:`telegram.Video`, optional): Media is a video, information about the video
Attributes:
animation (:class:`telegram.Animation`): Optional. Media is an animation, information about
the animation
audio (:class:`telegram.Audio`): Optional. Media is an audio file, information about the
file; currently, can't be received in a poll option
document (:class:`telegram.Document`): Optional. Media is a general file, information about
the file; currently, can't be received in a poll option
live_photo (:class:`telegram.LivePhoto`, optional): Media is a live photo, information
about the live photo
location (:class:`telegram.Location`): Optional. Media is a shared location, information
about the location
photo (tuple[:class:`telegram.PhotoSize`]): Optional. Media is a photo, available sizes
of the photo
sticker (:class:`telegram.Sticker`): Optional. Media is a sticker, information about the
sticker; currently, for poll options only
venue (:class:`telegram.Venue`): Optional. Media is a venue, information about the venue
video (:class:`telegram.Video`): Optional. Media is a video, information about the video
"""
__slots__ = (
"animation",
"audio",
"document",
"live_photo",
"location",
"photo",
"sticker",
"venue",
"video",
)
def __init__(
self,
animation: Animation | None = None,
audio: Audio | None = None,
document: Document | None = None,
live_photo: LivePhoto | None = None,
location: Location | None = None,
photo: Sequence[PhotoSize] | None = None,
sticker: Sticker | None = None,
venue: Venue | None = None,
video: Video | None = None,
*,
api_kwargs: JSONDict | None = None,
):
super().__init__(api_kwargs=api_kwargs)
self.animation: Animation | None = animation
self.audio: Audio | None = audio
self.document: Document | None = document
self.live_photo: LivePhoto | None = live_photo
self.location: Location | None = location
self.photo: tuple[PhotoSize, ...] = parse_sequence_arg(photo)
self.sticker: Sticker | None = sticker
self.venue: Venue | None = venue
self.video: Video | None = video
self._id_attrs = (
self.animation,
self.audio,
self.document,
self.live_photo,
self.location,
self.photo,
self.sticker,
self.venue,
self.video,
)
self._freeze()
@classmethod
def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "PollMedia":
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
data["animation"] = de_json_optional(data.get("animation"), Animation, bot)
data["audio"] = de_json_optional(data.get("audio"), Audio, bot)
data["document"] = de_json_optional(data.get("document"), Document, bot)
data["live_photo"] = de_json_optional(data.get("live_photo"), LivePhoto, bot)
data["location"] = de_json_optional(data.get("location"), Location, bot)
data["photo"] = de_list_optional(data.get("photo"), PhotoSize, bot)
data["sticker"] = de_json_optional(data.get("sticker"), Sticker, bot)
data["venue"] = de_json_optional(data.get("venue"), Venue, bot)
data["video"] = de_json_optional(data.get("video"), Video, bot)
return super().de_json(data=data, bot=bot)
class InputPollOption(TelegramObject):
@@ -69,6 +193,9 @@ class InputPollOption(TelegramObject):
:paramref:`text_parse_mode`.
Currently, only custom emoji entities are allowed.
This list is empty if the text does not contain entities.
media (:class:`telegram.InputPollOptionMedia`, optional): Media added to the poll option.
.. versionadded:: NEXT.VERSION
Attributes:
text (:obj:`str`): Option text,
@@ -81,15 +208,19 @@ class InputPollOption(TelegramObject):
:paramref:`text_parse_mode`.
Currently, only custom emoji entities are allowed.
This list is empty if the text does not contain entities.
media (:class:`telegram.InputPollOptionMedia`): Optional. Media added to the poll option.
.. versionadded:: NEXT.VERSION
"""
__slots__ = ("text", "text_entities", "text_parse_mode")
__slots__ = ("media", "text", "text_entities", "text_parse_mode")
def __init__(
self,
text: str,
text_parse_mode: ODVInput[str] = DEFAULT_NONE,
text_entities: Sequence[MessageEntity] | None = None,
media: "InputPollOptionMedia | None" = None,
*,
api_kwargs: JSONDict | None = None,
):
@@ -97,14 +228,30 @@ class InputPollOption(TelegramObject):
self.text: str = text
self.text_parse_mode: ODVInput[str] = text_parse_mode
self.text_entities: tuple[MessageEntity, ...] = parse_sequence_arg(text_entities)
self.media: InputPollOptionMedia | None = media
self._id_attrs = (self.text,)
self._freeze()
# tags: deprecated NEXT.VERSION
@classmethod
def de_json(cls, data: JSONDict, bot: "Bot | None" = None) -> "InputPollOption":
"""See :meth:`telegram.TelegramObject.de_json`."""
"""See :meth:`telegram.TelegramObject.de_json`. The :paramref:`media` field will
not be included for deserialization.
.. deprecated:: NEXT.VERSION
This class is input only and will be removed in the next version.
"""
warn(
PTBDeprecationWarning(
"NEXT.VERSION",
"`InputPollOption.de_json` is deprecated. This class is input only and will be "
"removed in the next version. The `media` field will not be included for "
"deserialization.",
),
stacklevel=2,
)
data = cls._parse_data(data)
data["text_entities"] = de_list_optional(data.get("text_entities"), MessageEntity, bot)
@@ -117,7 +264,12 @@ class PollOption(TelegramObject):
This object contains information about one answer option in a poll.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`text` and :attr:`voter_count` are equal.
considered equal, if their :attr:`text`, :attr:`voter_count` and :attr:`persistent_id`
are equal.
.. versionchanged:: NEXT.VERSION
Added attribute :attr:`persistent_id` to equality checks.
Args:
persistent_id (:obj:`str`): Unique identifier of the option, persistent on option addition
@@ -133,6 +285,9 @@ class PollOption(TelegramObject):
poll option texts.
.. versionadded:: 21.2
media (:class:`telegram.PollMedia`, optional): Media added to the poll option.
.. versionadded:: NEXT.VERSION
added_by_user (:class:`telegram.User`, optional): User who added the option;
omitted if the option wasn't added by a user after poll creation.
@@ -161,6 +316,9 @@ class PollOption(TelegramObject):
This list is empty if the question does not contain entities.
.. versionadded:: 21.2
media (:class:`telegram.PollMedia`): Optional. Media added to the poll option.
.. versionadded:: NEXT.VERSION
added_by_user (:class:`telegram.User`): Optional. User who added the option;
omitted if the option wasn't added by a user after poll creation.
@@ -179,6 +337,7 @@ class PollOption(TelegramObject):
"added_by_chat",
"added_by_user",
"addition_date",
"media",
"persistent_id",
"text",
"text_entities",
@@ -193,23 +352,28 @@ class PollOption(TelegramObject):
added_by_user: User | None = None,
added_by_chat: Chat | None = None,
addition_date: dtm.datetime | None = None,
media: PollMedia | None = None,
# tags: required in NEXT.VERSION, bot api 9.6
# temporarily optional to avoid breaking changes
persistent_id: str | None = None,
*,
api_kwargs: JSONDict | None = None,
):
if persistent_id is None:
raise TypeError("`persistent_id` is a required argument since Bot API 9.6")
super().__init__(api_kwargs=api_kwargs)
self.text: str = text
self.voter_count: int = voter_count
self.added_by_user: User | None = added_by_user
self.added_by_chat: Chat | None = added_by_chat
self.addition_date: dtm.datetime | None = addition_date
self.persistent_id: str | None = persistent_id
self.persistent_id: str = persistent_id
self.media: PollMedia | None = media
self.text_entities: tuple[MessageEntity, ...] = parse_sequence_arg(text_entities)
self._id_attrs = (self.text, self.voter_count)
self._id_attrs = (self.text, self.voter_count, self.persistent_id)
self._freeze()
@@ -225,6 +389,7 @@ class PollOption(TelegramObject):
data["added_by_user"] = de_json_optional(data.get("added_by_user"), User, bot)
data["added_by_chat"] = de_json_optional(data.get("added_by_chat"), Chat, bot)
data["addition_date"] = from_timestamp(data.get("addition_date"), tzinfo=loc_tzinfo)
data["media"] = de_json_optional(data.get("media"), PollMedia, bot)
return super().de_json(data=data, bot=bot)
@@ -358,6 +523,9 @@ class PollAnswer(TelegramObject):
*,
api_kwargs: JSONDict | None = None,
):
if option_persistent_ids is None:
raise TypeError("`option_persistent_ids` is a required argument since Bot API 9.6")
super().__init__(api_kwargs=api_kwargs)
self.poll_id: str = poll_id
self.voter_chat: Chat | None = voter_chat
@@ -637,6 +805,11 @@ class Poll(TelegramObject):
is_anonymous (:obj:`bool`): :obj:`True`, if the poll is anonymous.
type (:obj:`str`): Poll type, currently can be :attr:`REGULAR` or :attr:`QUIZ`.
allows_multiple_answers (:obj:`bool`): :obj:`True`, if the poll allows multiple answers.
members_only (:obj:`bool`): :obj:`True`, if voting is limited to users who have been
members of the chat where the poll was originally sent for more than
:tg-const:`telegram.Poll.MIN_MEMBERSHIP_HOURS` hours.
.. versionadded:: NEXT.VERSION
correct_option_id (:obj:`int`, optional): A zero based identifier of the correct answer
option. Available only for closed polls in the quiz mode, which were sent
(not forwarded), by the bot or to a private chat with the bot.
@@ -655,6 +828,10 @@ class Poll(TelegramObject):
* This attribute is now always a (possibly empty) list and never :obj:`None`.
* |sequenceclassargs|
explanation_media (:class:`telegram.PollMedia`, optional): Media added to the quiz
explanation.
.. versionadded:: NEXT.VERSION
open_period (:obj:`int` | :class:`datetime.timedelta`, optional): Amount of time in seconds
the poll will be active after creation.
@@ -678,6 +855,12 @@ class Poll(TelegramObject):
the correct answer options. Available only for polls in quiz mode which are closed or
were sent (not forwarded) by the bot or to the private chat with the bot.
.. versionadded:: NEXT.VERSION
country_codes (Sequence[:obj:`str`], optional): A list of two-letter ``ISO 3166-1 alpha-2``
country codes indicating the countries from which users can vote in the poll. The
country code ``"FT"`` is used for users with anonymous numbers. If omitted, then users
from any country can participate in the poll.
.. versionadded:: NEXT.VERSION
description (:obj:`str`, optional): Description of the poll;
for polls inside the :class:`~telegram.Message` object only.
@@ -686,6 +869,10 @@ class Poll(TelegramObject):
description_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special
entities like usernames, URLs, bot commands, etc. that appear in the description
.. versionadded:: NEXT.VERSION
media (:class:`telegram.PollMedia`, optional): Media added to the poll description;
for polls inside the :class:`~telegram.Message` object only.
.. versionadded:: NEXT.VERSION
Attributes:
@@ -701,12 +888,11 @@ class Poll(TelegramObject):
is_anonymous (:obj:`bool`): :obj:`True`, if the poll is anonymous.
type (:obj:`str`): Poll type, currently can be :attr:`REGULAR` or :attr:`QUIZ`.
allows_multiple_answers (:obj:`bool`): :obj:`True`, if the poll allows multiple answers.
correct_option_id (:obj:`int`): Optional. A zero based identifier of the correct answer
option. Available only for closed polls in the quiz mode, which were sent
(not forwarded), by the bot or to a private chat with the bot.
members_only (:obj:`bool`): :obj:`True`, if voting is limited to users who have been
members of the chat where the poll was originally sent for more than
:tg-const:`telegram.Poll.MIN_MEMBERSHIP_HOURS` hours.
.. deprecated:: NEXT.VERSION
Use :attr:`correct_option_ids` instead.
.. versionadded:: NEXT.VERSION
explanation (:obj:`str`): Optional. Text that is shown when a user chooses an incorrect
answer or taps on the lamp icon in a quiz-style poll,
0-:tg-const:`telegram.Poll.MAX_EXPLANATION_LENGTH` characters.
@@ -719,6 +905,10 @@ class Poll(TelegramObject):
.. versionchanged:: 20.0
This attribute is now always a (possibly empty) list and never :obj:`None`.
explanation_media (:class:`telegram.PollMedia`): Optional. Media added to the quiz
explanation.
.. versionadded:: NEXT.VERSION
open_period (:obj:`int` | :class:`datetime.timedelta`): Optional. Amount of time in seconds
the poll will be active after creation.
@@ -735,7 +925,7 @@ class Poll(TelegramObject):
This list is empty if the question does not contain entities.
.. versionadded:: 21.2
allows_revoting (:obj:`bool`): Optional. :obj:`True`, if the poll
allows_revoting (:obj:`bool`): :obj:`True`, if the poll
allows to change the chosenanswer options
.. versionadded:: NEXT.VERSION
@@ -743,6 +933,12 @@ class Poll(TelegramObject):
correct answer options. Available only for polls in quiz mode which are closed or were
sent (not forwarded) by the bot or to the private chat with the bot.
.. versionadded:: NEXT.VERSION
country_codes (tuple[:obj:`str`]): Optional. A list of two-letter ``ISO 3166-1 alpha-2``
country codes indicating the countries from which users can vote in the poll. The
country code ``"FT"`` is used for users with anonymous numbers. If omitted, then users
from any country can participate in the poll.
.. versionadded:: NEXT.VERSION
description (:obj:`str`): Optional. Description of the poll;
for polls inside the Message object only
@@ -751,6 +947,10 @@ class Poll(TelegramObject):
description_entities (tuple[:class:`telegram.MessageEntity`]): Special
entities like usernames, URLs, bot commands, etc. that appear in the description
.. versionadded:: NEXT.VERSION
media (:class:`telegram.PollMedia`): Optional. Media added to the poll description;
for polls inside the Message object only.
.. versionadded:: NEXT.VERSION
"""
@@ -761,13 +961,17 @@ class Poll(TelegramObject):
"allows_revoting",
"close_date",
"correct_option_ids",
"country_codes",
"description",
"description_entities",
"explanation",
"explanation_entities",
"explanation_media",
"id",
"is_anonymous",
"is_closed",
"media",
"members_only",
"options",
"question",
"question_entities",
@@ -788,6 +992,7 @@ class Poll(TelegramObject):
# tags: deprecated NEXT.VERSION
# Removed in bot api 9.6:
correct_option_id: int | None = None,
# ---
explanation: str | None = None,
explanation_entities: Sequence[MessageEntity] | None = None,
open_period: TimePeriod | None = None,
@@ -796,12 +1001,23 @@ class Poll(TelegramObject):
# tags: required in NEXT.VERSION
# temporarily optional to avoid breaking changes
allows_revoting: bool | None = None,
members_only: bool | None = None,
# ---
correct_option_ids: Sequence[int] | None = None,
description: str | None = None,
description_entities: Sequence[MessageEntity] | None = None,
country_codes: Sequence[str] | None = None,
media: PollMedia | None = None,
explanation_media: PollMedia | None = None,
*,
api_kwargs: JSONDict | None = None,
):
if allows_revoting is None:
raise TypeError("`allows_revoting` is a required argument since Bot API 9.6")
if members_only is None:
raise TypeError("`members_only` is a required argument since Bot API 10.0")
super().__init__(api_kwargs=api_kwargs)
self.id: str = id
self.question: str = question
@@ -811,7 +1027,8 @@ class Poll(TelegramObject):
self.is_anonymous: bool = is_anonymous
self.type: str = enum.get_member(constants.PollType, type, type)
self.allows_multiple_answers: bool = allows_multiple_answers
self.allows_revoting: bool | None = allows_revoting
self.allows_revoting: bool = allows_revoting
self.members_only: bool = members_only
# tag: deprecated NEXT.VERSION
if correct_option_id is not None:
@@ -838,6 +1055,9 @@ class Poll(TelegramObject):
self._open_period: dtm.timedelta | None = to_timedelta(open_period)
self.close_date: dtm.datetime | None = close_date
self.question_entities: tuple[MessageEntity, ...] = parse_sequence_arg(question_entities)
self.country_codes: tuple[str, ...] = parse_sequence_arg(country_codes)
self.media: PollMedia | None = media
self.explanation_media: PollMedia | None = explanation_media
self._id_attrs = (self.id,)
@@ -866,6 +1086,8 @@ class Poll(TelegramObject):
data["description_entities"] = de_list_optional(
data.get("description_entities"), MessageEntity, bot
)
data["media"] = de_json_optional(data.get("media"), PollMedia, bot)
data["explanation_media"] = de_json_optional(data.get("explanation_media"), PollMedia, bot)
return super().de_json(data=data, bot=bot)
@@ -1105,3 +1327,8 @@ class Poll(TelegramObject):
.. versionadded:: NEXT.VERSION
"""
MIN_MEMBERSHIP_HOURS: Final[int] = constants.PollLimit.MIN_MEMBERSHIP_HOURS
""":const:`telegram.constants.PollLimit.MIN_MEMBERSHIP_HOURS`
.. versionadded:: NEXT.VERSION
"""
+14
View File
@@ -28,6 +28,7 @@ from telegram._files.animation import Animation
from telegram._files.audio import Audio
from telegram._files.contact import Contact
from telegram._files.document import Document
from telegram._files.livephoto import LivePhoto
from telegram._files.location import Location
from telegram._files.photosize import PhotoSize
from telegram._files.sticker import Sticker
@@ -114,6 +115,10 @@ class ExternalReplyInfo(TelegramObject):
information about the paid media.
.. versionadded:: 21.4
live_photo (:class:`telegram.LivePhoto`, optional): Message is a live photo, information
about the live photo.
.. versionadded:: NEXT.VERSION
Attributes:
origin (:class:`telegram.MessageOrigin`): Origin of the message replied to by the given
@@ -166,6 +171,11 @@ class ExternalReplyInfo(TelegramObject):
information about the paid media.
.. versionadded:: 21.4
live_photo (:class:`telegram.LivePhoto`): Optional. Message is a live photo, information
about the live photo.
.. versionadded:: NEXT.VERSION
"""
__slots__ = (
@@ -182,6 +192,7 @@ class ExternalReplyInfo(TelegramObject):
"has_media_spoiler",
"invoice",
"link_preview_options",
"live_photo",
"location",
"message_id",
"origin",
@@ -223,6 +234,7 @@ class ExternalReplyInfo(TelegramObject):
venue: Venue | None = None,
paid_media: PaidMediaInfo | None = None,
checklist: Checklist | None = None,
live_photo: LivePhoto | None = None,
*,
api_kwargs: JSONDict | None = None,
):
@@ -253,6 +265,7 @@ class ExternalReplyInfo(TelegramObject):
self.poll: Poll | None = poll
self.venue: Venue | None = venue
self.paid_media: PaidMediaInfo | None = paid_media
self.live_photo: LivePhoto | None = live_photo
self._id_attrs = (self.origin,)
@@ -290,6 +303,7 @@ class ExternalReplyInfo(TelegramObject):
data["venue"] = de_json_optional(data.get("venue"), Venue, bot)
data["paid_media"] = de_json_optional(data.get("paid_media"), PaidMediaInfo, bot)
data["checklist"] = de_json_optional(data.get("checklist"), Checklist, bot)
data["live_photo"] = de_json_optional(data.get("live_photo"), LivePhoto, bot)
return super().de_json(data=data, bot=bot)
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2026
# 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 Sent Guest Message."""
from telegram._telegramobject import TelegramObject
from telegram._utils.types import JSONDict
class SentGuestMessage(TelegramObject):
"""Describes an inline message sent by a guest bot.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`inline_message_id` are equal.
.. versionadded:: NEXT.VERSION
Args:
inline_message_id (:obj:`str`): Identifier of the sent inline message.
Attributes:
inline_message_id (:obj:`str`): Identifier of the sent inline message.
"""
__slots__ = ("inline_message_id",)
def __init__(
self,
inline_message_id: str,
*,
api_kwargs: JSONDict | None = None,
):
super().__init__(api_kwargs=api_kwargs)
# Required
self.inline_message_id: str = inline_message_id
self._id_attrs = (self.inline_message_id,)
self._freeze()
+53 -33
View File
@@ -168,6 +168,11 @@ class Update(TelegramObject):
managed by the bot, or token or owner of a managed bot was changed.
.. versionadded:: NEXT.VERSION
guest_message (:class:`telegram.Message`, optional): New guest message. The bot can use
the field :attr:`telegram.Message.guest_query_id` and the method
:meth:`telegram.Bot.answer_guest_query` to send a message in response.
.. versionadded:: NEXT.VERSION
Attributes:
@@ -284,6 +289,11 @@ class Update(TelegramObject):
managed_bot (:class:`telegram.ManagedBotUpdated`): Optional. A new bot was created to be
managed by the bot, or token or owner of a managed bot was changed.
.. versionadded:: NEXT.VERSION
guest_message (:class:`telegram.Message`): Optional. New guest message. The bot can use
the field :attr:`telegram.Message.guest_query_id` and the method
:meth:`telegram.Bot.answerGuestQuery` to send a message in response.
.. versionadded:: NEXT.VERSION
"""
@@ -304,6 +314,7 @@ class Update(TelegramObject):
"edited_business_message",
"edited_channel_post",
"edited_message",
"guest_message",
"inline_query",
"managed_bot",
"message",
@@ -417,6 +428,11 @@ class Update(TelegramObject):
MANAGED_BOT: Final[str] = constants.UpdateType.MANAGED_BOT
""":const:`telegram.constants.UpdateType.MANAGED_BOT`
.. versionadded:: NEXT.VERSION
"""
GUEST_MESSAGE: Final[str] = constants.UpdateType.GUEST_MESSAGE
""":const:`telegram.constants.UpdateType.GUEST_MESSAGE`
.. versionadded:: NEXT.VERSION
"""
@@ -452,6 +468,7 @@ class Update(TelegramObject):
deleted_business_messages: BusinessMessagesDeleted | None = None,
purchased_paid_media: PaidMediaPurchased | None = None,
managed_bot: ManagedBotUpdated | None = None,
guest_message: Message | None = None,
*,
api_kwargs: JSONDict | None = None,
):
@@ -483,6 +500,7 @@ class Update(TelegramObject):
self.deleted_business_messages: BusinessMessagesDeleted | None = deleted_business_messages
self.purchased_paid_media: PaidMediaPurchased | None = purchased_paid_media
self.managed_bot: ManagedBotUpdated | None = managed_bot
self.guest_message: Message | None = guest_message
self._effective_user: User | None = None
self._effective_sender: User | Chat | None = None
@@ -516,8 +534,8 @@ class Update(TelegramObject):
This property now also considers :attr:`purchased_paid_media`.
.. versionchanged:: NEXT.VERSION
This property now also considers :attr:`managed_bot`, :attr:`channel_post`
and :attr:`edited_channel_post`.
This property now also considers :attr:`managed_bot`, :attr:`guest_message`,
:attr:`channel_post`, and :attr:`edited_channel_post`.
Example:
* If :attr:`message` is present, this will give
@@ -530,11 +548,14 @@ class Update(TelegramObject):
user = None
if self.message:
user = self.message.from_user
elif self.edited_message:
user = self.edited_message.from_user
if message := (
self.message
or self.edited_message
or self.business_message
or self.edited_business_message
or self.guest_message
):
user = message.from_user
elif self.channel_post:
user = self.channel_post.from_user
@@ -572,12 +593,6 @@ class Update(TelegramObject):
elif self.message_reaction:
user = self.message_reaction.user
elif self.business_message:
user = self.business_message.from_user
elif self.edited_business_message:
user = self.edited_business_message.from_user
elif self.business_connection:
user = self.business_connection.user
@@ -611,6 +626,9 @@ class Update(TelegramObject):
is present.
.. versionchanged:: NEXT.VERSION
This property now also considers :attr:`guest_message`.
Example:
* If :attr:`message` is present, this will give either
:attr:`telegram.Message.from_user` or :attr:`telegram.Message.sender_chat`.
@@ -633,6 +651,7 @@ class Update(TelegramObject):
or self.edited_channel_post
or self.business_message
or self.edited_business_message
or self.guest_message
):
sender = message.sender_chat
@@ -664,6 +683,9 @@ class Update(TelegramObject):
This property now also considers :attr:`business_message`,
:attr:`edited_business_message`, and :attr:`deleted_business_messages`.
.. versionchanged:: NEXT.VERSION
This property now also considers :attr:`guest_message`.
Example:
If :attr:`message` is present, this will give :attr:`telegram.Message.chat`.
@@ -673,21 +695,21 @@ class Update(TelegramObject):
chat = None
if self.message:
chat = self.message.chat
elif self.edited_message:
chat = self.edited_message.chat
if message := (
self.message
or self.edited_message
or self.channel_post
or self.edited_channel_post
or self.business_message
or self.edited_business_message
or self.deleted_business_messages
or self.guest_message
):
chat = message.chat
elif self.callback_query and self.callback_query.message:
chat = self.callback_query.message.chat
elif self.channel_post:
chat = self.channel_post.chat
elif self.edited_channel_post:
chat = self.edited_channel_post.chat
elif self.my_chat_member:
chat = self.my_chat_member.chat
@@ -709,15 +731,6 @@ class Update(TelegramObject):
elif self.message_reaction_count:
chat = self.message_reaction_count.chat
elif self.business_message:
chat = self.business_message.chat
elif self.edited_business_message:
chat = self.edited_business_message.chat
elif self.deleted_business_messages:
chat = self.deleted_business_messages.chat
self._effective_chat = chat
return chat
@@ -734,6 +747,9 @@ class Update(TelegramObject):
This property now also considers :attr:`business_message`, and
:attr:`edited_business_message`.
.. versionchanged:: NEXT.VERSION
This property now also considers :attr:`guest_message`.
Tip:
This property will only ever return objects of type :class:`telegram.Message` or
:obj:`None`, never :class:`telegram.MaybeInaccessibleMessage` or
@@ -782,6 +798,9 @@ class Update(TelegramObject):
elif self.edited_business_message:
message = self.edited_business_message
elif self.guest_message:
message = self.guest_message
self._effective_message = message
return message
@@ -838,5 +857,6 @@ class Update(TelegramObject):
data.get("purchased_paid_media"), PaidMediaPurchased, bot
)
data["managed_bot"] = de_json_optional(data.get("managed_bot"), ManagedBotUpdated, bot)
data["guest_message"] = de_json_optional(data.get("guest_message"), Message, bot)
return super().de_json(data=data, bot=bot)
+299 -5
View File
@@ -42,17 +42,21 @@ if TYPE_CHECKING:
from telegram import (
Animation,
Audio,
BotAccessSettings,
Contact,
Document,
Gift,
InlineKeyboardMarkup,
InputMediaAudio,
InputMediaDocument,
InputMediaLivePhoto,
InputMediaPhoto,
InputMediaVideo,
InputPollMedia,
InputPollOption,
LabeledPrice,
LinkPreviewOptions,
LivePhoto,
Location,
Message,
MessageEntity,
@@ -108,7 +112,7 @@ class User(TelegramObject):
.. versionadded:: 20.0
can_connect_to_business (:obj:`bool`, optional): :obj:`True`, if the bot can be connected
to a Telegram Business account to receive its messages. Returned only in
to a user account to manage it. Returned only in
:meth:`telegram.Bot.get_me`.
.. versionadded:: 21.1
@@ -128,6 +132,11 @@ class User(TelegramObject):
can_manage_bots (:obj:`bool`, optional): :obj:`True`, if other bots can be created to be
controlled by the bot. Returned only in :meth:`telegram.Bot.get_me`.
.. versionadded:: NEXT.VERSION
supports_guest_queries (:obj:`bool`, optional): :obj:`True`, if the bot supports guest
queries from chats it is not a member of. Returned only in
:meth:`telegram.Bot.get_me`.
.. versionadded:: NEXT.VERSION
Attributes:
@@ -172,6 +181,11 @@ class User(TelegramObject):
can_manage_bots (:obj:`bool`): Optional. :obj:`True`, if other bots can be created to be
controlled by the bot. Returned only in :meth:`telegram.Bot.get_me`.
.. versionadded:: NEXT.VERSION
supports_guest_queries (:obj:`bool`): Optional. :obj:`True`, if the bot supports guest
queries from chats it is not a member of. Returned only in
:meth:`telegram.Bot.get_me`.
.. versionadded:: NEXT.VERSION
.. |user_chat_id_note| replace:: This shortcuts build on the assumption that :attr:`User.id`
@@ -194,6 +208,7 @@ class User(TelegramObject):
"is_premium",
"language_code",
"last_name",
"supports_guest_queries",
"supports_inline_queries",
"username",
)
@@ -216,6 +231,7 @@ class User(TelegramObject):
has_topics_enabled: bool | None = None,
allows_users_to_create_topics: bool | None = None,
can_manage_bots: bool | None = None,
supports_guest_queries: bool | None = None,
*,
api_kwargs: JSONDict | None = None,
):
@@ -238,6 +254,7 @@ class User(TelegramObject):
self.has_topics_enabled: bool | None = has_topics_enabled
self.allows_users_to_create_topics: bool | None = allows_users_to_create_topics
self.can_manage_bots: bool | None = can_manage_bots
self.supports_guest_queries: bool | None = supports_guest_queries
self._id_attrs = (self.id,)
@@ -528,7 +545,7 @@ class User(TelegramObject):
async def send_message_draft(
self,
draft_id: int,
text: str,
text: str | None = None,
message_thread_id: int | None = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
entities: Sequence["MessageEntity"] | None = None,
@@ -550,6 +567,9 @@ class User(TelegramObject):
.. versionadded:: 22.6
.. versionchanged:: NEXT.VERSION
Bot API 10.0 makes the ``text`` argument optional.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
@@ -701,10 +721,83 @@ class User(TelegramObject):
suggested_post_parameters=suggested_post_parameters,
)
async def send_live_photo(
self,
live_photo: "FileInput | LivePhoto",
photo: "FileInput | PhotoSize",
business_connection_id: str | None = None,
message_thread_id: int | None = None,
direct_messages_topic_id: int | None = None,
caption: str | None = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
caption_entities: Sequence["MessageEntity"] | None = None,
show_caption_above_media: bool | None = None,
has_spoiler: bool | None = None,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
allow_paid_broadcast: bool | None = None,
message_effect_id: str | None = None,
suggested_post_parameters: "SuggestedPostParameters | None" = None,
reply_parameters: "ReplyParameters | None" = None,
reply_markup: "ReplyMarkup | None" = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: int | None = None,
filename: str | None = 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: JSONDict | None = None,
) -> "Message":
"""Shortcut for::
await bot.send_live_photo(update.effective_user.id, *args, **kwargs)
For the documentation of the arguments, please see :meth:`telegram.Bot.send_live_photo`.
.. versionadded:: NEXT.VERSION
Note:
|user_chat_id_note|
Returns:
:class:`telegram.Message`: On success, instance representing the message posted.
"""
return await self.get_bot().send_live_photo(
chat_id=self.id,
live_photo=live_photo,
photo=photo,
business_connection_id=business_connection_id,
message_thread_id=message_thread_id,
direct_messages_topic_id=direct_messages_topic_id,
caption=caption,
parse_mode=parse_mode,
caption_entities=caption_entities,
show_caption_above_media=show_caption_above_media,
has_spoiler=has_spoiler,
disable_notification=disable_notification,
protect_content=protect_content,
allow_paid_broadcast=allow_paid_broadcast,
message_effect_id=message_effect_id,
suggested_post_parameters=suggested_post_parameters,
reply_parameters=reply_parameters,
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
reply_to_message_id=reply_to_message_id,
filename=filename,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def send_media_group(
self,
media: Sequence[
"InputMediaAudio | InputMediaDocument | InputMediaPhoto | InputMediaVideo"
"InputMediaAudio | InputMediaDocument | InputMediaPhoto | InputMediaVideo | InputMediaLivePhoto" # noqa: E501 # pylint: disable=line-too-long
],
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
@@ -1748,8 +1841,12 @@ class User(TelegramObject):
allow_adding_options: bool | None = None,
hide_results_until_closes: bool | None = None,
description: str | None = None,
description_parse_mode: str | None = None,
description_parse_mode: ODVInput[str] | None = None,
description_entities: Sequence["MessageEntity"] | None = None,
members_only: bool | None = None,
country_codes: Sequence[str] | None = None,
explanation_media: "InputPollMedia | None" = None,
media: "InputPollMedia | None" = None,
*,
reply_to_message_id: int | None = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1811,6 +1908,10 @@ class User(TelegramObject):
description_entities=description_entities,
hide_results_until_closes=hide_results_until_closes,
allow_adding_options=allow_adding_options,
members_only=members_only,
country_codes=country_codes,
explanation_media=explanation_media,
media=media,
)
async def send_gift(
@@ -2761,7 +2862,7 @@ class User(TelegramObject):
.. versionadded:: NEXT.VERSION
Returns:
:obj:`bool`: On success, :obj:`str` is returned.
:obj:`str`: On success, :obj:`str` is returned.
"""
return await self.get_bot().replace_managed_bot_token(
user_id=self.id,
@@ -2771,3 +2872,196 @@ class User(TelegramObject):
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def get_managed_bot_access_settings(
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: JSONDict | None = None,
) -> "BotAccessSettings":
"""
Shortcut for::
await bot.get_managed_bot_access_settings(
user_id=update.effective_user.id,
*args, **kwargs
)
For the documentation of the arguments, please see
:meth:`telegram.Bot.get_managed_bot_access_settings`.
.. versionadded:: NEXT.VERSION
Returns:
:class:`telegram.BotAccessSettings`: On success, returns the access settings of the bot
managed by the user.
"""
return await self.get_bot().get_managed_bot_access_settings(
user_id=self.id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def set_managed_bot_access_settings(
self,
is_access_restricted: bool,
added_user_ids: Sequence[int] | None = 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: JSONDict | None = None,
) -> bool:
"""
Shortcut for::
await bot.set_managed_bot_access_settings(
user_id=update.effective_user.id,
*args, **kwargs
)
For the documentation of the arguments, please see
:meth:`telegram.Bot.set_managed_bot_access_settings`.
.. versionadded:: NEXT.VERSION
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return await self.get_bot().set_managed_bot_access_settings(
user_id=self.id,
is_access_restricted=is_access_restricted,
added_user_ids=added_user_ids,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def delete_reaction(
self,
chat_id: int | str,
message_id: int,
actor_chat_id: int | None = 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: JSONDict | None = None,
) -> bool:
"""
Shortcut for::
await bot.delete_message_reaction(
user_id=update.effective_user.id,
*args,
**kwargs
)
For the documentation of the arguments, please see
:meth:`telegram.Bot.delete_message_reaction`.
.. versionadded:: NEXT.VERSION
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return await self.get_bot().delete_message_reaction(
user_id=self.id,
chat_id=chat_id,
message_id=message_id,
actor_chat_id=actor_chat_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def get_personal_chat_messages(
self,
limit: int,
*,
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: JSONDict | None = None,
) -> tuple["Message", ...]:
"""
Shortcut for::
await bot.get_user_personal_chat_messages(
user_id=update.effective_user.id,
*args, **kwargs
)
For the documentation of the arguments, please see
:meth:`telegram.Bot.get_user_personal_chat_messages`.
.. versionadded:: NEXT.VERSION
Returns:
tuple[:class:`telegram.Message`]: On success, a tuple of messages from the personal
channel chat is returned.
"""
return await self.get_bot().get_user_personal_chat_messages(
user_id=self.id,
limit=limit,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
async def delete_all_reactions(
self,
chat_id: int | str,
actor_chat_id: int | None = 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: JSONDict | None = None,
) -> bool:
"""
Shortcut for::
await bot.delete_all_message_reactions(
user_id=update.effective_user.id,
*args,
**kwargs
)
For the documentation of the arguments, please see
:meth:`telegram.Bot.delete_all_message_reactions`.
.. versionadded:: NEXT.VERSION
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
"""
return await self.get_bot().delete_all_message_reactions(
chat_id=chat_id,
user_id=self.id,
actor_chat_id=actor_chat_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
+4 -2
View File
@@ -63,7 +63,8 @@ class WebhookInfo(TelegramObject):
connections to the webhook for update delivery.
allowed_updates (Sequence[:obj:`str`], optional): A sequence of update types the bot is
subscribed to. Defaults to all update types, except
:attr:`telegram.Update.chat_member`.
:attr:`~telegram.Update.chat_member`, :attr:`~telegram.Update.message_reaction`,
and :attr:`~telegram.Update.message_reaction_count`.
.. versionchanged:: 20.0
|sequenceclassargs|
@@ -93,7 +94,8 @@ class WebhookInfo(TelegramObject):
connections to the webhook for update delivery.
allowed_updates (tuple[:obj:`str`]): Optional. A tuple of update types the bot is
subscribed to. Defaults to all update types, except
:attr:`telegram.Update.chat_member`.
:attr:`~telegram.Update.chat_member`, :attr:`~telegram.Update.message_reaction`,
and :attr:`~telegram.Update.message_reaction_count`.
.. versionchanged:: 20.0
+126 -4
View File
@@ -45,6 +45,7 @@ __all__ = [
"BackgroundFillType",
"BackgroundTypeLimit",
"BackgroundTypeType",
"BaseInputMediaType",
"BotCommandLimit",
"BotCommandScopeType",
"BotDescriptionLimit",
@@ -87,6 +88,7 @@ __all__ = [
"KeyboardButtonRequestUsersLimit",
"KeyboardButtonStyle",
"LocationLimit",
"ManagedBotAccessLimit",
"MaskPosition",
"MediaGroupLimit",
"MenuButtonType",
@@ -101,6 +103,7 @@ __all__ = [
"OwnedGiftType",
"PaidMediaType",
"ParseMode",
"PersonalChatMessagesLimit",
"PollLimit",
"PollType",
"PollingLimit",
@@ -181,7 +184,7 @@ class _AccentColor(NamedTuple):
#: :data:`telegram.__bot_api_version_info__`.
#:
#: .. versionadded:: 20.0
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=9, minor=6)
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=10, minor=0)
#: :obj:`str`: Telegram Bot API
#: version supported by this version of `python-telegram-bot`. Also available as
#: :data:`telegram.__bot_api_version__`.
@@ -1519,10 +1522,43 @@ class InputChecklistLimit(IntEnum):
"""
class BaseInputMediaType(StringEnum):
"""This enum contains the available types of :class:`telegram.InputMedia`,
:class:`telegram.InputPollMedia` and :class:`telegram.InputPollOptionMedia`. The enum
members of this enumeration are instances of :class:`str` and can be treated as such.
.. versionadded:: NEXT.VERSION
"""
__slots__ = ()
ANIMATION = "animation"
""":obj:`str`: Type of :class:`telegram.InputMediaAnimation`."""
DOCUMENT = "document"
""":obj:`str`: Type of :class:`telegram.InputMediaDocument`."""
AUDIO = "audio"
""":obj:`str`: Type of :class:`telegram.InputMediaAudio`."""
PHOTO = "photo"
""":obj:`str`: Type of :class:`telegram.InputMediaPhoto`."""
VIDEO = "video"
""":obj:`str`: Type of :class:`telegram.InputMediaVideo`."""
LOCATION = "location"
""":obj:`str`: Type of :class:`telegram.InputMediaLocation`."""
STICKER = "sticker"
""":obj:`str`: Type of :class:`telegram.InputMediaSticker`."""
VENUE = "venue"
""":obj:`str`: Type of :class:`telegram.InputMediaVenue`."""
LIVE_PHOTO = "live_photo"
""":obj:`str`: Type of :class:`telegram.InputMediaLivePhoto`."""
class InputMediaType(StringEnum):
"""This enum contains the available types of :class:`telegram.InputMedia`. The enum
members of this enumeration are instances of :class:`str` and can be treated as such.
.. deprecated:: NEXT.VERSION
Use :class:`telegram.constants.BaseInputMediaType` instead.
.. versionadded:: 20.0
"""
@@ -1538,6 +1574,11 @@ class InputMediaType(StringEnum):
""":obj:`str`: Type of :class:`telegram.InputMediaPhoto`."""
VIDEO = "video"
""":obj:`str`: Type of :class:`telegram.InputMediaVideo`."""
LIVE_PHOTO = "live_photo"
""":obj:`str`: Type of :class:`telegram.InputMediaLivePhoto`.
.. versionadded:: NEXT.VERSION
"""
class InputPaidMediaType(StringEnum):
@@ -1550,9 +1591,14 @@ class InputPaidMediaType(StringEnum):
__slots__ = ()
PHOTO = "photo"
""":obj:`str`: Type of :class:`telegram.InputMediaPhoto`."""
""":obj:`str`: Type of :class:`telegram.InputPaidMediaPhoto`."""
VIDEO = "video"
""":obj:`str`: Type of :class:`telegram.InputMediaVideo`."""
""":obj:`str`: Type of :class:`telegram.InputPaidMediaVideo`."""
LIVE_PHOTO = "live_photo"
""":obj:`str`: Type of :class:`telegram.InputPaidMediaLivePhoto`.
.. versionadded:: NEXT.VERSION
"""
class InputProfilePhotoType(StringEnum):
@@ -1784,6 +1830,8 @@ class LocationLimit(IntEnum):
:meth:`telegram.Bot.edit_message_live_location`
* :paramref:`~telegram.Bot.send_location.horizontal_accuracy` parameter of
:meth:`telegram.Bot.send_location`
* :paramref:`~telegram.InputMediaLocation.horizontal_accuracy` parameter of
:class:`telegram.InputMediaLocation`
"""
MIN_HEADING = 1
@@ -1963,6 +2011,11 @@ class MessageAttachmentType(StringEnum):
""":obj:`str`: Messages with :attr:`telegram.Message.game`."""
INVOICE = "invoice"
""":obj:`str`: Messages with :attr:`telegram.Message.invoice`."""
LIVE_PHOTO = "live_photo"
""":obj:`str`: Messages with :attr:`telegram.Message.live_photo`.
.. versionadded:: NEXT.VERSION
"""
LOCATION = "location"
""":obj:`str`: Messages with :attr:`telegram.Message.location`."""
PAID_MEDIA = "paid_media"
@@ -2345,6 +2398,11 @@ class MessageType(StringEnum):
""":obj:`str`: Messages with :attr:`telegram.Message.invoice`."""
LEFT_CHAT_MEMBER = "left_chat_member"
""":obj:`str`: Messages with :attr:`telegram.Message.left_chat_member`."""
LIVE_PHOTO = "live_photo"
""":obj:`str`: Messages with :attr:`telegram.Message.live_photo`.
.. versionadded:: NEXT.VERSION
"""
LOCATION = "location"
""":obj:`str`: Messages with :attr:`telegram.Message.location`."""
MANAGED_BOT_CREATED = "managed_bot_created"
@@ -2566,6 +2624,33 @@ class PaidMediaType(StringEnum):
""":obj:`str`: The type of :class:`telegram.PaidMediaVideo`."""
PHOTO = "photo"
""":obj:`str`: The type of :class:`telegram.PaidMediaPhoto`."""
LIVE_PHOTO = "live_photo"
""":obj:`str`: The type of :class:`telegram.PaidMediaLivePhoto`
.. versionadded:: NEXT.VERSION
"""
class PersonalChatMessagesLimit(IntEnum):
"""This enum contains limitations for
:paramref:`telegram.Bot.get_user_personal_chat_messages.limit`.
The enum members of this enumeration are instances of :class:`int` and can be treated as such.
.. versionadded:: NEXT.VERSION
"""
__slots__ = ()
MIN_LIMIT = 1
""":obj:`int`: Minimum value allowed for the
:paramref:`~telegram.Bot.get_user_personal_chat_messages.limit`
parameter of :meth:`telegram.Bot.get_user_personal_chat_messages`.
"""
MAX_LIMIT = 20
""":obj:`int`: Maximum value allowed for the
:paramref:`~telegram.Bot.get_user_personal_chat_messages.limit`
parameter of :meth:`telegram.Bot.get_user_personal_chat_messages`.
"""
class PollingLimit(IntEnum):
@@ -3422,10 +3507,13 @@ class PollLimit(IntEnum):
to the :paramref:`~telegram.Bot.send_poll.options` parameter of
:meth:`telegram.Bot.send_poll`.
"""
MIN_OPTION_NUMBER = 2
MIN_OPTION_NUMBER = 1
""":obj:`int`: Minimum number of strings passed in a :obj:`list`
to the :paramref:`~telegram.Bot.send_poll.options` parameter of
:meth:`telegram.Bot.send_poll`.
.. versionchanged:: NEXT.VERSION
Bot API 10.0 decreased this value from ``2`` to ``1``.
"""
MAX_OPTION_NUMBER = 12
""":obj:`int`: Maximum number of strings passed in a :obj:`list`
@@ -3466,6 +3554,19 @@ class PollLimit(IntEnum):
.. versionadded:: NEXT.VERSION
"""
MIN_MEMBERSHIP_HOURS = 24
""":obj:`int`: Minimum number of hours a user must have been a member of the chat
before they can vote in a members-only poll.
.. versionadded:: NEXT.VERSION
"""
MAX_COUNTRY_CODES = 12
""":obj:`int`: Maximum number of two-letter ``ISO 3166-1 alpha-2`` country codes passed in a
:obj:`list` to the :paramref:`~telegram.Bot.send_poll.country_codes` parameter of
:meth:`telegram.Bot.send_poll`.
.. versionadded:: NEXT.VERSION
"""
class PollType(StringEnum):
@@ -3620,6 +3721,11 @@ class UpdateType(StringEnum):
.. versionadded:: NEXT.VERSION
"""
GUEST_MESSAGE = "guest_message"
""":obj:`str`: Updates with :attr:`telegram.Update.guest_message`.
.. versionadded:: NEXT.VERSION
"""
class InvoiceLimit(IntEnum):
@@ -4039,6 +4145,22 @@ class ReactionEmoji(StringEnum):
""":obj:`str`: Pouting face"""
class ManagedBotAccessLimit(IntEnum):
"""This enum contains limitations for :meth:`~telegram.Bot.set_managed_bot_access_settings`.
The enum members of this enumeration are instances of :class:`int` and can be treated as such.
.. versionadded:: NEXT.VERSION
"""
__slots__ = ()
MAX_ALLOWED_USERS = 10
""":obj:`int`: Maximum number of users that can be allowed to access a managed bot in the
:paramref:`~telegram.Bot.set_managed_bot_access_settings.added_user_ids` parameter of
:meth:`~telegram.Bot.set_managed_bot_access_settings`.
"""
class VerifyLimit(IntEnum):
"""This enum contains limitations for :meth:`~telegram.Bot.verify_chat` and
:meth:`~telegram.Bot.verify_user`.
+229 -6
View File
@@ -38,6 +38,7 @@ from telegram import (
Animation,
Audio,
Bot,
BotAccessSettings,
BotCommand,
BotCommandScope,
BotDescription,
@@ -76,6 +77,7 @@ from telegram import (
PreparedKeyboardButton,
ReactionType,
ReplyParameters,
SentGuestMessage,
SentWebAppMessage,
StarAmount,
StarTransactions,
@@ -119,11 +121,14 @@ if TYPE_CHECKING:
InlineQueryResult,
InputMediaAudio,
InputMediaDocument,
InputMediaLivePhoto,
InputMediaPhoto,
InputMediaVideo,
InputPollMedia,
InputSticker,
InputStoryContent,
LabeledPrice,
LivePhoto,
Location,
MessageEntity,
PassportElementError,
@@ -1119,6 +1124,28 @@ class ExtBot(Bot, Generic[RLARGS]):
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
async def answer_guest_query(
self,
guest_query_id: str,
result: "InlineQueryResult",
*,
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: JSONDict | None = None,
rate_limit_args: RLARGS | None = None,
) -> SentGuestMessage:
return await super().answer_guest_query(
guest_query_id=guest_query_id,
result=result,
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 approve_chat_join_request(
self,
chat_id: str | int,
@@ -1844,6 +1871,7 @@ class ExtBot(Bot, Generic[RLARGS]):
async def get_chat_administrators(
self,
chat_id: str | int,
return_bots: bool | None = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1854,6 +1882,7 @@ class ExtBot(Bot, Generic[RLARGS]):
) -> tuple[ChatMember, ...]:
return await super().get_chat_administrators(
chat_id=chat_id,
return_bots=return_bots,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -2689,7 +2718,7 @@ class ExtBot(Bot, Generic[RLARGS]):
async def send_checklist(
self,
business_connection_id: str,
chat_id: int,
chat_id: int | str,
checklist: InputChecklist,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
@@ -2727,7 +2756,7 @@ class ExtBot(Bot, Generic[RLARGS]):
async def edit_message_checklist(
self,
business_connection_id: str,
chat_id: int,
chat_id: int | str,
message_id: int,
checklist: InputChecklist,
reply_markup: "InlineKeyboardMarkup | None" = None,
@@ -2858,7 +2887,7 @@ class ExtBot(Bot, Generic[RLARGS]):
async def send_game(
self,
chat_id: int,
chat_id: int | str,
game_short_name: str,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
reply_markup: "InlineKeyboardMarkup | None" = None,
@@ -3044,7 +3073,7 @@ class ExtBot(Bot, Generic[RLARGS]):
self,
chat_id: int | str,
media: Sequence[
"InputMediaAudio | InputMediaDocument | InputMediaPhoto | InputMediaVideo"
"InputMediaAudio | InputMediaDocument | InputMediaPhoto | InputMediaVideo | InputMediaLivePhoto" # noqa: E501 # pylint: disable=line-too-long
],
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
@@ -3148,7 +3177,7 @@ class ExtBot(Bot, Generic[RLARGS]):
self,
chat_id: int,
draft_id: int,
text: str,
text: str | None = None,
message_thread_id: int | None = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
entities: Sequence["MessageEntity"] | None = None,
@@ -3262,9 +3291,13 @@ class ExtBot(Bot, Generic[RLARGS]):
hide_results_until_closes: bool | None = None,
correct_option_ids: CorrectOptionIds | None = None,
description: str | None = None,
description_parse_mode: str | None = None,
description_parse_mode: ODVInput[str] | None = None,
description_entities: Sequence["MessageEntity"] | None = None,
shuffle_options: bool | None = None,
members_only: bool | None = None,
country_codes: Sequence[str] | None = None,
explanation_media: "InputPollMedia | None" = None,
media: "InputPollMedia | None" = None,
*,
reply_to_message_id: int | None = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -3314,6 +3347,10 @@ class ExtBot(Bot, Generic[RLARGS]):
description_entities=description_entities,
hide_results_until_closes=hide_results_until_closes,
allow_adding_options=allow_adding_options,
members_only=members_only,
country_codes=country_codes,
explanation_media=explanation_media,
media=media,
)
async def send_sticker(
@@ -5032,6 +5069,74 @@ class ExtBot(Bot, Generic[RLARGS]):
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
async def get_managed_bot_access_settings(
self,
user_id: int,
*,
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: JSONDict | None = None,
rate_limit_args: RLARGS | None = None,
) -> BotAccessSettings:
return await super().get_managed_bot_access_settings(
user_id=user_id,
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_managed_bot_access_settings(
self,
user_id: int,
is_access_restricted: bool,
added_user_ids: Sequence[int] | None = 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: JSONDict | None = None,
rate_limit_args: RLARGS | None = None,
) -> bool:
return await super().set_managed_bot_access_settings(
user_id=user_id,
is_access_restricted=is_access_restricted,
added_user_ids=added_user_ids,
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 get_user_personal_chat_messages(
self,
user_id: int,
limit: int,
*,
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: JSONDict | None = None,
rate_limit_args: RLARGS | None = None,
) -> tuple[Message, ...]:
return await super().get_user_personal_chat_messages(
user_id=user_id,
limit=limit,
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: str | int,
@@ -5593,6 +5698,117 @@ class ExtBot(Bot, Generic[RLARGS]):
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
async def send_live_photo(
self,
chat_id: int | str,
live_photo: "FileInput | LivePhoto",
photo: "FileInput | PhotoSize",
business_connection_id: str | None = None,
message_thread_id: int | None = None,
direct_messages_topic_id: int | None = None,
caption: str | None = None,
parse_mode: ODVInput[str] = DEFAULT_NONE,
caption_entities: Sequence["MessageEntity"] | None = None,
show_caption_above_media: bool | None = None,
has_spoiler: bool | None = None,
disable_notification: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
allow_paid_broadcast: bool | None = None,
message_effect_id: str | None = None,
suggested_post_parameters: "SuggestedPostParameters | None" = None,
reply_parameters: "ReplyParameters | None" = None,
reply_markup: "ReplyMarkup | None" = None,
*,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_to_message_id: int | None = None,
filename: str | None = 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: JSONDict | None = None,
rate_limit_args: RLARGS | None = None,
) -> Message:
return await super().send_live_photo(
chat_id=chat_id,
live_photo=live_photo,
photo=photo,
business_connection_id=business_connection_id,
message_thread_id=message_thread_id,
direct_messages_topic_id=direct_messages_topic_id,
caption=caption,
parse_mode=parse_mode,
caption_entities=caption_entities,
show_caption_above_media=show_caption_above_media,
has_spoiler=has_spoiler,
disable_notification=disable_notification,
protect_content=protect_content,
allow_paid_broadcast=allow_paid_broadcast,
message_effect_id=message_effect_id,
suggested_post_parameters=suggested_post_parameters,
reply_parameters=reply_parameters,
allow_sending_without_reply=allow_sending_without_reply,
reply_to_message_id=reply_to_message_id,
reply_markup=reply_markup,
filename=filename,
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 delete_message_reaction(
self,
chat_id: int | str,
message_id: int,
user_id: int | None = None,
actor_chat_id: int | None = 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: JSONDict | None = None,
rate_limit_args: RLARGS | None = None,
) -> bool:
return await super().delete_message_reaction(
chat_id=chat_id,
message_id=message_id,
user_id=user_id,
actor_chat_id=actor_chat_id,
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 delete_all_message_reactions(
self,
chat_id: int | str,
user_id: int | None = None,
actor_chat_id: int | None = 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: JSONDict | None = None,
rate_limit_args: RLARGS | None = None,
) -> bool:
return await super().delete_all_message_reactions(
chat_id=chat_id,
user_id=user_id,
actor_chat_id=actor_chat_id,
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
@@ -5647,6 +5863,7 @@ class ExtBot(Bot, Generic[RLARGS]):
answerShippingQuery = answer_shipping_query
answerPreCheckoutQuery = answer_pre_checkout_query
answerWebAppQuery = answer_web_app_query
answerGuestQuery = answer_guest_query
restrictChatMember = restrict_chat_member
promoteChatMember = promote_chat_member
setChatPermissions = set_chat_permissions
@@ -5762,3 +5979,9 @@ class ExtBot(Bot, Generic[RLARGS]):
getManagedBotToken = get_managed_bot_token
replaceManagedBotToken = replace_managed_bot_token
savePreparedKeyboardButton = save_prepared_keyboard_button
sendLivePhoto = send_live_photo
getManagedBotAccessSettings = get_managed_bot_access_settings
setManagedBotAccessSettings = set_managed_bot_access_settings
getUserPersonalChatMessages = get_user_personal_chat_messages
deleteMessageReaction = delete_message_reaction
deleteAllMessageReactions = delete_all_message_reactions
+34 -1
View File
@@ -62,6 +62,7 @@ __all__ = (
"IS_AUTOMATIC_FORWARD",
"IS_FROM_OFFLINE",
"IS_TOPIC_MESSAGE",
"LIVE_PHOTO",
"LOCATION",
"PAID_MEDIA",
"PASSPORT_DATA",
@@ -273,6 +274,10 @@ class BaseFilter:
:attr:`~telegram.Update.business_message`
or :attr:`~telegram.Update.edited_business_message`.
.. versionchanged:: NEXT.VERSION
This filter now also returns :obj:`True` if the update contains
:attr:`~telegram.Update.guest_message`.
Args:
update (:class:`telegram.Update`): The update to check.
@@ -281,7 +286,8 @@ class BaseFilter:
:attr:`~telegram.Update.channel_post`, :attr:`~telegram.Update.message`,
:attr:`~telegram.Update.edited_channel_post`,
:attr:`~telegram.Update.edited_message`, :attr:`telegram.Update.business_message`,
:attr:`telegram.Update.edited_business_message`, or :obj:`False` otherwise.
:attr:`telegram.Update.edited_business_message`,
:attr:`telegram.Update.guest_message`, or :obj:`False` otherwise.
"""
return bool( # Only message updates should be handled.
update.channel_post
@@ -290,6 +296,7 @@ class BaseFilter:
or update.edited_message
or update.business_message
or update.edited_business_message
or update.guest_message
)
@@ -1651,6 +1658,20 @@ class Language(MessageFilter):
)
class _LivePhoto(MessageFilter):
__slots__ = ()
def filter(self, message: Message) -> bool:
return bool(message.live_photo)
LIVE_PHOTO = _LivePhoto(name="filters.LIVE_PHOTO")
"""Messages that contain :attr:`telegram.Message.live_photo`.
.. versionadded:: NEXT.VERSION
"""
class _Location(MessageFilter):
__slots__ = ()
@@ -2894,6 +2915,18 @@ class UpdateType:
.. versionadded:: 21.1
"""
class _GuestMessage(UpdateFilter):
__slots__ = ()
def filter(self, update: Update) -> bool:
return update.guest_message is not None
GUEST_MESSAGE = _GuestMessage(name="filters.UpdateType.GUEST_MESSAGE")
"""Updates with :attr:`telegram.Update.guest_message`.
.. versionadded:: NEXT.VERSION
"""
class User(_ChatUserBaseFilter):
"""Filters messages to allow only those which are from specified user ID(s) or
+16
View File
@@ -145,3 +145,19 @@ def video_sticker_file():
def video_sticker(bot, chat_id):
with data_file("telegram_video_sticker.webm").open("rb") as f:
return bot.send_sticker(chat_id, sticker=f, timeout=50).sticker
@pytest.fixture(scope="session")
async def real_live_photo(bot, chat_id):
with (
data_file("telegram.jpg").open("rb") as photo,
data_file("telegram.mp4").open("rb") as video,
):
return (
await bot.send_live_photo(
chat_id,
live_photo=video,
photo=photo,
read_timeout=50,
)
).live_photo
+743 -22
View File
@@ -29,15 +29,22 @@ from telegram import (
InputMediaAnimation,
InputMediaAudio,
InputMediaDocument,
InputMediaLivePhoto,
InputMediaLocation,
InputMediaPhoto,
InputMediaSticker,
InputMediaVenue,
InputMediaVideo,
InputPaidMediaLivePhoto,
InputPaidMediaPhoto,
InputPaidMediaVideo,
InputPollMedia,
InputPollOptionMedia,
Message,
MessageEntity,
ReplyParameters,
)
from telegram.constants import InputMediaType, ParseMode
from telegram.constants import BaseInputMediaType, ParseMode
from telegram.error import BadRequest
from telegram.request import RequestData
from telegram.warnings import PTBDeprecationWarning
@@ -121,6 +128,37 @@ def input_media_document(class_thumb_file):
)
@pytest.fixture(scope="module")
def input_media_location():
return InputMediaLocation(
latitude=InputMediaLocationTestBase.latitude,
longitude=InputMediaLocationTestBase.longitude,
horizontal_accuracy=InputMediaLocationTestBase.horizontal_accuracy,
)
@pytest.fixture(scope="module")
def input_media_venue():
return InputMediaVenue(
latitude=InputMediaVenueTestBase.latitude,
longitude=InputMediaVenueTestBase.longitude,
title=InputMediaVenueTestBase.title,
address=InputMediaVenueTestBase.address,
foursquare_id=InputMediaVenueTestBase.foursquare_id,
foursquare_type=InputMediaVenueTestBase.foursquare_type,
google_place_id=InputMediaVenueTestBase.google_place_id,
google_place_type=InputMediaVenueTestBase.google_place_type,
)
@pytest.fixture(scope="module")
def input_media_sticker():
return InputMediaSticker(
media=InputMediaStickerTestBase.media,
emoji=InputMediaStickerTestBase.emoji,
)
@pytest.fixture(scope="module")
def input_paid_media_photo():
return InputPaidMediaPhoto(
@@ -142,6 +180,27 @@ def input_paid_media_video(class_thumb_file):
)
@pytest.fixture(scope="module")
def input_media_live_photo():
return InputMediaLivePhoto(
media=InputMediaLivePhotoTestBase.media,
photo=InputMediaLivePhotoTestBase.photo,
caption=InputMediaLivePhotoTestBase.caption,
parse_mode=InputMediaLivePhotoTestBase.parse_mode,
caption_entities=InputMediaLivePhotoTestBase.caption_entities,
show_caption_above_media=InputMediaLivePhotoTestBase.show_caption_above_media,
has_spoiler=InputMediaLivePhotoTestBase.has_spoiler,
)
@pytest.fixture(scope="module")
def input_paid_media_live_photo():
return InputPaidMediaLivePhoto(
media=InputMediaLivePhotoTestBase.media,
photo=InputMediaLivePhotoTestBase.photo,
)
class InputMediaVideoTestBase:
type_ = "video"
media = "NOTAREALFILEID"
@@ -157,6 +216,21 @@ class InputMediaVideoTestBase:
show_caption_above_media = True
class TestInputMediaWithoutRequest:
def test_type_enum_conversion(self):
assert type(InputMedia(media_type="video", media="media").type) is BaseInputMediaType
assert InputMedia(media_type="unknown", media="media").type == "unknown"
def test_to_dict(self):
assert InputMedia(
media_type="video",
media="media",
).to_dict() == {
"type": BaseInputMediaType.VIDEO,
"media": "media",
}
class TestInputMediaVideoWithoutRequest(InputMediaVideoTestBase):
def test_slot_behaviour(self, input_media_video):
inst = input_media_video
@@ -180,6 +254,10 @@ class TestInputMediaVideoWithoutRequest(InputMediaVideoTestBase):
assert input_media_video.has_spoiler == self.has_spoiler
assert input_media_video.show_caption_above_media == self.show_caption_above_media
assert isinstance(input_media_video, InputMedia)
assert isinstance(input_media_video, InputPollMedia)
assert isinstance(input_media_video, InputPollOptionMedia)
def test_caption_entities_always_tuple(self):
input_media_video = InputMediaVideo(self.media)
assert input_media_video.caption_entities == ()
@@ -253,25 +331,91 @@ class TestInputMediaVideoWithoutRequest(InputMediaVideoTestBase):
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
# conversion only here. It is independent of the specific class
assert (
type(
InputMedia(
media_type="animation",
media="media",
).type
def test_effective_filename(self, video_file):
inst = InputMediaVideo(
video_file,
"caption",
24,
24,
10,
True,
"parse_mode",
[],
"pos_filename_depr",
)
assert inst.media.filename == "pos_filename_depr"
inst = InputMediaVideo(
video_file,
filename="kw_only_filename",
)
assert inst.media.filename == "kw_only_filename"
# Deprecated, but for completeness
inst = InputMediaVideo(
video_file,
filename_depr="kw_filename_depr",
)
assert inst.media.filename == "kw_filename_depr"
def test_filename_depr_mutually_exclusive_filename(self, video_file):
with pytest.raises(
ValueError, match="`filename_depr` and `filename` are mutually exclusive"
):
InputMediaVideo(
video_file,
"caption",
24,
24,
10,
True,
"parse_mode",
[],
"pos_filename_depr",
filename="kw_filename",
)
is InputMediaType
)
assert (
InputMedia(
media_type="unknown",
media="media",
).type
== "unknown"
)
with pytest.raises(
ValueError, match="`filename_depr` and `filename` are mutually exclusive"
):
InputMediaVideo(
video_file,
filename_depr="filename_depr",
filename="kw_filename",
)
def test_positional_filename_deprecated(self, video_file):
with pytest.warns(
PTBDeprecationWarning,
match="Positional.*`filename`.*keyword.*`filename_depr`.*deprecated",
) as record:
InputMediaVideo(
video_file,
"caption",
24,
24,
10,
True,
"parse_mode",
[],
"pos_filename_depr",
)
assert record[0].category == PTBDeprecationWarning
assert record[0].filename == __file__, "wrong stacklevel!"
def test_keyword_filename_depr_deprecated(self, video_file):
with pytest.warns(
PTBDeprecationWarning,
match="Positional.*`filename`.*keyword.*`filename_depr`.*deprecated",
) as record:
InputMediaVideo(
video_file,
filename_depr="filename_depr",
)
assert record[0].category == PTBDeprecationWarning
assert record[0].filename == __file__, "wrong stacklevel!"
class InputMediaPhotoTestBase:
@@ -300,6 +444,10 @@ class TestInputMediaPhotoWithoutRequest(InputMediaPhotoTestBase):
assert input_media_photo.has_spoiler == self.has_spoiler
assert input_media_photo.show_caption_above_media == self.show_caption_above_media
assert isinstance(input_media_photo, InputMedia)
assert isinstance(input_media_photo, InputPollMedia)
assert isinstance(input_media_photo, InputPollOptionMedia)
def test_caption_entities_always_tuple(self):
input_media_photo = InputMediaPhoto(self.media)
assert input_media_photo.caption_entities == ()
@@ -337,6 +485,80 @@ class TestInputMediaPhotoWithoutRequest(InputMediaPhotoTestBase):
input_media_photo = InputMediaPhoto(data_file("telegram.mp4"))
assert input_media_photo.media == data_file("telegram.mp4").as_uri()
def test_effective_filename(self, photo_file):
inst = InputMediaPhoto(
photo_file,
"caption",
"parse_mode",
[],
"pos_filename_depr",
)
assert inst.media.filename == "pos_filename_depr"
inst = InputMediaPhoto(
photo_file,
filename="kw_only_filename",
)
assert inst.media.filename == "kw_only_filename"
# Deprecated, but for completeness
inst = InputMediaPhoto(
photo_file,
filename_depr="kw_filename_depr",
)
assert inst.media.filename == "kw_filename_depr"
def test_filename_depr_mutually_exclusive_filename(self, photo_file):
with pytest.raises(
ValueError, match="`filename_depr` and `filename` are mutually exclusive"
):
InputMediaPhoto(
photo_file,
"caption",
"parse_mode",
[],
"filename_depr",
filename="kw_filename",
)
with pytest.raises(
ValueError, match="`filename_depr` and `filename` are mutually exclusive"
):
InputMediaPhoto(
photo_file,
filename_depr="filename_depr",
filename="kw_filename",
)
def test_positional_filename_deprecated(self, photo_file):
with pytest.warns(
PTBDeprecationWarning,
match="Positional.*`filename`.*keyword.*`filename_depr`.*deprecated",
) as record:
InputMediaPhoto(
photo_file,
"caption",
"parse_mode",
[],
"filename_depr",
)
assert record[0].category == PTBDeprecationWarning
assert record[0].filename == __file__, "wrong stacklevel!"
def test_keyword_filename_depr_deprecated(self, photo_file):
with pytest.warns(
PTBDeprecationWarning,
match="Positional.*`filename`.*keyword.*`filename_depr`.*deprecated",
) as record:
InputMediaPhoto(
photo_file,
filename_depr="filename_depr",
)
assert record[0].category == PTBDeprecationWarning
assert record[0].filename == __file__, "wrong stacklevel!"
class InputMediaAnimationTestBase:
type_ = "animation"
@@ -369,6 +591,10 @@ class TestInputMediaAnimationWithoutRequest(InputMediaAnimationTestBase):
assert input_media_animation.show_caption_above_media == self.show_caption_above_media
assert input_media_animation._duration == self.duration
assert isinstance(input_media_animation, InputMedia)
assert isinstance(input_media_animation, InputPollMedia)
assert isinstance(input_media_animation, InputPollOptionMedia)
def test_caption_entities_always_tuple(self):
input_media_animation = InputMediaAnimation(self.media)
assert input_media_animation.caption_entities == ()
@@ -433,6 +659,89 @@ class TestInputMediaAnimationWithoutRequest(InputMediaAnimationTestBase):
assert input_media_animation.media == data_file("telegram.mp4").as_uri()
assert input_media_animation.thumbnail == data_file("telegram.jpg").as_uri()
def test_effective_filename(self, animation_file):
inst = InputMediaAnimation(
animation_file,
"caption",
"parse_mode",
24,
24,
10,
[],
"pos_filename_depr",
)
assert inst.media.filename == "pos_filename_depr"
inst = InputMediaAnimation(
animation_file,
filename="kw_only_filename",
)
assert inst.media.filename == "kw_only_filename"
# Deprecated, but for completeness
inst = InputMediaAnimation(
animation_file,
filename_depr="kw_filename_depr",
)
assert inst.media.filename == "kw_filename_depr"
def test_filename_depr_mutually_exclusive_filename(self, animation_file):
with pytest.raises(
ValueError, match="`filename_depr` and `filename` are mutually exclusive"
):
InputMediaAnimation(
animation_file,
"caption",
"parse_mode",
24,
24,
10,
[],
"pos_filename_depr",
filename="kw_filename",
)
with pytest.raises(
ValueError, match="`filename_depr` and `filename` are mutually exclusive"
):
InputMediaAnimation(
animation_file,
filename_depr="filename_depr",
filename="kw_filename",
)
def test_positional_filename_deprecated(self, animation_file):
with pytest.warns(
PTBDeprecationWarning,
match="Positional.*`filename`.*keyword.*`filename_depr`.*deprecated",
) as record:
InputMediaAnimation(
animation_file,
"caption",
"parse_mode",
24,
24,
10,
[],
"pos_filename_depr",
)
assert record[0].category == PTBDeprecationWarning
assert record[0].filename == __file__, "wrong stacklevel!"
def test_keyword_filename_depr_deprecated(self, animation_file):
with pytest.warns(
PTBDeprecationWarning,
match="Positional.*`filename`.*keyword.*`filename_depr`.*deprecated",
) as record:
InputMediaAnimation(
animation_file,
filename_depr="filename_depr",
)
assert record[0].category == PTBDeprecationWarning
assert record[0].filename == __file__, "wrong stacklevel!"
class InputMediaAudioTestBase:
type_ = "audio"
@@ -463,6 +772,10 @@ class TestInputMediaAudioWithoutRequest(InputMediaAudioTestBase):
assert input_media_audio.caption_entities == tuple(self.caption_entities)
assert isinstance(input_media_audio.thumbnail, InputFile)
assert isinstance(input_media_audio, InputMedia)
assert isinstance(input_media_audio, InputPollMedia)
assert not isinstance(input_media_audio, InputPollOptionMedia)
def test_caption_entities_always_tuple(self):
input_media_audio = InputMediaAudio(self.media)
assert input_media_audio.caption_entities == ()
@@ -526,6 +839,89 @@ class TestInputMediaAudioWithoutRequest(InputMediaAudioTestBase):
assert input_media_audio.media == data_file("telegram.mp4").as_uri()
assert input_media_audio.thumbnail == data_file("telegram.jpg").as_uri()
def test_effective_filename(self, audio_file):
inst = InputMediaAudio(
audio_file,
"caption",
"parse_mode",
10,
"performer",
"title",
[],
"pos_filename_depr",
)
assert inst.media.filename == "pos_filename_depr"
inst = InputMediaAudio(
audio_file,
filename="kw_only_filename",
)
assert inst.media.filename == "kw_only_filename"
# Deprecated, but for completeness
inst = InputMediaAudio(
audio_file,
filename_depr="kw_filename_depr",
)
assert inst.media.filename == "kw_filename_depr"
def test_filename_depr_mutually_exclusive_filename(self, audio_file):
with pytest.raises(
ValueError, match="`filename_depr` and `filename` are mutually exclusive"
):
InputMediaAudio(
audio_file,
"caption",
"parse_mode",
10,
"performer",
"title",
[],
"pos_filename_depr",
filename="kw_filename",
)
with pytest.raises(
ValueError, match="`filename_depr` and `filename` are mutually exclusive"
):
InputMediaAudio(
audio_file,
filename_depr="filename_depr",
filename="kw_filename",
)
def test_positional_filename_deprecated(self, audio_file):
with pytest.warns(
PTBDeprecationWarning,
match="Positional.*`filename`.*keyword.*`filename_depr`.*deprecated",
) as record:
InputMediaAudio(
audio_file,
"caption",
"parse_mode",
10,
"performer",
"title",
[],
"pos_filename_depr",
)
assert record[0].category == PTBDeprecationWarning
assert record[0].filename == __file__, "wrong stacklevel!"
def test_keyword_filename_depr_deprecated(self, audio_file):
with pytest.warns(
PTBDeprecationWarning,
match="Positional.*`filename`.*keyword.*`filename_depr`.*deprecated",
) as record:
InputMediaAudio(
audio_file,
filename_depr="filename_depr",
)
assert record[0].category == PTBDeprecationWarning
assert record[0].filename == __file__, "wrong stacklevel!"
class InputMediaDocumentTestBase:
type_ = "document"
@@ -536,6 +932,136 @@ class InputMediaDocumentTestBase:
disable_content_type_detection = True
class InputMediaLocationTestBase:
type_ = "location"
latitude = 1.0
longitude = 2.0
horizontal_accuracy = 10.0
class TestInputMediaLocationWithoutRequest(InputMediaLocationTestBase):
def test_slot_behaviour(self, input_media_location):
inst = input_media_location
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, input_media_location):
assert input_media_location.type == self.type_
assert input_media_location.latitude == self.latitude
assert input_media_location.longitude == self.longitude
assert input_media_location.horizontal_accuracy == self.horizontal_accuracy
assert isinstance(input_media_location, InputPollMedia)
assert isinstance(input_media_location, InputPollOptionMedia)
assert not isinstance(input_media_location, InputMedia)
def test_to_dict(self, input_media_location):
input_media_location_dict = input_media_location.to_dict()
assert input_media_location_dict["type"] == input_media_location.type
assert input_media_location_dict["latitude"] == input_media_location.latitude
assert input_media_location_dict["longitude"] == input_media_location.longitude
assert (
input_media_location_dict["horizontal_accuracy"]
== input_media_location.horizontal_accuracy
)
class InputMediaVenueTestBase:
type_ = "venue"
latitude = 1.0
longitude = 2.0
title = "title"
address = "address"
foursquare_id = "foursquare_id"
foursquare_type = "food/icecream"
google_place_id = "google_place_id"
google_place_type = "restaurant"
class TestInputMediaVenueWithoutRequest(InputMediaVenueTestBase):
def test_slot_behaviour(self, input_media_venue):
inst = input_media_venue
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, input_media_venue):
assert input_media_venue.type == self.type_
assert input_media_venue.latitude == self.latitude
assert input_media_venue.longitude == self.longitude
assert input_media_venue.title == self.title
assert input_media_venue.address == self.address
assert input_media_venue.foursquare_id == self.foursquare_id
assert input_media_venue.foursquare_type == self.foursquare_type
assert input_media_venue.google_place_id == self.google_place_id
assert input_media_venue.google_place_type == self.google_place_type
assert isinstance(input_media_venue, InputPollMedia)
assert isinstance(input_media_venue, InputPollOptionMedia)
assert not isinstance(input_media_venue, InputMedia)
def test_to_dict(self, input_media_venue):
input_media_venue_dict = input_media_venue.to_dict()
assert input_media_venue_dict["type"] == input_media_venue.type
assert input_media_venue_dict["latitude"] == input_media_venue.latitude
assert input_media_venue_dict["longitude"] == input_media_venue.longitude
assert input_media_venue_dict["title"] == input_media_venue.title
assert input_media_venue_dict["address"] == input_media_venue.address
assert input_media_venue_dict["foursquare_id"] == input_media_venue.foursquare_id
assert input_media_venue_dict["foursquare_type"] == input_media_venue.foursquare_type
assert input_media_venue_dict["google_place_id"] == input_media_venue.google_place_id
assert input_media_venue_dict["google_place_type"] == input_media_venue.google_place_type
class InputMediaStickerTestBase:
type_ = "sticker"
media = "NOTAREALFILEID"
emoji = "💪"
class TestInputMediaStickerWithoutRequest(InputMediaStickerTestBase):
def test_slot_behaviour(self, input_media_sticker):
inst = input_media_sticker
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, input_media_sticker):
assert input_media_sticker.type == self.type_
assert input_media_sticker.media == self.media
assert input_media_sticker.emoji == self.emoji
assert isinstance(input_media_sticker, InputPollOptionMedia)
assert not isinstance(input_media_sticker, InputPollMedia)
assert not isinstance(input_media_sticker, InputMedia)
def test_to_dict(self, input_media_sticker):
input_media_sticker_dict = input_media_sticker.to_dict()
assert input_media_sticker_dict["type"] == input_media_sticker.type
assert input_media_sticker_dict["media"] == input_media_sticker.media
assert input_media_sticker_dict["emoji"] == input_media_sticker.emoji
def test_with_sticker(self, sticker):
input_media_sticker = InputMediaSticker(sticker, emoji=self.emoji)
assert input_media_sticker.type == self.type_
assert input_media_sticker.media == sticker.file_id
assert input_media_sticker.emoji == self.emoji
def test_with_sticker_file(self, sticker_file):
input_media_sticker = InputMediaSticker(sticker_file, emoji=self.emoji)
assert input_media_sticker.type == self.type_
assert isinstance(input_media_sticker.media, InputFile)
assert input_media_sticker.emoji == self.emoji
def test_with_local_files(self):
input_media_sticker = InputMediaSticker(
data_file("telegram_sticker.png"), emoji=self.emoji
)
assert input_media_sticker.media == data_file("telegram_sticker.png").as_uri()
assert input_media_sticker.emoji == self.emoji
class TestInputMediaDocumentWithoutRequest(InputMediaDocumentTestBase):
def test_slot_behaviour(self, input_media_document):
inst = input_media_document
@@ -555,6 +1081,10 @@ class TestInputMediaDocumentWithoutRequest(InputMediaDocumentTestBase):
)
assert isinstance(input_media_document.thumbnail, InputFile)
assert isinstance(input_media_document, InputMedia)
assert isinstance(input_media_document, InputPollMedia)
assert not isinstance(input_media_document, InputPollOptionMedia)
def test_caption_entities_always_tuple(self):
input_media_document = InputMediaDocument(self.media)
assert input_media_document.caption_entities == ()
@@ -594,6 +1124,83 @@ class TestInputMediaDocumentWithoutRequest(InputMediaDocumentTestBase):
assert input_media_document.media == data_file("telegram.mp4").as_uri()
assert input_media_document.thumbnail == data_file("telegram.jpg").as_uri()
def test_effective_filename(self, document_file):
inst = InputMediaDocument(
document_file,
"caption",
"parse_mode",
True,
[],
"pos_filename_depr",
)
assert inst.media.filename == "pos_filename_depr"
inst = InputMediaDocument(
document_file,
filename="kw_only_filename",
)
assert inst.media.filename == "kw_only_filename"
# Deprecated, but for completeness
inst = InputMediaDocument(
document_file,
filename_depr="kw_filename_depr",
)
assert inst.media.filename == "kw_filename_depr"
def test_filename_depr_mutually_exclusive_filename(self, document_file):
with pytest.raises(
ValueError, match="`filename_depr` and `filename` are mutually exclusive"
):
InputMediaDocument(
document_file,
"caption",
"parse_mode",
True,
[],
"pos_filename_depr",
filename="kw_filename",
)
with pytest.raises(
ValueError, match="`filename_depr` and `filename` are mutually exclusive"
):
InputMediaDocument(
document_file,
filename_depr="filename_depr",
filename="kw_filename",
)
def test_positional_filename_deprecated(self, document_file):
with pytest.warns(
PTBDeprecationWarning,
match="Positional.*`filename`.*keyword.*`filename_depr`.*deprecated",
) as record:
InputMediaDocument(
document_file,
"caption",
"parse_mode",
True,
[],
"pos_filename_depr",
)
assert record[0].category == PTBDeprecationWarning
assert record[0].filename == __file__, "wrong stacklevel!"
def test_keyword_filename_depr_deprecated(self, document_file):
with pytest.warns(
PTBDeprecationWarning,
match="Positional.*`filename`.*keyword.*`filename_depr`.*deprecated",
) as record:
InputMediaDocument(
document_file,
filename_depr="filename_depr",
)
assert record[0].category == PTBDeprecationWarning
assert record[0].filename == __file__, "wrong stacklevel!"
class TestInputPaidMediaPhotoWithoutRequest(InputMediaPhotoTestBase):
def test_slot_behaviour(self, input_paid_media_photo):
@@ -612,13 +1219,13 @@ class TestInputPaidMediaPhotoWithoutRequest(InputMediaPhotoTestBase):
assert input_paid_media_photo_dict["media"] == input_paid_media_photo.media
def test_with_photo(self, photo):
# fixture found in test_photo
# fixture found in conftest.py
input_paid_media_photo = InputPaidMediaPhoto(photo)
assert input_paid_media_photo.type == self.type_
assert input_paid_media_photo.media == photo.file_id
def test_with_photo_file(self, photo_file):
# fixture found in test_photo
# fixture found in conftest.py
input_paid_media_photo = InputPaidMediaPhoto(photo_file)
assert input_paid_media_photo.type == self.type_
assert isinstance(input_paid_media_photo.media, InputFile)
@@ -628,6 +1235,76 @@ class TestInputPaidMediaPhotoWithoutRequest(InputMediaPhotoTestBase):
assert input_paid_media_photo.media == data_file("telegram.jpg").as_uri()
class InputMediaLivePhotoTestBase:
type_ = "live_photo"
media = "NOTAREALFILEID"
photo = "NOTAREALFILEID"
caption = "My Caption"
parse_mode = "Markdown"
caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)]
show_caption_above_media = True
has_spoiler = True
class TestInputMediaLivePhotoWithoutRequest(InputMediaLivePhotoTestBase):
def test_slot_behaviour(self, input_media_live_photo):
inst = input_media_live_photo
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, input_media_live_photo):
assert input_media_live_photo.type == self.type_
assert input_media_live_photo.media == self.media
assert input_media_live_photo.photo == self.photo
assert input_media_live_photo.caption == self.caption
assert input_media_live_photo.parse_mode == self.parse_mode
assert input_media_live_photo.caption_entities == tuple(self.caption_entities)
assert input_media_live_photo.show_caption_above_media == self.show_caption_above_media
assert input_media_live_photo.has_spoiler == self.has_spoiler
def test_caption_entities_always_tuple(self):
input_media_live_photo = InputMediaLivePhoto(self.media, self.photo)
assert input_media_live_photo.caption_entities == ()
def test_to_dict(self, input_media_live_photo):
input_media_live_photo_dict = input_media_live_photo.to_dict()
assert input_media_live_photo_dict["type"] == input_media_live_photo.type
assert input_media_live_photo_dict["media"] == input_media_live_photo.media
assert input_media_live_photo_dict["photo"] == input_media_live_photo.photo
assert input_media_live_photo_dict["caption"] == input_media_live_photo.caption
assert input_media_live_photo_dict["parse_mode"] == input_media_live_photo.parse_mode
assert input_media_live_photo_dict["caption_entities"] == [
ce.to_dict() for ce in input_media_live_photo.caption_entities
]
assert (
input_media_live_photo_dict["show_caption_above_media"]
== input_media_live_photo.show_caption_above_media
)
assert input_media_live_photo_dict["has_spoiler"] == input_media_live_photo.has_spoiler
def test_with_photo_and_video(self, video, photo):
# fixtures found in conftest.py
input_media_live_photo = InputMediaLivePhoto(video, photo)
assert input_media_live_photo.type == self.type_
assert input_media_live_photo.media == video.file_id
assert input_media_live_photo.photo == photo.file_id
def test_with_photo_and_video_files(self, video_file, photo_file):
# fixture found in conftest.py
input_media_live_photo = InputMediaLivePhoto(video_file, photo_file)
assert input_media_live_photo.type == self.type_
assert isinstance(input_media_live_photo.media, InputFile)
assert isinstance(input_media_live_photo.photo, InputFile)
def test_with_local_files(self):
input_media_live_photo = InputMediaLivePhoto(
media=data_file("telegram.mp4"), photo=data_file("telegram.jpg")
)
assert input_media_live_photo.media == data_file("telegram.mp4").as_uri()
assert input_media_live_photo.photo == data_file("telegram.jpg").as_uri()
class TestInputPaidMediaVideoWithoutRequest(InputMediaVideoTestBase):
def test_slot_behaviour(self, input_paid_media_video):
inst = input_paid_media_video
@@ -711,6 +1388,46 @@ class TestInputPaidMediaVideoWithoutRequest(InputMediaVideoTestBase):
assert input_paid_media_video.cover == data_file("telegram.jpg").as_uri()
class TestInputPaidMediaLivePhotoWithoutRequest(InputMediaLivePhotoTestBase):
def test_slot_behaviour(self, input_paid_media_live_photo):
inst = input_paid_media_live_photo
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, input_paid_media_live_photo):
assert input_paid_media_live_photo.type == self.type_
assert input_paid_media_live_photo.media == self.media
assert input_paid_media_live_photo.photo == self.photo
def test_to_dict(self, input_paid_media_live_photo):
input_paid_media_live_photo_dict = input_paid_media_live_photo.to_dict()
assert input_paid_media_live_photo_dict["type"] == input_paid_media_live_photo.type
assert input_paid_media_live_photo_dict["media"] == input_paid_media_live_photo.media
assert input_paid_media_live_photo_dict["photo"] == input_paid_media_live_photo.photo
def test_with_photo(self, video, photo):
# fixtures found in conftest.py
input_paid_media_live_photo = InputPaidMediaLivePhoto(video, photo)
assert input_paid_media_live_photo.type == self.type_
assert input_paid_media_live_photo.media == video.file_id
assert input_paid_media_live_photo.photo == photo.file_id
def test_with_photo_file(self, photo_file):
# fixture found in conftest.py
input_paid_media_live_photo = InputPaidMediaLivePhoto(photo_file, photo_file)
assert input_paid_media_live_photo.type == self.type_
assert isinstance(input_paid_media_live_photo.media, InputFile)
assert isinstance(input_paid_media_live_photo.photo, InputFile)
def test_with_local_files(self):
input_paid_media_live_photo = InputPaidMediaLivePhoto(
media=data_file("telegram.mp4"), photo=data_file("telegram.jpg")
)
assert input_paid_media_live_photo.media == data_file("telegram.mp4").as_uri()
assert input_paid_media_live_photo.photo == data_file("telegram.jpg").as_uri()
@pytest.fixture(scope="module")
def media_group(photo, thumb):
return [
@@ -1155,7 +1872,9 @@ class TestSendMediaGroupWithRequest:
@pytest.mark.parametrize(
"default_bot", [{"parse_mode": ParseMode.HTML}], indirect=True, ids=["HTML-Bot"]
)
@pytest.mark.parametrize("media_type", ["animation", "document", "audio", "photo", "video"])
@pytest.mark.parametrize(
"media_type", ["animation", "document", "audio", "live_photo", "photo", "video"]
)
async def test_edit_message_media_default_parse_mode(
self,
chat_id,
@@ -1194,6 +1913,8 @@ class TestSendMediaGroupWithRequest:
return InputMediaPhoto(photo, **kwargs)
if med_type == "video":
return InputMediaVideo(video, **kwargs)
if med_type == "live_photo":
return InputMediaLivePhoto(video, photo, **kwargs)
return None
message = await default_bot.send_photo(chat_id, photo)
+389
View File
@@ -0,0 +1,389 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2026
# 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 tests a Telegram LivePhoto."""
import asyncio
import datetime as dtm
import os
from pathlib import Path
import pytest
from telegram import (
InputFile,
LivePhoto,
MessageEntity,
PhotoSize,
ReplyParameters,
Voice,
)
from telegram.constants import ParseMode
from telegram.error import BadRequest, TelegramError
from telegram.helpers import escape_markdown
from telegram.request import RequestData
from tests.auxil.build_messages import make_message
from tests.auxil.files import data_file
from tests.auxil.slots import mro_slots
@pytest.fixture(scope="module")
def live_photo():
return LivePhoto(
file_id=LivePhotoTestBase.file_id,
file_unique_id=LivePhotoTestBase.file_unique_id,
width=LivePhotoTestBase.width,
height=LivePhotoTestBase.height,
duration=LivePhotoTestBase.duration,
photo=LivePhotoTestBase.photo,
mime_type=LivePhotoTestBase.mime_type,
file_size=LivePhotoTestBase.file_size,
)
class LivePhotoTestBase:
caption = "LivePhotoTest - *Caption*"
width = 360
height = 640
duration = dtm.timedelta(seconds=5)
file_size = 326534
mime_type = "video/mp4"
photo = (PhotoSize("file_id", "unique_id", 640, 360, file_size=0),)
file_id = "5a3128a4d2a04750b5b58397f3b5e812"
file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e"
class TestLivePhotoWithoutRequest(LivePhotoTestBase):
def test_slot_behaviour(self, live_photo):
for attr in live_photo.__slots__:
assert getattr(live_photo, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(live_photo)) == len(set(mro_slots(live_photo))), "duplicate slot"
def test_de_json(self, offline_bot):
json_dict = {
"file_id": self.file_id,
"file_unique_id": self.file_unique_id,
"width": self.width,
"height": self.height,
"duration": int(self.duration.total_seconds()),
"mime_type": self.mime_type,
"file_size": self.file_size,
"photo": [photo_size.to_dict() for photo_size in self.photo],
}
json_live_photo = LivePhoto.de_json(json_dict, offline_bot)
assert json_live_photo.api_kwargs == {}
assert json_live_photo.file_id == self.file_id
assert json_live_photo.file_unique_id == self.file_unique_id
assert json_live_photo.width == self.width
assert json_live_photo.height == self.height
assert json_live_photo.duration == self.duration
assert json_live_photo.mime_type == self.mime_type
assert json_live_photo.file_size == self.file_size
assert json_live_photo.photo == self.photo
def test_to_dict(self, live_photo):
live_photo_dict = live_photo.to_dict()
assert isinstance(live_photo_dict, dict)
assert live_photo_dict["file_id"] == live_photo.file_id
assert live_photo_dict["file_unique_id"] == live_photo.file_unique_id
assert live_photo_dict["width"] == live_photo.width
assert live_photo_dict["height"] == live_photo.height
assert live_photo_dict["duration"] == int(self.duration.total_seconds())
assert isinstance(live_photo_dict["duration"], int)
assert live_photo_dict["mime_type"] == live_photo.mime_type
assert live_photo_dict["file_size"] == live_photo.file_size
assert live_photo_dict["photo"] == [p.to_dict() for p in self.photo]
def test_equality(self, live_photo):
a = LivePhoto(
live_photo.file_id, live_photo.file_unique_id, self.width, self.height, self.duration
)
b = LivePhoto("", live_photo.file_unique_id, self.width, self.height, self.duration)
c = LivePhoto(live_photo.file_id, live_photo.file_unique_id, 0, 0, 0)
d = LivePhoto("", "", self.width, self.height, self.duration)
e = Voice(live_photo.file_id, live_photo.file_unique_id, self.duration)
assert a == b
assert hash(a) == hash(b)
assert a is not 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)
async def test_send_with_live_photo(self, monkeypatch, offline_bot, chat_id, live_photo):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
data = request_data.parameters
return (
data["live_photo"] == live_photo.file_id
and data["photo"] == live_photo.photo[0].file_id
)
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
assert await offline_bot.send_live_photo(
chat_id=chat_id,
live_photo=live_photo,
photo=live_photo.photo[0],
)
@pytest.mark.parametrize(
("default_bot", "custom"),
[
({"parse_mode": ParseMode.HTML}, None),
({"parse_mode": ParseMode.HTML}, ParseMode.MARKDOWN_V2),
({"parse_mode": None}, ParseMode.MARKDOWN_V2),
],
indirect=["default_bot"],
)
async def test_send_live_photo_default_quote_parse_mode(
self, default_bot, chat_id, live_photo, custom, monkeypatch
):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
assert request_data.parameters["reply_parameters"].get("quote_parse_mode") == (
custom or default_bot.defaults.quote_parse_mode
)
return make_message("dummy reply").to_dict()
kwargs = {"message_id": "1"}
if custom is not None:
kwargs["quote_parse_mode"] = custom
monkeypatch.setattr(default_bot.request, "post", make_assertion)
await default_bot.send_live_photo(
chat_id=chat_id,
live_photo=live_photo,
photo=live_photo.photo[0],
reply_parameters=ReplyParameters(**kwargs),
)
@pytest.mark.parametrize("local_mode", [True, False])
async def test_send_live_photo(
self, dummy_message_dict, monkeypatch, offline_bot, chat_id, local_mode
):
try:
offline_bot._local_mode = local_mode
# For just test that the correct paths are passed as we have no local Bot API set up
test_flag = False
photo = data_file("telegram.jpg")
expected_photo = photo.as_uri()
live_photo = data_file("telegram.mp4")
expected_live_photo = live_photo.as_uri()
async def make_assertion(_, data, *args, **kwargs):
nonlocal test_flag
if local_mode:
test_flag = (
data.get("live_photo") == expected_live_photo
and data.get("photo") == expected_photo
)
else:
test_flag = isinstance(data.get("live_photo"), InputFile) and isinstance(
data.get("photo"), InputFile
)
return dummy_message_dict
monkeypatch.setattr(offline_bot, "_post", make_assertion)
await offline_bot.send_live_photo(chat_id, live_photo=live_photo, photo=photo)
assert test_flag
finally:
offline_bot._local_mode = False
class TestLivePhotoWithRequest(LivePhotoTestBase):
async def test_error_send_empty_file(self, bot, chat_id):
with Path(os.devnull).open("rb") as f, pytest.raises(TelegramError):
await bot.send_live_photo(chat_id=chat_id, live_photo=f, photo=f)
async def test_error_send_empty_file_id(self, bot, chat_id):
with pytest.raises(TelegramError):
await bot.send_live_photo(chat_id=chat_id, live_photo="", photo="")
async def test_get_and_download(self, bot, real_live_photo, tmp_file):
new_file = await bot.get_file(real_live_photo.file_id)
assert new_file.file_size == real_live_photo.file_size
assert new_file.file_unique_id == real_live_photo.file_unique_id
assert new_file.file_path.startswith("https://")
await new_file.download_to_drive(tmp_file)
assert tmp_file.is_file()
async def test_send_resend(self, bot, chat_id, real_live_photo, photo_file):
message = await bot.send_live_photo(
chat_id=chat_id, live_photo=real_live_photo.file_id, photo=photo_file
)
assert message.live_photo == real_live_photo
async def test_send_all_args(self, bot, chat_id, video_file, live_photo, photo_file):
message = await bot.send_live_photo(
chat_id,
live_photo=video_file,
photo=photo_file,
caption=self.caption,
disable_notification=False,
protect_content=True,
filename="telegram_custom.png",
parse_mode="Markdown",
)
assert isinstance(message.live_photo, LivePhoto)
assert isinstance(message.live_photo.file_id, str)
assert message.live_photo.file_id
assert isinstance(message.live_photo.file_unique_id, str)
assert message.live_photo.file_unique_id
assert message.live_photo.photo
assert isinstance(message.live_photo.photo[0], PhotoSize)
assert message.live_photo.mime_type == live_photo.mime_type
assert message.live_photo.file_size == live_photo.file_size
assert message.caption == self.caption.replace("*", "")
assert message.has_protected_content
@pytest.mark.parametrize("default_bot", [{"protect_content": True}], indirect=True)
async def test_send_live_photo_default_protect_content(
self,
chat_id,
default_bot,
real_live_photo,
):
tasks = asyncio.gather(
default_bot.send_live_photo(
chat_id, photo=real_live_photo.photo[0], live_photo=real_live_photo
),
default_bot.send_live_photo(
chat_id,
photo=real_live_photo.photo[0],
live_photo=real_live_photo,
protect_content=False,
),
)
protected, unprotected = await tasks
assert protected.has_protected_content
assert not unprotected.has_protected_content
async def test_send_live_photo_caption_entities(self, bot, chat_id, video_file, photo_file):
test_string = "Italic Bold Code"
entities = [
MessageEntity(MessageEntity.ITALIC, 0, 6),
MessageEntity(MessageEntity.ITALIC, 7, 4),
MessageEntity(MessageEntity.ITALIC, 12, 4),
]
message = await bot.send_live_photo(
chat_id,
photo=photo_file,
live_photo=video_file,
caption=test_string,
caption_entities=entities,
)
assert message.caption == test_string
assert message.caption_entities == tuple(entities)
@pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True)
async def test_send_live_photo_default_parse_mode_1(
self, default_bot, chat_id, video_file, photo_file
):
test_string = "Italic Bold Code"
test_markdown_string = "_Italic_ *Bold* `Code`"
message = await default_bot.send_live_photo(
chat_id, photo=photo_file, live_photo=video_file, caption=test_markdown_string
)
assert message.caption_markdown == test_markdown_string
assert message.caption == test_string
@pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True)
async def test_send_live_photo_default_parse_mode_2(
self, default_bot, chat_id, video_file, photo_file
):
test_markdown_string = "_Italic_ *Bold* `Code`"
message = await default_bot.send_live_photo(
chat_id,
photo=photo_file,
live_photo=video_file,
caption=test_markdown_string,
parse_mode=None,
)
assert message.caption == test_markdown_string
assert message.caption_markdown == escape_markdown(test_markdown_string)
@pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True)
async def test_send_live_photo_default_parse_mode_3(
self, default_bot, chat_id, video_file, photo_file
):
test_markdown_string = "_Italic_ *Bold* `Code`"
message = await default_bot.send_live_photo(
chat_id,
photo=photo_file,
live_photo=video_file,
caption=test_markdown_string,
parse_mode="HTML",
)
assert message.caption == test_markdown_string
assert message.caption_markdown == escape_markdown(test_markdown_string)
@pytest.mark.parametrize(
("default_bot", "custom"),
[
({"allow_sending_without_reply": True}, None),
({"allow_sending_without_reply": False}, None),
({"allow_sending_without_reply": False}, True),
],
indirect=["default_bot"],
)
async def test_send_live_photo_default_allow_sending_without_reply(
self, default_bot, chat_id, video_file, photo_file, custom
):
reply_to_message = await default_bot.send_message(chat_id, "test")
await reply_to_message.delete()
if custom is not None:
message = await default_bot.send_live_photo(
chat_id,
photo=photo_file,
live_photo=video_file,
allow_sending_without_reply=custom,
reply_to_message_id=reply_to_message.message_id,
)
assert message.reply_to_message is None
elif default_bot.defaults.allow_sending_without_reply:
message = await default_bot.send_live_photo(
chat_id,
photo=photo_file,
live_photo=video_file,
reply_to_message_id=reply_to_message.message_id,
)
assert message.reply_to_message is None
else:
with pytest.raises(BadRequest, match="Message to be replied not found"):
await default_bot.send_live_photo(
chat_id,
photo=photo_file,
live_photo=video_file,
reply_to_message_id=reply_to_message.message_id,
)
+2 -2
View File
@@ -45,7 +45,7 @@ from telegram import (
)
from telegram._utils.datetime import to_timestamp
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
from telegram.constants import InputMediaType
from telegram.constants import BaseInputMediaType
from telegram.ext import Defaults, ExtBot
from telegram.request import RequestData
from tests.auxil.dummy_objects import get_dummy_object_json_dict
@@ -512,7 +512,7 @@ async def make_assertion(
media = data.pop("media", None)
paid_media = media and data.pop("star_count", None)
if media and not paid_media:
if isinstance(media, dict) and isinstance(media.get("type", None), InputMediaType):
if isinstance(media, dict) and isinstance(media.get("type", None), BaseInputMediaType):
check_input_media(media)
else:
for m in media:
+7 -1
View File
@@ -4,6 +4,7 @@ from typing import TypeAlias
from telegram import (
AcceptedGiftTypes,
BotAccessSettings,
BotCommand,
BotDescription,
BotName,
@@ -30,6 +31,7 @@ from telegram import (
PollOption,
PreparedInlineMessage,
PreparedKeyboardButton,
SentGuestMessage,
SentWebAppMessage,
StarAmount,
StarTransaction,
@@ -63,6 +65,7 @@ _DUMMY_STICKER = Sticker(
_PREPARED_DUMMY_OBJECTS: dict[str, object] = {
"bool": True,
"BotAccessSettings": BotAccessSettings(is_access_restricted=True, added_users=[_DUMMY_USER]),
"BotCommand": BotCommand(command="dummy_command", description="dummy_description"),
"BotDescription": BotDescription(description="dummy_description"),
"BotName": BotName(name="dummy_name"),
@@ -127,15 +130,18 @@ _PREPARED_DUMMY_OBJECTS: dict[str, object] = {
"Poll": Poll(
id="dummy_id",
question="dummy_question",
options=[PollOption(text="dummy_text", voter_count=1)],
options=[PollOption(text="dummy_text", voter_count=1, persistent_id="persistent_id")],
is_closed=False,
is_anonymous=False,
total_voter_count=1,
type="dummy_type",
allows_multiple_answers=False,
allows_revoting=True,
members_only=True,
),
"PreparedKeyboardButton": PreparedKeyboardButton(id=1234),
"PreparedInlineMessage": PreparedInlineMessage(id="dummy_id", expiration_date=_DUMMY_DATE),
"SentGuestMessage": SentGuestMessage(inline_message_id="dummy_inline_message_id"),
"SentWebAppMessage": SentWebAppMessage(inline_message_id="dummy_inline_message_id"),
"StarAmount": StarAmount(amount=100, nanostar_amount=356),
"StarTransactions": StarTransactions(
+25
View File
@@ -945,6 +945,11 @@ class TestFilters:
update.message.location = "test"
assert filters.LOCATION.check_update(update)
def test_filters_live_photo(self, update):
assert not filters.LIVE_PHOTO.check_update(update)
update.message.live_photo = "test"
assert filters.LIVE_PHOTO.check_update(update)
def test_filters_venue(self, update):
assert not filters.VENUE.check_update(update)
update.message.venue = "test"
@@ -2471,6 +2476,7 @@ class TestFilters:
assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update)
assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.GUEST_MESSAGE.check_update(update)
def test_update_type_edited_message(self, update):
update.edited_message, update.message = update.message, update.edited_message
@@ -2484,6 +2490,7 @@ class TestFilters:
assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update)
assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.GUEST_MESSAGE.check_update(update)
def test_update_type_channel_post(self, update):
update.channel_post, update.message = update.message, update.edited_message
@@ -2497,6 +2504,7 @@ class TestFilters:
assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update)
assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.GUEST_MESSAGE.check_update(update)
def test_update_type_edited_channel_post(self, update):
update.edited_channel_post, update.message = update.message, update.edited_message
@@ -2510,6 +2518,7 @@ class TestFilters:
assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update)
assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.GUEST_MESSAGE.check_update(update)
def test_update_type_business_message(self, update):
update.business_message, update.message = update.message, update.edited_message
@@ -2523,6 +2532,7 @@ class TestFilters:
assert filters.UpdateType.BUSINESS_MESSAGES.check_update(update)
assert filters.UpdateType.BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.GUEST_MESSAGE.check_update(update)
def test_update_type_edited_business_message(self, update):
update.edited_business_message, update.message = update.message, update.edited_message
@@ -2536,6 +2546,21 @@ class TestFilters:
assert filters.UpdateType.BUSINESS_MESSAGES.check_update(update)
assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update)
assert filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.GUEST_MESSAGE.check_update(update)
def test_update_type_guest_message(self, update):
update.guest_message, update.message = update.message, update.edited_message
assert not filters.UpdateType.MESSAGE.check_update(update)
assert not filters.UpdateType.EDITED_MESSAGE.check_update(update)
assert not filters.UpdateType.MESSAGES.check_update(update)
assert not filters.UpdateType.CHANNEL_POST.check_update(update)
assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update)
assert not filters.UpdateType.CHANNEL_POSTS.check_update(update)
assert not filters.UpdateType.EDITED.check_update(update)
assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update)
assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update)
assert filters.UpdateType.GUEST_MESSAGE.check_update(update)
def test_merged_short_circuit_and(self, update, base_class):
update.message.text = "/test"
+10 -1
View File
@@ -69,7 +69,16 @@ def false_update(request):
@pytest.fixture
def poll_answer(bot):
return Update(0, poll_answer=PollAnswer(1, [0, 1], User(2, "test user", False), Chat(1, "")))
return Update(
0,
poll_answer=PollAnswer(
poll_id=1,
option_ids=[0, 1],
option_persistent_ids=["0", "1"],
user=User(2, "test user", False),
voter_chat=Chat(1, ""),
),
)
class TestPollAnswerHandler:
+13 -8
View File
@@ -73,14 +73,19 @@ def poll(bot):
return Update(
0,
poll=Poll(
1,
"question",
[PollOption("1", 0), PollOption("2", 0)],
0,
False,
False,
Poll.REGULAR,
True,
id=1,
question="question",
options=[
PollOption(text="1", voter_count=0, persistent_id="1"),
PollOption(text="2", voter_count=0, persistent_id="2"),
],
total_voter_count=0,
is_closed=False,
is_anonymous=False,
type=Poll.REGULAR,
allows_revoting=True,
members_only=True,
allows_multiple_answers=True,
),
)
+250 -3
View File
@@ -33,6 +33,7 @@ import pytest
from telegram import (
Bot,
BotAccessSettings,
BotCommand,
BotCommandScopeChat,
BotDescription,
@@ -53,6 +54,7 @@ from telegram import (
InlineQueryResultVoice,
InputFile,
InputMediaDocument,
InputMediaLocation,
InputMediaPhoto,
InputMessageContent,
InputPollOption,
@@ -76,6 +78,7 @@ from telegram import (
ReactionTypeCustomEmoji,
ReactionTypeEmoji,
ReplyParameters,
SentGuestMessage,
SentWebAppMessage,
ShippingOption,
StarTransaction,
@@ -860,6 +863,150 @@ class TestBotWithoutRequest:
== copied_result.input_message_content.parse_mode
)
async def test_answer_guest_query(self, offline_bot, raw_bot, monkeypatch):
params = False
# For now just test that our internals pass the correct data
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
nonlocal params
params = request_data.parameters == {
"guest_query_id": "12345",
"result": {
"title": "title",
"input_message_content": {
"message_text": "text",
},
"type": InlineQueryResultType.ARTICLE,
"id": "1",
},
}
return SentGuestMessage("321").to_dict()
result = InlineQueryResultArticle("1", "title", InputTextMessageContent("text"))
copied_result = copy.copy(result)
ext_bot = offline_bot
for bot_type in (ext_bot, raw_bot):
monkeypatch.setattr(bot_type.request, "post", make_assertion)
guest_msg = await bot_type.answer_guest_query("12345", result)
assert params, "something went wrong with passing arguments to the request"
assert isinstance(guest_msg, SentGuestMessage)
assert guest_msg.inline_message_id == "321"
# make sure that the results were not edited in-place
assert result == copied_result
assert (
result.input_message_content.parse_mode
== copied_result.input_message_content.parse_mode
)
@pytest.mark.parametrize(
"default_bot",
[{"parse_mode": "Markdown", "link_preview_options": LinkPreviewOptions(is_disabled=True)}],
indirect=True,
)
@pytest.mark.parametrize(
("ilq_result", "expected_params"),
[
(
InlineQueryResultArticle("1", "title", InputTextMessageContent("text")),
{
"guest_query_id": "12345",
"result": {
"title": "title",
"input_message_content": {
"message_text": "text",
"parse_mode": "Markdown",
"link_preview_options": {
"is_disabled": True,
},
},
"type": InlineQueryResultType.ARTICLE,
"id": "1",
},
},
),
(
InlineQueryResultArticle(
"1",
"title",
InputTextMessageContent(
"text", parse_mode="HTML", disable_web_page_preview=False
),
),
{
"guest_query_id": "12345",
"result": {
"title": "title",
"input_message_content": {
"message_text": "text",
"parse_mode": "HTML",
"link_preview_options": {
"is_disabled": False,
},
},
"type": InlineQueryResultType.ARTICLE,
"id": "1",
},
},
),
(
InlineQueryResultArticle(
"1",
"title",
InputTextMessageContent(
"text", parse_mode=None, disable_web_page_preview="False"
),
),
{
"guest_query_id": "12345",
"result": {
"title": "title",
"input_message_content": {
"message_text": "text",
"link_preview_options": {
"is_disabled": "False",
},
},
"type": InlineQueryResultType.ARTICLE,
"id": "1",
},
},
),
],
)
async def test_answer_guest_query_defaults(
self, default_bot, ilq_result, expected_params, monkeypatch
):
offline_bot = default_bot
params = False
# For now just test that our internals pass the correct data
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
nonlocal params
params = request_data.parameters == expected_params
return SentGuestMessage("321").to_dict()
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
# We test different result types more thoroughly for answer_inline_query, so we just
# use the one type here
copied_result = copy.copy(ilq_result)
guest_msg = await offline_bot.answer_guest_query("12345", ilq_result)
assert params, "something went wrong with passing arguments to the request"
assert isinstance(guest_msg, SentGuestMessage)
assert guest_msg.inline_message_id == "321"
# make sure that the results were not edited in-place
assert ilq_result == copied_result
assert (
ilq_result.input_message_content.parse_mode
== copied_result.input_message_content.parse_mode
)
# TODO: Needs improvement. We need incoming inline query to test answer.
@pytest.mark.parametrize("button_type", ["start", "web_app"])
@pytest.mark.parametrize("cache_time", [74, dtm.timedelta(seconds=74)])
@@ -1794,12 +1941,16 @@ class TestBotWithoutRequest:
poll=Poll(
"42",
"question",
options=[PollOption("option", 0)],
options=[
PollOption(text="option", voter_count=0, persistent_id="persistent_id")
],
total_voter_count=0,
is_closed=False,
is_anonymous=True,
type=Poll.REGULAR,
allows_multiple_answers=False,
allows_revoting=True,
members_only=True,
),
)
return [update.to_dict()]
@@ -2446,12 +2597,14 @@ class TestBotWithoutRequest:
Poll(
id="42",
question="question",
options=[PollOption("option", 0)],
options=[PollOption(text="option", voter_count=0, persistent_id="persistent_id")],
total_voter_count=5,
is_closed=True,
is_anonymous=True,
type="regular",
allows_multiple_answers=False,
allows_revoting=True,
members_only=True,
).to_dict()
)
await return_values.put(True)
@@ -2920,6 +3073,62 @@ class TestBotWithoutRequest:
)
assert isinstance(inst, PreparedKeyboardButton)
async def test_get_managed_bot_access_settings(self, offline_bot, monkeypatch):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
assert request_data.parameters.get("user_id") == 1234
return BotAccessSettings(
is_access_restricted=True,
added_users=[User(1, "first", False)],
).to_dict()
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
settings = await offline_bot.get_managed_bot_access_settings(1234)
assert isinstance(settings, BotAccessSettings)
async def test_set_managed_bot_access_settings(self, offline_bot, monkeypatch):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
assert request_data.parameters.get("user_id") == 1234
assert request_data.parameters.get("is_access_restricted") is True
assert request_data.parameters.get("added_user_ids") == [1, 2, 3]
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
await offline_bot.set_managed_bot_access_settings(
1234,
is_access_restricted=True,
added_user_ids=[1, 2, 3],
)
async def test_get_user_personal_chat_messages(self, offline_bot, monkeypatch):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
assert request_data.parameters.get("user_id") == 1234
assert request_data.parameters.get("limit") == 1
return [make_message("dummy reply").to_dict()]
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
msgs = await offline_bot.get_user_personal_chat_messages(1234, limit=1)
assert isinstance(msgs, tuple)
assert all(isinstance(msg, Message) for msg in msgs)
# Bots cannot delete their own reaction from my testing, so we aren't making a real request
async def test_delete_message_reaction(self, offline_bot, monkeypatch):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
assert request_data.parameters.get("chat_id") == 1234
assert request_data.parameters.get("message_id") == 12
assert request_data.parameters.get("user_id") == 3432
assert request_data.parameters.get("actor_chat_id") == 1232
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
await offline_bot.delete_message_reaction(1234, 12, 3432, 1232)
async def test_delete_all_message_reactions(self, offline_bot, monkeypatch):
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
assert request_data.parameters.get("chat_id") == 1234
assert request_data.parameters.get("user_id") == 3432
assert request_data.parameters.get("actor_chat_id") == 1232
monkeypatch.setattr(offline_bot.request, "post", make_assertion)
await offline_bot.delete_all_message_reactions(1234, 3432, 1232)
class TestBotWithRequest:
"""
@@ -3275,6 +3484,34 @@ class TestBotWithRequest:
assert message.poll.explanation == test_string
assert message.poll.explanation_entities == tuple(entities)
async def test_send_poll_media_parameters(self, bot, channel_id):
with (
data_file("telegram.jpg").open("rb") as photo_file,
data_file("text_file.txt").open("rb") as document_file,
):
i_photo = InputMediaPhoto(InputFile(photo_file, attach=True))
i_document = InputMediaDocument(InputFile(document_file, attach=True))
i_location = InputMediaLocation(latitude=0, longitude=0)
message = await bot.send_poll(
channel_id,
question="question",
options=[
InputPollOption("option1", media=i_location),
InputPollOption("option2"),
],
type=Poll.QUIZ,
correct_option_ids=[0],
media=i_photo,
explanation_media=i_document,
is_closed=True,
read_timeout=60,
)
assert message.poll.media.photo
assert message.poll.explanation_media.document
assert message.poll.options[0].media.location
@pytest.mark.parametrize("default_bot", [{"parse_mode": "Markdown"}], indirect=True)
async def test_send_poll_default_parse_mode(self, default_bot, super_group_id):
explanation = "Italic Bold Code"
@@ -3705,11 +3942,15 @@ class TestBotWithRequest:
assert cfi.id == int(super_group_id)
async def test_get_chat_administrators(self, bot, channel_id):
admins = await bot.get_chat_administrators(channel_id)
admins = await bot.get_chat_administrators(channel_id, return_bots=True)
assert isinstance(admins, tuple)
bots_found = 0
for a in admins:
assert a.status in ("administrator", "creator")
if a.user.is_bot:
bots_found += 1
assert bots_found > 1 # will be False if return_bots=False
async def test_get_chat_member_count(self, bot, channel_id):
count = await bot.get_chat_member_count(channel_id)
@@ -4924,6 +5165,12 @@ class TestBotWithRequest:
bot_profile_photos = await bot.get_user_profile_photos(bot.id)
assert bot_profile_photos.total_count == 1
async def test_get_user_personal_chat_messages(self, bot):
# id is of the Test User
messages = await bot.get_user_personal_chat_messages(user_id=675666224, limit=2)
assert isinstance(messages, tuple)
assert len(messages) == 2
async def test_initialize_tracks_requests_and_bot_separately(self, offline_bot, monkeypatch):
"""Test that requests and bot user are initialized separately and only once."""
request_init_count = 0
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2026
# 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 Bot Access Settings."""
import pytest
from telegram import BotAccessSettings, Dice
from telegram._user import User
from tests.auxil.slots import mro_slots
@pytest.fixture(scope="module")
def bot_access_settings():
return BotAccessSettings(
is_access_restricted=BotAccessSettingsTestBase.is_access_restricted,
added_users=BotAccessSettingsTestBase.added_users,
)
class BotAccessSettingsTestBase:
is_access_restricted = True
added_users = [User(id=123, first_name="John", is_bot=False)]
class TestBotAccessSettingsWithoutRequest(BotAccessSettingsTestBase):
def test_slot_behaviour(self, bot_access_settings):
inst = bot_access_settings
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 = {
"is_access_restricted": self.is_access_restricted,
"added_users": [user.to_dict() for user in self.added_users],
}
bot_access_settings = BotAccessSettings.de_json(json_dict, offline_bot)
assert bot_access_settings.api_kwargs == {}
assert bot_access_settings.is_access_restricted == self.is_access_restricted
assert bot_access_settings.added_users == tuple(self.added_users)
def test_to_dict(self, bot_access_settings):
bot_access_settings_dict = bot_access_settings.to_dict()
assert isinstance(bot_access_settings_dict, dict)
assert (
bot_access_settings_dict["is_access_restricted"]
== bot_access_settings.is_access_restricted
)
assert isinstance(bot_access_settings_dict["added_users"], list)
assert bot_access_settings_dict["added_users"][0] == self.added_users[0].to_dict()
def test_equality(self):
a = BotAccessSettings(is_access_restricted=True, added_users=self.added_users)
b = BotAccessSettings(is_access_restricted=True, added_users=self.added_users)
c = BotAccessSettings(is_access_restricted=False, added_users=self.added_users)
d = BotAccessSettings(is_access_restricted=True, added_users=None)
e = Dice(emoji="🎲", value=4)
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)
+77
View File
@@ -563,6 +563,21 @@ class TestChatWithoutRequest(ChatTestBase):
monkeypatch.setattr(chat.get_bot(), "send_photo", make_assertion)
assert await chat.send_photo(photo="test_photo")
async def test_instance_method_send_live_photo(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
return (
kwargs["chat_id"] == chat.id
and kwargs["live_photo"] == "test_live_photo"
and kwargs["photo"] == "test_photo"
)
assert check_shortcut_signature(Chat.send_live_photo, Bot.send_live_photo, ["chat_id"], [])
assert await check_shortcut_call(chat.send_live_photo, chat.get_bot(), "send_live_photo")
assert await check_defaults_handling(chat.send_live_photo, chat.get_bot())
monkeypatch.setattr(chat.get_bot(), "send_live_photo", make_assertion)
assert await chat.send_live_photo(live_photo="test_live_photo", photo="test_photo")
async def test_instance_method_send_contact(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
return kwargs["chat_id"] == chat.id and kwargs["phone_number"] == "test_contact"
@@ -1537,6 +1552,68 @@ class TestChatWithoutRequest(ChatTestBase):
active_period=3600,
)
async def test_instance_method_delete_reaction(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
return (
kwargs["chat_id"] == chat.id
and kwargs["message_id"] == 321
and kwargs["user_id"] == 123
and kwargs["actor_chat_id"] == 222
)
assert check_shortcut_signature(
Chat.delete_reaction,
Bot.delete_message_reaction,
[
"chat_id",
],
additional_kwargs=[],
)
assert await check_shortcut_call(
chat.delete_reaction,
chat.get_bot(),
"delete_message_reaction",
shortcut_kwargs=["chat_id"],
)
assert await check_defaults_handling(chat.delete_reaction, chat.get_bot())
monkeypatch.setattr(chat.get_bot(), "delete_message_reaction", make_assertion)
assert await chat.delete_reaction(
user_id=123,
message_id=321,
actor_chat_id=222,
)
async def test_instance_method_delete_all_reactions(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
return (
kwargs["chat_id"] == chat.id
and kwargs["user_id"] == 123
and kwargs["actor_chat_id"] == 222
)
assert check_shortcut_signature(
Chat.delete_all_reactions,
Bot.delete_all_message_reactions,
[
"chat_id",
],
additional_kwargs=[],
)
assert await check_shortcut_call(
chat.delete_all_reactions,
chat.get_bot(),
"delete_all_message_reactions",
shortcut_kwargs=["chat_id"],
)
assert await check_defaults_handling(chat.delete_all_reactions, chat.get_bot())
monkeypatch.setattr(chat.get_bot(), "delete_all_message_reactions", make_assertion)
assert await chat.delete_all_reactions(
user_id=123,
actor_chat_id=222,
)
async def test_instance_method_get_gifts(self, monkeypatch, chat):
async def make_assertion(*_, **kwargs):
return kwargs["chat_id"] == chat.id
+18
View File
@@ -61,6 +61,7 @@ class ChatMemberTestBase:
can_pin_messages = True
can_post_stories = True
can_edit_stories = True
can_react_to_messages = True
can_delete_stories = True
can_manage_topics = True
until_date = dtm.datetime.now(UTC).replace(microsecond=0)
@@ -576,6 +577,7 @@ def chat_member_restricted():
is_member=TestChatMemberRestrictedWithoutRequest.is_member,
until_date=TestChatMemberRestrictedWithoutRequest.until_date,
can_edit_tag=TestChatMemberRestrictedWithoutRequest.can_edit_tag,
can_react_to_messages=TestChatMemberRestrictedWithoutRequest.can_react_to_messages,
tag=TestChatMemberRestrictedWithoutRequest.tag,
)
@@ -609,6 +611,7 @@ class TestChatMemberRestrictedWithoutRequest(ChatMemberTestBase):
"is_member": self.is_member,
"until_date": to_timestamp(self.until_date),
"can_edit_tag": self.can_edit_tag,
"can_react_to_messages": self.can_react_to_messages,
"tag": self.tag,
# legacy argument
"can_send_media_messages": False,
@@ -636,6 +639,7 @@ class TestChatMemberRestrictedWithoutRequest(ChatMemberTestBase):
assert chat_member.is_member == self.is_member
assert chat_member.until_date == self.until_date
assert chat_member.can_edit_tag == self.can_edit_tag
assert chat_member.can_react_to_messages == self.can_react_to_messages
assert chat_member.tag == self.tag
def test_de_json_localization(self, tz_bot, offline_bot, raw_bot, chat_member_restricted):
@@ -676,9 +680,22 @@ class TestChatMemberRestrictedWithoutRequest(ChatMemberTestBase):
"is_member": chat_member_restricted.is_member,
"until_date": to_timestamp(chat_member_restricted.until_date),
"can_edit_tag": chat_member_restricted.can_edit_tag,
"can_react_to_messages": chat_member_restricted.can_react_to_messages,
"tag": chat_member_restricted.tag,
}
def test_can_react_to_messages_raises(self, chat_member_restricted):
with pytest.raises(
TypeError, match="`can_react_to_messages` is required and cannot be None"
):
ChatMemberRestricted(
*[
getattr(chat_member_restricted, k)
for k in chat_member_restricted.__slots__
if k != "can_react_to_messages"
]
)
def test_equality(self, chat_member_restricted):
a = chat_member_restricted
b = deepcopy(chat_member_restricted)
@@ -701,6 +718,7 @@ class TestChatMemberRestrictedWithoutRequest(ChatMemberTestBase):
False,
False,
False,
False,
"tag",
)
d = Dice(5, "test")
+7
View File
@@ -41,6 +41,7 @@ def chat_permissions():
can_send_video_notes=True,
can_send_voice_notes=True,
can_edit_tag=True,
can_react_to_messages=True,
)
@@ -60,6 +61,7 @@ class ChatPermissionsTestBase:
can_send_video_notes = False
can_send_voice_notes = None
can_edit_tag = None
can_react_to_messages = True
class TestChatPermissionsWithoutRequest(ChatPermissionsTestBase):
@@ -86,6 +88,7 @@ class TestChatPermissionsWithoutRequest(ChatPermissionsTestBase):
"can_send_video_notes": self.can_send_video_notes,
"can_send_voice_notes": self.can_send_voice_notes,
"can_edit_tag": self.can_edit_tag,
"can_react_to_messages": self.can_react_to_messages,
}
permissions = ChatPermissions.de_json(json_dict, offline_bot)
assert permissions.api_kwargs == {"can_send_media_messages": "can_send_media_messages"}
@@ -105,6 +108,7 @@ class TestChatPermissionsWithoutRequest(ChatPermissionsTestBase):
assert permissions.can_send_video_notes == self.can_send_video_notes
assert permissions.can_send_voice_notes == self.can_send_voice_notes
assert permissions.can_edit_tag == self.can_edit_tag
assert permissions.can_react_to_messages == self.can_react_to_messages
def test_to_dict(self, chat_permissions):
permissions_dict = chat_permissions.to_dict()
@@ -130,6 +134,7 @@ class TestChatPermissionsWithoutRequest(ChatPermissionsTestBase):
assert permissions_dict["can_send_video_notes"] == chat_permissions.can_send_video_notes
assert permissions_dict["can_send_voice_notes"] == chat_permissions.can_send_voice_notes
assert permissions_dict["can_edit_tag"] == chat_permissions.can_edit_tag
assert permissions_dict["can_react_to_messages"] == chat_permissions.can_react_to_messages
def test_equality(self):
a = ChatPermissions(
@@ -159,6 +164,7 @@ class TestChatPermissionsWithoutRequest(ChatPermissionsTestBase):
can_send_video_notes=True,
can_send_voice_notes=True,
can_edit_tag=True,
can_react_to_messages=True,
)
f = ChatPermissions(
can_send_messages=True,
@@ -171,6 +177,7 @@ class TestChatPermissionsWithoutRequest(ChatPermissionsTestBase):
can_send_video_notes=True,
can_send_voice_notes=True,
can_edit_tag=True,
can_react_to_messages=True,
)
assert a == b
+3
View File
@@ -204,6 +204,9 @@ class TestConstantsWithoutRequest:
"external_reply",
"via_bot",
"is_from_offline",
"guest_bot_caller_chat",
"guest_bot_caller_user",
"guest_query_id",
"show_caption_above_media",
"paid_star_count",
"is_paid_post",
+118 -1
View File
@@ -50,11 +50,14 @@ from telegram import (
GiveawayCompleted,
GiveawayCreated,
GiveawayWinners,
InlineQueryResultArticle,
InputChecklist,
InputChecklistTask,
InputPaidMediaPhoto,
InputTextMessageContent,
Invoice,
LinkPreviewOptions,
LivePhoto,
Location,
ManagedBotCreated,
Message,
@@ -131,6 +134,7 @@ def message(bot):
chat=copy(MessageTestBase.chat),
from_user=copy(MessageTestBase.from_user),
business_connection_id="123456789",
guest_query_id="706654132",
)
message.set_bot(bot)
message._unfreeze()
@@ -213,13 +217,18 @@ def message(bot):
"poll": Poll(
id="abc",
question="What is this?",
options=[PollOption(text="a", voter_count=1), PollOption(text="b", voter_count=2)],
options=[
PollOption(text="a", voter_count=1, persistent_id="persistent_id_a"),
PollOption(text="b", voter_count=2, persistent_id="persistent_id_b"),
],
is_closed=False,
total_voter_count=0,
is_anonymous=False,
type=Poll.REGULAR,
allows_multiple_answers=True,
explanation_entities=[],
allows_revoting=True,
members_only=True,
)
},
{
@@ -443,6 +452,10 @@ def message(bot):
{"poll_option_deleted": PollOptionDeleted(option_persistent_id="abc", option_text="this")},
{"reply_to_poll_option_id": "3123"},
{"managed_bot_created": ManagedBotCreated(bot=User(6, "ManagedBot", True))},
{"guest_bot_caller_user": User(10, "hm", False)},
{"guest_bot_caller_chat": Chat(14, "om")},
{"guest_query_id": "This is a guest_query_id"},
{"live_photo": LivePhoto("file_id", "file_unique_id", 12, 12, 5)},
],
ids=[
"reply",
@@ -541,6 +554,10 @@ def message(bot):
"poll_option_deleted",
"reply_to_poll_option_id",
"managed_bot_created",
"guest_bot_caller_user",
"guest_bot_caller_chat",
"guest_query_id",
"live_photo",
],
)
def message_params(bot, request):
@@ -1486,6 +1503,7 @@ class TestMessageWithoutRequest(MessageTestBase):
"document",
"game",
"invoice",
"live_photo",
"location",
"paid_media",
"passport_data",
@@ -2009,6 +2027,54 @@ class TestMessageWithoutRequest(MessageTestBase):
message, message.reply_photo, "send_photo", ["test_photo"], monkeypatch
)
async def test_reply_live_photo(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
live_photo = kwargs["live_photo"] == "test_live_photo"
photo = kwargs["photo"] == "test_photo"
return id_ and live_photo and photo
assert check_shortcut_signature(
Message.reply_live_photo,
Bot.send_live_photo,
[
"chat_id",
"reply_to_message_id",
"business_connection_id",
"direct_messages_topic_id",
],
["do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_live_photo,
message.get_bot(),
"send_live_photo",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id", "direct_messages_topic_id"],
)
assert await check_defaults_handling(
message.reply_live_photo, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
monkeypatch.setattr(message.get_bot(), "send_live_photo", make_assertion)
assert await message.reply_live_photo(live_photo="test_live_photo", photo="test_photo")
await self.check_quote_parsing(
message,
message.reply_live_photo,
"send_live_photo",
["test_live_photo", "test_photo"],
monkeypatch,
)
await self.check_thread_id_parsing(
message,
message.reply_live_photo,
"send_live_photo",
["test_live_photo", "test_photo"],
monkeypatch,
)
async def test_reply_audio(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -3419,3 +3485,54 @@ class TestMessageWithoutRequest(MessageTestBase):
monkeypatch.setattr(message.get_bot(), "decline_suggested_post", make_assertion)
assert await message.decline_suggested_post(comment="some comment")
async def test_delete_reaction(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
return (
kwargs["chat_id"] == message.chat_id
and kwargs["message_id"] == message.message_id
and kwargs["user_id"] == 23
and kwargs["actor_chat_id"] == 12
)
assert check_shortcut_signature(
Message.delete_reaction,
Bot.delete_message_reaction,
["chat_id", "message_id"],
[],
)
assert await check_shortcut_call(
message.delete_reaction,
message.get_bot(),
"delete_message_reaction",
shortcut_kwargs=["chat_id", "message_id"],
)
assert await check_defaults_handling(message.delete_reaction, message.get_bot())
monkeypatch.setattr(message.get_bot(), "delete_message_reaction", make_assertion)
assert await message.delete_reaction(user_id=23, actor_chat_id=12)
async def test_answer_guest_query(self, monkeypatch, message):
iqra = InlineQueryResultArticle(
id="iqra_id", title="title", input_message_content=InputTextMessageContent("content")
)
async def make_assertion(*_, **kwargs):
return kwargs["guest_query_id"] == message.guest_query_id and kwargs["result"] == iqra
assert check_shortcut_signature(
Message.answer_guest_query,
Bot.answer_guest_query,
["guest_query_id"],
[],
)
assert await check_shortcut_call(
message.answer_guest_query,
message.get_bot(),
"answer_guest_query",
shortcut_kwargs=["guest_query_id"],
)
assert await check_defaults_handling(message.answer_guest_query, message.get_bot())
monkeypatch.setattr(message.get_bot(), "answer_guest_query", make_assertion)
assert await message.answer_guest_query(result=iqra)
+31 -4
View File
@@ -20,7 +20,18 @@
from collections.abc import Sequence
from telegram import Animation, Audio, Document, Gift, PhotoSize, Sticker, Video, VideoNote, Voice
from telegram import (
Animation,
Audio,
Document,
Gift,
LivePhoto,
PhotoSize,
Sticker,
Video,
VideoNote,
Voice,
)
from tests.test_official.helpers import _get_params_base
IGNORED_OBJECTS = ("ResponseParameters",)
@@ -41,6 +52,7 @@ class ParamTypeCheckingExceptions:
ADDITIONAL_TYPES = {
r"send_\w*": {
"photo$": PhotoSize,
"live_photo": LivePhoto,
"video$": Video,
"video_note": VideoNote,
"audio": Audio,
@@ -67,6 +79,7 @@ class ParamTypeCheckingExceptions:
("keyboard", True): "KeyboardButton", # + sequence[sequence[str]]
("reaction", False): "ReactionType", # + str
("options", False): "InputPollOption", # + str
("correct_option_ids", False): "Sequence[typing.Literal[",
}
# Special cases for other parameters that accept more types than the official API, and are
@@ -91,6 +104,7 @@ class ParamTypeCheckingExceptions:
},
"Input(Paid)?Media.*": {
"media": str, # actual: Union[str, InputMedia*, FileInput]
"photo": str, # actual: Union[str, FileInput]
# see also https://github.com/tdlib/telegram-bot-api/issues/707
"thumbnail": str, # actual: Union[str, FileInput]
"cover": str, # actual: Union[str, FileInput]
@@ -138,7 +152,7 @@ PTB_EXTRA_PARAMS = {
"send_venue": {"venue"},
"answer_inline_query": {"current_offset"},
"send_media_group": {"caption", "parse_mode", "caption_entities"},
"send_(animation|audio|document|photo|video(_note)?|voice)": {"filename"},
"send_(animation|audio|document|photo|video(_note)?|voice|live_photo)": {"filename"},
"InlineQueryResult": {"id", "type"}, # attributes common to all subclasses
"ChatMember": {"user", "status"}, # attributes common to all subclasses
"BotCommandScope": {"type"}, # attributes common to all subclasses
@@ -146,8 +160,13 @@ PTB_EXTRA_PARAMS = {
"PassportFile": {"credentials"},
"EncryptedPassportElement": {"credentials"},
"PassportElementError": {"source", "type", "message"},
"InputPoll(Option)?Media": {"media_type"},
"InputMedia": {"caption", "caption_entities", "media", "media_type", "parse_mode"},
"InputMedia(Animation|Audio|Document|Photo|Video|VideoNote|Voice)": {"filename"},
"InputMedia(Animation|Audio|Document|Photo|Sticker|Video|VideoNote|Voice)": {
"filename",
# tags: deprecated NEXT.VERSION
"filename_depr",
},
"InputFile": {"attach", "filename", "obj", "read_file_handle"},
"MaybeInaccessibleMessage": {"date", "message_id", "chat"}, # attributes common to all subcls
"ChatBoostSource": {"source"}, # attributes common to all subclasses
@@ -164,6 +183,12 @@ PTB_EXTRA_PARAMS = {
"InputStoryContent": {"type"}, # attributes common to all subclasses
"StoryAreaType": {"type"}, # attributes common to all subclasses
"InputProfilePhoto": {"type"}, # attributes common to all subclasses
"InputPollOptionMedia": {"args", "kwargs"}, # UnionType's __init__ signature
"InputPollMedia": {"args", "kwargs"}, # UnionType's __init__ signature
# backwards compatibility for api 10.0 changes
# tags: deprecated NEXT.VERSION, bot api 10.0
"Poll": {"correct_option_id"},
"send_poll": {"correct_option_id"},
}
@@ -222,7 +247,9 @@ def ignored_param_requirements(object_name: str) -> set[str]:
BACKWARDS_COMPAT_KWARGS: dict[str, set[str]] = {
"PollOption": {"persistent_id"},
"PollAnswer": {"option_persistent_ids"},
"Poll": {"allows_revoting"},
"Poll": {"allows_revoting", "members_only"},
"ChatMemberRestricted": {"can_react_to_messages"},
"send_poll": {"correct_option_id"},
}
+62
View File
@@ -24,8 +24,10 @@ import pytest
from telegram import (
Dice,
LivePhoto,
PaidMedia,
PaidMediaInfo,
PaidMediaLivePhoto,
PaidMediaPhoto,
PaidMediaPreview,
PaidMediaPurchased,
@@ -64,6 +66,14 @@ class PaidMediaTestBase:
file_unique_id="file_unique_id",
),
)
live_photo = LivePhoto(
file_id="live_photo_file_id",
file_unique_id="live_photo_file_unique_id",
width=640,
height=480,
duration=dtm.timedelta(seconds=60),
photo=photo,
)
class TestPaidMediaWithoutRequest(PaidMediaTestBase):
@@ -89,6 +99,7 @@ class TestPaidMediaWithoutRequest(PaidMediaTestBase):
("photo", PaidMediaPhoto),
("video", PaidMediaVideo),
("preview", PaidMediaPreview),
("live_photo", PaidMediaLivePhoto),
],
)
def test_de_json_subclass(self, offline_bot, pm_type, subclass):
@@ -99,6 +110,7 @@ class TestPaidMediaWithoutRequest(PaidMediaTestBase):
"width": self.width,
"height": self.height,
"duration": int(self.duration.total_seconds()),
"live_photo": self.live_photo.to_dict(),
}
pm = PaidMedia.de_json(json_dict, offline_bot)
@@ -226,6 +238,56 @@ class TestPaidMediaVideoWithoutRequest(PaidMediaTestBase):
assert hash(a) != hash(d)
@pytest.fixture
def paid_media_live_photo():
return PaidMediaLivePhoto(
live_photo=TestPaidMediaLivePhotoWithoutRequest.live_photo,
)
class TestPaidMediaLivePhotoWithoutRequest(PaidMediaTestBase):
type = PaidMediaType.LIVE_PHOTO
def test_slot_behaviour(self, paid_media_live_photo):
inst = paid_media_live_photo
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 = {
"live_photo": self.live_photo.to_dict(),
}
pmlp = PaidMediaLivePhoto.de_json(json_dict, offline_bot)
assert pmlp.live_photo == self.live_photo
assert pmlp.api_kwargs == {}
def test_to_dict(self, paid_media_live_photo):
assert paid_media_live_photo.to_dict() == {
"type": self.type,
"live_photo": paid_media_live_photo.live_photo.to_dict(),
}
def test_equality(self, paid_media_live_photo):
a = paid_media_live_photo
b = PaidMediaLivePhoto(
live_photo=deepcopy(self.live_photo),
)
c = PaidMediaLivePhoto(
live_photo=LivePhoto("test", "test_unique", 640, 480, 60),
)
d = Dice(5, "test")
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.fixture
def paid_media_preview():
return PaidMediaPreview(
+306 -22
View File
@@ -20,16 +20,28 @@ import datetime as dtm
import pytest
from telegram import (
Animation,
Audio,
Chat,
Document,
InputMediaPhoto,
InputPollOption,
LivePhoto,
Location,
MaybeInaccessibleMessage,
MessageEntity,
PhotoSize,
Poll,
PollAnswer,
PollMedia,
PollOption,
PollOptionAdded,
PollOptionDeleted,
Sticker,
User,
Venue,
Video,
)
from telegram._poll import PollOptionAdded, PollOptionDeleted
from telegram._utils.datetime import UTC, to_timestamp
from telegram.constants import PollType
from telegram.warnings import PTBDeprecationWarning
@@ -42,6 +54,7 @@ def input_poll_option():
text=InputPollOptionTestBase.text,
text_parse_mode=InputPollOptionTestBase.text_parse_mode,
text_entities=InputPollOptionTestBase.text_entities,
media=InputPollOptionTestBase.media,
)
out._unfreeze()
return out
@@ -54,6 +67,7 @@ class InputPollOptionTestBase:
MessageEntity(0, 4, MessageEntity.BOLD),
MessageEntity(5, 7, MessageEntity.ITALIC),
]
media = InputMediaPhoto("media")
class TestInputPollOptionWithoutRequest(InputPollOptionTestBase):
@@ -64,6 +78,7 @@ class TestInputPollOptionWithoutRequest(InputPollOptionTestBase):
"duplicate slot"
)
# tags: deprecated NEXT.VERSION
def test_de_json(self):
json_dict = {
"text": self.text,
@@ -77,6 +92,16 @@ class TestInputPollOptionWithoutRequest(InputPollOptionTestBase):
assert input_poll_option.text_parse_mode == self.text_parse_mode
assert input_poll_option.text_entities == tuple(self.text_entities)
def test_de_json_deprecated(self, recwarn):
InputPollOption.de_json({"text": self.text}, None)
assert len(recwarn) == 1
assert "`InputPollOption.de_json` is deprecated" in str(recwarn[0].message)
assert "The `media` field will not be included for deserialization" in str(
recwarn[0].message
)
assert recwarn[0].category is PTBDeprecationWarning
def test_to_dict(self, input_poll_option):
input_poll_option_dict = input_poll_option.to_dict()
@@ -86,6 +111,7 @@ class TestInputPollOptionWithoutRequest(InputPollOptionTestBase):
assert input_poll_option_dict["text_entities"] == [
e.to_dict() for e in input_poll_option.text_entities
]
assert input_poll_option_dict["media"] == input_poll_option.media.to_dict()
# Test that the default-value parameter is handled correctly
input_poll_option = InputPollOption("text")
@@ -97,7 +123,18 @@ class TestInputPollOptionWithoutRequest(InputPollOptionTestBase):
b = InputPollOption("text", self.text_parse_mode)
c = InputPollOption("text", text_entities=self.text_entities)
d = InputPollOption("different_text")
e = Poll(123, "question", ["O1", "O2"], 1, False, True, Poll.REGULAR, True)
e = Poll(
123,
"question",
["O1", "O2"],
1,
False,
True,
Poll.REGULAR,
True,
allows_revoting=True,
members_only=True,
)
assert a == b
assert hash(a) == hash(b)
@@ -112,6 +149,112 @@ class TestInputPollOptionWithoutRequest(InputPollOptionTestBase):
assert hash(a) != hash(e)
@pytest.fixture(scope="module")
def poll_media():
return PollMedia(
animation=PollMediaTestBase.animation,
audio=PollMediaTestBase.audio,
document=PollMediaTestBase.document,
live_photo=PollMediaTestBase.live_photo,
location=PollMediaTestBase.location,
photo=PollMediaTestBase.photo,
sticker=PollMediaTestBase.sticker,
venue=PollMediaTestBase.venue,
video=PollMediaTestBase.video,
)
class PollMediaTestBase:
animation = Animation("blah", "unique_id", 320, 180, 1)
audio = Audio(file_id="file_id", file_unique_id="file_unique_id", duration=30)
document = Document("file_id", "file_unique_id", "file_name", 42)
location = Location(123, 456)
photo = (PhotoSize("file_id", "file_unique_id", 1, 1),)
sticker = Sticker("file_id", "file_unique_id", 512, 512, False, False, "regular")
venue = Venue(location=Location(123, 456), title="title", address="address")
video = Video(
file_id="video_file_id",
width=640,
height=480,
file_unique_id="file_unique_id",
duration=dtm.timedelta(seconds=60),
)
live_photo = LivePhoto(
file_id="video_file_id",
file_unique_id="file_unique_id",
width=640,
height=480,
duration=dtm.timedelta(seconds=60),
mime_type="video/mp4",
file_size=326534,
)
class TestPollMediaWithoutRequest(PollMediaTestBase):
def test_slot_behaviour(self, poll_media):
for attr in poll_media.__slots__:
assert getattr(poll_media, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(poll_media)) == len(set(mro_slots(poll_media))), "duplicate slot"
def test_de_json(self):
json_dict = {
"animation": self.animation.to_dict(),
"audio": self.audio.to_dict(),
"document": self.document.to_dict(),
"live_photo": self.live_photo.to_dict(),
"location": self.location.to_dict(),
"photo": [photo.to_dict() for photo in self.photo],
"sticker": self.sticker.to_dict(),
"venue": self.venue.to_dict(),
"video": self.video.to_dict(),
}
poll_media = PollMedia.de_json(json_dict, None)
assert poll_media.api_kwargs == {}
assert poll_media.animation == self.animation
assert poll_media.audio == self.audio
assert poll_media.document == self.document
assert poll_media.live_photo == self.live_photo
assert poll_media.location == self.location
assert poll_media.photo == self.photo
assert poll_media.sticker == self.sticker
assert poll_media.venue == self.venue
assert poll_media.video == self.video
def test_to_dict(self, poll_media):
poll_media_dict = poll_media.to_dict()
assert isinstance(poll_media_dict, dict)
assert poll_media_dict["animation"] == poll_media.animation.to_dict()
assert poll_media_dict["audio"] == poll_media.audio.to_dict()
assert poll_media_dict["document"] == poll_media.document.to_dict()
assert poll_media_dict["live_photo"] == poll_media.live_photo.to_dict()
assert poll_media_dict["location"] == poll_media.location.to_dict()
assert poll_media_dict["photo"] == [photo.to_dict() for photo in poll_media.photo]
assert poll_media_dict["sticker"] == poll_media.sticker.to_dict()
assert poll_media_dict["venue"] == poll_media.venue.to_dict()
assert poll_media_dict["video"] == poll_media.video.to_dict()
def test_equality(self):
a = PollMedia(photo=self.photo)
b = PollMedia(photo=self.photo)
c = PollMedia(photo=(PhotoSize("file_id", "other_file_unique_id", 1, 1),))
d = PollMedia(video=self.video)
e = PollOption("text", 1, persistent_id="persistent_id")
assert a == b
assert hash(a) == hash(b)
assert a != d
assert hash(a) != hash(d)
assert a != c
assert hash(a) != hash(c)
assert a != e
assert hash(a) != hash(e)
@pytest.fixture(scope="module")
def poll_option():
out = PollOption(
@@ -121,6 +264,8 @@ def poll_option():
added_by_user=PollOptionTestBase.added_by_user,
added_by_chat=PollOptionTestBase.added_by_chat,
addition_date=PollOptionTestBase.addition_date,
persistent_id=PollOptionTestBase.persistent_id,
media=PollOptionTestBase.media,
)
out._unfreeze()
return out
@@ -136,6 +281,8 @@ class PollOptionTestBase:
added_by_user = User(1, "test_user", False)
added_by_chat = Chat(1, "test_chat")
addition_date = dtm.datetime.now(dtm.timezone.utc)
persistent_id = "persistent_id"
media = PollMedia(location=Location(123, 456))
class TestPollOptionWithoutRequest(PollOptionTestBase):
@@ -152,6 +299,8 @@ class TestPollOptionWithoutRequest(PollOptionTestBase):
"added_by_user": self.added_by_user.to_dict(),
"added_by_chat": self.added_by_chat.to_dict(),
"addition_date": to_timestamp(self.addition_date),
"persistent_id": self.persistent_id,
"media": self.media.to_dict(),
}
poll_option = PollOption.de_json(json_dict, None)
assert poll_option.api_kwargs == {}
@@ -162,6 +311,8 @@ class TestPollOptionWithoutRequest(PollOptionTestBase):
assert poll_option.added_by_user == self.added_by_user
assert poll_option.added_by_chat == self.added_by_chat
assert abs((poll_option.addition_date - self.addition_date).total_seconds()) < 1
assert poll_option.persistent_id == self.persistent_id
assert poll_option.media == self.media
def test_to_dict(self, poll_option):
poll_option_dict = poll_option.to_dict()
@@ -175,6 +326,8 @@ class TestPollOptionWithoutRequest(PollOptionTestBase):
assert poll_option_dict["added_by_user"] == poll_option.added_by_user.to_dict()
assert poll_option_dict["added_by_chat"] == poll_option.added_by_chat.to_dict()
assert poll_option_dict["addition_date"] == to_timestamp(poll_option.addition_date)
assert poll_option_dict["persistent_id"] == poll_option.persistent_id
assert poll_option_dict["media"] == poll_option.media.to_dict()
def test_parse_entity(self, poll_option):
entity = MessageEntity(MessageEntity.BOLD, 0, 4)
@@ -190,12 +343,29 @@ class TestPollOptionWithoutRequest(PollOptionTestBase):
assert poll_option.parse_entities(MessageEntity.BOLD) == {entity: "test"}
assert poll_option.parse_entities() == {entity: "test", entity_2: "option"}
def test_persistent_id_required_workaround(self):
# tags: deprecated NEXT.VERSION, bot api 9.6
with pytest.raises(TypeError, match="`persistent_id` is a required"):
PollOption(self.text, self.voter_count)
def test_equality(self):
a = PollOption("text", 1)
b = PollOption("text", 1)
c = PollOption("text_1", 1)
d = PollOption("text", 2)
e = Poll(123, "question", ["O1", "O2"], 1, False, True, Poll.REGULAR, True)
a = PollOption("text", 1, persistent_id="persistent_id")
b = PollOption("text", 1, persistent_id="persistent_id")
c = PollOption("other_text", 1, persistent_id="persistent_id")
d = PollOption("text", 1 + 9, persistent_id="persistent_id")
e = PollOption("text", 1, persistent_id="other_persistent_id")
f = Poll(
123,
"question",
["O1", "O2"],
1,
False,
True,
Poll.REGULAR,
True,
allows_revoting=True,
members_only=True,
)
assert a == b
assert hash(a) == hash(b)
@@ -209,6 +379,9 @@ class TestPollOptionWithoutRequest(PollOptionTestBase):
assert a != e
assert hash(a) != hash(e)
assert a != f
assert hash(a) != hash(f)
@pytest.fixture(scope="module")
def poll_answer():
@@ -257,13 +430,20 @@ class TestPollAnswerWithoutRequest(PollAnswerTestBase):
assert poll_answer_dict["voter_chat"] == poll_answer.voter_chat.to_dict()
assert poll_answer_dict["option_persistent_ids"] == list(poll_answer.option_persistent_ids)
def test_persistent_id_required_workaround(self):
# tags: deprecated NEXT.VERSION, bot api 9.6
with pytest.raises(TypeError, match="`option_persistent_ids` is a required"):
PollAnswer(poll_id=123, option_ids=[2], user=self.user, voter_chat=self.voter_chat)
def test_equality(self):
a = PollAnswer(123, [2], self.user, self.voter_chat)
b = PollAnswer(123, [2], self.user, Chat(1, ""))
c = PollAnswer(123, [2], User(1, "first", False), self.voter_chat)
d = PollAnswer(123, [1, 2], self.user, self.voter_chat)
e = PollAnswer(456, [2], self.user, self.voter_chat)
f = PollOption("Text", 1)
a = PollAnswer(123, [2], self.user, self.voter_chat, option_persistent_ids=["2"])
b = PollAnswer(123, [2], self.user, Chat(1, ""), option_persistent_ids=["2"])
c = PollAnswer(
123, [2], User(1, "first", False), self.voter_chat, option_persistent_ids=["2"]
)
d = PollAnswer(123, [1, 2], self.user, self.voter_chat, option_persistent_ids=["1", "2"])
e = PollAnswer(456, [2], self.user, self.voter_chat, option_persistent_ids=["2"])
f = PollOption("Text", 1, persistent_id="persistent_id")
assert a == b
assert hash(a) == hash(b)
@@ -298,9 +478,13 @@ def poll():
close_date=PollTestBase.close_date,
question_entities=PollTestBase.question_entities,
allows_revoting=PollTestBase.allows_revoting,
members_only=PollTestBase.members_only,
correct_option_ids=PollTestBase.correct_option_ids,
description=PollTestBase.description,
description_entities=PollTestBase.description_entities,
country_codes=PollTestBase.country_codes,
media=PollTestBase.media,
explanation_media=PollTestBase.explanation_media,
)
poll._unfreeze()
return poll
@@ -309,12 +493,16 @@ def poll():
class PollTestBase:
id_ = "id"
question = "Test Question?"
options = [PollOption("test", 10), PollOption("test2", 11)]
options = [
PollOption("test", 10, persistent_id="persistent_id"),
PollOption("test2", 11, persistent_id="persistent_id_2"),
]
total_voter_count = 0
is_closed = True
is_anonymous = False
type = Poll.REGULAR
allows_multiple_answers = True
members_only = True
explanation = (
b"\\U0001f469\\u200d\\U0001f469\\u200d\\U0001f467"
b"\\u200d\\U0001f467\\U0001f431http://google.com"
@@ -330,6 +518,9 @@ class PollTestBase:
correct_option_ids = [1, 2]
description = "description"
description_entities = [MessageEntity(MessageEntity.ITALIC, 0, 11)]
country_codes = ["AB", "CD"]
media = PollMedia(document=Document("file_id", "file_unique_id", "file_name", 42))
explanation_media = PollMedia(animation=Animation("blah", "unique_id", 320, 180, 1))
class TestPollWithoutRequest(PollTestBase):
@@ -349,9 +540,13 @@ class TestPollWithoutRequest(PollTestBase):
"close_date": to_timestamp(self.close_date),
"question_entities": [e.to_dict() for e in self.question_entities],
"allows_revoting": self.allows_revoting,
"members_only": self.members_only,
"correct_option_ids": self.correct_option_ids,
"description": self.description,
"description_entities": [e.to_dict() for e in self.description_entities],
"country_codes": self.country_codes,
"media": self.media.to_dict(),
"explanation_media": self.explanation_media.to_dict(),
}
poll = Poll.de_json(json_dict, offline_bot)
assert poll.api_kwargs == {}
@@ -368,6 +563,7 @@ class TestPollWithoutRequest(PollTestBase):
assert poll.is_anonymous == self.is_anonymous
assert poll.type == self.type
assert poll.allows_multiple_answers == self.allows_multiple_answers
assert poll.members_only == self.members_only
assert poll.explanation == self.explanation
assert poll.explanation_entities == tuple(self.explanation_entities)
assert poll._open_period == self.open_period
@@ -378,6 +574,9 @@ class TestPollWithoutRequest(PollTestBase):
assert poll.correct_option_ids == tuple(self.correct_option_ids)
assert poll.description == self.description
assert poll.description_entities == tuple(self.description_entities)
assert poll.country_codes == tuple(self.country_codes)
assert poll.media == self.media
assert poll.explanation_media == self.explanation_media
def test_de_json_localization(self, tz_bot, offline_bot, raw_bot):
json_dict = {
@@ -395,9 +594,13 @@ class TestPollWithoutRequest(PollTestBase):
"close_date": to_timestamp(self.close_date),
"question_entities": [e.to_dict() for e in self.question_entities],
"allows_revoting": self.allows_revoting,
"members_only": self.members_only,
"correct_option_ids": self.correct_option_ids,
"description": self.description,
"description_entities": [e.to_dict() for e in self.description_entities],
"country_codes": self.country_codes,
"media": self.media.to_dict(),
"explanation_media": self.explanation_media.to_dict(),
}
poll_raw = Poll.de_json(json_dict, raw_bot)
@@ -426,6 +629,7 @@ class TestPollWithoutRequest(PollTestBase):
assert poll_dict["is_anonymous"] == poll.is_anonymous
assert poll_dict["type"] == poll.type
assert poll_dict["allows_multiple_answers"] == poll.allows_multiple_answers
assert poll_dict["members_only"] == poll.members_only
assert poll_dict["explanation"] == poll.explanation
assert poll_dict["explanation_entities"] == [poll.explanation_entities[0].to_dict()]
assert poll_dict["open_period"] == int(self.open_period.total_seconds())
@@ -437,6 +641,9 @@ class TestPollWithoutRequest(PollTestBase):
assert poll_dict["description_entities"] == [
e.to_dict() for e in poll.description_entities
]
assert poll_dict["country_codes"] == list(poll.country_codes)
assert poll_dict["media"] == poll.media.to_dict()
assert poll_dict["explanation_media"] == poll.explanation_media.to_dict()
def test_time_period_properties(self, PTB_TIMEDELTA, poll):
if PTB_TIMEDELTA:
@@ -473,14 +680,79 @@ class TestPollWithoutRequest(PollTestBase):
PollTestBase.type,
PollTestBase.allows_multiple_answers,
correct_option_id=1,
allows_revoting=PollTestBase.allows_revoting,
members_only=PollTestBase.members_only,
)
assert poll.correct_option_ids == (1,)
def test_allows_revoting_required_workaround(self):
# tags: deprecated NEXT.VERSION, bot api 9.6
with pytest.raises(TypeError, match="`allows_revoting` is a required"):
Poll(
self.id_,
self.question,
self.options,
self.total_voter_count,
self.is_closed,
self.is_anonymous,
self.type,
self.allows_multiple_answers,
members_only=self.members_only,
)
def test_members_only_required_workaround(self):
# tags: deprecated NEXT.VERSION, bot api 10.0
with pytest.raises(TypeError, match="`members_only` is a required"):
Poll(
self.id_,
self.question,
self.options,
self.total_voter_count,
self.is_closed,
self.is_anonymous,
self.type,
self.allows_multiple_answers,
allows_revoting=self.allows_revoting,
)
def test_equality(self):
a = Poll(123, "question", ["O1", "O2"], 1, False, True, Poll.REGULAR, True)
b = Poll(123, "question", ["o1", "o2"], 1, True, False, Poll.REGULAR, True)
c = Poll(456, "question", ["o1", "o2"], 1, True, False, Poll.REGULAR, True)
d = PollOption("Text", 1)
a = Poll(
123,
"question",
["O1", "O2"],
1,
False,
True,
Poll.REGULAR,
True,
allows_revoting=True,
members_only=True,
)
b = Poll(
123,
"question",
["o1", "o2"],
1,
True,
False,
Poll.REGULAR,
True,
allows_revoting=False,
members_only=False,
)
c = Poll(
456,
"question",
["o1", "o2"],
1,
True,
False,
Poll.REGULAR,
True,
allows_revoting=True,
members_only=True,
)
d = PollOption("Text", 1, persistent_id="persistent_id")
assert a == b
assert hash(a) == hash(b)
@@ -501,6 +773,8 @@ class TestPollWithoutRequest(PollTestBase):
is_closed=False,
is_anonymous=False,
allows_multiple_answers=False,
allows_revoting=True,
members_only=True,
)
assert poll.type == "foo"
poll = Poll(
@@ -512,6 +786,8 @@ class TestPollWithoutRequest(PollTestBase):
is_closed=False,
is_anonymous=False,
allows_multiple_answers=False,
allows_revoting=True,
members_only=True,
)
assert poll.type is PollType.QUIZ
@@ -525,12 +801,14 @@ class TestPollWithoutRequest(PollTestBase):
Poll(
"id",
"question",
[PollOption("text", voter_count=0)],
[PollOption("text", voter_count=0, persistent_id="persistent_id")],
total_voter_count=0,
is_closed=False,
is_anonymous=False,
type=Poll.QUIZ,
allows_multiple_answers=False,
allows_revoting=True,
members_only=True,
).parse_explanation_entity(entity)
def test_parse_explanation_entities(self, poll):
@@ -545,12 +823,14 @@ class TestPollWithoutRequest(PollTestBase):
Poll(
"id",
"question",
[PollOption("text", voter_count=0)],
[PollOption("text", voter_count=0, persistent_id="persistent_id")],
total_voter_count=0,
is_closed=False,
is_anonymous=False,
type=Poll.QUIZ,
allows_multiple_answers=False,
allows_revoting=True,
members_only=True,
).parse_explanation_entities()
def test_parse_question_entity(self, poll):
@@ -576,12 +856,14 @@ class TestPollWithoutRequest(PollTestBase):
Poll(
"id",
"question",
[PollOption("text", voter_count=0)],
[PollOption("text", voter_count=0, persistent_id="persistent_id")],
total_voter_count=0,
is_closed=False,
is_anonymous=False,
type=Poll.QUIZ,
allows_multiple_answers=False,
allows_revoting=True,
members_only=True,
).parse_description_entity(entity)
def test_parse_description_entities(self, poll):
@@ -595,12 +877,14 @@ class TestPollWithoutRequest(PollTestBase):
Poll(
"id",
"question",
[PollOption("text", voter_count=0)],
[PollOption("text", voter_count=0, persistent_id="persistent_id")],
total_voter_count=0,
is_closed=False,
is_anonymous=False,
type=Poll.QUIZ,
allows_multiple_answers=False,
allows_revoting=True,
members_only=True,
).parse_description_entities()
+15
View File
@@ -29,6 +29,7 @@ from telegram import (
ExternalReplyInfo,
Giveaway,
LinkPreviewOptions,
LivePhoto,
MessageEntity,
MessageOriginUser,
PaidMediaInfo,
@@ -50,6 +51,7 @@ def external_reply_info():
giveaway=ExternalReplyInfoTestBase.giveaway,
paid_media=ExternalReplyInfoTestBase.paid_media,
checklist=ExternalReplyInfoTestBase.checklist,
live_photo=ExternalReplyInfoTestBase.live_photo,
)
@@ -73,6 +75,16 @@ class ExternalReplyInfoTestBase:
ChecklistTask(text="Item 2", id=2),
],
)
live_photo = LivePhoto(
file_id="file_id",
file_unique_id="file_unique_id",
width=100,
height=100,
duration=dtm.timedelta(seconds=10),
photo=[],
mime_type="image/jpeg",
file_size=1024,
)
class TestExternalReplyInfoWithoutRequest(ExternalReplyInfoTestBase):
@@ -92,6 +104,7 @@ class TestExternalReplyInfoWithoutRequest(ExternalReplyInfoTestBase):
"giveaway": self.giveaway.to_dict(),
"paid_media": self.paid_media.to_dict(),
"checklist": self.checklist.to_dict(),
"live_photo": self.live_photo.to_dict(),
}
external_reply_info = ExternalReplyInfo.de_json(json_dict, offline_bot)
@@ -104,6 +117,7 @@ class TestExternalReplyInfoWithoutRequest(ExternalReplyInfoTestBase):
assert external_reply_info.giveaway == self.giveaway
assert external_reply_info.paid_media == self.paid_media
assert external_reply_info.checklist == self.checklist
assert external_reply_info.live_photo == self.live_photo
def test_to_dict(self, external_reply_info):
ext_reply_info_dict = external_reply_info.to_dict()
@@ -116,6 +130,7 @@ class TestExternalReplyInfoWithoutRequest(ExternalReplyInfoTestBase):
assert ext_reply_info_dict["giveaway"] == self.giveaway.to_dict()
assert ext_reply_info_dict["paid_media"] == self.paid_media.to_dict()
assert ext_reply_info_dict["checklist"] == self.checklist.to_dict()
assert ext_reply_info_dict["live_photo"] == self.live_photo.to_dict()
def test_equality(self, external_reply_info):
a = external_reply_info
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2026
# 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 pytest
from telegram import SentGuestMessage
from tests.auxil.slots import mro_slots
@pytest.fixture(scope="module")
def sent_guest_message():
return SentGuestMessage(inline_message_id=SentGuestMessageTestBase.inline_message_id)
class SentGuestMessageTestBase:
inline_message_id = "123"
class TestSentGuestMessageWithoutRequest(SentGuestMessageTestBase):
def test_slot_behaviour(self, sent_guest_message):
inst = sent_guest_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_to_dict(self, sent_guest_message):
sent_guest_message_dict = sent_guest_message.to_dict()
assert isinstance(sent_guest_message_dict, dict)
assert sent_guest_message_dict["inline_message_id"] == self.inline_message_id
def test_de_json(self, offline_bot):
data = {"inline_message_id": self.inline_message_id}
m = SentGuestMessage.de_json(data, None)
assert m.api_kwargs == {}
assert m.inline_message_id == self.inline_message_id
def test_equality(self):
a = SentGuestMessage(self.inline_message_id)
b = SentGuestMessage(self.inline_message_id)
c = SentGuestMessage("not_inline_message_id")
assert a == b
assert hash(a) == hash(b)
assert a is not b
assert a != c
assert hash(a) != hash(c)
+32 -7
View File
@@ -156,6 +156,14 @@ managed_bot = ManagedBotUpdated(
user=User(1, "creator", True),
bot=User(2, "bot", True),
)
guest_message = Message(
1,
dtm.datetime.utcnow(),
Chat(1, ""),
User(1, "", False),
sender_chat=Chat(1, ""),
)
params = [
{"message": message},
@@ -171,17 +179,31 @@ params = [
)
},
{"pre_checkout_query": PreCheckoutQuery("id", User(1, "", False), "", 0, "")},
{"poll": Poll("id", "?", [PollOption(".", 1)], False, False, False, Poll.REGULAR, True)},
{
"poll": Poll(
"id",
"?",
[PollOption(text=".", voter_count=1, persistent_id="persistent_id")],
False,
False,
False,
Poll.REGULAR,
True,
allows_revoting=True,
members_only=True,
)
},
{
"poll_answer": PollAnswer(
"id",
[1],
User(
poll_id="id",
option_ids=[1],
option_persistent_ids=["1"],
user=User(
1,
"",
False,
),
Chat(1, ""),
voter_chat=Chat(1, ""),
)
},
{"my_chat_member": chat_member_updated},
@@ -197,6 +219,7 @@ params = [
{"edited_business_message": business_message},
{"purchased_paid_media": purchased_paid_media},
{"managed_bot": managed_bot},
{"guest_message": guest_message},
# Must be last to conform with `ids` below!
{"callback_query": CallbackQuery(1, User(1, "", False), "chat")},
]
@@ -226,6 +249,7 @@ all_types = (
"edited_business_message",
"purchased_paid_media",
"managed_bot",
"guest_message",
)
ids = (*all_types, "callback_query_without_message")
@@ -332,7 +356,7 @@ class TestUpdateWithoutRequest(UpdateTestBase):
def test_effective_sender_non_anonymous(self, update):
update = deepcopy(update)
# Simulate 'Remain anonymous' being turned off
if message := (update.message or update.edited_message):
if message := (update.message or update.edited_message or update.guest_message):
message._unfreeze()
message.sender_chat = None
elif reaction := (update.message_reaction):
@@ -365,7 +389,7 @@ class TestUpdateWithoutRequest(UpdateTestBase):
def test_effective_sender_anonymous(self, update):
update = deepcopy(update)
# Simulate 'Remain anonymous' being turned on
if message := (update.message or update.edited_message):
if message := (update.message or update.edited_message or update.guest_message):
message._unfreeze()
message.from_user = None
elif reaction := (update.message_reaction):
@@ -391,6 +415,7 @@ class TestUpdateWithoutRequest(UpdateTestBase):
or update.edited_channel_post
or update.message_reaction
or update.poll_answer
or update.guest_message
):
assert isinstance(sender, Chat)
else:
+132
View File
@@ -47,6 +47,7 @@ def json_dict():
"has_topics_enabled": UserTestBase.has_topics_enabled,
"allows_users_to_create_topics": UserTestBase.allows_users_to_create_topics,
"can_manage_bots": UserTestBase.can_manage_bots,
"supports_guest_queries": UserTestBase.supports_guest_queries,
}
@@ -69,6 +70,7 @@ def user(bot):
has_topics_enabled=UserTestBase.has_topics_enabled,
allows_users_to_create_topics=UserTestBase.allows_users_to_create_topics,
can_manage_bots=UserTestBase.can_manage_bots,
supports_guest_queries=UserTestBase.supports_guest_queries,
)
user.set_bot(bot)
user._unfreeze()
@@ -92,6 +94,7 @@ class UserTestBase:
has_topics_enabled = False
allows_users_to_create_topics = False
can_manage_bots = True
supports_guest_queries = False
class TestUserWithoutRequest(UserTestBase):
@@ -120,6 +123,7 @@ class TestUserWithoutRequest(UserTestBase):
assert user.has_topics_enabled == self.has_topics_enabled
assert user.allows_users_to_create_topics == self.allows_users_to_create_topics
assert user.can_manage_bots == self.can_manage_bots
assert user.supports_guest_queries == self.supports_guest_queries
def test_to_dict(self, user):
user_dict = user.to_dict()
@@ -141,6 +145,7 @@ class TestUserWithoutRequest(UserTestBase):
assert user_dict["has_topics_enabled"] == user.has_topics_enabled
assert user_dict["allows_users_to_create_topics"] == user.allows_users_to_create_topics
assert user_dict["can_manage_bots"] == user.can_manage_bots
assert user_dict["supports_guest_queries"] == user.supports_guest_queries
def test_equality(self):
a = User(self.id_, self.first_name, self.is_bot, self.last_name)
@@ -291,6 +296,21 @@ class TestUserWithoutRequest(UserTestBase):
monkeypatch.setattr(user.get_bot(), "send_photo", make_assertion)
assert await user.send_photo("test_photo")
async def test_instance_method_send_live_photo(self, monkeypatch, user):
async def make_assertion(*_, **kwargs):
return (
kwargs["chat_id"] == user.id
and kwargs["live_photo"] == "test_live_photo"
and kwargs["photo"] == "test_photo"
)
assert check_shortcut_signature(User.send_live_photo, Bot.send_live_photo, ["chat_id"], [])
assert await check_shortcut_call(user.send_live_photo, user.get_bot(), "send_live_photo")
assert await check_defaults_handling(user.send_live_photo, user.get_bot())
monkeypatch.setattr(user.get_bot(), "send_live_photo", make_assertion)
assert await user.send_live_photo("test_live_photo", "test_photo")
async def test_instance_method_send_media_group(self, monkeypatch, user):
async def make_assertion(*_, **kwargs):
return kwargs["chat_id"] == user.id and kwargs["media"] == "test_media_group"
@@ -926,3 +946,115 @@ class TestUserWithoutRequest(UserTestBase):
monkeypatch.setattr(user.get_bot(), "replace_managed_bot_token", make_assertion)
assert await user.replace_token()
async def test_instance_method_get_managed_bot_access_settings(self, monkeypatch, user):
async def make_assertion(*_, **kwargs):
return kwargs["user_id"] == user.id
assert check_shortcut_signature(
user.get_managed_bot_access_settings,
Bot.get_managed_bot_access_settings,
["user_id"],
[],
)
assert await check_shortcut_call(
user.get_managed_bot_access_settings,
user.get_bot(),
"get_managed_bot_access_settings",
)
assert await check_defaults_handling(user.get_managed_bot_access_settings, user.get_bot())
monkeypatch.setattr(user.get_bot(), "get_managed_bot_access_settings", make_assertion)
assert await user.get_managed_bot_access_settings()
async def test_instance_method_set_managed_bot_access_settings(self, monkeypatch, user):
async def make_assertion(*_, **kwargs):
return (
kwargs["user_id"] == user.id
and kwargs["is_access_restricted"] is True
and kwargs["added_user_ids"] == [123]
)
assert check_shortcut_signature(
user.set_managed_bot_access_settings,
Bot.set_managed_bot_access_settings,
["user_id"],
[],
)
assert await check_shortcut_call(
user.set_managed_bot_access_settings,
user.get_bot(),
"set_managed_bot_access_settings",
)
assert await check_defaults_handling(user.set_managed_bot_access_settings, user.get_bot())
monkeypatch.setattr(user.get_bot(), "set_managed_bot_access_settings", make_assertion)
assert await user.set_managed_bot_access_settings(
is_access_restricted=True,
added_user_ids=[123],
)
async def test_instance_method_get_personal_chat_messages(self, monkeypatch, user):
async def make_assertion(*_, **kwargs):
return kwargs["user_id"] == user.id and kwargs["limit"] == 2
assert check_shortcut_signature(
user.get_personal_chat_messages,
Bot.get_user_personal_chat_messages,
["user_id"],
[],
)
assert await check_shortcut_call(
user.get_personal_chat_messages,
user.get_bot(),
"get_user_personal_chat_messages",
)
assert await check_defaults_handling(user.get_personal_chat_messages, user.get_bot())
monkeypatch.setattr(user.get_bot(), "get_user_personal_chat_messages", make_assertion)
assert await user.get_personal_chat_messages(limit=2)
async def test_instance_method_delete_reaction(self, monkeypatch, user):
async def make_assertion(*_, **kwargs):
return (
kwargs["user_id"] == user.id
and kwargs["chat_id"] == 1234
and kwargs["message_id"] == 123
and kwargs["actor_chat_id"] == 42
)
assert check_shortcut_signature(
user.delete_reaction, Bot.delete_message_reaction, ["user_id"], []
)
assert await check_shortcut_call(
user.delete_reaction,
user.get_bot(),
"delete_message_reaction",
shortcut_kwargs=["user_id"],
)
assert await check_defaults_handling(user.delete_reaction, user.get_bot())
monkeypatch.setattr(user.get_bot(), "delete_message_reaction", make_assertion)
assert await user.delete_reaction(chat_id=1234, message_id=123, actor_chat_id=42)
async def test_instance_method_delete_all_reactions(self, monkeypatch, user):
async def make_assertion(*_, **kwargs):
return (
kwargs["user_id"] == user.id
and kwargs["chat_id"] == 1234
and kwargs["actor_chat_id"] == 42
)
assert check_shortcut_signature(
user.delete_all_reactions, Bot.delete_all_message_reactions, ["user_id"], []
)
assert await check_shortcut_call(
user.delete_all_reactions,
user.get_bot(),
"delete_all_message_reactions",
shortcut_kwargs=["user_id"],
)
assert await check_defaults_handling(user.delete_all_reactions, user.get_bot())
monkeypatch.setattr(user.get_bot(), "delete_all_message_reactions", make_assertion)
assert await user.delete_all_reactions(chat_id=1234, actor_chat_id=42)
Generated
+9 -5
View File
@@ -10,6 +10,10 @@ resolution-markers = [
"python_full_version < '3.11'",
]
[options]
exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
exclude-newer-span = "P7D"
[[package]]
name = "accessible-pygments"
version = "0.0.5"
@@ -1653,7 +1657,7 @@ all = [
{ name = "sphinx-copybutton", specifier = "==0.5.2" },
{ name = "sphinx-inline-tabs", specifier = "==2025.12.21.14" },
{ name = "sphinx-paramlinks", specifier = "==0.6.0" },
{ name = "sphinxcontrib-mermaid", specifier = "==2.0.1" },
{ name = "sphinxcontrib-mermaid", specifier = "==2.0.2" },
{ name = "tzdata" },
]
docs = [
@@ -1665,7 +1669,7 @@ docs = [
{ name = "sphinx-copybutton", specifier = "==0.5.2" },
{ name = "sphinx-inline-tabs", specifier = "==2025.12.21.14" },
{ name = "sphinx-paramlinks", specifier = "==0.6.0" },
{ name = "sphinxcontrib-mermaid", specifier = "==2.0.1" },
{ name = "sphinxcontrib-mermaid", specifier = "==2.0.2" },
]
linting = [
{ name = "mypy", specifier = "==1.20.2" },
@@ -2065,7 +2069,7 @@ wheels = [
[[package]]
name = "sphinxcontrib-mermaid"
version = "2.0.1"
version = "2.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jinja2" },
@@ -2074,9 +2078,9 @@ dependencies = [
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
{ name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2b/ae/999891de292919b66ea34f2c22fc22c9be90ab3536fbc0fca95716277351/sphinxcontrib_mermaid-2.0.1.tar.gz", hash = "sha256:a21a385a059a6cafd192aa3a586b14bf5c42721e229db67b459dc825d7f0a497", size = 19839, upload-time = "2026-03-05T14:10:41.901Z" }
sdist = { url = "https://files.pythonhosted.org/packages/19/75/3a1cc926da8c563c58ddc124a7b3fe5ccadcae96c96e3a6f8ac3653a210a/sphinxcontrib_mermaid-2.0.2.tar.gz", hash = "sha256:f09576c78ca93fa0e3034fd9c45aaffa7c44ab449de9c43b8b8d262afe52bc66", size = 19265, upload-time = "2026-05-05T13:59:02.959Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/46/25d64bcd7821c8d6f1080e1c43d5fcdfc442a18f759a230b5ccdc891093e/sphinxcontrib_mermaid-2.0.1-py3-none-any.whl", hash = "sha256:9dca7fbe827bad5e7e2b97c4047682cfd26e3e07398cfdc96c7a8842ae7f06e7", size = 14064, upload-time = "2026-03-05T14:10:40.533Z" },
{ url = "https://files.pythonhosted.org/packages/16/8d/93be7e0f7fa915a576859b3bfac7a7baa3303181c44d7db7eefbd3e8a69f/sphinxcontrib_mermaid-2.0.2-py3-none-any.whl", hash = "sha256:d862e514991279fb4816302c5cfe167d2557bf3ce7125ae0cb47dac80a0f46ce", size = 14094, upload-time = "2026-05-05T13:59:01.585Z" },
]
[[package]]