mirror of
https://github.com/python-telegram-bot/python-telegram-bot.git
synced 2025-01-19 23:43:51 +01:00
Merge branch 'master' into overhaul-tests
This commit is contained in:
commit
b768001c5a
81 changed files with 3437 additions and 396 deletions
4
.github/CONTRIBUTING.rst
vendored
4
.github/CONTRIBUTING.rst
vendored
|
@ -157,7 +157,7 @@ Check-list for PRs
|
|||
This checklist is a non-exhaustive reminder of things that should be done before a PR is merged, both for you as contributor and for the maintainers.
|
||||
Feel free to copy (parts of) the checklist to the PR description to remind you or the maintainers of open points or if you have questions on anything.
|
||||
|
||||
- Added ``.. versionadded:: NEXT.VERSION``, ``.. versionchanged:: NEXT.VERSION`` or ``.. deprecated:: NEXT.VERSION`` to the docstrings for user facing changes (for methods/class descriptions, arguments and attributes)
|
||||
- Added ``.. versionadded:: NEXT.VERSION``, ``.. versionchanged:: NEXT.VERSION``, ``.. deprecated:: NEXT.VERSION`` or ``.. versionremoved:: NEXT.VERSION`` to the docstrings for user facing changes (for methods/class descriptions, arguments and attributes)
|
||||
- Created new or adapted existing unit tests
|
||||
- Documented code changes according to the `CSI standard <https://standards.mousepawmedia.com/en/stable/csi.html>`__
|
||||
- Added myself alphabetically to ``AUTHORS.rst`` (optional)
|
||||
|
@ -276,7 +276,7 @@ This gives us the flexibility to re-order arguments and more importantly
|
|||
to add new required arguments. It's also more explicit and easier to read.
|
||||
|
||||
|
||||
.. _`Code of Conduct`: https://www.python.org/psf/conduct/
|
||||
.. _`Code of Conduct`: https://policies.python.org/python.org/code-of-conduct/
|
||||
.. _`issue tracker`: https://github.com/python-telegram-bot/python-telegram-bot/issues
|
||||
.. _`Telegram group`: https://telegram.me/pythontelegrambotgroup
|
||||
.. _`PEP 8 Style Guide`: https://peps.python.org/pep-0008/
|
||||
|
|
2
.github/ISSUE_TEMPLATE/question.yml
vendored
2
.github/ISSUE_TEMPLATE/question.yml
vendored
|
@ -12,6 +12,8 @@ body:
|
|||
To make it easier for us to help you, please read this [article](https://github.com/python-telegram-bot/python-telegram-bot/wiki/Ask-Right).
|
||||
|
||||
Please mind that there is also a users' [Telegram group](https://t.me/pythontelegrambotgroup) for questions about the library. Questions asked there might be answered quicker than here. Moreover, [GitHub Discussions](https://github.com/python-telegram-bot/python-telegram-bot/discussions) offer a slightly better format to discuss usage questions.
|
||||
|
||||
If you have asked the same question elsewhere (e.g. the [Telegram group](https://t.me/pythontelegrambotgroup) or [StackOverflow](https://stackoverflow.com/questions/tagged/python-telegram-bot)), provide a link to that thread.
|
||||
|
||||
- type: textarea
|
||||
id: issue-faced
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
ci:
|
||||
autofix_prs: false
|
||||
autoupdate_schedule: monthly
|
||||
autoupdate_schedule: quarterly
|
||||
autoupdate_commit_msg: 'Bump `pre-commit` Hooks to Latest Versions'
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
|
|
52
CHANGES.rst
52
CHANGES.rst
|
@ -4,6 +4,58 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
Version 21.2
|
||||
============
|
||||
|
||||
*Released 2024-05-20*
|
||||
|
||||
This is the technical changelog for version 21.2. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`_.
|
||||
|
||||
Major Changes
|
||||
-------------
|
||||
|
||||
- Full Support for Bot API 7.3 (:pr:`4246`, :pr:`4260`, :pr:`4243`, :pr:`4248`, :pr:`4242` closes :issue:`4236`, :pr:`4247` by `aelkheir <https://github.com/aelkheir>`_)
|
||||
- Remove Functionality Deprecated by Bot API 7.2 (:pr:`4245`)
|
||||
|
||||
New Features
|
||||
------------
|
||||
|
||||
- Add Version to ``PTBDeprecationWarning`` (:pr:`4262` closes :issue:`4261`)
|
||||
- Handle Exceptions in building ``CallbackContext`` (:pr:`4222`)
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Call ``Application.post_stop`` Only if ``Application.stop`` was called (:pr:`4211` closes :issue:`4210`)
|
||||
- Handle ``SystemExit`` raised in Handlers (:pr:`4157` closes :issue:`4155` and :issue:`4156`)
|
||||
- Make ``Birthdate.to_date`` Return a ``datetime.date`` Object (:pr:`4251`)
|
||||
|
||||
Documentation Improvements
|
||||
--------------------------
|
||||
|
||||
- Documentation Improvements (:pr:`4217`)
|
||||
|
||||
Internal Changes
|
||||
----------------
|
||||
|
||||
- Add New Rules to ``ruff`` Config (:pr:`4250`)
|
||||
- Adapt Test Suite to Changes in Error Messages (:pr:`4238`)
|
||||
|
||||
Dependency Updates
|
||||
------------------
|
||||
|
||||
- Bump ``furo`` from 2024.4.27 to 2024.5.6 (:pr:`4252`)
|
||||
- ``pre-commit`` autoupdate (:pr:`4239`)
|
||||
- Bump ``pytest`` from 8.1.1 to 8.2.0 (:pr:`4231`)
|
||||
- Bump ``dependabot/fetch-metadata`` from 2.0.0 to 2.1.0 (:pr:`4228`)
|
||||
- Bump ``pytest-asyncio`` from 0.21.1 to 0.21.2 (:pr:`4232`)
|
||||
- Bump ``pytest-xdist`` from 3.6.0 to 3.6.1 (:pr:`4233`)
|
||||
- Bump ``furo`` from 2024.1.29 to 2024.4.27 (:pr:`4230`)
|
||||
- Bump ``srvaroa/labeler`` from 1.10.0 to 1.10.1 (:pr:`4227`)
|
||||
- Bump ``pytest`` from 7.4.4 to 8.1.1 (:pr:`4218`)
|
||||
- Bump ``sphinx`` from 7.2.6 to 7.3.7 (:pr:`4215`)
|
||||
- Bump ``pytest-xdist`` from 3.5.0 to 3.6.0 (:pr:`4215`)
|
||||
|
||||
Version 21.1.1
|
||||
==============
|
||||
|
||||
|
|
|
@ -14,9 +14,9 @@
|
|||
:target: https://pypi.org/project/python-telegram-bot/
|
||||
:alt: Supported Python versions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Bot%20API-7.2-blue?logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Bot%20API-7.3-blue?logo=telegram
|
||||
:target: https://core.telegram.org/bots/api-changelog
|
||||
:alt: Supported Bot API versions
|
||||
:alt: Supported Bot API version
|
||||
|
||||
.. image:: https://img.shields.io/pypi/dm/python-telegram-bot
|
||||
:target: https://pypistats.org/packages/python-telegram-bot
|
||||
|
@ -89,7 +89,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju
|
|||
Telegram API support
|
||||
====================
|
||||
|
||||
All types and methods of the Telegram Bot API **7.2** are supported.
|
||||
All types and methods of the Telegram Bot API **7.3** are supported.
|
||||
|
||||
Installing
|
||||
==========
|
||||
|
|
|
@ -14,9 +14,9 @@
|
|||
:target: https://pypi.org/project/python-telegram-bot-raw/
|
||||
:alt: Supported Python versions
|
||||
|
||||
.. image:: https://img.shields.io/badge/Bot%20API-7.2-blue?logo=telegram
|
||||
.. image:: https://img.shields.io/badge/Bot%20API-7.3-blue?logo=telegram
|
||||
:target: https://core.telegram.org/bots/api-changelog
|
||||
:alt: Supported Bot API versions
|
||||
:alt: Supported Bot API version
|
||||
|
||||
.. image:: https://img.shields.io/pypi/dm/python-telegram-bot-raw
|
||||
:target: https://pypistats.org/packages/python-telegram-bot-raw
|
||||
|
@ -85,7 +85,7 @@ Installing both ``python-telegram-bot`` and ``python-telegram-bot-raw`` in conju
|
|||
Telegram API support
|
||||
====================
|
||||
|
||||
All types and methods of the Telegram Bot API **7.2** are supported.
|
||||
All types and methods of the Telegram Bot API **7.3** are supported.
|
||||
|
||||
Installing
|
||||
==========
|
||||
|
|
|
@ -20,9 +20,9 @@ author = "Leandro Toledo"
|
|||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = "21.1.1" # telegram.__version__[:3]
|
||||
version = "21.2" # telegram.__version__[:3]
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = "21.1.1" # telegram.__version__
|
||||
release = "21.2" # telegram.__version__
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
needs_sphinx = "6.1.3"
|
||||
|
@ -67,7 +67,9 @@ source_suffix = ".rst"
|
|||
master_doc = "index"
|
||||
|
||||
# Global substitutions
|
||||
rst_prolog = (Path.cwd() / "../substitutions/global.rst").read_text(encoding="utf-8")
|
||||
rst_prolog = ""
|
||||
for file in Path.cwd().glob("../substitutions/*.rst"):
|
||||
rst_prolog += "\n" + file.read_text(encoding="utf-8")
|
||||
|
||||
# -- Extension settings ------------------------------------------------
|
||||
napoleon_use_admonition_for_examples = True
|
||||
|
|
|
@ -28,6 +28,16 @@ Available Types
|
|||
telegram.callbackquery
|
||||
telegram.chat
|
||||
telegram.chatadministratorrights
|
||||
telegram.chatbackground
|
||||
telegram.backgroundtype
|
||||
telegram.backgroundtypefill
|
||||
telegram.backgroundtypewallpaper
|
||||
telegram.backgroundtypepattern
|
||||
telegram.backgroundtypechattheme
|
||||
telegram.backgroundfill
|
||||
telegram.backgroundfillsolid
|
||||
telegram.backgroundfillgradient
|
||||
telegram.backgroundfillfreeformgradient
|
||||
telegram.chatboost
|
||||
telegram.chatboostadded
|
||||
telegram.chatboostremoved
|
||||
|
@ -36,6 +46,7 @@ Available Types
|
|||
telegram.chatboostsourcegiveaway
|
||||
telegram.chatboostsourcepremium
|
||||
telegram.chatboostupdated
|
||||
telegram.chatfullinfo
|
||||
telegram.chatinvitelink
|
||||
telegram.chatjoinrequest
|
||||
telegram.chatlocation
|
||||
|
@ -77,6 +88,7 @@ Available Types
|
|||
telegram.inputmediadocument
|
||||
telegram.inputmediaphoto
|
||||
telegram.inputmediavideo
|
||||
telegram.inputpolloption
|
||||
telegram.inputsticker
|
||||
telegram.keyboardbutton
|
||||
telegram.keyboardbuttonpolltype
|
||||
|
|
8
docs/source/telegram.backgroundfill.rst
Normal file
8
docs/source/telegram.backgroundfill.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
BackgroundFill
|
||||
==============
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
.. autoclass:: telegram.BackgroundFill
|
||||
:members:
|
||||
:show-inheritance:
|
8
docs/source/telegram.backgroundfillfreeformgradient.rst
Normal file
8
docs/source/telegram.backgroundfillfreeformgradient.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
BackgroundFillFreeformGradient
|
||||
==============================
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
.. autoclass:: telegram.BackgroundFillFreeformGradient
|
||||
:members:
|
||||
:show-inheritance:
|
8
docs/source/telegram.backgroundfillgradient.rst
Normal file
8
docs/source/telegram.backgroundfillgradient.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
BackgroundFillGradient
|
||||
======================
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
.. autoclass:: telegram.BackgroundFillGradient
|
||||
:members:
|
||||
:show-inheritance:
|
8
docs/source/telegram.backgroundfillsolid.rst
Normal file
8
docs/source/telegram.backgroundfillsolid.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
BackgroundFillSolid
|
||||
===================
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
.. autoclass:: telegram.BackgroundFillSolid
|
||||
:members:
|
||||
:show-inheritance:
|
8
docs/source/telegram.backgroundtype.rst
Normal file
8
docs/source/telegram.backgroundtype.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
BackgroundType
|
||||
==============
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
.. autoclass:: telegram.BackgroundType
|
||||
:members:
|
||||
:show-inheritance:
|
8
docs/source/telegram.backgroundtypechattheme.rst
Normal file
8
docs/source/telegram.backgroundtypechattheme.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
BackgroundTypeChatTheme
|
||||
=======================
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
.. autoclass:: telegram.BackgroundTypeChatTheme
|
||||
:members:
|
||||
:show-inheritance:
|
8
docs/source/telegram.backgroundtypefill.rst
Normal file
8
docs/source/telegram.backgroundtypefill.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
BackgroundTypeFill
|
||||
==================
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
.. autoclass:: telegram.BackgroundTypeFill
|
||||
:members:
|
||||
:show-inheritance:
|
8
docs/source/telegram.backgroundtypepattern.rst
Normal file
8
docs/source/telegram.backgroundtypepattern.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
BackgroundTypePattern
|
||||
=====================
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
.. autoclass:: telegram.BackgroundTypePattern
|
||||
:members:
|
||||
:show-inheritance:
|
8
docs/source/telegram.backgroundtypewallpaper.rst
Normal file
8
docs/source/telegram.backgroundtypewallpaper.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
BackgroundTypeWallpaper
|
||||
=======================
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
.. autoclass:: telegram.BackgroundTypeWallpaper
|
||||
:members:
|
||||
:show-inheritance:
|
8
docs/source/telegram.chatbackground.rst
Normal file
8
docs/source/telegram.chatbackground.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
ChatBackground
|
||||
==============
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
.. autoclass:: telegram.ChatBackground
|
||||
:members:
|
||||
:show-inheritance:
|
6
docs/source/telegram.chatfullinfo.rst
Normal file
6
docs/source/telegram.chatfullinfo.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
ChatFullInfo
|
||||
============
|
||||
|
||||
.. autoclass:: telegram.ChatFullInfo
|
||||
:members:
|
||||
:show-inheritance:
|
6
docs/source/telegram.inputpolloption.rst
Normal file
6
docs/source/telegram.inputpolloption.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
InputPollOption
|
||||
===============
|
||||
|
||||
.. autoclass:: telegram.InputPollOption
|
||||
:members:
|
||||
:show-inheritance:
|
1
docs/substitutions/application.rst
Normal file
1
docs/substitutions/application.rst
Normal file
|
@ -0,0 +1 @@
|
|||
.. |app_run_shutdown| replace:: The app will shut down when :exc:`KeyboardInterrupt` or :exc:`SystemExit` is raised. This also works from within handlers, error handlers and jobs. However, using :meth:`~telegram.ext.Application.stop_running` will give a somewhat cleaner shutdown behavior than manually raising those exceptions. On unix, the app will also shut down on receiving the signals specified by
|
|
@ -22,6 +22,15 @@ __author__ = "devs@python-telegram-bot.org"
|
|||
__all__ = (
|
||||
"Animation",
|
||||
"Audio",
|
||||
"BackgroundFill",
|
||||
"BackgroundFillFreeformGradient",
|
||||
"BackgroundFillGradient",
|
||||
"BackgroundFillSolid",
|
||||
"BackgroundType",
|
||||
"BackgroundTypeChatTheme",
|
||||
"BackgroundTypeFill",
|
||||
"BackgroundTypePattern",
|
||||
"BackgroundTypeWallpaper",
|
||||
"Birthdate",
|
||||
"Bot",
|
||||
"BotCommand",
|
||||
|
@ -46,6 +55,7 @@ __all__ = (
|
|||
"CallbackQuery",
|
||||
"Chat",
|
||||
"ChatAdministratorRights",
|
||||
"ChatBackground",
|
||||
"ChatBoost",
|
||||
"ChatBoostAdded",
|
||||
"ChatBoostRemoved",
|
||||
|
@ -54,6 +64,7 @@ __all__ = (
|
|||
"ChatBoostSourceGiveaway",
|
||||
"ChatBoostSourcePremium",
|
||||
"ChatBoostUpdated",
|
||||
"ChatFullInfo",
|
||||
"ChatInviteLink",
|
||||
"ChatJoinRequest",
|
||||
"ChatLocation",
|
||||
|
@ -131,6 +142,7 @@ __all__ = (
|
|||
"InputMediaPhoto",
|
||||
"InputMediaVideo",
|
||||
"InputMessageContent",
|
||||
"InputPollOption",
|
||||
"InputSticker",
|
||||
"InputTextMessageContent",
|
||||
"InputVenueMessageContent",
|
||||
|
@ -258,6 +270,18 @@ from ._business import (
|
|||
from ._callbackquery import CallbackQuery
|
||||
from ._chat import Chat
|
||||
from ._chatadministratorrights import ChatAdministratorRights
|
||||
from ._chatbackground import (
|
||||
BackgroundFill,
|
||||
BackgroundFillFreeformGradient,
|
||||
BackgroundFillGradient,
|
||||
BackgroundFillSolid,
|
||||
BackgroundType,
|
||||
BackgroundTypeChatTheme,
|
||||
BackgroundTypeFill,
|
||||
BackgroundTypePattern,
|
||||
BackgroundTypeWallpaper,
|
||||
ChatBackground,
|
||||
)
|
||||
from ._chatboost import (
|
||||
ChatBoost,
|
||||
ChatBoostAdded,
|
||||
|
@ -269,6 +293,7 @@ from ._chatboost import (
|
|||
ChatBoostUpdated,
|
||||
UserChatBoosts,
|
||||
)
|
||||
from ._chatfullinfo import ChatFullInfo
|
||||
from ._chatinvitelink import ChatInviteLink
|
||||
from ._chatjoinrequest import ChatJoinRequest
|
||||
from ._chatlocation import ChatLocation
|
||||
|
@ -403,7 +428,7 @@ from ._payment.shippingaddress import ShippingAddress
|
|||
from ._payment.shippingoption import ShippingOption
|
||||
from ._payment.shippingquery import ShippingQuery
|
||||
from ._payment.successfulpayment import SuccessfulPayment
|
||||
from ._poll import Poll, PollAnswer, PollOption
|
||||
from ._poll import InputPollOption, Poll, PollAnswer, PollOption
|
||||
from ._proximityalerttriggered import ProximityAlertTriggered
|
||||
from ._reaction import ReactionCount, ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji
|
||||
from ._reply import ExternalReplyInfo, ReplyParameters, TextQuote
|
||||
|
|
|
@ -26,7 +26,7 @@ from telegram._utils.types import JSONDict
|
|||
|
||||
class Birthdate(TelegramObject):
|
||||
"""
|
||||
This object represents a user's birthday.
|
||||
This object describes the birthdate of a user.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`day`, and :attr:`month` are equal.
|
||||
|
@ -71,9 +71,9 @@ class Birthdate(TelegramObject):
|
|||
self._freeze()
|
||||
|
||||
def to_date(self, year: Optional[int] = None) -> date:
|
||||
"""Return the birthdate as a datetime object.
|
||||
"""Return the birthdate as a date object.
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
.. versionchanged:: 21.2
|
||||
Now returns a :obj:`datetime.date` object instead of a :obj:`datetime.datetime` object,
|
||||
as was originally intended.
|
||||
|
||||
|
|
|
@ -58,9 +58,9 @@ from telegram._botcommandscope import BotCommandScope
|
|||
from telegram._botdescription import BotDescription, BotShortDescription
|
||||
from telegram._botname import BotName
|
||||
from telegram._business import BusinessConnection
|
||||
from telegram._chat import Chat
|
||||
from telegram._chatadministratorrights import ChatAdministratorRights
|
||||
from telegram._chatboost import UserChatBoosts
|
||||
from telegram._chatfullinfo import ChatFullInfo
|
||||
from telegram._chatinvitelink import ChatInviteLink
|
||||
from telegram._chatmember import ChatMember
|
||||
from telegram._chatpermissions import ChatPermissions
|
||||
|
@ -84,7 +84,7 @@ from telegram._inline.inlinequeryresultsbutton import InlineQueryResultsButton
|
|||
from telegram._menubutton import MenuButton
|
||||
from telegram._message import Message
|
||||
from telegram._messageid import MessageId
|
||||
from telegram._poll import Poll
|
||||
from telegram._poll import InputPollOption, Poll
|
||||
from telegram._reaction import ReactionType, ReactionTypeCustomEmoji, ReactionTypeEmoji
|
||||
from telegram._reply import ReplyParameters
|
||||
from telegram._sentwebappmessage import SentWebAppMessage
|
||||
|
@ -524,7 +524,10 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
|
|||
|
||||
@classmethod
|
||||
def _warn(
|
||||
cls, message: str, category: Type[Warning] = PTBUserWarning, stacklevel: int = 0
|
||||
cls,
|
||||
message: Union[str, PTBUserWarning],
|
||||
category: Type[Warning] = PTBUserWarning,
|
||||
stacklevel: int = 0,
|
||||
) -> None:
|
||||
"""Convenience method to issue a warning. This method is here mostly to make it easier
|
||||
for ExtBot to add 1 level to all warning calls.
|
||||
|
@ -837,7 +840,6 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
|
|||
f"Please use 'Bot.{endpoint}' instead of "
|
||||
f"'Bot.do_api_request(\"{endpoint}\", ...)'"
|
||||
),
|
||||
PTBDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
@ -2287,9 +2289,10 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
|
|||
"""
|
||||
Use this method to send audio files, if you want Telegram clients to display the file
|
||||
as a playable voice message. For this to work, your audio must be in an ``.ogg`` file
|
||||
encoded with OPUS (other formats may be sent as Audio or Document). Bots can currently
|
||||
send voice messages of up to :tg-const:`telegram.constants.FileSizeLimit.FILESIZE_UPLOAD`
|
||||
in size, this limit may be changed in the future.
|
||||
encoded with OPUS , or in .MP3 format, or in .M4A format (other formats may be sent as
|
||||
:class:`~telegram.Audio` or :class:`~telegram.Document`). Bots can currently send voice
|
||||
messages of up to :tg-const:`telegram.constants.FileSizeLimit.FILESIZE_UPLOAD` in size,
|
||||
this limit may be changed in the future.
|
||||
|
||||
Note:
|
||||
To use this method, the file must have the type :mimetype:`audio/ogg` and be no more
|
||||
|
@ -2610,7 +2613,9 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
|
|||
live_period (:obj:`int`, optional): Period in seconds for which the location will be
|
||||
updated, should be between
|
||||
:tg-const:`telegram.constants.LocationLimit.MIN_LIVE_PERIOD` and
|
||||
:tg-const:`telegram.constants.LocationLimit.MAX_LIVE_PERIOD`.
|
||||
:tg-const:`telegram.constants.LocationLimit.MAX_LIVE_PERIOD`, or
|
||||
:tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` for live
|
||||
locations that can be edited indefinitely.
|
||||
heading (:obj:`int`, optional): For live locations, a direction in which the user is
|
||||
moving, in degrees. Must be between
|
||||
:tg-const:`telegram.constants.LocationLimit.MIN_HEADING` and
|
||||
|
@ -2720,6 +2725,7 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
|
|||
horizontal_accuracy: Optional[float] = None,
|
||||
heading: Optional[int] = None,
|
||||
proximity_alert_radius: Optional[int] = None,
|
||||
live_period: Optional[int] = None,
|
||||
*,
|
||||
location: Optional[Location] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
|
@ -2758,6 +2764,15 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
|
|||
if specified.
|
||||
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for a new
|
||||
inline keyboard.
|
||||
live_period (:obj:`int`, optional): New period in seconds during which the location
|
||||
can be updated, starting from the message send date. If
|
||||
:tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` is specified,
|
||||
then the location can be updated forever. Otherwise, the new value must not exceed
|
||||
the current ``live_period`` by more than a day, and the live location expiration
|
||||
date must remain within the next 90 days. If not specified, then ``live_period``
|
||||
remains unchanged
|
||||
|
||||
.. versionadded:: 21.2.
|
||||
|
||||
Keyword Args:
|
||||
location (:class:`telegram.Location`, optional): The location to send.
|
||||
|
@ -2790,6 +2805,7 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
|
|||
"horizontal_accuracy": horizontal_accuracy,
|
||||
"heading": heading,
|
||||
"proximity_alert_radius": proximity_alert_radius,
|
||||
"live_period": live_period,
|
||||
}
|
||||
|
||||
return await self._send_message(
|
||||
|
@ -4195,10 +4211,12 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
|
|||
except NotImplementedError:
|
||||
arg_read_timeout = 2
|
||||
self._warn(
|
||||
f"The class {self._request[0].__class__.__name__} does not override "
|
||||
"the property `read_timeout`. Overriding this property will be mandatory in "
|
||||
"future versions. Using 2 seconds as fallback.",
|
||||
PTBDeprecationWarning,
|
||||
PTBDeprecationWarning(
|
||||
"20.7",
|
||||
f"The class {self._request[0].__class__.__name__} does not override "
|
||||
"the property `read_timeout`. Overriding this property will be mandatory "
|
||||
"in future versions. Using 2 seconds as fallback.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
@ -4434,16 +4452,19 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
|
|||
connect_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
) -> Chat:
|
||||
) -> ChatFullInfo:
|
||||
"""
|
||||
Use this method to get up to date information about the chat (current name of the user for
|
||||
one-on-one conversations, current username of a user, group or channel, etc.).
|
||||
|
||||
.. versionchanged:: 21.2
|
||||
In accordance to Bot API 7.3, this method now returns a :class:`telegram.ChatFullInfo`.
|
||||
|
||||
Args:
|
||||
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
|
||||
|
||||
Returns:
|
||||
:class:`telegram.Chat`
|
||||
:class:`telegram.ChatFullInfo`
|
||||
|
||||
Raises:
|
||||
:class:`telegram.error.TelegramError`
|
||||
|
@ -4461,7 +4482,7 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
|
|||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
return Chat.de_json(result, self) # type: ignore[return-value]
|
||||
return ChatFullInfo.de_json(result, self) # type: ignore[return-value]
|
||||
|
||||
async def get_chat_administrators(
|
||||
self,
|
||||
|
@ -5311,7 +5332,8 @@ class Bot(TelegramObject, AsyncContextManager["Bot"]):
|
|||
|
||||
.. versionadded:: 20.6
|
||||
can_edit_stories (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can
|
||||
edit stories posted by other users.
|
||||
edit stories posted by other users, post stories to the chat page, pin chat
|
||||
stories, and access the chat's story archive
|
||||
|
||||
.. versionadded:: 20.6
|
||||
can_delete_stories (:obj:`bool`, optional): Pass :obj:`True`, if the administrator can
|
||||
|
@ -6338,7 +6360,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
|||
Removed the deprecated parameters mentioned above and adjusted the order of the
|
||||
parameters.
|
||||
|
||||
.. versionremoved:: NEXT.VERSION
|
||||
.. versionremoved:: 21.2
|
||||
Removed the deprecated parameter ``sticker_format``.
|
||||
|
||||
Args:
|
||||
|
@ -6798,7 +6820,7 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
|||
self,
|
||||
chat_id: Union[int, str],
|
||||
question: str,
|
||||
options: Sequence[str],
|
||||
options: Sequence[Union[str, "InputPollOption"]],
|
||||
is_anonymous: Optional[bool] = None,
|
||||
type: Optional[str] = None, # pylint: disable=redefined-builtin
|
||||
allows_multiple_answers: Optional[bool] = None,
|
||||
|
@ -6815,6 +6837,8 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
|||
message_thread_id: Optional[int] = None,
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
business_connection_id: Optional[str] = None,
|
||||
question_parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
question_entities: Optional[Sequence["MessageEntity"]] = None,
|
||||
*,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
|
@ -6831,14 +6855,20 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
|||
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
|
||||
question (:obj:`str`): Poll question, :tg-const:`telegram.Poll.MIN_QUESTION_LENGTH`-
|
||||
:tg-const:`telegram.Poll.MAX_QUESTION_LENGTH` characters.
|
||||
options (Sequence[:obj:`str`]): Sequence of answer options,
|
||||
options (Sequence[:obj:`str` | :class:`telegram.InputPollOption`]): Sequence of
|
||||
:tg-const:`telegram.Poll.MIN_OPTION_NUMBER`-
|
||||
:tg-const:`telegram.Poll.MAX_OPTION_NUMBER` strings
|
||||
:tg-const:`telegram.Poll.MAX_OPTION_NUMBER` answer options. Each option may either
|
||||
be a string with
|
||||
:tg-const:`telegram.Poll.MIN_OPTION_LENGTH`-
|
||||
:tg-const:`telegram.Poll.MAX_OPTION_LENGTH` characters each.
|
||||
:tg-const:`telegram.Poll.MAX_OPTION_LENGTH` characters or an
|
||||
:class:`~telegram.InputPollOption` object. Strings are converted to
|
||||
:class:`~telegram.InputPollOption` objects automatically.
|
||||
|
||||
.. versionchanged:: 20.0
|
||||
|sequenceargs|
|
||||
|
||||
.. versionchanged:: 21.2
|
||||
Bot API 7.3 adds support for :class:`~telegram.InputPollOption` objects.
|
||||
is_anonymous (:obj:`bool`, optional): :obj:`True`, if the poll needs to be anonymous,
|
||||
defaults to :obj:`True`.
|
||||
type (:obj:`str`, optional): Poll type, :tg-const:`telegram.Poll.QUIZ` or
|
||||
|
@ -6893,6 +6923,16 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
|||
business_connection_id (:obj:`str`, optional): |business_id_str|
|
||||
|
||||
.. versionadded:: 21.1
|
||||
question_parse_mode (:obj:`str`, optional): Mode for parsing entities in the question.
|
||||
See the constants in :class:`telegram.constants.ParseMode` for the available modes.
|
||||
Currently, only custom emoji entities are allowed.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
question_entities (Sequence[:class:`telegram.Message`], optional): Special entities
|
||||
that appear in the poll :paramref:`question`. It can be specified instead of
|
||||
:paramref:`question_parse_mode`.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Keyword Args:
|
||||
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
|
||||
|
@ -6924,7 +6964,10 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
|||
data: JSONDict = {
|
||||
"chat_id": chat_id,
|
||||
"question": question,
|
||||
"options": options,
|
||||
"options": [
|
||||
InputPollOption(option) if isinstance(option, str) else option
|
||||
for option in options
|
||||
],
|
||||
"explanation_parse_mode": explanation_parse_mode,
|
||||
"is_anonymous": is_anonymous,
|
||||
"type": type,
|
||||
|
@ -6935,6 +6978,8 @@ CUSTOM_EMOJI_IDENTIFIER_LIMIT` custom emoji identifiers can be specified.
|
|||
"explanation_entities": explanation_entities,
|
||||
"open_period": open_period,
|
||||
"close_date": close_date,
|
||||
"question_parse_mode": question_parse_mode,
|
||||
"question_entities": question_entities,
|
||||
}
|
||||
|
||||
return await self._send_message(
|
||||
|
|
|
@ -189,7 +189,7 @@ class BusinessMessagesDeleted(TelegramObject):
|
|||
|
||||
class BusinessIntro(TelegramObject):
|
||||
"""
|
||||
This object represents the intro of a business account.
|
||||
This object contains information about the start page settings of a Telegram Business account.
|
||||
|
||||
Objects of this class are comparable in terms of equality.
|
||||
Two objects of this class are considered equal, if their
|
||||
|
@ -246,7 +246,7 @@ class BusinessIntro(TelegramObject):
|
|||
|
||||
class BusinessLocation(TelegramObject):
|
||||
"""
|
||||
This object represents the location of a business account.
|
||||
This object contains information about the location of a Telegram Business account.
|
||||
|
||||
Objects of this class are comparable in terms of equality.
|
||||
Two objects of this class are considered equal, if their
|
||||
|
@ -298,7 +298,7 @@ class BusinessLocation(TelegramObject):
|
|||
|
||||
class BusinessOpeningHoursInterval(TelegramObject):
|
||||
"""
|
||||
This object represents the time intervals describing business opening hours.
|
||||
This object describes an interval of time during which a business is open.
|
||||
|
||||
Objects of this class are comparable in terms of equality.
|
||||
Two objects of this class are considered equal, if their
|
||||
|
@ -390,7 +390,7 @@ class BusinessOpeningHoursInterval(TelegramObject):
|
|||
|
||||
class BusinessOpeningHours(TelegramObject):
|
||||
"""
|
||||
This object represents the opening hours of a business account.
|
||||
This object describes the opening hours of a business.
|
||||
|
||||
Objects of this class are comparable in terms of equality.
|
||||
Two objects of this class are considered equal, if their
|
||||
|
|
|
@ -461,6 +461,7 @@ class CallbackQuery(TelegramObject):
|
|||
horizontal_accuracy: Optional[float] = None,
|
||||
heading: Optional[int] = None,
|
||||
proximity_alert_radius: Optional[int] = None,
|
||||
live_period: Optional[int] = None,
|
||||
*,
|
||||
location: Optional[Location] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
|
@ -509,6 +510,7 @@ class CallbackQuery(TelegramObject):
|
|||
horizontal_accuracy=horizontal_accuracy,
|
||||
heading=heading,
|
||||
proximity_alert_radius=proximity_alert_radius,
|
||||
live_period=live_period,
|
||||
chat_id=None,
|
||||
message_id=None,
|
||||
)
|
||||
|
@ -525,6 +527,7 @@ class CallbackQuery(TelegramObject):
|
|||
horizontal_accuracy=horizontal_accuracy,
|
||||
heading=heading,
|
||||
proximity_alert_radius=proximity_alert_radius,
|
||||
live_period=live_period,
|
||||
)
|
||||
|
||||
async def stop_message_live_location(
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
"""This module contains an object that represents a Telegram Chat."""
|
||||
from datetime import datetime
|
||||
from html import escape
|
||||
from typing import TYPE_CHECKING, Final, Optional, Sequence, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Any, Final, Optional, Sequence, Tuple, Union
|
||||
|
||||
from telegram import constants
|
||||
from telegram._birthdate import Birthdate
|
||||
|
@ -36,9 +36,11 @@ from telegram._utils.argumentparsing import parse_sequence_arg
|
|||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.types import CorrectOptionID, FileInput, JSONDict, ODVInput, ReplyMarkup
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.helpers import escape_markdown
|
||||
from telegram.helpers import mention_html as helpers_mention_html
|
||||
from telegram.helpers import mention_markdown as helpers_mention_markdown
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import (
|
||||
|
@ -57,6 +59,7 @@ if TYPE_CHECKING:
|
|||
InputMediaDocument,
|
||||
InputMediaPhoto,
|
||||
InputMediaVideo,
|
||||
InputPollOption,
|
||||
LabeledPrice,
|
||||
LinkPreviewOptions,
|
||||
Location,
|
||||
|
@ -74,6 +77,45 @@ if TYPE_CHECKING:
|
|||
)
|
||||
|
||||
|
||||
_deprecated_attrs = (
|
||||
"accent_color_id",
|
||||
"active_usernames",
|
||||
"available_reactions",
|
||||
"background_custom_emoji_id",
|
||||
"bio",
|
||||
"birthdate",
|
||||
"business_intro",
|
||||
"business_location",
|
||||
"business_opening_hours",
|
||||
"can_set_sticker_set",
|
||||
"custom_emoji_sticker_set_name",
|
||||
"description",
|
||||
"emoji_status_custom_emoji_id",
|
||||
"emoji_status_expiration_date",
|
||||
"has_aggressive_anti_spam_enabled",
|
||||
"has_hidden_members",
|
||||
"has_private_forwards",
|
||||
"has_protected_content",
|
||||
"has_restricted_voice_and_video_messages",
|
||||
"has_visible_history",
|
||||
"invite_link",
|
||||
"join_by_request",
|
||||
"join_to_send_messages",
|
||||
"linked_chat_id",
|
||||
"location",
|
||||
"message_auto_delete_time",
|
||||
"permissions",
|
||||
"personal_chat",
|
||||
"photo",
|
||||
"pinned_message",
|
||||
"profile_accent_color_id",
|
||||
"profile_background_custom_emoji_id",
|
||||
"slow_mode_delay",
|
||||
"sticker_set_name",
|
||||
"unrestrict_boost_count",
|
||||
)
|
||||
|
||||
|
||||
class Chat(TelegramObject):
|
||||
"""This object represents a chat.
|
||||
|
||||
|
@ -107,62 +149,134 @@ class Chat(TelegramObject):
|
|||
last_name (:obj:`str`, optional): Last name of the other party in a private chat.
|
||||
photo (:class:`telegram.ChatPhoto`, optional): Chat photo.
|
||||
Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
bio (:obj:`str`, optional): Bio of the other party in a private chat. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
has_private_forwards (:obj:`bool`, optional): :obj:`True`, if privacy settings of the other
|
||||
party in the private chat allows to use ``tg://user?id=<user_id>`` links only in chats
|
||||
with the user. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 13.9
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
description (:obj:`str`, optional): Description, for groups, supergroups and channel chats.
|
||||
Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
invite_link (:obj:`str`, optional): Primary invite link, for groups, supergroups and
|
||||
channel. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
pinned_message (:class:`telegram.Message`, optional): The most recent pinned message
|
||||
(by sending date). Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions,
|
||||
for groups and supergroups. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
slow_mode_delay (:obj:`int`, optional): For supergroups, the minimum allowed delay between
|
||||
consecutive messages sent by each unprivileged user.
|
||||
Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
message_auto_delete_time (:obj:`int`, optional): The time after which all messages sent to
|
||||
the chat will be automatically deleted; in seconds. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 13.4
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
has_protected_content (:obj:`bool`, optional): :obj:`True`, if messages from the chat can't
|
||||
be forwarded to other chats. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 13.9
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
has_visible_history (:obj:`bool`, optional): :obj:`True`, if new chat members will have
|
||||
access to old messages; available only to chat administrators. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
sticker_set_name (:obj:`str`, optional): For supergroups, name of group sticker set.
|
||||
Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
can_set_sticker_set (:obj:`bool`, optional): :obj:`True`, if the bot can change group the
|
||||
sticker set. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
linked_chat_id (:obj:`int`, optional): Unique identifier for the linked chat, i.e. the
|
||||
discussion group identifier for a channel and vice versa; for supergroups and channel
|
||||
chats. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
location (:class:`telegram.ChatLocation`, optional): For supergroups, the location to which
|
||||
the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
join_to_send_messages (:obj:`bool`, optional): :obj:`True`, if users need to join the
|
||||
supergroup before they can send messages. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
join_by_request (:obj:`bool`, optional): :obj:`True`, if all users directly joining the
|
||||
supergroup need to be approved by supergroup administrators. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
supergroup without using an invite link need to be approved by supergroup
|
||||
administrators. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
has_restricted_voice_and_video_messages (:obj:`bool`, optional): :obj:`True`, if the
|
||||
privacy settings of the other party restrict sending voice and video note messages
|
||||
in the private chat. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
is_forum (:obj:`bool`, optional): :obj:`True`, if the supergroup chat is a forum
|
||||
(has topics_ enabled).
|
||||
|
||||
|
@ -173,27 +287,47 @@ class Chat(TelegramObject):
|
|||
only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
business_intro (:class:`telegram.BusinessIntro`, optional): For private chats with
|
||||
business accounts, the intro of the business. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
business_location (:class:`telegram.BusinessLocation`, optional): For private chats with
|
||||
business accounts, the location of the business. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
business_opening_hours (:class:`telegram.BusinessOpeningHours`, optional): For private
|
||||
chats with business accounts, the opening hours of the business. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
available_reactions (Sequence[:class:`telegram.ReactionType`], optional): List of available
|
||||
reactions allowed in the chat. If omitted, then all of
|
||||
:const:`telegram.constants.ReactionEmoji` are allowed. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
accent_color_id (:obj:`int`, optional): Identifier of the
|
||||
:class:`accent color <telegram.constants.AccentColor>` for the chat name and
|
||||
backgrounds of the chat photo, reply header, and link preview. See `accent colors`_
|
||||
|
@ -201,62 +335,110 @@ class Chat(TelegramObject):
|
|||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
background_custom_emoji_id (:obj:`str`, optional): Custom emoji identifier of emoji chosen
|
||||
by the chat for the reply header and link preview background. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
profile_accent_color_id (:obj:`int`, optional): Identifier of the
|
||||
:class:`accent color <telegram.constants.ProfileAccentColor>` for the chat's profile
|
||||
background. See profile `accent colors`_ for more details. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
profile_background_custom_emoji_id (:obj:`str`, optional): Custom emoji identifier of
|
||||
the emoji chosen by the chat for its profile background. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
emoji_status_custom_emoji_id (:obj:`str`, optional): Custom emoji identifier of emoji
|
||||
status of the chat or the other party in a private chat. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
emoji_status_expiration_date (:class:`datetime.datetime`, optional): Expiration date of
|
||||
emoji status of the chat or the other party in a private chat, in seconds. Returned
|
||||
only in :meth:`telegram.Bot.get_chat`.
|
||||
|datetime_localization|
|
||||
|
||||
.. versionadded:: 20.5
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
has_aggressive_anti_spam_enabled (:obj:`bool`, optional): :obj:`True`, if aggressive
|
||||
anti-spam checks are enabled in the supergroup. The field is only available to chat
|
||||
administrators. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
has_hidden_members (:obj:`bool`, optional): :obj:`True`, if non-administrators can only
|
||||
get the list of bots and administrators in the chat. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
unrestrict_boost_count (:obj:`int`, optional): For supergroups, the minimum number of
|
||||
boosts that a non-administrator user needs to add in order to ignore slow mode and chat
|
||||
permissions. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 21.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
custom_emoji_sticker_set_name (:obj:`str`, optional): For supergroups, the name of the
|
||||
group's custom emoji sticker set. Custom emoji from this set can be used by all users
|
||||
and bots in the group. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 21.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
birthdate (:obj:`telegram.Birthdate`, optional): For private chats,
|
||||
the date of birth of the user. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
personal_chat (:obj:`telegram.Chat`, optional): For private chats, the personal channel of
|
||||
the user. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
|
||||
Attributes:
|
||||
id (:obj:`int`): Unique identifier for this chat. This number may be greater than 32 bits
|
||||
and some programming languages may have difficulty/silent defects in interpreting it.
|
||||
|
@ -271,62 +453,134 @@ class Chat(TelegramObject):
|
|||
last_name (:obj:`str`): Optional. Last name of the other party in a private chat.
|
||||
photo (:class:`telegram.ChatPhoto`): Optional. Chat photo.
|
||||
Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
bio (:obj:`str`): Optional. Bio of the other party in a private chat. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
has_private_forwards (:obj:`bool`): Optional. :obj:`True`, if privacy settings of the other
|
||||
party in the private chat allows to use ``tg://user?id=<user_id>`` links only in chats
|
||||
with the user. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 13.9
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
description (:obj:`str`): Optional. Description, for groups, supergroups and channel chats.
|
||||
Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
invite_link (:obj:`str`): Optional. Primary invite link, for groups, supergroups and
|
||||
channel. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
pinned_message (:class:`telegram.Message`): Optional. The most recent pinned message
|
||||
(by sending date). Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions,
|
||||
for groups and supergroups. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
slow_mode_delay (:obj:`int`): Optional. For supergroups, the minimum allowed delay between
|
||||
consecutive messages sent by each unprivileged user. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
message_auto_delete_time (:obj:`int`): Optional. The time after which all messages sent to
|
||||
the chat will be automatically deleted; in seconds. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 13.4
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
has_protected_content (:obj:`bool`): Optional. :obj:`True`, if messages from the chat can't
|
||||
be forwarded to other chats. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 13.9
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
has_visible_history (:obj:`bool`): Optional. :obj:`True`, if new chat members will have
|
||||
access to old messages; available only to chat administrators. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
sticker_set_name (:obj:`str`): Optional. For supergroups, name of Group sticker set.
|
||||
Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
can_set_sticker_set (:obj:`bool`): Optional. :obj:`True`, if the bot can change group the
|
||||
sticker set. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
linked_chat_id (:obj:`int`): Optional. Unique identifier for the linked chat, i.e. the
|
||||
discussion group identifier for a channel and vice versa; for supergroups and channel
|
||||
chats. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
location (:class:`telegram.ChatLocation`): Optional. For supergroups, the location to which
|
||||
the supergroup is connected. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
join_to_send_messages (:obj:`bool`): Optional. :obj:`True`, if users need to join
|
||||
the supergroup before they can send messages. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
join_by_request (:obj:`bool`): Optional. :obj:`True`, if all users directly
|
||||
joining the supergroup need to be approved by supergroup administrators. Returned only
|
||||
in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
join_by_request (:obj:`bool`): Optional. :obj:`True`, if all users directly joining the
|
||||
supergroup without using an invite link need to be approved by supergroup
|
||||
administrators. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
has_restricted_voice_and_video_messages (:obj:`bool`): Optional. :obj:`True`, if the
|
||||
privacy settings of the other party restrict sending voice and video note messages
|
||||
in the private chat. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
is_forum (:obj:`bool`): Optional. :obj:`True`, if the supergroup chat is a forum
|
||||
(has topics_ enabled).
|
||||
|
||||
|
@ -339,27 +593,47 @@ class Chat(TelegramObject):
|
|||
obtained via :meth:`~telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
business_intro (:class:`telegram.BusinessIntro`): Optional. For private chats with
|
||||
business accounts, the intro of the business. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
business_location (:class:`telegram.BusinessLocation`): Optional. For private chats with
|
||||
business accounts, the location of the business. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
business_opening_hours (:class:`telegram.BusinessOpeningHours`): Optional. For private
|
||||
chats with business accounts, the opening hours of the business. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
available_reactions (Tuple[:class:`telegram.ReactionType`]): Optional. List of available
|
||||
reactions allowed in the chat. If omitted, then all of
|
||||
:const:`telegram.constants.ReactionEmoji` are allowed. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
accent_color_id (:obj:`int`): Optional. Identifier of the
|
||||
:class:`accent color <telegram.constants.AccentColor>` for the chat name and
|
||||
backgrounds of the chat photo, reply header, and link preview. See `accent colors`_
|
||||
|
@ -367,62 +641,110 @@ class Chat(TelegramObject):
|
|||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
background_custom_emoji_id (:obj:`str`): Optional. Custom emoji identifier of emoji chosen
|
||||
by the chat for the reply header and link preview background. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
profile_accent_color_id (:obj:`int`): Optional. Identifier of the
|
||||
:class:`accent color <telegram.constants.ProfileAccentColor>` for the chat's profile
|
||||
background. See profile `accent colors`_ for more details. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
profile_background_custom_emoji_id (:obj:`str`): Optional. Custom emoji identifier of
|
||||
the emoji chosen by the chat for its profile background. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.8
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
emoji_status_custom_emoji_id (:obj:`str`): Optional. Custom emoji identifier of emoji
|
||||
status of the chat or the other party in a private chat. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
emoji_status_expiration_date (:class:`datetime.datetime`): Optional. Expiration date of
|
||||
emoji status of the chat or the other party in a private chat, in seconds. Returned
|
||||
only in :meth:`telegram.Bot.get_chat`.
|
||||
|datetime_localization|
|
||||
|
||||
.. versionadded:: 20.5
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
has_aggressive_anti_spam_enabled (:obj:`bool`): Optional. :obj:`True`, if aggressive
|
||||
anti-spam checks are enabled in the supergroup. The field is only available to chat
|
||||
administrators. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
has_hidden_members (:obj:`bool`): Optional. :obj:`True`, if non-administrators can only
|
||||
get the list of bots and administrators in the chat. Returned only in
|
||||
:meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
unrestrict_boost_count (:obj:`int`): Optional. For supergroups, the minimum number of
|
||||
boosts that a non-administrator user needs to add in order to ignore slow mode and chat
|
||||
permissions. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 21.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
custom_emoji_sticker_set_name (:obj:`str`): Optional. For supergroups, the name of the
|
||||
group's custom emoji sticker set. Custom emoji from this set can be used by all users
|
||||
and bots in the group. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 21.0
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
birthdate (:obj:`telegram.Birthdate`): Optional. For private chats,
|
||||
the date of birth of the user. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
personal_chat (:obj:`telegram.Chat`): Optional. For private chats, the personal channel of
|
||||
the user. Returned only in :meth:`telegram.Bot.get_chat`.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
.. deprecated:: 21.2
|
||||
In accordance to Bot API 7.3, this attribute will be moved to
|
||||
:class:`telegram.ChatFullInfo`.
|
||||
|
||||
.. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups
|
||||
.. _accent colors: https://core.telegram.org/bots/api#accent-colors
|
||||
"""
|
||||
|
@ -471,7 +793,6 @@ class Chat(TelegramObject):
|
|||
"unrestrict_boost_count",
|
||||
"username",
|
||||
)
|
||||
|
||||
SENDER: Final[str] = constants.ChatType.SENDER
|
||||
""":const:`telegram.constants.ChatType.SENDER`
|
||||
|
||||
|
@ -518,7 +839,7 @@ class Chat(TelegramObject):
|
|||
has_aggressive_anti_spam_enabled: Optional[bool] = None,
|
||||
has_hidden_members: Optional[bool] = None,
|
||||
available_reactions: Optional[Sequence[ReactionType]] = None,
|
||||
accent_color_id: Optional[int] = None,
|
||||
accent_color_id: Optional[int] = None, # required in API 7.3 - Optional for back compat
|
||||
background_custom_emoji_id: Optional[str] = None,
|
||||
profile_accent_color_id: Optional[int] = None,
|
||||
profile_background_custom_emoji_id: Optional[str] = None,
|
||||
|
@ -585,10 +906,34 @@ class Chat(TelegramObject):
|
|||
self.business_location: Optional["BusinessLocation"] = business_location
|
||||
self.business_opening_hours: Optional["BusinessOpeningHours"] = business_opening_hours
|
||||
|
||||
if self.__class__ is Chat:
|
||||
for arg in _deprecated_attrs:
|
||||
if (val := object.__getattribute__(self, arg)) is not None and val != ():
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"21.2",
|
||||
f"The argument `{arg}` is deprecated and will only be available via "
|
||||
"`ChatFullInfo` in the future.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
self._id_attrs = (self.id,)
|
||||
|
||||
self._freeze()
|
||||
|
||||
def __getattribute__(self, name: str) -> Any:
|
||||
if name in _deprecated_attrs and self.__class__ is Chat:
|
||||
warn(
|
||||
PTBDeprecationWarning(
|
||||
"21.2",
|
||||
f"The attribute `{name}` is deprecated and will only be accessible via "
|
||||
"`ChatFullInfo` in the future.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
return super().__getattribute__(name)
|
||||
|
||||
@property
|
||||
def effective_name(self) -> Optional[str]:
|
||||
"""
|
||||
|
@ -658,7 +1003,7 @@ class Chat(TelegramObject):
|
|||
data["location"] = ChatLocation.de_json(data.get("location"), bot)
|
||||
data["available_reactions"] = ReactionType.de_list(data.get("available_reactions"), bot)
|
||||
data["birthdate"] = Birthdate.de_json(data.get("birthdate"), bot)
|
||||
data["personal_chat"] = cls.de_json(data.get("personal_chat"), bot)
|
||||
data["personal_chat"] = Chat.de_json(data.get("personal_chat"), bot)
|
||||
data["business_intro"] = BusinessIntro.de_json(data.get("business_intro"), bot)
|
||||
data["business_location"] = BusinessLocation.de_json(data.get("business_location"), bot)
|
||||
data["business_opening_hours"] = BusinessOpeningHours.de_json(
|
||||
|
@ -2545,7 +2890,7 @@ class Chat(TelegramObject):
|
|||
async def send_poll(
|
||||
self,
|
||||
question: str,
|
||||
options: Sequence[str],
|
||||
options: Sequence[Union[str, "InputPollOption"]],
|
||||
is_anonymous: Optional[bool] = None,
|
||||
type: Optional[str] = None,
|
||||
allows_multiple_answers: Optional[bool] = None,
|
||||
|
@ -2562,6 +2907,8 @@ class Chat(TelegramObject):
|
|||
message_thread_id: Optional[int] = None,
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
business_connection_id: Optional[str] = None,
|
||||
question_parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
question_entities: Optional[Sequence["MessageEntity"]] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
|
@ -2608,6 +2955,8 @@ class Chat(TelegramObject):
|
|||
protect_content=protect_content,
|
||||
message_thread_id=message_thread_id,
|
||||
business_connection_id=business_connection_id,
|
||||
question_parse_mode=question_parse_mode,
|
||||
question_entities=question_entities,
|
||||
)
|
||||
|
||||
async def send_copy(
|
||||
|
|
|
@ -80,8 +80,9 @@ class ChatAdministratorRights(TelegramObject):
|
|||
.. versionadded:: 20.6
|
||||
.. versionchanged:: 21.0
|
||||
|non_optional_story_argument|
|
||||
can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit
|
||||
stories posted by other users.
|
||||
can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit stories posted
|
||||
by other users, post stories to the chat page, pin chat stories, and access the chat's
|
||||
story archive
|
||||
|
||||
.. versionadded:: 20.6
|
||||
.. versionchanged:: 21.0
|
||||
|
@ -128,8 +129,9 @@ class ChatAdministratorRights(TelegramObject):
|
|||
.. versionadded:: 20.6
|
||||
.. versionchanged:: 21.0
|
||||
|non_optional_story_argument|
|
||||
can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit
|
||||
stories posted by other users.
|
||||
can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit stories posted
|
||||
by other users, post stories to the chat page, pin chat stories, and access the chat's
|
||||
story archive
|
||||
|
||||
.. versionadded:: 20.6
|
||||
.. versionchanged:: 21.0
|
||||
|
|
540
telegram/_chatbackground.py
Normal file
540
telegram/_chatbackground.py
Normal file
|
@ -0,0 +1,540 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains objects related to chat backgrounds."""
|
||||
from typing import TYPE_CHECKING, Dict, Final, Optional, Sequence, Tuple, Type
|
||||
|
||||
from telegram import constants
|
||||
from telegram._files.document import Document
|
||||
from telegram._telegramobject import TelegramObject
|
||||
from telegram._utils import enum
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Bot
|
||||
|
||||
|
||||
class BackgroundFill(TelegramObject):
|
||||
"""Base class for Telegram BackgroundFill Objects. It can be one of:
|
||||
|
||||
* :class:`telegram.BackgroundFillSolid`
|
||||
* :class:`telegram.BackgroundFillGradient`
|
||||
* :class:`telegram.BackgroundFillFreeformGradient`
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`type` is equal.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Args:
|
||||
type (:obj:`str`): Type of the background fill. Can be one of:
|
||||
:attr:`~telegram.BackgroundFill.SOLID`, :attr:`~telegram.BackgroundFill.GRADIENT`
|
||||
or :attr:`~telegram.BackgroundFill.FREEFORM_GRADIENT`.
|
||||
|
||||
Attributes:
|
||||
type (:obj:`str`): Type of the background fill. Can be one of:
|
||||
:attr:`~telegram.BackgroundFill.SOLID`, :attr:`~telegram.BackgroundFill.GRADIENT`
|
||||
or :attr:`~telegram.BackgroundFill.FREEFORM_GRADIENT`.
|
||||
"""
|
||||
|
||||
__slots__ = ("type",)
|
||||
|
||||
SOLID: Final[constants.BackgroundFillType] = constants.BackgroundFillType.SOLID
|
||||
""":const:`telegram.constants.BackgroundFillType.SOLID`"""
|
||||
GRADIENT: Final[constants.BackgroundFillType] = constants.BackgroundFillType.GRADIENT
|
||||
""":const:`telegram.constants.BackgroundFillType.GRADIENT`"""
|
||||
FREEFORM_GRADIENT: Final[constants.BackgroundFillType] = (
|
||||
constants.BackgroundFillType.FREEFORM_GRADIENT
|
||||
)
|
||||
""":const:`telegram.constants.BackgroundFillType.FREEFORM_GRADIENT`"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
type: str, # pylint: disable=redefined-builtin
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
super().__init__(api_kwargs=api_kwargs)
|
||||
# Required by all subclasses
|
||||
self.type: str = enum.get_member(constants.BackgroundFillType, type, type)
|
||||
|
||||
self._id_attrs = (self.type,)
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BackgroundFill"]:
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
_class_mapping: Dict[str, Type[BackgroundFill]] = {
|
||||
cls.SOLID: BackgroundFillSolid,
|
||||
cls.GRADIENT: BackgroundFillGradient,
|
||||
cls.FREEFORM_GRADIENT: BackgroundFillFreeformGradient,
|
||||
}
|
||||
|
||||
if cls is BackgroundFill and data.get("type") in _class_mapping:
|
||||
return _class_mapping[data.pop("type")].de_json(data=data, bot=bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
class BackgroundFillSolid(BackgroundFill):
|
||||
"""
|
||||
The background is filled using the selected color.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`color` is equal.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Args:
|
||||
color (:obj:`int`): The color of the background fill in the `RGB24` format.
|
||||
|
||||
Attributes:
|
||||
type (:obj:`str`): Type of the background fill. Always
|
||||
:attr:`~telegram.BackgroundFill.SOLID`.
|
||||
color (:obj:`int`): The color of the background fill in the `RGB24` format.
|
||||
"""
|
||||
|
||||
__slots__ = ("color",)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
color: int,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
super().__init__(type=self.SOLID, api_kwargs=api_kwargs)
|
||||
|
||||
with self._unfrozen():
|
||||
self.color: int = color
|
||||
|
||||
self._id_attrs = (self.color,)
|
||||
|
||||
|
||||
class BackgroundFillGradient(BackgroundFill):
|
||||
"""
|
||||
The background is a gradient fill.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`top_color`, :attr:`bottom_color`
|
||||
and :attr:`rotation_angle` are equal.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Args:
|
||||
top_color (:obj:`int`): Top color of the gradient in the `RGB24` format.
|
||||
bottom_color (:obj:`int`): Bottom color of the gradient in the `RGB24` format.
|
||||
rotation_angle (:obj:`int`): Clockwise rotation angle of the background
|
||||
fill in degrees;
|
||||
0-:tg-const:`telegram.constants.BackgroundFillLimit.MAX_ROTATION_ANGLE`.
|
||||
|
||||
|
||||
Attributes:
|
||||
type (:obj:`str`): Type of the background fill. Always
|
||||
:attr:`~telegram.BackgroundFill.GRADIENT`.
|
||||
top_color (:obj:`int`): Top color of the gradient in the `RGB24` format.
|
||||
bottom_color (:obj:`int`): Bottom color of the gradient in the `RGB24` format.
|
||||
rotation_angle (:obj:`int`): Clockwise rotation angle of the background
|
||||
fill in degrees;
|
||||
0-:tg-const:`telegram.constants.BackgroundFillLimit.MAX_ROTATION_ANGLE`.
|
||||
"""
|
||||
|
||||
__slots__ = ("bottom_color", "rotation_angle", "top_color")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
top_color: int,
|
||||
bottom_color: int,
|
||||
rotation_angle: int,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
super().__init__(type=self.GRADIENT, api_kwargs=api_kwargs)
|
||||
|
||||
with self._unfrozen():
|
||||
self.top_color: int = top_color
|
||||
self.bottom_color: int = bottom_color
|
||||
self.rotation_angle: int = rotation_angle
|
||||
|
||||
self._id_attrs = (self.top_color, self.bottom_color, self.rotation_angle)
|
||||
|
||||
|
||||
class BackgroundFillFreeformGradient(BackgroundFill):
|
||||
"""
|
||||
The background is a freeform gradient that rotates after every message in the chat.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`colors` is equal.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Args:
|
||||
colors (Sequence[:obj:`int`]): A list of the 3 or 4 base colors that are used to
|
||||
generate the freeform gradient in the `RGB24` format
|
||||
|
||||
Attributes:
|
||||
type (:obj:`str`): Type of the background fill. Always
|
||||
:attr:`~telegram.BackgroundFill.FREEFORM_GRADIENT`.
|
||||
colors (Sequence[:obj:`int`]): A list of the 3 or 4 base colors that are used to
|
||||
generate the freeform gradient in the `RGB24` format
|
||||
"""
|
||||
|
||||
__slots__ = ("colors",)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
colors: Sequence[int],
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
super().__init__(type=self.FREEFORM_GRADIENT, api_kwargs=api_kwargs)
|
||||
|
||||
with self._unfrozen():
|
||||
self.colors: Tuple[int, ...] = parse_sequence_arg(colors)
|
||||
|
||||
self._id_attrs = (self.colors,)
|
||||
|
||||
|
||||
class BackgroundType(TelegramObject):
|
||||
"""Base class for Telegram BackgroundType Objects. It can be one of:
|
||||
|
||||
* :class:`telegram.BackgroundTypeFill`
|
||||
* :class:`telegram.BackgroundTypeWallpaper`
|
||||
* :class:`telegram.BackgroundTypePattern`
|
||||
* :class:`telegram.BackgroundTypeChatTheme`.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`type` is equal.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Args:
|
||||
type (:obj:`str`): Type of the background. Can be one of:
|
||||
:attr:`~telegram.BackgroundType.FILL`, :attr:`~telegram.BackgroundType.WALLPAPER`
|
||||
:attr:`~telegram.BackgroundType.PATTERN` or
|
||||
:attr:`~telegram.BackgroundType.CHAT_THEME`.
|
||||
|
||||
Attributes:
|
||||
type (:obj:`str`): Type of the background. Can be one of:
|
||||
:attr:`~telegram.BackgroundType.FILL`, :attr:`~telegram.BackgroundType.WALLPAPER`
|
||||
:attr:`~telegram.BackgroundType.PATTERN` or
|
||||
:attr:`~telegram.BackgroundType.CHAT_THEME`.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("type",)
|
||||
|
||||
FILL: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.FILL
|
||||
""":const:`telegram.constants.BackgroundTypeType.FILL`"""
|
||||
WALLPAPER: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.WALLPAPER
|
||||
""":const:`telegram.constants.BackgroundTypeType.WALLPAPER`"""
|
||||
PATTERN: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.PATTERN
|
||||
""":const:`telegram.constants.BackgroundTypeType.PATTERN`"""
|
||||
CHAT_THEME: Final[constants.BackgroundTypeType] = constants.BackgroundTypeType.CHAT_THEME
|
||||
""":const:`telegram.constants.BackgroundTypeType.CHAT_THEME`"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
type: str, # pylint: disable=redefined-builtin
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
super().__init__(api_kwargs=api_kwargs)
|
||||
# Required by all subclasses
|
||||
self.type: str = enum.get_member(constants.BackgroundTypeType, type, type)
|
||||
|
||||
self._id_attrs = (self.type,)
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BackgroundType"]:
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
_class_mapping: Dict[str, Type[BackgroundType]] = {
|
||||
cls.FILL: BackgroundTypeFill,
|
||||
cls.WALLPAPER: BackgroundTypeWallpaper,
|
||||
cls.PATTERN: BackgroundTypePattern,
|
||||
cls.CHAT_THEME: BackgroundTypeChatTheme,
|
||||
}
|
||||
|
||||
if cls is BackgroundType and data.get("type") in _class_mapping:
|
||||
return _class_mapping[data.pop("type")].de_json(data=data, bot=bot)
|
||||
|
||||
if "fill" in data:
|
||||
data["fill"] = BackgroundFill.de_json(data.get("fill"), bot)
|
||||
|
||||
if "document" in data:
|
||||
data["document"] = Document.de_json(data.get("document"), bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
class BackgroundTypeFill(BackgroundType):
|
||||
"""
|
||||
The background is automatically filled based on the selected colors.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`fill` and :attr:`dark_theme_dimming` are equal.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Args:
|
||||
fill (:obj:`telegram.BackgroundFill`): The background fill.
|
||||
dark_theme_dimming (:obj:`int`): Dimming of the background in dark themes, as a
|
||||
percentage;
|
||||
0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`.
|
||||
|
||||
Attributes:
|
||||
type (:obj:`str`): Type of the background. Always
|
||||
:attr:`~telegram.BackgroundType.FILL`.
|
||||
fill (:obj:`telegram.BackgroundFill`): The background fill.
|
||||
dark_theme_dimming (:obj:`int`): Dimming of the background in dark themes, as a
|
||||
percentage;
|
||||
0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`.
|
||||
"""
|
||||
|
||||
__slots__ = ("dark_theme_dimming", "fill")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fill: BackgroundFill,
|
||||
dark_theme_dimming: int,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
super().__init__(type=self.FILL, api_kwargs=api_kwargs)
|
||||
|
||||
with self._unfrozen():
|
||||
self.fill: BackgroundFill = fill
|
||||
self.dark_theme_dimming: int = dark_theme_dimming
|
||||
|
||||
self._id_attrs = (self.fill, self.dark_theme_dimming)
|
||||
|
||||
|
||||
class BackgroundTypeWallpaper(BackgroundType):
|
||||
"""
|
||||
The background is a wallpaper in the `JPEG` format.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`document` and :attr:`dark_theme_dimming` are equal.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Args:
|
||||
document (:obj:`telegram.Document`): Document with the wallpaper
|
||||
dark_theme_dimming (:obj:`int`): Dimming of the background in dark themes, as a
|
||||
percentage;
|
||||
0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`.
|
||||
is_blurred (:obj:`bool`, optional): :obj:`True`, if the wallpaper is downscaled to fit
|
||||
in a 450x450 square and then box-blurred with radius 12
|
||||
is_moving (:obj:`bool`, optional): :obj:`True`, if the background moves slightly
|
||||
when the device is tilted
|
||||
|
||||
Attributes:
|
||||
type (:obj:`str`): Type of the background. Always
|
||||
:attr:`~telegram.BackgroundType.WALLPAPER`.
|
||||
document (:obj:`telegram.Document`): Document with the wallpaper
|
||||
dark_theme_dimming (:obj:`int`): Dimming of the background in dark themes, as a
|
||||
percentage;
|
||||
0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_DIMMING`.
|
||||
is_blurred (:obj:`bool`): Optional. :obj:`True`, if the wallpaper is downscaled to fit
|
||||
in a 450x450 square and then box-blurred with radius 12
|
||||
is_moving (:obj:`bool`): Optional. :obj:`True`, if the background moves slightly
|
||||
when the device is tilted
|
||||
"""
|
||||
|
||||
__slots__ = ("dark_theme_dimming", "document", "is_blurred", "is_moving")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
document: Document,
|
||||
dark_theme_dimming: int,
|
||||
is_blurred: Optional[bool] = None,
|
||||
is_moving: Optional[bool] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
super().__init__(type=self.WALLPAPER, api_kwargs=api_kwargs)
|
||||
|
||||
with self._unfrozen():
|
||||
# Required
|
||||
self.document: Document = document
|
||||
self.dark_theme_dimming: int = dark_theme_dimming
|
||||
# Optionals
|
||||
self.is_blurred: Optional[bool] = is_blurred
|
||||
self.is_moving: Optional[bool] = is_moving
|
||||
|
||||
self._id_attrs = (self.document, self.dark_theme_dimming)
|
||||
|
||||
|
||||
class BackgroundTypePattern(BackgroundType):
|
||||
"""
|
||||
The background is a `PNG` or `TGV` (gzipped subset of `SVG` with `MIME` type
|
||||
`"application/x-tgwallpattern"`) pattern to be combined with the background fill
|
||||
chosen by the user.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`document` and :attr:`fill` and :attr:`intensity` are equal.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Args:
|
||||
document (:obj:`telegram.Document`): Document with the pattern.
|
||||
fill (:obj:`telegram.BackgroundFill`): The background fill that is combined with
|
||||
the pattern.
|
||||
intensity (:obj:`int`): Intensity of the pattern when it is shown above the filled
|
||||
background;
|
||||
0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_INTENSITY`.
|
||||
is_inverted (:obj:`int`, optional): :obj:`True`, if the background fill must be applied
|
||||
only to the pattern itself. All other pixels are black in this case. For dark
|
||||
themes only.
|
||||
is_moving (:obj:`bool`, optional): :obj:`True`, if the background moves slightly
|
||||
when the device is tilted.
|
||||
|
||||
Attributes:
|
||||
type (:obj:`str`): Type of the background. Always
|
||||
:attr:`~telegram.BackgroundType.PATTERN`.
|
||||
document (:obj:`telegram.Document`): Document with the pattern.
|
||||
fill (:obj:`telegram.BackgroundFill`): The background fill that is combined with
|
||||
the pattern.
|
||||
intensity (:obj:`int`): Intensity of the pattern when it is shown above the filled
|
||||
background;
|
||||
0-:tg-const:`telegram.constants.BackgroundTypeLimit.MAX_INTENSITY`.
|
||||
is_inverted (:obj:`int`): Optional. :obj:`True`, if the background fill must be applied
|
||||
only to the pattern itself. All other pixels are black in this case. For dark
|
||||
themes only.
|
||||
is_moving (:obj:`bool`): Optional. :obj:`True`, if the background moves slightly
|
||||
when the device is tilted.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"document",
|
||||
"fill",
|
||||
"intensity",
|
||||
"is_inverted",
|
||||
"is_moving",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
document: Document,
|
||||
fill: BackgroundFill,
|
||||
intensity: int,
|
||||
is_inverted: Optional[bool] = None,
|
||||
is_moving: Optional[bool] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
super().__init__(type=self.PATTERN, api_kwargs=api_kwargs)
|
||||
|
||||
with self._unfrozen():
|
||||
# Required
|
||||
self.document: Document = document
|
||||
self.fill: BackgroundFill = fill
|
||||
self.intensity: int = intensity
|
||||
# Optionals
|
||||
self.is_inverted: Optional[bool] = is_inverted
|
||||
self.is_moving: Optional[bool] = is_moving
|
||||
|
||||
self._id_attrs = (self.document, self.fill, self.intensity)
|
||||
|
||||
|
||||
class BackgroundTypeChatTheme(BackgroundType):
|
||||
"""
|
||||
The background is taken directly from a built-in chat theme.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`theme_name` is equal.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Args:
|
||||
theme_name (:obj:`str`): Name of the chat theme, which is usually an emoji.
|
||||
|
||||
Attributes:
|
||||
type (:obj:`str`): Type of the background. Always
|
||||
:attr:`~telegram.BackgroundType.CHAT_THEME`.
|
||||
theme_name (:obj:`str`): Name of the chat theme, which is usually an emoji.
|
||||
"""
|
||||
|
||||
__slots__ = ("theme_name",)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
theme_name: str,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
super().__init__(type=self.CHAT_THEME, api_kwargs=api_kwargs)
|
||||
|
||||
with self._unfrozen():
|
||||
self.theme_name: str = theme_name
|
||||
|
||||
self._id_attrs = (self.theme_name,)
|
||||
|
||||
|
||||
class ChatBackground(TelegramObject):
|
||||
"""
|
||||
This object represents a chat background.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`type` is equal.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Args:
|
||||
type (:obj:`telegram.BackgroundType`): Type of the background.
|
||||
|
||||
Attributes:
|
||||
type (:obj:`telegram.BackgroundType`): Type of the background.
|
||||
"""
|
||||
|
||||
__slots__ = ("type",)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
type: BackgroundType, # pylint: disable=redefined-builtin
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
super().__init__(api_kwargs=api_kwargs)
|
||||
self.type: BackgroundType = type
|
||||
|
||||
self._id_attrs = (self.type,)
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatBackground"]:
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["type"] = BackgroundType.de_json(data.get("type"), bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
166
telegram/_chatfullinfo.py
Normal file
166
telegram/_chatfullinfo.py
Normal file
|
@ -0,0 +1,166 @@
|
|||
#!/usr/bin/env python
|
||||
# pylint: disable=redefined-builtin
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains an object that represents a Telegram ChatFullInfo."""
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Optional, Sequence
|
||||
|
||||
from telegram._birthdate import Birthdate
|
||||
from telegram._chat import Chat
|
||||
from telegram._chatlocation import ChatLocation
|
||||
from telegram._chatpermissions import ChatPermissions
|
||||
from telegram._files.chatphoto import ChatPhoto
|
||||
from telegram._reaction import ReactionType
|
||||
from telegram._utils.types import JSONDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import BusinessIntro, BusinessLocation, BusinessOpeningHours, Message
|
||||
|
||||
|
||||
class ChatFullInfo(Chat):
|
||||
"""
|
||||
This object contains full information about a chat.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`~telegram.Chat.id` is equal.
|
||||
|
||||
Caution:
|
||||
This class is a subclass of :class:`telegram.Chat` and inherits all attributes and methods
|
||||
for backwards compatibility. In the future, this class will *NOT* inherit from
|
||||
:class:`telegram.Chat`.
|
||||
|
||||
.. seealso::
|
||||
All arguments and attributes can be found in :class:`telegram.Chat`.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Args:
|
||||
max_reaction_count (:obj:`int`): The maximum number of reactions that can be set on a
|
||||
message in the chat.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Attributes:
|
||||
max_reaction_count (:obj:`int`): The maximum number of reactions that can be set on a
|
||||
message in the chat.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
"""
|
||||
|
||||
__slots__ = ("max_reaction_count",)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: int,
|
||||
type: str,
|
||||
accent_color_id: int, # API 7.3 made this argument required
|
||||
max_reaction_count: int, # NEW arg in api 7.3 and is required
|
||||
title: Optional[str] = None,
|
||||
username: Optional[str] = None,
|
||||
first_name: Optional[str] = None,
|
||||
last_name: Optional[str] = None,
|
||||
is_forum: Optional[bool] = None,
|
||||
photo: Optional[ChatPhoto] = None,
|
||||
active_usernames: Optional[Sequence[str]] = None,
|
||||
birthdate: Optional[Birthdate] = None,
|
||||
business_intro: Optional["BusinessIntro"] = None,
|
||||
business_location: Optional["BusinessLocation"] = None,
|
||||
business_opening_hours: Optional["BusinessOpeningHours"] = None,
|
||||
personal_chat: Optional["Chat"] = None,
|
||||
available_reactions: Optional[Sequence[ReactionType]] = None,
|
||||
background_custom_emoji_id: Optional[str] = None,
|
||||
profile_accent_color_id: Optional[int] = None,
|
||||
profile_background_custom_emoji_id: Optional[str] = None,
|
||||
emoji_status_custom_emoji_id: Optional[str] = None,
|
||||
emoji_status_expiration_date: Optional[datetime] = None,
|
||||
bio: Optional[str] = None,
|
||||
has_private_forwards: Optional[bool] = None,
|
||||
has_restricted_voice_and_video_messages: Optional[bool] = None,
|
||||
join_to_send_messages: Optional[bool] = None,
|
||||
join_by_request: Optional[bool] = None,
|
||||
description: Optional[str] = None,
|
||||
invite_link: Optional[str] = None,
|
||||
pinned_message: Optional["Message"] = None,
|
||||
permissions: Optional[ChatPermissions] = None,
|
||||
slow_mode_delay: Optional[int] = None,
|
||||
unrestrict_boost_count: Optional[int] = None,
|
||||
message_auto_delete_time: Optional[int] = None,
|
||||
has_aggressive_anti_spam_enabled: Optional[bool] = None,
|
||||
has_hidden_members: Optional[bool] = None,
|
||||
has_protected_content: Optional[bool] = None,
|
||||
has_visible_history: Optional[bool] = None,
|
||||
sticker_set_name: Optional[str] = None,
|
||||
can_set_sticker_set: Optional[bool] = None,
|
||||
custom_emoji_sticker_set_name: Optional[str] = None,
|
||||
linked_chat_id: Optional[int] = None,
|
||||
location: Optional[ChatLocation] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
super().__init__(
|
||||
id=id,
|
||||
type=type,
|
||||
title=title,
|
||||
username=username,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
photo=photo,
|
||||
description=description,
|
||||
invite_link=invite_link,
|
||||
pinned_message=pinned_message,
|
||||
permissions=permissions,
|
||||
sticker_set_name=sticker_set_name,
|
||||
can_set_sticker_set=can_set_sticker_set,
|
||||
slow_mode_delay=slow_mode_delay,
|
||||
bio=bio,
|
||||
linked_chat_id=linked_chat_id,
|
||||
location=location,
|
||||
message_auto_delete_time=message_auto_delete_time,
|
||||
has_private_forwards=has_private_forwards,
|
||||
has_protected_content=has_protected_content,
|
||||
join_to_send_messages=join_to_send_messages,
|
||||
join_by_request=join_by_request,
|
||||
has_restricted_voice_and_video_messages=has_restricted_voice_and_video_messages,
|
||||
is_forum=is_forum,
|
||||
active_usernames=active_usernames,
|
||||
emoji_status_custom_emoji_id=emoji_status_custom_emoji_id,
|
||||
emoji_status_expiration_date=emoji_status_expiration_date,
|
||||
has_aggressive_anti_spam_enabled=has_aggressive_anti_spam_enabled,
|
||||
has_hidden_members=has_hidden_members,
|
||||
available_reactions=available_reactions,
|
||||
accent_color_id=accent_color_id,
|
||||
background_custom_emoji_id=background_custom_emoji_id,
|
||||
profile_accent_color_id=profile_accent_color_id,
|
||||
profile_background_custom_emoji_id=profile_background_custom_emoji_id,
|
||||
has_visible_history=has_visible_history,
|
||||
unrestrict_boost_count=unrestrict_boost_count,
|
||||
custom_emoji_sticker_set_name=custom_emoji_sticker_set_name,
|
||||
birthdate=birthdate,
|
||||
personal_chat=personal_chat,
|
||||
business_intro=business_intro,
|
||||
business_location=business_location,
|
||||
business_opening_hours=business_opening_hours,
|
||||
api_kwargs=api_kwargs,
|
||||
)
|
||||
|
||||
# Required and unique to this class-
|
||||
with self._unfrozen():
|
||||
self.max_reaction_count: int = max_reaction_count
|
||||
|
||||
self._freeze()
|
|
@ -235,8 +235,9 @@ class ChatMemberAdministrator(ChatMember):
|
|||
.. versionadded:: 20.6
|
||||
.. versionchanged:: 21.0
|
||||
|non_optional_story_argument|
|
||||
can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit
|
||||
stories posted by other users.
|
||||
can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit stories posted
|
||||
by other users, post stories to the chat page, pin chat stories, and access the chat's
|
||||
story archive
|
||||
|
||||
.. versionadded:: 20.6
|
||||
.. versionchanged:: 21.0
|
||||
|
@ -294,8 +295,9 @@ class ChatMemberAdministrator(ChatMember):
|
|||
.. versionadded:: 20.6
|
||||
.. versionchanged:: 21.0
|
||||
|non_optional_story_argument|
|
||||
can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit
|
||||
stories posted by other users.
|
||||
can_edit_stories (:obj:`bool`): :obj:`True`, if the administrator can edit stories posted
|
||||
by other users, post stories to the chat page, pin chat stories, and access the chat's
|
||||
story archive
|
||||
|
||||
.. versionadded:: 20.6
|
||||
.. versionchanged:: 21.0
|
||||
|
|
|
@ -63,6 +63,11 @@ class ChatMemberUpdated(TelegramObject):
|
|||
chat via a chat folder invite link
|
||||
|
||||
.. versionadded:: 20.3
|
||||
via_join_request (:obj:`bool`, optional): :obj:`True`, if the user joined the chat after
|
||||
sending a direct join request without using an invite link and being approved by
|
||||
an administrator
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Attributes:
|
||||
chat (:class:`telegram.Chat`): Chat the user belongs to.
|
||||
|
@ -80,6 +85,11 @@ class ChatMemberUpdated(TelegramObject):
|
|||
chat via a chat folder invite link
|
||||
|
||||
.. versionadded:: 20.3
|
||||
via_join_request (:obj:`bool`): Optional. :obj:`True`, if the user joined the chat after
|
||||
sending a direct join request without using an invite link and being approved
|
||||
by an administrator
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
"""
|
||||
|
||||
|
@ -91,6 +101,7 @@ class ChatMemberUpdated(TelegramObject):
|
|||
"new_chat_member",
|
||||
"old_chat_member",
|
||||
"via_chat_folder_invite_link",
|
||||
"via_join_request",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
|
@ -102,6 +113,7 @@ class ChatMemberUpdated(TelegramObject):
|
|||
new_chat_member: ChatMember,
|
||||
invite_link: Optional[ChatInviteLink] = None,
|
||||
via_chat_folder_invite_link: Optional[bool] = None,
|
||||
via_join_request: Optional[bool] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
|
@ -116,6 +128,7 @@ class ChatMemberUpdated(TelegramObject):
|
|||
|
||||
# Optionals
|
||||
self.invite_link: Optional[ChatInviteLink] = invite_link
|
||||
self.via_join_request: Optional[bool] = via_join_request
|
||||
|
||||
self._id_attrs = (
|
||||
self.chat,
|
||||
|
|
|
@ -235,7 +235,7 @@ class StickerSet(TelegramObject):
|
|||
.. versionchanged:: 20.5
|
||||
|removed_thumb_note|
|
||||
|
||||
.. versionremoved:: NEXT.VERSION
|
||||
.. versionremoved:: 21.2
|
||||
Removed the deprecated arguments and attributes ``is_animated`` and ``is_video``.
|
||||
|
||||
Args:
|
||||
|
|
|
@ -30,7 +30,8 @@ class ForceReply(TelegramObject):
|
|||
Upon receiving a message with this object, Telegram clients will display a reply interface to
|
||||
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.
|
||||
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.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`selective` is equal.
|
||||
|
|
|
@ -91,6 +91,7 @@ class InlineKeyboardButton(TelegramObject):
|
|||
to the bot when button is pressed, UTF-8
|
||||
:tg-const:`telegram.InlineKeyboardButton.MIN_CALLBACK_DATA`-
|
||||
:tg-const:`telegram.InlineKeyboardButton.MAX_CALLBACK_DATA` bytes.
|
||||
Not supported for messages sent on behalf of a Telegram Business account.
|
||||
If the bot instance allows arbitrary callback data, anything can be passed.
|
||||
|
||||
Tip:
|
||||
|
@ -102,25 +103,25 @@ class InlineKeyboardButton(TelegramObject):
|
|||
<https://core.telegram.org/bots/webapps>`_ that will be launched when the user presses
|
||||
the button. The Web App will be able to send an arbitrary message on behalf of the user
|
||||
using the method :meth:`~telegram.Bot.answer_web_app_query`. Available only in
|
||||
private chats between a user and the bot.
|
||||
private chats between a user and the bot. Not supported for messages sent on behalf of
|
||||
a Telegram Business account.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
switch_inline_query (:obj:`str`, optional): If set, pressing the button will prompt the
|
||||
user to select one of their chats, open that chat and insert the bot's username and the
|
||||
specified inline query in the input field. Can be empty, in which case just the bot's
|
||||
username will be inserted. This offers an easy way for users to start using your bot
|
||||
in inline mode when they are currently in a private chat with it. Especially useful
|
||||
when combined with ``switch_pm*`` actions - in this case the user will be automatically
|
||||
returned to the chat they switched from, skipping the chat selection screen.
|
||||
switch_inline_query (:obj:`str`, optional): If set, pressing the button will insert the
|
||||
bot's username and the specified inline query in the current chat's input field. May be
|
||||
empty, in which case only the bot's username will be inserted.
|
||||
|
||||
This offers a quick way for the user to open your bot in inline mode in the same chat -
|
||||
good for selecting something from multiple options. Not supported in channels and for
|
||||
messages sent on behalf of a Telegram Business account.
|
||||
|
||||
Tip:
|
||||
This is similar to the new parameter :paramref:`switch_inline_query_chosen_chat`,
|
||||
but gives no control over which chats can be selected.
|
||||
switch_inline_query_current_chat (:obj:`str`, optional): If set, pressing the button will
|
||||
insert the bot's username and the specified inline query in the current chat's input
|
||||
field. Can be empty, in which case only the bot's username will be inserted. This
|
||||
offers a quick way for the user to open your bot in inline mode in the same chat - good
|
||||
for selecting something from multiple options.
|
||||
prompt the user to select one of their chats of the specified type, open that chat and
|
||||
insert the bot's username and the specified inline query in the input field. Not
|
||||
supported for messages sent on behalf of a Telegram Business account.
|
||||
callback_game (:class:`telegram.CallbackGame`, optional): Description of the game that will
|
||||
be launched when the user presses the button. This type of button **must** always be
|
||||
the **first** button in the first row.
|
||||
|
@ -130,7 +131,8 @@ class InlineKeyboardButton(TelegramObject):
|
|||
switch_inline_query_chosen_chat (:obj:`telegram.SwitchInlineQueryChosenChat`, optional):
|
||||
If set, pressing the button will prompt the user to select one of their chats of the
|
||||
specified type, open that chat and insert the bot's username and the specified inline
|
||||
query in the input field.
|
||||
query in the input field. Not supported for messages sent on behalf of a Telegram
|
||||
Business account.
|
||||
|
||||
.. versionadded:: 20.3
|
||||
|
||||
|
@ -159,29 +161,30 @@ class InlineKeyboardButton(TelegramObject):
|
|||
to the bot when button is pressed, UTF-8
|
||||
:tg-const:`telegram.InlineKeyboardButton.MIN_CALLBACK_DATA`-
|
||||
:tg-const:`telegram.InlineKeyboardButton.MAX_CALLBACK_DATA` bytes.
|
||||
Not supported for messages sent on behalf of a Telegram Business account.
|
||||
web_app (:obj:`telegram.WebAppInfo`): Optional. Description of the `Web App
|
||||
<https://core.telegram.org/bots/webapps>`_ that will be launched when the user presses
|
||||
the button. The Web App will be able to send an arbitrary message on behalf of the user
|
||||
using the method :meth:`~telegram.Bot.answer_web_app_query`. Available only in
|
||||
private chats between a user and the bot.
|
||||
private chats between a user and the bot. Not supported for messages sent on behalf of
|
||||
a Telegram Business account.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
switch_inline_query (:obj:`str`): Optional. If set, pressing the button will prompt the
|
||||
user to select one of their chats, open that chat and insert the bot's username and the
|
||||
specified inline query in the input field. Can be empty, in which case just the bot's
|
||||
username will be inserted. This offers an easy way for users to start using your bot
|
||||
in inline mode when they are currently in a private chat with it. Especially useful
|
||||
when combined with ``switch_pm*`` actions - in this case the user will be automatically
|
||||
returned to the chat they switched from, skipping the chat selection screen.
|
||||
switch_inline_query (:obj:`str`): Optional. If set, pressing the button will insert the
|
||||
bot's username and the specified inline query in the current chat's input field. May be
|
||||
empty, in which case only the bot's username will be inserted.
|
||||
|
||||
This offers a quick way for the user to open your bot in inline mode in the same chat -
|
||||
good for selecting something from multiple options. Not supported in channels and for
|
||||
messages sent on behalf of a Telegram Business account.
|
||||
|
||||
Tip:
|
||||
This is similar to the new parameter :paramref:`switch_inline_query_chosen_chat`,
|
||||
but gives no control over which chats can be selected.
|
||||
switch_inline_query_current_chat (:obj:`str`): Optional. If set, pressing the button will
|
||||
insert the bot's username and the specified inline query in the current chat's input
|
||||
field. Can be empty, in which case only the bot's username will be inserted. This
|
||||
offers a quick way for the user to open your bot in inline mode in the same chat - good
|
||||
for selecting something from multiple options.
|
||||
prompt the user to select one of their chats of the specified type, open that chat and
|
||||
insert the bot's username and the specified inline query in the input field. Not
|
||||
supported for messages sent on behalf of a Telegram Business account.
|
||||
callback_game (:class:`telegram.CallbackGame`): Optional. Description of the game that will
|
||||
be launched when the user presses the button. This type of button **must** always be
|
||||
the **first** button in the first row.
|
||||
|
@ -191,7 +194,8 @@ class InlineKeyboardButton(TelegramObject):
|
|||
switch_inline_query_chosen_chat (:obj:`telegram.SwitchInlineQueryChosenChat`): Optional.
|
||||
If set, pressing the button will prompt the user to select one of their chats of the
|
||||
specified type, open that chat and insert the bot's username and the specified inline
|
||||
query in the input field.
|
||||
query in the input field. Not supported for messages sent on behalf of a Telegram
|
||||
Business account.
|
||||
|
||||
.. versionadded:: 20.3
|
||||
|
||||
|
|
|
@ -89,7 +89,9 @@ class InlineQueryResultLocation(InlineQueryResult):
|
|||
live_period (:obj:`int`): Optional. Period in seconds for which the location will be
|
||||
updated, should be between
|
||||
:tg-const:`telegram.InlineQueryResultLocation.MIN_LIVE_PERIOD` and
|
||||
:tg-const:`telegram.InlineQueryResultLocation.MAX_LIVE_PERIOD`.
|
||||
:tg-const:`telegram.InlineQueryResultLocation.MAX_LIVE_PERIOD` or
|
||||
:tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` for live
|
||||
locations that can be edited indefinitely.
|
||||
heading (:obj:`int`): Optional. For live locations, a direction in which the user is
|
||||
moving, in degrees. Must be between
|
||||
:tg-const:`telegram.InlineQueryResultLocation.MIN_HEADING` and
|
||||
|
|
|
@ -42,7 +42,9 @@ class InputLocationMessageContent(InputMessageContent):
|
|||
live_period (:obj:`int`, optional): Period in seconds for which the location will be
|
||||
updated, should be between
|
||||
:tg-const:`telegram.InputLocationMessageContent.MIN_LIVE_PERIOD` and
|
||||
:tg-const:`telegram.InputLocationMessageContent.MAX_LIVE_PERIOD`.
|
||||
:tg-const:`telegram.InputLocationMessageContent.MAX_LIVE_PERIOD` or
|
||||
:tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` for live
|
||||
locations that can be edited indefinitely.
|
||||
heading (:obj:`int`, optional): For live locations, a direction in which the user is
|
||||
moving, in degrees. Must be between
|
||||
:tg-const:`telegram.InputLocationMessageContent.MIN_HEADING` and
|
||||
|
|
|
@ -25,6 +25,7 @@ from html import escape
|
|||
from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Tuple, TypedDict, Union
|
||||
|
||||
from telegram._chat import Chat
|
||||
from telegram._chatbackground import ChatBackground
|
||||
from telegram._chatboost import ChatBoostAdded
|
||||
from telegram._dice import Dice
|
||||
from telegram._files.animation import Animation
|
||||
|
@ -64,6 +65,7 @@ from telegram._user import User
|
|||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE, DefaultValue
|
||||
from telegram._utils.entities import parse_message_entities, parse_message_entity
|
||||
from telegram._utils.types import (
|
||||
CorrectOptionID,
|
||||
FileInput,
|
||||
|
@ -99,6 +101,7 @@ if TYPE_CHECKING:
|
|||
InputMediaDocument,
|
||||
InputMediaPhoto,
|
||||
InputMediaVideo,
|
||||
InputPollOption,
|
||||
LabeledPrice,
|
||||
MessageId,
|
||||
MessageOrigin,
|
||||
|
@ -553,6 +556,11 @@ class Message(MaybeInaccessibleMessage):
|
|||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
chat_background_set (:obj:`telegram.ChatBackground`, optional): Service message: chat
|
||||
background set.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Attributes:
|
||||
message_id (:obj:`int`): Unique message identifier inside this chat.
|
||||
from_user (:class:`telegram.User`): Optional. Sender of the message; empty for messages
|
||||
|
@ -853,6 +861,11 @@ class Message(MaybeInaccessibleMessage):
|
|||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
chat_background_set (:obj:`telegram.ChatBackground`): Optional. Service message: chat
|
||||
background set
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
.. |custom_emoji_no_md1_support| replace:: Since custom emoji entities are not supported by
|
||||
:attr:`~telegram.constants.ParseMode.MARKDOWN`, this method now raises a
|
||||
:exc:`ValueError` when encountering a custom emoji.
|
||||
|
@ -876,6 +889,7 @@ class Message(MaybeInaccessibleMessage):
|
|||
"caption",
|
||||
"caption_entities",
|
||||
"channel_chat_created",
|
||||
"chat_background_set",
|
||||
"chat_shared",
|
||||
"connected_website",
|
||||
"contact",
|
||||
|
@ -1029,6 +1043,7 @@ class Message(MaybeInaccessibleMessage):
|
|||
business_connection_id: Optional[str] = None,
|
||||
sender_business_bot: Optional[User] = None,
|
||||
is_from_offline: Optional[bool] = None,
|
||||
chat_background_set: Optional[ChatBackground] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
|
@ -1127,6 +1142,7 @@ class Message(MaybeInaccessibleMessage):
|
|||
self.business_connection_id: Optional[str] = business_connection_id
|
||||
self.sender_business_bot: Optional[User] = sender_business_bot
|
||||
self.is_from_offline: Optional[bool] = is_from_offline
|
||||
self.chat_background_set: Optional[ChatBackground] = chat_background_set
|
||||
|
||||
self._effective_attachment = DEFAULT_NONE
|
||||
|
||||
|
@ -1241,6 +1257,7 @@ class Message(MaybeInaccessibleMessage):
|
|||
)
|
||||
data["users_shared"] = UsersShared.de_json(data.get("users_shared"), bot)
|
||||
data["chat_shared"] = ChatShared.de_json(data.get("chat_shared"), bot)
|
||||
data["chat_background_set"] = ChatBackground.de_json(data.get("chat_background_set"), bot)
|
||||
|
||||
# Unfortunately, this needs to be here due to cyclic imports
|
||||
from telegram._giveaway import ( # pylint: disable=import-outside-toplevel
|
||||
|
@ -1562,9 +1579,11 @@ class Message(MaybeInaccessibleMessage):
|
|||
|
||||
if quote is not None:
|
||||
warn(
|
||||
"The `quote` parameter is deprecated in favor of the `do_quote` parameter. Please "
|
||||
"update your code to use `do_quote` instead.",
|
||||
PTBDeprecationWarning,
|
||||
PTBDeprecationWarning(
|
||||
"20.8",
|
||||
"The `quote` parameter is deprecated in favor of the `do_quote` parameter. "
|
||||
"Please update your code to use `do_quote` instead.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
@ -2890,7 +2909,7 @@ class Message(MaybeInaccessibleMessage):
|
|||
async def reply_poll(
|
||||
self,
|
||||
question: str,
|
||||
options: Sequence[str],
|
||||
options: Sequence[Union[str, "InputPollOption"]],
|
||||
is_anonymous: Optional[bool] = None,
|
||||
type: Optional[str] = None, # pylint: disable=redefined-builtin
|
||||
allows_multiple_answers: Optional[bool] = None,
|
||||
|
@ -2906,6 +2925,8 @@ class Message(MaybeInaccessibleMessage):
|
|||
protect_content: ODVInput[bool] = DEFAULT_NONE,
|
||||
message_thread_id: ODVInput[int] = DEFAULT_NONE,
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
question_parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
question_entities: Optional[Sequence["MessageEntity"]] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
|
@ -2976,6 +2997,8 @@ class Message(MaybeInaccessibleMessage):
|
|||
protect_content=protect_content,
|
||||
message_thread_id=message_thread_id,
|
||||
business_connection_id=self.business_connection_id,
|
||||
question_parse_mode=question_parse_mode,
|
||||
question_entities=question_entities,
|
||||
)
|
||||
|
||||
async def reply_dice(
|
||||
|
@ -3653,6 +3676,7 @@ class Message(MaybeInaccessibleMessage):
|
|||
horizontal_accuracy: Optional[float] = None,
|
||||
heading: Optional[int] = None,
|
||||
proximity_alert_radius: Optional[int] = None,
|
||||
live_period: Optional[int] = None,
|
||||
*,
|
||||
location: Optional[Location] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
|
@ -3694,6 +3718,7 @@ class Message(MaybeInaccessibleMessage):
|
|||
horizontal_accuracy=horizontal_accuracy,
|
||||
heading=heading,
|
||||
proximity_alert_radius=proximity_alert_radius,
|
||||
live_period=live_period,
|
||||
inline_message_id=None,
|
||||
)
|
||||
|
||||
|
@ -4184,9 +4209,7 @@ class Message(MaybeInaccessibleMessage):
|
|||
if not self.text:
|
||||
raise RuntimeError("This Message has no 'text'.")
|
||||
|
||||
entity_text = self.text.encode("utf-16-le")
|
||||
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
|
||||
return entity_text.decode("utf-16-le")
|
||||
return parse_message_entity(self.text, entity)
|
||||
|
||||
def parse_caption_entity(self, entity: MessageEntity) -> str:
|
||||
"""Returns the text from a given :class:`telegram.MessageEntity`.
|
||||
|
@ -4210,9 +4233,7 @@ class Message(MaybeInaccessibleMessage):
|
|||
if not self.caption:
|
||||
raise RuntimeError("This Message has no 'caption'.")
|
||||
|
||||
entity_text = self.caption.encode("utf-16-le")
|
||||
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
|
||||
return entity_text.decode("utf-16-le")
|
||||
return parse_message_entity(self.caption, entity)
|
||||
|
||||
def parse_entities(self, types: Optional[List[str]] = None) -> Dict[MessageEntity, str]:
|
||||
"""
|
||||
|
@ -4237,12 +4258,7 @@ class Message(MaybeInaccessibleMessage):
|
|||
the text that belongs to them, calculated based on UTF-16 codepoints.
|
||||
|
||||
"""
|
||||
if types is None:
|
||||
types = MessageEntity.ALL_TYPES
|
||||
|
||||
return {
|
||||
entity: self.parse_entity(entity) for entity in self.entities if entity.type in types
|
||||
}
|
||||
return parse_message_entities(self.text, self.entities, types=types)
|
||||
|
||||
def parse_caption_entities(
|
||||
self, types: Optional[List[str]] = None
|
||||
|
@ -4269,14 +4285,7 @@ class Message(MaybeInaccessibleMessage):
|
|||
the text that belongs to them, calculated based on UTF-16 codepoints.
|
||||
|
||||
"""
|
||||
if types is None:
|
||||
types = MessageEntity.ALL_TYPES
|
||||
|
||||
return {
|
||||
entity: self.parse_caption_entity(entity)
|
||||
for entity in self.caption_entities
|
||||
if entity.type in types
|
||||
}
|
||||
return parse_message_entities(self.caption, self.caption_entities, types=types)
|
||||
|
||||
@classmethod
|
||||
def _parse_html(
|
||||
|
|
|
@ -210,9 +210,11 @@ class PassportElementErrorFiles(PassportElementError):
|
|||
This attribute will return a tuple instead of a list in future major versions.
|
||||
"""
|
||||
warn(
|
||||
"The attribute `file_hashes` will return a tuple instead of a list in future major"
|
||||
" versions.",
|
||||
PTBDeprecationWarning,
|
||||
PTBDeprecationWarning(
|
||||
"20.6",
|
||||
"The attribute `file_hashes` will return a tuple instead of a list in future major"
|
||||
" versions.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
return self._file_hashes
|
||||
|
@ -427,10 +429,12 @@ class PassportElementErrorTranslationFiles(PassportElementError):
|
|||
This attribute will return a tuple instead of a list in future major versions.
|
||||
"""
|
||||
warn(
|
||||
"The attribute `file_hashes` will return a tuple instead of a list in future major"
|
||||
" versions. See the stability policy:"
|
||||
" https://docs.python-telegram-bot.org/en/stable/stability_policy.html",
|
||||
PTBDeprecationWarning,
|
||||
PTBDeprecationWarning(
|
||||
"20.6",
|
||||
"The attribute `file_hashes` will return a tuple instead of a list in future major"
|
||||
" versions. See the stability policy:"
|
||||
" https://docs.python-telegram-bot.org/en/stable/stability_policy.html",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
return self._file_hashes
|
||||
|
|
|
@ -107,9 +107,11 @@ class PassportFile(TelegramObject):
|
|||
This attribute will return a datetime instead of a integer in future major versions.
|
||||
"""
|
||||
warn(
|
||||
"The attribute `file_date` will return a datetime instead of an integer in future"
|
||||
" major versions.",
|
||||
PTBDeprecationWarning,
|
||||
PTBDeprecationWarning(
|
||||
"20.6",
|
||||
"The attribute `file_date` will return a datetime instead of an integer in future"
|
||||
" major versions.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
return self._file_date
|
||||
|
|
|
@ -28,12 +28,80 @@ from telegram._user import User
|
|||
from telegram._utils import enum
|
||||
from telegram._utils.argumentparsing import parse_sequence_arg
|
||||
from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram._utils.defaultvalue import DEFAULT_NONE
|
||||
from telegram._utils.entities import parse_message_entities, parse_message_entity
|
||||
from telegram._utils.types import JSONDict, ODVInput
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from telegram import Bot
|
||||
|
||||
|
||||
class InputPollOption(TelegramObject):
|
||||
"""
|
||||
This object contains information about one answer option in a poll to send.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their :attr:`text` is equal.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Args:
|
||||
text (:obj:`str`): Option text,
|
||||
:tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH`
|
||||
characters.
|
||||
text_parse_mode (:obj:`str`, optional): |parse_mode|
|
||||
Currently, only custom emoji entities are allowed.
|
||||
text_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities
|
||||
that appear in the option :paramref:`text`. It can be specified instead of
|
||||
:paramref:`text_parse_mode`.
|
||||
Currently, only custom emoji entities are allowed.
|
||||
This list is empty if the text does not contain entities.
|
||||
|
||||
Attributes:
|
||||
text (:obj:`str`): Option text,
|
||||
:tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH`
|
||||
characters.
|
||||
text_parse_mode (:obj:`str`): Optional. |parse_mode|
|
||||
Currently, only custom emoji entities are allowed.
|
||||
text_entities (Sequence[:class:`telegram.MessageEntity`]): Special entities
|
||||
that appear in the option :paramref:`text`. It can be specified instead of
|
||||
:paramref:`text_parse_mode`.
|
||||
Currently, only custom emoji entities are allowed.
|
||||
This list is empty if the text does not contain entities.
|
||||
"""
|
||||
|
||||
__slots__ = ("text", "text_entities", "text_parse_mode")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
text: str,
|
||||
text_parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
text_entities: Optional[Sequence[MessageEntity]] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
super().__init__(api_kwargs=api_kwargs)
|
||||
self.text: str = text
|
||||
self.text_parse_mode: ODVInput[str] = text_parse_mode
|
||||
self.text_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(text_entities)
|
||||
|
||||
self._id_attrs = (self.text,)
|
||||
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["InputPollOption"]:
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["text_entities"] = MessageEntity.de_list(data.get("text_entities"), bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
|
||||
class PollOption(TelegramObject):
|
||||
"""
|
||||
This object contains information about one answer option in a poll.
|
||||
|
@ -46,26 +114,101 @@ class PollOption(TelegramObject):
|
|||
:tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH`
|
||||
characters.
|
||||
voter_count (:obj:`int`): Number of users that voted for this option.
|
||||
text_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities
|
||||
that appear in the option text. Currently, only custom emoji entities are allowed in
|
||||
poll option texts.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Attributes:
|
||||
text (:obj:`str`): Option text,
|
||||
:tg-const:`telegram.PollOption.MIN_LENGTH`-:tg-const:`telegram.PollOption.MAX_LENGTH`
|
||||
characters.
|
||||
voter_count (:obj:`int`): Number of users that voted for this option.
|
||||
text_entities (Tuple[:class:`telegram.MessageEntity`]): Special entities
|
||||
that appear in the option text. Currently, only custom emoji entities are allowed in
|
||||
poll option texts.
|
||||
This list is empty if the question does not contain entities.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("text", "voter_count")
|
||||
__slots__ = ("text", "text_entities", "voter_count")
|
||||
|
||||
def __init__(self, text: str, voter_count: int, *, api_kwargs: Optional[JSONDict] = None):
|
||||
def __init__(
|
||||
self,
|
||||
text: str,
|
||||
voter_count: int,
|
||||
text_entities: Optional[Sequence[MessageEntity]] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
super().__init__(api_kwargs=api_kwargs)
|
||||
self.text: str = text
|
||||
self.voter_count: int = voter_count
|
||||
self.text_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(text_entities)
|
||||
|
||||
self._id_attrs = (self.text, self.voter_count)
|
||||
|
||||
self._freeze()
|
||||
|
||||
@classmethod
|
||||
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["PollOption"]:
|
||||
"""See :meth:`telegram.TelegramObject.de_json`."""
|
||||
data = cls._parse_data(data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
data["text_entities"] = MessageEntity.de_list(data.get("text_entities"), bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
def parse_entity(self, entity: MessageEntity) -> str:
|
||||
"""Returns the text in :attr:`text`
|
||||
from a given :class:`telegram.MessageEntity` of :attr:`text_entities`.
|
||||
|
||||
Note:
|
||||
This method is present because Telegram calculates the offset and length in
|
||||
UTF-16 codepoint pairs, which some versions of Python don't handle automatically.
|
||||
(That is, you can't just slice ``Message.text`` with the offset and length.)
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Args:
|
||||
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
|
||||
be an entity that belongs to :attr:`text_entities`.
|
||||
|
||||
Returns:
|
||||
:obj:`str`: The text of the given entity.
|
||||
"""
|
||||
return parse_message_entity(self.text, entity)
|
||||
|
||||
def parse_entities(self, types: Optional[List[str]] = None) -> Dict[MessageEntity, str]:
|
||||
"""
|
||||
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
|
||||
It contains entities from this polls question filtered by their ``type`` attribute as
|
||||
the key, and the text that each entity belongs to as the value of the :obj:`dict`.
|
||||
|
||||
Note:
|
||||
This method should always be used instead of the :attr:`text_entities`
|
||||
attribute, since it calculates the correct substring from the message text based on
|
||||
UTF-16 codepoints. See :attr:`parse_entity` for more info.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Args:
|
||||
types (List[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the
|
||||
``type`` attribute of an entity is contained in this list, it will be returned.
|
||||
Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`.
|
||||
|
||||
Returns:
|
||||
Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
|
||||
the text that belongs to them, calculated based on UTF-16 codepoints.
|
||||
"""
|
||||
return parse_message_entities(self.text, self.text_entities, types)
|
||||
|
||||
MIN_LENGTH: Final[int] = constants.PollLimit.MIN_OPTION_LENGTH
|
||||
""":const:`telegram.constants.PollLimit.MIN_OPTION_LENGTH`
|
||||
|
||||
|
@ -215,6 +358,11 @@ class Poll(TelegramObject):
|
|||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
question_entities (Sequence[:class:`telegram.MessageEntity`], optional): Special entities
|
||||
that appear in the :attr:`question`. Currently, only custom emoji entities are allowed
|
||||
in poll questions.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Attributes:
|
||||
id (:obj:`str`): Unique poll identifier.
|
||||
|
@ -251,6 +399,12 @@ class Poll(TelegramObject):
|
|||
|
||||
.. versionchanged:: 20.3
|
||||
|datetime_localization|
|
||||
question_entities (Tuple[:class:`telegram.MessageEntity`]): Special entities
|
||||
that appear in the :attr:`question`. Currently, only custom emoji entities are allowed
|
||||
in poll questions.
|
||||
This list is empty if the question does not contain entities.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
"""
|
||||
|
||||
|
@ -266,6 +420,7 @@ class Poll(TelegramObject):
|
|||
"open_period",
|
||||
"options",
|
||||
"question",
|
||||
"question_entities",
|
||||
"total_voter_count",
|
||||
"type",
|
||||
)
|
||||
|
@ -285,6 +440,7 @@ class Poll(TelegramObject):
|
|||
explanation_entities: Optional[Sequence[MessageEntity]] = None,
|
||||
open_period: Optional[int] = None,
|
||||
close_date: Optional[datetime.datetime] = None,
|
||||
question_entities: Optional[Sequence[MessageEntity]] = None,
|
||||
*,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
):
|
||||
|
@ -304,6 +460,7 @@ class Poll(TelegramObject):
|
|||
)
|
||||
self.open_period: Optional[int] = open_period
|
||||
self.close_date: Optional[datetime.datetime] = close_date
|
||||
self.question_entities: Tuple[MessageEntity, ...] = parse_sequence_arg(question_entities)
|
||||
|
||||
self._id_attrs = (self.id,)
|
||||
|
||||
|
@ -323,11 +480,13 @@ class Poll(TelegramObject):
|
|||
data["options"] = [PollOption.de_json(option, bot) for option in data["options"]]
|
||||
data["explanation_entities"] = MessageEntity.de_list(data.get("explanation_entities"), bot)
|
||||
data["close_date"] = from_timestamp(data.get("close_date"), tzinfo=loc_tzinfo)
|
||||
data["question_entities"] = MessageEntity.de_list(data.get("question_entities"), bot)
|
||||
|
||||
return super().de_json(data=data, bot=bot)
|
||||
|
||||
def parse_explanation_entity(self, entity: MessageEntity) -> str:
|
||||
"""Returns the text from a given :class:`telegram.MessageEntity`.
|
||||
"""Returns the text in :attr:`explanation` from a given :class:`telegram.MessageEntity` of
|
||||
:attr:`explanation_entities`.
|
||||
|
||||
Note:
|
||||
This method is present because Telegram calculates the offset and length in
|
||||
|
@ -336,7 +495,7 @@ class Poll(TelegramObject):
|
|||
|
||||
Args:
|
||||
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
|
||||
be an entity that belongs to this message.
|
||||
be an entity that belongs to :attr:`explanation_entities`.
|
||||
|
||||
Returns:
|
||||
:obj:`str`: The text of the given entity.
|
||||
|
@ -348,10 +507,7 @@ class Poll(TelegramObject):
|
|||
if not self.explanation:
|
||||
raise RuntimeError("This Poll has no 'explanation'.")
|
||||
|
||||
entity_text = self.explanation.encode("utf-16-le")
|
||||
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
|
||||
|
||||
return entity_text.decode("utf-16-le")
|
||||
return parse_message_entity(self.explanation, entity)
|
||||
|
||||
def parse_explanation_entities(
|
||||
self, types: Optional[List[str]] = None
|
||||
|
@ -375,15 +531,61 @@ class Poll(TelegramObject):
|
|||
Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
|
||||
the text that belongs to them, calculated based on UTF-16 codepoints.
|
||||
|
||||
"""
|
||||
if types is None:
|
||||
types = MessageEntity.ALL_TYPES
|
||||
Raises:
|
||||
RuntimeError: If the poll has no explanation.
|
||||
|
||||
return {
|
||||
entity: self.parse_explanation_entity(entity)
|
||||
for entity in self.explanation_entities
|
||||
if entity.type in types
|
||||
}
|
||||
"""
|
||||
if not self.explanation:
|
||||
raise RuntimeError("This Poll has no 'explanation'.")
|
||||
|
||||
return parse_message_entities(self.explanation, self.explanation_entities, types)
|
||||
|
||||
def parse_question_entity(self, entity: MessageEntity) -> str:
|
||||
"""Returns the text in :attr:`question` from a given :class:`telegram.MessageEntity` of
|
||||
:attr:`question_entities`.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Note:
|
||||
This method is present because Telegram calculates the offset and length in
|
||||
UTF-16 codepoint pairs, which some versions of Python don't handle automatically.
|
||||
(That is, you can't just slice ``Message.text`` with the offset and length.)
|
||||
|
||||
Args:
|
||||
entity (:class:`telegram.MessageEntity`): The entity to extract the text from. It must
|
||||
be an entity that belongs to :attr:`question_entities`.
|
||||
|
||||
Returns:
|
||||
:obj:`str`: The text of the given entity.
|
||||
"""
|
||||
return parse_message_entity(self.question, entity)
|
||||
|
||||
def parse_question_entities(
|
||||
self, types: Optional[List[str]] = None
|
||||
) -> Dict[MessageEntity, str]:
|
||||
"""
|
||||
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
|
||||
It contains entities from this polls question filtered by their ``type`` attribute as
|
||||
the key, and the text that each entity belongs to as the value of the :obj:`dict`.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Note:
|
||||
This method should always be used instead of the :attr:`question_entities`
|
||||
attribute, since it calculates the correct substring from the message text based on
|
||||
UTF-16 codepoints. See :attr:`parse_question_entity` for more info.
|
||||
|
||||
Args:
|
||||
types (List[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the
|
||||
``type`` attribute of an entity is contained in this list, it will be returned.
|
||||
Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`.
|
||||
|
||||
Returns:
|
||||
Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
|
||||
the text that belongs to them, calculated based on UTF-16 codepoints.
|
||||
|
||||
"""
|
||||
return parse_message_entities(self.question, self.question_entities, types)
|
||||
|
||||
REGULAR: Final[str] = constants.PollType.REGULAR
|
||||
""":const:`telegram.constants.PollType.REGULAR`"""
|
||||
|
|
|
@ -355,6 +355,7 @@ class ReplyParameters(TelegramObject):
|
|||
chat, or in the chat :paramref:`chat_id` if it is specified.
|
||||
chat_id (:obj:`int` | :obj:`str`, optional): If the message to be replied to is from a
|
||||
different chat, |chat_id_channel|
|
||||
Not supported for messages sent on behalf of a business account.
|
||||
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply| Can be
|
||||
used only for replies in the same chat and forum topic.
|
||||
quote (:obj:`str`, optional): Quoted part of the message to be replied to; 0-1024
|
||||
|
@ -376,6 +377,7 @@ class ReplyParameters(TelegramObject):
|
|||
chat, or in the chat :paramref:`chat_id` if it is specified.
|
||||
chat_id (:obj:`int` | :obj:`str`): Optional. If the message to be replied to is from a
|
||||
different chat, |chat_id_channel|
|
||||
Not supported for messages sent on behalf of a business account.
|
||||
allow_sending_without_reply (:obj:`bool`): Optional. |allow_sending_without_reply| Can be
|
||||
used only for replies in the same chat and forum topic.
|
||||
quote (:obj:`str`): Optional. Quoted part of the message to be replied to; 0-1024
|
||||
|
|
|
@ -28,7 +28,8 @@ from telegram._utils.types import JSONDict
|
|||
|
||||
|
||||
class ReplyKeyboardMarkup(TelegramObject):
|
||||
"""This object represents a custom keyboard with reply options.
|
||||
"""This object represents a custom keyboard with reply options. Not supported in channels and
|
||||
for messages sent on behalf of a Telegram Business account.
|
||||
|
||||
Objects of this class are comparable in terms of equality. Two objects of this class are
|
||||
considered equal, if their size of :attr:`keyboard` and all the buttons are equal.
|
||||
|
|
|
@ -29,6 +29,7 @@ class ReplyKeyboardRemove(TelegramObject):
|
|||
keyboard and display the default letter-keyboard. By default, custom keyboards are displayed
|
||||
until a new keyboard is sent by a bot. An exception is made for one-time keyboards that are
|
||||
hidden immediately after the user presses a button (see :class:`telegram.ReplyKeyboardMarkup`).
|
||||
Not supported in channels and for messages sent on behalf of a Telegram Business account.
|
||||
|
||||
Note:
|
||||
User will not be able to summon this keyboard; if you want to hide the keyboard from
|
||||
|
|
|
@ -44,7 +44,7 @@ class UsersShared(TelegramObject):
|
|||
The argument :attr:`users` is now considered for the equality comparison instead of
|
||||
``user_ids``.
|
||||
|
||||
.. versionremoved:: NEXT.VERSION
|
||||
.. versionremoved:: 21.2
|
||||
Removed the deprecated argument and attribute ``user_ids``.
|
||||
|
||||
Args:
|
||||
|
@ -54,7 +54,7 @@ class UsersShared(TelegramObject):
|
|||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
.. versionchanged:: NEXT.VERSION
|
||||
.. versionchanged:: 21.2
|
||||
This argument is now required.
|
||||
|
||||
Attributes:
|
||||
|
|
|
@ -141,8 +141,8 @@ class Update(TelegramObject):
|
|||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
business_message (:class:`telegram.Message`, optional): New non-service message
|
||||
from a connected business account.
|
||||
business_message (:class:`telegram.Message`, optional): New message from a connected
|
||||
business account.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
|
@ -249,8 +249,8 @@ class Update(TelegramObject):
|
|||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
business_message (:class:`telegram.Message`): Optional. New non-service message
|
||||
from a connected business account.
|
||||
business_message (:class:`telegram.Message`): Optional. New message from a connected
|
||||
business account.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ if TYPE_CHECKING:
|
|||
InputMediaDocument,
|
||||
InputMediaPhoto,
|
||||
InputMediaVideo,
|
||||
InputPollOption,
|
||||
LabeledPrice,
|
||||
LinkPreviewOptions,
|
||||
Location,
|
||||
|
@ -1482,7 +1483,7 @@ class User(TelegramObject):
|
|||
async def send_poll(
|
||||
self,
|
||||
question: str,
|
||||
options: Sequence[str],
|
||||
options: Sequence[Union[str, "InputPollOption"]],
|
||||
is_anonymous: Optional[bool] = None,
|
||||
type: Optional[str] = None,
|
||||
allows_multiple_answers: Optional[bool] = None,
|
||||
|
@ -1499,6 +1500,8 @@ class User(TelegramObject):
|
|||
message_thread_id: Optional[int] = None,
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
business_connection_id: Optional[str] = None,
|
||||
question_parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
question_entities: Optional[Sequence["MessageEntity"]] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
|
@ -1548,6 +1551,8 @@ class User(TelegramObject):
|
|||
protect_content=protect_content,
|
||||
message_thread_id=message_thread_id,
|
||||
business_connection_id=business_connection_id,
|
||||
question_parse_mode=question_parse_mode,
|
||||
question_entities=question_entities,
|
||||
)
|
||||
|
||||
async def send_copy(
|
||||
|
|
71
telegram/_utils/entities.py
Normal file
71
telegram/_utils/entities.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
"""This module contains auxiliary functionality for parsing MessageEntity objects.
|
||||
|
||||
Warning:
|
||||
Contents of this module are intended to be used internally by the library and *not* by the
|
||||
user. Changes to this module are not considered breaking changes and may not be documented in
|
||||
the changelog.
|
||||
"""
|
||||
from typing import Dict, Optional, Sequence
|
||||
|
||||
from telegram._messageentity import MessageEntity
|
||||
|
||||
|
||||
def parse_message_entity(text: str, entity: MessageEntity) -> str:
|
||||
"""Returns the text from a given :class:`telegram.MessageEntity`.
|
||||
|
||||
Args:
|
||||
text (:obj:`str`): The text to extract the entity from.
|
||||
entity (:class:`telegram.MessageEntity`): The entity to extract the text from.
|
||||
|
||||
Returns:
|
||||
:obj:`str`: The text of the given entity.
|
||||
"""
|
||||
entity_text = text.encode("utf-16-le")
|
||||
entity_text = entity_text[entity.offset * 2 : (entity.offset + entity.length) * 2]
|
||||
|
||||
return entity_text.decode("utf-16-le")
|
||||
|
||||
|
||||
def parse_message_entities(
|
||||
text: str, entities: Sequence[MessageEntity], types: Optional[Sequence[str]] = None
|
||||
) -> Dict[MessageEntity, str]:
|
||||
"""
|
||||
Returns a :obj:`dict` that maps :class:`telegram.MessageEntity` to :obj:`str`.
|
||||
It contains entities filtered by their ``type`` attribute as
|
||||
the key, and the text that each entity belongs to as the value of the :obj:`dict`.
|
||||
|
||||
Args:
|
||||
text (:obj:`str`): The text to extract the entity from.
|
||||
entities (List[:class:`telegram.MessageEntity`]): The entities to extract the text from.
|
||||
types (List[:obj:`str`], optional): List of ``MessageEntity`` types as strings. If the
|
||||
``type`` attribute of an entity is contained in this list, it will be returned.
|
||||
Defaults to :attr:`telegram.MessageEntity.ALL_TYPES`.
|
||||
|
||||
Returns:
|
||||
Dict[:class:`telegram.MessageEntity`, :obj:`str`]: A dictionary of entities mapped to
|
||||
the text that belongs to them, calculated based on UTF-16 codepoints.
|
||||
"""
|
||||
if types is None:
|
||||
types = MessageEntity.ALL_TYPES
|
||||
|
||||
return {
|
||||
entity: parse_message_entity(text, entity) for entity in entities if entity.type in types
|
||||
}
|
|
@ -26,19 +26,28 @@ Warning:
|
|||
the changelog.
|
||||
"""
|
||||
import warnings
|
||||
from typing import Type
|
||||
from typing import Type, Union
|
||||
|
||||
from telegram.warnings import PTBUserWarning
|
||||
|
||||
|
||||
def warn(message: str, category: Type[Warning] = PTBUserWarning, stacklevel: int = 0) -> None:
|
||||
def warn(
|
||||
message: Union[str, PTBUserWarning],
|
||||
category: Type[Warning] = PTBUserWarning,
|
||||
stacklevel: int = 0,
|
||||
) -> None:
|
||||
"""
|
||||
Helper function used as a shortcut for warning with default values.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
|
||||
Args:
|
||||
message (:obj:`str`): Specify the warnings message to pass to ``warnings.warn()``.
|
||||
message (:obj:`str` | :obj:`PTBUserWarning`): Specify the warnings message to pass to
|
||||
``warnings.warn()``.
|
||||
|
||||
.. versionchanged:: 21.2
|
||||
Now also accepts a :obj:`PTBUserWarning` instance.
|
||||
|
||||
category (:obj:`Type[Warning]`, optional): Specify the Warning class to pass to
|
||||
``warnings.warn()``. Defaults to :class:`telegram.warnings.PTBUserWarning`.
|
||||
stacklevel (:obj:`int`, optional): Specify the stacklevel to pass to ``warnings.warn()``.
|
||||
|
|
|
@ -23,10 +23,10 @@ inside warnings.py.
|
|||
|
||||
.. versionadded:: 20.2
|
||||
"""
|
||||
from typing import Any, Callable, Type
|
||||
from typing import Any, Callable, Type, Union
|
||||
|
||||
from telegram._utils.warnings import warn
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
from telegram.warnings import PTBDeprecationWarning, PTBUserWarning
|
||||
|
||||
|
||||
def build_deprecation_warning_message(
|
||||
|
@ -54,8 +54,9 @@ def warn_about_deprecated_arg_return_new_arg(
|
|||
deprecated_arg_name: str,
|
||||
new_arg_name: str,
|
||||
bot_api_version: str,
|
||||
ptb_version: str,
|
||||
stacklevel: int = 2,
|
||||
warn_callback: Callable[[str, Type[Warning], int], None] = warn,
|
||||
warn_callback: Callable[[Union[str, PTBUserWarning], Type[Warning], int], None] = warn,
|
||||
) -> Any:
|
||||
"""A helper function for the transition in API when argument is renamed.
|
||||
|
||||
|
@ -80,10 +81,12 @@ def warn_about_deprecated_arg_return_new_arg(
|
|||
|
||||
if deprecated_arg:
|
||||
warn_callback(
|
||||
f"Bot API {bot_api_version} renamed the argument '{deprecated_arg_name}' to "
|
||||
f"'{new_arg_name}'.",
|
||||
PTBDeprecationWarning,
|
||||
stacklevel + 1,
|
||||
PTBDeprecationWarning(
|
||||
ptb_version,
|
||||
f"Bot API {bot_api_version} renamed the argument '{deprecated_arg_name}' to "
|
||||
f"'{new_arg_name}'.",
|
||||
),
|
||||
stacklevel=stacklevel + 1, # type: ignore[call-arg]
|
||||
)
|
||||
return deprecated_arg
|
||||
|
||||
|
@ -94,6 +97,7 @@ def warn_about_deprecated_attr_in_property(
|
|||
deprecated_attr_name: str,
|
||||
new_attr_name: str,
|
||||
bot_api_version: str,
|
||||
ptb_version: str,
|
||||
stacklevel: int = 2,
|
||||
) -> None:
|
||||
"""A helper function for the transition in API when attribute is renamed. Call from properties.
|
||||
|
@ -101,8 +105,10 @@ def warn_about_deprecated_attr_in_property(
|
|||
The properties replace deprecated attributes in classes and issue these deprecation warnings.
|
||||
"""
|
||||
warn(
|
||||
f"Bot API {bot_api_version} renamed the attribute '{deprecated_attr_name}' to "
|
||||
f"'{new_attr_name}'.",
|
||||
PTBDeprecationWarning,
|
||||
PTBDeprecationWarning(
|
||||
ptb_version,
|
||||
f"Bot API {bot_api_version} renamed the attribute '{deprecated_attr_name}' to "
|
||||
f"'{new_attr_name}'.",
|
||||
),
|
||||
stacklevel=stacklevel + 1,
|
||||
)
|
||||
|
|
|
@ -51,7 +51,7 @@ class Version(NamedTuple):
|
|||
|
||||
|
||||
__version_info__: Final[Version] = Version(
|
||||
major=21, minor=1, micro=1, releaselevel="final", serial=0
|
||||
major=21, minor=2, micro=0, releaselevel="final", serial=0
|
||||
)
|
||||
__version__: Final[str] = str(__version_info__)
|
||||
|
||||
|
|
|
@ -37,6 +37,10 @@ __all__ = [
|
|||
"SUPPORTED_WEBHOOK_PORTS",
|
||||
"ZERO_DATE",
|
||||
"AccentColor",
|
||||
"BackgroundFillLimit",
|
||||
"BackgroundFillType",
|
||||
"BackgroundTypeLimit",
|
||||
"BackgroundTypeType",
|
||||
"BotCommandLimit",
|
||||
"BotCommandScopeType",
|
||||
"BotDescriptionLimit",
|
||||
|
@ -142,7 +146,7 @@ class _AccentColor(NamedTuple):
|
|||
#: :data:`telegram.__bot_api_version_info__`.
|
||||
#:
|
||||
#: .. versionadded:: 20.0
|
||||
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=2)
|
||||
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=3)
|
||||
#: :obj:`str`: Telegram Bot API
|
||||
#: version supported by this version of `python-telegram-bot`. Also available as
|
||||
#: :data:`telegram.__bot_api_version__`.
|
||||
|
@ -822,6 +826,46 @@ class ChatLimit(IntEnum):
|
|||
"""
|
||||
|
||||
|
||||
class BackgroundTypeLimit(IntEnum):
|
||||
"""This enum contains limitations for :class:`telegram.BackgroundTypeFill`,
|
||||
:class:`telegram.BackgroundTypeWallpaper` and :class:`telegram.BackgroundTypePattern`.
|
||||
The enum members of this enumeration are instances of :class:`int` and can be treated as such.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
MAX_DIMMING = 100
|
||||
""":obj:`int`: Maximum value allowed for:
|
||||
|
||||
* :paramref:`~telegram.BackgroundTypeFill.dark_theme_dimming` parameter of
|
||||
:class:`telegram.BackgroundTypeFill`
|
||||
* :paramref:`~telegram.BackgroundTypeWallpaper.dark_theme_dimming` parameter of
|
||||
:class:`telegram.BackgroundTypeWallpaper`
|
||||
"""
|
||||
MAX_INTENSITY = 100
|
||||
""":obj:`int`: Maximum value allowed for :paramref:`~telegram.BackgroundTypePattern.intensity`
|
||||
parameter of :class:`telegram.BackgroundTypePattern`
|
||||
"""
|
||||
|
||||
|
||||
class BackgroundFillLimit(IntEnum):
|
||||
"""This enum contains limitations for :class:`telegram.BackgroundFillGradient`.
|
||||
The enum members of this enumeration are instances of :class:`int` and can be treated as such.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
MAX_ROTATION_ANGLE = 359
|
||||
""":obj:`int`: Maximum value allowed for:
|
||||
:paramref:`~telegram.BackgroundFillGradient.rotation_angle` parameter of
|
||||
:class:`telegram.BackgroundFillGradient`
|
||||
"""
|
||||
|
||||
|
||||
class ChatMemberStatus(StringEnum):
|
||||
"""This enum contains the available states for :class:`telegram.ChatMember`. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
@ -1427,6 +1471,21 @@ class LocationLimit(IntEnum):
|
|||
:meth:`telegram.Bot.send_location`
|
||||
"""
|
||||
|
||||
LIVE_PERIOD_FOREVER = int(hex(0x7FFFFFFF), 16)
|
||||
""":obj:`int`: Value for live locations that can be edited indefinitely. Passed in:
|
||||
|
||||
* :paramref:`~telegram.InlineQueryResultLocation.live_period` parameter of
|
||||
:class:`telegram.InlineQueryResultLocation`
|
||||
* :paramref:`~telegram.InputLocationMessageContent.live_period` parameter of
|
||||
:class:`telegram.InputLocationMessageContent`
|
||||
* :paramref:`~telegram.Bot.edit_message_live_location.live_period` parameter of
|
||||
:meth:`telegram.Bot.edit_message_live_location`
|
||||
* :paramref:`~telegram.Bot.send_location.live_period` parameter of
|
||||
:meth:`telegram.Bot.send_location`
|
||||
|
||||
.. versionadded:: 21.2
|
||||
"""
|
||||
|
||||
MIN_PROXIMITY_ALERT_RADIUS = 1
|
||||
""":obj:`int`: Minimum value allowed for:
|
||||
|
||||
|
@ -1726,6 +1785,11 @@ class MessageType(StringEnum):
|
|||
|
||||
.. versionadded:: 20.8
|
||||
"""
|
||||
CHAT_BACKGROUND_SET = "chat_background_set"
|
||||
""":obj:`str`: Messages with :attr:`telegram.Message.chat_background_set`.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
"""
|
||||
CONNECTED_WEBSITE = "connected_website"
|
||||
""":obj:`str`: Messages with :attr:`telegram.Message.connected_website`."""
|
||||
CONTACT = "contact"
|
||||
|
@ -2878,3 +2942,39 @@ class ReactionEmoji(StringEnum):
|
|||
""":obj:`str`: Woman Shrugging"""
|
||||
POUTING_FACE = "😡"
|
||||
""":obj:`str`: Pouting face"""
|
||||
|
||||
|
||||
class BackgroundTypeType(StringEnum):
|
||||
"""This enum contains the available types of :class:`telegram.BackgroundType`. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
FILL = "fill"
|
||||
""":obj:`str`: A :class:`telegram.BackgroundType` with fill background."""
|
||||
WALLPAPER = "wallpaper"
|
||||
""":obj:`str`: A :class:`telegram.BackgroundType` with wallpaper background."""
|
||||
PATTERN = "pattern"
|
||||
""":obj:`str`: A :class:`telegram.BackgroundType` with pattern background."""
|
||||
CHAT_THEME = "chat_theme"
|
||||
""":obj:`str`: A :class:`telegram.BackgroundType` with chat_theme background."""
|
||||
|
||||
|
||||
class BackgroundFillType(StringEnum):
|
||||
"""This enum contains the available types of :class:`telegram.BackgroundFill`. The enum
|
||||
members of this enumeration are instances of :class:`str` and can be treated as such.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
SOLID = "solid"
|
||||
""":obj:`str`: A :class:`telegram.BackgroundFill` with solid fill."""
|
||||
GRADIENT = "gradient"
|
||||
""":obj:`str`: A :class:`telegram.BackgroundFill` with gradient fill."""
|
||||
FREEFORM_GRADIENT = "freeform_gradient"
|
||||
""":obj:`str`: A :class:`telegram.BackgroundFill` with freeform_gradient fill."""
|
||||
|
|
|
@ -365,6 +365,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
|||
self.__update_persistence_event = asyncio.Event()
|
||||
self.__update_persistence_lock = asyncio.Lock()
|
||||
self.__create_task_tasks: Set[asyncio.Task] = set() # Used for awaiting tasks upon exit
|
||||
self.__stop_running_marker = asyncio.Event()
|
||||
|
||||
async def __aenter__(self: _AppType) -> _AppType: # noqa: PYI019
|
||||
"""|async_context_manager| :meth:`initializes <initialize>` the App.
|
||||
|
@ -516,6 +517,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
|||
await self._add_ch_to_persistence(handler)
|
||||
|
||||
self._initialized = True
|
||||
self.__stop_running_marker.clear()
|
||||
|
||||
async def _add_ch_to_persistence(self, handler: "ConversationHandler") -> None:
|
||||
self._conversation_handler_conversations.update(
|
||||
|
@ -670,14 +672,26 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
|||
raise RuntimeError("This Application is not running!")
|
||||
|
||||
self._running = False
|
||||
self.__stop_running_marker.clear()
|
||||
_LOGGER.info("Application is stopping. This might take a moment.")
|
||||
|
||||
# Stop listening for new updates and handle all pending ones
|
||||
await self.update_queue.put(_STOP_SIGNAL)
|
||||
_LOGGER.debug("Waiting for update_queue to join")
|
||||
await self.update_queue.join()
|
||||
if self.__update_fetcher_task:
|
||||
await self.__update_fetcher_task
|
||||
if self.__update_fetcher_task.done():
|
||||
try:
|
||||
self.__update_fetcher_task.result()
|
||||
except BaseException as exc:
|
||||
_LOGGER.critical(
|
||||
"Fetching updates was aborted due to %r. Suppressing "
|
||||
"exception to ensure graceful shutdown.",
|
||||
exc,
|
||||
exc_info=True,
|
||||
)
|
||||
else:
|
||||
await self.update_queue.put(_STOP_SIGNAL)
|
||||
_LOGGER.debug("Waiting for update_queue to join")
|
||||
await self.update_queue.join()
|
||||
await self.__update_fetcher_task
|
||||
_LOGGER.debug("Application stopped fetching of updates.")
|
||||
|
||||
if self._job_queue:
|
||||
|
@ -703,17 +717,36 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
|||
shutdown of the application, i.e. the methods listed in :attr:`run_polling` and
|
||||
:attr:`run_webhook` will still be executed.
|
||||
|
||||
This method can also be called within :meth:`post_init`. This allows for a graceful,
|
||||
early shutdown of the application if some condition is met (e.g., a database connection
|
||||
could not be established).
|
||||
|
||||
Note:
|
||||
If the application is not running, this method does nothing.
|
||||
If the application is not running and this method is not called within
|
||||
:meth:`post_init`, this method does nothing.
|
||||
|
||||
Warning:
|
||||
This method is designed to for use in combination with :meth:`run_polling` or
|
||||
:meth:`run_webhook`. Using this method in combination with a custom logic for starting
|
||||
and stopping the application is not guaranteed to work as expected. Use at your own
|
||||
risk.
|
||||
|
||||
.. versionadded:: 20.5
|
||||
|
||||
.. versionchanged:: 21.2
|
||||
Added support for calling within :meth:`post_init`.
|
||||
"""
|
||||
if self.running:
|
||||
# This works because `__run` is using `loop.run_forever()`. If that changes, this
|
||||
# method needs to be adapted.
|
||||
asyncio.get_running_loop().stop()
|
||||
else:
|
||||
_LOGGER.debug("Application is not running, stop_running() does nothing.")
|
||||
self.__stop_running_marker.set()
|
||||
if not self._initialized:
|
||||
_LOGGER.debug(
|
||||
"Application is not running and not initialized. `stop_running()` likely has "
|
||||
"no effect."
|
||||
)
|
||||
|
||||
def run_polling(
|
||||
self,
|
||||
|
@ -733,9 +766,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
|||
polling updates from Telegram using :meth:`telegram.ext.Updater.start_polling` and
|
||||
a graceful shutdown of the app on exit.
|
||||
|
||||
The app will shut down when :exc:`KeyboardInterrupt` or :exc:`SystemExit` is raised.
|
||||
On unix, the app will also shut down on receiving the signals specified by
|
||||
:paramref:`stop_signals`.
|
||||
|app_run_shutdown| :paramref:`stop_signals`.
|
||||
|
||||
The order of execution by :meth:`run_polling` is roughly as follows:
|
||||
|
||||
|
@ -826,9 +857,11 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
|||
|
||||
if (read_timeout, write_timeout, connect_timeout, pool_timeout) != ((DEFAULT_NONE,) * 4):
|
||||
warn(
|
||||
"Setting timeouts via `Application.run_polling` is deprecated. "
|
||||
"Please use `ApplicationBuilder.get_updates_*_timeout` instead.",
|
||||
PTBDeprecationWarning,
|
||||
PTBDeprecationWarning(
|
||||
"20.6",
|
||||
"Setting timeouts via `Application.run_polling` is deprecated. "
|
||||
"Please use `ApplicationBuilder.get_updates_*_timeout` instead.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
@ -874,9 +907,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
|||
listening for updates from Telegram using :meth:`telegram.ext.Updater.start_webhook` and
|
||||
a graceful shutdown of the app on exit.
|
||||
|
||||
The app will shut down when :exc:`KeyboardInterrupt` or :exc:`SystemExit` is raised.
|
||||
On unix, the app will also shut down on receiving the signals specified by
|
||||
:paramref:`stop_signals`.
|
||||
|app_run_shutdown| :paramref:`stop_signals`.
|
||||
|
||||
If :paramref:`cert`
|
||||
and :paramref:`key` are not provided, the webhook will be started directly on
|
||||
|
@ -1038,6 +1069,9 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
|||
loop.run_until_complete(self.initialize())
|
||||
if self.post_init:
|
||||
loop.run_until_complete(self.post_init(self))
|
||||
if self.__stop_running_marker.is_set():
|
||||
_LOGGER.info("Application received stop signal via `stop_running`. Shutting down.")
|
||||
return
|
||||
loop.run_until_complete(updater_coroutine) # one of updater.start_webhook/polling
|
||||
loop.run_until_complete(self.start())
|
||||
loop.run_forever()
|
||||
|
@ -1054,8 +1088,9 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
|||
loop.run_until_complete(self.updater.stop()) # type: ignore[union-attr]
|
||||
if self.running:
|
||||
loop.run_until_complete(self.stop())
|
||||
if self.post_stop:
|
||||
loop.run_until_complete(self.post_stop(self))
|
||||
# post_stop should be called only if stop was called!
|
||||
if self.post_stop:
|
||||
loop.run_until_complete(self.post_stop(self))
|
||||
loop.run_until_complete(self.shutdown())
|
||||
if self.post_shutdown:
|
||||
loop.run_until_complete(self.post_shutdown(self))
|
||||
|
@ -1150,9 +1185,11 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
|||
# Generator-based coroutines are not supported in Python 3.12+
|
||||
if sys.version_info < (3, 12) and isinstance(coroutine, Generator):
|
||||
warn(
|
||||
"Generator-based coroutines are deprecated in create_task and will not work"
|
||||
" in Python 3.12+",
|
||||
category=PTBDeprecationWarning,
|
||||
PTBDeprecationWarning(
|
||||
"20.4",
|
||||
"Generator-based coroutines are deprecated in create_task and will not"
|
||||
" work in Python 3.12+",
|
||||
),
|
||||
)
|
||||
return await asyncio.create_task(coroutine)
|
||||
# If user uses generator in python 3.12+, Exception will happen and we cannot do
|
||||
|
@ -1184,45 +1221,44 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
|||
finally:
|
||||
self._mark_for_persistence_update(update=update)
|
||||
|
||||
async def _update_fetcher(self) -> None:
|
||||
async def __update_fetcher(self) -> None:
|
||||
# Continuously fetch updates from the queue. Exit only once the signal object is found.
|
||||
while True:
|
||||
try:
|
||||
update = await self.update_queue.get()
|
||||
update = await self.update_queue.get()
|
||||
|
||||
if update is _STOP_SIGNAL:
|
||||
_LOGGER.debug("Dropping pending updates")
|
||||
while not self.update_queue.empty():
|
||||
self.update_queue.task_done()
|
||||
if update is _STOP_SIGNAL:
|
||||
# For the _STOP_SIGNAL
|
||||
self.update_queue.task_done()
|
||||
return
|
||||
|
||||
# For the _STOP_SIGNAL
|
||||
self.update_queue.task_done()
|
||||
return
|
||||
_LOGGER.debug("Processing update %s", update)
|
||||
|
||||
_LOGGER.debug("Processing update %s", update)
|
||||
|
||||
if self._update_processor.max_concurrent_updates > 1:
|
||||
# We don't await the below because it has to be run concurrently
|
||||
self.create_task(
|
||||
self.__process_update_wrapper(update),
|
||||
update=update,
|
||||
name=f"Application:{self.bot.id}:process_concurrent_update",
|
||||
)
|
||||
else:
|
||||
await self.__process_update_wrapper(update)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
# This may happen if the application is manually run via application.start() and
|
||||
# then a KeyboardInterrupt is sent. We must prevent this loop to die since
|
||||
# application.stop() will wait for it's clean shutdown.
|
||||
_LOGGER.warning(
|
||||
"Fetching updates got a asyncio.CancelledError. Ignoring as this task may only"
|
||||
"be closed via `Application.stop`."
|
||||
if self._update_processor.max_concurrent_updates > 1:
|
||||
# We don't await the below because it has to be run concurrently
|
||||
self.create_task(
|
||||
self.__process_update_wrapper(update),
|
||||
update=update,
|
||||
name=f"Application:{self.bot.id}:process_concurrent_update",
|
||||
)
|
||||
else:
|
||||
await self.__process_update_wrapper(update)
|
||||
|
||||
async def _update_fetcher(self) -> None:
|
||||
try:
|
||||
await self.__update_fetcher()
|
||||
finally:
|
||||
while not self.update_queue.empty():
|
||||
_LOGGER.debug("Dropping pending update: %s", self.update_queue.get_nowait())
|
||||
with contextlib.suppress(ValueError):
|
||||
# Since we're shutting down here, it's not too bad if we call task_done
|
||||
# on an empty queue
|
||||
self.update_queue.task_done()
|
||||
|
||||
async def __process_update_wrapper(self, update: object) -> None:
|
||||
await self._update_processor.process_update(update, self.process_update(update))
|
||||
self.update_queue.task_done()
|
||||
try:
|
||||
await self._update_processor.process_update(update, self.process_update(update))
|
||||
finally:
|
||||
self.update_queue.task_done()
|
||||
|
||||
async def process_update(self, update: object) -> None:
|
||||
"""Processes a single update and marks the update to be updated by the persistence later.
|
||||
|
@ -1251,30 +1287,43 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
|||
try:
|
||||
for handler in handlers:
|
||||
check = handler.check_update(update) # Should the handler handle this update?
|
||||
if not (check is None or check is False): # if yes,
|
||||
if not context: # build a context if not already built
|
||||
context = self.context_types.context.from_update(update, self)
|
||||
await context.refresh_data()
|
||||
coroutine: Coroutine = handler.handle_update(update, self, check, context)
|
||||
if check is None or check is False:
|
||||
continue
|
||||
|
||||
if not handler.block or ( # if handler is running with block=False,
|
||||
handler.block is DEFAULT_TRUE
|
||||
and isinstance(self.bot, ExtBot)
|
||||
and self.bot.defaults
|
||||
and not self.bot.defaults.block
|
||||
):
|
||||
self.create_task(
|
||||
coroutine,
|
||||
update=update,
|
||||
name=(
|
||||
f"Application:{self.bot.id}:process_update_non_blocking"
|
||||
f":{handler}"
|
||||
if not context: # build a context if not already built
|
||||
try:
|
||||
context = self.context_types.context.from_update(update, self)
|
||||
except Exception as exc:
|
||||
_LOGGER.critical(
|
||||
(
|
||||
"Error while building CallbackContext for update %s. "
|
||||
"Update will not be processed."
|
||||
),
|
||||
update,
|
||||
exc_info=exc,
|
||||
)
|
||||
else:
|
||||
any_blocking = True
|
||||
await coroutine
|
||||
break # Only a max of 1 handler per group is handled
|
||||
return
|
||||
await context.refresh_data()
|
||||
coroutine: Coroutine = handler.handle_update(update, self, check, context)
|
||||
|
||||
if not handler.block or ( # if handler is running with block=False,
|
||||
handler.block is DEFAULT_TRUE
|
||||
and isinstance(self.bot, ExtBot)
|
||||
and self.bot.defaults
|
||||
and not self.bot.defaults.block
|
||||
):
|
||||
self.create_task(
|
||||
coroutine,
|
||||
update=update,
|
||||
name=(
|
||||
f"Application:{self.bot.id}:process_update_non_blocking"
|
||||
f":{handler}"
|
||||
),
|
||||
)
|
||||
else:
|
||||
any_blocking = True
|
||||
await coroutine
|
||||
break # Only a max of 1 handler per group is handled
|
||||
|
||||
# Stop processing with any other handler.
|
||||
except ApplicationHandlerStop:
|
||||
|
@ -1808,13 +1857,25 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
|
|||
callback,
|
||||
block,
|
||||
) in self.error_handlers.items():
|
||||
context = self.context_types.context.from_error(
|
||||
update=update,
|
||||
error=error,
|
||||
application=self,
|
||||
job=job,
|
||||
coroutine=coroutine,
|
||||
)
|
||||
try:
|
||||
context = self.context_types.context.from_error(
|
||||
update=update,
|
||||
error=error,
|
||||
application=self,
|
||||
job=job,
|
||||
coroutine=coroutine,
|
||||
)
|
||||
except Exception as exc:
|
||||
_LOGGER.critical(
|
||||
(
|
||||
"Error while building CallbackContext for exception %s. "
|
||||
"Exception will not be processed by error handlers."
|
||||
),
|
||||
error,
|
||||
exc_info=exc,
|
||||
)
|
||||
return False
|
||||
|
||||
if not block or ( # If error handler has `block=False`, create a Task to run cb
|
||||
block is DEFAULT_TRUE
|
||||
and isinstance(self.bot, ExtBot)
|
||||
|
|
|
@ -528,9 +528,11 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
|||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
warn(
|
||||
"`ApplicationBuilder.proxy_url` is deprecated since version "
|
||||
"20.7. Use `ApplicationBuilder.proxy` instead.",
|
||||
PTBDeprecationWarning,
|
||||
PTBDeprecationWarning(
|
||||
"20.7",
|
||||
"`ApplicationBuilder.proxy_url` is deprecated. Use `ApplicationBuilder.proxy` "
|
||||
"instead.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.proxy(proxy_url)
|
||||
|
@ -760,9 +762,11 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
|||
:class:`ApplicationBuilder`: The same builder with the updated argument.
|
||||
"""
|
||||
warn(
|
||||
"`ApplicationBuilder.get_updates_proxy_url` is deprecated since version "
|
||||
"20.7. Use `ApplicationBuilder.get_updates_proxy` instead.",
|
||||
PTBDeprecationWarning,
|
||||
PTBDeprecationWarning(
|
||||
"20.7",
|
||||
"`ApplicationBuilder.get_updates_proxy_url` is deprecated. Use "
|
||||
"`ApplicationBuilder.get_updates_proxy` instead.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.get_updates_proxy(get_updates_proxy_url)
|
||||
|
@ -1334,7 +1338,13 @@ class ApplicationBuilder(Generic[BT, CCT, UD, CD, BD, JQ]):
|
|||
|
||||
Tip:
|
||||
This can be used for custom stop logic that requires to await coroutines, e.g.
|
||||
sending message to a chat before shutting down the bot
|
||||
sending message to a chat before shutting down the bot.
|
||||
|
||||
Hint:
|
||||
The callback will be called only, if :meth:`Application.stop` was indeed called
|
||||
successfully. For example, if the application is stopped early by calling
|
||||
:meth:`Application.stop_running` within :meth:`post_init`, then the set callback will
|
||||
*not* be called.
|
||||
|
||||
Example:
|
||||
.. code::
|
||||
|
|
|
@ -156,9 +156,11 @@ class Defaults:
|
|||
raise ValueError("`quote` and `do_quote` are mutually exclusive")
|
||||
if disable_web_page_preview is not None:
|
||||
warn(
|
||||
"`Defaults.disable_web_page_preview` is deprecated. Use "
|
||||
"`Defaults.link_preview_options` instead.",
|
||||
category=PTBDeprecationWarning,
|
||||
PTBDeprecationWarning(
|
||||
"20.8",
|
||||
"`Defaults.disable_web_page_preview` is deprecated. Use "
|
||||
"`Defaults.link_preview_options` instead.",
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
self._link_preview_options: Optional[LinkPreviewOptions] = LinkPreviewOptions(
|
||||
|
@ -169,8 +171,9 @@ class Defaults:
|
|||
|
||||
if quote is not None:
|
||||
warn(
|
||||
"`Defaults.quote` is deprecated. Use `Defaults.do_quote` instead.",
|
||||
category=PTBDeprecationWarning,
|
||||
PTBDeprecationWarning(
|
||||
"20.8", "`Defaults.quote` is deprecated. Use `Defaults.do_quote` instead."
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
self._do_quote: Optional[bool] = quote
|
||||
|
@ -179,13 +182,14 @@ class Defaults:
|
|||
# Gather all defaults that actually have a default value
|
||||
self._api_defaults = {}
|
||||
for kwarg in (
|
||||
"parse_mode",
|
||||
"explanation_parse_mode",
|
||||
"disable_notification",
|
||||
"allow_sending_without_reply",
|
||||
"protect_content",
|
||||
"link_preview_options",
|
||||
"disable_notification",
|
||||
"do_quote",
|
||||
"explanation_parse_mode",
|
||||
"link_preview_options",
|
||||
"parse_mode",
|
||||
"protect_content",
|
||||
"question_parse_mode",
|
||||
):
|
||||
value = getattr(self, kwarg)
|
||||
if value is not None:
|
||||
|
@ -264,6 +268,36 @@ class Defaults:
|
|||
"You can not assign a new value to quote_parse_mode after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def text_parse_mode(self) -> Optional[str]:
|
||||
""":obj:`str`: Optional. Alias for :attr:`parse_mode`, used for
|
||||
the corresponding parameter of :class:`telegram.InputPollOption`.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
"""
|
||||
return self._parse_mode
|
||||
|
||||
@text_parse_mode.setter
|
||||
def text_parse_mode(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
"You can not assign a new value to text_parse_mode after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def question_parse_mode(self) -> Optional[str]:
|
||||
""":obj:`str`: Optional. Alias for :attr:`parse_mode`, used for
|
||||
the corresponding parameter of :meth:`telegram.Bot.send_poll`.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
"""
|
||||
return self._parse_mode
|
||||
|
||||
@question_parse_mode.setter
|
||||
def question_parse_mode(self, _: object) -> NoReturn:
|
||||
raise AttributeError(
|
||||
"You can not assign a new value to question_parse_mode after initialization."
|
||||
)
|
||||
|
||||
@property
|
||||
def disable_notification(self) -> Optional[bool]:
|
||||
""":obj:`bool`: Optional. Sends the message silently. Users will
|
||||
|
|
|
@ -50,8 +50,8 @@ from telegram import (
|
|||
BotShortDescription,
|
||||
BusinessConnection,
|
||||
CallbackQuery,
|
||||
Chat,
|
||||
ChatAdministratorRights,
|
||||
ChatFullInfo,
|
||||
ChatInviteLink,
|
||||
ChatMember,
|
||||
ChatPermissions,
|
||||
|
@ -64,6 +64,7 @@ from telegram import (
|
|||
InlineKeyboardMarkup,
|
||||
InlineQueryResultsButton,
|
||||
InputMedia,
|
||||
InputPollOption,
|
||||
LinkPreviewOptions,
|
||||
Location,
|
||||
MaskPosition,
|
||||
|
@ -113,7 +114,7 @@ if TYPE_CHECKING:
|
|||
)
|
||||
from telegram.ext import BaseRateLimiter, Defaults
|
||||
|
||||
HandledTypes = TypeVar("HandledTypes", bound=Union[Message, CallbackQuery, Chat])
|
||||
HandledTypes = TypeVar("HandledTypes", bound=Union[Message, CallbackQuery, ChatFullInfo])
|
||||
KT = TypeVar("KT", bound=ReplyMarkup)
|
||||
|
||||
|
||||
|
@ -262,7 +263,10 @@ class ExtBot(Bot, Generic[RLARGS]):
|
|||
|
||||
@classmethod
|
||||
def _warn(
|
||||
cls, message: str, category: Type[Warning] = PTBUserWarning, stacklevel: int = 0
|
||||
cls,
|
||||
message: Union[str, PTBUserWarning],
|
||||
category: Type[Warning] = PTBUserWarning,
|
||||
stacklevel: int = 0,
|
||||
) -> None:
|
||||
"""We override this method to add one more level to the stacklevel, so that the warning
|
||||
points to the user's code, not to the PTB code.
|
||||
|
@ -436,6 +440,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
|||
# 3) set the correct parse_mode for all InputMedia objects
|
||||
# 4) handle the LinkPreviewOptions case (see below)
|
||||
# 5) handle the ReplyParameters case (see below)
|
||||
# 6) handle text_parse_mode in InputPollOption
|
||||
for key, val in data.items():
|
||||
# 1)
|
||||
if isinstance(val, DefaultValue):
|
||||
|
@ -487,6 +492,21 @@ class ExtBot(Bot, Generic[RLARGS]):
|
|||
|
||||
data[key] = new_value
|
||||
|
||||
# 6)
|
||||
elif isinstance(val, Sequence) and all(
|
||||
isinstance(obj, InputPollOption) for obj in val
|
||||
):
|
||||
new_val = []
|
||||
for option in val:
|
||||
if not isinstance(option.text_parse_mode, DefaultValue):
|
||||
new_val.append(option)
|
||||
else:
|
||||
new_option = copy(option)
|
||||
with new_option._unfrozen():
|
||||
new_option.text_parse_mode = self.defaults.text_parse_mode
|
||||
new_val.append(new_option)
|
||||
data[key] = new_val
|
||||
|
||||
def _replace_keyboard(self, reply_markup: Optional[KT]) -> Optional[KT]:
|
||||
# If the reply_markup is an inline keyboard and we allow arbitrary callback data, let the
|
||||
# CallbackDataCache build a new keyboard with the data replaced. Otherwise return the input
|
||||
|
@ -554,7 +574,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
|||
self.callback_data_cache.process_message(message=obj)
|
||||
return obj # type: ignore[return-value]
|
||||
|
||||
if isinstance(obj, Chat) and obj.pinned_message:
|
||||
if isinstance(obj, ChatFullInfo) and obj.pinned_message:
|
||||
self.callback_data_cache.process_message(obj.pinned_message)
|
||||
|
||||
return obj
|
||||
|
@ -853,7 +873,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
|||
pool_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
api_kwargs: Optional[JSONDict] = None,
|
||||
rate_limit_args: Optional[RLARGS] = None,
|
||||
) -> Chat:
|
||||
) -> ChatFullInfo:
|
||||
# We override this method to call self._insert_callback_data
|
||||
result = await super().get_chat(
|
||||
chat_id=chat_id,
|
||||
|
@ -1520,6 +1540,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
|||
horizontal_accuracy: Optional[float] = None,
|
||||
heading: Optional[int] = None,
|
||||
proximity_alert_radius: Optional[int] = None,
|
||||
live_period: Optional[int] = None,
|
||||
*,
|
||||
location: Optional[Location] = None,
|
||||
read_timeout: ODVInput[float] = DEFAULT_NONE,
|
||||
|
@ -1539,6 +1560,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
|||
horizontal_accuracy=horizontal_accuracy,
|
||||
heading=heading,
|
||||
proximity_alert_radius=proximity_alert_radius,
|
||||
live_period=live_period,
|
||||
location=location,
|
||||
read_timeout=read_timeout,
|
||||
write_timeout=write_timeout,
|
||||
|
@ -2915,7 +2937,7 @@ class ExtBot(Bot, Generic[RLARGS]):
|
|||
self,
|
||||
chat_id: Union[int, str],
|
||||
question: str,
|
||||
options: Sequence[str],
|
||||
options: Sequence[Union[str, "InputPollOption"]],
|
||||
is_anonymous: Optional[bool] = None,
|
||||
type: Optional[str] = None, # pylint: disable=redefined-builtin
|
||||
allows_multiple_answers: Optional[bool] = None,
|
||||
|
@ -2932,6 +2954,8 @@ class ExtBot(Bot, Generic[RLARGS]):
|
|||
message_thread_id: Optional[int] = None,
|
||||
reply_parameters: Optional["ReplyParameters"] = None,
|
||||
business_connection_id: Optional[str] = None,
|
||||
question_parse_mode: ODVInput[str] = DEFAULT_NONE,
|
||||
question_entities: Optional[Sequence["MessageEntity"]] = None,
|
||||
*,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
|
||||
|
@ -2969,6 +2993,8 @@ class ExtBot(Bot, Generic[RLARGS]):
|
|||
connect_timeout=connect_timeout,
|
||||
pool_timeout=pool_timeout,
|
||||
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
|
||||
question_parse_mode=question_parse_mode,
|
||||
question_entities=question_entities,
|
||||
)
|
||||
|
||||
async def send_sticker(
|
||||
|
|
|
@ -49,7 +49,7 @@ class StringCommandHandler(BaseHandler[str, CCT]):
|
|||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
this handler. Callback signature::
|
||||
|
||||
async def callback(update: Update, context: CallbackContext)
|
||||
async def callback(update: str, context: CallbackContext)
|
||||
|
||||
The return value of the callback is usually ignored except for the special case of
|
||||
:class:`telegram.ext.ConversationHandler`.
|
||||
|
|
|
@ -52,7 +52,7 @@ class StringRegexHandler(BaseHandler[str, CCT]):
|
|||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
this handler. Callback signature::
|
||||
|
||||
async def callback(update: Update, context: CallbackContext)
|
||||
async def callback(update: str, context: CallbackContext)
|
||||
|
||||
The return value of the callback is usually ignored except for the special case of
|
||||
:class:`telegram.ext.ConversationHandler`.
|
||||
|
|
|
@ -43,7 +43,7 @@ class TypeHandler(BaseHandler[UT, CCT]):
|
|||
called when :meth:`check_update` has determined that an update should be processed by
|
||||
this handler. Callback signature::
|
||||
|
||||
async def callback(update: Update, context: CallbackContext)
|
||||
async def callback(update: object, context: CallbackContext)
|
||||
|
||||
The return value of the callback is usually ignored except for the special case of
|
||||
:class:`telegram.ext.ConversationHandler`.
|
||||
|
|
|
@ -31,6 +31,7 @@ try:
|
|||
except ImportError:
|
||||
APS_AVAILABLE = False
|
||||
|
||||
from telegram._utils.logging import get_logger
|
||||
from telegram._utils.repr import build_repr_with_selected_attrs
|
||||
from telegram._utils.types import JSONDict
|
||||
from telegram.ext._extbot import ExtBot
|
||||
|
@ -44,6 +45,7 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
_ALL_DAYS = tuple(range(7))
|
||||
_LOGGER = get_logger(__name__, class_name="JobQueue")
|
||||
|
||||
|
||||
class JobQueue(Generic[CCT]):
|
||||
|
@ -953,7 +955,16 @@ class Job(Generic[CCT]):
|
|||
self, application: "Application[Any, CCT, Any, Any, Any, JobQueue[CCT]]"
|
||||
) -> None:
|
||||
try:
|
||||
context = application.context_types.context.from_job(self, application)
|
||||
try:
|
||||
context = application.context_types.context.from_job(self, application)
|
||||
except Exception as exc:
|
||||
_LOGGER.critical(
|
||||
"Error while building CallbackContext for job %s. Job will not be run.",
|
||||
self._job,
|
||||
exc_info=exc,
|
||||
)
|
||||
return
|
||||
|
||||
await context.refresh_data()
|
||||
await self.callback(context)
|
||||
except Exception as exc:
|
||||
|
|
|
@ -1909,7 +1909,8 @@ class StatusUpdate:
|
|||
def filter(self, update: Update) -> bool:
|
||||
return bool(
|
||||
# keep this alphabetically sorted for easier maintenance
|
||||
StatusUpdate.CHAT_CREATED.check_update(update)
|
||||
StatusUpdate.CHAT_BACKGROUND_SET.check_update(update)
|
||||
or StatusUpdate.CHAT_CREATED.check_update(update)
|
||||
or StatusUpdate.CHAT_SHARED.check_update(update)
|
||||
or StatusUpdate.CONNECTED_WEBSITE.check_update(update)
|
||||
or StatusUpdate.DELETE_CHAT_PHOTO.check_update(update)
|
||||
|
@ -1942,6 +1943,15 @@ class StatusUpdate:
|
|||
ALL = _All(name="filters.StatusUpdate.ALL")
|
||||
"""Messages that contain any of the below."""
|
||||
|
||||
class _ChatBackgroundSet(MessageFilter):
|
||||
__slots__ = ()
|
||||
|
||||
def filter(self, message: Message) -> bool:
|
||||
return bool(message.chat_background_set)
|
||||
|
||||
CHAT_BACKGROUND_SET = _ChatBackgroundSet(name="filters.StatusUpdate.CHAT_BACKGROUND_SET")
|
||||
"""Messages that contain :attr:`telegram.Message.chat_background_set`."""
|
||||
|
||||
class _ChatCreated(MessageFilter):
|
||||
__slots__ = ()
|
||||
|
||||
|
|
|
@ -318,10 +318,12 @@ class BaseRequest(
|
|||
and isinstance(write_timeout, DefaultValue)
|
||||
):
|
||||
warn(
|
||||
f"The `write_timeout` parameter passed to {self.__class__.__name__}.do_request "
|
||||
"will default to `BaseRequest.DEFAULT_NONE` instead of 20 in future versions "
|
||||
"for *all* methods of the `Bot` class, including methods sending media.",
|
||||
PTBDeprecationWarning,
|
||||
PTBDeprecationWarning(
|
||||
"20.7",
|
||||
f"The `write_timeout` parameter passed to {self.__class__.__name__}.do_request"
|
||||
" will default to `BaseRequest.DEFAULT_NONE` instead of 20 in future versions "
|
||||
"for *all* methods of the `Bot` class, including methods sending media.",
|
||||
),
|
||||
stacklevel=3,
|
||||
)
|
||||
write_timeout = 20
|
||||
|
|
|
@ -146,9 +146,9 @@ class HTTPXRequest(BaseRequest):
|
|||
if proxy_url is not None:
|
||||
proxy = proxy_url
|
||||
warn(
|
||||
"The parameter `proxy_url` is deprecated since version 20.7. Use `proxy` "
|
||||
"instead.",
|
||||
PTBDeprecationWarning,
|
||||
PTBDeprecationWarning(
|
||||
"20.7", "The parameter `proxy_url` is deprecated. Use `proxy` instead."
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
|
|
@ -54,6 +54,34 @@ class PTBDeprecationWarning(PTBUserWarning, DeprecationWarning):
|
|||
|
||||
.. versionchanged:: 20.0
|
||||
Renamed TelegramDeprecationWarning to PTBDeprecationWarning.
|
||||
|
||||
Args:
|
||||
version (:obj:`str`): The version in which the feature was deprecated.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
message (:obj:`str`): The message to display.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
|
||||
Attributes:
|
||||
version (:obj:`str`): The version in which the feature was deprecated.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
message (:obj:`str`): The message to display.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
__slots__ = ("message", "version")
|
||||
|
||||
def __init__(self, version: str, message: str) -> None:
|
||||
self.version: str = version
|
||||
self.message: str = message
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Returns a string representation of the warning, using :attr:`message` and
|
||||
:attr:`version`.
|
||||
|
||||
.. versionadded:: 21.2
|
||||
"""
|
||||
return f"Deprecated since version {self.version}: {self.message}"
|
||||
|
|
|
@ -124,7 +124,8 @@ class TestLocationWithoutRequest(TestLocationBase):
|
|||
ha = data["horizontal_accuracy"] == "50"
|
||||
heading = data["heading"] == "90"
|
||||
prox_alert = data["proximity_alert_radius"] == "1000"
|
||||
return lat and lon and id_ and ha and heading and prox_alert
|
||||
live = data["live_period"] == "900"
|
||||
return lat and lon and id_ and ha and heading and prox_alert and live
|
||||
|
||||
monkeypatch.setattr(bot.request, "post", make_assertion)
|
||||
assert await bot.edit_message_live_location(
|
||||
|
@ -133,6 +134,7 @@ class TestLocationWithoutRequest(TestLocationBase):
|
|||
horizontal_accuracy=50,
|
||||
heading=90,
|
||||
proximity_alert_radius=1000,
|
||||
live_period=900,
|
||||
)
|
||||
|
||||
# TODO: Needs improvement with in inline sent live location.
|
||||
|
@ -262,6 +264,7 @@ class TestLocationWithRequest:
|
|||
horizontal_accuracy=30,
|
||||
heading=10,
|
||||
proximity_alert_radius=500,
|
||||
live_period=200,
|
||||
)
|
||||
|
||||
assert pytest.approx(message2.location.latitude, rel=1e-5) == 52.223098
|
||||
|
@ -269,6 +272,7 @@ class TestLocationWithRequest:
|
|||
assert message2.location.horizontal_accuracy == 30
|
||||
assert message2.location.heading == 10
|
||||
assert message2.location.proximity_alert_radius == 500
|
||||
assert message2.location.live_period == 200
|
||||
|
||||
await bot.stop_message_live_location(message.chat_id, message.message_id)
|
||||
with pytest.raises(BadRequest, match="Message can't be edited"):
|
||||
|
|
|
@ -314,6 +314,8 @@ def build_kwargs(
|
|||
elif name == "ok":
|
||||
kws["ok"] = False
|
||||
kws["error_message"] = "error"
|
||||
elif name == "options":
|
||||
kws[name] = ["option1", "option2"]
|
||||
else:
|
||||
kws[name] = True
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"""The integration of persistence into the application is tested in test_basepersistence.
|
||||
"""
|
||||
import asyncio
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
|
@ -2083,75 +2084,174 @@ class TestApplication:
|
|||
assert set(self.received.keys()) == set(expected.keys())
|
||||
assert self.received == expected
|
||||
|
||||
@pytest.mark.skipif(
|
||||
platform.system() == "Windows",
|
||||
reason="Can't send signals without stopping whole process on windows",
|
||||
)
|
||||
async def test_cancellation_error_does_not_stop_polling(
|
||||
self, one_time_bot, monkeypatch, caplog
|
||||
@pytest.mark.parametrize("exception", [SystemExit, KeyboardInterrupt])
|
||||
def test_raise_system_exit_keyboard_interrupt_post_init(
|
||||
self, one_time_bot, monkeypatch, exception
|
||||
):
|
||||
"""
|
||||
Ensures that hitting CTRL+C while polling *without* run_polling doesn't kill
|
||||
the update_fetcher loop such that a shutdown is still possible.
|
||||
This test is far from perfect, but it's the closest we can come with sane effort.
|
||||
"""
|
||||
async def post_init(application):
|
||||
raise exception
|
||||
|
||||
called_callbacks = set()
|
||||
|
||||
async def callback(*args, **kwargs):
|
||||
called_callbacks.add(kwargs["name"])
|
||||
|
||||
for cls, method, entry in [
|
||||
(Application, "initialize", "app_initialize"),
|
||||
(Application, "start", "app_start"),
|
||||
(Application, "stop", "app_stop"),
|
||||
(Application, "shutdown", "app_shutdown"),
|
||||
(Updater, "initialize", "updater_initialize"),
|
||||
(Updater, "shutdown", "updater_shutdown"),
|
||||
(Updater, "stop", "updater_stop"),
|
||||
(Updater, "start_polling", "updater_start_polling"),
|
||||
]:
|
||||
|
||||
def after(_, name):
|
||||
called_callbacks.add(name)
|
||||
|
||||
monkeypatch.setattr(
|
||||
cls,
|
||||
method,
|
||||
call_after(getattr(cls, method), functools.partial(after, name=entry)),
|
||||
)
|
||||
|
||||
app = (
|
||||
ApplicationBuilder()
|
||||
.bot(one_time_bot)
|
||||
.post_init(post_init)
|
||||
.post_stop(functools.partial(callback, name="post_stop"))
|
||||
.post_shutdown(functools.partial(callback, name="post_shutdown"))
|
||||
.build()
|
||||
)
|
||||
|
||||
app.run_polling(close_loop=False)
|
||||
|
||||
# This checks two things:
|
||||
# 1. start/stop are *not* called!
|
||||
# 2. we do have a graceful shutdown
|
||||
assert called_callbacks == {
|
||||
"app_initialize",
|
||||
"updater_initialize",
|
||||
"app_shutdown",
|
||||
"post_shutdown",
|
||||
"updater_shutdown",
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize("exception", [SystemExit("PTBTest"), KeyboardInterrupt("PTBTest")])
|
||||
@pytest.mark.parametrize("kind", ["handler", "error_handler", "job"])
|
||||
# @pytest.mark.parametrize("block", [True, False])
|
||||
# Testing with block=False would be nice but that doesn't work well with pytest for some reason
|
||||
# in any case, block=False is the simpler behavior since it is roughly similar to what happens
|
||||
# when you hit CTRL+C in the commandline.
|
||||
def test_raise_system_exit_keyboard_jobs_handlers(
|
||||
self, one_time_bot, monkeypatch, exception, kind, caplog
|
||||
):
|
||||
async def queue_and_raise(application):
|
||||
await application.update_queue.put("will_not_be_processed")
|
||||
raise exception
|
||||
|
||||
async def handler_callback(update, context):
|
||||
if kind == "handler":
|
||||
await queue_and_raise(context.application)
|
||||
elif kind == "error_handler":
|
||||
raise TelegramError("Triggering error callback")
|
||||
|
||||
async def error_callback(update, context):
|
||||
await queue_and_raise(context.application)
|
||||
|
||||
async def job_callback(context):
|
||||
await queue_and_raise(context.application)
|
||||
|
||||
async def enqueue_update():
|
||||
await asyncio.sleep(0.5)
|
||||
await app.update_queue.put(1)
|
||||
|
||||
async def post_init(application):
|
||||
if kind == "job":
|
||||
application.job_queue.run_once(when=0.5, callback=job_callback)
|
||||
else:
|
||||
app.create_task(enqueue_update())
|
||||
|
||||
async def update_logger_callback(update, context):
|
||||
context.bot_data.setdefault("processed_updates", set()).add(update)
|
||||
|
||||
called_callbacks = set()
|
||||
|
||||
async def callback(*args, **kwargs):
|
||||
called_callbacks.add(kwargs["name"])
|
||||
|
||||
async def get_updates(*args, **kwargs):
|
||||
await asyncio.sleep(0)
|
||||
return [None]
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(one_time_bot, "get_updates", get_updates)
|
||||
app = ApplicationBuilder().bot(one_time_bot).build()
|
||||
for cls, method, entry in [
|
||||
(Application, "initialize", "app_initialize"),
|
||||
(Application, "start", "app_start"),
|
||||
(Application, "stop", "app_stop"),
|
||||
(Application, "shutdown", "app_shutdown"),
|
||||
(Updater, "initialize", "updater_initialize"),
|
||||
(Updater, "shutdown", "updater_shutdown"),
|
||||
(Updater, "stop", "updater_stop"),
|
||||
(Updater, "start_polling", "updater_start_polling"),
|
||||
]:
|
||||
|
||||
original_get = app.update_queue.get
|
||||
raise_cancelled_error = threading.Event()
|
||||
def after(_, name):
|
||||
called_callbacks.add(name)
|
||||
|
||||
async def get(*arg, **kwargs):
|
||||
await asyncio.sleep(0.05)
|
||||
if raise_cancelled_error.is_set():
|
||||
raise_cancelled_error.clear()
|
||||
raise asyncio.CancelledError("Mocked CancelledError")
|
||||
return await original_get(*arg, **kwargs)
|
||||
monkeypatch.setattr(
|
||||
cls,
|
||||
method,
|
||||
call_after(getattr(cls, method), functools.partial(after, name=entry)),
|
||||
)
|
||||
|
||||
monkeypatch.setattr(app.update_queue, "get", get)
|
||||
|
||||
def thread_target():
|
||||
waited = 0
|
||||
while not app.running:
|
||||
time.sleep(0.05)
|
||||
waited += 0.05
|
||||
if waited > 5:
|
||||
pytest.fail("App apparently won't start")
|
||||
|
||||
time.sleep(0.1)
|
||||
raise_cancelled_error.set()
|
||||
|
||||
async with app:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
thread = Thread(target=thread_target)
|
||||
await app.start()
|
||||
thread.start()
|
||||
assert thread.is_alive()
|
||||
raise_cancelled_error.wait()
|
||||
|
||||
# The exit should have been caught and the app should still be running
|
||||
assert not thread.is_alive()
|
||||
assert app.running
|
||||
|
||||
# Explicit shutdown is required
|
||||
await app.stop()
|
||||
thread.join()
|
||||
|
||||
assert not thread.is_alive()
|
||||
assert not app.running
|
||||
|
||||
# Make sure that we were warned about the necessity of a manual shutdown
|
||||
assert len(caplog.records) == 1
|
||||
record = caplog.records[0]
|
||||
assert record.name == "telegram.ext.Application"
|
||||
assert record.getMessage().startswith(
|
||||
"Fetching updates got a asyncio.CancelledError. Ignoring"
|
||||
app = (
|
||||
ApplicationBuilder()
|
||||
.bot(one_time_bot)
|
||||
.post_init(post_init)
|
||||
.post_stop(functools.partial(callback, name="post_stop"))
|
||||
.post_shutdown(functools.partial(callback, name="post_shutdown"))
|
||||
.build()
|
||||
)
|
||||
monkeypatch.setattr(app.bot, "get_updates", get_updates)
|
||||
|
||||
app.add_handler(TypeHandler(object, update_logger_callback), group=-10)
|
||||
app.add_handler(TypeHandler(object, handler_callback))
|
||||
app.add_error_handler(error_callback)
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
app.run_polling(close_loop=False)
|
||||
|
||||
# This checks that we have a clean shutdown even when the user raises SystemExit
|
||||
# or KeyboardInterrupt in a handler/error handler/job callback
|
||||
assert called_callbacks == {
|
||||
"app_initialize",
|
||||
"app_shutdown",
|
||||
"app_start",
|
||||
"app_stop",
|
||||
"post_shutdown",
|
||||
"post_stop",
|
||||
"updater_initialize",
|
||||
"updater_shutdown",
|
||||
"updater_start_polling",
|
||||
"updater_stop",
|
||||
}
|
||||
|
||||
# These next checks make sure that the update queue is properly cleaned even if there are
|
||||
# still pending updates in the queue
|
||||
# Unfortunately this is apparently extremely hard to get right with jobs, so we're
|
||||
# skipping that case for the sake of simplicity
|
||||
if kind == "job":
|
||||
return
|
||||
|
||||
found = False
|
||||
for record in caplog.records:
|
||||
if record.getMessage() != "Dropping pending update: will_not_be_processed":
|
||||
continue
|
||||
assert record.name == "telegram.ext.Application"
|
||||
assert record.levelno == logging.DEBUG
|
||||
found = True
|
||||
assert found, "`Dropping pending updates` message not found in logs!"
|
||||
assert "will_not_be_processed" not in app.bot_data.get("processed_updates", set())
|
||||
|
||||
def test_run_without_updater(self, one_time_bot):
|
||||
app = ApplicationBuilder().bot(one_time_bot).updater(None).build()
|
||||
|
@ -2311,7 +2411,44 @@ class TestApplication:
|
|||
|
||||
assert len(caplog.records) == 1
|
||||
assert caplog.records[-1].name == "telegram.ext.Application"
|
||||
assert caplog.records[-1].getMessage().endswith("stop_running() does nothing.")
|
||||
assert caplog.records[-1].getMessage().endswith("`stop_running()` likely has no effect.")
|
||||
|
||||
def test_stop_running_post_init(self, app, monkeypatch, caplog, one_time_bot):
|
||||
async def post_init(app):
|
||||
app.stop_running()
|
||||
|
||||
called_callbacks = []
|
||||
|
||||
async def callback(*args, **kwargs):
|
||||
called_callbacks.append(kwargs["name"])
|
||||
|
||||
monkeypatch.setattr(Application, "start", functools.partial(callback, name="start"))
|
||||
monkeypatch.setattr(
|
||||
Updater, "start_polling", functools.partial(callback, name="start_polling")
|
||||
)
|
||||
|
||||
app = (
|
||||
ApplicationBuilder()
|
||||
.bot(one_time_bot)
|
||||
.post_init(post_init)
|
||||
.post_stop(functools.partial(callback, name="post_stop"))
|
||||
.post_shutdown(functools.partial(callback, name="post_shutdown"))
|
||||
.build()
|
||||
)
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
app.run_polling(close_loop=False)
|
||||
|
||||
# The important part here is that start(_polling) are *not* called!
|
||||
# post_stop must not be called either, since we never called stop()
|
||||
assert called_callbacks == ["post_shutdown"]
|
||||
|
||||
assert len(caplog.records) == 1
|
||||
assert caplog.records[-1].name == "telegram.ext.Application"
|
||||
assert (
|
||||
"Application received stop signal via `stop_running`"
|
||||
in caplog.records[-1].getMessage()
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("method", ["polling", "webhook"])
|
||||
def test_stop_running(self, one_time_bot, monkeypatch, method):
|
||||
|
@ -2421,3 +2558,83 @@ class TestApplication:
|
|||
assert len(assertions) == 5
|
||||
for key, value in assertions.items():
|
||||
assert value, f"assertion '{key}' failed!"
|
||||
|
||||
async def test_process_update_exception_in_building_context(self, monkeypatch, caplog, app):
|
||||
# Makes sure that exceptions in building the context don't stop the application
|
||||
exception = ValueError("TestException")
|
||||
original_from_update = CallbackContext.from_update
|
||||
|
||||
def raise_exception(update, application):
|
||||
if update == 1:
|
||||
raise exception
|
||||
return original_from_update(update, application)
|
||||
|
||||
monkeypatch.setattr(CallbackContext, "from_update", raise_exception)
|
||||
|
||||
received_updates = set()
|
||||
|
||||
async def callback(update, context):
|
||||
received_updates.add(update)
|
||||
|
||||
app.add_handler(TypeHandler(int, callback))
|
||||
|
||||
async with app:
|
||||
with caplog.at_level(logging.CRITICAL):
|
||||
await app.process_update(1)
|
||||
|
||||
assert received_updates == set()
|
||||
assert len(caplog.records) == 1
|
||||
record = caplog.records[0]
|
||||
assert record.name == "telegram.ext.Application"
|
||||
assert record.getMessage().startswith(
|
||||
"Error while building CallbackContext for update 1"
|
||||
)
|
||||
assert record.levelno == logging.CRITICAL
|
||||
|
||||
# Let's also check that no critical log is produced when the exception is not raised
|
||||
caplog.clear()
|
||||
with caplog.at_level(logging.CRITICAL):
|
||||
await app.process_update(2)
|
||||
|
||||
assert received_updates == {2}
|
||||
assert len(caplog.records) == 0
|
||||
|
||||
async def test_process_error_exception_in_building_context(self, monkeypatch, caplog, app):
|
||||
# Makes sure that exceptions in building the context don't stop the application
|
||||
exception = ValueError("TestException")
|
||||
original_from_error = CallbackContext.from_error
|
||||
|
||||
def raise_exception(update, error, application, *args, **kwargs):
|
||||
if error == 1:
|
||||
raise exception
|
||||
return original_from_error(update, error, application, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(CallbackContext, "from_error", raise_exception)
|
||||
|
||||
received_errors = set()
|
||||
|
||||
async def callback(update, context):
|
||||
received_errors.add(context.error)
|
||||
|
||||
app.add_error_handler(callback)
|
||||
|
||||
async with app:
|
||||
with caplog.at_level(logging.CRITICAL):
|
||||
await app.process_error(update=None, error=1)
|
||||
|
||||
assert received_errors == set()
|
||||
assert len(caplog.records) == 1
|
||||
record = caplog.records[0]
|
||||
assert record.name == "telegram.ext.Application"
|
||||
assert record.getMessage().startswith(
|
||||
"Error while building CallbackContext for exception 1"
|
||||
)
|
||||
assert record.levelno == logging.CRITICAL
|
||||
|
||||
# Let's also check that no critical log is produced when the exception is not raised
|
||||
caplog.clear()
|
||||
with caplog.at_level(logging.CRITICAL):
|
||||
await app.process_error(update=None, error=2)
|
||||
|
||||
assert received_errors == {2}
|
||||
assert len(caplog.records) == 0
|
||||
|
|
|
@ -1090,6 +1090,11 @@ class TestFilters:
|
|||
assert filters.StatusUpdate.GIVEAWAY_COMPLETED.check_update(update)
|
||||
update.message.giveaway_completed = None
|
||||
|
||||
update.message.chat_background_set = "test_background"
|
||||
assert filters.StatusUpdate.ALL.check_update(update)
|
||||
assert filters.StatusUpdate.CHAT_BACKGROUND_SET.check_update(update)
|
||||
update.message.chat_background_set = None
|
||||
|
||||
def test_filters_forwarded(self, update, message_origin_user):
|
||||
assert filters.FORWARDED.check_update(update)
|
||||
update.message.forward_origin = MessageOriginHiddenUser(datetime.datetime.utcnow(), 1)
|
||||
|
|
|
@ -646,3 +646,44 @@ class TestJobQueue:
|
|||
tg_job = Job.from_aps_job(aps_job)
|
||||
assert tg_job is job
|
||||
assert tg_job.job is aps_job
|
||||
|
||||
async def test_run_job_exception_in_building_context(
|
||||
self, monkeypatch, job_queue, caplog, app
|
||||
):
|
||||
# Makes sure that exceptions in building the context don't stop the application
|
||||
exception = ValueError("TestException")
|
||||
original_from_job = CallbackContext.from_job
|
||||
|
||||
def raise_exception(job, application):
|
||||
if job.data == 1:
|
||||
raise exception
|
||||
return original_from_job(job, application)
|
||||
|
||||
monkeypatch.setattr(CallbackContext, "from_job", raise_exception)
|
||||
|
||||
received_jobs = set()
|
||||
|
||||
async def job_callback(context):
|
||||
received_jobs.add(context.job.data)
|
||||
|
||||
with caplog.at_level(logging.CRITICAL):
|
||||
job_queue.run_once(job_callback, 0.1, data=1)
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
assert received_jobs == set()
|
||||
assert len(caplog.records) == 1
|
||||
record = caplog.records[0]
|
||||
assert record.name == "telegram.ext.JobQueue"
|
||||
assert record.getMessage().startswith(
|
||||
"Error while building CallbackContext for job job_callback"
|
||||
)
|
||||
assert record.levelno == logging.CRITICAL
|
||||
|
||||
# Let's also check that no critical log is produced when the exception is not raised
|
||||
caplog.clear()
|
||||
with caplog.at_level(logging.CRITICAL):
|
||||
job_queue.run_once(job_callback, 0.1, data=2)
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
assert received_jobs == {2}
|
||||
assert len(caplog.records) == 0
|
||||
|
|
|
@ -43,6 +43,7 @@ from telegram import (
|
|||
CallbackQuery,
|
||||
Chat,
|
||||
ChatAdministratorRights,
|
||||
ChatFullInfo,
|
||||
ChatPermissions,
|
||||
Dice,
|
||||
InlineKeyboardButton,
|
||||
|
@ -55,6 +56,7 @@ from telegram import (
|
|||
InputMediaDocument,
|
||||
InputMediaPhoto,
|
||||
InputMessageContent,
|
||||
InputPollOption,
|
||||
InputTextMessageContent,
|
||||
LabeledPrice,
|
||||
LinkPreviewOptions,
|
||||
|
@ -1936,6 +1938,59 @@ class TestBotWithoutRequest:
|
|||
chat_id, message, reply_parameters=ReplyParameters(**kwargs)
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("default_bot", "custom"),
|
||||
[
|
||||
({"parse_mode": ParseMode.HTML}, "NOTHING"),
|
||||
({"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_poll_default_text_question_parse_mode(
|
||||
self, default_bot, raw_bot, chat_id, custom, monkeypatch
|
||||
):
|
||||
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
|
||||
expected = default_bot.defaults.text_parse_mode if custom == "NOTHING" else custom
|
||||
|
||||
option_1 = request_data.parameters["options"][0]
|
||||
option_2 = request_data.parameters["options"][1]
|
||||
assert option_1.get("text_parse_mode") == (default_bot.defaults.text_parse_mode)
|
||||
assert option_2.get("text_parse_mode") == expected
|
||||
assert request_data.parameters.get("question_parse_mode") == expected
|
||||
|
||||
return make_message("dummy reply").to_dict()
|
||||
|
||||
async def make_raw_assertion(url, request_data: RequestData, *args, **kwargs):
|
||||
expected = None if custom == "NOTHING" else custom
|
||||
|
||||
option_1 = request_data.parameters["options"][0]
|
||||
option_2 = request_data.parameters["options"][1]
|
||||
assert option_1.get("text_parse_mode") is None
|
||||
assert option_2.get("text_parse_mode") == expected
|
||||
|
||||
assert request_data.parameters.get("question_parse_mode") == expected
|
||||
|
||||
return make_message("dummy reply").to_dict()
|
||||
|
||||
if custom == "NOTHING":
|
||||
option_2 = InputPollOption("option2")
|
||||
kwargs = {}
|
||||
else:
|
||||
option_2 = InputPollOption("option2", text_parse_mode=custom)
|
||||
kwargs = {"question_parse_mode": custom}
|
||||
|
||||
monkeypatch.setattr(default_bot.request, "post", make_assertion)
|
||||
await default_bot.send_poll(
|
||||
chat_id, question="question", options=["option1", option_2], **kwargs
|
||||
)
|
||||
|
||||
monkeypatch.setattr(raw_bot.request, "post", make_raw_assertion)
|
||||
await raw_bot.send_poll(
|
||||
chat_id, question="question", options=["option1", option_2], **kwargs
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("default_bot", "custom"),
|
||||
[
|
||||
|
@ -1966,6 +2021,30 @@ class TestBotWithoutRequest:
|
|||
reply_parameters=ReplyParameters(**kwargs),
|
||||
)
|
||||
|
||||
async def test_send_poll_question_parse_mode_entities(self, bot, monkeypatch):
|
||||
# Currently only custom emoji are supported as entities which we can't test
|
||||
# We just test that the correct data is passed for now
|
||||
|
||||
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
|
||||
assert request_data.parameters["question_entities"] == [
|
||||
{"type": "custom_emoji", "offset": 0, "length": 1},
|
||||
{"type": "custom_emoji", "offset": 2, "length": 1},
|
||||
]
|
||||
assert request_data.parameters["question_parse_mode"] == ParseMode.MARKDOWN_V2
|
||||
return make_message("dummy reply").to_dict()
|
||||
|
||||
monkeypatch.setattr(bot.request, "post", make_assertion)
|
||||
await bot.send_poll(
|
||||
1,
|
||||
question="😀😃",
|
||||
options=["option1", "option2"],
|
||||
question_entities=[
|
||||
MessageEntity(MessageEntity.CUSTOM_EMOJI, 0, 1),
|
||||
MessageEntity(MessageEntity.CUSTOM_EMOJI, 2, 1),
|
||||
],
|
||||
question_parse_mode=ParseMode.MARKDOWN_V2,
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("default_bot", "custom"),
|
||||
[
|
||||
|
@ -2025,6 +2104,7 @@ class TestBotWithoutRequest:
|
|||
monkeypatch.setattr(bot.request, "post", make_assertion)
|
||||
assert await bot.do_api_request("camel_case")
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning")
|
||||
async def test_do_api_request_media_write_timeout(self, bot, chat_id, monkeypatch):
|
||||
test_flag = None
|
||||
|
||||
|
@ -2063,6 +2143,7 @@ class TestBotWithoutRequest:
|
|||
DEFAULT_NONE,
|
||||
)
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning")
|
||||
async def test_do_api_request_default_timezone(self, tz_bot, monkeypatch):
|
||||
until = dtm.datetime(2020, 1, 11, 16, 13)
|
||||
until_timestamp = to_timestamp(until, tzinfo=tz_bot.defaults.tzinfo)
|
||||
|
@ -2324,7 +2405,7 @@ class TestBotWithRequest:
|
|||
)
|
||||
async def test_send_and_stop_poll(self, bot, super_group_id, reply_markup):
|
||||
question = "Is this a test?"
|
||||
answers = ["Yes", "No", "Maybe"]
|
||||
answers = ["Yes", InputPollOption("No"), "Maybe"]
|
||||
explanation = "[Here is a link](https://google.com)"
|
||||
explanation_entities = [
|
||||
MessageEntity(MessageEntity.TEXT_LINK, 0, 14, url="https://google.com")
|
||||
|
@ -2358,7 +2439,7 @@ class TestBotWithRequest:
|
|||
assert message.poll
|
||||
assert message.poll.question == question
|
||||
assert message.poll.options[0].text == answers[0]
|
||||
assert message.poll.options[1].text == answers[1]
|
||||
assert message.poll.options[1].text == answers[1].text
|
||||
assert message.poll.options[2].text == answers[2]
|
||||
assert not message.poll.is_anonymous
|
||||
assert message.poll.allows_multiple_answers
|
||||
|
@ -2378,7 +2459,7 @@ class TestBotWithRequest:
|
|||
assert poll.is_closed
|
||||
assert poll.options[0].text == answers[0]
|
||||
assert poll.options[0].voter_count == 0
|
||||
assert poll.options[1].text == answers[1]
|
||||
assert poll.options[1].text == answers[1].text
|
||||
assert poll.options[1].voter_count == 0
|
||||
assert poll.options[2].text == answers[2]
|
||||
assert poll.options[2].voter_count == 0
|
||||
|
@ -2921,10 +3002,10 @@ class TestBotWithRequest:
|
|||
await bot.leave_chat(-123456)
|
||||
|
||||
async def test_get_chat(self, bot, super_group_id):
|
||||
chat = await bot.get_chat(super_group_id)
|
||||
assert chat.type == "supergroup"
|
||||
assert chat.title == f">>> telegram.Bot(test) @{bot.username}"
|
||||
assert chat.id == int(super_group_id)
|
||||
cfi = await bot.get_chat(super_group_id)
|
||||
assert cfi.type == "supergroup"
|
||||
assert cfi.title == f">>> telegram.Bot(test) @{bot.username}"
|
||||
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)
|
||||
|
@ -3916,9 +3997,9 @@ class TestBotWithRequest:
|
|||
)
|
||||
assert data == "callback_data"
|
||||
|
||||
chat = await bot.get_chat(channel_id)
|
||||
assert chat.pinned_message == message
|
||||
assert chat.pinned_message.reply_markup == reply_markup
|
||||
cfi = await bot.get_chat(channel_id)
|
||||
assert cfi.pinned_message == message
|
||||
assert cfi.pinned_message.reply_markup == reply_markup
|
||||
assert await message.unpin() # (not placed in finally block since msg can be unbound)
|
||||
finally:
|
||||
bot.callback_data_cache.clear_callback_data()
|
||||
|
@ -3931,11 +4012,11 @@ class TestBotWithRequest:
|
|||
await bot.unpin_all_chat_messages(super_group_id)
|
||||
|
||||
try:
|
||||
chat = await bot.get_chat(super_group_id)
|
||||
cfi = await bot.get_chat(super_group_id)
|
||||
|
||||
assert isinstance(chat, Chat)
|
||||
assert int(chat.id) == int(super_group_id)
|
||||
assert chat.pinned_message is None
|
||||
assert isinstance(cfi, ChatFullInfo)
|
||||
assert int(cfi.id) == int(super_group_id)
|
||||
assert cfi.pinned_message is None
|
||||
finally:
|
||||
bot.callback_data_cache.clear_callback_data()
|
||||
bot.callback_data_cache.clear_callback_queries()
|
||||
|
@ -4027,7 +4108,7 @@ class TestBotWithRequest:
|
|||
|
||||
@pytest.mark.parametrize("bot_class", [Bot, ExtBot])
|
||||
async def test_do_api_request_warning_known_method(self, bot, bot_class):
|
||||
with pytest.warns(PTBDeprecationWarning, match="Please use 'Bot.get_me'") as record:
|
||||
with pytest.warns(PTBUserWarning, match="Please use 'Bot.get_me'") as record:
|
||||
await bot_class(bot.token).do_api_request("get_me")
|
||||
|
||||
assert record[0].filename == __file__, "Wrong stack level!"
|
||||
|
@ -4036,6 +4117,7 @@ class TestBotWithRequest:
|
|||
with pytest.raises(EndPointNotFound, match="'unknownEndpoint' not found"):
|
||||
await bot.do_api_request("unknown_endpoint")
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning")
|
||||
async def test_do_api_request_invalid_token(self, bot):
|
||||
# we do not initialize the bot here on purpose b/c that's the case were we actually
|
||||
# do not know for sure if the token is invalid or the method was not found
|
||||
|
@ -4050,6 +4132,7 @@ class TestBotWithRequest:
|
|||
):
|
||||
await Bot(bot.token).do_api_request("unknown_endpoint")
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning")
|
||||
@pytest.mark.parametrize("return_type", [Message, None])
|
||||
async def test_do_api_request_basic_and_files(self, bot, chat_id, return_type):
|
||||
result = await bot.do_api_request(
|
||||
|
@ -4074,6 +4157,7 @@ class TestBotWithRequest:
|
|||
assert out.read() == data_file("telegram.png").open("rb").read()
|
||||
assert result.document.file_name == "telegram.png"
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning")
|
||||
@pytest.mark.parametrize("return_type", [Message, None])
|
||||
async def test_do_api_request_list_return_type(self, bot, chat_id, return_type):
|
||||
result = await bot.do_api_request(
|
||||
|
@ -4112,6 +4196,7 @@ class TestBotWithRequest:
|
|||
assert out.read() == data_file(file_name).open("rb").read()
|
||||
assert message.document.file_name == file_name
|
||||
|
||||
@pytest.mark.filterwarnings("ignore::telegram.warnings.PTBUserWarning")
|
||||
@pytest.mark.parametrize("return_type", [Message, None])
|
||||
async def test_do_api_request_bool_return_type(self, bot, chat_id, return_type):
|
||||
assert await bot.do_api_request("delete_my_commands", return_type=return_type) is True
|
||||
|
|
|
@ -301,8 +301,9 @@ class TestCallbackQueryWithoutRequest(TestCallbackQueryBase):
|
|||
async def make_assertion(*_, **kwargs):
|
||||
latitude = kwargs.get("latitude") == 1
|
||||
longitude = kwargs.get("longitude") == 2
|
||||
live = kwargs.get("live_period") == 900
|
||||
ids = self.check_passed_ids(callback_query, kwargs)
|
||||
return ids and latitude and longitude
|
||||
return ids and latitude and longitude and live
|
||||
|
||||
assert check_shortcut_signature(
|
||||
CallbackQuery.edit_message_live_location,
|
||||
|
@ -322,8 +323,10 @@ class TestCallbackQueryWithoutRequest(TestCallbackQueryBase):
|
|||
)
|
||||
|
||||
monkeypatch.setattr(callback_query.get_bot(), "edit_message_live_location", make_assertion)
|
||||
assert await callback_query.edit_message_live_location(latitude=1, longitude=2)
|
||||
assert await callback_query.edit_message_live_location(1, 2)
|
||||
assert await callback_query.edit_message_live_location(
|
||||
latitude=1, longitude=2, live_period=900
|
||||
)
|
||||
assert await callback_query.edit_message_live_location(1, 2, live_period=900)
|
||||
|
||||
async def test_stop_message_live_location(self, monkeypatch, callback_query):
|
||||
if isinstance(callback_query.message, InaccessibleMessage):
|
||||
|
|
|
@ -16,7 +16,9 @@
|
|||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
|
||||
import datetime
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -35,9 +37,11 @@ from telegram import (
|
|||
ReactionTypeEmoji,
|
||||
User,
|
||||
)
|
||||
from telegram._chat import _deprecated_attrs
|
||||
from telegram._utils.datetime import UTC, to_timestamp
|
||||
from telegram.constants import ChatAction, ChatType, ReactionEmoji
|
||||
from telegram.helpers import escape_markdown
|
||||
from telegram.warnings import PTBDeprecationWarning
|
||||
from tests.auxil.bot_method_checks import (
|
||||
check_defaults_handling,
|
||||
check_shortcut_call,
|
||||
|
@ -84,6 +88,8 @@ def chat(bot):
|
|||
business_opening_hours=TestChatBase.business_opening_hours,
|
||||
birthdate=Birthdate(1, 1),
|
||||
personal_chat=TestChatBase.personal_chat,
|
||||
first_name=TestChatBase.first_name,
|
||||
last_name=TestChatBase.last_name,
|
||||
)
|
||||
chat.set_bot(bot)
|
||||
chat._unfreeze()
|
||||
|
@ -137,6 +143,8 @@ class TestChatBase:
|
|||
custom_emoji_sticker_set_name = "custom_emoji_sticker_set_name"
|
||||
birthdate = Birthdate(1, 1)
|
||||
personal_chat = Chat(3, "private", "private")
|
||||
first_name = "first"
|
||||
last_name = "last"
|
||||
|
||||
|
||||
class TestChatWithoutRequest(TestChatBase):
|
||||
|
@ -185,6 +193,8 @@ class TestChatWithoutRequest(TestChatBase):
|
|||
"custom_emoji_sticker_set_name": self.custom_emoji_sticker_set_name,
|
||||
"birthdate": self.birthdate.to_dict(),
|
||||
"personal_chat": self.personal_chat.to_dict(),
|
||||
"first_name": self.first_name,
|
||||
"last_name": self.last_name,
|
||||
}
|
||||
chat = Chat.de_json(json_dict, bot)
|
||||
|
||||
|
@ -230,6 +240,8 @@ class TestChatWithoutRequest(TestChatBase):
|
|||
assert chat.custom_emoji_sticker_set_name == self.custom_emoji_sticker_set_name
|
||||
assert chat.birthdate == self.birthdate
|
||||
assert chat.personal_chat == self.personal_chat
|
||||
assert chat.first_name == self.first_name
|
||||
assert chat.last_name == self.last_name
|
||||
|
||||
def test_de_json_localization(self, bot, raw_bot, tz_bot):
|
||||
json_dict = {
|
||||
|
@ -251,6 +263,15 @@ class TestChatWithoutRequest(TestChatBase):
|
|||
assert chat_bot_raw.emoji_status_expiration_date.tzinfo == UTC
|
||||
assert emoji_expire_offset_tz == emoji_expire_offset
|
||||
|
||||
def test_always_tuples_attributes(self):
|
||||
chat = Chat(
|
||||
id=123,
|
||||
title="title",
|
||||
type=Chat.PRIVATE,
|
||||
)
|
||||
assert isinstance(chat.active_usernames, tuple)
|
||||
assert chat.active_usernames == ()
|
||||
|
||||
def test_to_dict(self, chat):
|
||||
chat_dict = chat.to_dict()
|
||||
|
||||
|
@ -300,15 +321,25 @@ class TestChatWithoutRequest(TestChatBase):
|
|||
assert chat_dict["unrestrict_boost_count"] == chat.unrestrict_boost_count
|
||||
assert chat_dict["birthdate"] == chat.birthdate.to_dict()
|
||||
assert chat_dict["personal_chat"] == chat.personal_chat.to_dict()
|
||||
assert chat_dict["first_name"] == chat.first_name
|
||||
assert chat_dict["last_name"] == chat.last_name
|
||||
|
||||
def test_always_tuples_attributes(self):
|
||||
chat = Chat(
|
||||
id=123,
|
||||
title="title",
|
||||
type=Chat.PRIVATE,
|
||||
)
|
||||
assert isinstance(chat.active_usernames, tuple)
|
||||
assert chat.active_usernames == ()
|
||||
def test_deprecated_attributes(self, chat):
|
||||
for depr_attr in _deprecated_attrs:
|
||||
with pytest.warns(PTBDeprecationWarning, match="deprecated and will only be accessib"):
|
||||
getattr(chat, depr_attr)
|
||||
with warnings.catch_warnings(): # No warning should be raised
|
||||
warnings.simplefilter("error")
|
||||
chat.id
|
||||
chat.first_name
|
||||
|
||||
def test_deprecated_arguments(self):
|
||||
for depr_attr in _deprecated_attrs:
|
||||
with pytest.warns(PTBDeprecationWarning, match="deprecated and will only be availabl"):
|
||||
Chat(1, "type", **{depr_attr: "1"})
|
||||
with warnings.catch_warnings(): # No warning should be raised
|
||||
warnings.simplefilter("error")
|
||||
Chat(1, "type", first_name="first_name")
|
||||
|
||||
def test_enum_init(self):
|
||||
chat = Chat(id=1, type="foo")
|
||||
|
@ -348,10 +379,7 @@ class TestChatWithoutRequest(TestChatBase):
|
|||
assert chat.full_name == "first\u2022name last\u2022name"
|
||||
chat = Chat(id=1, type=Chat.PRIVATE, first_name="first\u2022name")
|
||||
assert chat.full_name == "first\u2022name"
|
||||
chat = Chat(
|
||||
id=1,
|
||||
type=Chat.PRIVATE,
|
||||
)
|
||||
chat = Chat(id=1, type=Chat.PRIVATE)
|
||||
assert chat.full_name is None
|
||||
|
||||
def test_effective_name(self):
|
||||
|
@ -588,7 +616,7 @@ class TestChatWithoutRequest(TestChatBase):
|
|||
async def test_set_permissions(self, monkeypatch, chat):
|
||||
async def make_assertion(*_, **kwargs):
|
||||
chat_id = kwargs["chat_id"] == chat.id
|
||||
permissions = kwargs["permissions"] == self.permissions
|
||||
permissions = kwargs["permissions"] == ChatPermissions.all_permissions()
|
||||
return chat_id and permissions
|
||||
|
||||
assert check_shortcut_signature(
|
||||
|
@ -600,7 +628,7 @@ class TestChatWithoutRequest(TestChatBase):
|
|||
assert await check_defaults_handling(chat.set_permissions, chat.get_bot())
|
||||
|
||||
monkeypatch.setattr(chat.get_bot(), "set_chat_permissions", make_assertion)
|
||||
assert await chat.set_permissions(permissions=self.permissions)
|
||||
assert await chat.set_permissions(permissions=ChatPermissions.all_permissions())
|
||||
|
||||
async def test_set_administrator_custom_title(self, monkeypatch, chat):
|
||||
async def make_assertion(*_, **kwargs):
|
||||
|
|
361
tests/test_chatbackground.py
Normal file
361
tests/test_chatbackground.py
Normal file
|
@ -0,0 +1,361 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
import inspect
|
||||
from copy import deepcopy
|
||||
from typing import Union
|
||||
|
||||
import pytest
|
||||
|
||||
from telegram import (
|
||||
BackgroundFill,
|
||||
BackgroundFillFreeformGradient,
|
||||
BackgroundFillGradient,
|
||||
BackgroundFillSolid,
|
||||
BackgroundType,
|
||||
BackgroundTypeChatTheme,
|
||||
BackgroundTypeFill,
|
||||
BackgroundTypePattern,
|
||||
BackgroundTypeWallpaper,
|
||||
Dice,
|
||||
Document,
|
||||
)
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
ignored = ["self", "api_kwargs"]
|
||||
|
||||
|
||||
class BFDefaults:
|
||||
color = 0
|
||||
top_color = 1
|
||||
bottom_color = 2
|
||||
rotation_angle = 45
|
||||
colors = [0, 1, 2]
|
||||
|
||||
|
||||
def background_fill_solid():
|
||||
return BackgroundFillSolid(BFDefaults.color)
|
||||
|
||||
|
||||
def background_fill_gradient():
|
||||
return BackgroundFillGradient(
|
||||
BFDefaults.top_color, BFDefaults.bottom_color, BFDefaults.rotation_angle
|
||||
)
|
||||
|
||||
|
||||
def background_fill_freeform_gradient():
|
||||
return BackgroundFillFreeformGradient(BFDefaults.colors)
|
||||
|
||||
|
||||
class BTDefaults:
|
||||
document = Document(1, 2)
|
||||
fill = BackgroundFillSolid(color=0)
|
||||
dark_theme_dimming = 20
|
||||
is_blurred = True
|
||||
is_moving = False
|
||||
intensity = 90
|
||||
is_inverted = False
|
||||
theme_name = "ice"
|
||||
|
||||
|
||||
def background_type_fill():
|
||||
return BackgroundTypeFill(BTDefaults.fill, BTDefaults.dark_theme_dimming)
|
||||
|
||||
|
||||
def background_type_wallpaper():
|
||||
return BackgroundTypeWallpaper(
|
||||
BTDefaults.document,
|
||||
BTDefaults.dark_theme_dimming,
|
||||
BTDefaults.is_blurred,
|
||||
BTDefaults.is_moving,
|
||||
)
|
||||
|
||||
|
||||
def background_type_pattern():
|
||||
return BackgroundTypePattern(
|
||||
BTDefaults.document,
|
||||
BTDefaults.fill,
|
||||
BTDefaults.intensity,
|
||||
BTDefaults.is_inverted,
|
||||
BTDefaults.is_moving,
|
||||
)
|
||||
|
||||
|
||||
def background_type_chat_theme():
|
||||
return BackgroundTypeChatTheme(BTDefaults.theme_name)
|
||||
|
||||
|
||||
def make_json_dict(
|
||||
instance: Union[BackgroundType, BackgroundFill], include_optional_args: bool = False
|
||||
) -> dict:
|
||||
"""Used to make the json dict which we use for testing de_json. Similar to iter_args()"""
|
||||
json_dict = {"type": instance.type}
|
||||
sig = inspect.signature(instance.__class__.__init__)
|
||||
|
||||
for param in sig.parameters.values():
|
||||
if param.name in ignored: # ignore irrelevant params
|
||||
continue
|
||||
|
||||
val = getattr(instance, param.name)
|
||||
# Compulsory args-
|
||||
if param.default is inspect.Parameter.empty:
|
||||
if hasattr(val, "to_dict"): # convert the user object or any future ones to dict.
|
||||
val = val.to_dict()
|
||||
json_dict[param.name] = val
|
||||
|
||||
# If we want to test all args (for de_json)-
|
||||
elif param.default is not inspect.Parameter.empty and include_optional_args:
|
||||
json_dict[param.name] = val
|
||||
return json_dict
|
||||
|
||||
|
||||
def iter_args(
|
||||
instance: Union[BackgroundType, BackgroundFill],
|
||||
de_json_inst: Union[BackgroundType, BackgroundFill],
|
||||
include_optional: bool = False,
|
||||
):
|
||||
"""
|
||||
We accept both the regular instance and de_json created instance and iterate over them for
|
||||
easy one line testing later one.
|
||||
"""
|
||||
yield instance.type, de_json_inst.type # yield this here cause it's not available in sig.
|
||||
|
||||
sig = inspect.signature(instance.__class__.__init__)
|
||||
for param in sig.parameters.values():
|
||||
if param.name in ignored:
|
||||
continue
|
||||
inst_at, json_at = getattr(instance, param.name), getattr(de_json_inst, param.name)
|
||||
if (
|
||||
param.default is not inspect.Parameter.empty and include_optional
|
||||
) or param.default is inspect.Parameter.empty:
|
||||
yield inst_at, json_at
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def background_type(request):
|
||||
return request.param()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"background_type",
|
||||
[
|
||||
background_type_fill,
|
||||
background_type_wallpaper,
|
||||
background_type_pattern,
|
||||
background_type_chat_theme,
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
class TestBackgroundTypeWithoutRequest:
|
||||
def test_slot_behaviour(self, background_type):
|
||||
inst = background_type
|
||||
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_required_args(self, bot, background_type):
|
||||
cls = background_type.__class__
|
||||
assert cls.de_json({}, bot) is None
|
||||
|
||||
json_dict = make_json_dict(background_type)
|
||||
const_background_type = BackgroundType.de_json(json_dict, bot)
|
||||
assert const_background_type.api_kwargs == {}
|
||||
|
||||
assert isinstance(const_background_type, BackgroundType)
|
||||
assert isinstance(const_background_type, cls)
|
||||
for bg_type_at, const_bg_type_at in iter_args(background_type, const_background_type):
|
||||
assert bg_type_at == const_bg_type_at
|
||||
|
||||
def test_de_json_all_args(self, bot, background_type):
|
||||
json_dict = make_json_dict(background_type, include_optional_args=True)
|
||||
const_background_type = BackgroundType.de_json(json_dict, bot)
|
||||
|
||||
assert const_background_type.api_kwargs == {}
|
||||
|
||||
assert isinstance(const_background_type, BackgroundType)
|
||||
assert isinstance(const_background_type, background_type.__class__)
|
||||
for bg_type_at, const_bg_type_at in iter_args(
|
||||
background_type, const_background_type, True
|
||||
):
|
||||
assert bg_type_at == const_bg_type_at
|
||||
|
||||
def test_de_json_invalid_type(self, background_type, bot):
|
||||
json_dict = {"type": "invalid", "theme_name": BTDefaults.theme_name}
|
||||
background_type = BackgroundType.de_json(json_dict, bot)
|
||||
|
||||
assert type(background_type) is BackgroundType
|
||||
assert background_type.type == "invalid"
|
||||
|
||||
def test_de_json_subclass(self, background_type, bot, chat_id):
|
||||
"""This makes sure that e.g. BackgroundTypeFill(data, bot) never returns a
|
||||
BackgroundTypeWallpaper instance."""
|
||||
cls = background_type.__class__
|
||||
json_dict = make_json_dict(background_type, True)
|
||||
assert type(cls.de_json(json_dict, bot)) is cls
|
||||
|
||||
def test_to_dict(self, background_type):
|
||||
bg_type_dict = background_type.to_dict()
|
||||
|
||||
assert isinstance(bg_type_dict, dict)
|
||||
assert bg_type_dict["type"] == background_type.type
|
||||
|
||||
for slot in background_type.__slots__: # additional verification for the optional args
|
||||
if slot in ("fill", "document"):
|
||||
assert (getattr(background_type, slot)).to_dict() == bg_type_dict[slot]
|
||||
continue
|
||||
assert getattr(background_type, slot) == bg_type_dict[slot]
|
||||
|
||||
def test_equality(self, background_type):
|
||||
a = BackgroundType(type="type")
|
||||
b = BackgroundType(type="type")
|
||||
c = background_type
|
||||
d = deepcopy(background_type)
|
||||
e = Dice(4, "emoji")
|
||||
sig = inspect.signature(background_type.__class__.__init__)
|
||||
params = [
|
||||
"random" for param in sig.parameters.values() if param.name not in [*ignored, "type"]
|
||||
]
|
||||
f = background_type.__class__(*params)
|
||||
|
||||
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)
|
||||
|
||||
assert c == d
|
||||
assert hash(c) == hash(d)
|
||||
|
||||
assert c != e
|
||||
assert hash(c) != hash(e)
|
||||
|
||||
assert f != c
|
||||
assert hash(f) != hash(c)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def background_fill(request):
|
||||
return request.param()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"background_fill",
|
||||
[
|
||||
background_fill_solid,
|
||||
background_fill_gradient,
|
||||
background_fill_freeform_gradient,
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
class TestBackgroundFillWithoutRequest:
|
||||
def test_slot_behaviour(self, background_fill):
|
||||
inst = background_fill
|
||||
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_required_args(self, bot, background_fill):
|
||||
cls = background_fill.__class__
|
||||
assert cls.de_json({}, bot) is None
|
||||
|
||||
json_dict = make_json_dict(background_fill)
|
||||
const_background_fill = BackgroundFill.de_json(json_dict, bot)
|
||||
assert const_background_fill.api_kwargs == {}
|
||||
|
||||
assert isinstance(const_background_fill, BackgroundFill)
|
||||
assert isinstance(const_background_fill, cls)
|
||||
for bg_fill_at, const_bg_fill_at in iter_args(background_fill, const_background_fill):
|
||||
assert bg_fill_at == const_bg_fill_at
|
||||
|
||||
def test_de_json_all_args(self, bot, background_fill):
|
||||
json_dict = make_json_dict(background_fill, include_optional_args=True)
|
||||
const_background_fill = BackgroundFill.de_json(json_dict, bot)
|
||||
|
||||
assert const_background_fill.api_kwargs == {}
|
||||
|
||||
assert isinstance(const_background_fill, BackgroundFill)
|
||||
assert isinstance(const_background_fill, background_fill.__class__)
|
||||
for bg_fill_at, const_bg_fill_at in iter_args(
|
||||
background_fill, const_background_fill, True
|
||||
):
|
||||
assert bg_fill_at == const_bg_fill_at
|
||||
|
||||
def test_de_json_invalid_type(self, background_fill, bot):
|
||||
json_dict = {"type": "invalid", "theme_name": BTDefaults.theme_name}
|
||||
background_fill = BackgroundFill.de_json(json_dict, bot)
|
||||
|
||||
assert type(background_fill) is BackgroundFill
|
||||
assert background_fill.type == "invalid"
|
||||
|
||||
def test_de_json_subclass(self, background_fill, bot):
|
||||
"""This makes sure that e.g. BackgroundFillSolid(data, bot) never returns a
|
||||
BackgroundFillGradient instance."""
|
||||
cls = background_fill.__class__
|
||||
json_dict = make_json_dict(background_fill, True)
|
||||
assert type(cls.de_json(json_dict, bot)) is cls
|
||||
|
||||
def test_to_dict(self, background_fill):
|
||||
bg_fill_dict = background_fill.to_dict()
|
||||
|
||||
assert isinstance(bg_fill_dict, dict)
|
||||
assert bg_fill_dict["type"] == background_fill.type
|
||||
|
||||
for slot in background_fill.__slots__: # additional verification for the optional args
|
||||
if slot == "colors":
|
||||
assert getattr(background_fill, slot) == tuple(bg_fill_dict[slot])
|
||||
continue
|
||||
assert getattr(background_fill, slot) == bg_fill_dict[slot]
|
||||
|
||||
def test_equality(self, background_fill):
|
||||
a = BackgroundFill(type="type")
|
||||
b = BackgroundFill(type="type")
|
||||
c = background_fill
|
||||
d = deepcopy(background_fill)
|
||||
e = Dice(4, "emoji")
|
||||
sig = inspect.signature(background_fill.__class__.__init__)
|
||||
params = [
|
||||
"random" for param in sig.parameters.values() if param.name not in [*ignored, "type"]
|
||||
]
|
||||
f = background_fill.__class__(*params)
|
||||
|
||||
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)
|
||||
|
||||
assert c == d
|
||||
assert hash(c) == hash(d)
|
||||
|
||||
assert c != e
|
||||
assert hash(c) != hash(e)
|
||||
|
||||
assert f != c
|
||||
assert hash(f) != hash(c)
|
209
tests/test_chatfullinfo.py
Normal file
209
tests/test_chatfullinfo.py
Normal file
|
@ -0,0 +1,209 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# A library that provides a Python interface to the Telegram Bot API
|
||||
# Copyright (C) 2015-2024
|
||||
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser Public License
|
||||
# along with this program. If not, see [http://www.gnu.org/licenses/].
|
||||
import datetime
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
from telegram import (
|
||||
Birthdate,
|
||||
BusinessIntro,
|
||||
BusinessLocation,
|
||||
BusinessOpeningHours,
|
||||
BusinessOpeningHoursInterval,
|
||||
Chat,
|
||||
ChatFullInfo,
|
||||
ChatLocation,
|
||||
ChatPermissions,
|
||||
Location,
|
||||
ReactionTypeCustomEmoji,
|
||||
ReactionTypeEmoji,
|
||||
)
|
||||
from telegram._chat import _deprecated_attrs
|
||||
from telegram._utils.datetime import UTC, to_timestamp
|
||||
from telegram.constants import ReactionEmoji
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def chat_full_info(bot):
|
||||
chat = ChatFullInfo(
|
||||
TestChatInfoBase.id_,
|
||||
type=TestChatInfoBase.type_,
|
||||
accent_color_id=TestChatInfoBase.accent_color_id,
|
||||
max_reaction_count=TestChatInfoBase.max_reaction_count,
|
||||
title=TestChatInfoBase.title,
|
||||
username=TestChatInfoBase.username,
|
||||
sticker_set_name=TestChatInfoBase.sticker_set_name,
|
||||
can_set_sticker_set=TestChatInfoBase.can_set_sticker_set,
|
||||
permissions=TestChatInfoBase.permissions,
|
||||
slow_mode_delay=TestChatInfoBase.slow_mode_delay,
|
||||
bio=TestChatInfoBase.bio,
|
||||
linked_chat_id=TestChatInfoBase.linked_chat_id,
|
||||
location=TestChatInfoBase.location,
|
||||
has_private_forwards=True,
|
||||
has_protected_content=True,
|
||||
has_visible_history=True,
|
||||
join_to_send_messages=True,
|
||||
join_by_request=True,
|
||||
has_restricted_voice_and_video_messages=True,
|
||||
is_forum=True,
|
||||
active_usernames=TestChatInfoBase.active_usernames,
|
||||
emoji_status_custom_emoji_id=TestChatInfoBase.emoji_status_custom_emoji_id,
|
||||
emoji_status_expiration_date=TestChatInfoBase.emoji_status_expiration_date,
|
||||
has_aggressive_anti_spam_enabled=TestChatInfoBase.has_aggressive_anti_spam_enabled,
|
||||
has_hidden_members=TestChatInfoBase.has_hidden_members,
|
||||
available_reactions=TestChatInfoBase.available_reactions,
|
||||
background_custom_emoji_id=TestChatInfoBase.background_custom_emoji_id,
|
||||
profile_accent_color_id=TestChatInfoBase.profile_accent_color_id,
|
||||
profile_background_custom_emoji_id=TestChatInfoBase.profile_background_custom_emoji_id,
|
||||
unrestrict_boost_count=TestChatInfoBase.unrestrict_boost_count,
|
||||
custom_emoji_sticker_set_name=TestChatInfoBase.custom_emoji_sticker_set_name,
|
||||
business_intro=TestChatInfoBase.business_intro,
|
||||
business_location=TestChatInfoBase.business_location,
|
||||
business_opening_hours=TestChatInfoBase.business_opening_hours,
|
||||
birthdate=Birthdate(1, 1),
|
||||
personal_chat=TestChatInfoBase.personal_chat,
|
||||
)
|
||||
chat.set_bot(bot)
|
||||
chat._unfreeze()
|
||||
return chat
|
||||
|
||||
|
||||
class TestChatInfoBase:
|
||||
id_ = -28767330
|
||||
max_reaction_count = 2
|
||||
title = "ToledosPalaceBot - Group"
|
||||
type_ = "group"
|
||||
username = "username"
|
||||
all_members_are_administrators = False
|
||||
sticker_set_name = "stickers"
|
||||
can_set_sticker_set = False
|
||||
permissions = ChatPermissions(
|
||||
can_send_messages=True,
|
||||
can_change_info=False,
|
||||
can_invite_users=True,
|
||||
)
|
||||
slow_mode_delay = 30
|
||||
bio = "I'm a Barbie Girl in a Barbie World"
|
||||
linked_chat_id = 11880
|
||||
location = ChatLocation(Location(123, 456), "Barbie World")
|
||||
has_protected_content = True
|
||||
has_visible_history = True
|
||||
has_private_forwards = True
|
||||
join_to_send_messages = True
|
||||
join_by_request = True
|
||||
has_restricted_voice_and_video_messages = True
|
||||
is_forum = True
|
||||
active_usernames = ["These", "Are", "Usernames!"]
|
||||
emoji_status_custom_emoji_id = "VeryUniqueCustomEmojiID"
|
||||
emoji_status_expiration_date = datetime.datetime.now(tz=UTC).replace(microsecond=0)
|
||||
has_aggressive_anti_spam_enabled = True
|
||||
has_hidden_members = True
|
||||
available_reactions = [
|
||||
ReactionTypeEmoji(ReactionEmoji.THUMBS_DOWN),
|
||||
ReactionTypeCustomEmoji("custom_emoji_id"),
|
||||
]
|
||||
business_intro = BusinessIntro("Title", "Description", None)
|
||||
business_location = BusinessLocation("Address", Location(123, 456))
|
||||
business_opening_hours = BusinessOpeningHours(
|
||||
"Country/City",
|
||||
[BusinessOpeningHoursInterval(opening, opening + 60) for opening in (0, 24 * 60)],
|
||||
)
|
||||
accent_color_id = 1
|
||||
background_custom_emoji_id = "background_custom_emoji_id"
|
||||
profile_accent_color_id = 2
|
||||
profile_background_custom_emoji_id = "profile_background_custom_emoji_id"
|
||||
unrestrict_boost_count = 100
|
||||
custom_emoji_sticker_set_name = "custom_emoji_sticker_set_name"
|
||||
birthdate = Birthdate(1, 1)
|
||||
personal_chat = Chat(3, "private", "private")
|
||||
|
||||
|
||||
class TestChatWithoutRequest(TestChatInfoBase):
|
||||
def test_slot_behaviour(self, chat_full_info):
|
||||
cfi = chat_full_info
|
||||
for attr in cfi.__slots__:
|
||||
assert getattr(cfi, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(cfi)) == len(set(mro_slots(cfi))), "duplicate slot"
|
||||
|
||||
def test_de_json(self, bot):
|
||||
json_dict = {
|
||||
"id": self.id_,
|
||||
"title": self.title,
|
||||
"type": self.type_,
|
||||
"accent_color_id": self.accent_color_id,
|
||||
"max_reaction_count": self.max_reaction_count,
|
||||
"username": self.username,
|
||||
"all_members_are_administrators": self.all_members_are_administrators,
|
||||
"sticker_set_name": self.sticker_set_name,
|
||||
"can_set_sticker_set": self.can_set_sticker_set,
|
||||
"permissions": self.permissions.to_dict(),
|
||||
"slow_mode_delay": self.slow_mode_delay,
|
||||
"bio": self.bio,
|
||||
"business_intro": self.business_intro.to_dict(),
|
||||
"business_location": self.business_location.to_dict(),
|
||||
"business_opening_hours": self.business_opening_hours.to_dict(),
|
||||
"has_protected_content": self.has_protected_content,
|
||||
"has_visible_history": self.has_visible_history,
|
||||
"has_private_forwards": self.has_private_forwards,
|
||||
"linked_chat_id": self.linked_chat_id,
|
||||
"location": self.location.to_dict(),
|
||||
"join_to_send_messages": self.join_to_send_messages,
|
||||
"join_by_request": self.join_by_request,
|
||||
"has_restricted_voice_and_video_messages": (
|
||||
self.has_restricted_voice_and_video_messages
|
||||
),
|
||||
"is_forum": self.is_forum,
|
||||
"active_usernames": self.active_usernames,
|
||||
"emoji_status_custom_emoji_id": self.emoji_status_custom_emoji_id,
|
||||
"emoji_status_expiration_date": to_timestamp(self.emoji_status_expiration_date),
|
||||
"has_aggressive_anti_spam_enabled": self.has_aggressive_anti_spam_enabled,
|
||||
"has_hidden_members": self.has_hidden_members,
|
||||
"available_reactions": [reaction.to_dict() for reaction in self.available_reactions],
|
||||
"background_custom_emoji_id": self.background_custom_emoji_id,
|
||||
"profile_accent_color_id": self.profile_accent_color_id,
|
||||
"profile_background_custom_emoji_id": self.profile_background_custom_emoji_id,
|
||||
"unrestrict_boost_count": self.unrestrict_boost_count,
|
||||
"custom_emoji_sticker_set_name": self.custom_emoji_sticker_set_name,
|
||||
"birthdate": self.birthdate.to_dict(),
|
||||
"personal_chat": self.personal_chat.to_dict(),
|
||||
}
|
||||
cfi = ChatFullInfo.de_json(json_dict, bot)
|
||||
assert cfi.max_reaction_count == self.max_reaction_count
|
||||
|
||||
def test_to_dict(self, chat_full_info):
|
||||
cfi = chat_full_info
|
||||
cfi_dict = cfi.to_dict()
|
||||
|
||||
assert isinstance(cfi_dict, dict)
|
||||
assert cfi_dict["max_reaction_count"] == cfi.max_reaction_count
|
||||
|
||||
def test_attr_access_no_warning(self, chat_full_info):
|
||||
cfi = chat_full_info
|
||||
for depr_attr in _deprecated_attrs:
|
||||
with warnings.catch_warnings(): # No warning should be raised
|
||||
warnings.simplefilter("error")
|
||||
getattr(cfi, depr_attr)
|
||||
|
||||
def test_cfi_creation_no_warning(self, chat_full_info):
|
||||
cfi = chat_full_info
|
||||
with warnings.catch_warnings():
|
||||
dict = cfi.to_dict()
|
||||
ChatFullInfo(**dict)
|
|
@ -82,7 +82,9 @@ def invite_link(user):
|
|||
|
||||
@pytest.fixture(scope="module")
|
||||
def chat_member_updated(user, chat, old_chat_member, new_chat_member, invite_link, time):
|
||||
return ChatMemberUpdated(chat, user, time, old_chat_member, new_chat_member, invite_link, True)
|
||||
return ChatMemberUpdated(
|
||||
chat, user, time, old_chat_member, new_chat_member, invite_link, True, True
|
||||
)
|
||||
|
||||
|
||||
class TestChatMemberUpdatedBase:
|
||||
|
@ -129,6 +131,7 @@ class TestChatMemberUpdatedWithoutRequest(TestChatMemberUpdatedBase):
|
|||
"new_chat_member": new_chat_member.to_dict(),
|
||||
"invite_link": invite_link.to_dict(),
|
||||
"via_chat_folder_invite_link": True,
|
||||
"via_join_request": True,
|
||||
}
|
||||
|
||||
chat_member_updated = ChatMemberUpdated.de_json(json_dict, bot)
|
||||
|
@ -142,6 +145,7 @@ class TestChatMemberUpdatedWithoutRequest(TestChatMemberUpdatedBase):
|
|||
assert chat_member_updated.new_chat_member == new_chat_member
|
||||
assert chat_member_updated.invite_link == invite_link
|
||||
assert chat_member_updated.via_chat_folder_invite_link is True
|
||||
assert chat_member_updated.via_join_request is True
|
||||
|
||||
def test_de_json_localization(
|
||||
self, bot, raw_bot, tz_bot, user, chat, old_chat_member, new_chat_member, time, invite_link
|
||||
|
@ -188,6 +192,7 @@ class TestChatMemberUpdatedWithoutRequest(TestChatMemberUpdatedBase):
|
|||
chat_member_updated_dict["via_chat_folder_invite_link"]
|
||||
== chat_member_updated.via_chat_folder_invite_link
|
||||
)
|
||||
assert chat_member_updated_dict["via_join_request"] == chat_member_updated.via_join_request
|
||||
|
||||
def test_equality(self, time, old_chat_member, new_chat_member, invite_link):
|
||||
a = ChatMemberUpdated(
|
||||
|
|
|
@ -27,7 +27,10 @@ exclude_dirs = {
|
|||
/ "_passport",
|
||||
}
|
||||
|
||||
exclude_patterns = {re.compile(re.escape("self.type: ReactionType = type"))}
|
||||
exclude_patterns = {
|
||||
re.compile(re.escape("self.type: ReactionType = type")),
|
||||
re.compile(re.escape("self.type: BackgroundType = type")),
|
||||
}
|
||||
|
||||
|
||||
def test_types_are_converted_to_enum():
|
||||
|
|
|
@ -24,8 +24,10 @@ import pytest
|
|||
from telegram import (
|
||||
Animation,
|
||||
Audio,
|
||||
BackgroundTypeChatTheme,
|
||||
Bot,
|
||||
Chat,
|
||||
ChatBackground,
|
||||
ChatBoostAdded,
|
||||
ChatShared,
|
||||
Contact,
|
||||
|
@ -270,6 +272,7 @@ def message(bot):
|
|||
{"is_from_offline": True},
|
||||
{"sender_business_bot": User(1, "BusinessBot", True)},
|
||||
{"business_connection_id": "123456789"},
|
||||
{"chat_background_set": ChatBackground(type=BackgroundTypeChatTheme("ice"))},
|
||||
],
|
||||
ids=[
|
||||
"reply",
|
||||
|
@ -338,6 +341,7 @@ def message(bot):
|
|||
"sender_business_bot",
|
||||
"business_connection_id",
|
||||
"is_from_offline",
|
||||
"chat_background_set",
|
||||
],
|
||||
)
|
||||
def message_params(bot, request):
|
||||
|
@ -2414,7 +2418,8 @@ class TestMessageWithoutRequest(TestMessageBase):
|
|||
message_id = kwargs["message_id"] == message.message_id
|
||||
latitude = kwargs["latitude"] == 1
|
||||
longitude = kwargs["longitude"] == 2
|
||||
return chat_id and message_id and longitude and latitude
|
||||
live = kwargs["live_period"] == 900
|
||||
return chat_id and message_id and longitude and latitude and live
|
||||
|
||||
assert check_shortcut_signature(
|
||||
Message.edit_live_location,
|
||||
|
@ -2432,7 +2437,7 @@ class TestMessageWithoutRequest(TestMessageBase):
|
|||
assert await check_defaults_handling(message.edit_live_location, message.get_bot())
|
||||
|
||||
monkeypatch.setattr(message.get_bot(), "edit_message_live_location", make_assertion)
|
||||
assert await message.edit_live_location(latitude=1, longitude=2)
|
||||
assert await message.edit_live_location(latitude=1, longitude=2, live_period=900)
|
||||
|
||||
async def test_stop_live_location(self, monkeypatch, message):
|
||||
async def make_assertion(*_, **kwargs):
|
||||
|
|
|
@ -148,8 +148,11 @@ def check_param_type(
|
|||
# Now let's do the checking, starting with "Array of ..." types.
|
||||
if "Array of " in tg_param_type:
|
||||
# For exceptions just check if they contain the annotation
|
||||
if ptb_param.name in PTCE.ARRAY_OF_EXCEPTIONS:
|
||||
return PTCE.ARRAY_OF_EXCEPTIONS[ptb_param.name] in str(ptb_annotation), Sequence
|
||||
if any(ptb_param.name in key for key in PTCE.ARRAY_OF_EXCEPTIONS):
|
||||
for (p_name, is_expected_class), exception_type in PTCE.ARRAY_OF_EXCEPTIONS.items():
|
||||
if ptb_param.name == p_name and is_class is is_expected_class:
|
||||
log("Checking that `%s` is an exception!\n", ptb_param.name)
|
||||
return exception_type in str(ptb_annotation), Sequence
|
||||
|
||||
obj_match: re.Match | None = re.search(ARRAY_OF_PATTERN, tg_param_type)
|
||||
if obj_match is None:
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
|
||||
from telegram import Animation, Audio, Document, PhotoSize, Sticker, Video, VideoNote, Voice
|
||||
from telegram._chat import _deprecated_attrs
|
||||
from tests.test_official.helpers import _get_params_base
|
||||
|
||||
IGNORED_OBJECTS = ("ResponseParameters",)
|
||||
|
@ -47,15 +48,17 @@ class ParamTypeCheckingExceptions:
|
|||
"sticker": Sticker,
|
||||
}
|
||||
|
||||
# TODO: Look into merging this with COMPLEX_TYPES
|
||||
# Exceptions to the "Array of" types, where we accept more types than the official API
|
||||
# key: parameter name, value: type which must be present in the annotation
|
||||
# key: (parameter name, is_class), value: type which must be present in the annotation
|
||||
ARRAY_OF_EXCEPTIONS = {
|
||||
"results": "InlineQueryResult", # + Callable
|
||||
"commands": "BotCommand", # + tuple[str, str]
|
||||
"keyboard": "KeyboardButton", # + sequence[sequence[str]]
|
||||
"reaction": "ReactionType", # + str
|
||||
("results", False): "InlineQueryResult", # + Callable
|
||||
("commands", False): "BotCommand", # + tuple[str, str]
|
||||
("keyboard", True): "KeyboardButton", # + sequence[sequence[str]]
|
||||
("reaction", False): "ReactionType", # + str
|
||||
("options", False): "InputPollOption", # + str
|
||||
# TODO: Deprecated and will be corrected (and removed) in next major PTB version:
|
||||
"file_hashes": "List[str]",
|
||||
("file_hashes", True): "List[str]",
|
||||
}
|
||||
|
||||
# Special cases for other parameters that accept more types than the official API, and are
|
||||
|
@ -120,6 +123,8 @@ PTB_EXTRA_PARAMS = {
|
|||
"ChatBoostSource": {"source"}, # attributes common to all subclasses
|
||||
"MessageOrigin": {"type", "date"}, # attributes common to all subclasses
|
||||
"ReactionType": {"type"}, # attributes common to all subclasses
|
||||
"BackgroundType": {"type"}, # attributes common to all subclasses
|
||||
"BackgroundFill": {"type"}, # attributes common to all subclasses
|
||||
"InputTextMessageContent": {"disable_web_page_preview"}, # convenience arg, here for bw compat
|
||||
}
|
||||
|
||||
|
@ -143,6 +148,8 @@ PTB_IGNORED_PARAMS = {
|
|||
r"MessageOrigin\w+": {"type"},
|
||||
r"ChatBoostSource\w+": {"source"},
|
||||
r"ReactionType\w+": {"type"},
|
||||
r"BackgroundType\w+": {"type"},
|
||||
r"BackgroundFill\w+": {"type"},
|
||||
}
|
||||
|
||||
|
||||
|
@ -166,7 +173,9 @@ def ignored_param_requirements(object_name: str) -> set[str]:
|
|||
|
||||
|
||||
# Arguments that are optional arguments for now for backwards compatibility
|
||||
BACKWARDS_COMPAT_KWARGS: dict[str, set[str]] = {}
|
||||
BACKWARDS_COMPAT_KWARGS: dict[str, set[str]] = {
|
||||
"Chat": set(_deprecated_attrs), # removed by bot api 7.3
|
||||
}
|
||||
|
||||
|
||||
def backwards_compat_kwargs(object_name: str) -> set[str]:
|
||||
|
|
|
@ -19,15 +19,97 @@ from datetime import datetime, timedelta, timezone
|
|||
|
||||
import pytest
|
||||
|
||||
from telegram import Chat, MessageEntity, Poll, PollAnswer, PollOption, User
|
||||
from telegram import Chat, InputPollOption, MessageEntity, Poll, PollAnswer, PollOption, User
|
||||
from telegram._utils.datetime import UTC, to_timestamp
|
||||
from telegram.constants import PollType
|
||||
from tests.auxil.slots import mro_slots
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def input_poll_option():
|
||||
out = InputPollOption(
|
||||
text=TestInputPollOptionBase.text,
|
||||
text_parse_mode=TestInputPollOptionBase.text_parse_mode,
|
||||
text_entities=TestInputPollOptionBase.text_entities,
|
||||
)
|
||||
out._unfreeze()
|
||||
return out
|
||||
|
||||
|
||||
class TestInputPollOptionBase:
|
||||
text = "test option"
|
||||
text_parse_mode = "MarkdownV2"
|
||||
text_entities = [
|
||||
MessageEntity(0, 4, MessageEntity.BOLD),
|
||||
MessageEntity(5, 7, MessageEntity.ITALIC),
|
||||
]
|
||||
|
||||
|
||||
class TestInputPollOptionWithoutRequest(TestInputPollOptionBase):
|
||||
def test_slot_behaviour(self, input_poll_option):
|
||||
for attr in input_poll_option.__slots__:
|
||||
assert getattr(input_poll_option, attr, "err") != "err", f"got extra slot '{attr}'"
|
||||
assert len(mro_slots(input_poll_option)) == len(
|
||||
set(mro_slots(input_poll_option))
|
||||
), "duplicate slot"
|
||||
|
||||
def test_de_json(self):
|
||||
assert InputPollOption.de_json({}, None) is None
|
||||
|
||||
json_dict = {
|
||||
"text": self.text,
|
||||
"text_parse_mode": self.text_parse_mode,
|
||||
"text_entities": [e.to_dict() for e in self.text_entities],
|
||||
}
|
||||
input_poll_option = InputPollOption.de_json(json_dict, None)
|
||||
assert input_poll_option.api_kwargs == {}
|
||||
|
||||
assert input_poll_option.text == self.text
|
||||
assert input_poll_option.text_parse_mode == self.text_parse_mode
|
||||
assert input_poll_option.text_entities == tuple(self.text_entities)
|
||||
|
||||
def test_to_dict(self, input_poll_option):
|
||||
input_poll_option_dict = input_poll_option.to_dict()
|
||||
|
||||
assert isinstance(input_poll_option_dict, dict)
|
||||
assert input_poll_option_dict["text"] == input_poll_option.text
|
||||
assert input_poll_option_dict["text_parse_mode"] == input_poll_option.text_parse_mode
|
||||
assert input_poll_option_dict["text_entities"] == [
|
||||
e.to_dict() for e in input_poll_option.text_entities
|
||||
]
|
||||
|
||||
# Test that the default-value parameter is handled correctly
|
||||
input_poll_option = InputPollOption("text")
|
||||
input_poll_option_dict = input_poll_option.to_dict()
|
||||
assert "text_parse_mode" not in input_poll_option_dict
|
||||
|
||||
def test_equality(self):
|
||||
a = InputPollOption("text")
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def poll_option():
|
||||
out = PollOption(text=TestPollOptionBase.text, voter_count=TestPollOptionBase.voter_count)
|
||||
out = PollOption(
|
||||
text=TestPollOptionBase.text,
|
||||
voter_count=TestPollOptionBase.voter_count,
|
||||
text_entities=TestPollOptionBase.text_entities,
|
||||
)
|
||||
out._unfreeze()
|
||||
return out
|
||||
|
||||
|
@ -35,6 +117,10 @@ def poll_option():
|
|||
class TestPollOptionBase:
|
||||
text = "test option"
|
||||
voter_count = 3
|
||||
text_entities = [
|
||||
MessageEntity(MessageEntity.BOLD, 0, 4),
|
||||
MessageEntity(MessageEntity.ITALIC, 5, 6),
|
||||
]
|
||||
|
||||
|
||||
class TestPollOptionWithoutRequest(TestPollOptionBase):
|
||||
|
@ -51,12 +137,43 @@ class TestPollOptionWithoutRequest(TestPollOptionBase):
|
|||
assert poll_option.text == self.text
|
||||
assert poll_option.voter_count == self.voter_count
|
||||
|
||||
def test_de_json_all(self):
|
||||
json_dict = {
|
||||
"text": self.text,
|
||||
"voter_count": self.voter_count,
|
||||
"text_entities": [e.to_dict() for e in self.text_entities],
|
||||
}
|
||||
poll_option = PollOption.de_json(json_dict, None)
|
||||
assert PollOption.de_json(None, None) is None
|
||||
assert poll_option.api_kwargs == {}
|
||||
|
||||
assert poll_option.text == self.text
|
||||
assert poll_option.voter_count == self.voter_count
|
||||
assert poll_option.text_entities == tuple(self.text_entities)
|
||||
|
||||
def test_to_dict(self, poll_option):
|
||||
poll_option_dict = poll_option.to_dict()
|
||||
|
||||
assert isinstance(poll_option_dict, dict)
|
||||
assert poll_option_dict["text"] == poll_option.text
|
||||
assert poll_option_dict["voter_count"] == poll_option.voter_count
|
||||
assert poll_option_dict["text_entities"] == [
|
||||
e.to_dict() for e in poll_option.text_entities
|
||||
]
|
||||
|
||||
def test_parse_entity(self, poll_option):
|
||||
entity = MessageEntity(MessageEntity.BOLD, 0, 4)
|
||||
poll_option.text_entities = [entity]
|
||||
|
||||
assert poll_option.parse_entity(entity) == "test"
|
||||
|
||||
def test_parse_entities(self, poll_option):
|
||||
entity = MessageEntity(MessageEntity.BOLD, 0, 4)
|
||||
entity_2 = MessageEntity(MessageEntity.ITALIC, 5, 6)
|
||||
poll_option.text_entities = [entity, entity_2]
|
||||
|
||||
assert poll_option.parse_entities(MessageEntity.BOLD) == {entity: "test"}
|
||||
assert poll_option.parse_entities() == {entity: "test", entity_2: "option"}
|
||||
|
||||
def test_equality(self):
|
||||
a = PollOption("text", 1)
|
||||
|
@ -159,6 +276,7 @@ def poll():
|
|||
explanation_entities=TestPollBase.explanation_entities,
|
||||
open_period=TestPollBase.open_period,
|
||||
close_date=TestPollBase.close_date,
|
||||
question_entities=TestPollBase.question_entities,
|
||||
)
|
||||
poll._unfreeze()
|
||||
return poll
|
||||
|
@ -166,7 +284,7 @@ def poll():
|
|||
|
||||
class TestPollBase:
|
||||
id_ = "id"
|
||||
question = "Test?"
|
||||
question = "Test Question?"
|
||||
options = [PollOption("test", 10), PollOption("test2", 11)]
|
||||
total_voter_count = 0
|
||||
is_closed = True
|
||||
|
@ -180,6 +298,10 @@ class TestPollBase:
|
|||
explanation_entities = [MessageEntity(13, 17, MessageEntity.URL)]
|
||||
open_period = 42
|
||||
close_date = datetime.now(timezone.utc)
|
||||
question_entities = [
|
||||
MessageEntity(MessageEntity.BOLD, 0, 4),
|
||||
MessageEntity(MessageEntity.ITALIC, 5, 8),
|
||||
]
|
||||
|
||||
|
||||
class TestPollWithoutRequest(TestPollBase):
|
||||
|
@ -197,6 +319,7 @@ class TestPollWithoutRequest(TestPollBase):
|
|||
"explanation_entities": [self.explanation_entities[0].to_dict()],
|
||||
"open_period": self.open_period,
|
||||
"close_date": to_timestamp(self.close_date),
|
||||
"question_entities": [e.to_dict() for e in self.question_entities],
|
||||
}
|
||||
poll = Poll.de_json(json_dict, bot)
|
||||
assert poll.api_kwargs == {}
|
||||
|
@ -218,6 +341,7 @@ class TestPollWithoutRequest(TestPollBase):
|
|||
assert poll.open_period == self.open_period
|
||||
assert abs(poll.close_date - self.close_date) < timedelta(seconds=1)
|
||||
assert to_timestamp(poll.close_date) == to_timestamp(self.close_date)
|
||||
assert poll.question_entities == tuple(self.question_entities)
|
||||
|
||||
def test_de_json_localization(self, tz_bot, bot, raw_bot):
|
||||
json_dict = {
|
||||
|
@ -233,6 +357,7 @@ class TestPollWithoutRequest(TestPollBase):
|
|||
"explanation_entities": [self.explanation_entities[0].to_dict()],
|
||||
"open_period": self.open_period,
|
||||
"close_date": to_timestamp(self.close_date),
|
||||
"question_entities": [e.to_dict() for e in self.question_entities],
|
||||
}
|
||||
|
||||
poll_raw = Poll.de_json(json_dict, raw_bot)
|
||||
|
@ -265,6 +390,7 @@ class TestPollWithoutRequest(TestPollBase):
|
|||
assert poll_dict["explanation_entities"] == [poll.explanation_entities[0].to_dict()]
|
||||
assert poll_dict["open_period"] == poll.open_period
|
||||
assert poll_dict["close_date"] == to_timestamp(poll.close_date)
|
||||
assert poll_dict["question_entities"] == [e.to_dict() for e in poll.question_entities]
|
||||
|
||||
def test_equality(self):
|
||||
a = Poll(123, "question", ["O1", "O2"], 1, False, True, Poll.REGULAR, True)
|
||||
|
@ -305,7 +431,7 @@ class TestPollWithoutRequest(TestPollBase):
|
|||
)
|
||||
assert poll.type is PollType.QUIZ
|
||||
|
||||
def test_parse_entity(self, poll):
|
||||
def test_parse_explanation_entity(self, poll):
|
||||
entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17)
|
||||
poll.explanation_entities = [entity]
|
||||
|
||||
|
@ -323,10 +449,36 @@ class TestPollWithoutRequest(TestPollBase):
|
|||
allows_multiple_answers=False,
|
||||
).parse_explanation_entity(entity)
|
||||
|
||||
def test_parse_entities(self, poll):
|
||||
def test_parse_explanation_entities(self, poll):
|
||||
entity = MessageEntity(type=MessageEntity.URL, offset=13, length=17)
|
||||
entity_2 = MessageEntity(type=MessageEntity.BOLD, offset=13, length=1)
|
||||
poll.explanation_entities = [entity_2, entity]
|
||||
|
||||
assert poll.parse_explanation_entities(MessageEntity.URL) == {entity: "http://google.com"}
|
||||
assert poll.parse_explanation_entities() == {entity: "http://google.com", entity_2: "h"}
|
||||
|
||||
with pytest.raises(RuntimeError, match="Poll has no"):
|
||||
Poll(
|
||||
"id",
|
||||
"question",
|
||||
[PollOption("text", voter_count=0)],
|
||||
total_voter_count=0,
|
||||
is_closed=False,
|
||||
is_anonymous=False,
|
||||
type=Poll.QUIZ,
|
||||
allows_multiple_answers=False,
|
||||
).parse_explanation_entities()
|
||||
|
||||
def test_parse_question_entity(self, poll):
|
||||
entity = MessageEntity(MessageEntity.ITALIC, 5, 8)
|
||||
poll.question_entities = [entity]
|
||||
|
||||
assert poll.parse_question_entity(entity) == "Question"
|
||||
|
||||
def test_parse_question_entities(self, poll):
|
||||
entity = MessageEntity(MessageEntity.ITALIC, 5, 8)
|
||||
entity_2 = MessageEntity(MessageEntity.BOLD, 0, 4)
|
||||
poll.question_entities = [entity_2, entity]
|
||||
|
||||
assert poll.parse_question_entities(MessageEntity.ITALIC) == {entity: "Question"}
|
||||
assert poll.parse_question_entities() == {entity: "Question", entity_2: "Test"}
|
||||
|
|
|
@ -33,7 +33,7 @@ class TestWarnings:
|
|||
[
|
||||
(PTBUserWarning("test message")),
|
||||
(PTBRuntimeWarning("test message")),
|
||||
(PTBDeprecationWarning()),
|
||||
(PTBDeprecationWarning("20.6", "test message")),
|
||||
],
|
||||
)
|
||||
def test_slots_behavior(self, inst):
|
||||
|
@ -80,9 +80,8 @@ class TestWarnings:
|
|||
assert str(recwarn[1].message) == "test message 2"
|
||||
assert Path(recwarn[1].filename) == expected_file, "incorrect stacklevel!"
|
||||
|
||||
warn("test message 3", stacklevel=1, category=PTBDeprecationWarning)
|
||||
expected_file = Path(__file__)
|
||||
warn(PTBDeprecationWarning("20.6", "test message 3"), stacklevel=1)
|
||||
assert len(recwarn) == 3
|
||||
assert recwarn[2].category is PTBDeprecationWarning
|
||||
assert str(recwarn[2].message) == "test message 3"
|
||||
assert Path(recwarn[2].filename) == expected_file, "incorrect stacklevel!"
|
||||
assert str(recwarn[2].message) == "Deprecated since version 20.6: test message 3"
|
||||
assert Path(recwarn[2].filename) == Path(__file__), "incorrect stacklevel!"
|
||||
|
|
Loading…
Reference in a new issue