mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2026-06-19 07:35:19 +00:00
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:
committed by
GitHub
parent
ab2996713d
commit
0fb5678180
@@ -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
@@ -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"] },
|
||||
]
|
||||
@@ -52,6 +52,7 @@ PRIVATE_BASE_CLASSES = {
|
||||
"_BaseMedium": "TelegramObject",
|
||||
"_CredentialsBase": "TelegramObject",
|
||||
"_ChatBase": "TelegramObject",
|
||||
"_BaseInputMedia": "TelegramObject",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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:
|
||||
@@ -0,0 +1,6 @@
|
||||
InputMediaVenue
|
||||
===============
|
||||
|
||||
.. autoclass:: telegram.InputMediaVenue
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -0,0 +1,6 @@
|
||||
InputPaidMediaLivePhoto
|
||||
======================
|
||||
|
||||
.. autoclass:: telegram.InputPaidMediaLivePhoto
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -0,0 +1,6 @@
|
||||
InputPollMedia
|
||||
==============
|
||||
|
||||
.. versionadded:: NEXT.VERSION
|
||||
|
||||
.. autoclass:: telegram.InputPollMedia
|
||||
@@ -0,0 +1,6 @@
|
||||
InputPollOptionMedia
|
||||
====================
|
||||
|
||||
.. versionadded:: NEXT.VERSION
|
||||
|
||||
.. autoclass:: telegram.InputPollOptionMedia
|
||||
@@ -0,0 +1,6 @@
|
||||
LivePhoto
|
||||
=========
|
||||
|
||||
.. autoclass:: telegram.LivePhoto
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -0,0 +1,6 @@
|
||||
PaidMediaLivePhoto
|
||||
==================
|
||||
|
||||
.. autoclass:: telegram.PaidMediaLivePhoto
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -0,0 +1,6 @@
|
||||
PollMedia
|
||||
=========
|
||||
|
||||
.. autoclass:: telegram.PollMedia
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -0,0 +1,6 @@
|
||||
SentGuestMessage
|
||||
================
|
||||
|
||||
.. autoclass:: telegram.SentGuestMessage
|
||||
:members:
|
||||
:show-inheritance:
|
||||
@@ -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
@@ -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`"""
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
@@ -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
@@ -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`.
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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"},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]]
|
||||
|
||||
Reference in New Issue
Block a user