Compare commits

..

17 Commits

Author SHA1 Message Date
Hinrich Mahler 75d946e4be Bump version to v21.1.1 2024-04-15 17:00:17 +02:00
Bibo-Joshi fed8d8875e Fix Bug With Parameter message_thread_id of Message.reply_* (#4207) 2024-04-15 16:49:36 +02:00
Nano 42b68f1a70 Remove Deprecation Warning in JobQueue.run_daily (#4206) 2024-04-14 14:14:45 +02:00
marinelay 58b8ef4ce4 Fix Annotation of EncryptedCredentials.decrypted_secret (#4199)
Co-authored-by: marinelay <marinelay@gmail.com>
2024-04-13 20:27:29 +02:00
Hinrich Mahler f6d009d3ac Bump version to v21.1 2024-04-12 12:39:38 +02:00
Bibo-Joshi 153894728c Documentation Improvements (#4171, #4158)
Signed-off-by: teslaedison <qingchengqiushuang@gmail.com>
Co-authored-by: teslaedison <156734008+teslaedison@users.noreply.github.com>
2024-04-12 12:03:01 +02:00
Harshil 5fa457974d API 7.2 (#4180, #4181)
Co-authored-by: Mahdyar Hasanpour <mahdyar@duck.com>
Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
Co-authored-by: Abdelrahman Elkheir <90580077+aelkheir@users.noreply.github.com>
Co-authored-by: Aditya <adityayadav11082@gmail.com>
2024-04-12 11:58:25 +02:00
Harshil 3ec7bb819c Make ChatAdministratorRights.can_*_stories Required (API 7.1) (#4192)
Co-authored-by: Hinrich Mahler <22366557+Bibo-Joshi@users.noreply.github.com>
2024-04-06 17:13:43 +02:00
pre-commit-ci[bot] 040cd2c2fc pre-commit autoupdate (#4184)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
2024-04-05 17:26:08 +02:00
Abdelrahman Elkheir 474f9c9693 Make Message.reply_* Reply in the Same Topic by Default (#4170)
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
2024-04-03 11:32:39 +02:00
dependabot[bot] e18ca0d5e1 Bump dependabot/fetch-metadata from 1.6.0 to 2.0.0 (#4185)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bibo-Joshi <22366557+Bibo-Joshi@users.noreply.github.com>
2024-04-02 22:19:33 +02:00
Bibo-Joshi 7331fff3fc Temporarily Mark Tests with get_sticker_set as XFAIL due to API 7.2 Update (#4190) 2024-04-02 22:13:54 +02:00
Abdelrahman Elkheir 23536ee759 Add Update.effective_sender (#4168) 2024-03-30 18:21:13 +01:00
Poolitzer 2d8d43f2a5 Accept Socket Objects for Webhooks (#4161) 2024-03-24 21:04:10 +01:00
Bibo-Joshi 8a542e22a0 Refactor Debug logging in Bot to Improve Type Hinting (#4151) 2024-03-24 12:34:08 +01:00
Hinrich Mahler c0716dd344 Bump version to v21.0.1 2024-03-06 22:15:30 +01:00
Bibo-Joshi 668b49b048 Remove docs from Package (#4150) 2024-03-06 22:04:19 +01:00
77 changed files with 4157 additions and 558 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- name: Fetch Dependabot metadata
id: dependabot-metadata
uses: dependabot/fetch-metadata@v1.6.0
uses: dependabot/fetch-metadata@v2.0.0
- uses: actions/checkout@v4
with:
+5 -5
View File
@@ -6,7 +6,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.2.1'
rev: 'v0.3.5'
hooks:
- id: ruff
name: ruff
@@ -17,7 +17,7 @@ repos:
- cachetools~=5.3.3
- aiolimiter~=1.1.0
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.1.1
rev: 24.3.0
hooks:
- id: black
args:
@@ -28,7 +28,7 @@ repos:
hooks:
- id: flake8
- repo: https://github.com/PyCQA/pylint
rev: v3.0.3
rev: v3.1.0
hooks:
- id: pylint
files: ^(?!(tests|docs)).*\.py$
@@ -40,7 +40,7 @@ repos:
- aiolimiter~=1.1.0
- . # this basically does `pip install -e .`
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
rev: v1.9.0
hooks:
- id: mypy
name: mypy-ptb
@@ -67,7 +67,7 @@ repos:
- cachetools~=5.3.3
- . # this basically does `pip install -e .`
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
rev: v3.15.2
hooks:
- id: pyupgrade
args:
+1
View File
@@ -123,6 +123,7 @@ The following wonderful people contributed directly or indirectly to this projec
- `Vorobjev Simon <https://github.com/simonvorobjev>`_
- `Wagner Macedo <https://github.com/wagnerluis1982>`_
- `wjt <https://github.com/wjt>`_
- `Wonseok Oh <https://github.com/marinelay>`_
- `Yaw Danso <https://github.com/dglitxh>`_
- `Yao Kuan <https://github.com/thatguylah>`_
- `zeroone2numeral2 <https://github.com/zeroone2numeral2>`_
+74
View File
@@ -4,6 +4,80 @@
Changelog
=========
Version 21.1.1
==============
*Released 2024-04-15*
This is the technical changelog for version 21.1.1. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`__.
Bug Fixes
---------
- Fix Bug With Parameter ``message_thread_id`` of ``Message.reply_*`` (:pr:`4207` closes :issue:`4205`)
Minor Changes
-------------
- Remove Deprecation Warning in ``JobQueue.run_daily`` (:pr:`4206` by `@Konano <https://github.com/Konano>`__)
- Fix Annotation of ``EncryptedCredentials.decrypted_secret`` (:pr:`4199` by `@marinelay <https://github.com/marinelay>`__ closes :issue:`4198`)
Version 21.1
==============
*Released 2024-04-12*
This is the technical changelog for version 21.1. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`__.
Major Changes
-------------
- API 7.2 (:pr:`4180` closes :issue:`4179` and :issue:`4181`, :issue:`4181`)
- Make ``ChatAdministratorRights/ChatMemberAdministrator.can_*_stories`` Required (API 7.1) (:pr:`4192`)
Minor Changes
-------------
- Refactor Debug logging in ``Bot`` to Improve Type Hinting (:pr:`4151` closes :issue:`4010`)
New Features
------------
- Make ``Message.reply_*`` Reply in the Same Topic by Default (:pr:`4170` by `@aelkheir <https://github.com/aelkheir>`__ closes :issue:`4139`)
- Accept Socket Objects for Webhooks (:pr:`4161` closes :issue:`4078`)
- Add ``Update.effective_sender`` (:pr:`4168` by `@aelkheir <https://github.com/aelkheir>`__ closes :issue:`4085`)
Documentation Improvements
--------------------------
- Documentation Improvements (:pr:`4171`, :pr:`4158` by `@teslaedison <https://github.com/teslaedison>`__)
Internal Changes
----------------
- Temporarily Mark Tests with ``get_sticker_set`` as XFAIL due to API 7.2 Update (:pr:`4190`)
Dependency Updates
------------------
- ``pre-commit`` autoupdate (:pr:`4184`)
- Bump ``dependabot/fetch-metadata`` from 1.6.0 to 2.0.0 (:pr:`4185`)
Version 21.0.1
==============
*Released 2024-03-06*
This is the technical changelog for version 21.0.1. More elaborate release notes can be found in the news channel `@pythontelegrambotchannel <https://t.me/pythontelegrambotchannel>`__.
Bug Fixes
---------
- Remove ``docs`` from Package (:pr:`4150`)
Version 21.0
============
+2 -2
View File
@@ -14,7 +14,7 @@
:target: https://pypi.org/project/python-telegram-bot/
:alt: Supported Python versions
.. image:: https://img.shields.io/badge/Bot%20API-7.1-blue?logo=telegram
.. image:: https://img.shields.io/badge/Bot%20API-7.2-blue?logo=telegram
:target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API versions
@@ -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.1** are supported.
All types and methods of the Telegram Bot API **7.2** are supported.
Installing
==========
+2 -2
View File
@@ -14,7 +14,7 @@
:target: https://pypi.org/project/python-telegram-bot-raw/
:alt: Supported Python versions
.. image:: https://img.shields.io/badge/Bot%20API-7.1-blue?logo=telegram
.. image:: https://img.shields.io/badge/Bot%20API-7.2-blue?logo=telegram
:target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API versions
@@ -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.1** are supported.
All types and methods of the Telegram Bot API **7.2** are supported.
Installing
==========
+2 -8
View File
@@ -20,9 +20,9 @@ author = "Leandro Toledo"
# built documents.
#
# The short X.Y version.
version = "21.0" # telegram.__version__[:3]
version = "21.1.1" # telegram.__version__[:3]
# The full version, including alpha/beta/rc tags.
release = "21.0" # telegram.__version__
release = "21.1.1" # telegram.__version__
# If your documentation needs a minimal Sphinx version, state it here.
needs_sphinx = "6.1.3"
@@ -140,12 +140,6 @@ html_theme_options = {
"admonition-title-font-size": "0.95rem",
"admonition-font-size": "0.92rem",
},
"announcement": (
"PTB has undergone significant changes in v20. Please read the documentation "
"carefully and also check out the transition guide in the "
'<a href="https://github.com/python-telegram-bot/python-telegram-bot/wiki/'
'Transition-guide-to-Version-20.0">wiki</a>.'
),
"footer_icons": [
{
# Telegram channel logo
+8 -4
View File
@@ -113,6 +113,10 @@
:align: left
:widths: 1 4
* - :meth:`~telegram.Bot.approve_chat_join_request`
- Used for approving a chat join request
* - :meth:`~telegram.Bot.decline_chat_join_request`
- Used for declining a chat join request
* - :meth:`~telegram.Bot.ban_chat_member`
- Used for banning a member from the chat
* - :meth:`~telegram.Bot.unban_chat_member`
@@ -137,10 +141,6 @@
- Used for editing a non-primary invite link
* - :meth:`~telegram.Bot.revoke_chat_invite_link`
- Used for revoking an invite link created by the bot
* - :meth:`~telegram.Bot.approve_chat_join_request`
- Used for approving a chat join request
* - :meth:`~telegram.Bot.decline_chat_join_request`
- Used for declining a chat join request
* - :meth:`~telegram.Bot.set_chat_photo`
- Used for setting a photo to a chat
* - :meth:`~telegram.Bot.delete_chat_photo`
@@ -155,6 +155,8 @@
- Used for unpinning a message
* - :meth:`~telegram.Bot.unpin_all_chat_messages`
- Used for unpinning all pinned chat messages
* - :meth:`~telegram.Bot.get_business_connection`
- Used for getting information about the business account.
* - :meth:`~telegram.Bot.get_user_profile_photos`
- Used for obtaining user's profile pictures
* - :meth:`~telegram.Bot.get_chat`
@@ -237,6 +239,8 @@
- Used for setting a sticker set of a chat
* - :meth:`~telegram.Bot.delete_chat_sticker_set`
- Used for deleting the set sticker set of a chat
* - :meth:`~telegram.Bot.replace_sticker_in_set`
- Used for replacing a sticker in a set
* - :meth:`~telegram.Bot.set_sticker_position_in_set`
- Used for moving a sticker's position in the set
* - :meth:`~telegram.Bot.set_sticker_set_title`
+8
View File
@@ -6,6 +6,7 @@ Available Types
telegram.animation
telegram.audio
telegram.birthdate
telegram.botcommand
telegram.botcommandscope
telegram.botcommandscopeallchatadministrators
@@ -18,6 +19,12 @@ Available Types
telegram.botdescription
telegram.botname
telegram.botshortdescription
telegram.businessconnection
telegram.businessintro
telegram.businesslocation
telegram.businessopeninghours
telegram.businessopeninghoursinterval
telegram.businessmessagesdeleted
telegram.callbackquery
telegram.chat
telegram.chatadministratorrights
@@ -107,6 +114,7 @@ Available Types
telegram.replykeyboardremove
telegram.replyparameters
telegram.sentwebappmessage
telegram.shareduser
telegram.story
telegram.switchinlinequerychosenchat
telegram.telegramobject
+7
View File
@@ -0,0 +1,7 @@
Birthdate
=========
.. autoclass:: telegram.Birthdate
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
BusinessConnection
==================
.. autoclass:: telegram.BusinessConnection
:members:
:show-inheritance:
+6
View File
@@ -0,0 +1,6 @@
BusinessIntro
==================
.. autoclass:: telegram.BusinessIntro
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
BusinessLocation
==================
.. autoclass:: telegram.BusinessLocation
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
BusinessMessagesDeleted
=======================
.. autoclass:: telegram.BusinessMessagesDeleted
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
BusinessOpeningHours
====================
.. autoclass:: telegram.BusinessOpeningHours
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
BusinessOpeningHoursInterval
============================
.. autoclass:: telegram.BusinessOpeningHoursInterval
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
BusinessConnectionHandler
=========================
.. autoclass:: telegram.ext.BusinessConnectionHandler
:members:
:show-inheritance:
@@ -0,0 +1,6 @@
BusinessMessagesDeletedHandler
==============================
.. autoclass:: telegram.ext.BusinessMessagesDeletedHandler
:members:
:show-inheritance:
@@ -5,6 +5,8 @@ Handlers
:titlesonly:
telegram.ext.basehandler
telegram.ext.businessconnectionhandler
telegram.ext.businessmessagesdeletedhandler
telegram.ext.callbackqueryhandler
telegram.ext.chatboosthandler
telegram.ext.chatjoinrequesthandler
+7
View File
@@ -0,0 +1,7 @@
SharedUser
==========
.. autoclass:: telegram.SharedUser
:members:
:show-inheritance:
+2
View File
@@ -79,3 +79,5 @@
.. |do_quote| replace:: If set to :obj:`True`, the replied message is quoted. For a dict, it must be the output of :meth:`~telegram.Message.build_reply_arguments` to specify exact ``reply_parameters``. If ``reply_to_message_id`` or ``reply_parameters`` are passed, this parameter will be ignored. Default: :obj:`True` in group chats and :obj:`False` in private chats.
.. |non_optional_story_argument| replace:: As of this version, this argument is now required. In accordance with our `stability policy <https://docs.python-telegram-bot.org/en/stable/stability_policy.html>`__, the signature will be kept as optional for now, though they are mandatory and an error will be raised if you don't pass it.
.. |business_id_str| replace:: Unique identifier of the business connection on behalf of which the message will be sent.
+1 -1
View File
@@ -20,7 +20,7 @@ explicit-preview-rules = true
ignore = ["PLR2004", "PLR0911", "PLR0912", "PLR0913", "PLR0915", "PERF203"]
select = ["E", "F", "I", "PL", "UP", "RUF", "PTH", "C4", "B", "PIE", "SIM", "RET", "RSE",
"G", "ISC", "PT", "ASYNC", "TCH", "SLOT", "PERF", "PYI", "FLY", "AIR", "RUF022",
"RUF023", "Q", "INP",]
"RUF023", "Q", "INP", "W"]
# Add "FURB" after it's out of preview
[tool.ruff.lint.per-file-ignores]
+1 -1
View File
@@ -26,7 +26,7 @@ def get_packages_requirements(raw: bool = False) -> Tuple[List[str], List[str]]:
"""Build the package & requirements list for this project"""
reqs = get_requirements()
exclude = ["tests*"]
exclude = ["tests*", "docs*"]
if raw:
exclude.append("telegram.ext*")
+18 -1
View File
@@ -22,6 +22,7 @@ __author__ = "devs@python-telegram-bot.org"
__all__ = (
"Animation",
"Audio",
"Birthdate",
"Bot",
"BotCommand",
"BotCommandScope",
@@ -35,6 +36,12 @@ __all__ = (
"BotDescription",
"BotName",
"BotShortDescription",
"BusinessConnection",
"BusinessIntro",
"BusinessLocation",
"BusinessMessagesDeleted",
"BusinessOpeningHours",
"BusinessOpeningHoursInterval",
"CallbackGame",
"CallbackQuery",
"Chat",
@@ -184,6 +191,7 @@ __all__ = (
"SecureData",
"SecureValue",
"SentWebAppMessage",
"SharedUser",
"ShippingAddress",
"ShippingOption",
"ShippingQuery",
@@ -224,6 +232,7 @@ __all__ = (
from . import _version, constants, error, helpers, request, warnings
from ._birthdate import Birthdate
from ._bot import Bot
from ._botcommand import BotCommand
from ._botcommandscope import (
@@ -238,6 +247,14 @@ from ._botcommandscope import (
)
from ._botdescription import BotDescription, BotShortDescription
from ._botname import BotName
from ._business import (
BusinessConnection,
BusinessIntro,
BusinessLocation,
BusinessMessagesDeleted,
BusinessOpeningHours,
BusinessOpeningHoursInterval,
)
from ._callbackquery import CallbackQuery
from ._chat import Chat
from ._chatadministratorrights import ChatAdministratorRights
@@ -393,7 +410,7 @@ from ._reply import ExternalReplyInfo, ReplyParameters, TextQuote
from ._replykeyboardmarkup import ReplyKeyboardMarkup
from ._replykeyboardremove import ReplyKeyboardRemove
from ._sentwebappmessage import SentWebAppMessage
from ._shared import ChatShared, UsersShared
from ._shared import ChatShared, SharedUser, UsersShared
from ._story import Story
from ._switchinlinequerychosenchat import SwitchInlineQueryChosenChat
from ._telegramobject import TelegramObject
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Birthday."""
from datetime import datetime
from typing import Optional
from telegram._telegramobject import TelegramObject
from telegram._utils.types import JSONDict
class Birthdate(TelegramObject):
"""
This object represents a user's birthday.
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.
.. versionadded:: 21.1
Args:
day (:obj:`int`): Day of the user's birth; 1-31.
month (:obj:`int`): Month of the user's birth; 1-12.
year (:obj:`int`, optional): Year of the user's birth.
Attributes:
day (:obj:`int`): Day of the user's birth; 1-31.
month (:obj:`int`): Month of the user's birth; 1-12.
year (:obj:`int`): Optional. Year of the user's birth.
"""
__slots__ = ("day", "month", "year")
def __init__(
self,
day: int,
month: int,
year: Optional[int] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
# Required
self.day: int = day
self.month: int = month
# Optional
self.year: Optional[int] = year
self._id_attrs = (
self.day,
self.month,
)
self._freeze()
def to_date(self, year: Optional[int] = None) -> datetime:
"""Return the birthdate as a datetime object.
Args:
year (:obj:`int`, optional): The year to use. Required, if the :attr:`year` was not
present.
Returns:
:obj:`datetime.datetime`: The birthdate as a datetime object.
"""
if self.year is None and year is None:
raise ValueError(
"The `year` argument is required if the `year` attribute was not present."
)
return datetime(year or self.year, self.month, self.day) # type: ignore[arg-type]
+223 -146
View File
File diff suppressed because it is too large Load Diff
+445
View File
@@ -0,0 +1,445 @@
#!/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 the Telegram Business related classes."""
from datetime import datetime
from typing import TYPE_CHECKING, Optional, Sequence, Tuple
from telegram._chat import Chat
from telegram._files.location import Location
from telegram._files.sticker import Sticker
from telegram._telegramobject import TelegramObject
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.types import JSONDict
if TYPE_CHECKING:
from telegram import Bot
class BusinessConnection(TelegramObject):
"""
Describes the connection of the bot with a business account.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal if their :attr:`id`, :attr:`user`, :attr:`user_chat_id`, :attr:`date`,
:attr:`can_reply`, and :attr:`is_enabled` are equal.
.. versionadded:: 21.1
Args:
id (:obj:`str`): Unique identifier of the business connection.
user (:class:`telegram.User`): Business account user that created the business connection.
user_chat_id (:obj:`int`): Identifier of a private chat with the user who created the
business connection.
date (:obj:`datetime.datetime`): Date the connection was established in Unix time.
can_reply (:obj:`bool`): True, if the bot can act on behalf of the business account in
chats that were active in the last 24 hours.
is_enabled (:obj:`bool`): True, if the connection is active.
Attributes:
id (:obj:`str`): Unique identifier of the business connection.
user (:class:`telegram.User`): Business account user that created the business connection.
user_chat_id (:obj:`int`): Identifier of a private chat with the user who created the
business connection.
date (:obj:`datetime.datetime`): Date the connection was established in Unix time.
can_reply (:obj:`bool`): True, if the bot can act on behalf of the business account in
chats that were active in the last 24 hours.
is_enabled (:obj:`bool`): True, if the connection is active.
"""
__slots__ = (
"can_reply",
"date",
"id",
"is_enabled",
"user",
"user_chat_id",
)
def __init__(
self,
id: str,
user: "User",
user_chat_id: int,
date: datetime,
can_reply: bool,
is_enabled: bool,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.id: str = id
self.user: User = user
self.user_chat_id: int = user_chat_id
self.date: datetime = date
self.can_reply: bool = can_reply
self.is_enabled: bool = is_enabled
self._id_attrs = (
self.id,
self.user,
self.user_chat_id,
self.date,
self.can_reply,
self.is_enabled,
)
self._freeze()
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BusinessConnection"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
# Get the local timezone from the bot if it has defaults
loc_tzinfo = extract_tzinfo_from_defaults(bot)
data["date"] = from_timestamp(data.get("date"), tzinfo=loc_tzinfo)
data["user"] = User.de_json(data.get("user"), bot)
return super().de_json(data=data, bot=bot)
class BusinessMessagesDeleted(TelegramObject):
"""
This object is received when messages are deleted from a connected business account.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal if their :attr:`business_connection_id`, :attr:`message_ids`, and
:attr:`chat` are equal.
.. versionadded:: 21.1
Args:
business_connection_id (:obj:`str`): Unique identifier of the business connection.
chat (:class:`telegram.Chat`): Information about a chat in the business account. The bot
may not have access to the chat or the corresponding user.
message_ids (Sequence[:obj:`int`]): A list of identifiers of the deleted messages in the
chat of the business account.
Attributes:
business_connection_id (:obj:`str`): Unique identifier of the business connection.
chat (:class:`telegram.Chat`): Information about a chat in the business account. The bot
may not have access to the chat or the corresponding user.
message_ids (Tuple[:obj:`int`]): A list of identifiers of the deleted messages in the
chat of the business account.
"""
__slots__ = (
"business_connection_id",
"chat",
"message_ids",
)
def __init__(
self,
business_connection_id: str,
chat: Chat,
message_ids: Sequence[int],
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.business_connection_id: str = business_connection_id
self.chat: Chat = chat
self.message_ids: Tuple[int, ...] = parse_sequence_arg(message_ids)
self._id_attrs = (
self.business_connection_id,
self.chat,
self.message_ids,
)
self._freeze()
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BusinessMessagesDeleted"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
data["chat"] = Chat.de_json(data.get("chat"), bot)
return super().de_json(data=data, bot=bot)
class BusinessIntro(TelegramObject):
"""
This object represents the intro of a business account.
Objects of this class are comparable in terms of equality.
Two objects of this class are considered equal, if their
:attr:`title`, :attr:`message` and :attr:`sticker` are equal.
.. versionadded:: 21.1
Args:
title (:obj:`str`, optional): Title text of the business intro.
message (:obj:`str`, optional): Message text of the business intro.
sticker (:class:`telegram.Sticker`, optional): Sticker of the business intro.
Attributes:
title (:obj:`str`): Optional. Title text of the business intro.
message (:obj:`str`): Optional. Message text of the business intro.
sticker (:class:`telegram.Sticker`): Optional. Sticker of the business intro.
"""
__slots__ = (
"message",
"sticker",
"title",
)
def __init__(
self,
title: Optional[str] = None,
message: Optional[str] = None,
sticker: Optional[Sticker] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.title: Optional[str] = title
self.message: Optional[str] = message
self.sticker: Optional[Sticker] = sticker
self._id_attrs = (self.title, self.message, self.sticker)
self._freeze()
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BusinessIntro"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
data["sticker"] = Sticker.de_json(data.get("sticker"), bot)
return super().de_json(data=data, bot=bot)
class BusinessLocation(TelegramObject):
"""
This object represents the location of a business account.
Objects of this class are comparable in terms of equality.
Two objects of this class are considered equal, if their
:attr:`address` is equal.
.. versionadded:: 21.1
Args:
address (:obj:`str`): Address of the business.
location (:class:`telegram.Location`, optional): Location of the business.
Attributes:
address (:obj:`str`): Address of the business.
location (:class:`telegram.Location`): Optional. Location of the business.
"""
__slots__ = (
"address",
"location",
)
def __init__(
self,
address: str,
location: Optional[Location] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.address: str = address
self.location: Optional[Location] = location
self._id_attrs = (self.address,)
self._freeze()
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BusinessLocation"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
data["location"] = Location.de_json(data.get("location"), bot)
return super().de_json(data=data, bot=bot)
class BusinessOpeningHoursInterval(TelegramObject):
"""
This object represents the time intervals describing business opening hours.
Objects of this class are comparable in terms of equality.
Two objects of this class are considered equal, if their
:attr:`opening_minute` and :attr:`closing_minute` are equal.
.. versionadded:: 21.1
Examples:
A day has (24 * 60 =) 1440 minutes, a week has (7 * 1440 =) 10080 minutes.
Starting the the minute's sequence from Monday, example values of
:attr:`opening_minute`, :attr:`closing_minute` will map to the following day times:
* Monday - 8am to 8:30pm:
- ``opening_minute = 480`` :guilabel:`8 * 60`
- ``closing_minute = 1230`` :guilabel:`20 * 60 + 30`
* Tuesday - 24 hours:
- ``opening_minute = 1440`` :guilabel:`24 * 60`
- ``closing_minute = 2879`` :guilabel:`2 * 24 * 60 - 1`
* Sunday - 12am - 11:58pm:
- ``opening_minute = 8640`` :guilabel:`6 * 24 * 60`
- ``closing_minute = 10078`` :guilabel:`7 * 24 * 60 - 2`
Args:
opening_minute (:obj:`int`): The minute's sequence number in a week, starting on Monday,
marking the start of the time interval during which the business is open;
0 - 7 * 24 * 60.
closing_minute (:obj:`int`): The minute's
sequence number in a week, starting on Monday, marking the end of the time interval
during which the business is open; 0 - 8 * 24 * 60
Attributes:
opening_minute (:obj:`int`): The minute's sequence number in a week, starting on Monday,
marking the start of the time interval during which the business is open;
0 - 7 * 24 * 60.
closing_minute (:obj:`int`): The minute's
sequence number in a week, starting on Monday, marking the end of the time interval
during which the business is open; 0 - 8 * 24 * 60
"""
__slots__ = ("_closing_time", "_opening_time", "closing_minute", "opening_minute")
def __init__(
self,
opening_minute: int,
closing_minute: int,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.opening_minute: int = opening_minute
self.closing_minute: int = closing_minute
self._opening_time: Optional[Tuple[int, int, int]] = None
self._closing_time: Optional[Tuple[int, int, int]] = None
self._id_attrs = (self.opening_minute, self.closing_minute)
self._freeze()
def _parse_minute(self, minute: int) -> Tuple[int, int, int]:
return (minute // 1440, minute % 1440 // 60, minute % 1440 % 60)
@property
def opening_time(self) -> Tuple[int, int, int]:
"""Convenience attribute. A :obj:`tuple` parsed from :attr:`opening_minute`. It contains
the `weekday`, `hour` and `minute` in the same ranges as :attr:`datetime.datetime.weekday`,
:attr:`datetime.datetime.hour` and :attr:`datetime.datetime.minute`
Returns:
Tuple[:obj:`int`, :obj:`int`, :obj:`int`]:
"""
if self._opening_time is None:
self._opening_time = self._parse_minute(self.opening_minute)
return self._opening_time
@property
def closing_time(self) -> Tuple[int, int, int]:
"""Convenience attribute. A :obj:`tuple` parsed from :attr:`closing_minute`. It contains
the `weekday`, `hour` and `minute` in the same ranges as :attr:`datetime.datetime.weekday`,
:attr:`datetime.datetime.hour` and :attr:`datetime.datetime.minute`
Returns:
Tuple[:obj:`int`, :obj:`int`, :obj:`int`]:
"""
if self._closing_time is None:
self._closing_time = self._parse_minute(self.closing_minute)
return self._closing_time
class BusinessOpeningHours(TelegramObject):
"""
This object represents the opening hours of a business account.
Objects of this class are comparable in terms of equality.
Two objects of this class are considered equal, if their
:attr:`time_zone_name` and :attr:`opening_hours` are equal.
.. versionadded:: 21.1
Args:
time_zone_name (:obj:`str`): Unique name of the time zone for which the opening
hours are defined.
opening_hours (Sequence[:class:`telegram.BusinessOpeningHoursInterval`]): List of
time intervals describing business opening hours.
Attributes:
time_zone_name (:obj:`str`): Unique name of the time zone for which the opening
hours are defined.
opening_hours (Sequence[:class:`telegram.BusinessOpeningHoursInterval`]): List of
time intervals describing business opening hours.
"""
__slots__ = ("opening_hours", "time_zone_name")
def __init__(
self,
time_zone_name: str,
opening_hours: Sequence[BusinessOpeningHoursInterval],
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.time_zone_name: str = time_zone_name
self.opening_hours: Sequence[BusinessOpeningHoursInterval] = parse_sequence_arg(
opening_hours
)
self._id_attrs = (self.time_zone_name, self.opening_hours)
self._freeze()
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["BusinessOpeningHours"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
data["opening_hours"] = BusinessOpeningHoursInterval.de_list(
data.get("opening_hours"), bot
)
return super().de_json(data=data, bot=bot)
+113 -2
View File
@@ -23,6 +23,7 @@ from html import escape
from typing import TYPE_CHECKING, Final, Optional, Sequence, Tuple, Union
from telegram import constants
from telegram._birthdate import Birthdate
from telegram._chatlocation import ChatLocation
from telegram._chatpermissions import ChatPermissions
from telegram._files.chatphoto import ChatPhoto
@@ -44,6 +45,9 @@ if TYPE_CHECKING:
Animation,
Audio,
Bot,
BusinessIntro,
BusinessLocation,
BusinessOpeningHours,
ChatInviteLink,
ChatMember,
Contact,
@@ -169,6 +173,21 @@ class Chat(TelegramObject):
only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
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
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
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
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
@@ -229,6 +248,14 @@ class Chat(TelegramObject):
and bots in the group. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 21.0
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
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
Attributes:
id (:obj:`int`): Unique identifier for this chat. This number may be greater than 32 bits
@@ -312,6 +339,21 @@ class Chat(TelegramObject):
obtained via :meth:`~telegram.Bot.get_chat`.
.. versionadded:: 20.0
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
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
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
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
@@ -372,6 +414,14 @@ class Chat(TelegramObject):
and bots in the group. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 21.0
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
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
.. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups
.. _accent colors: https://core.telegram.org/bots/api#accent-colors
@@ -383,6 +433,10 @@ class Chat(TelegramObject):
"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",
@@ -405,6 +459,7 @@ class Chat(TelegramObject):
"location",
"message_auto_delete_time",
"permissions",
"personal_chat",
"photo",
"pinned_message",
"profile_accent_color_id",
@@ -470,6 +525,11 @@ class Chat(TelegramObject):
has_visible_history: Optional[bool] = None,
unrestrict_boost_count: Optional[int] = None,
custom_emoji_sticker_set_name: Optional[str] = None,
birthdate: Optional[Birthdate] = None,
personal_chat: Optional["Chat"] = None,
business_intro: Optional["BusinessIntro"] = None,
business_location: Optional["BusinessLocation"] = None,
business_opening_hours: Optional["BusinessOpeningHours"] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -519,6 +579,11 @@ class Chat(TelegramObject):
self.profile_background_custom_emoji_id: Optional[str] = profile_background_custom_emoji_id
self.unrestrict_boost_count: Optional[int] = unrestrict_boost_count
self.custom_emoji_sticker_set_name: Optional[str] = custom_emoji_sticker_set_name
self.birthdate: Optional[Birthdate] = birthdate
self.personal_chat: Optional["Chat"] = personal_chat
self.business_intro: Optional["BusinessIntro"] = business_intro
self.business_location: Optional["BusinessLocation"] = business_location
self.business_opening_hours: Optional["BusinessOpeningHours"] = business_opening_hours
self._id_attrs = (self.id,)
@@ -581,12 +646,24 @@ class Chat(TelegramObject):
)
data["photo"] = ChatPhoto.de_json(data.get("photo"), bot)
from telegram import Message # pylint: disable=import-outside-toplevel
from telegram import ( # pylint: disable=import-outside-toplevel
BusinessIntro,
BusinessLocation,
BusinessOpeningHours,
Message,
)
data["pinned_message"] = Message.de_json(data.get("pinned_message"), bot)
data["permissions"] = ChatPermissions.de_json(data.get("permissions"), bot)
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["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(
data.get("business_opening_hours"), bot
)
api_kwargs = {}
# This is a deprecated field that TG still returns for backwards compatibility
@@ -1169,7 +1246,7 @@ class Chat(TelegramObject):
async def set_administrator_custom_title(
self,
user_id: Union[int, str],
user_id: int,
custom_title: str,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1444,6 +1521,7 @@ class Chat(TelegramObject):
message_thread_id: Optional[int] = None,
link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1483,6 +1561,7 @@ class Chat(TelegramObject):
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
)
async def delete_message(
@@ -1558,6 +1637,7 @@ class Chat(TelegramObject):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1598,12 +1678,14 @@ class Chat(TelegramObject):
parse_mode=parse_mode,
caption_entities=caption_entities,
reply_parameters=reply_parameters,
business_connection_id=business_connection_id,
)
async def send_chat_action(
self,
action: str,
message_thread_id: Optional[int] = None,
business_connection_id: Optional[str] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1630,6 +1712,7 @@ class Chat(TelegramObject):
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
)
send_action = send_chat_action
@@ -1647,6 +1730,7 @@ class Chat(TelegramObject):
message_thread_id: Optional[int] = None,
has_spoiler: Optional[bool] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1687,6 +1771,7 @@ class Chat(TelegramObject):
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
has_spoiler=has_spoiler,
business_connection_id=business_connection_id,
)
async def send_contact(
@@ -1700,6 +1785,7 @@ class Chat(TelegramObject):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1739,6 +1825,7 @@ class Chat(TelegramObject):
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_audio(
@@ -1756,6 +1843,7 @@ class Chat(TelegramObject):
message_thread_id: Optional[int] = None,
thumbnail: Optional[FileInput] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1799,6 +1887,7 @@ class Chat(TelegramObject):
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
thumbnail=thumbnail,
business_connection_id=business_connection_id,
)
async def send_document(
@@ -1814,6 +1903,7 @@ class Chat(TelegramObject):
message_thread_id: Optional[int] = None,
thumbnail: Optional[FileInput] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1855,6 +1945,7 @@ class Chat(TelegramObject):
caption_entities=caption_entities,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_dice(
@@ -1865,6 +1956,7 @@ class Chat(TelegramObject):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1899,6 +1991,7 @@ class Chat(TelegramObject):
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_game(
@@ -1909,6 +2002,7 @@ class Chat(TelegramObject):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1943,6 +2037,7 @@ class Chat(TelegramObject):
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_invoice(
@@ -2052,6 +2147,7 @@ class Chat(TelegramObject):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2093,6 +2189,7 @@ class Chat(TelegramObject):
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_animation(
@@ -2111,6 +2208,7 @@ class Chat(TelegramObject):
has_spoiler: Optional[bool] = None,
thumbnail: Optional[FileInput] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2155,6 +2253,7 @@ class Chat(TelegramObject):
message_thread_id=message_thread_id,
has_spoiler=has_spoiler,
thumbnail=thumbnail,
business_connection_id=business_connection_id,
)
async def send_sticker(
@@ -2166,6 +2265,7 @@ class Chat(TelegramObject):
message_thread_id: Optional[int] = None,
emoji: Optional[str] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2201,6 +2301,7 @@ class Chat(TelegramObject):
protect_content=protect_content,
message_thread_id=message_thread_id,
emoji=emoji,
business_connection_id=business_connection_id,
)
async def send_venue(
@@ -2218,6 +2319,7 @@ class Chat(TelegramObject):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2261,6 +2363,7 @@ class Chat(TelegramObject):
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_video(
@@ -2280,6 +2383,7 @@ class Chat(TelegramObject):
has_spoiler: Optional[bool] = None,
thumbnail: Optional[FileInput] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2325,6 +2429,7 @@ class Chat(TelegramObject):
protect_content=protect_content,
message_thread_id=message_thread_id,
has_spoiler=has_spoiler,
business_connection_id=business_connection_id,
)
async def send_video_note(
@@ -2338,6 +2443,7 @@ class Chat(TelegramObject):
message_thread_id: Optional[int] = None,
thumbnail: Optional[FileInput] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2377,6 +2483,7 @@ class Chat(TelegramObject):
filename=filename,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_voice(
@@ -2391,6 +2498,7 @@ class Chat(TelegramObject):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2431,6 +2539,7 @@ class Chat(TelegramObject):
filename=filename,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_poll(
@@ -2452,6 +2561,7 @@ class Chat(TelegramObject):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2497,6 +2607,7 @@ class Chat(TelegramObject):
explanation_entities=explanation_entities,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_copy(
+8 -9
View File
@@ -44,6 +44,11 @@ class ChatAdministratorRights(TelegramObject):
:attr:`can_post_stories`, :attr:`can_edit_stories`, and :attr:`can_delete_stories` are
considered as well when comparing objects of this type in terms of equality.
.. versionchanged:: 21.1
As of this version, :attr:`can_post_stories`, :attr:`can_edit_stories`,
and :attr:`can_delete_stories` is now required. Thus, the order of arguments had to be
changed.
Args:
is_anonymous (:obj:`bool`): :obj:`True`, if the user's presence in the chat is hidden.
can_manage_chat (:obj:`bool`): :obj:`True`, if the administrator can access the chat event
@@ -169,13 +174,13 @@ class ChatAdministratorRights(TelegramObject):
can_promote_members: bool,
can_change_info: bool,
can_invite_users: bool,
can_post_stories: bool,
can_edit_stories: bool,
can_delete_stories: bool,
can_post_messages: Optional[bool] = None,
can_edit_messages: Optional[bool] = None,
can_pin_messages: Optional[bool] = None,
can_manage_topics: Optional[bool] = None,
can_post_stories: Optional[bool] = None,
can_edit_stories: Optional[bool] = None,
can_delete_stories: Optional[bool] = None,
*,
api_kwargs: Optional[JSONDict] = None,
) -> None:
@@ -189,12 +194,6 @@ class ChatAdministratorRights(TelegramObject):
self.can_promote_members: bool = can_promote_members
self.can_change_info: bool = can_change_info
self.can_invite_users: bool = can_invite_users
# Not actually optionals but because of backwards compatability we pretend they are
if can_post_stories is None or can_edit_stories is None or can_delete_stories is None:
raise TypeError(
"As of v21.0 can_post_stories, can_edit_stories and can_delete_stories"
" must be set in order to create this object."
)
self.can_post_stories: bool = can_post_stories
self.can_edit_stories: bool = can_edit_stories
self.can_delete_stories: bool = can_delete_stories
+8 -9
View File
@@ -191,6 +191,11 @@ class ChatMemberAdministrator(ChatMember):
* The argument :paramref:`can_manage_topics` was added, which changes the position of the
optional argument :paramref:`custom_title`.
.. versionchanged:: 21.1
As of this version, :attr:`can_post_stories`, :attr:`can_edit_stories`,
and :attr:`can_delete_stories` is now required. Thus, the order of arguments had to be
changed.
Args:
user (:class:`telegram.User`): Information about the user.
can_be_edited (:obj:`bool`): :obj:`True`, if the bot
@@ -340,14 +345,14 @@ class ChatMemberAdministrator(ChatMember):
can_promote_members: bool,
can_change_info: bool,
can_invite_users: bool,
can_post_stories: bool,
can_edit_stories: bool,
can_delete_stories: bool,
can_post_messages: Optional[bool] = None,
can_edit_messages: Optional[bool] = None,
can_pin_messages: Optional[bool] = None,
can_manage_topics: Optional[bool] = None,
custom_title: Optional[str] = None,
can_post_stories: Optional[bool] = None,
can_edit_stories: Optional[bool] = None,
can_delete_stories: Optional[bool] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -362,12 +367,6 @@ class ChatMemberAdministrator(ChatMember):
self.can_promote_members: bool = can_promote_members
self.can_change_info: bool = can_change_info
self.can_invite_users: bool = can_invite_users
# Not actually optionals but because of backwards compatability we pretend they are
if can_post_stories is None or can_edit_stories is None or can_delete_stories is None:
raise TypeError(
"As of 21.0 can_post_stories, can_edit_stories and can_delete_stories "
"must be set in order to create this object."
)
self.can_post_stories: bool = can_post_stories
self.can_edit_stories: bool = can_edit_stories
self.can_delete_stories: bool = can_delete_stories
+21 -1
View File
@@ -36,6 +36,10 @@ class InputSticker(TelegramObject):
.. versionadded:: 20.2
.. versionchanged:: 21.1
As of Bot API 7.2, the new argument :paramref:`format` is a required argument, and thus the
order of the arguments has changed.
Args:
sticker (:obj:`str` | :term:`file object` | :obj:`bytes` | :class:`pathlib.Path`): The
added sticker. |uploadinputnopath| Animated and video stickers can't be uploaded via
@@ -52,6 +56,13 @@ class InputSticker(TelegramObject):
:tg-const:`telegram.constants.StickerLimit.MAX_KEYWORD_LENGTH` characters. For
":tg-const:`telegram.constants.StickerType.REGULAR`" and
":tg-const:`telegram.constants.StickerType.CUSTOM_EMOJI`" stickers only.
format (:obj:`str`): Format of the added sticker, must be one of
:tg-const:`telegram.constants.StickerFormat.STATIC` for a
``.WEBP`` or ``.PNG`` image, :tg-const:`telegram.constants.StickerFormat.ANIMATED`
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a WEBM
video.
.. versionadded:: 21.1
Attributes:
sticker (:obj:`str` | :class:`telegram.InputFile`): The added sticker.
@@ -67,15 +78,23 @@ class InputSticker(TelegramObject):
:tg-const:`telegram.constants.StickerLimit.MAX_KEYWORD_LENGTH` characters. For
":tg-const:`telegram.constants.StickerType.REGULAR`" and
":tg-const:`telegram.constants.StickerType.CUSTOM_EMOJI`" stickers only.
":tg-const:`telegram.constants.StickerType.CUSTOM_EMOJI`" stickers only.
format (:obj:`str`): Format of the added sticker, must be one of
:tg-const:`telegram.constants.StickerFormat.STATIC` for a
``.WEBP`` or ``.PNG`` image, :tg-const:`telegram.constants.StickerFormat.ANIMATED`
for a ``.TGS`` animation, :tg-const:`telegram.constants.StickerFormat.VIDEO` for a WEBM
video.
.. versionadded:: 21.1
"""
__slots__ = ("emoji_list", "keywords", "mask_position", "sticker")
__slots__ = ("emoji_list", "format", "keywords", "mask_position", "sticker")
def __init__(
self,
sticker: FileInput,
emoji_list: Sequence[str],
format: str, # pylint: disable=redefined-builtin
mask_position: Optional[MaskPosition] = None,
keywords: Optional[Sequence[str]] = None,
*,
@@ -91,6 +110,7 @@ class InputSticker(TelegramObject):
attach=True,
)
self.emoji_list: Tuple[str, ...] = parse_sequence_arg(emoji_list)
self.format: str = format
self.mask_position: Optional[MaskPosition] = mask_position
self.keywords: Tuple[str, ...] = parse_sequence_arg(keywords)
+34 -7
View File
@@ -27,6 +27,8 @@ from telegram._telegramobject import TelegramObject
from telegram._utils import enum
from telegram._utils.argumentparsing import parse_sequence_arg
from telegram._utils.types import JSONDict
from telegram._utils.warnings import warn
from telegram.warnings import PTBDeprecationWarning
if TYPE_CHECKING:
from telegram import Bot
@@ -227,6 +229,11 @@ class StickerSet(TelegramObject):
.. versionchanged:: 20.0
The parameter ``contains_masks`` has been removed. Use :paramref:`sticker_type` instead.
.. versionchanged:: 21.1
The parameters ``is_video`` and ``is_animated`` are deprecated and now made optional. Thus,
the order of the arguments had to be changed.
.. versionchanged:: 20.5
|removed_thumb_note|
@@ -234,9 +241,16 @@ class StickerSet(TelegramObject):
name (:obj:`str`): Sticker set name.
title (:obj:`str`): Sticker set title.
is_animated (:obj:`bool`): :obj:`True`, if the sticker set contains animated stickers.
is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers.
.. deprecated:: 21.1
Bot API 7.2 deprecated this field. This parameter will be removed in a future
version of the library.
is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers.
.. versionadded:: 13.11
.. deprecated:: 21.1
Bot API 7.2 deprecated this field. This parameter will be removed in a future
version of the library.
stickers (Sequence[:class:`telegram.Sticker`]): List of all set stickers.
.. versionchanged:: 20.0
@@ -256,9 +270,16 @@ class StickerSet(TelegramObject):
name (:obj:`str`): Sticker set name.
title (:obj:`str`): Sticker set title.
is_animated (:obj:`bool`): :obj:`True`, if the sticker set contains animated stickers.
is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers.
.. deprecated:: 21.1
Bot API 7.2 deprecated this field. This parameter will be removed in a future
version of the library.
is_video (:obj:`bool`): :obj:`True`, if the sticker set contains video stickers.
.. versionadded:: 13.11
.. deprecated:: 21.1
Bot API 7.2 deprecated this field. This parameter will be removed in a future
version of the library.
stickers (Tuple[:class:`telegram.Sticker`]): List of all set stickers.
.. versionchanged:: 20.0
@@ -289,10 +310,10 @@ class StickerSet(TelegramObject):
self,
name: str,
title: str,
is_animated: bool,
stickers: Sequence[Sticker],
is_video: bool,
sticker_type: str,
is_animated: Optional[bool] = None,
is_video: Optional[bool] = None,
thumbnail: Optional[PhotoSize] = None,
*,
api_kwargs: Optional[JSONDict] = None,
@@ -300,13 +321,19 @@ class StickerSet(TelegramObject):
super().__init__(api_kwargs=api_kwargs)
self.name: str = name
self.title: str = title
self.is_animated: bool = is_animated
self.is_video: bool = is_video
self.stickers: Tuple[Sticker, ...] = parse_sequence_arg(stickers)
self.sticker_type: str = sticker_type
# Optional
self.thumbnail: Optional[PhotoSize] = thumbnail
if is_animated is not None or is_video is not None:
warn(
"The parameters `is_animated` and `is_video` are deprecated and will be removed "
"in a future version.",
PTBDeprecationWarning,
stacklevel=2,
)
self.is_animated: Optional[bool] = is_animated
self.is_video: Optional[bool] = is_video
self._id_attrs = (self.name,)
self._freeze()
+59 -1
View File
@@ -17,6 +17,7 @@
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains two objects to request chats/users."""
from typing import TYPE_CHECKING, Optional
from telegram._chatadministratorrights import ChatAdministratorRights
@@ -56,6 +57,16 @@ class KeyboardButtonRequestUsers(TelegramObject):
.
.. versionadded:: 20.8
request_name (:obj:`bool`, optional): Pass :obj:`True` to request the users' first and last
name.
.. versionadded:: 21.1
request_username (:obj:`bool`, optional): Pass :obj:`True` to request the users' username.
.. versionadded:: 21.1
request_photo (:obj:`bool`, optional): Pass :obj:`True` to request the users' photo.
.. versionadded:: 21.1
Attributes:
request_id (:obj:`int`): Identifier of the request.
@@ -71,11 +82,25 @@ class KeyboardButtonRequestUsers(TelegramObject):
.
.. versionadded:: 20.8
request_name (:obj:`bool`): Optional. Pass :obj:`True` to request the users' first and last
name.
.. versionadded:: 21.1
request_username (:obj:`bool`): Optional. Pass :obj:`True` to request the users' username.
.. versionadded:: 21.1
request_photo (:obj:`bool`): Optional. Pass :obj:`True` to request the users' photo.
.. versionadded:: 21.1
"""
__slots__ = (
"max_quantity",
"request_id",
"request_name",
"request_photo",
"request_username",
"user_is_bot",
"user_is_premium",
)
@@ -86,6 +111,9 @@ class KeyboardButtonRequestUsers(TelegramObject):
user_is_bot: Optional[bool] = None,
user_is_premium: Optional[bool] = None,
max_quantity: Optional[int] = None,
request_name: Optional[bool] = None,
request_username: Optional[bool] = None,
request_photo: Optional[bool] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -97,6 +125,9 @@ class KeyboardButtonRequestUsers(TelegramObject):
self.user_is_bot: Optional[bool] = user_is_bot
self.user_is_premium: Optional[bool] = user_is_premium
self.max_quantity: Optional[int] = max_quantity
self.request_name: Optional[bool] = request_name
self.request_username: Optional[bool] = request_username
self.request_photo: Optional[bool] = request_photo
self._id_attrs = (self.request_id,)
@@ -138,6 +169,15 @@ class KeyboardButtonRequestChat(TelegramObject):
applied.
bot_is_member (:obj:`bool`, optional): Pass :obj:`True` to request a chat with the bot
as a member. Otherwise, no additional restrictions are applied.
request_title (:obj:`bool`, optional): Pass :obj:`True` to request the chat's title.
.. versionadded:: 21.1
request_username (:obj:`bool`, optional): Pass :obj:`True` to request the chat's username.
.. versionadded:: 21.1
request_photo (:obj:`bool`, optional): Pass :obj:`True` to request the chat's photo.
.. versionadded:: 21.1
Attributes:
request_id (:obj:`int`): Identifier of the request.
chat_is_channel (:obj:`bool`): Pass :obj:`True` to request a channel chat, pass
@@ -145,7 +185,7 @@ class KeyboardButtonRequestChat(TelegramObject):
chat_is_forum (:obj:`bool`): Optional. Pass :obj:`True` to request a forum supergroup, pass
:obj:`False` to request a non-forum chat. If not specified, no additional
restrictions are applied.
chat_has_username (:obj:`bool`, optional): Pass :obj:`True` to request a supergroup or a
chat_has_username (:obj:`bool`): Optional. Pass :obj:`True` to request a supergroup or a
channel with a username, pass :obj:`False` to request a chat without a username. If
not specified, no additional restrictions are applied.
chat_is_created (:obj:`bool`) Optional. Pass :obj:`True` to request a chat owned by the
@@ -159,6 +199,15 @@ class KeyboardButtonRequestChat(TelegramObject):
applied.
bot_is_member (:obj:`bool`) Optional. Pass :obj:`True` to request a chat with the bot
as a member. Otherwise, no additional restrictions are applied.
request_title (:obj:`bool`): Optional. Pass :obj:`True` to request the chat's title.
.. versionadded:: 21.1
request_username (:obj:`bool`): Optional. Pass :obj:`True` to request the chat's username.
.. versionadded:: 21.1
request_photo (:obj:`bool`): Optional. Pass :obj:`True` to request the chat's photo.
.. versionadded:: 21.1
"""
__slots__ = (
@@ -169,6 +218,9 @@ class KeyboardButtonRequestChat(TelegramObject):
"chat_is_created",
"chat_is_forum",
"request_id",
"request_photo",
"request_title",
"request_username",
"user_administrator_rights",
)
@@ -182,6 +234,9 @@ class KeyboardButtonRequestChat(TelegramObject):
user_administrator_rights: Optional[ChatAdministratorRights] = None,
bot_administrator_rights: Optional[ChatAdministratorRights] = None,
bot_is_member: Optional[bool] = None,
request_title: Optional[bool] = None,
request_username: Optional[bool] = None,
request_photo: Optional[bool] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -199,6 +254,9 @@ class KeyboardButtonRequestChat(TelegramObject):
)
self.bot_administrator_rights: Optional[ChatAdministratorRights] = bot_administrator_rights
self.bot_is_member: Optional[bool] = bot_is_member
self.request_title: Optional[bool] = request_title
self.request_username: Optional[bool] = request_username
self.request_photo: Optional[bool] = request_photo
self._id_attrs = (self.request_id,)
+333 -43
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -153,15 +153,15 @@ class EncryptedCredentials(TelegramObject):
self._id_attrs = (self.data, self.hash, self.secret)
self._decrypted_secret: Optional[str] = None
self._decrypted_secret: Optional[bytes] = None
self._decrypted_data: Optional[Credentials] = None
self._freeze()
@property
def decrypted_secret(self) -> str:
def decrypted_secret(self) -> bytes:
"""
:obj:`str`: Lazily decrypt and return secret.
:obj:`bytes`: Lazily decrypt and return secret.
Raises:
telegram.error.PassportDecryptionError: Decryption failed. Usually due to bad
@@ -60,8 +60,8 @@ class EncryptedPassportElement(TelegramObject):
email (:obj:`str`, optional): User's verified email address; available only for "email"
type.
files (Sequence[:class:`telegram.PassportFile`], optional): Array of encrypted/decrypted
files with documents provided by the user; available only for "utility_bill",
"bank_statement", "rental_agreement", "passport_registration" and
files with documents provided by the user; available only for "utility_bill",
"bank_statement", "rental_agreement", "passport_registration" and
"temporary_registration" types.
.. versionchanged:: 20.0
@@ -74,12 +74,12 @@ class EncryptedPassportElement(TelegramObject):
reverse side of the document, provided by the user; Available only for
"driver_license" and "identity_card".
selfie (:class:`telegram.PassportFile`, optional): Encrypted/decrypted file with the
selfie of the user holding a document, provided by the user; available if requested for
selfie of the user holding a document, provided by the user; available if requested for
"passport", "driver_license", "identity_card" and "internal_passport".
translation (Sequence[:class:`telegram.PassportFile`], optional): Array of
encrypted/decrypted files with translated versions of documents provided by the user;
available if requested requested for "passport", "driver_license", "identity_card",
"internal_passport", "utility_bill", "bank_statement", "rental_agreement",
encrypted/decrypted files with translated versions of documents provided by the user;
available if requested requested for "passport", "driver_license", "identity_card",
"internal_passport", "utility_bill", "bank_statement", "rental_agreement",
"passport_registration" and "temporary_registration" types.
.. versionchanged:: 20.0
@@ -101,8 +101,8 @@ class EncryptedPassportElement(TelegramObject):
email (:obj:`str`): Optional. User's verified email address; available only for "email"
type.
files (Tuple[:class:`telegram.PassportFile`]): Optional. Array of encrypted/decrypted
files with documents provided by the user; available only for "utility_bill",
"bank_statement", "rental_agreement", "passport_registration" and
files with documents provided by the user; available only for "utility_bill",
"bank_statement", "rental_agreement", "passport_registration" and
"temporary_registration" types.
.. versionchanged:: 20.0
+2 -1
View File
@@ -203,5 +203,6 @@ class PassportFile(TelegramObject):
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
)
file.set_credentials(self._credentials)
if self._credentials:
file.set_credentials(self._credentials)
return file
+217 -19
View File
@@ -17,10 +17,21 @@
# 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 two objects used for request chats/users service messages."""
from typing import Optional, Sequence, Tuple
from typing import TYPE_CHECKING, Optional, Sequence, Tuple
from telegram._files.photosize import PhotoSize
from telegram._telegramobject import TelegramObject
from telegram._utils.argumentparsing import parse_sequence_arg
from telegram._utils.types import JSONDict
from telegram._utils.warnings import warn
from telegram._utils.warnings_transition import (
build_deprecation_warning_message,
warn_about_deprecated_attr_in_property,
)
from telegram.warnings import PTBDeprecationWarning
if TYPE_CHECKING:
from telegram._bot import Bot
class UsersShared(TelegramObject):
@@ -29,48 +40,118 @@ class UsersShared(TelegramObject):
using a :class:`telegram.KeyboardButtonRequestUsers` button.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`request_id` and :attr:`user_ids` are equal.
considered equal, if their :attr:`request_id` and :attr:`users` are equal.
.. versionadded:: 20.8
Bot API 7.0 replaces ``UserShared`` with this class. The only difference is that now
the :attr:`user_ids` is a sequence instead of a single integer.
.. versionchanged:: 21.1
The argument :attr:`users` is now considered for the equality comparison instead of
:attr:`user_ids`.
Args:
request_id (:obj:`int`): Identifier of the request.
user_ids (Sequence[:obj:`int`]): Identifiers of the shared users. These numbers may have
more than 32 significant bits and some programming languages may have difficulty/silent
defects in interpreting them. But they have at most 52 significant bits, so 64-bit
integers or double-precision float types are safe for storing these identifiers. The
bot may not have access to the users and could be unable to use these identifiers,
unless the users are already known to the bot by some other means.
users (Sequence[:class:`telegram.SharedUser`]): Information about users shared with the
bot.
.. versionadded:: 21.1
.. deprecated:: 21.1
In future versions, this argument will become keyword only.
user_ids (Sequence[:obj:`int`], optional): Identifiers of the shared users. These numbers
may have more than 32 significant bits and some programming languages may have
difficulty/silent defects in interpreting them. But they have at most 52 significant
bits, so 64-bit integers or double-precision float types are safe for storing these
identifiers. The bot may not have access to the users and could be unable to use
these identifiers, unless the users are already known to the bot by some other means.
.. deprecated:: 21.1
Bot API 7.2 introduced by :paramref:`users`, replacing this argument. Hence, this
argument is now optional and will be removed in future versions.
Attributes:
request_id (:obj:`int`): Identifier of the request.
user_ids (Tuple[:obj:`int`]): Identifiers of the shared users. These numbers may have
more than 32 significant bits and some programming languages may have difficulty/silent
defects in interpreting them. But they have at most 52 significant bits, so 64-bit
integers or double-precision float types are safe for storing these identifiers. The
bot may not have access to the users and could be unable to use these identifiers,
unless the users are already known to the bot by some other means.
users (Tuple[:class:`telegram.SharedUser`]): Information about users shared with the
bot.
.. versionadded:: 21.1
"""
__slots__ = ("request_id", "user_ids")
__slots__ = ("request_id", "users")
def __init__(
self,
request_id: int,
user_ids: Sequence[int],
user_ids: Optional[Sequence[int]] = None,
users: Optional[Sequence["SharedUser"]] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.request_id: int = request_id
self.user_ids: Tuple[int, ...] = tuple(user_ids)
self._id_attrs = (self.request_id, self.user_ids)
if users is None:
raise TypeError("`users` is a required argument since Bot API 7.2")
self.users: Tuple[SharedUser, ...] = parse_sequence_arg(users)
if user_ids is not None:
warn(
build_deprecation_warning_message(
deprecated_name="user_ids",
new_name="users",
object_type="parameter",
bot_api_version="7.2",
),
PTBDeprecationWarning,
stacklevel=2,
)
self._id_attrs = (self.request_id, self.users)
self._freeze()
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["UsersShared"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
data["users"] = SharedUser.de_list(data.get("users"), bot)
api_kwargs = {}
# This is a deprecated field that TG still returns for backwards compatibility
# Let's filter it out to speed up the de-json process
if user_ids := data.get("user_ids"):
api_kwargs = {"user_ids": user_ids}
return super()._de_json(data=data, bot=bot, api_kwargs=api_kwargs)
@property
def user_ids(self) -> Tuple[int, ...]:
"""
Tuple[:obj:`int`]: Identifiers of the shared users. These numbers may have
more than 32 significant bits and some programming languages may have difficulty/silent
defects in interpreting them. But they have at most 52 significant bits, so 64-bit
integers or double-precision float types are safe for storing these identifiers. The
bot may not have access to the users and could be unable to use these identifiers,
unless the users are already known to the bot by some other means.
.. deprecated:: 21.1
As Bot API 7.2 replaces this attribute with :attr:`users`, this attribute will be
removed in future versions.
"""
warn_about_deprecated_attr_in_property(
deprecated_attr_name="user_ids",
new_attr_name="users",
bot_api_version="7.2",
stacklevel=2,
)
return tuple(user.user_id for user in self.users)
class ChatShared(TelegramObject):
"""
@@ -88,6 +169,17 @@ class ChatShared(TelegramObject):
bits and some programming languages may have difficulty/silent defects in interpreting
it. But it is smaller than 52 bits, so a signed 64-bit integer or double-precision
float type are safe for storing this identifier.
title (:obj:`str`, optional): Title of the chat, if the title was requested by the bot.
.. versionadded:: 21.1
username (:obj:`str`, optional): Username of the chat, if the username was requested by
the bot and available.
.. versionadded:: 21.1
photo (Sequence[:class:`telegram.PhotoSize`], optional): Available sizes of the chat photo,
if the photo was requested by the bot
.. versionadded:: 21.1
Attributes:
request_id (:obj:`int`): Identifier of the request.
@@ -95,21 +187,127 @@ class ChatShared(TelegramObject):
bits and some programming languages may have difficulty/silent defects in interpreting
it. But it is smaller than 52 bits, so a signed 64-bit integer or double-precision
float type are safe for storing this identifier.
title (:obj:`str`): Optional. Title of the chat, if the title was requested by the bot.
.. versionadded:: 21.1
username (:obj:`str`): Optional. Username of the chat, if the username was requested by
the bot and available.
.. versionadded:: 21.1
photo (Tuple[:class:`telegram.PhotoSize`]): Optional. Available sizes of the chat photo,
if the photo was requested by the bot
.. versionadded:: 21.1
"""
__slots__ = ("chat_id", "request_id")
__slots__ = ("chat_id", "photo", "request_id", "title", "username")
def __init__(
self,
request_id: int,
chat_id: int,
title: Optional[str] = None,
username: Optional[str] = None,
photo: Optional[Sequence[PhotoSize]] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.request_id: int = request_id
self.chat_id: int = chat_id
self.title: Optional[str] = title
self.username: Optional[str] = username
self.photo: Optional[Tuple[PhotoSize, ...]] = parse_sequence_arg(photo)
self._id_attrs = (self.request_id, self.chat_id)
self._freeze()
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["ChatShared"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
data["photo"] = PhotoSize.de_list(data.get("photo"), bot)
return super().de_json(data=data, bot=bot)
class SharedUser(TelegramObject):
"""
This object contains information about a user that was shared with the bot using a
:class:`telegram.KeyboardButtonRequestUsers` button.
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`user_id` is equal.
.. versionadded:: 21.1
Args:
user_id (:obj:`int`): Identifier of the shared user. This number may have 32 significant
bits and some programming languages may have difficulty/silent defects in interpreting
it. But it has atmost 52 significant bits, so 64-bit integers or double-precision
float types are safe for storing these identifiers. The bot may not have access to the
user and could be unable to use this identifier, unless the user is already known to
the bot by some other means.
first_name (:obj:`str`, optional): First name of the user, if the name was requested by the
bot.
last_name (:obj:`str`, optional): Last name of the user, if the name was requested by the
bot.
username (:obj:`str`, optional): Username of the user, if the username was requested by the
bot.
photo (Sequence[:class:`telegram.PhotoSize`], optional): Available sizes of the chat photo,
if the photo was requested by the bot.
Attributes:
user_id (:obj:`int`): Identifier of the shared user. This number may have 32 significant
bits and some programming languages may have difficulty/silent defects in interpreting
it. But it has atmost 52 significant bits, so 64-bit integers or double-precision
float types are safe for storing these identifiers. The bot may not have access to the
user and could be unable to use this identifier, unless the user is already known to
the bot by some other means.
first_name (:obj:`str`): Optional. First name of the user, if the name was requested by the
bot.
last_name (:obj:`str`): Optional. Last name of the user, if the name was requested by the
bot.
username (:obj:`str`): Optional. Username of the user, if the username was requested by the
bot.
photo (Tuple[:class:`telegram.PhotoSize`]): Available sizes of the chat photo, if
the photo was requested by the bot. This list is empty if the photo was not requsted.
"""
__slots__ = ("first_name", "last_name", "photo", "user_id", "username")
def __init__(
self,
user_id: int,
first_name: Optional[str] = None,
last_name: Optional[str] = None,
username: Optional[str] = None,
photo: Optional[Sequence[PhotoSize]] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
super().__init__(api_kwargs=api_kwargs)
self.user_id: int = user_id
self.first_name: Optional[str] = first_name
self.last_name: Optional[str] = last_name
self.username: Optional[str] = username
self.photo: Optional[Tuple[PhotoSize, ...]] = parse_sequence_arg(photo)
self._id_attrs = (self.user_id,)
self._freeze()
@classmethod
def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["SharedUser"]:
"""See :meth:`telegram.TelegramObject.de_json`."""
data = cls._parse_data(data)
if not data:
return None
data["photo"] = PhotoSize.de_list(data.get("photo"), bot)
return super().de_json(data=data, bot=bot)
+182 -3
View File
@@ -18,9 +18,10 @@
# along with this program. If not, see [http://www.gnu.org/licenses/].
"""This module contains an object that represents a Telegram Update."""
from typing import TYPE_CHECKING, Final, List, Optional
from typing import TYPE_CHECKING, Final, List, Optional, Union
from telegram import constants
from telegram._business import BusinessConnection, BusinessMessagesDeleted
from telegram._callbackquery import CallbackQuery
from telegram._chatboost import ChatBoostRemoved, ChatBoostUpdated
from telegram._chatjoinrequest import ChatJoinRequest
@@ -134,6 +135,28 @@ class Update(TelegramObject):
.. versionadded:: 20.8
business_connection (:class:`telegram.BusinessConnection`, optional): The bot was connected
to or disconnected from a business account, or a user edited an existing connection
with the bot.
.. versionadded:: 21.1
business_message (:class:`telegram.Message`, optional): New non-service message
from a connected business account.
.. versionadded:: 21.1
edited_business_message (:class:`telegram.Message`, optional): New version of a message
from a connected business account.
.. versionadded:: 21.1
deleted_business_messages (:class:`telegram.BusinessMessagesDeleted`, optional): Messages
were deleted from a connected business account.
.. versionadded:: 21.1
Attributes:
update_id (:obj:`int`): The update's unique identifier. Update identifiers start from a
certain positive number and increase sequentially. This ID becomes especially handy if
@@ -219,18 +242,44 @@ class Update(TelegramObject):
with delay up to a few minutes.
.. versionadded:: 20.8
business_connection (:class:`telegram.BusinessConnection`): Optional. The bot was connected
to or disconnected from a business account, or a user edited an existing connection
with the bot.
.. versionadded:: 21.1
business_message (:class:`telegram.Message`): Optional. New non-service message
from a connected business account.
.. versionadded:: 21.1
edited_business_message (:class:`telegram.Message`): Optional. New version of a message
from a connected business account.
.. versionadded:: 21.1
deleted_business_messages (:class:`telegram.BusinessMessagesDeleted`): Optional. Messages
were deleted from a connected business account.
.. versionadded:: 21.1
"""
__slots__ = (
"_effective_chat",
"_effective_message",
"_effective_sender",
"_effective_user",
"business_connection",
"business_message",
"callback_query",
"channel_post",
"chat_boost",
"chat_join_request",
"chat_member",
"chosen_inline_result",
"deleted_business_messages",
"edited_business_message",
"edited_channel_post",
"edited_message",
"inline_query",
@@ -318,6 +367,22 @@ class Update(TelegramObject):
""":const:`telegram.constants.UpdateType.MESSAGE_REACTION_COUNT`
.. versionadded:: 20.8"""
BUSINESS_CONNECTION: Final[str] = constants.UpdateType.BUSINESS_CONNECTION
""":const:`telegram.constants.UpdateType.BUSINESS_CONNECTION`
.. versionadded:: 21.1"""
BUSINESS_MESSAGE: Final[str] = constants.UpdateType.BUSINESS_MESSAGE
""":const:`telegram.constants.UpdateType.BUSINESS_MESSAGE`
.. versionadded:: 21.1"""
EDITED_BUSINESS_MESSAGE: Final[str] = constants.UpdateType.EDITED_BUSINESS_MESSAGE
""":const:`telegram.constants.UpdateType.EDITED_BUSINESS_MESSAGE`
.. versionadded:: 21.1"""
DELETED_BUSINESS_MESSAGES: Final[str] = constants.UpdateType.DELETED_BUSINESS_MESSAGES
""":const:`telegram.constants.UpdateType.DELETED_BUSINESS_MESSAGES`
.. versionadded:: 21.1"""
ALL_TYPES: Final[List[str]] = list(constants.UpdateType)
"""List[:obj:`str`]: A list of all available update types.
@@ -344,6 +409,10 @@ class Update(TelegramObject):
removed_chat_boost: Optional[ChatBoostRemoved] = None,
message_reaction: Optional[MessageReactionUpdated] = None,
message_reaction_count: Optional[MessageReactionCountUpdated] = None,
business_connection: Optional[BusinessConnection] = None,
business_message: Optional[Message] = None,
edited_business_message: Optional[Message] = None,
deleted_business_messages: Optional[BusinessMessagesDeleted] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -369,8 +438,15 @@ class Update(TelegramObject):
self.removed_chat_boost: Optional[ChatBoostRemoved] = removed_chat_boost
self.message_reaction: Optional[MessageReactionUpdated] = message_reaction
self.message_reaction_count: Optional[MessageReactionCountUpdated] = message_reaction_count
self.business_connection: Optional[BusinessConnection] = business_connection
self.business_message: Optional[Message] = business_message
self.edited_business_message: Optional[Message] = edited_business_message
self.deleted_business_messages: Optional[BusinessMessagesDeleted] = (
deleted_business_messages
)
self._effective_user: Optional[User] = None
self._effective_sender: Optional[Union["User", "Chat"]] = None
self._effective_chat: Optional[Chat] = None
self._effective_message: Optional[Message] = None
@@ -391,9 +467,14 @@ class Update(TelegramObject):
* :attr:`chat_boost`
* :attr:`removed_chat_boost`
* :attr:`message_reaction_count`
* :attr:`deleted_business_messages`
is present.
.. versionchanged:: 21.1
This property now also considers :attr:`business_connection`, :attr:`business_message`
and :attr:`edited_business_message`.
Example:
* If :attr:`message` is present, this will give
:attr:`telegram.Message.from_user`.
@@ -441,9 +522,76 @@ class Update(TelegramObject):
elif self.message_reaction:
user = self.message_reaction.user
elif self.business_message:
user = self.business_message.from_user
elif self.edited_business_message:
user = self.edited_business_message.from_user
elif self.business_connection:
user = self.business_connection.user
self._effective_user = user
return user
@property
def effective_sender(self) -> Optional[Union["User", "Chat"]]:
"""
:class:`telegram.User` or :class:`telegram.Chat`: The user or chat that sent this update,
no matter what kind of update this is.
Note:
* Depending on the type of update and the user's 'Remain anonymous' setting, this
could either be :class:`telegram.User`, :class:`telegram.Chat` or :obj:`None`.
If no user whatsoever is associated with this update, this gives :obj:`None`. This
is the case if any of
* :attr:`poll`
* :attr:`chat_boost`
* :attr:`removed_chat_boost`
* :attr:`message_reaction_count`
* :attr:`deleted_business_messages`
is present.
Example:
* If :attr:`message` is present, this will give either
:attr:`telegram.Message.from_user` or :attr:`telegram.Message.sender_chat`.
* If :attr:`poll_answer` is present, this will give either
:attr:`telegram.PollAnswer.user` or :attr:`telegram.PollAnswer.voter_chat`.
* If :attr:`channel_post` is present, this will give
:attr:`telegram.Message.sender_chat`.
.. versionadded:: 21.1
"""
if self._effective_sender:
return self._effective_sender
sender: Optional[Union["User", "Chat"]] = None
if message := (
self.message
or self.edited_message
or self.channel_post
or self.edited_channel_post
or self.business_message
or self.edited_business_message
):
sender = message.sender_chat
elif self.poll_answer:
sender = self.poll_answer.voter_chat
elif self.message_reaction:
sender = self.message_reaction.actor_chat
if sender is None:
sender = self.effective_user
self._effective_sender = sender
return sender
@property
def effective_chat(self) -> Optional["Chat"]:
"""
@@ -452,8 +600,12 @@ class Update(TelegramObject):
If no chat is associated with this update, this gives :obj:`None`.
This is the case, if :attr:`inline_query`,
:attr:`chosen_inline_result`, :attr:`callback_query` from inline messages,
:attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll` or
:attr:`poll_answer` is present.
:attr:`shipping_query`, :attr:`pre_checkout_query`, :attr:`poll`,
:attr:`poll_answer`, or :attr:`business_connection` is present.
.. versionchanged:: 21.1
This property now also considers :attr:`business_message`,
:attr:`edited_business_message`, and :attr:`deleted_business_messages`.
Example:
If :attr:`message` is present, this will give :attr:`telegram.Message.chat`.
@@ -500,6 +652,15 @@ class Update(TelegramObject):
elif self.message_reaction_count:
chat = self.message_reaction_count.chat
elif self.business_message:
chat = self.business_message.chat
elif self.edited_business_message:
chat = self.edited_business_message.chat
elif self.deleted_business_messages:
chat = self.deleted_business_messages.chat
self._effective_chat = chat
return chat
@@ -512,6 +673,10 @@ class Update(TelegramObject):
:attr:`callback_query` (i.e. :attr:`telegram.CallbackQuery.message`) or :obj:`None`, if
none of those are present.
.. versionchanged:: 21.1
This property now also considers :attr:`business_message`, and
:attr:`edited_business_message`.
Tip:
This property will only ever return objects of type :class:`telegram.Message` or
:obj:`None`, never :class:`telegram.MaybeInaccessibleMessage` or
@@ -554,6 +719,12 @@ class Update(TelegramObject):
elif self.edited_channel_post:
message = self.edited_channel_post
elif self.business_message:
message = self.business_message
elif self.edited_business_message:
message = self.edited_business_message
self._effective_message = message
return message
@@ -589,5 +760,13 @@ class Update(TelegramObject):
data["message_reaction_count"] = MessageReactionCountUpdated.de_json(
data.get("message_reaction_count"), bot
)
data["business_connection"] = BusinessConnection.de_json(
data.get("business_connection"), bot
)
data["business_message"] = Message.de_json(data.get("business_message"), bot)
data["edited_business_message"] = Message.de_json(data.get("edited_business_message"), bot)
data["deleted_business_messages"] = BusinessMessagesDeleted.de_json(
data.get("deleted_business_messages"), bot
)
return super().de_json(data=data, bot=bot)
+51 -3
View File
@@ -78,11 +78,11 @@ class User(TelegramObject):
username (:obj:`str`, optional): User's or bot's username.
language_code (:obj:`str`, optional): IETF language tag of the user's language.
can_join_groups (:obj:`str`, optional): :obj:`True`, if the bot can be invited to groups.
Returned only in :attr:`telegram.Bot.get_me` requests.
Returned only in :meth:`telegram.Bot.get_me`.
can_read_all_group_messages (:obj:`str`, optional): :obj:`True`, if privacy mode is
disabled for the bot. Returned only in :attr:`telegram.Bot.get_me` requests.
disabled for the bot. Returned only in :meth:`telegram.Bot.get_me`.
supports_inline_queries (:obj:`str`, optional): :obj:`True`, if the bot supports inline
queries. Returned only in :attr:`telegram.Bot.get_me` requests.
queries. Returned only in :meth:`telegram.Bot.get_me`.
is_premium (:obj:`bool`, optional): :obj:`True`, if this user is a Telegram Premium user.
@@ -91,6 +91,12 @@ class User(TelegramObject):
the bot to the attachment menu.
.. versionadded:: 20.0
can_connect_to_business (:obj:`bool`, optional): :obj:`True`, if the bot can be connected
to a Telegram Business account to receive its messages. Returned only in
:meth:`telegram.Bot.get_me`.
.. versionadded:: 21.1
Attributes:
id (:obj:`int`): Unique identifier for this user or bot.
is_bot (:obj:`bool`): :obj:`True`, if this user is a bot.
@@ -112,6 +118,11 @@ class User(TelegramObject):
the bot to the attachment menu.
.. versionadded:: 20.0
can_connect_to_business (:obj:`bool`): Optional. :obj:`True`, if the bot can be connected
to a Telegram Business account to receive its messages. Returned only in
:meth:`telegram.Bot.get_me`.
.. versionadded:: 21.1
.. |user_chat_id_note| replace:: This shortcuts build on the assumption that :attr:`User.id`
coincides with the :attr:`Chat.id` of the private chat with the user. This has been the
case so far, but Telegram does not guarantee that this stays this way.
@@ -119,6 +130,7 @@ class User(TelegramObject):
__slots__ = (
"added_to_attachment_menu",
"can_connect_to_business",
"can_join_groups",
"can_read_all_group_messages",
"first_name",
@@ -144,6 +156,7 @@ class User(TelegramObject):
supports_inline_queries: Optional[bool] = None,
is_premium: Optional[bool] = None,
added_to_attachment_menu: Optional[bool] = None,
can_connect_to_business: Optional[bool] = None,
*,
api_kwargs: Optional[JSONDict] = None,
):
@@ -161,6 +174,7 @@ class User(TelegramObject):
self.supports_inline_queries: Optional[bool] = supports_inline_queries
self.is_premium: Optional[bool] = is_premium
self.added_to_attachment_menu: Optional[bool] = added_to_attachment_menu
self.can_connect_to_business: Optional[bool] = can_connect_to_business
self._id_attrs = (self.id,)
@@ -393,6 +407,7 @@ class User(TelegramObject):
message_thread_id: Optional[int] = None,
link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
disable_web_page_preview: Optional[bool] = None,
@@ -435,6 +450,7 @@ class User(TelegramObject):
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
)
async def delete_message(
@@ -513,6 +529,7 @@ class User(TelegramObject):
message_thread_id: Optional[int] = None,
has_spoiler: Optional[bool] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -556,6 +573,7 @@ class User(TelegramObject):
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
has_spoiler=has_spoiler,
business_connection_id=business_connection_id,
)
async def send_media_group(
@@ -567,6 +585,7 @@ class User(TelegramObject):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -610,6 +629,7 @@ class User(TelegramObject):
caption=caption,
parse_mode=parse_mode,
caption_entities=caption_entities,
business_connection_id=business_connection_id,
)
async def send_audio(
@@ -627,6 +647,7 @@ class User(TelegramObject):
message_thread_id: Optional[int] = None,
thumbnail: Optional[FileInput] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -673,12 +694,14 @@ class User(TelegramObject):
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
thumbnail=thumbnail,
business_connection_id=business_connection_id,
)
async def send_chat_action(
self,
action: str,
message_thread_id: Optional[int] = None,
business_connection_id: Optional[str] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -708,6 +731,7 @@ class User(TelegramObject):
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
)
send_action = send_chat_action
@@ -724,6 +748,7 @@ class User(TelegramObject):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -766,6 +791,7 @@ class User(TelegramObject):
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_dice(
@@ -776,6 +802,7 @@ class User(TelegramObject):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -813,6 +840,7 @@ class User(TelegramObject):
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_document(
@@ -828,6 +856,7 @@ class User(TelegramObject):
message_thread_id: Optional[int] = None,
thumbnail: Optional[FileInput] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -872,6 +901,7 @@ class User(TelegramObject):
caption_entities=caption_entities,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_game(
@@ -882,6 +912,7 @@ class User(TelegramObject):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -919,6 +950,7 @@ class User(TelegramObject):
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_invoice(
@@ -1031,6 +1063,7 @@ class User(TelegramObject):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1075,6 +1108,7 @@ class User(TelegramObject):
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_animation(
@@ -1093,6 +1127,7 @@ class User(TelegramObject):
has_spoiler: Optional[bool] = None,
thumbnail: Optional[FileInput] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1140,6 +1175,7 @@ class User(TelegramObject):
message_thread_id=message_thread_id,
has_spoiler=has_spoiler,
thumbnail=thumbnail,
business_connection_id=business_connection_id,
)
async def send_sticker(
@@ -1151,6 +1187,7 @@ class User(TelegramObject):
message_thread_id: Optional[int] = None,
emoji: Optional[str] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1189,6 +1226,7 @@ class User(TelegramObject):
protect_content=protect_content,
message_thread_id=message_thread_id,
emoji=emoji,
business_connection_id=business_connection_id,
)
async def send_video(
@@ -1208,6 +1246,7 @@ class User(TelegramObject):
has_spoiler: Optional[bool] = None,
thumbnail: Optional[FileInput] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1256,6 +1295,7 @@ class User(TelegramObject):
protect_content=protect_content,
message_thread_id=message_thread_id,
has_spoiler=has_spoiler,
business_connection_id=business_connection_id,
)
async def send_venue(
@@ -1273,6 +1313,7 @@ class User(TelegramObject):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1319,6 +1360,7 @@ class User(TelegramObject):
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_video_note(
@@ -1332,6 +1374,7 @@ class User(TelegramObject):
message_thread_id: Optional[int] = None,
thumbnail: Optional[FileInput] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1374,6 +1417,7 @@ class User(TelegramObject):
protect_content=protect_content,
message_thread_id=message_thread_id,
thumbnail=thumbnail,
business_connection_id=business_connection_id,
)
async def send_voice(
@@ -1388,6 +1432,7 @@ class User(TelegramObject):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1431,6 +1476,7 @@ class User(TelegramObject):
filename=filename,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_poll(
@@ -1452,6 +1498,7 @@ class User(TelegramObject):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -1500,6 +1547,7 @@ class User(TelegramObject):
explanation_entities=explanation_entities,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
)
async def send_copy(
+1 -1
View File
@@ -51,7 +51,7 @@ class Version(NamedTuple):
__version_info__: Final[Version] = Version(
major=21, minor=0, micro=0, releaselevel="final", serial=0
major=21, minor=1, micro=1, releaselevel="final", serial=0
)
__version__: Final[str] = str(__version_info__)
+40 -3
View File
@@ -142,7 +142,7 @@ class _AccentColor(NamedTuple):
#: :data:`telegram.__bot_api_version_info__`.
#:
#: .. versionadded:: 20.0
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=1)
BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=2)
#: :obj:`str`: Telegram Bot API
#: version supported by this version of `python-telegram-bot`. Also available as
#: :data:`telegram.__bot_api_version__`.
@@ -1690,8 +1690,12 @@ class MessageOriginType(StringEnum):
class MessageType(StringEnum):
"""This enum contains the available types of :class:`telegram.Message`. The enum
members of this enumeration are instances of :class:`str` and can be treated as such.
"""This enum contains the available types of :class:`telegram.Message`. Here, a "type" means
a kind of message that is visually distinct from other kinds of messages in the Telegram app.
In particular, auxiliary attributes that can be present for multiple types of messages are
not considered in this enumeration.
The enum members of this enumeration are instances of :class:`str` and can be treated as such.
.. versionadded:: 20.0
"""
@@ -1710,6 +1714,11 @@ class MessageType(StringEnum):
.. versionadded:: 21.0
"""
BUSINESS_CONNECTION_ID = "business_connection_id"
""":obj:`str`: Messages with :attr:`telegram.Message.business_connection_id`.
.. versionadded:: 21.1
"""
CHANNEL_CHAT_CREATED = "channel_chat_created"
""":obj:`str`: Messages with :attr:`telegram.Message.channel_chat_created`."""
CHAT_SHARED = "chat_shared"
@@ -1817,6 +1826,11 @@ class MessageType(StringEnum):
.. versionadded:: 21.0
"""
SENDER_BUSINESS_BOT = "sender_business_bot"
""":obj:`str`: Messages with :attr:`telegram.Message.sender_business_bot`.
.. versionadded:: 21.1
"""
STICKER = "sticker"
""":obj:`str`: Messages with :attr:`telegram.Message.sticker`."""
STORY = "story"
@@ -2312,6 +2326,9 @@ class StickerSetLimit(IntEnum):
MAX_ANIMATED_STICKERS = 50
""":obj:`int`: Maximum number of stickers allowed in an animated or video sticker set, as given
in :meth:`telegram.Bot.add_sticker_to_set`.
.. deprecated:: 21.1
The animated sticker limit is now 120, the same as :attr:`MAX_STATIC_STICKERS`.
"""
MAX_STATIC_STICKERS = 120
""":obj:`int`: Maximum number of stickers allowed in a static sticker set, as given in
@@ -2504,6 +2521,26 @@ class UpdateType(StringEnum):
.. versionadded:: 20.8
"""
BUSINESS_CONNECTION = "business_connection"
""":obj:`str`: Updates with :attr:`telegram.Update.business_connection`.
.. versionadded:: 21.1
"""
BUSINESS_MESSAGE = "business_message"
""":obj:`str`: Updates with :attr:`telegram.Update.business_message`.
.. versionadded:: 21.1
"""
EDITED_BUSINESS_MESSAGE = "edited_business_message"
""":obj:`str`: Updates with :attr:`telegram.Update.edited_business_message`.
.. versionadded:: 21.1
"""
DELETED_BUSINESS_MESSAGES = "deleted_business_messages"
""":obj:`str`: Updates with :attr:`telegram.Update.deleted_business_messages`.
.. versionadded:: 21.1
"""
class InvoiceLimit(IntEnum):
+4
View File
@@ -27,6 +27,8 @@ __all__ = (
"BasePersistence",
"BaseRateLimiter",
"BaseUpdateProcessor",
"BusinessConnectionHandler",
"BusinessMessagesDeletedHandler",
"CallbackContext",
"CallbackDataCache",
"CallbackQueryHandler",
@@ -75,6 +77,8 @@ from ._defaults import Defaults
from ._dictpersistence import DictPersistence
from ._extbot import ExtBot
from ._handlers.basehandler import BaseHandler
from ._handlers.businessconnectionhandler import BusinessConnectionHandler
from ._handlers.businessmessagesdeletedhandler import BusinessMessagesDeletedHandler
from ._handlers.callbackqueryhandler import CallbackQueryHandler
from ._handlers.chatboosthandler import ChatBoostHandler
from ._handlers.chatjoinrequesthandler import ChatJoinRequestHandler
+16 -3
View File
@@ -75,6 +75,8 @@ from telegram.ext._utils.types import BD, BT, CCT, CD, JQ, RT, UD, ConversationK
from telegram.warnings import PTBDeprecationWarning
if TYPE_CHECKING:
from socket import socket
from telegram import Message
from telegram.ext import ConversationHandler, JobQueue
from telegram.ext._applicationbuilder import InitApplicationBuilder
@@ -866,7 +868,7 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
close_loop: bool = True,
stop_signals: ODVInput[Sequence[int]] = DEFAULT_NONE,
secret_token: Optional[str] = None,
unix: Optional[Union[str, Path]] = None,
unix: Optional[Union[str, Path, "socket"]] = None,
) -> None:
"""Convenience method that takes care of initializing and starting the app,
listening for updates from Telegram using :meth:`telegram.ext.Updater.start_webhook` and
@@ -959,8 +961,17 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
header isn't set or it is set to a wrong token.
.. versionadded:: 20.0
unix (:class:`pathlib.Path` | :obj:`str`, optional): Path to the unix socket file. Path
does not need to exist, in which case the file will be created.
unix (:class:`pathlib.Path` | :obj:`str` | :class:`socket.socket`, optional): Can be
either:
* the path to the unix socket file as :class:`pathlib.Path` or :obj:`str`. This
will be passed to `tornado.netutil.bind_unix_socket <https://www.tornadoweb.org/
en/stable/netutil.html#tornado.netutil.bind_unix_socket>`_ to create the socket.
If the Path does not exist, the file will be created.
* or the socket itself. This option allows you to e.g. restrict the permissions of
the socket for improved security. Note that you need to pass the correct family,
type and socket options yourself.
Caution:
This parameter is a replacement for the default TCP bind. Therefore, it is
@@ -969,6 +980,8 @@ class Application(Generic[BT, CCT, UD, CD, BD, JQ], AsyncContextManager["Applica
appropriate :paramref:`webhook_url`.
.. versionadded:: 20.8
.. versionchanged:: 21.1
Added support to pass a socket instance itself.
"""
if not self.updater:
raise RuntimeError(
+94 -3
View File
@@ -48,6 +48,7 @@ from telegram import (
BotDescription,
BotName,
BotShortDescription,
BusinessConnection,
CallbackQuery,
Chat,
ChatAdministratorRights,
@@ -113,6 +114,7 @@ if TYPE_CHECKING:
from telegram.ext import BaseRateLimiter, Defaults
HandledTypes = TypeVar("HandledTypes", bound=Union[Message, CallbackQuery, Chat])
KT = TypeVar("KT", bound=ReplyMarkup)
class ExtBot(Bot, Generic[RLARGS]):
@@ -485,11 +487,14 @@ class ExtBot(Bot, Generic[RLARGS]):
data[key] = new_value
def _replace_keyboard(self, reply_markup: Optional[ReplyMarkup]) -> Optional[ReplyMarkup]:
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
if isinstance(reply_markup, InlineKeyboardMarkup) and self.callback_data_cache is not None:
return self.callback_data_cache.process_keyboard(reply_markup)
# for some reason mypy doesn't understand that IKB is a subtype of Optional[KT]
return self.callback_data_cache.process_keyboard( # type: ignore[return-value]
reply_markup
)
return reply_markup
@@ -567,6 +572,7 @@ class ExtBot(Bot, Generic[RLARGS]):
caption_entities: Optional[Sequence["MessageEntity"]] = None,
link_preview_options: ODVInput["LinkPreviewOptions"] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -597,6 +603,7 @@ class ExtBot(Bot, Generic[RLARGS]):
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
business_connection_id=business_connection_id,
)
if isinstance(result, Message):
self._insert_callback_data(result)
@@ -1178,7 +1185,7 @@ class ExtBot(Bot, Generic[RLARGS]):
name: str,
title: str,
stickers: Sequence["InputSticker"],
sticker_format: str,
sticker_format: Optional[str] = None,
sticker_type: Optional[str] = None,
needs_repainting: Optional[bool] = None,
*,
@@ -2351,6 +2358,7 @@ class ExtBot(Bot, Generic[RLARGS]):
has_spoiler: Optional[bool] = None,
thumbnail: Optional[FileInput] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2384,6 +2392,7 @@ class ExtBot(Bot, Generic[RLARGS]):
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
business_connection_id=business_connection_id,
pool_timeout=pool_timeout,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
@@ -2404,6 +2413,7 @@ class ExtBot(Bot, Generic[RLARGS]):
message_thread_id: Optional[int] = None,
thumbnail: Optional[FileInput] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2420,6 +2430,7 @@ class ExtBot(Bot, Generic[RLARGS]):
audio=audio,
duration=duration,
performer=performer,
business_connection_id=business_connection_id,
title=title,
caption=caption,
disable_notification=disable_notification,
@@ -2445,6 +2456,7 @@ class ExtBot(Bot, Generic[RLARGS]):
chat_id: Union[str, int],
action: str,
message_thread_id: Optional[int] = None,
business_connection_id: Optional[str] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2455,6 +2467,7 @@ class ExtBot(Bot, Generic[RLARGS]):
) -> bool:
return await super().send_chat_action(
chat_id=chat_id,
business_connection_id=business_connection_id,
action=action,
message_thread_id=message_thread_id,
read_timeout=read_timeout,
@@ -2476,6 +2489,7 @@ class ExtBot(Bot, Generic[RLARGS]):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2505,6 +2519,7 @@ class ExtBot(Bot, Generic[RLARGS]):
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
business_connection_id=business_connection_id,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
@@ -2517,6 +2532,7 @@ class ExtBot(Bot, Generic[RLARGS]):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2530,6 +2546,7 @@ class ExtBot(Bot, Generic[RLARGS]):
return await super().send_dice(
chat_id=chat_id,
disable_notification=disable_notification,
business_connection_id=business_connection_id,
reply_to_message_id=reply_to_message_id,
reply_markup=reply_markup,
emoji=emoji,
@@ -2558,6 +2575,7 @@ class ExtBot(Bot, Generic[RLARGS]):
message_thread_id: Optional[int] = None,
thumbnail: Optional[FileInput] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2581,6 +2599,7 @@ class ExtBot(Bot, Generic[RLARGS]):
allow_sending_without_reply=allow_sending_without_reply,
caption_entities=caption_entities,
protect_content=protect_content,
business_connection_id=business_connection_id,
message_thread_id=message_thread_id,
thumbnail=thumbnail,
reply_parameters=reply_parameters,
@@ -2601,6 +2620,7 @@ class ExtBot(Bot, Generic[RLARGS]):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2617,6 +2637,7 @@ class ExtBot(Bot, Generic[RLARGS]):
disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id,
reply_markup=reply_markup,
business_connection_id=business_connection_id,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
message_thread_id=message_thread_id,
@@ -2718,6 +2739,7 @@ class ExtBot(Bot, Generic[RLARGS]):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2748,6 +2770,7 @@ class ExtBot(Bot, Generic[RLARGS]):
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
business_connection_id=business_connection_id,
pool_timeout=pool_timeout,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
@@ -2762,6 +2785,7 @@ class ExtBot(Bot, Generic[RLARGS]):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2790,6 +2814,7 @@ class ExtBot(Bot, Generic[RLARGS]):
pool_timeout=pool_timeout,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
caption=caption,
business_connection_id=business_connection_id,
parse_mode=parse_mode,
caption_entities=caption_entities,
)
@@ -2806,6 +2831,7 @@ class ExtBot(Bot, Generic[RLARGS]):
message_thread_id: Optional[int] = None,
link_preview_options: ODVInput["LinkPreviewOptions"] = DEFAULT_NONE,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
disable_web_page_preview: Optional[bool] = None,
reply_to_message_id: Optional[int] = None,
@@ -2824,6 +2850,7 @@ class ExtBot(Bot, Generic[RLARGS]):
entities=entities,
disable_web_page_preview=disable_web_page_preview,
disable_notification=disable_notification,
business_connection_id=business_connection_id,
protect_content=protect_content,
message_thread_id=message_thread_id,
reply_to_message_id=reply_to_message_id,
@@ -2851,6 +2878,7 @@ class ExtBot(Bot, Generic[RLARGS]):
message_thread_id: Optional[int] = None,
has_spoiler: Optional[bool] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2877,6 +2905,7 @@ class ExtBot(Bot, Generic[RLARGS]):
has_spoiler=has_spoiler,
reply_parameters=reply_parameters,
filename=filename,
business_connection_id=business_connection_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -2904,6 +2933,7 @@ class ExtBot(Bot, Generic[RLARGS]):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2932,6 +2962,7 @@ class ExtBot(Bot, Generic[RLARGS]):
close_date=close_date,
allow_sending_without_reply=allow_sending_without_reply,
explanation_entities=explanation_entities,
business_connection_id=business_connection_id,
protect_content=protect_content,
message_thread_id=message_thread_id,
reply_parameters=reply_parameters,
@@ -2952,6 +2983,7 @@ class ExtBot(Bot, Generic[RLARGS]):
message_thread_id: Optional[int] = None,
emoji: Optional[str] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -2968,6 +3000,7 @@ class ExtBot(Bot, Generic[RLARGS]):
disable_notification=disable_notification,
reply_to_message_id=reply_to_message_id,
reply_markup=reply_markup,
business_connection_id=business_connection_id,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
message_thread_id=message_thread_id,
@@ -2996,6 +3029,7 @@ class ExtBot(Bot, Generic[RLARGS]):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -3022,6 +3056,7 @@ class ExtBot(Bot, Generic[RLARGS]):
google_place_type=google_place_type,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
business_connection_id=business_connection_id,
message_thread_id=message_thread_id,
reply_parameters=reply_parameters,
venue=venue,
@@ -3050,6 +3085,7 @@ class ExtBot(Bot, Generic[RLARGS]):
has_spoiler: Optional[bool] = None,
thumbnail: Optional[FileInput] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -3077,6 +3113,7 @@ class ExtBot(Bot, Generic[RLARGS]):
caption_entities=caption_entities,
protect_content=protect_content,
message_thread_id=message_thread_id,
business_connection_id=business_connection_id,
has_spoiler=has_spoiler,
thumbnail=thumbnail,
filename=filename,
@@ -3100,6 +3137,7 @@ class ExtBot(Bot, Generic[RLARGS]):
message_thread_id: Optional[int] = None,
thumbnail: Optional[FileInput] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -3130,6 +3168,7 @@ 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),
business_connection_id=business_connection_id,
)
async def send_voice(
@@ -3145,6 +3184,7 @@ class ExtBot(Bot, Generic[RLARGS]):
protect_content: ODVInput[bool] = DEFAULT_NONE,
message_thread_id: Optional[int] = None,
reply_parameters: Optional["ReplyParameters"] = None,
business_connection_id: Optional[str] = None,
*,
reply_to_message_id: Optional[int] = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
@@ -3176,6 +3216,7 @@ 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),
business_connection_id=business_connection_id,
)
async def set_chat_administrator_custom_title(
@@ -3462,6 +3503,7 @@ class ExtBot(Bot, Generic[RLARGS]):
self,
name: str,
user_id: int,
format: str, # pylint: disable=redefined-builtin
thumbnail: Optional[FileInput] = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -3475,6 +3517,7 @@ class ExtBot(Bot, Generic[RLARGS]):
name=name,
user_id=user_id,
thumbnail=thumbnail,
format=format,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -3998,6 +4041,52 @@ class ExtBot(Bot, Generic[RLARGS]):
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
async def get_business_connection(
self,
business_connection_id: str,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
rate_limit_args: Optional[RLARGS] = None,
) -> BusinessConnection:
return await super().get_business_connection(
business_connection_id=business_connection_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
async def replace_sticker_in_set(
self,
user_id: int,
name: str,
old_sticker: str,
sticker: "InputSticker",
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
connect_timeout: ODVInput[float] = DEFAULT_NONE,
pool_timeout: ODVInput[float] = DEFAULT_NONE,
api_kwargs: Optional[JSONDict] = None,
rate_limit_args: Optional[RLARGS] = None,
) -> bool:
return await super().replace_sticker_in_set(
user_id=user_id,
name=name,
old_sticker=old_sticker,
sticker=sticker,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
pool_timeout=pool_timeout,
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
# updated camelCase aliases
getMe = get_me
sendMessage = send_message
@@ -4117,3 +4206,5 @@ class ExtBot(Bot, Generic[RLARGS]):
unpinAllGeneralForumTopicMessages = unpin_all_general_forum_topic_messages
getUserChatBoosts = get_user_chat_boosts
setMessageReaction = set_message_reaction
getBusinessConnection = get_business_connection
replaceStickerInSet = replace_sticker_in_set
@@ -0,0 +1,95 @@
#!/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 the BusinessConnectionHandler class."""
from typing import Optional, TypeVar
from telegram import Update
from telegram._utils.defaultvalue import DEFAULT_TRUE
from telegram._utils.types import SCT, DVType
from telegram.ext._handlers.basehandler import BaseHandler
from telegram.ext._utils._update_parsing import parse_chat_id, parse_username
from telegram.ext._utils.types import CCT, HandlerCallback
RT = TypeVar("RT")
class BusinessConnectionHandler(BaseHandler[Update, CCT]):
"""Handler class to handle Telegram
:attr:`Business Connections <telegram.Update.business_connection>`.
.. versionadded:: 21.1
Args:
callback (:term:`coroutine function`): The callback function for this handler. Will be
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)
user_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only
those which are from the specified user ID(s).
username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only
those which are from the specified username(s).
block (:obj:`bool`, optional): Determines whether the return value of the callback should
be awaited before processing the next handler in
:meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`.
.. seealso:: :wiki:`Concurrency`
Attributes:
callback (:term:`coroutine function`): The callback function for this handler.
block (:obj:`bool`): Determines whether the return value of the callback should be
awaited before processing the next handler in
:meth:`telegram.ext.Application.process_update`.
"""
__slots__ = (
"_user_ids",
"_usernames",
)
def __init__(
self,
callback: HandlerCallback[Update, CCT, RT],
user_id: Optional[SCT[int]] = None,
username: Optional[SCT[str]] = None,
block: DVType[bool] = DEFAULT_TRUE,
):
super().__init__(callback, block=block)
self._user_ids = parse_chat_id(user_id)
self._usernames = parse_username(username)
def check_update(self, update: object) -> bool:
"""Determines whether an update should be passed to this handler's :attr:`callback`.
Args:
update (:class:`telegram.Update` | :obj:`object`): Incoming update.
Returns:
:obj:`bool`
"""
if isinstance(update, Update) and update.business_connection:
if not self._user_ids and not self._usernames:
return True
if update.business_connection.user.id in self._user_ids:
return True
return update.business_connection.user.username in self._usernames
return False
@@ -0,0 +1,95 @@
#!/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 the BusinessMessagesDeletedHandler class."""
from typing import Optional, TypeVar
from telegram import Update
from telegram._utils.defaultvalue import DEFAULT_TRUE
from telegram._utils.types import SCT, DVType
from telegram.ext._handlers.basehandler import BaseHandler
from telegram.ext._utils._update_parsing import parse_chat_id, parse_username
from telegram.ext._utils.types import CCT, HandlerCallback
RT = TypeVar("RT")
class BusinessMessagesDeletedHandler(BaseHandler[Update, CCT]):
"""Handler class to handle
:attr:`deleted Telegram Business messages <telegram.Update.deleted_business_messages>`.
.. versionadded:: 21.1
Args:
callback (:term:`coroutine function`): The callback function for this handler. Will be
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)
chat_id (:obj:`int` | Collection[:obj:`int`], optional): Filters requests to allow only
those which are from the specified chat ID(s).
username (:obj:`str` | Collection[:obj:`str`], optional): Filters requests to allow only
those which are from the specified username(s).
block (:obj:`bool`, optional): Determines whether the return value of the callback should
be awaited before processing the next handler in
:meth:`telegram.ext.Application.process_update`. Defaults to :obj:`True`.
.. seealso:: :wiki:`Concurrency`
Attributes:
callback (:term:`coroutine function`): The callback function for this handler.
block (:obj:`bool`): Determines whether the return value of the callback should be
awaited before processing the next handler in
:meth:`telegram.ext.Application.process_update`.
"""
__slots__ = (
"_chat_ids",
"_usernames",
)
def __init__(
self,
callback: HandlerCallback[Update, CCT, RT],
chat_id: Optional[SCT[int]] = None,
username: Optional[SCT[str]] = None,
block: DVType[bool] = DEFAULT_TRUE,
):
super().__init__(callback, block=block)
self._chat_ids = parse_chat_id(chat_id)
self._usernames = parse_username(username)
def check_update(self, update: object) -> bool:
"""Determines whether an update should be passed to this handler's :attr:`callback`.
Args:
update (:class:`telegram.Update` | :obj:`object`): Incoming update.
Returns:
:obj:`bool`
"""
if isinstance(update, Update) and update.deleted_business_messages:
if not self._chat_ids and not self._usernames:
return True
if update.deleted_business_messages.chat.id in self._chat_ids:
return True
return update.deleted_business_messages.chat.username in self._usernames
return False
@@ -107,7 +107,5 @@ class ChatJoinRequestHandler(BaseHandler[Update, CCT]):
return True
if update.chat_join_request.chat.id in self._chat_ids:
return True
if update.chat_join_request.from_user.username in self._usernames:
return True
return False
return update.chat_join_request.from_user.username in self._usernames
return False
+2 -4
View File
@@ -153,14 +153,12 @@ class CommandHandler(BaseHandler[Update, CCT]):
:obj:`bool`: Whether the args are valid for this handler.
"""
# pylint: disable=too-many-boolean-expressions
if (
return bool(
(self.has_args is None)
or (self.has_args is True and args)
or (self.has_args is False and not args)
or (isinstance(self.has_args, int) and len(args) == self.has_args)
):
return True
return False
)
def check_update(
self, update: object
-8
View File
@@ -33,7 +33,6 @@ except ImportError:
from telegram._utils.repr import build_repr_with_selected_attrs
from telegram._utils.types import JSONDict
from telegram._utils.warnings import warn
from telegram.ext._extbot import ExtBot
from telegram.ext._utils.types import CCT, JobCallback
@@ -587,13 +586,6 @@ class JobQueue(Generic[CCT]):
queue.
"""
# TODO: After v20.0, we should remove this warning.
if days != tuple(range(7)): # checks if user passed a custom value
warn(
"Prior to v20.0 the `days` parameter was not aligned to that of cron's weekday "
"scheme. We recommend double checking if the passed value is correct.",
stacklevel=2,
)
if not job_kwargs:
job_kwargs = {}
+18 -5
View File
@@ -49,6 +49,8 @@ except ImportError:
WEBHOOKS_AVAILABLE = False
if TYPE_CHECKING:
from socket import socket
from telegram import Bot
@@ -472,7 +474,7 @@ class Updater(AsyncContextManager["Updater"]):
ip_address: Optional[str] = None,
max_connections: int = 40,
secret_token: Optional[str] = None,
unix: Optional[Union[str, Path]] = None,
unix: Optional[Union[str, Path, "socket"]] = None,
) -> "asyncio.Queue[object]":
"""
Starts a small http server to listen for updates via webhook. If :paramref:`cert`
@@ -541,8 +543,17 @@ class Updater(AsyncContextManager["Updater"]):
header isn't set or it is set to a wrong token.
.. versionadded:: 20.0
unix (:class:`pathlib.Path` | :obj:`str`, optional): Path to the unix socket file. Path
does not need to exist, in which case the file will be created.
unix (:class:`pathlib.Path` | :obj:`str` | :class:`socket.socket`, optional): Can be
either:
* the path to the unix socket file as :class:`pathlib.Path` or :obj:`str`. This
will be passed to `tornado.netutil.bind_unix_socket <https://www.tornadoweb.org/
en/stable/netutil.html#tornado.netutil.bind_unix_socket>`_ to create the socket.
If the Path does not exist, the file will be created.
* or the socket itself. This option allows you to e.g. restrict the permissions of
the socket for improved security. Note that you need to pass the correct family,
type and socket options yourself.
Caution:
This parameter is a replacement for the default TCP bind. Therefore, it is
@@ -551,6 +562,8 @@ class Updater(AsyncContextManager["Updater"]):
appropriate :paramref:`webhook_url`.
.. versionadded:: 20.8
.. versionchanged:: 21.1
Added support to pass a socket instance itself.
Returns:
:class:`queue.Queue`: The update queue that can be filled from the main thread.
@@ -632,7 +645,7 @@ class Updater(AsyncContextManager["Updater"]):
ip_address: Optional[str] = None,
max_connections: int = 40,
secret_token: Optional[str] = None,
unix: Optional[Union[str, Path]] = None,
unix: Optional[Union[str, Path, "socket"]] = None,
) -> None:
_LOGGER.debug("Updater thread started (webhook)")
@@ -793,7 +806,7 @@ class Updater(AsyncContextManager["Updater"]):
if drop_pending_updates:
_LOGGER.debug("Dropping pending updates from Telegram server")
await self.bot.set_webhook(
url=webhook_url,
url=webhook_url, # type: ignore[arg-type]
certificate=cert,
allowed_updates=allowed_updates,
ip_address=ip_address,
+8 -4
View File
@@ -21,6 +21,7 @@ import asyncio
import json
from http import HTTPStatus
from pathlib import Path
from socket import socket
from ssl import SSLContext
from types import TracebackType
from typing import TYPE_CHECKING, Optional, Type, Union
@@ -67,7 +68,7 @@ class WebhookServer:
port: int,
webhook_app: "WebhookAppClass",
ssl_ctx: Optional[SSLContext],
unix: Optional[Union[str, Path]] = None,
unix: Optional[Union[str, Path, socket]] = None,
):
if unix and not UNIX_AVAILABLE:
raise RuntimeError("This OS does not support binding unix sockets.")
@@ -75,15 +76,18 @@ class WebhookServer:
self.listen = listen
self.port = port
self.is_running = False
self.unix = unix
self.unix = None
if unix and isinstance(unix, socket):
self.unix = unix
elif unix:
self.unix = bind_unix_socket(str(unix))
self._server_lock = asyncio.Lock()
self._shutdown_lock = asyncio.Lock()
async def serve_forever(self, ready: Optional[asyncio.Event] = None) -> None:
async with self._server_lock:
if self.unix:
socket = bind_unix_socket(str(self.unix))
self._http_server.add_socket(socket)
self._http_server.add_socket(self.unix)
else:
self._http_server.listen(self.port, address=self.listen)
+86 -12
View File
@@ -54,6 +54,7 @@ __all__ = (
"HAS_PROTECTED_CONTENT",
"INVOICE",
"IS_AUTOMATIC_FORWARD",
"IS_FROM_OFFLINE",
"IS_TOPIC_MESSAGE",
"LOCATION",
"PASSPORT_DATA",
@@ -272,23 +273,29 @@ class BaseFilter:
def check_update(self, update: Update) -> Optional[Union[bool, FilterDataDict]]:
"""Checks if the specified update should be handled by this filter.
.. versionchanged:: 21.1
This filter now also returns :obj:`True` if the update contains
:attr:`~telegram.Update.business_message`
or :attr:`~telegram.Update.edited_business_message`.
Args:
update (:class:`telegram.Update`): The update to check.
Returns:
:obj:`bool`: :obj:`True` if the update contains one of
:attr:`~telegram.Update.channel_post`, :attr:`~telegram.Update.message`,
:attr:`~telegram.Update.edited_channel_post` or
:attr:`~telegram.Update.edited_message`, :obj:`False` otherwise.
:attr:`~telegram.Update.edited_channel_post`,
:attr:`~telegram.Update.edited_message`, :attr:`telegram.Update.business_message`,
:attr:`telegram.Update.edited_business_message`, or :obj:`False` otherwise.
"""
if ( # Only message updates should be handled.
update.channel_post
return bool( # Only message updates should be handled.
update.channel_post # pylint: disable=too-many-boolean-expressions
or update.message
or update.edited_channel_post
or update.edited_message
):
return True
return False
or update.business_message
or update.edited_business_message
)
class MessageFilter(BaseFilter):
@@ -1556,6 +1563,20 @@ IS_TOPIC_MESSAGE = _IsTopicMessage(name="filters.IS_TOPIC_MESSAGE")
"""
class _IsFromOffline(MessageFilter):
__slots__ = ()
def filter(self, message: Message) -> bool:
return bool(message.is_from_offline)
IS_FROM_OFFLINE = _IsFromOffline(name="filters.IS_FROM_OFFLINE")
"""Messages that contain :attr:`telegram.Message.is_from_offline`.
.. versionadded:: 21.1
"""
class Language(MessageFilter):
"""Filters messages to only allow those which are from users with a certain language code.
@@ -2488,13 +2509,21 @@ class UpdateType:
__slots__ = ()
def filter(self, update: Update) -> bool:
return update.edited_message is not None or update.edited_channel_post is not None
return (
update.edited_message is not None
or update.edited_channel_post is not None
or update.edited_business_message is not None
)
EDITED = _Edited(name="filters.UpdateType.EDITED")
"""Updates with either :attr:`telegram.Update.edited_message` or
:attr:`telegram.Update.edited_channel_post`.
"""Updates with :attr:`telegram.Update.edited_message`,
:attr:`telegram.Update.edited_channel_post`, or
:attr:`telegram.Update.edited_business_message`.
.. versionadded:: 20.0
.. versionchanged:: 21.1
Added :attr:`telegram.Update.edited_business_message` to the filter.
"""
class _EditedChannelPost(UpdateFilter):
@@ -2532,7 +2561,48 @@ class UpdateType:
MESSAGES = _Messages(name="filters.UpdateType.MESSAGES")
"""Updates with either :attr:`telegram.Update.message` or
:attr:`telegram.Update.edited_message`."""
:attr:`telegram.Update.edited_message`.
"""
class _BusinessMessage(UpdateFilter):
__slots__ = ()
def filter(self, update: Update) -> bool:
return update.business_message is not None
BUSINESS_MESSAGE = _BusinessMessage(name="filters.UpdateType.BUSINESS_MESSAGE")
"""Updates with :attr:`telegram.Update.business_message`.
.. versionadded:: 21.1"""
class _EditedBusinessMessage(UpdateFilter):
__slots__ = ()
def filter(self, update: Update) -> bool:
return update.edited_business_message is not None
EDITED_BUSINESS_MESSAGE = _EditedBusinessMessage(
name="filters.UpdateType.EDITED_BUSINESS_MESSAGE"
)
"""Updates with :attr:`telegram.Update.edited_business_message`.
.. versionadded:: 21.1
"""
class _BusinessMessages(UpdateFilter):
__slots__ = ()
def filter(self, update: Update) -> bool:
return (
update.business_message is not None or update.edited_business_message is not None
)
BUSINESS_MESSAGES = _BusinessMessages(name="filters.UpdateType.BUSINESS_MESSAGES")
"""Updates with either :attr:`telegram.Update.business_message` or
:attr:`telegram.Update.edited_business_message`.
.. versionadded:: 21.1
"""
class User(_ChatUserBaseFilter):
@@ -2677,6 +2747,8 @@ class ViaBot(_ChatUserBaseFilter):
Examples:
``MessageHandler(filters.ViaBot(1234), callback_method)``
.. seealso:: :attr:`~telegram.ext.filters.VIA_BOT`
Args:
bot_id(:obj:`int` | Collection[:obj:`int`], optional): Which bot ID(s) to
allow through.
@@ -2758,7 +2830,9 @@ class _ViaBot(MessageFilter):
VIA_BOT = _ViaBot(name="filters.VIA_BOT")
"""This filter filters for message that were sent via *any* bot."""
"""This filter filters for message that were sent via *any* bot.
.. seealso:: :class:`~telegram.ext.filters.ViaBot`"""
class _Video(MessageFilter):
+8 -4
View File
@@ -33,6 +33,7 @@ def input_sticker():
emoji_list=TestInputStickerBase.emoji_list,
mask_position=TestInputStickerBase.mask_position,
keywords=TestInputStickerBase.keywords,
format=TestInputStickerBase.format,
)
@@ -41,9 +42,10 @@ class TestInputStickerBase:
emoji_list = ("👍", "👎")
mask_position = MaskPosition("forehead", 0.5, 0.5, 0.5)
keywords = ("thumbsup", "thumbsdown")
format = "static"
class TestInputStickerNoRequest(TestInputStickerBase):
class TestInputStickerWithoutRequest(TestInputStickerBase):
def test_slot_behaviour(self, input_sticker):
inst = input_sticker
for attr in inst.__slots__:
@@ -56,11 +58,12 @@ class TestInputStickerNoRequest(TestInputStickerBase):
assert input_sticker.emoji_list == self.emoji_list
assert input_sticker.mask_position == self.mask_position
assert input_sticker.keywords == self.keywords
assert input_sticker.format == self.format
def test_attributes_tuple(self, input_sticker):
assert isinstance(input_sticker.keywords, tuple)
assert isinstance(input_sticker.emoji_list, tuple)
a = InputSticker("sticker", ["emoji"])
a = InputSticker("sticker", ["emoji"], "static")
assert isinstance(a.emoji_list, tuple)
assert a.keywords == ()
@@ -72,9 +75,10 @@ class TestInputStickerNoRequest(TestInputStickerBase):
assert input_sticker_dict["emoji_list"] == list(input_sticker.emoji_list)
assert input_sticker_dict["mask_position"] == input_sticker.mask_position.to_dict()
assert input_sticker_dict["keywords"] == list(input_sticker.keywords)
assert input_sticker_dict["format"] == input_sticker.format
def test_with_sticker_input_types(self, video_sticker_file): # noqa: F811
sticker = InputSticker(sticker=video_sticker_file, emoji_list=["👍"])
sticker = InputSticker(sticker=video_sticker_file, emoji_list=["👍"], format="video")
assert isinstance(sticker.sticker, InputFile)
sticker = InputSticker(data_file("telegram_video_sticker.webm"), ["👍"])
sticker = InputSticker(data_file("telegram_video_sticker.webm"), ["👍"], "video")
assert sticker.sticker == data_file("telegram_video_sticker.webm").as_uri()
+85 -38
View File
@@ -39,6 +39,7 @@ from telegram import (
from telegram.constants import ParseMode, StickerFormat, StickerType
from telegram.error import BadRequest, TelegramError
from telegram.request import RequestData
from telegram.warnings import PTBDeprecationWarning
from tests.auxil.bot_method_checks import (
check_defaults_handling,
check_shortcut_call,
@@ -574,8 +575,6 @@ def sticker_set_thumb_file():
class TestStickerSetBase:
title = "Test stickers"
is_animated = True
is_video = True
stickers = [Sticker("file_id", "file_un_id", 512, 512, True, True, Sticker.REGULAR)]
name = "NOTAREALNAME"
sticker_type = Sticker.REGULAR
@@ -584,7 +583,7 @@ class TestStickerSetBase:
class TestStickerSetWithoutRequest(TestStickerSetBase):
def test_slot_behaviour(self):
inst = StickerSet("this", "is", True, self.stickers, True, "not")
inst = StickerSet("this", "is", self.stickers, "not")
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"
@@ -594,8 +593,6 @@ class TestStickerSetWithoutRequest(TestStickerSetBase):
json_dict = {
"name": name,
"title": self.title,
"is_animated": self.is_animated,
"is_video": self.is_video,
"stickers": [x.to_dict() for x in self.stickers],
"thumbnail": sticker.thumbnail.to_dict(),
"sticker_type": self.sticker_type,
@@ -605,8 +602,6 @@ class TestStickerSetWithoutRequest(TestStickerSetBase):
assert sticker_set.name == name
assert sticker_set.title == self.title
assert sticker_set.is_animated == self.is_animated
assert sticker_set.is_video == self.is_video
assert sticker_set.stickers == tuple(self.stickers)
assert sticker_set.thumbnail == sticker.thumbnail
assert sticker_set.sticker_type == self.sticker_type
@@ -618,8 +613,6 @@ class TestStickerSetWithoutRequest(TestStickerSetBase):
assert isinstance(sticker_set_dict, dict)
assert sticker_set_dict["name"] == sticker_set.name
assert sticker_set_dict["title"] == sticker_set.title
assert sticker_set_dict["is_animated"] == sticker_set.is_animated
assert sticker_set_dict["is_video"] == sticker_set.is_video
assert sticker_set_dict["stickers"][0] == sticker_set.stickers[0].to_dict()
assert sticker_set_dict["thumbnail"] == sticker_set.thumbnail.to_dict()
assert sticker_set_dict["sticker_type"] == sticker_set.sticker_type
@@ -628,26 +621,20 @@ class TestStickerSetWithoutRequest(TestStickerSetBase):
a = StickerSet(
self.name,
self.title,
self.is_animated,
self.stickers,
self.is_video,
self.sticker_type,
)
b = StickerSet(
self.name,
self.title,
self.is_animated,
self.stickers,
self.is_video,
self.sticker_type,
)
c = StickerSet(self.name, "title", False, [], True, Sticker.CUSTOM_EMOJI)
c = StickerSet(self.name, "title", [], Sticker.CUSTOM_EMOJI)
d = StickerSet(
"blah",
self.title,
self.is_animated,
self.stickers,
self.is_video,
self.sticker_type,
)
e = Audio(self.name, "", 0, None, None)
@@ -685,7 +672,9 @@ class TestStickerSetWithoutRequest(TestStickerSetBase):
)
monkeypatch.setattr(bot, "_post", make_assertion)
await bot.upload_sticker_file(chat_id, sticker=file, sticker_format="static")
await bot.upload_sticker_file(
chat_id, sticker=file, sticker_format=StickerFormat.STATIC
)
assert test_flag
finally:
bot._local_mode = False
@@ -715,8 +704,7 @@ class TestStickerSetWithoutRequest(TestStickerSetBase):
chat_id,
"name",
"title",
stickers=[InputSticker(file, emoji_list=["emoji"])],
sticker_format=StickerFormat.STATIC,
stickers=[InputSticker(file, emoji_list=["emoji"], format=StickerFormat.STATIC)],
)
assert test_flag
@@ -755,7 +743,9 @@ class TestStickerSetWithoutRequest(TestStickerSetBase):
monkeypatch.setattr(bot, "_post", make_assertion)
await bot.add_sticker_to_set(
chat_id, "name", sticker=InputSticker(sticker=file, emoji_list=["this"])
chat_id,
"name",
sticker=InputSticker(sticker=file, emoji_list=["this"], format="static"),
)
assert test_flag
@@ -778,7 +768,7 @@ class TestStickerSetWithoutRequest(TestStickerSetBase):
test_flag = isinstance(data.get("thumbnail"), InputFile)
monkeypatch.setattr(bot, "_post", make_assertion)
await bot.set_sticker_set_thumbnail("name", chat_id, thumbnail=file)
await bot.set_sticker_set_thumbnail("name", chat_id, thumbnail=file, format="static")
assert test_flag
finally:
bot._local_mode = False
@@ -794,6 +784,27 @@ class TestStickerSetWithoutRequest(TestStickerSetBase):
monkeypatch.setattr(sticker.get_bot(), "get_file", make_assertion)
assert await sticker.get_file()
async def test_create_new_sticker_set_format_arg_depr(
self, bot, chat_id, sticker_file, monkeypatch
):
async def make_assertion(*_, **kwargs):
pass
monkeypatch.setattr(bot, "_post", make_assertion)
with pytest.warns(PTBDeprecationWarning, match="`sticker_format` is deprecated"):
await bot.create_new_sticker_set(
chat_id,
"name",
"title",
stickers=sticker_file,
sticker_format="static",
)
async def test_deprecation_creation_args(self, recwarn):
with pytest.warns(PTBDeprecationWarning, match="The parameters `is_animated` and ") as w:
StickerSet("name", "title", [], "static", is_animated=True)
assert w[0].filename == __file__, "wrong stacklevel!"
@pytest.mark.xdist_group("stickerset")
class TestStickerSetWithRequest:
@@ -817,8 +828,11 @@ class TestStickerSetWithRequest:
chat_id,
name=sticker_set,
title="Sticker Test",
stickers=[InputSticker(sticker_file, emoji_list=["😄"])],
sticker_format=StickerFormat.STATIC,
stickers=[
InputSticker(
sticker_file, emoji_list=["😄"], format=StickerFormat.STATIC
)
],
)
assert s
elif sticker_set.startswith("animated"):
@@ -826,8 +840,13 @@ class TestStickerSetWithRequest:
chat_id,
name=sticker_set,
title="Animated Test",
stickers=[InputSticker(animated_sticker_file, emoji_list=["😄"])],
sticker_format=StickerFormat.ANIMATED,
stickers=[
InputSticker(
animated_sticker_file,
emoji_list=["😄"],
format=StickerFormat.ANIMATED,
)
],
)
assert a
elif sticker_set.startswith("video"):
@@ -835,8 +854,11 @@ class TestStickerSetWithRequest:
chat_id,
name=sticker_set,
title="Video Test",
stickers=[InputSticker(video_sticker_file, emoji_list=["😄"])],
sticker_format=StickerFormat.VIDEO,
stickers=[
InputSticker(
video_sticker_file, emoji_list=["😄"], format=StickerFormat.VIDEO
)
],
)
assert v
@@ -850,8 +872,7 @@ class TestStickerSetWithRequest:
chat_id,
name=name,
title="Stickerset delete Test",
stickers=[InputSticker(sticker_file, emoji_list=["😄"])],
sticker_format=StickerFormat.STATIC,
stickers=[InputSticker(sticker_file, emoji_list=["😄"], format="static")],
)
# this prevents a second issue when calling delete too soon after creating the set leads
# to it failing as well
@@ -870,8 +891,11 @@ class TestStickerSetWithRequest:
chat_id,
name=ss_name,
title="Custom Emoji Sticker Set",
stickers=[InputSticker(animated_sticker_file, emoji_list=["😄"])],
sticker_format=StickerFormat.ANIMATED,
stickers=[
InputSticker(
animated_sticker_file, emoji_list=["😄"], format=StickerFormat.ANIMATED
)
],
sticker_type=Sticker.CUSTOM_EMOJI,
)
assert await bot.set_custom_emoji_sticker_set_thumbnail(ss_name, "")
@@ -890,7 +914,9 @@ class TestStickerSetWithRequest:
bot.add_sticker_to_set(
chat_id,
f"test_by_{bot.username}",
sticker=InputSticker(sticker=file.file_id, emoji_list=["😄"]),
sticker=InputSticker(
sticker=file.file_id, emoji_list=["😄"], format=StickerFormat.STATIC
),
),
bot.add_sticker_to_set( # Also test with file input and mask
chat_id,
@@ -899,6 +925,7 @@ class TestStickerSetWithRequest:
sticker=sticker_file,
emoji_list=["😄"],
mask_position=MaskPosition(MaskPosition.EYES, -1, 1, 2),
format=StickerFormat.STATIC,
),
),
)
@@ -910,7 +937,9 @@ class TestStickerSetWithRequest:
chat_id,
f"animated_test_by_{bot.username}",
sticker=InputSticker(
sticker=data_file("telegram_animated_sticker.tgs").open("rb"), emoji_list=["😄"]
sticker=data_file("telegram_animated_sticker.tgs").open("rb"),
emoji_list=["😄"],
format=StickerFormat.ANIMATED,
),
)
@@ -920,7 +949,7 @@ class TestStickerSetWithRequest:
assert await bot.add_sticker_to_set(
chat_id,
f"video_test_by_{bot.username}",
sticker=InputSticker(sticker=f, emoji_list=["🤔"]),
sticker=InputSticker(sticker=f, emoji_list=["🤔"], format=StickerFormat.VIDEO),
)
# Test set_sticker_position_in_set
@@ -943,7 +972,7 @@ class TestStickerSetWithRequest:
async def test_bot_methods_3_png(self, bot, chat_id, sticker_set_thumb_file):
await asyncio.sleep(1)
assert await bot.set_sticker_set_thumbnail(
f"test_by_{bot.username}", chat_id, sticker_set_thumb_file
f"test_by_{bot.username}", chat_id, format="static", thumbnail=sticker_set_thumb_file
)
async def test_bot_methods_3_tgs(
@@ -953,8 +982,13 @@ class TestStickerSetWithRequest:
animated_test = f"animated_test_by_{bot.username}"
file_id = animated_sticker_set.stickers[-1].file_id
tasks = asyncio.gather(
bot.set_sticker_set_thumbnail(animated_test, chat_id, animated_sticker_file),
bot.set_sticker_set_thumbnail(animated_test, chat_id, file_id),
bot.set_sticker_set_thumbnail(
animated_test,
chat_id,
"animated",
thumbnail=animated_sticker_file,
),
bot.set_sticker_set_thumbnail(animated_test, chat_id, "animated", thumbnail=file_id),
)
assert all(await tasks)
@@ -1037,6 +1071,19 @@ class TestStickerSetWithRequest:
file_id = video_sticker_set.stickers[-1].file_id
assert await bot.set_sticker_keywords(file_id, ["test", "test2"])
async def test_bot_methods_8_png(self, bot, sticker_set, sticker_file):
file_id = sticker_set.stickers[-1].file_id
assert await bot.replace_sticker_in_set(
bot.id,
f"test_by_{bot.username}",
file_id,
sticker=InputSticker(
sticker=sticker_file,
emoji_list=["😄"],
format=StickerFormat.STATIC,
),
)
@pytest.fixture(scope="module")
def mask_position():
@@ -1126,9 +1173,9 @@ class TestMaskPositionWithRequest(TestMaskPositionBase):
emoji_list=["😔"],
mask_position=mask_position,
keywords=["sad"],
format=StickerFormat.STATIC,
)
],
sticker_format=StickerFormat.STATIC,
sticker_type=Sticker.MASK,
)
assert sticker_set
+27 -2
View File
@@ -21,7 +21,7 @@ import datetime
import functools
import inspect
import re
from typing import Any, Callable, Dict, Iterable, List, Optional
from typing import Any, Callable, Collection, Dict, Iterable, List, Optional, Tuple
import pytest
@@ -59,6 +59,7 @@ def check_shortcut_signature(
bot_method: Callable,
shortcut_kwargs: List[str],
additional_kwargs: List[str],
annotation_overrides: Optional[Dict[str, Tuple[Any, Any]]] = None,
) -> bool:
"""
Checks that the signature of a shortcut matches the signature of the underlying bot method.
@@ -69,10 +70,14 @@ def check_shortcut_signature(
shortcut_kwargs: The kwargs passed by the shortcut directly, e.g. ``chat_id``
additional_kwargs: Additional kwargs of the shortcut that the bot method doesn't have, e.g.
``quote``.
annotation_overrides: A dictionary of exceptions for the annotation comparison. The key is
the name of the argument, the value is a tuple of the expected annotation and
the default value. E.g. ``{'parse_mode': (str, 'None')}``.
Returns:
:obj:`bool`: Whether or not the signature matches.
"""
annotation_overrides = annotation_overrides or {}
def resolve_class(class_name: str) -> Optional[type]:
"""Attempts to resolve a PTB class (telegram module only) from a ForwardRef.
@@ -117,6 +122,14 @@ def check_shortcut_signature(
if shortcut_sig.parameters[kwarg].kind != expected_kind:
raise Exception(f"Argument {kwarg} must be of kind {expected_kind}.")
if kwarg in annotation_overrides:
if shortcut_sig.parameters[kwarg].annotation != annotation_overrides[kwarg][0]:
raise Exception(
f"For argument {kwarg} I expected {annotation_overrides[kwarg]}, "
f"but got {shortcut_sig.parameters[kwarg].annotation}"
)
continue
if bot_sig.parameters[kwarg].annotation != shortcut_sig.parameters[kwarg].annotation:
if FORWARD_REF_PATTERN.search(str(shortcut_sig.parameters[kwarg])):
# If a shortcut signature contains a ForwardRef, the simple comparison of
@@ -155,6 +168,13 @@ def check_shortcut_signature(
bot_method_sig = inspect.signature(bot_method)
shortcut_sig = inspect.signature(shortcut)
for arg in expected_args:
if arg in annotation_overrides:
if shortcut_sig.parameters[arg].default == annotation_overrides[arg][1]:
continue
raise Exception(
f"For argument {arg} I expected default {annotation_overrides[arg][1]}, "
f"but got {shortcut_sig.parameters[arg].default}"
)
if not shortcut_sig.parameters[arg].default == bot_method_sig.parameters[arg].default:
raise Exception(
f"Default for argument {arg} does not match the default of the Bot method."
@@ -525,6 +545,7 @@ async def check_defaults_handling(
method: Callable,
bot: Bot,
return_value=None,
no_default_kwargs: Collection[str] = frozenset(),
) -> bool:
"""
Checks that tg.ext.Defaults are handled correctly.
@@ -536,6 +557,8 @@ async def check_defaults_handling(
return_value: Optional. The return value of Bot._post that the method expects. Defaults to
None. get_file is automatically handled. If this is a `TelegramObject`, Bot._post will
return the `to_dict` representation of it.
no_default_kwargs: Optional. A collection of keyword arguments that should not have default
values. Defaults to an empty frozenset.
"""
raw_bot = not isinstance(bot, ExtBot)
@@ -545,7 +568,9 @@ async def check_defaults_handling(
kwargs_need_default = {
kwarg
for kwarg, value in shortcut_signature.parameters.items()
if isinstance(value.default, DefaultValue) and not kwarg.endswith("_timeout")
if isinstance(value.default, DefaultValue)
and not kwarg.endswith("_timeout")
and kwarg not in no_default_kwargs
}
# We tested this for a long time, but Bot API 7.0 deprecated it in favor of
# reply_parameters. In the transition phase, both exist in a mutually exclusive
+1 -3
View File
@@ -73,9 +73,7 @@ def no_rerun_after_xfail_or_flood(error, name, test: pytest.Function, plugin):
if getattr(error[1], "msg", "") is None:
raise error[1]
did_we_flood = "flood" in getattr(error[1], "msg", "") # _pytest.outcomes.XFailed has 'msg'
if xfail_present or did_we_flood:
return False
return True
return not (xfail_present or did_we_flood)
def pytest_collection_modifyitems(items: List[pytest.Item]):
+173
View File
@@ -0,0 +1,173 @@
#!/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 asyncio
import datetime
import pytest
from telegram import (
Bot,
BusinessConnection,
CallbackQuery,
Chat,
ChosenInlineResult,
Message,
PreCheckoutQuery,
ShippingQuery,
Update,
User,
)
from telegram._utils.datetime import UTC
from telegram.ext import BusinessConnectionHandler, CallbackContext, JobQueue
from tests.auxil.slots import mro_slots
message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text")
params = [
{"message": message},
{"edited_message": message},
{"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)},
{"channel_post": message},
{"edited_channel_post": message},
{"chosen_inline_result": ChosenInlineResult("id", User(1, "", False), "")},
{"shipping_query": ShippingQuery("id", User(1, "", False), "", None)},
{"pre_checkout_query": PreCheckoutQuery("id", User(1, "", False), "", 0, "")},
{"callback_query": CallbackQuery(1, User(1, "", False), "chat")},
]
ids = (
"message",
"edited_message",
"callback_query",
"channel_post",
"edited_channel_post",
"chosen_inline_result",
"shipping_query",
"pre_checkout_query",
"callback_query_without_message",
)
@pytest.fixture(scope="class", params=params, ids=ids)
def false_update(request):
return Update(update_id=2, **request.param)
@pytest.fixture(scope="class")
def time():
return datetime.datetime.now(tz=UTC)
@pytest.fixture(scope="class")
def business_connection(bot):
bc = BusinessConnection(
id="1",
user_chat_id=1,
user=User(1, "name", username="user_a", is_bot=False),
date=datetime.datetime.now(tz=UTC),
can_reply=True,
is_enabled=True,
)
bc.set_bot(bot)
return bc
@pytest.fixture()
def business_connection_update(bot, business_connection):
return Update(0, business_connection=business_connection)
class TestBusinessConnectionHandler:
test_flag = False
def test_slot_behaviour(self):
action = BusinessConnectionHandler(self.callback)
for attr in action.__slots__:
assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot"
@pytest.fixture(autouse=True)
def _reset(self):
self.test_flag = False
async def callback(self, update, context):
self.test_flag = (
isinstance(context, CallbackContext)
and isinstance(context.bot, Bot)
and isinstance(update, Update)
and isinstance(context.update_queue, asyncio.Queue)
and isinstance(context.job_queue, JobQueue)
and isinstance(context.user_data, dict)
and isinstance(context.bot_data, dict)
and isinstance(
update.business_connection,
BusinessConnection,
)
)
def test_with_user_id(self, business_connection_update):
handler = BusinessConnectionHandler(self.callback, user_id=1)
assert handler.check_update(business_connection_update)
handler = BusinessConnectionHandler(self.callback, user_id=[1])
assert handler.check_update(business_connection_update)
handler = BusinessConnectionHandler(self.callback, user_id=2, username="@user_a")
assert handler.check_update(business_connection_update)
handler = BusinessConnectionHandler(self.callback, user_id=2)
assert not handler.check_update(business_connection_update)
handler = BusinessConnectionHandler(self.callback, user_id=[2])
assert not handler.check_update(business_connection_update)
def test_with_username(self, business_connection_update):
handler = BusinessConnectionHandler(self.callback, username="user_a")
assert handler.check_update(business_connection_update)
handler = BusinessConnectionHandler(self.callback, username="@user_a")
assert handler.check_update(business_connection_update)
handler = BusinessConnectionHandler(self.callback, username=["user_a"])
assert handler.check_update(business_connection_update)
handler = BusinessConnectionHandler(self.callback, username=["@user_a"])
assert handler.check_update(business_connection_update)
handler = BusinessConnectionHandler(self.callback, user_id=1, username="@user_b")
assert handler.check_update(business_connection_update)
handler = BusinessConnectionHandler(self.callback, username="user_b")
assert not handler.check_update(business_connection_update)
handler = BusinessConnectionHandler(self.callback, username="@user_b")
assert not handler.check_update(business_connection_update)
handler = BusinessConnectionHandler(self.callback, username=["user_b"])
assert not handler.check_update(business_connection_update)
handler = BusinessConnectionHandler(self.callback, username=["@user_b"])
assert not handler.check_update(business_connection_update)
business_connection_update.business_connection.user._unfreeze()
business_connection_update.business_connection.user.username = None
assert not handler.check_update(business_connection_update)
def test_other_update_types(self, false_update):
handler = BusinessConnectionHandler(self.callback)
assert not handler.check_update(false_update)
assert not handler.check_update(True)
async def test_context(self, app, business_connection_update):
handler = BusinessConnectionHandler(callback=self.callback)
app.add_handler(handler)
async with app:
await app.process_update(business_connection_update)
assert self.test_flag
@@ -0,0 +1,170 @@
#!/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 asyncio
import datetime
import pytest
from telegram import (
Bot,
BusinessMessagesDeleted,
CallbackQuery,
Chat,
ChosenInlineResult,
Message,
PreCheckoutQuery,
ShippingQuery,
Update,
User,
)
from telegram._utils.datetime import UTC
from telegram.ext import BusinessMessagesDeletedHandler, CallbackContext, JobQueue
from tests.auxil.slots import mro_slots
message = Message(1, None, Chat(1, ""), from_user=User(1, "", False), text="Text")
params = [
{"message": message},
{"edited_message": message},
{"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)},
{"channel_post": message},
{"edited_channel_post": message},
{"chosen_inline_result": ChosenInlineResult("id", User(1, "", False), "")},
{"shipping_query": ShippingQuery("id", User(1, "", False), "", None)},
{"pre_checkout_query": PreCheckoutQuery("id", User(1, "", False), "", 0, "")},
{"callback_query": CallbackQuery(1, User(1, "", False), "chat")},
]
ids = (
"message",
"edited_message",
"callback_query",
"channel_post",
"edited_channel_post",
"chosen_inline_result",
"shipping_query",
"pre_checkout_query",
"callback_query_without_message",
)
@pytest.fixture(scope="class", params=params, ids=ids)
def false_update(request):
return Update(update_id=2, **request.param)
@pytest.fixture(scope="class")
def time():
return datetime.datetime.now(tz=UTC)
@pytest.fixture(scope="class")
def business_messages_deleted(bot):
bmd = BusinessMessagesDeleted(
business_connection_id="1",
chat=Chat(1, Chat.PRIVATE, username="user_a"),
message_ids=[1, 2, 3],
)
bmd.set_bot(bot)
return bmd
@pytest.fixture()
def business_messages_deleted_update(bot, business_messages_deleted):
return Update(0, deleted_business_messages=business_messages_deleted)
class TestBusinessMessagesDeletedHandler:
test_flag = False
def test_slot_behaviour(self):
action = BusinessMessagesDeletedHandler(self.callback)
for attr in action.__slots__:
assert getattr(action, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot"
@pytest.fixture(autouse=True)
def _reset(self):
self.test_flag = False
async def callback(self, update, context):
self.test_flag = (
isinstance(context, CallbackContext)
and isinstance(context.bot, Bot)
and isinstance(update, Update)
and isinstance(context.update_queue, asyncio.Queue)
and isinstance(context.job_queue, JobQueue)
and isinstance(context.chat_data, dict)
and isinstance(context.bot_data, dict)
and isinstance(
update.deleted_business_messages,
BusinessMessagesDeleted,
)
)
def test_with_chat_id(self, business_messages_deleted_update):
handler = BusinessMessagesDeletedHandler(self.callback, chat_id=1)
assert handler.check_update(business_messages_deleted_update)
handler = BusinessMessagesDeletedHandler(self.callback, chat_id=[1])
assert handler.check_update(business_messages_deleted_update)
handler = BusinessMessagesDeletedHandler(self.callback, chat_id=2, username="@user_a")
assert handler.check_update(business_messages_deleted_update)
handler = BusinessMessagesDeletedHandler(self.callback, chat_id=2)
assert not handler.check_update(business_messages_deleted_update)
handler = BusinessMessagesDeletedHandler(self.callback, chat_id=[2])
assert not handler.check_update(business_messages_deleted_update)
def test_with_username(self, business_messages_deleted_update):
handler = BusinessMessagesDeletedHandler(self.callback, username="user_a")
assert handler.check_update(business_messages_deleted_update)
handler = BusinessMessagesDeletedHandler(self.callback, username="@user_a")
assert handler.check_update(business_messages_deleted_update)
handler = BusinessMessagesDeletedHandler(self.callback, username=["user_a"])
assert handler.check_update(business_messages_deleted_update)
handler = BusinessMessagesDeletedHandler(self.callback, username=["@user_a"])
assert handler.check_update(business_messages_deleted_update)
handler = BusinessMessagesDeletedHandler(self.callback, chat_id=1, username="@user_b")
assert handler.check_update(business_messages_deleted_update)
handler = BusinessMessagesDeletedHandler(self.callback, username="user_b")
assert not handler.check_update(business_messages_deleted_update)
handler = BusinessMessagesDeletedHandler(self.callback, username="@user_b")
assert not handler.check_update(business_messages_deleted_update)
handler = BusinessMessagesDeletedHandler(self.callback, username=["user_b"])
assert not handler.check_update(business_messages_deleted_update)
handler = BusinessMessagesDeletedHandler(self.callback, username=["@user_b"])
assert not handler.check_update(business_messages_deleted_update)
business_messages_deleted_update.deleted_business_messages.chat._unfreeze()
business_messages_deleted_update.deleted_business_messages.chat.username = None
assert not handler.check_update(business_messages_deleted_update)
def test_other_update_types(self, false_update):
handler = BusinessMessagesDeletedHandler(self.callback)
assert not handler.check_update(false_update)
assert not handler.check_update(True)
async def test_context(self, app, business_messages_deleted_update):
handler = BusinessMessagesDeletedHandler(callback=self.callback)
app.add_handler(handler)
async with app:
await app.process_update(business_messages_deleted_update)
assert self.test_flag
+4 -2
View File
@@ -32,6 +32,7 @@ from telegram import (
from telegram.error import TelegramError
from telegram.ext import ApplicationBuilder, CallbackContext, Job
from telegram.warnings import PTBUserWarning
from tests.auxil.pytest_classes import make_bot
from tests.auxil.slots import mro_slots
"""
@@ -211,8 +212,9 @@ class TestCallbackContext:
finally:
app.bot = bot
async def test_drop_callback_data(self, bot, monkeypatch, chat_id):
app = ApplicationBuilder().token(bot.token).arbitrary_callback_data(True).build()
async def test_drop_callback_data(self, bot, chat_id):
new_bot = make_bot(token=bot.token, arbitrary_callback_data=True)
app = ApplicationBuilder().bot(new_bot).build()
update = Update(
0, message=Message(0, None, Chat(1, "chat"), from_user=User(1, "user", False))
+43
View File
@@ -2035,6 +2035,11 @@ class TestFilters:
update.message.is_automatic_forward = True
assert filters.IS_AUTOMATIC_FORWARD.check_update(update)
def test_filters_is_from_offline(self, update):
assert not filters.IS_FROM_OFFLINE.check_update(update)
update.message.is_from_offline = True
assert filters.IS_FROM_OFFLINE.check_update(update)
def test_filters_is_topic_message(self, update):
assert not filters.IS_TOPIC_MESSAGE.check_update(update)
update.message.is_topic_message = True
@@ -2343,6 +2348,9 @@ class TestFilters:
assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update)
assert not filters.UpdateType.CHANNEL_POSTS.check_update(update)
assert not filters.UpdateType.EDITED.check_update(update)
assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update)
assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update)
def test_update_type_edited_message(self, update):
update.edited_message, update.message = update.message, update.edited_message
@@ -2353,6 +2361,9 @@ class TestFilters:
assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update)
assert not filters.UpdateType.CHANNEL_POSTS.check_update(update)
assert filters.UpdateType.EDITED.check_update(update)
assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update)
assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update)
def test_update_type_channel_post(self, update):
update.channel_post, update.message = update.message, update.edited_message
@@ -2363,6 +2374,9 @@ class TestFilters:
assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update)
assert filters.UpdateType.CHANNEL_POSTS.check_update(update)
assert not filters.UpdateType.EDITED.check_update(update)
assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update)
assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update)
def test_update_type_edited_channel_post(self, update):
update.edited_channel_post, update.message = update.message, update.edited_message
@@ -2373,6 +2387,35 @@ class TestFilters:
assert filters.UpdateType.EDITED_CHANNEL_POST.check_update(update)
assert filters.UpdateType.CHANNEL_POSTS.check_update(update)
assert filters.UpdateType.EDITED.check_update(update)
assert not filters.UpdateType.BUSINESS_MESSAGES.check_update(update)
assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update)
def test_update_type_business_message(self, update):
update.business_message, update.message = update.message, update.edited_message
assert not filters.UpdateType.MESSAGE.check_update(update)
assert not filters.UpdateType.EDITED_MESSAGE.check_update(update)
assert not filters.UpdateType.MESSAGES.check_update(update)
assert not filters.UpdateType.CHANNEL_POST.check_update(update)
assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update)
assert not filters.UpdateType.CHANNEL_POSTS.check_update(update)
assert not filters.UpdateType.EDITED.check_update(update)
assert filters.UpdateType.BUSINESS_MESSAGES.check_update(update)
assert filters.UpdateType.BUSINESS_MESSAGE.check_update(update)
assert not filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update)
def test_update_type_edited_business_message(self, update):
update.edited_business_message, update.message = update.message, update.edited_message
assert not filters.UpdateType.MESSAGE.check_update(update)
assert not filters.UpdateType.EDITED_MESSAGE.check_update(update)
assert not filters.UpdateType.MESSAGES.check_update(update)
assert not filters.UpdateType.CHANNEL_POST.check_update(update)
assert not filters.UpdateType.EDITED_CHANNEL_POST.check_update(update)
assert not filters.UpdateType.CHANNEL_POSTS.check_update(update)
assert filters.UpdateType.EDITED.check_update(update)
assert filters.UpdateType.BUSINESS_MESSAGES.check_update(update)
assert not filters.UpdateType.BUSINESS_MESSAGE.check_update(update)
assert filters.UpdateType.EDITED_BUSINESS_MESSAGE.check_update(update)
def test_merged_short_circuit_and(self, update, base_class):
update.message.text = "/test"
+1 -23
View File
@@ -26,7 +26,6 @@ import time
import pytest
from telegram.ext import ApplicationBuilder, CallbackContext, ContextTypes, Defaults, Job, JobQueue
from telegram.warnings import PTBUserWarning
from tests.auxil.envvars import GITHUB_ACTION, TEST_WITH_OPT_DEPS
from tests.auxil.pytest_classes import make_bot
from tests.auxil.slots import mro_slots
@@ -80,11 +79,6 @@ class TestJobQueue:
job_time = 0
received_error = None
expected_warning = (
"Prior to v20.0 the `days` parameter was not aligned to that of cron's weekday scheme."
" We recommend double checking if the passed value is correct."
)
async def test_repr(self, app):
jq = JobQueue()
jq.set_application(app)
@@ -375,20 +369,8 @@ class TestJobQueue:
scheduled_time = job_queue.jobs()[0].next_t.timestamp()
assert scheduled_time == pytest.approx(expected_reschedule_time)
async def test_run_daily_warning(self, job_queue, recwarn):
delta, now = 1, dtm.datetime.now(UTC)
time_of_day = (now + dtm.timedelta(seconds=delta)).time()
job_queue.run_daily(self.job_run_once, time_of_day)
assert len(recwarn) == 0
job_queue.run_daily(self.job_run_once, time_of_day, days=(0, 1, 2, 3))
assert len(recwarn) == 1
assert str(recwarn[0].message) == self.expected_warning
assert recwarn[0].category is PTBUserWarning
assert recwarn[0].filename == __file__, "wrong stacklevel"
@pytest.mark.parametrize("weekday", [0, 1, 2, 3, 4, 5, 6])
async def test_run_daily_days_of_week(self, job_queue, recwarn, weekday):
async def test_run_daily_days_of_week(self, job_queue, weekday):
delta, now = 1, dtm.datetime.now(UTC)
time_of_day = (now + dtm.timedelta(seconds=delta)).time()
# offset in days until next weekday
@@ -400,10 +382,6 @@ class TestJobQueue:
await asyncio.sleep(delta + 0.1)
scheduled_time = job_queue.jobs()[0].next_t.timestamp()
assert scheduled_time == pytest.approx(expected_reschedule_time)
assert len(recwarn) == 1
assert str(recwarn[0].message) == self.expected_warning
assert recwarn[0].category is PTBUserWarning
assert recwarn[0].filename == __file__, "wrong stacklevel"
async def test_run_monthly(self, job_queue, timezone):
delta, now = 1, dtm.datetime.now(timezone)
+17 -7
View File
@@ -38,7 +38,16 @@ from tests.auxil.networking import send_webhook_message
from tests.auxil.pytest_classes import PytestBot, make_bot
from tests.auxil.slots import mro_slots
UNIX_AVAILABLE = False
if TEST_WITH_OPT_DEPS:
try:
from tornado.netutil import bind_unix_socket
UNIX_AVAILABLE = True
except ImportError:
UNIX_AVAILABLE = False
from telegram.ext._utils.webhookhandler import WebhookServer
@@ -692,13 +701,12 @@ class TestUpdater:
@pytest.mark.parametrize("ext_bot", [True, False])
@pytest.mark.parametrize("drop_pending_updates", [True, False])
@pytest.mark.parametrize("secret_token", ["SecretToken", None])
@pytest.mark.parametrize("unix", [None, True])
@pytest.mark.parametrize(
"unix", [None, "file_path", "socket_object"] if UNIX_AVAILABLE else [None]
)
async def test_webhook_basic(
self, monkeypatch, updater, drop_pending_updates, ext_bot, secret_token, unix, file_path
):
# Skipping unix test on windows since they fail
if unix and platform.system() == "Windows":
pytest.skip("Windows doesn't support unix bind")
# Testing with both ExtBot and Bot to make sure any logic in WebhookHandler
# that depends on this distinction works
if ext_bot and not isinstance(updater.bot, ExtBot):
@@ -723,11 +731,12 @@ class TestUpdater:
async with updater:
if unix:
socket = file_path if unix == "file_path" else bind_unix_socket(file_path)
return_value = await updater.start_webhook(
drop_pending_updates=drop_pending_updates,
secret_token=secret_token,
url_path="TOKEN",
unix=file_path,
unix=socket,
webhook_url="string",
)
else:
@@ -815,10 +824,11 @@ class TestUpdater:
# We call the same logic twice to make sure that restarting the updater works as well
if unix:
socket = file_path if unix == "file_path" else bind_unix_socket(file_path)
await updater.start_webhook(
drop_pending_updates=drop_pending_updates,
secret_token=secret_token,
unix=file_path,
unix=socket,
webhook_url="string",
)
else:
@@ -1039,7 +1049,7 @@ class TestUpdater:
assert updater.running is False
async def test_webhook_ssl_just_for_telegram(self, monkeypatch, updater):
"""Here we just test that the SSL info is pased to Telegram, but __not__ to the the
"""Here we just test that the SSL info is pased to Telegram, but __not__ to the
webhook server"""
async def set_webhook(**kwargs):
+1 -1
View File
@@ -163,7 +163,7 @@ class TestRequestParameterWithoutRequest:
assert request_parameter.input_files == [input_media.media, input_media.thumbnail]
def test_from_input_inputsticker(self):
input_sticker = InputSticker(data_file("telegram.png").read_bytes(), ["emoji"])
input_sticker = InputSticker(data_file("telegram.png").read_bytes(), ["emoji"], "static")
expected = input_sticker.to_dict()
expected.update({"sticker": input_sticker.sticker.attach_uri})
request_parameter = RequestParameter.from_input("key", input_sticker)
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
from datetime import datetime
import pytest
from telegram import Birthdate
from tests.auxil.slots import mro_slots
class TestBirthdateBase:
day = 1
month = 1
year = 2022
@pytest.fixture(scope="module")
def birthdate():
return Birthdate(TestBirthdateBase.day, TestBirthdateBase.month, TestBirthdateBase.year)
class TestBirthdateWithoutRequest(TestBirthdateBase):
def test_slot_behaviour(self, birthdate):
for attr in birthdate.__slots__:
assert getattr(birthdate, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(birthdate)) == len(set(mro_slots(birthdate))), "duplicate slot"
def test_to_dict(self, birthdate):
bd_dict = birthdate.to_dict()
assert isinstance(bd_dict, dict)
assert bd_dict["day"] == self.day
assert bd_dict["month"] == self.month
assert bd_dict["year"] == self.year
def test_de_json(self, bot):
json_dict = {"day": self.day, "month": self.month, "year": self.year}
bd = Birthdate.de_json(json_dict, bot)
assert isinstance(bd, Birthdate)
assert bd.day == self.day
assert bd.month == self.month
assert bd.year == self.year
def test_equality(self):
bd1 = Birthdate(1, 1, 2022)
bd2 = Birthdate(1, 1, 2022)
bd3 = Birthdate(1, 1, 2023)
bd4 = Birthdate(1, 2, 2022)
assert bd1 == bd2
assert hash(bd1) == hash(bd2)
assert bd1 == bd3
assert hash(bd1) == hash(bd3)
assert bd1 != bd4
assert hash(bd1) != hash(bd4)
def test_to_date(self, birthdate):
assert isinstance(birthdate.to_date(), datetime)
assert birthdate.to_date() == datetime(self.year, self.month, self.day)
new_bd = birthdate.to_date(2023)
assert new_bd == datetime(2023, self.month, self.day)
def test_to_date_no_year(self):
bd = Birthdate(1, 1)
with pytest.raises(ValueError, match="The `year` argument is required"):
bd.to_date()
+46 -19
View File
@@ -22,7 +22,6 @@ import datetime as dtm
import inspect
import logging
import pickle
import re
import socket
import time
from collections import defaultdict
@@ -40,6 +39,7 @@ from telegram import (
BotDescription,
BotName,
BotShortDescription,
BusinessConnection,
CallbackQuery,
Chat,
ChatAdministratorRights,
@@ -388,22 +388,10 @@ class TestBotWithoutRequest:
with pytest.raises(TypeError, match="Bot objects cannot be deepcopied"):
copy.deepcopy(bot)
@bot_methods(ext_bot=False)
def test_api_methods_have_log_decorator(self, bot_class, bot_method_name, bot_method):
"""Check that all bot methods have the log decorator ..."""
# not islower() skips the camelcase aliases
if not bot_method_name.islower():
return
source = inspect.getsource(bot_method)
assert (
# Use re.match to only match at *the beginning* of the string
re.match(rf"\s*\@\_log\s*async def {bot_method_name}", source)
), f"{bot_method_name} is missing the @_log decorator"
@pytest.mark.parametrize(
("cls", "logger_name"), [(Bot, "telegram.Bot"), (ExtBot, "telegram.ext.ExtBot")]
)
async def test_log_decorator(self, bot: PytestExtBot, cls, logger_name, caplog):
async def test_bot_method_logging(self, bot: PytestExtBot, cls, logger_name, caplog):
# Second argument makes sure that we ignore logs from e.g. httpx
with caplog.at_level(logging.DEBUG, logger="telegram"):
await cls(bot.token).get_me()
@@ -415,11 +403,19 @@ class TestBotWithoutRequest:
caplog.records.pop(idx)
if record.getMessage().startswith("Task exception was never retrieved"):
caplog.records.pop(idx)
assert len(caplog.records) == 3
assert len(caplog.records) == 2
assert all(caplog.records[i].name == logger_name for i in [-1, 0])
assert caplog.records[0].getMessage().startswith("Entering: get_me")
assert caplog.records[-1].getMessage().startswith("Exiting: get_me")
assert (
caplog.records[0]
.getMessage()
.startswith("Calling Bot API endpoint `getMe` with parameters `{}`")
)
assert (
caplog.records[-1]
.getMessage()
.startswith("Call to Bot API endpoint `getMe` finished with return value")
)
@bot_methods()
def test_camel_case_aliases(self, bot_class, bot_method_name, bot_method):
@@ -2092,6 +2088,37 @@ class TestBotWithoutRequest:
api_kwargs={"chat_id": 2, "user_id": 32, "until_date": until_timestamp},
)
async def test_business_connection_id_argument(self, bot, monkeypatch):
"""We can't connect to a business acc, so we just test that the correct data is passed.
We also can't test every single method easily, so we just test one. Our linting will catch
any unused args with the others."""
async def make_assertion(url, request_data: RequestData, *args, **kwargs):
return request_data.parameters.get("business_connection_id") == 42
monkeypatch.setattr(bot.request, "post", make_assertion)
assert await bot.send_message(2, "text", business_connection_id=42)
async def test_get_business_connection(self, bot, monkeypatch):
bci = "42"
user = User(1, "first", False)
user_chat_id = 1
date = dtm.datetime.utcnow()
can_reply = True
is_enabled = True
bc = BusinessConnection(bci, user, user_chat_id, date, can_reply, is_enabled).to_json()
async def do_request(*args, **kwargs):
data = kwargs.get("request_data")
obj = data.parameters.get("business_connection_id")
if obj == bci:
return 200, f'{{"ok": true, "result": {bc}}}'.encode()
return 400, b'{"ok": false, "result": []}'
monkeypatch.setattr(bot.request, "do_request", do_request)
obj = await bot.get_business_connection(business_connection_id=bci)
assert isinstance(obj, BusinessConnection)
class TestBotWithRequest:
"""
@@ -3378,8 +3405,8 @@ class TestBotWithRequest:
assert await bot.unpin_all_chat_messages(super_group_id, read_timeout=10)
# get_sticker_set, upload_sticker_file, create_new_sticker_set, add_sticker_to_set,
# set_sticker_position_in_set, delete_sticker_from_set and get_custom_emoji_stickers
# are tested in the test_sticker module.
# set_sticker_position_in_set, delete_sticker_from_set and get_custom_emoji_stickers,
# replace_sticker_in_set are tested in the test_sticker module.
# get_forum_topic_icon_stickers, edit_forum_topic, general_forum etc...
# are tested in the test_forum module.
+412
View File
@@ -0,0 +1,412 @@
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser Public License for more details.
#
# You should have received a copy of the GNU Lesser Public License
# along with this program. If not, see [http://www.gnu.org/licenses/].
from datetime import datetime
import pytest
from telegram import (
BusinessConnection,
BusinessIntro,
BusinessLocation,
BusinessMessagesDeleted,
BusinessOpeningHours,
BusinessOpeningHoursInterval,
Chat,
Location,
Sticker,
User,
)
from telegram._utils.datetime import UTC, to_timestamp
from tests.auxil.slots import mro_slots
class TestBusinessBase:
id_ = "123"
user = User(123, "test_user", False)
user_chat_id = 123
date = datetime.now(tz=UTC).replace(microsecond=0)
can_reply = True
is_enabled = True
message_ids = (123, 321)
business_connection_id = "123"
chat = Chat(123, "test_chat")
title = "Business Title"
message = "Business description"
sticker = Sticker("sticker_id", "unique_id", 50, 50, True, False, Sticker.REGULAR)
address = "address"
location = Location(-23.691288, 46.788279)
opening_minute = 0
closing_minute = 60
time_zone_name = "Country/City"
opening_hours = [
BusinessOpeningHoursInterval(opening, opening + 60) for opening in (0, 24 * 60)
]
@pytest.fixture(scope="module")
def business_connection():
return BusinessConnection(
TestBusinessBase.id_,
TestBusinessBase.user,
TestBusinessBase.user_chat_id,
TestBusinessBase.date,
TestBusinessBase.can_reply,
TestBusinessBase.is_enabled,
)
@pytest.fixture(scope="module")
def business_messages_deleted():
return BusinessMessagesDeleted(
TestBusinessBase.business_connection_id,
TestBusinessBase.chat,
TestBusinessBase.message_ids,
)
@pytest.fixture(scope="module")
def business_intro():
return BusinessIntro(
TestBusinessBase.title,
TestBusinessBase.message,
TestBusinessBase.sticker,
)
@pytest.fixture(scope="module")
def business_location():
return BusinessLocation(
TestBusinessBase.address,
TestBusinessBase.location,
)
@pytest.fixture(scope="module")
def business_opening_hours_interval():
return BusinessOpeningHoursInterval(
TestBusinessBase.opening_minute,
TestBusinessBase.closing_minute,
)
@pytest.fixture(scope="module")
def business_opening_hours():
return BusinessOpeningHours(
TestBusinessBase.time_zone_name,
TestBusinessBase.opening_hours,
)
class TestBusinessConnectionWithoutRequest(TestBusinessBase):
def test_slots(self, business_connection):
bc = business_connection
for attr in bc.__slots__:
assert getattr(bc, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(bc)) == len(set(mro_slots(bc))), "duplicate slot"
def test_de_json(self):
json_dict = {
"id": self.id_,
"user": self.user.to_dict(),
"user_chat_id": self.user_chat_id,
"date": to_timestamp(self.date),
"can_reply": self.can_reply,
"is_enabled": self.is_enabled,
}
bc = BusinessConnection.de_json(json_dict, None)
assert bc.id == self.id_
assert bc.user == self.user
assert bc.user_chat_id == self.user_chat_id
assert bc.date == self.date
assert bc.can_reply == self.can_reply
assert bc.is_enabled == self.is_enabled
assert bc.api_kwargs == {}
assert isinstance(bc, BusinessConnection)
def test_de_json_localization(self, bot, raw_bot, tz_bot):
json_dict = {
"id": self.id_,
"user": self.user.to_dict(),
"user_chat_id": self.user_chat_id,
"date": to_timestamp(self.date),
"can_reply": self.can_reply,
"is_enabled": self.is_enabled,
}
chat_bot = BusinessConnection.de_json(json_dict, bot)
chat_bot_raw = BusinessConnection.de_json(json_dict, raw_bot)
chat_bot_tz = BusinessConnection.de_json(json_dict, tz_bot)
# comparing utcoffsets because comparing tzinfo objects is not reliable
date_offset = chat_bot_tz.date.utcoffset()
date_offset_tz = tz_bot.defaults.tzinfo.utcoffset(chat_bot_tz.date.replace(tzinfo=None))
assert chat_bot.date.tzinfo == UTC
assert chat_bot_raw.date.tzinfo == UTC
assert date_offset_tz == date_offset
def test_to_dict(self, business_connection):
bc_dict = business_connection.to_dict()
assert isinstance(bc_dict, dict)
assert bc_dict["id"] == self.id_
assert bc_dict["user"] == self.user.to_dict()
assert bc_dict["user_chat_id"] == self.user_chat_id
assert bc_dict["date"] == to_timestamp(self.date)
assert bc_dict["can_reply"] == self.can_reply
assert bc_dict["is_enabled"] == self.is_enabled
def test_equality(self):
bc1 = BusinessConnection(
self.id_, self.user, self.user_chat_id, self.date, self.can_reply, self.is_enabled
)
bc2 = BusinessConnection(
self.id_, self.user, self.user_chat_id, self.date, self.can_reply, self.is_enabled
)
bc3 = BusinessConnection(
"321", self.user, self.user_chat_id, self.date, self.can_reply, self.is_enabled
)
assert bc1 == bc2
assert hash(bc1) == hash(bc2)
assert bc1 != bc3
assert hash(bc1) != hash(bc3)
class TestBusinessMessagesDeleted(TestBusinessBase):
def test_slots(self, business_messages_deleted):
bmd = business_messages_deleted
for attr in bmd.__slots__:
assert getattr(bmd, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(bmd)) == len(set(mro_slots(bmd))), "duplicate slot"
def test_to_dict(self, business_messages_deleted):
bmd_dict = business_messages_deleted.to_dict()
assert isinstance(bmd_dict, dict)
assert bmd_dict["message_ids"] == list(self.message_ids)
assert bmd_dict["business_connection_id"] == self.business_connection_id
assert bmd_dict["chat"] == self.chat.to_dict()
def test_de_json(self):
json_dict = {
"business_connection_id": self.business_connection_id,
"chat": self.chat.to_dict(),
"message_ids": self.message_ids,
}
bmd = BusinessMessagesDeleted.de_json(json_dict, None)
assert bmd.business_connection_id == self.business_connection_id
assert bmd.chat == self.chat
assert bmd.message_ids == self.message_ids
assert bmd.api_kwargs == {}
assert isinstance(bmd, BusinessMessagesDeleted)
def test_equality(self):
bmd1 = BusinessMessagesDeleted(self.business_connection_id, self.chat, self.message_ids)
bmd2 = BusinessMessagesDeleted(self.business_connection_id, self.chat, self.message_ids)
bmd3 = BusinessMessagesDeleted("1", Chat(4, "random"), [321, 123])
assert bmd1 == bmd2
assert hash(bmd1) == hash(bmd2)
assert bmd1 != bmd3
assert hash(bmd1) != hash(bmd3)
class TestBusinessIntroWithoutRequest(TestBusinessBase):
def test_slot_behaviour(self, business_intro):
intro = business_intro
for attr in intro.__slots__:
assert getattr(intro, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(intro)) == len(set(mro_slots(intro))), "duplicate slot"
def test_to_dict(self, business_intro):
intro_dict = business_intro.to_dict()
assert isinstance(intro_dict, dict)
assert intro_dict["title"] == self.title
assert intro_dict["message"] == self.message
assert intro_dict["sticker"] == self.sticker.to_dict()
def test_de_json(self):
json_dict = {
"title": self.title,
"message": self.message,
"sticker": self.sticker.to_dict(),
}
intro = BusinessIntro.de_json(json_dict, None)
assert intro.title == self.title
assert intro.message == self.message
assert intro.sticker == self.sticker
assert intro.api_kwargs == {}
assert isinstance(intro, BusinessIntro)
def test_equality(self):
intro1 = BusinessIntro(self.title, self.message, self.sticker)
intro2 = BusinessIntro(self.title, self.message, self.sticker)
intro3 = BusinessIntro("Other Business", self.message, self.sticker)
assert intro1 == intro2
assert hash(intro1) == hash(intro2)
assert intro1 is not intro2
assert intro1 != intro3
assert hash(intro1) != hash(intro3)
class TestBusinessLocationWithoutRequest(TestBusinessBase):
def test_slot_behaviour(self, business_location):
inst = business_location
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_to_dict(self, business_location):
blc_dict = business_location.to_dict()
assert isinstance(blc_dict, dict)
assert blc_dict["address"] == self.address
assert blc_dict["location"] == self.location.to_dict()
def test_de_json(self):
json_dict = {
"address": self.address,
"location": self.location.to_dict(),
}
blc = BusinessLocation.de_json(json_dict, None)
assert blc.address == self.address
assert blc.location == self.location
assert blc.api_kwargs == {}
assert isinstance(blc, BusinessLocation)
def test_equality(self):
blc1 = BusinessLocation(self.address, self.location)
blc2 = BusinessLocation(self.address, self.location)
blc3 = BusinessLocation("Other Address", self.location)
assert blc1 == blc2
assert hash(blc1) == hash(blc2)
assert blc1 is not blc2
assert blc1 != blc3
assert hash(blc1) != hash(blc3)
class TestBusinessOpeningHoursIntervalWithoutRequest(TestBusinessBase):
def test_slot_behaviour(self, business_opening_hours_interval):
inst = business_opening_hours_interval
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_to_dict(self, business_opening_hours_interval):
bohi_dict = business_opening_hours_interval.to_dict()
assert isinstance(bohi_dict, dict)
assert bohi_dict["opening_minute"] == self.opening_minute
assert bohi_dict["closing_minute"] == self.closing_minute
def test_de_json(self):
json_dict = {
"opening_minute": self.opening_minute,
"closing_minute": self.closing_minute,
}
bohi = BusinessOpeningHoursInterval.de_json(json_dict, None)
assert bohi.opening_minute == self.opening_minute
assert bohi.closing_minute == self.closing_minute
assert bohi.api_kwargs == {}
assert isinstance(bohi, BusinessOpeningHoursInterval)
def test_equality(self):
bohi1 = BusinessOpeningHoursInterval(self.opening_minute, self.closing_minute)
bohi2 = BusinessOpeningHoursInterval(self.opening_minute, self.closing_minute)
bohi3 = BusinessOpeningHoursInterval(61, 100)
assert bohi1 == bohi2
assert hash(bohi1) == hash(bohi2)
assert bohi1 is not bohi2
assert bohi1 != bohi3
assert hash(bohi1) != hash(bohi3)
@pytest.mark.parametrize(
("opening_minute", "expected"),
[ # openings per docstring
(8 * 60, (0, 8, 0)),
(24 * 60, (1, 0, 0)),
(6 * 24 * 60, (6, 0, 0)),
],
)
def test_opening_time(self, opening_minute, expected):
bohi = BusinessOpeningHoursInterval(opening_minute, -0)
opening_time = bohi.opening_time
assert opening_time == expected
cached = bohi.opening_time
assert cached is opening_time
@pytest.mark.parametrize(
("closing_minute", "expected"),
[ # closings per docstring
(20 * 60 + 30, (0, 20, 30)),
(2 * 24 * 60 - 1, (1, 23, 59)),
(7 * 24 * 60 - 2, (6, 23, 58)),
],
)
def test_closing_time(self, closing_minute, expected):
bohi = BusinessOpeningHoursInterval(-0, closing_minute)
closing_time = bohi.closing_time
assert closing_time == expected
cached = bohi.closing_time
assert cached is closing_time
class TestBusinessOpeningHoursWithoutRequest(TestBusinessBase):
def test_slot_behaviour(self, business_opening_hours):
inst = business_opening_hours
for attr in inst.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot"
def test_to_dict(self, business_opening_hours):
boh_dict = business_opening_hours.to_dict()
assert isinstance(boh_dict, dict)
assert boh_dict["time_zone_name"] == self.time_zone_name
assert boh_dict["opening_hours"] == [opening.to_dict() for opening in self.opening_hours]
def test_de_json(self):
json_dict = {
"time_zone_name": self.time_zone_name,
"opening_hours": [opening.to_dict() for opening in self.opening_hours],
}
boh = BusinessOpeningHours.de_json(json_dict, None)
assert boh.time_zone_name == self.time_zone_name
assert boh.opening_hours == tuple(self.opening_hours)
assert boh.api_kwargs == {}
assert isinstance(boh, BusinessOpeningHours)
def test_equality(self):
boh1 = BusinessOpeningHours(self.time_zone_name, self.opening_hours)
boh2 = BusinessOpeningHours(self.time_zone_name, self.opening_hours)
boh3 = BusinessOpeningHours("Other/Timezone", self.opening_hours)
assert boh1 == boh2
assert hash(boh1) == hash(boh2)
assert boh1 is not boh2
assert boh1 != boh3
assert hash(boh1) != hash(boh3)
+46
View File
@@ -21,7 +21,12 @@ import datetime
import pytest
from telegram import (
Birthdate,
Bot,
BusinessIntro,
BusinessLocation,
BusinessOpeningHours,
BusinessOpeningHoursInterval,
Chat,
ChatLocation,
ChatPermissions,
@@ -74,6 +79,11 @@ def chat(bot):
profile_background_custom_emoji_id=TestChatBase.profile_background_custom_emoji_id,
unrestrict_boost_count=TestChatBase.unrestrict_boost_count,
custom_emoji_sticker_set_name=TestChatBase.custom_emoji_sticker_set_name,
business_intro=TestChatBase.business_intro,
business_location=TestChatBase.business_location,
business_opening_hours=TestChatBase.business_opening_hours,
birthdate=Birthdate(1, 1),
personal_chat=TestChatBase.personal_chat,
)
chat.set_bot(bot)
chat._unfreeze()
@@ -113,12 +123,20 @@ class TestChatBase:
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(TestChatBase):
@@ -139,6 +157,9 @@ class TestChatWithoutRequest(TestChatBase):
"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,
@@ -162,6 +183,8 @@ class TestChatWithoutRequest(TestChatBase):
"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(),
}
chat = Chat.de_json(json_dict, bot)
@@ -174,6 +197,9 @@ class TestChatWithoutRequest(TestChatBase):
assert chat.permissions == self.permissions
assert chat.slow_mode_delay == self.slow_mode_delay
assert chat.bio == self.bio
assert chat.business_intro == self.business_intro
assert chat.business_location == self.business_location
assert chat.business_opening_hours == self.business_opening_hours
assert chat.has_protected_content == self.has_protected_content
assert chat.has_visible_history == self.has_visible_history
assert chat.has_private_forwards == self.has_private_forwards
@@ -202,6 +228,8 @@ class TestChatWithoutRequest(TestChatBase):
assert chat.profile_background_custom_emoji_id == self.profile_background_custom_emoji_id
assert chat.unrestrict_boost_count == self.unrestrict_boost_count
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
def test_de_json_localization(self, bot, raw_bot, tz_bot):
json_dict = {
@@ -234,6 +262,9 @@ class TestChatWithoutRequest(TestChatBase):
assert chat_dict["permissions"] == chat.permissions.to_dict()
assert chat_dict["slow_mode_delay"] == chat.slow_mode_delay
assert chat_dict["bio"] == chat.bio
assert chat_dict["business_intro"] == chat.business_intro.to_dict()
assert chat_dict["business_location"] == chat.business_location.to_dict()
assert chat_dict["business_opening_hours"] == chat.business_opening_hours.to_dict()
assert chat_dict["has_private_forwards"] == chat.has_private_forwards
assert chat_dict["has_protected_content"] == chat.has_protected_content
assert chat_dict["has_visible_history"] == chat.has_visible_history
@@ -267,6 +298,8 @@ class TestChatWithoutRequest(TestChatBase):
)
assert chat_dict["custom_emoji_sticker_set_name"] == chat.custom_emoji_sticker_set_name
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()
def test_always_tuples_attributes(self):
chat = Chat(
@@ -576,6 +609,19 @@ class TestChatWithoutRequest(TestChatBase):
custom_title = kwargs["custom_title"] == "custom_title"
return chat_id and user_id and custom_title
assert check_shortcut_signature(
Chat.set_administrator_custom_title,
Bot.set_chat_administrator_custom_title,
["chat_id"],
[],
)
assert await check_shortcut_call(
chat.set_administrator_custom_title,
chat.get_bot(),
"set_chat_administrator_custom_title",
)
assert await check_defaults_handling(chat.set_administrator_custom_title, chat.get_bot())
monkeypatch.setattr("telegram.Bot.set_chat_administrator_custom_title", make_assertion)
assert await chat.set_administrator_custom_title(user_id=42, custom_title="custom_title")
+5 -37
View File
@@ -98,38 +98,23 @@ class TestChatAdministratorRightsWithoutRequest:
a = ChatAdministratorRights(
True,
*((False,) * 11),
can_post_stories=False,
can_edit_stories=False,
can_delete_stories=False,
)
b = ChatAdministratorRights(
True,
*((False,) * 11),
can_post_stories=False,
can_edit_stories=False,
can_delete_stories=False,
)
c = ChatAdministratorRights(
*(False,) * 12,
can_post_stories=False,
can_edit_stories=False,
can_delete_stories=False,
)
d = ChatAdministratorRights(
True,
True,
*((False,) * 10),
can_post_stories=False,
can_edit_stories=False,
can_delete_stories=False,
)
e = ChatAdministratorRights(
True,
True,
*((False,) * 10),
can_post_stories=False,
can_edit_stories=False,
can_delete_stories=False,
)
assert a == b
@@ -156,9 +141,8 @@ class TestChatAdministratorRightsWithoutRequest:
True,
True,
True,
can_post_stories=True,
can_edit_stories=True,
can_delete_stories=True,
True,
True,
)
t = ChatAdministratorRights.all_rights()
# if the dirs are the same, the attributes will all be there
@@ -181,9 +165,9 @@ class TestChatAdministratorRightsWithoutRequest:
False,
False,
False,
can_post_stories=False,
can_edit_stories=False,
can_delete_stories=False,
False,
False,
False,
)
t = ChatAdministratorRights.no_rights()
# if the dirs are the same, the attributes will all be there
@@ -194,19 +178,3 @@ class TestChatAdministratorRightsWithoutRequest:
assert t[key] is False
# and as a finisher, make sure the default is different.
assert f != t
def test_depreciation_typeerror(self):
with pytest.raises(TypeError, match="must be set in order"):
ChatAdministratorRights(
*(False,) * 12,
)
with pytest.raises(TypeError, match="must be set in order"):
ChatAdministratorRights(*(False,) * 12, can_edit_stories=True)
with pytest.raises(TypeError, match="must be set in order"):
ChatAdministratorRights(*(False,) * 12, can_post_stories=True)
with pytest.raises(TypeError, match="must be set in order"):
ChatAdministratorRights(*(False,) * 12, can_delete_stories=True)
with pytest.raises(TypeError, match="must be set in order"):
ChatAdministratorRights(*(False,) * 12, can_edit_stories=True, can_post_stories=True)
with pytest.raises(TypeError, match="must be set in order"):
ChatAdministratorRights(*(False,) * 12, can_delete_stories=True, can_post_stories=True)
+3 -19
View File
@@ -89,14 +89,14 @@ def chat_member_administrator():
CMDefaults.can_promote_members,
CMDefaults.can_change_info,
CMDefaults.can_invite_users,
CMDefaults.can_post_stories,
CMDefaults.can_edit_stories,
CMDefaults.can_delete_stories,
CMDefaults.can_post_messages,
CMDefaults.can_edit_messages,
CMDefaults.can_pin_messages,
CMDefaults.can_manage_topics,
CMDefaults.custom_title,
CMDefaults.can_post_stories,
CMDefaults.can_edit_stories,
CMDefaults.can_delete_stories,
)
@@ -302,19 +302,3 @@ class TestChatMemberTypesWithoutRequest:
assert c != e
assert hash(c) != hash(e)
def test_deprecation_typeerror(self, chat_member_type):
with pytest.raises(TypeError, match="must be set in order"):
ChatMemberAdministrator(
*(False,) * 12,
)
with pytest.raises(TypeError, match="must be set in order"):
ChatMemberAdministrator(*(False,) * 12, can_edit_stories=True)
with pytest.raises(TypeError, match="must be set in order"):
ChatMemberAdministrator(*(False,) * 12, can_post_stories=True)
with pytest.raises(TypeError, match="must be set in order"):
ChatMemberAdministrator(*(False,) * 12, can_delete_stories=True)
with pytest.raises(TypeError, match="must be set in order"):
ChatMemberAdministrator(*(False,) * 12, can_edit_stories=True, can_post_stories=True)
with pytest.raises(TypeError, match="must be set in order"):
ChatMemberAdministrator(*(False,) * 12, can_delete_stories=True, can_post_stories=True)
+4 -4
View File
@@ -54,7 +54,6 @@ def old_chat_member(user):
def new_chat_member(user):
return ChatMemberAdministrator(
user,
TestChatMemberUpdatedBase.new_status,
True,
True,
True,
@@ -64,9 +63,10 @@ def new_chat_member(user):
True,
True,
True,
can_post_stories=True,
can_edit_stories=True,
can_delete_stories=True,
True,
True,
True,
custom_title=TestChatMemberUpdatedBase.new_status,
)
+3 -5
View File
@@ -149,7 +149,7 @@ class TestConstantsWithoutRequest:
if any(re.match(pattern, name) for pattern in patters):
return False
if name in {
return name not in {
"author_signature",
"api_kwargs",
"caption",
@@ -176,10 +176,8 @@ class TestConstantsWithoutRequest:
# attribute is deprecated, no need to add it to MessageType
"user_shared",
"via_bot",
}:
return False
return True
"is_from_offline",
}
@pytest.mark.parametrize(
"attribute",
+314 -48
View File
@@ -50,6 +50,7 @@ from telegram import (
PollOption,
ProximityAlertTriggered,
ReplyParameters,
SharedUser,
Sticker,
Story,
SuccessfulPayment,
@@ -68,6 +69,8 @@ from telegram import (
WebAppData,
)
from telegram._utils.datetime import UTC
from telegram._utils.defaultvalue import DEFAULT_NONE
from telegram._utils.types import ODVInput
from telegram.constants import ChatAction, ParseMode
from telegram.ext import Defaults
from telegram.warnings import PTBDeprecationWarning
@@ -89,6 +92,7 @@ def message(bot):
date=TestMessageBase.date,
chat=copy(TestMessageBase.chat),
from_user=copy(TestMessageBase.from_user),
business_connection_id="123456789",
)
message.set_bot(bot)
message._unfreeze()
@@ -218,7 +222,7 @@ def message(bot):
},
{"web_app_data": WebAppData("some_data", "some_button_text")},
{"message_thread_id": 123},
{"users_shared": UsersShared(1, [2, 3])},
{"users_shared": UsersShared(1, users=[SharedUser(2, "user2"), SharedUser(3, "user3")])},
{"chat_shared": ChatShared(3, 4)},
{
"giveaway": Giveaway(
@@ -263,6 +267,9 @@ def message(bot):
{"reply_to_story": Story(Chat(1, Chat.PRIVATE), 0)},
{"boost_added": ChatBoostAdded(100)},
{"sender_boost_count": 1},
{"is_from_offline": True},
{"sender_business_bot": User(1, "BusinessBot", True)},
{"business_connection_id": "123456789"},
],
ids=[
"reply",
@@ -328,6 +335,9 @@ def message(bot):
"reply_to_story",
"boost_added",
"sender_boost_count",
"sender_business_bot",
"business_connection_id",
"is_from_offline",
],
)
def message_params(bot, request):
@@ -482,6 +492,55 @@ class TestMessageWithoutRequest(TestMessageBase):
if reply_parameters is None or reply_parameters.message_id != 42:
pytest.fail(f"reply_parameters is {reply_parameters} but should be 42")
@staticmethod
async def check_thread_id_parsing(
message: Message, method, bot_method_name: str, args, monkeypatch
):
"""Used in testing reply_* below. Makes sure that meassage_thread_id is parsed
correctly."""
async def extract_message_thread_id(*args, **kwargs):
return kwargs.get("message_thread_id")
monkeypatch.setattr(message.get_bot(), bot_method_name, extract_message_thread_id)
for is_topic_message in (True, False):
message.is_topic_message = is_topic_message
message.message_thread_id = None
message_thread_id = await method(*args)
assert message_thread_id is None
message.message_thread_id = 99
message_thread_id = await method(*args)
assert message_thread_id == (99 if is_topic_message else None)
message_thread_id = await method(*args, message_thread_id=50)
assert message_thread_id == 50
message_thread_id = await method(*args, message_thread_id=None)
assert message_thread_id is None
if bot_method_name == "send_chat_action":
return
message_thread_id = await method(
*args,
do_quote=message.build_reply_arguments(
target_chat_id=123,
),
)
assert message_thread_id is None
for target_chat_id in (message.chat_id, message.chat.username):
message_thread_id = await method(
*args,
do_quote=message.build_reply_arguments(
target_chat_id=target_chat_id,
),
)
assert message_thread_id == (message.message_thread_id if is_topic_message else None)
def test_slot_behaviour(self):
message = Message(
message_id=TestMessageBase.id_,
@@ -1344,16 +1403,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_text,
Bot.send_message,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_text,
message.get_bot(),
"send_message",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_text, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_text, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_message", make_assertion)
assert await message.reply_text("test")
@@ -1361,6 +1424,10 @@ class TestMessageWithoutRequest(TestMessageBase):
message, message.reply_text, "send_message", ["test"], monkeypatch
)
await self.check_thread_id_parsing(
message, message.reply_text, "send_message", ["test"], monkeypatch
)
async def test_reply_markdown(self, monkeypatch, message):
test_md_string = (
r"Test for <*bold*, _ita_\__lic_, `code`, "
@@ -1378,16 +1445,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_markdown,
Bot.send_message,
["chat_id", "parse_mode", "reply_to_message_id"],
["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_text,
message.get_bot(),
"send_message",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_text, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_text, message.get_bot())
text_markdown = self.test_message.text_markdown
assert text_markdown == test_md_string
@@ -1395,6 +1466,10 @@ class TestMessageWithoutRequest(TestMessageBase):
monkeypatch.setattr(message.get_bot(), "send_message", make_assertion)
assert await message.reply_markdown(self.test_message.text_markdown)
await self.check_thread_id_parsing(
message, message.reply_markdown, "send_message", ["test"], monkeypatch
)
async def test_reply_markdown_v2(self, monkeypatch, message):
test_md_string = (
r"__Test__ for <*bold*, _ita\_lic_, `\\\`code`, "
@@ -1416,16 +1491,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_markdown_v2,
Bot.send_message,
["chat_id", "parse_mode", "reply_to_message_id"],
["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_text,
message.get_bot(),
"send_message",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_text, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_text, message.get_bot())
text_markdown = self.test_message_v2.text_markdown_v2
assert text_markdown == test_md_string
@@ -1436,6 +1515,10 @@ class TestMessageWithoutRequest(TestMessageBase):
message, message.reply_markdown_v2, "send_message", [test_md_string], monkeypatch
)
await self.check_thread_id_parsing(
message, message.reply_markdown_v2, "send_message", ["test"], monkeypatch
)
async def test_reply_html(self, monkeypatch, message):
test_html_string = (
"<u>Test</u> for &lt;<b>bold</b>, <i>ita_lic</i>, "
@@ -1459,16 +1542,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_html,
Bot.send_message,
["chat_id", "parse_mode", "reply_to_message_id"],
["chat_id", "parse_mode", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_text,
message.get_bot(),
"send_message",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_text, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_text, message.get_bot())
text_html = self.test_message_v2.text_html
assert text_html == test_html_string
@@ -1479,6 +1566,10 @@ class TestMessageWithoutRequest(TestMessageBase):
message, message.reply_html, "send_message", [test_html_string], monkeypatch
)
await self.check_thread_id_parsing(
message, message.reply_html, "send_message", ["test"], monkeypatch
)
async def test_reply_media_group(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -1488,16 +1579,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_media_group,
Bot.send_media_group,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_media_group,
message.get_bot(),
"send_media_group",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_media_group, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_media_group, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_media_group", make_assertion)
assert await message.reply_media_group(media="reply_media_group")
@@ -1509,6 +1604,14 @@ class TestMessageWithoutRequest(TestMessageBase):
monkeypatch,
)
await self.check_thread_id_parsing(
message,
message.reply_media_group,
"send_media_group",
["reply_media_group"],
monkeypatch,
)
async def test_reply_photo(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -1518,16 +1621,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_photo,
Bot.send_photo,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_photo,
message.get_bot(),
"send_photo",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_photo, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_photo, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_photo", make_assertion)
assert await message.reply_photo(photo="test_photo")
@@ -1535,6 +1642,10 @@ class TestMessageWithoutRequest(TestMessageBase):
message, message.reply_photo, "send_photo", ["test_photo"], monkeypatch
)
await self.check_thread_id_parsing(
message, message.reply_photo, "send_photo", ["test_photo"], monkeypatch
)
async def test_reply_audio(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -1544,16 +1655,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_audio,
Bot.send_audio,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_audio,
message.get_bot(),
"send_audio",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_audio, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_audio, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_audio", make_assertion)
assert await message.reply_audio(audio="test_audio")
@@ -1561,6 +1676,10 @@ class TestMessageWithoutRequest(TestMessageBase):
message, message.reply_audio, "send_audio", ["test_audio"], monkeypatch
)
await self.check_thread_id_parsing(
message, message.reply_audio, "send_audio", ["test_audio"], monkeypatch
)
async def test_reply_document(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -1570,16 +1689,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_document,
Bot.send_document,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_document,
message.get_bot(),
"send_document",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_document, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_document, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_document", make_assertion)
assert await message.reply_document(document="test_document")
@@ -1587,6 +1710,10 @@ class TestMessageWithoutRequest(TestMessageBase):
message, message.reply_document, "send_document", ["test_document"], monkeypatch
)
await self.check_thread_id_parsing(
message, message.reply_document, "send_document", ["test_document"], monkeypatch
)
async def test_reply_animation(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -1596,16 +1723,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_animation,
Bot.send_animation,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_animation,
message.get_bot(),
"send_animation",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_animation, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_animation, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_animation", make_assertion)
assert await message.reply_animation(animation="test_animation")
@@ -1613,6 +1744,10 @@ class TestMessageWithoutRequest(TestMessageBase):
message, message.reply_animation, "send_animation", ["test_animation"], monkeypatch
)
await self.check_thread_id_parsing(
message, message.reply_animation, "send_animation", ["test_animation"], monkeypatch
)
async def test_reply_sticker(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -1622,16 +1757,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_sticker,
Bot.send_sticker,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_sticker,
message.get_bot(),
"send_sticker",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_sticker, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_sticker, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_sticker", make_assertion)
assert await message.reply_sticker(sticker="test_sticker")
@@ -1639,6 +1778,10 @@ class TestMessageWithoutRequest(TestMessageBase):
message, message.reply_sticker, "send_sticker", ["test_sticker"], monkeypatch
)
await self.check_thread_id_parsing(
message, message.reply_sticker, "send_sticker", ["test_sticker"], monkeypatch
)
async def test_reply_video(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -1648,16 +1791,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_video,
Bot.send_video,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_video,
message.get_bot(),
"send_video",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_video, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_video, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_video", make_assertion)
assert await message.reply_video(video="test_video")
@@ -1665,6 +1812,10 @@ class TestMessageWithoutRequest(TestMessageBase):
message, message.reply_video, "send_video", ["test_video"], monkeypatch
)
await self.check_thread_id_parsing(
message, message.reply_video, "send_video", ["test_video"], monkeypatch
)
async def test_reply_video_note(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -1674,16 +1825,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_video_note,
Bot.send_video_note,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_video_note,
message.get_bot(),
"send_video_note",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_video_note, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_video_note, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_video_note", make_assertion)
assert await message.reply_video_note(video_note="test_video_note")
@@ -1691,6 +1846,10 @@ class TestMessageWithoutRequest(TestMessageBase):
message, message.reply_video_note, "send_video_note", ["test_video_note"], monkeypatch
)
await self.check_thread_id_parsing(
message, message.reply_video_note, "send_video_note", ["test_video_note"], monkeypatch
)
async def test_reply_voice(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -1700,16 +1859,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_voice,
Bot.send_voice,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_voice,
message.get_bot(),
"send_voice",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_voice, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_voice, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_voice", make_assertion)
assert await message.reply_voice(voice="test_voice")
@@ -1717,6 +1880,10 @@ class TestMessageWithoutRequest(TestMessageBase):
message, message.reply_voice, "send_voice", ["test_voice"], monkeypatch
)
await self.check_thread_id_parsing(
message, message.reply_voice, "send_voice", ["test_voice"], monkeypatch
)
async def test_reply_location(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -1726,16 +1893,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_location,
Bot.send_location,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_location,
message.get_bot(),
"send_location",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_location, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_location, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_location", make_assertion)
assert await message.reply_location(location="test_location")
@@ -1743,6 +1914,10 @@ class TestMessageWithoutRequest(TestMessageBase):
message, message.reply_location, "send_location", ["test_location"], monkeypatch
)
await self.check_thread_id_parsing(
message, message.reply_location, "send_location", ["test_location"], monkeypatch
)
async def test_reply_venue(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -1752,16 +1927,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_venue,
Bot.send_venue,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_venue,
message.get_bot(),
"send_venue",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_venue, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_venue, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_venue", make_assertion)
assert await message.reply_venue(venue="test_venue")
@@ -1769,6 +1948,10 @@ class TestMessageWithoutRequest(TestMessageBase):
message, message.reply_venue, "send_venue", ["test_venue"], monkeypatch
)
await self.check_thread_id_parsing(
message, message.reply_venue, "send_venue", ["test_venue"], monkeypatch
)
async def test_reply_contact(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -1778,16 +1961,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_contact,
Bot.send_contact,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_contact,
message.get_bot(),
"send_contact",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_contact, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_contact, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_contact", make_assertion)
assert await message.reply_contact(contact="test_contact")
@@ -1795,6 +1982,10 @@ class TestMessageWithoutRequest(TestMessageBase):
message, message.reply_contact, "send_contact", ["test_contact"], monkeypatch
)
await self.check_thread_id_parsing(
message, message.reply_contact, "send_contact", ["test_contact"], monkeypatch
)
async def test_reply_poll(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -1805,13 +1996,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_poll,
Bot.send_poll,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_poll, message.get_bot(), "send_poll", skip_params=["reply_to_message_id"]
message.reply_poll,
message.get_bot(),
"send_poll",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_poll, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_poll, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_poll", make_assertion)
assert await message.reply_poll(question="test_poll", options=["1", "2", "3"])
@@ -1819,6 +2017,10 @@ class TestMessageWithoutRequest(TestMessageBase):
message, message.reply_poll, "send_poll", ["test_poll", ["1", "2", "3"]], monkeypatch
)
await self.check_thread_id_parsing(
message, message.reply_poll, "send_poll", ["test_poll", ["1", "2", "3"]], monkeypatch
)
async def test_reply_dice(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -1828,13 +2030,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_dice,
Bot.send_dice,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_dice, message.get_bot(), "send_dice", skip_params=["reply_to_message_id"]
message.reply_dice,
message.get_bot(),
"send_dice",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_dice, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_dice, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_dice", make_assertion)
assert await message.reply_dice(disable_notification=True)
@@ -1846,6 +2055,10 @@ class TestMessageWithoutRequest(TestMessageBase):
monkeypatch,
)
await self.check_thread_id_parsing(
message, message.reply_dice, "send_dice", [], monkeypatch
)
async def test_reply_action(self, monkeypatch, message: Message):
async def make_assertion(*_, **kwargs):
id_ = kwargs["chat_id"] == message.chat_id
@@ -1853,16 +2066,33 @@ class TestMessageWithoutRequest(TestMessageBase):
return id_ and action
assert check_shortcut_signature(
Message.reply_chat_action, Bot.send_chat_action, ["chat_id", "reply_to_message_id"], []
Message.reply_chat_action,
Bot.send_chat_action,
["chat_id", "reply_to_message_id", "business_connection_id"],
[],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_chat_action, message.get_bot(), "send_chat_action"
message.reply_chat_action,
message.get_bot(),
"send_chat_action",
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_chat_action, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_chat_action, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_chat_action", make_assertion)
assert await message.reply_chat_action(action=ChatAction.TYPING)
await self.check_thread_id_parsing(
message,
message.reply_chat_action,
"send_chat_action",
[ChatAction.TYPING],
monkeypatch,
)
async def test_reply_game(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
return (
@@ -1872,13 +2102,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_game,
Bot.send_game,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_game, message.get_bot(), "send_game", skip_params=["reply_to_message_id"]
message.reply_game,
message.get_bot(),
"send_game",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_game, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_game, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_game", make_assertion)
assert await message.reply_game(game_short_name="test_game")
@@ -1886,6 +2123,14 @@ class TestMessageWithoutRequest(TestMessageBase):
message, message.reply_game, "send_game", ["test_game"], monkeypatch
)
await self.check_thread_id_parsing(
message,
message.reply_game,
"send_game",
["test_game"],
monkeypatch,
)
async def test_reply_invoice(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
title = kwargs["title"] == "title"
@@ -1900,16 +2145,20 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_invoice,
Bot.send_invoice,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(
message.reply_invoice,
message.get_bot(),
"send_invoice",
skip_params=["reply_to_message_id"],
shortcut_kwargs=["business_connection_id"],
)
assert await check_defaults_handling(
message.reply_invoice, message.get_bot(), no_default_kwargs={"message_thread_id"}
)
assert await check_defaults_handling(message.reply_invoice, message.get_bot())
monkeypatch.setattr(message.get_bot(), "send_invoice", make_assertion)
assert await message.reply_invoice(
@@ -1928,6 +2177,14 @@ class TestMessageWithoutRequest(TestMessageBase):
monkeypatch,
)
await self.check_thread_id_parsing(
message,
message.reply_invoice,
"send_invoice",
["title", "description", "payload", "provider_token", "currency", "prices"],
monkeypatch,
)
@pytest.mark.parametrize(("disable_notification", "protected"), [(False, True), (True, False)])
async def test_forward(self, monkeypatch, message, disable_notification, protected):
async def make_assertion(*_, **kwargs):
@@ -2017,8 +2274,9 @@ class TestMessageWithoutRequest(TestMessageBase):
assert check_shortcut_signature(
Message.reply_copy,
Bot.copy_message,
["chat_id", "reply_to_message_id"],
["chat_id", "reply_to_message_id", "business_connection_id"],
["quote", "do_quote", "reply_to_message_id"],
annotation_overrides={"message_thread_id": (ODVInput[int], DEFAULT_NONE)},
)
assert await check_shortcut_call(message.copy, message.get_bot(), "copy_message")
assert await check_defaults_handling(message.copy, message.get_bot())
@@ -2042,6 +2300,14 @@ class TestMessageWithoutRequest(TestMessageBase):
monkeypatch,
)
await self.check_thread_id_parsing(
message,
message.reply_copy,
"copy_message",
[123456, 456789],
monkeypatch,
)
async def test_edit_text(self, monkeypatch, message):
async def make_assertion(*_, **kwargs):
chat_id = kwargs["chat_id"] == message.chat_id
+3 -6
View File
@@ -132,9 +132,6 @@ def ptb_extra_params(object_name: str) -> set[str]:
# Mostly due to the value being fixed anyway
PTB_IGNORED_PARAMS = {
r"InlineQueryResult\w+": {"type"},
# TODO: Remove this in v21.0 (API 7.1) when this can stop being optional
r"ChatAdministratorRights": {"can_post_stories", "can_edit_stories", "can_delete_stories"},
r"ChatMemberAdministrator": {"can_post_stories", "can_edit_stories", "can_delete_stories"},
r"ChatMember\w+": {"status"},
r"PassportElementError\w+": {"source"},
"ForceReply": {"force_reply"},
@@ -170,9 +167,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]] = {
# TODO: Remove this in v21.0 (API 7.1) when this can stop being optional
r"ChatAdministratorRights": {"can_post_stories", "can_edit_stories", "can_delete_stories"},
r"ChatMemberAdministrator": {"can_post_stories", "can_edit_stories", "can_delete_stories"},
"create_new_sticker_set": {"sticker_format"}, # removed by bot api 7.2
"StickerSet": {"is_animated", "is_video"}, # removed by bot api 7.2
"UsersShared": {"user_ids", "users"}, # removed/added by bot api 7.2
}
+132 -11
View File
@@ -19,19 +19,20 @@
import pytest
from telegram import ChatShared, UsersShared
from telegram import ChatShared, PhotoSize, SharedUser, UsersShared
from telegram.warnings import PTBDeprecationWarning
from tests.auxil.slots import mro_slots
@pytest.fixture(scope="class")
def users_shared():
return UsersShared(TestUsersSharedBase.request_id, TestUsersSharedBase.user_ids)
return UsersShared(TestUsersSharedBase.request_id, users=TestUsersSharedBase.users)
class TestUsersSharedBase:
request_id = 789
user_id = 101112
user_ids = (user_id, 101113)
user_ids = (101112, 101113)
users = (SharedUser(101112, "user1"), SharedUser(101113, "user2"))
class TestUsersSharedWithoutRequest(TestUsersSharedBase):
@@ -45,24 +46,43 @@ class TestUsersSharedWithoutRequest(TestUsersSharedBase):
assert isinstance(users_shared_dict, dict)
assert users_shared_dict["request_id"] == self.request_id
assert users_shared_dict["user_ids"] == list(self.user_ids)
assert users_shared_dict["users"] == [user.to_dict() for user in self.users]
def test_de_json(self, bot):
json_dict = {
"request_id": self.request_id,
"users": [user.to_dict() for user in self.users],
"user_ids": self.user_ids,
}
users_shared = UsersShared.de_json(json_dict, bot)
assert users_shared.api_kwargs == {}
assert users_shared.api_kwargs == {"user_ids": self.user_ids}
assert users_shared.request_id == self.request_id
assert users_shared.users == self.users
assert users_shared.user_ids == tuple(self.user_ids)
assert UsersShared.de_json({}, bot) is None
def test_users_is_required_argument(self):
with pytest.raises(TypeError, match="`users` is a required argument"):
UsersShared(self.request_id, user_ids=self.user_ids)
def test_user_ids_deprecation_warning(self):
with pytest.warns(
PTBDeprecationWarning, match="'user_ids' was renamed to 'users' in Bot API 7.2"
):
users_shared = UsersShared(self.request_id, user_ids=self.user_ids, users=self.users)
with pytest.warns(
PTBDeprecationWarning, match="renamed the attribute 'user_ids' to 'users'"
):
users_shared.user_ids
def test_equality(self):
a = UsersShared(self.request_id, self.user_ids)
b = UsersShared(self.request_id, self.user_ids)
c = UsersShared(1, self.user_ids)
d = UsersShared(self.request_id, [1, 2])
a = UsersShared(self.request_id, users=self.users)
b = UsersShared(self.request_id, users=self.users)
c = UsersShared(1, users=self.users)
d = UsersShared(self.request_id, users=(SharedUser(1, "user1"), SharedUser(1, "user2")))
e = PhotoSize("file_id", "1", 1, 1)
assert a == b
assert hash(a) == hash(b)
@@ -74,6 +94,9 @@ class TestUsersSharedWithoutRequest(TestUsersSharedBase):
assert a != d
assert hash(a) != hash(d)
assert a != e
assert hash(a) != hash(e)
@pytest.fixture(scope="class")
def chat_shared():
@@ -112,11 +135,109 @@ class TestChatSharedWithoutRequest(TestChatSharedBase):
assert chat_shared.request_id == self.request_id
assert chat_shared.chat_id == self.chat_id
def test_equality(self):
def test_equality(self, users_shared):
a = ChatShared(self.request_id, self.chat_id)
b = ChatShared(self.request_id, self.chat_id)
c = ChatShared(1, self.chat_id)
d = ChatShared(self.request_id, 1)
e = users_shared
assert a == b
assert hash(a) == hash(b)
assert a is not b
assert a != c
assert hash(a) != hash(c)
assert a != d
assert hash(a) != hash(d)
assert a != e
assert hash(a) != hash(e)
@pytest.fixture(scope="class")
def shared_user():
return SharedUser(
TestSharedUserBase.user_id,
TestSharedUserBase.first_name,
last_name=TestSharedUserBase.last_name,
username=TestSharedUserBase.username,
photo=TestSharedUserBase.photo,
)
class TestSharedUserBase:
user_id = 101112
first_name = "first"
last_name = "last"
username = "user"
photo = (
PhotoSize(file_id="file_id", width=1, height=1, file_unique_id="1"),
PhotoSize(file_id="file_id", width=2, height=2, file_unique_id="2"),
)
class TestSharedUserWithoutRequest(TestSharedUserBase):
def test_slot_behaviour(self, shared_user):
for attr in shared_user.__slots__:
assert getattr(shared_user, attr, "err") != "err", f"got extra slot '{attr}'"
assert len(mro_slots(shared_user)) == len(set(mro_slots(shared_user))), "duplicate slot"
def test_to_dict(self, shared_user):
shared_user_dict = shared_user.to_dict()
assert isinstance(shared_user_dict, dict)
assert shared_user_dict["user_id"] == self.user_id
assert shared_user_dict["first_name"] == self.first_name
assert shared_user_dict["last_name"] == self.last_name
assert shared_user_dict["username"] == self.username
assert shared_user_dict["photo"] == [photo.to_dict() for photo in self.photo]
def test_de_json_required(self, bot):
json_dict = {
"user_id": self.user_id,
"first_name": self.first_name,
}
shared_user = SharedUser.de_json(json_dict, bot)
assert shared_user.api_kwargs == {}
assert shared_user.user_id == self.user_id
assert shared_user.first_name == self.first_name
assert shared_user.last_name is None
assert shared_user.username is None
assert shared_user.photo == ()
def test_de_json_all(self, bot):
json_dict = {
"user_id": self.user_id,
"first_name": self.first_name,
"last_name": self.last_name,
"username": self.username,
"photo": [photo.to_dict() for photo in self.photo],
}
shared_user = SharedUser.de_json(json_dict, bot)
assert shared_user.api_kwargs == {}
assert shared_user.user_id == self.user_id
assert shared_user.first_name == self.first_name
assert shared_user.last_name == self.last_name
assert shared_user.username == self.username
assert shared_user.photo == self.photo
assert SharedUser.de_json({}, bot) is None
def test_equality(self, chat_shared):
a = SharedUser(
self.user_id,
self.first_name,
last_name=self.last_name,
username=self.username,
photo=self.photo,
)
b = SharedUser(self.user_id, "other_firs_name")
c = SharedUser(self.user_id + 1, self.first_name)
d = chat_shared
assert a == b
assert hash(a) == hash(b)
+127 -3
View File
@@ -17,11 +17,14 @@
# 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 time
from copy import deepcopy
from datetime import datetime
import pytest
from telegram import (
BusinessConnection,
BusinessMessagesDeleted,
CallbackQuery,
Chat,
ChatBoost,
@@ -51,7 +54,21 @@ from telegram._utils.datetime import from_timestamp
from telegram.warnings import PTBUserWarning
from tests.auxil.slots import mro_slots
message = Message(1, datetime.utcnow(), Chat(1, ""), from_user=User(1, "", False), text="Text")
message = Message(
1,
datetime.utcnow(),
Chat(1, ""),
from_user=User(1, "", False),
text="Text",
sender_chat=Chat(1, ""),
)
channel_post = Message(
1,
datetime.utcnow(),
Chat(1, ""),
text="Text",
sender_chat=Chat(1, ""),
)
chat_member_updated = ChatMemberUpdated(
Chat(1, "chat"),
User(1, "", False),
@@ -93,6 +110,7 @@ message_reaction = MessageReactionUpdated(
old_reaction=(ReactionTypeEmoji("👍"),),
new_reaction=(ReactionTypeEmoji("👍"),),
user=User(1, "name", False),
actor_chat=Chat(1, ""),
)
@@ -103,13 +121,35 @@ message_reaction_count = MessageReactionCountUpdated(
reactions=(ReactionCount(ReactionTypeEmoji("👍"), 1),),
)
business_connection = BusinessConnection(
"1",
User(1, "name", False),
1,
from_timestamp(int(time.time())),
True,
True,
)
deleted_business_messages = BusinessMessagesDeleted(
"1",
Chat(1, ""),
(1, 2),
)
business_message = Message(
1,
datetime.utcnow(),
Chat(1, ""),
User(1, "", False),
)
params = [
{"message": message},
{"edited_message": message},
{"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)},
{"channel_post": message},
{"edited_channel_post": message},
{"channel_post": channel_post},
{"edited_channel_post": channel_post},
{"inline_query": InlineQuery(1, User(1, "", False), "", "")},
{"chosen_inline_result": ChosenInlineResult("id", User(1, "", False), "")},
{"shipping_query": ShippingQuery("id", User(1, "", False), "", None)},
@@ -134,6 +174,10 @@ params = [
{"removed_chat_boost": removed_chat_boost},
{"message_reaction": message_reaction},
{"message_reaction_count": message_reaction_count},
{"business_connection": business_connection},
{"deleted_business_messages": deleted_business_messages},
{"business_message": business_message},
{"edited_business_message": business_message},
# Must be last to conform with `ids` below!
{"callback_query": CallbackQuery(1, User(1, "", False), "chat")},
]
@@ -157,6 +201,10 @@ all_types = (
"removed_chat_boost",
"message_reaction",
"message_reaction_count",
"business_connection",
"deleted_business_messages",
"business_message",
"edited_business_message",
)
ids = (*all_types, "callback_query_without_message")
@@ -241,6 +289,7 @@ class TestUpdateWithoutRequest(TestUpdateBase):
or update.pre_checkout_query is not None
or update.poll is not None
or update.poll_answer is not None
or update.business_connection is not None
):
assert chat.id == 1
else:
@@ -256,11 +305,84 @@ class TestUpdateWithoutRequest(TestUpdateBase):
or update.chat_boost is not None
or update.removed_chat_boost is not None
or update.message_reaction_count is not None
or update.deleted_business_messages is not None
):
assert user.id == 1
else:
assert user is None
def test_effective_sender_non_anonymous(self, update):
update = deepcopy(update)
# Simulate 'Remain anonymous' being turned off
if message := (update.message or update.edited_message):
message._unfreeze()
message.sender_chat = None
elif reaction := (update.message_reaction):
reaction._unfreeze()
reaction.actor_chat = None
elif answer := (update.poll_answer):
answer._unfreeze()
answer.voter_chat = None
# Test that it's sometimes None per docstring
sender = update.effective_sender
if not (
update.poll is not None
or update.chat_boost is not None
or update.removed_chat_boost is not None
or update.message_reaction_count is not None
or update.deleted_business_messages is not None
):
if update.channel_post or update.edited_channel_post:
assert isinstance(sender, Chat)
else:
assert isinstance(sender, User)
else:
assert sender is None
cached = update.effective_sender
assert cached is sender
def test_effective_sender_anonymous(self, update):
update = deepcopy(update)
# Simulate 'Remain anonymous' being turned on
if message := (update.message or update.edited_message):
message._unfreeze()
message.from_user = None
elif reaction := (update.message_reaction):
reaction._unfreeze()
reaction.user = None
elif answer := (update.poll_answer):
answer._unfreeze()
answer.user = None
# Test that it's sometimes None per docstring
sender = update.effective_sender
if not (
update.poll is not None
or update.chat_boost is not None
or update.removed_chat_boost is not None
or update.message_reaction_count is not None
or update.deleted_business_messages is not None
):
if (
update.message
or update.edited_message
or update.channel_post
or update.edited_channel_post
or update.message_reaction
or update.poll_answer
):
assert isinstance(sender, Chat)
else:
assert isinstance(sender, User)
else:
assert sender is None
cached = update.effective_sender
assert cached is sender
def test_effective_message(self, update):
# Test that it's sometimes None per docstring
eff_message = update.effective_message
@@ -279,6 +401,8 @@ class TestUpdateWithoutRequest(TestUpdateBase):
or update.removed_chat_boost is not None
or update.message_reaction is not None
or update.message_reaction_count is not None
or update.deleted_business_messages is not None
or update.business_connection is not None
):
assert eff_message.message_id == message.message_id
else:
+5
View File
@@ -42,6 +42,7 @@ def json_dict():
"supports_inline_queries": TestUserBase.supports_inline_queries,
"is_premium": TestUserBase.is_premium,
"added_to_attachment_menu": TestUserBase.added_to_attachment_menu,
"can_connect_to_business": TestUserBase.can_connect_to_business,
}
@@ -59,6 +60,7 @@ def user(bot):
supports_inline_queries=TestUserBase.supports_inline_queries,
is_premium=TestUserBase.is_premium,
added_to_attachment_menu=TestUserBase.added_to_attachment_menu,
can_connect_to_business=TestUserBase.can_connect_to_business,
)
user.set_bot(bot)
user._unfreeze()
@@ -77,6 +79,7 @@ class TestUserBase:
supports_inline_queries = False
is_premium = True
added_to_attachment_menu = False
can_connect_to_business = True
class TestUserWithoutRequest(TestUserBase):
@@ -100,6 +103,7 @@ class TestUserWithoutRequest(TestUserBase):
assert user.supports_inline_queries == self.supports_inline_queries
assert user.is_premium == self.is_premium
assert user.added_to_attachment_menu == self.added_to_attachment_menu
assert user.can_connect_to_business == self.can_connect_to_business
def test_to_dict(self, user):
user_dict = user.to_dict()
@@ -116,6 +120,7 @@ class TestUserWithoutRequest(TestUserBase):
assert user_dict["supports_inline_queries"] == user.supports_inline_queries
assert user_dict["is_premium"] == user.is_premium
assert user_dict["added_to_attachment_menu"] == user.added_to_attachment_menu
assert user_dict["can_connect_to_business"] == user.can_connect_to_business
def test_equality(self):
a = User(self.id_, self.first_name, self.is_bot, self.last_name)