diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b62fa9d4880..e1809bc7acb 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -45,36 +45,35 @@ jobs:
# Test without passport
pytest -v --cov -k test_no_passport.py
status=$?
-
+
# test without pytz
pytest -v --cov --cov-append -k test_datetime.py
status=$(( $? > status ? $? : status))
pytest -v --cov --cov-append -k test_defaults.py
status=$(( $? > status ? $? : status))
-
+
# test without pytz & jobqueue
pytest -v --cov --cov-append -k test_jobqueue.py
pytest -v --cov --cov-append -k test_applicationbuilder.py
status=$(( $? > status ? $? : status))
-
+
# Test without ratelimiter
pytest -v --cov --cov-append -k test_ratelimiter.py
status=$(( $? > status ? $? : status))
-
+
# Test without webhooks
pytest -v --cov --cov-append -k test_updater.py
status=$(( $? > status ? $? : status))
-
+
# Test without callback-data
pytest -v --cov --cov-append -k test_callbackdatacache.py
status=$(( $? > status ? $? : status))
-
+
# Test without socks
pytest -v --cov --cov-append -k test_request.py
status=$(( $? > status ? $? : status))
-
+
# Test the rest
-
export TEST_WITH_OPT_DEPS='true'
pip install -r requirements-opts.txt
# `-n auto --dist loadfile` uses pytest-xdist to run each test file on a different CPU
diff --git a/README.rst b/README.rst
index 021e267e04a..542c80ae450 100644
--- a/README.rst
+++ b/README.rst
@@ -14,7 +14,7 @@
:target: https://pypi.org/project/python-telegram-bot/
:alt: Supported Python versions
-.. image:: https://img.shields.io/badge/Bot%20API-6.2-blue?logo=telegram
+.. image:: https://img.shields.io/badge/Bot%20API-6.3-blue?logo=telegram
:target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API versions
diff --git a/README_RAW.rst b/README_RAW.rst
index 69d9642eb66..e71a2095a9c 100644
--- a/README_RAW.rst
+++ b/README_RAW.rst
@@ -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-6.2-blue?logo=telegram
+.. image:: https://img.shields.io/badge/Bot%20API-6.3-blue?logo=telegram
:target: https://core.telegram.org/bots/api-changelog
:alt: Supported Bot API versions
diff --git a/docs/source/inclusions/bot_methods.rst b/docs/source/inclusions/bot_methods.rst
index df699e796aa..aff45cc164f 100644
--- a/docs/source/inclusions/bot_methods.rst
+++ b/docs/source/inclusions/bot_methods.rst
@@ -256,6 +256,35 @@
+.. raw:: html
+
+
+ Forum topic management
+
+.. list-table::
+ :align: left
+ :widths: 1 4
+
+ * - :meth:`~telegram.Bot.close_forum_topic`
+ - Used for closing a forum topic
+ * - :meth:`~telegram.Bot.create_forum_topic`
+ - Used to create a topic
+ * - :meth:`~telegram.Bot.delete_forum_topic`
+ - Used for deleting a forum topic
+ * - :meth:`~telegram.Bot.edit_forum_topic`
+ - Used to edit a topic
+ * - :meth:`~telegram.Bot.reopen_forum_topic`
+ - Used to reopen a topic
+ * - :meth:`~telegram.Bot.get_forum_topic_icon_stickers`
+ - Used to get custom emojis to use as topic icons
+ * - :meth:`~telegram.Bot.unpin_all_forum_topic_messages`
+ - Used to unpin all messages in a forum topic
+
+.. raw:: html
+
+
+
+
.. raw:: html
diff --git a/docs/source/telegram.at-tree.rst b/docs/source/telegram.at-tree.rst
index 4f2a4ec1a97..41fd5e6385a 100644
--- a/docs/source/telegram.at-tree.rst
+++ b/docs/source/telegram.at-tree.rst
@@ -35,6 +35,10 @@ Available Types
telegram.document
telegram.file
telegram.forcereply
+ telegram.forumtopic
+ telegram.forumtopicclosed
+ telegram.forumtopiccreated
+ telegram.forumtopicreopened
telegram.inlinekeyboardbutton
telegram.inlinekeyboardmarkup
telegram.inputfile
diff --git a/docs/source/telegram.forumtopic.rst b/docs/source/telegram.forumtopic.rst
new file mode 100644
index 00000000000..a2dd6389c77
--- /dev/null
+++ b/docs/source/telegram.forumtopic.rst
@@ -0,0 +1,6 @@
+telegram.ForumTopic
+===================
+
+.. autoclass:: telegram.ForumTopic
+ :members:
+ :show-inheritance:
diff --git a/docs/source/telegram.forumtopicclosed.rst b/docs/source/telegram.forumtopicclosed.rst
new file mode 100644
index 00000000000..198a6737255
--- /dev/null
+++ b/docs/source/telegram.forumtopicclosed.rst
@@ -0,0 +1,6 @@
+telegram.ForumTopicClosed
+=========================
+
+.. autoclass:: telegram.ForumTopicClosed
+ :members:
+ :show-inheritance:
diff --git a/docs/source/telegram.forumtopiccreated.rst b/docs/source/telegram.forumtopiccreated.rst
new file mode 100644
index 00000000000..f1da6f81d37
--- /dev/null
+++ b/docs/source/telegram.forumtopiccreated.rst
@@ -0,0 +1,6 @@
+telegram.ForumTopicCreated
+==========================
+
+.. autoclass:: telegram.ForumTopicCreated
+ :members:
+ :show-inheritance:
diff --git a/docs/source/telegram.forumtopicreopened.rst b/docs/source/telegram.forumtopicreopened.rst
new file mode 100644
index 00000000000..b81c395d254
--- /dev/null
+++ b/docs/source/telegram.forumtopicreopened.rst
@@ -0,0 +1,6 @@
+telegram.ForumTopicReopened
+===========================
+
+.. autoclass:: telegram.ForumTopicReopened
+ :members:
+ :show-inheritance:
diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst
index 75c1c90cbe7..c021020a799 100644
--- a/docs/substitutions/global.rst
+++ b/docs/substitutions/global.rst
@@ -26,6 +26,10 @@
.. |chat_id_group| replace:: Unique identifier for the target chat or username of the target supergroup (in the format ``@supergroupusername``).
+.. |message_thread_id| replace:: Unique identifier for the target message thread of the forum topic.
+
+.. |message_thread_id_arg| replace:: Unique identifier for the target message thread (topic) of the forum; for forum supergroups only.
+
.. |parse_mode| replace:: Mode for parsing entities. See :class:`telegram.constants.ParseMode` and `formatting options `__ for more details.
.. |allow_sending_without_reply| replace:: Pass :obj:`True`, if the message should be sent even if the specified replied-to message is not found.
diff --git a/telegram/__init__.py b/telegram/__init__.py
index 11b5e9b720f..c0230f076c8 100644
--- a/telegram/__init__.py
+++ b/telegram/__init__.py
@@ -67,6 +67,10 @@
"File",
"FileCredentials",
"ForceReply",
+ "ForumTopic",
+ "ForumTopicClosed",
+ "ForumTopicCreated",
+ "ForumTopicReopened",
"Game",
"GameHighScore",
"helpers",
@@ -230,6 +234,7 @@
from ._files.videonote import VideoNote
from ._files.voice import Voice
from ._forcereply import ForceReply
+from ._forumtopic import ForumTopic, ForumTopicClosed, ForumTopicCreated, ForumTopicReopened
from ._games.callbackgame import CallbackGame
from ._games.game import Game
from ._games.gamehighscore import GameHighScore
diff --git a/telegram/_bot.py b/telegram/_bot.py
index d7e83362f51..5ff220ee99a 100644
--- a/telegram/_bot.py
+++ b/telegram/_bot.py
@@ -74,6 +74,7 @@
from telegram._files.video import Video
from telegram._files.videonote import VideoNote
from telegram._files.voice import Voice
+from telegram._forumtopic import ForumTopic
from telegram._games.gamehighscore import GameHighScore
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from telegram._menubutton import MenuButton
@@ -440,6 +441,7 @@ async def _send_message(
reply_markup: ReplyMarkup = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -459,6 +461,9 @@ async def _send_message(
if reply_markup is not None:
data["reply_markup"] = reply_markup
+ if message_thread_id is not None:
+ data["message_thread_id"] = message_thread_id
+
result = await self._post(
endpoint,
data,
@@ -660,6 +665,7 @@ async def send_message(
reply_to_message_id: int = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_markup: ReplyMarkup = None,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -693,6 +699,9 @@ async def send_message(
:class:`ReplyKeyboardRemove` | :class:`ForceReply`, optional):
Additional interface options. An object for an inline keyboard, custom reply
keyboard, instructions to remove reply keyboard or to force a reply from the user.
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
Returns:
:class:`telegram.Message`: On success, the sent message is returned.
@@ -719,6 +728,7 @@ async def send_message(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -743,6 +753,7 @@ async def delete_message(
limitations:
- A message can only be deleted if it was sent less than 48 hours ago.
+ - Service messages about a supergroup, channel, or forum topic creation can't be deleted.
- A dice message in a private chat can only be deleted if it was sent more than 24
hours ago.
- Bots can delete outgoing messages in private chats, groups, and supergroups.
@@ -787,6 +798,7 @@ async def forward_message(
message_id: int,
disable_notification: DVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -818,6 +830,9 @@ async def forward_message(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
@@ -838,6 +853,7 @@ async def forward_message(
data,
disable_notification=disable_notification,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -858,6 +874,7 @@ async def send_photo(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -893,6 +910,9 @@ async def send_photo(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \
@@ -934,6 +954,7 @@ async def send_photo(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -958,6 +979,7 @@ async def send_audio(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1004,6 +1026,10 @@ async def send_audio(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
+
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \
@@ -1062,6 +1088,7 @@ async def send_audio(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1084,6 +1111,7 @@ async def send_document(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1129,6 +1157,10 @@ async def send_document(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
+
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \
@@ -1181,6 +1213,7 @@ async def send_document(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1198,6 +1231,7 @@ async def send_sticker(
reply_markup: ReplyMarkup = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = 20,
@@ -1228,6 +1262,10 @@ async def send_sticker(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
+
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \
@@ -1251,6 +1289,7 @@ async def send_sticker(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1276,6 +1315,7 @@ async def send_video(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1327,6 +1367,10 @@ async def send_video(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
+
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \
@@ -1386,6 +1430,7 @@ async def send_video(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1406,6 +1451,7 @@ async def send_video_note(
thumb: FileInput = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1450,6 +1496,10 @@ async def send_video_note(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
+
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \
@@ -1500,6 +1550,7 @@ async def send_video_note(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1524,6 +1575,7 @@ async def send_animation(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1579,6 +1631,10 @@ async def send_animation(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
+
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \
@@ -1627,6 +1683,7 @@ async def send_animation(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1648,6 +1705,7 @@ async def send_voice(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1693,6 +1751,10 @@ async def send_voice(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
+
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \
@@ -1736,6 +1798,7 @@ async def send_voice(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1754,6 +1817,7 @@ async def send_media_group(
reply_to_message_id: int = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = 20,
@@ -1784,6 +1848,10 @@ async def send_media_group(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
+
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -1847,6 +1915,9 @@ async def send_media_group(
if reply_to_message_id:
data["reply_to_message_id"] = reply_to_message_id
+ if message_thread_id:
+ data["message_thread_id"] = message_thread_id
+
result = await self._post(
"sendMediaGroup",
data,
@@ -1874,6 +1945,7 @@ async def send_location(
proximity_alert_radius: int = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
location: Location = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1910,6 +1982,10 @@ async def send_location(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
+
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \
@@ -1960,6 +2036,7 @@ async def send_location(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -2137,6 +2214,7 @@ async def send_venue(
google_place_type: str = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
venue: Venue = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2176,6 +2254,9 @@ async def send_venue(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -2242,6 +2323,7 @@ async def send_venue(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -2262,6 +2344,7 @@ async def send_contact(
vcard: str = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
contact: Contact = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2291,6 +2374,9 @@ async def send_contact(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
@@ -2346,6 +2432,7 @@ async def send_contact(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -2363,6 +2450,7 @@ async def send_game(
reply_markup: InlineKeyboardMarkup = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2383,6 +2471,10 @@ async def send_game(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
+
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for a new
@@ -2406,6 +2498,7 @@ async def send_game(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -4057,6 +4150,7 @@ async def send_invoice(
max_tip_amount: int = None,
suggested_tip_amounts: List[int] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -4147,6 +4241,10 @@ async def send_invoice(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
+
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): An object for an
@@ -4208,6 +4306,7 @@ async def send_invoice(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -4460,6 +4559,7 @@ async def promote_chat_member(
is_anonymous: bool = None,
can_manage_chat: bool = None,
can_manage_video_chats: bool = None,
+ can_manage_topics: bool = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -4513,6 +4613,10 @@ async def promote_chat_member(
add new administrators with a subset of his own privileges or demote administrators
that he has promoted, directly or indirectly (promoted by administrators that were
appointed by him).
+ can_manage_topics (:obj:`bool`, optional): Pass :obj:`True`, if the user is
+ allowed to create, rename, close, and reopen forum topics; supergroups only.
+
+ .. versionadded:: 20.0
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
@@ -4545,6 +4649,8 @@ async def promote_chat_member(
data["can_manage_chat"] = can_manage_chat
if can_manage_video_chats is not None:
data["can_manage_video_chats"] = can_manage_video_chats
+ if can_manage_topics is not None:
+ data["can_manage_topics"] = can_manage_topics
result = await self._post(
"promoteChatMember",
@@ -5111,7 +5217,9 @@ async def set_chat_title(
Args:
chat_id (:obj:`int` | :obj:`str`): |chat_id_channel|
- title (:obj:`str`): New chat title, 1-255 characters.
+ title (:obj:`str`): New chat title,
+ :tg-const:`telegram.constants.ChatLimit.MIN_CHAT_TITLE_LENGTH`-
+ :tg-const:`telegram.constants.ChatLimit.MAX_CHAT_TITLE_LENGTH` characters.
Returns:
:obj:`bool`: On success, :obj:`True` is returned.
@@ -5856,6 +5964,7 @@ async def send_poll(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
explanation_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -5908,6 +6017,10 @@ async def send_poll(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
+
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \
@@ -5956,6 +6069,7 @@ async def send_poll(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -6020,6 +6134,7 @@ async def send_dice(
emoji: str = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -6057,6 +6172,9 @@ async def send_dice(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
Returns:
:class:`telegram.Message`: On success, the sent Message is returned.
@@ -6077,6 +6195,7 @@ async def send_dice(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -6433,6 +6552,7 @@ async def copy_message(
allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
reply_markup: ReplyMarkup = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -6464,6 +6584,10 @@ async def copy_message(
protect_content (:obj:`bool`, optional): |protect_content|
.. versionadded:: 13.10
+ message_thread_id (:obj:`int`, optional): |message_thread_id_arg|
+
+ .. versionadded:: 20.0
+
reply_to_message_id (:obj:`int`, optional): |reply_to_msg_id|
allow_sending_without_reply (:obj:`bool`, optional): |allow_sending_without_reply|
reply_markup (:class:`InlineKeyboardMarkup` | :class:`ReplyKeyboardMarkup` | \
@@ -6495,6 +6619,8 @@ async def copy_message(
data["reply_to_message_id"] = reply_to_message_id
if reply_markup:
data["reply_markup"] = reply_markup
+ if message_thread_id:
+ data["message_thread_id"] = message_thread_id
result = await self._post(
"copyMessage",
@@ -6523,7 +6649,7 @@ async def set_chat_menu_button(
button.
.. seealso:: :meth:`get_chat_menu_button`, :meth:`telegram.Chat.set_menu_button`,
- :meth:`telegram.Chat.get_menu_button`, meth:`telegram.User.set_menu_button`,
+ :meth:`telegram.Chat.get_menu_button`, :meth:`telegram.User.set_menu_button`,
:meth:`telegram.User.get_menu_button`
.. versionadded:: 20.0
@@ -6734,6 +6860,349 @@ async def create_invoice_link(
api_kwargs=api_kwargs,
)
+ @_log
+ async def get_forum_topic_icon_stickers(
+ self,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> List[Sticker]:
+ """Use this method to get custom emoji stickers, which can be used as a forum topic
+ icon by any user. Requires no parameters.
+
+ .. versionadded:: 20.0
+
+ Returns:
+ List[:class:`telegram.Sticker`]
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ result = await self._post(
+ "getForumTopicIconStickers",
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+ return Sticker.de_list(result, self) # type: ignore[return-value, arg-type]
+
+ @_log
+ async def create_forum_topic(
+ self,
+ chat_id: Union[str, int],
+ name: str,
+ icon_color: int = None,
+ icon_custom_emoji_id: str = None,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> ForumTopic:
+ """
+ Use this method to create a topic in a forum supergroup chat. The bot must be
+ an administrator in the chat for this to work and must have
+ :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights.
+
+ .. seealso:: :meth:`telegram.Chat.create_forum_topic`,
+
+ .. versionadded:: 20.0
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): |chat_id_group|
+ name (:obj:`str`): New topic name,
+ :tg-const:`telegram.constants.ForumTopicLimit.MIN_NAME_LENGTH`-
+ :tg-const:`telegram.constants.ForumTopicLimit.MAX_NAME_LENGTH` characters.
+ icon_color (:obj:`int`, optional): Color of the topic icon in RGB format. Currently,
+ must be one of :attr:`telegram.constants.ForumIconColor.BLUE`,
+ :attr:`telegram.constants.ForumIconColor.YELLOW`,
+ :attr:`telegram.constants.ForumIconColor.PURPLE`,
+ :attr:`telegram.constants.ForumIconColor.GREEN`,
+ :attr:`telegram.constants.ForumIconColor.PINK`, or
+ :attr:`telegram.constants.ForumIconColor.RED`.
+ icon_custom_emoji_id (:obj:`str`, optional): New unique identifier of the custom emoji
+ shown as the topic icon. Use :meth:`~telegram.Bot.get_forum_topic_icon_stickers`
+ to get all allowed custom emoji identifiers.
+
+ Returns:
+ :class:`telegram.ForumTopic`
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+ """
+ data: JSONDict = {
+ "chat_id": chat_id,
+ "name": name,
+ "icon_color": icon_color,
+ "icon_custom_emoji_id": icon_custom_emoji_id,
+ }
+ result = await self._post(
+ "createForumTopic",
+ data,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+ return ForumTopic.de_json(result, self) # type: ignore[return-value, arg-type]
+
+ @_log
+ async def edit_forum_topic(
+ self,
+ chat_id: Union[str, int],
+ message_thread_id: int,
+ name: str,
+ icon_custom_emoji_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: JSONDict = None,
+ ) -> bool:
+ """
+ Use this method to edit name and icon of a topic in a forum supergroup chat. The bot must
+ be an administrator in the chat for this to work and must have
+ :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights,
+ unless it is the creator of the topic.
+
+ .. seealso:: :meth:`telegram.Message.edit_forum_topic`,
+ :meth:`telegram.Chat.edit_forum_topic`,
+
+ .. versionadded:: 20.0
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): |chat_id_group|
+ message_thread_id (:obj:`int`): |message_thread_id|
+ name (:obj:`str`): New topic name,
+ :tg-const:`telegram.constants.ForumTopicLimit.MIN_NAME_LENGTH`-
+ :tg-const:`telegram.constants.ForumTopicLimit.MAX_NAME_LENGTH` characters.
+ icon_custom_emoji_id (:obj:`str`): New unique identifier of the custom emoji shown as
+ the topic icon. Use :meth:`~telegram.Bot.get_forum_topic_icon_stickers` to get all
+ allowed custom emoji identifiers.
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {
+ "chat_id": chat_id,
+ "message_thread_id": message_thread_id,
+ "name": name,
+ "icon_custom_emoji_id": icon_custom_emoji_id,
+ }
+ return await self._post( # type: ignore[return-value]
+ "editForumTopic",
+ data,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ @_log
+ async def close_forum_topic(
+ self,
+ chat_id: Union[str, int],
+ message_thread_id: int,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """
+ Use this method to close an open topic in a forum supergroup chat. The bot must
+ be an administrator in the chat for this to work and must have
+ :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights,
+ unless it is the creator of the topic.
+
+ .. seealso:: :meth:`telegram.Message.close_forum_topic`,
+ :meth:`telegram.Chat.close_forum_topic`,
+
+ .. versionadded:: 20.0
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): |chat_id_group|
+ message_thread_id (:obj:`int`): |message_thread_id|
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {
+ "chat_id": chat_id,
+ "message_thread_id": message_thread_id,
+ }
+ return await self._post( # type: ignore[return-value]
+ "closeForumTopic",
+ data,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ @_log
+ async def reopen_forum_topic(
+ self,
+ chat_id: Union[str, int],
+ message_thread_id: int,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """
+ Use this method to reopen a closed topic in a forum supergroup chat. The bot must
+ be an administrator in the chat for this to work and must have
+ :paramref:`~telegram.ChatAdministratorRights.can_manage_topics` administrator rights,
+ unless it is the creator of the topic.
+
+ .. seealso:: :meth:`telegram.Message.reopen_forum_topic`,
+ :meth:`telegram.Chat.reopen_forum_topic`,
+
+ .. versionadded:: 20.0
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): |chat_id_group|
+ message_thread_id (:obj:`int`): |message_thread_id|
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {
+ "chat_id": chat_id,
+ "message_thread_id": message_thread_id,
+ }
+ return await self._post( # type: ignore[return-value]
+ "reopenForumTopic",
+ data,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ @_log
+ async def delete_forum_topic(
+ self,
+ chat_id: Union[str, int],
+ message_thread_id: int,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """
+ Use this method to delete a forum topic along with all its messages in a forum supergroup
+ chat. The bot must be an administrator in the chat for this to work and must have
+ :paramref:`~telegram.ChatAdministratorRights.can_delete_messages` administrator rights.
+
+ .. seealso:: :meth:`telegram.Message.delete_forum_topic`,
+ :meth:`telegram.Chat.delete_forum_topic`,
+
+ .. versionadded:: 20.0
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): |chat_id_group|
+ message_thread_id (:obj:`int`): |message_thread_id|
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {
+ "chat_id": chat_id,
+ "message_thread_id": message_thread_id,
+ }
+ return await self._post( # type: ignore[return-value]
+ "deleteForumTopic",
+ data,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ @_log
+ async def unpin_all_forum_topic_messages(
+ self,
+ chat_id: Union[str, int],
+ message_thread_id: int,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """
+ Use this method to clear the list of pinned messages in a forum topic. The bot must
+ be an administrator in the chat for this to work and must have
+ :paramref:`~telegram.ChatAdministratorRights.can_pin_messages` administrator rights
+ in the supergroup.
+
+ .. seealso:: :meth:`telegram.Message.unpin_all_forum_topic_messages`,
+ :meth:`telegram.Chat.unpin_all_forum_topic_messages`,
+
+ .. versionadded:: 20.0
+
+ Args:
+ chat_id (:obj:`int` | :obj:`str`): |chat_id_group|
+ message_thread_id (:obj:`int`): |message_thread_id|
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+
+ Raises:
+ :class:`telegram.error.TelegramError`
+
+ """
+ data: JSONDict = {
+ "chat_id": chat_id,
+ "message_thread_id": message_thread_id,
+ }
+ return await self._post( # type: ignore[return-value]
+ "unpinAllForumTopicMessages",
+ data,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
def to_dict(self, recursive: bool = True) -> JSONDict: # skipcq: PYL-W0613
"""See :meth:`telegram.TelegramObject.to_dict`."""
data: JSONDict = {"id": self.id, "username": self.username, "first_name": self.first_name}
@@ -6926,3 +7395,17 @@ def __hash__(self) -> int:
"""Alias for :meth:`set_my_default_administrator_rights`"""
createInvoiceLink = create_invoice_link
"""Alias for :meth:`create_invoice_link`"""
+ getForumTopicIconStickers = get_forum_topic_icon_stickers
+ """Alias for :meth:`get_forum_topic_icon_stickers`"""
+ createForumTopic = create_forum_topic
+ """Alias for :meth:`create_forum_topic`"""
+ editForumTopic = edit_forum_topic
+ """Alias for :meth:`edit_forum_topic`"""
+ closeForumTopic = close_forum_topic
+ """Alias for :meth:`close_forum_topic`"""
+ reopenForumTopic = reopen_forum_topic
+ """Alias for :meth:`reopen_forum_topic`"""
+ deleteForumTopic = delete_forum_topic
+ """Alias for :meth:`delete_forum_topic`"""
+ unpinAllForumTopicMessages = unpin_all_forum_topic_messages
+ """Alias for :meth:`unpin_all_forum_topic_messages`"""
diff --git a/telegram/_callbackquery.py b/telegram/_callbackquery.py
index 73674e0d491..2a6fd1ba682 100644
--- a/telegram/_callbackquery.py
+++ b/telegram/_callbackquery.py
@@ -724,6 +724,7 @@ async def copy_message(
allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
reply_markup: ReplyMarkup = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -761,6 +762,7 @@ async def copy_message(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
MAX_ANSWER_TEXT_LENGTH: ClassVar[
diff --git a/telegram/_chat.py b/telegram/_chat.py
index 296a32cc4da..3acf2d9e34c 100644
--- a/telegram/_chat.py
+++ b/telegram/_chat.py
@@ -148,6 +148,22 @@ class Chat(TelegramObject):
in the private chat. Returned only in :meth:`telegram.Bot.get_chat`.
.. versionadded:: 20.0
+ is_forum (:obj:`bool`, optional): :obj:`True`, if the supergroup chat is a forum
+ (has topics_ enabled).
+
+ .. versionadded:: 20.0
+ active_usernames (List[:obj:`str`], optional): If set, the list of all `active chat
+ usernames `_; for private chats, supergroups and channels. Returned
+ only in :meth:`telegram.Bot.get_chat`.
+
+ .. versionadded:: 20.0
+ emoji_status_custom_emoji_id (:obj:`str`, optional): Custom emoji identifier of emoji
+ status of the other party in a private chat. Returned only in
+ :meth:`telegram.Bot.get_chat`.
+
+ .. versionadded:: 20.0
+
Attributes:
id (:obj:`int`): Unique identifier for this chat. This number may be greater than 32 bits
and some programming languages may have difficulty/silent defects in interpreting it.
@@ -212,8 +228,24 @@ class Chat(TelegramObject):
privacy settings of the other party restrict sending voice and video note messages
in the private chat. Returned only in :meth:`telegram.Bot.get_chat`.
+ .. versionadded:: 20.0
+ is_forum (:obj:`bool`): Optional. :obj:`True`, if the supergroup chat is a forum
+ (has topics_ enabled).
+
+ .. versionadded:: 20.0
+ active_usernames (List[:obj:`str`]): Optional. If set, the list of all `active chat
+ usernames `_; for private chats, supergroups and channels. Returned
+ only in :meth:`telegram.Bot.get_chat`.
+
+ .. versionadded:: 20.0
+ emoji_status_custom_emoji_id (:obj:`str`): Optional. Custom emoji identifier of emoji
+ status of the other party in a private chat. Returned only in
+ :meth:`telegram.Bot.get_chat`.
+
.. versionadded:: 20.0
+ .. _topics: https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups
"""
__slots__ = (
@@ -240,6 +272,9 @@ class Chat(TelegramObject):
"join_to_send_messages",
"join_by_request",
"has_restricted_voice_and_video_messages",
+ "is_forum",
+ "active_usernames",
+ "emoji_status_custom_emoji_id",
)
SENDER: ClassVar[str] = constants.ChatType.SENDER
@@ -281,6 +316,9 @@ def __init__(
join_to_send_messages: bool = None,
join_by_request: bool = None,
has_restricted_voice_and_video_messages: bool = None,
+ is_forum: bool = None,
+ active_usernames: List[str] = None,
+ emoji_status_custom_emoji_id: str = None,
*,
api_kwargs: JSONDict = None,
):
@@ -312,6 +350,9 @@ def __init__(
self.join_to_send_messages = join_to_send_messages
self.join_by_request = join_by_request
self.has_restricted_voice_and_video_messages = has_restricted_voice_and_video_messages
+ self.is_forum = is_forum
+ self.active_usernames = active_usernames
+ self.emoji_status_custom_emoji_id = emoji_status_custom_emoji_id
self._id_attrs = (self.id,)
@@ -802,6 +843,7 @@ async def promote_member(
is_anonymous: bool = None,
can_manage_chat: bool = None,
can_manage_video_chats: bool = None,
+ can_manage_topics: bool = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -845,6 +887,7 @@ async def promote_member(
is_anonymous=is_anonymous,
can_manage_chat=can_manage_chat,
can_manage_video_chats=can_manage_video_chats,
+ can_manage_topics=can_manage_topics,
)
async def restrict_member(
@@ -1192,6 +1235,7 @@ async def send_message(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1220,6 +1264,7 @@ async def send_message(
allow_sending_without_reply=allow_sending_without_reply,
entities=entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1236,6 +1281,7 @@ async def send_media_group(
reply_to_message_id: int = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = 20,
@@ -1268,6 +1314,7 @@ async def send_media_group(
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
caption=caption,
parse_mode=parse_mode,
caption_entities=caption_entities,
@@ -1317,6 +1364,7 @@ async def send_photo(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1347,6 +1395,7 @@ async def send_photo(
caption_entities=caption_entities,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1365,6 +1414,7 @@ async def send_contact(
vcard: str = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
contact: "Contact" = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1400,6 +1450,7 @@ async def send_contact(
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_audio(
@@ -1417,6 +1468,7 @@ async def send_audio(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1451,6 +1503,7 @@ async def send_audio(
caption_entities=caption_entities,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1471,6 +1524,7 @@ async def send_document(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1508,6 +1562,7 @@ async def send_document(
allow_sending_without_reply=allow_sending_without_reply,
caption_entities=caption_entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_dice(
@@ -1518,6 +1573,7 @@ async def send_dice(
emoji: str = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1548,6 +1604,7 @@ async def send_dice(
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_game(
@@ -1558,6 +1615,7 @@ async def send_game(
reply_markup: "InlineKeyboardMarkup" = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1588,6 +1646,7 @@ async def send_game(
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_invoice(
@@ -1618,6 +1677,7 @@ async def send_invoice(
max_tip_amount: int = None,
suggested_tip_amounts: List[int] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1678,6 +1738,7 @@ async def send_invoice(
max_tip_amount=max_tip_amount,
suggested_tip_amounts=suggested_tip_amounts,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_location(
@@ -1693,6 +1754,7 @@ async def send_location(
proximity_alert_radius: int = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
location: "Location" = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1730,6 +1792,7 @@ async def send_location(
proximity_alert_radius=proximity_alert_radius,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_animation(
@@ -1747,6 +1810,7 @@ async def send_animation(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1786,6 +1850,7 @@ async def send_animation(
caption_entities=caption_entities,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_sticker(
@@ -1796,6 +1861,7 @@ async def send_sticker(
reply_markup: ReplyMarkup = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = 20,
@@ -1826,6 +1892,7 @@ async def send_sticker(
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_venue(
@@ -1843,6 +1910,7 @@ async def send_venue(
google_place_type: str = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
venue: "Venue" = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1882,6 +1950,7 @@ async def send_venue(
google_place_type=google_place_type,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_video(
@@ -1900,6 +1969,7 @@ async def send_video(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1940,6 +2010,7 @@ async def send_video(
caption_entities=caption_entities,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_video_note(
@@ -1953,6 +2024,7 @@ async def send_video_note(
thumb: FileInput = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1988,6 +2060,7 @@ async def send_video_note(
allow_sending_without_reply=allow_sending_without_reply,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_voice(
@@ -2002,6 +2075,7 @@ async def send_voice(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2038,6 +2112,7 @@ async def send_voice(
caption_entities=caption_entities,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_poll(
@@ -2059,6 +2134,7 @@ async def send_poll(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
explanation_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2100,6 +2176,7 @@ async def send_poll(
allow_sending_without_reply=allow_sending_without_reply,
explanation_entities=explanation_entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_copy(
@@ -2114,6 +2191,7 @@ async def send_copy(
allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
reply_markup: ReplyMarkup = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2148,6 +2226,7 @@ async def send_copy(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def copy_message(
@@ -2162,6 +2241,7 @@ async def copy_message(
allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
reply_markup: ReplyMarkup = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2196,6 +2276,7 @@ async def copy_message(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def forward_from(
@@ -2204,6 +2285,7 @@ async def forward_from(
message_id: int,
disable_notification: DVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2236,6 +2318,7 @@ async def forward_from(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def forward_to(
@@ -2244,6 +2327,7 @@ async def forward_to(
message_id: int,
disable_notification: DVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2276,6 +2360,7 @@ async def forward_to(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def export_invite_link(
@@ -2532,6 +2617,207 @@ async def set_menu_button(
api_kwargs=api_kwargs,
)
+ async def create_forum_topic(
+ self,
+ name: str,
+ icon_color: int = None,
+ icon_custom_emoji_id: str = None,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ await bot.create_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.create_forum_topic`.
+
+ .. versionadded:: 20.0
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+ """
+ return await self.get_bot().create_forum_topic(
+ chat_id=self.id,
+ name=name,
+ icon_color=icon_color,
+ icon_custom_emoji_id=icon_custom_emoji_id,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ async def edit_forum_topic(
+ self,
+ message_thread_id: int,
+ name: str,
+ icon_custom_emoji_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: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ await bot.edit_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.edit_forum_topic`.
+
+ .. versionadded:: 20.0
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+ """
+ return await self.get_bot().edit_forum_topic(
+ chat_id=self.id,
+ message_thread_id=message_thread_id,
+ name=name,
+ icon_custom_emoji_id=icon_custom_emoji_id,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ async def close_forum_topic(
+ self,
+ message_thread_id: int,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ await bot.close_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.close_forum_topic`.
+
+ .. versionadded:: 20.0
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+ """
+ return await self.get_bot().close_forum_topic(
+ chat_id=self.id,
+ message_thread_id=message_thread_id,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ async def reopen_forum_topic(
+ self,
+ message_thread_id: int,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ await bot.reopen_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.reopen_forum_topic`.
+
+ .. versionadded:: 20.0
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+ """
+ return await self.get_bot().reopen_forum_topic(
+ chat_id=self.id,
+ message_thread_id=message_thread_id,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ async def delete_forum_topic(
+ self,
+ message_thread_id: int,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ await bot.delete_forum_topic(chat_id=update.effective_chat.id, *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.delete_forum_topic`.
+
+ .. versionadded:: 20.0
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+ """
+ return await self.get_bot().delete_forum_topic(
+ chat_id=self.id,
+ message_thread_id=message_thread_id,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ async def unpin_all_forum_topic_messages(
+ self,
+ message_thread_id: int,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ await bot.unpin_all_forum_topic_messages(chat_id=update.effective_chat.id,
+ *args, **kwargs)
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.unpin_all_forum_topic_messages`.
+
+ .. versionadded:: 20.0
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+ """
+ return await self.get_bot().unpin_all_forum_topic_messages(
+ chat_id=self.id,
+ message_thread_id=message_thread_id,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
async def get_menu_button(
self,
*,
diff --git a/telegram/_chatadministratorrights.py b/telegram/_chatadministratorrights.py
index 185782373ca..32175cba586 100644
--- a/telegram/_chatadministratorrights.py
+++ b/telegram/_chatadministratorrights.py
@@ -29,7 +29,12 @@ class ChatAdministratorRights(TelegramObject):
considered equal, if their :attr:`is_anonymous`, :attr:`can_manage_chat`,
:attr:`can_delete_messages`, :attr:`can_manage_video_chats`, :attr:`can_restrict_members`,
:attr:`can_promote_members`, :attr:`can_change_info`, :attr:`can_invite_users`,
- :attr:`can_post_messages`, :attr:`can_edit_messages`, :attr:`can_pin_messages` are equal.
+ :attr:`can_post_messages`, :attr:`can_edit_messages`, :attr:`can_pin_messages`,
+ :attr:`can_manage_topics` are equal.
+
+ .. versionchanged:: 20.0
+ :attr:`can_manage_topics` is considered as well when comparing objects of
+ this type in terms of equality.
.. seealso: :meth:`Bot.set_my_default_administrator_rights`,
:meth:`Bot.get_my_default_administrator_rights`
@@ -62,6 +67,10 @@ class ChatAdministratorRights(TelegramObject):
messages of other users.
can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin
messages; groups and supergroups only.
+ can_manage_topics (:obj:`bool`, optional): :obj:`True`, if the user is allowed
+ to create, rename, close, and reopen forum topics; supergroups only.
+
+ .. versionadded:: 20.0
Attributes:
is_anonymous (:obj:`bool`): :obj:`True`, if the user's presence in the chat is hidden.
@@ -89,6 +98,10 @@ class ChatAdministratorRights(TelegramObject):
messages of other users.
can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin
messages; groups and supergroups only.
+ can_manage_topics (:obj:`bool`): Optional. :obj:`True`, if the user is allowed
+ to create, rename, close, and reopen forum topics; supergroups only.
+
+ .. versionadded:: 20.0
"""
__slots__ = (
@@ -103,6 +116,7 @@ class ChatAdministratorRights(TelegramObject):
"can_post_messages",
"can_edit_messages",
"can_pin_messages",
+ "can_manage_topics",
)
def __init__(
@@ -118,6 +132,7 @@ def __init__(
can_post_messages: bool = None,
can_edit_messages: bool = None,
can_pin_messages: bool = None,
+ can_manage_topics: bool = None,
*,
api_kwargs: JSONDict = None,
) -> None:
@@ -135,6 +150,7 @@ def __init__(
self.can_post_messages = can_post_messages
self.can_edit_messages = can_edit_messages
self.can_pin_messages = can_pin_messages
+ self.can_manage_topics = can_manage_topics
self._id_attrs = (
self.is_anonymous,
@@ -148,6 +164,7 @@ def __init__(
self.can_post_messages,
self.can_edit_messages,
self.can_pin_messages,
+ self.can_manage_topics,
)
@classmethod
@@ -159,7 +176,7 @@ def all_rights(cls) -> "ChatAdministratorRights":
.. versionadded:: 20.0
"""
- return cls(True, True, True, True, True, True, True, True, True, True, True)
+ return cls(True, True, True, True, True, True, True, True, True, True, True, True)
@classmethod
def no_rights(cls) -> "ChatAdministratorRights":
@@ -169,4 +186,6 @@ def no_rights(cls) -> "ChatAdministratorRights":
.. versionadded:: 20.0
"""
- return cls(False, False, False, False, False, False, False, False, False, False, False)
+ return cls(
+ False, False, False, False, False, False, False, False, False, False, False, False
+ )
diff --git a/telegram/_chatmember.py b/telegram/_chatmember.py
index 363009af607..a1db9db3666 100644
--- a/telegram/_chatmember.py
+++ b/telegram/_chatmember.py
@@ -173,6 +173,8 @@ class ChatMemberAdministrator(ChatMember):
* Argument and attribute ``can_manage_voice_chats`` were renamed to
:paramref:`can_manage_video_chats` and :attr:`can_manage_video_chats` in accordance to
Bot API 6.0.
+ * The argument :paramref:`can_manage_topics` was added, which changes the position of the
+ optional argument :paramref:`custom_title`.
Args:
user (:class:`telegram.User`): Information about the user.
@@ -207,6 +209,10 @@ class ChatMemberAdministrator(ChatMember):
messages; channels only.
can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed
to pin messages; groups and supergroups only.
+ can_manage_topics (:obj:`bool`, optional): :obj:`True`, if the user is allowed
+ to create, rename, close, and reopen forum topics; supergroups only.
+
+ .. versionadded:: 20.0
custom_title (:obj:`str`, optional): Custom title for this user.
Attributes:
@@ -244,6 +250,10 @@ class ChatMemberAdministrator(ChatMember):
messages; channels only.
can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed
to pin messages; groups and supergroups only.
+ can_manage_topics (:obj:`bool`): Optional. :obj:`True`, if the user is allowed
+ to create, rename, close, and reopen forum topics; supergroups only
+
+ .. versionadded:: 20.0
custom_title (:obj:`str`): Optional. Custom title for this user.
"""
@@ -260,6 +270,7 @@ class ChatMemberAdministrator(ChatMember):
"can_post_messages",
"can_edit_messages",
"can_pin_messages",
+ "can_manage_topics",
"custom_title",
)
@@ -278,6 +289,7 @@ def __init__(
can_post_messages: bool = None,
can_edit_messages: bool = None,
can_pin_messages: bool = None,
+ can_manage_topics: bool = None,
custom_title: str = None,
*,
api_kwargs: JSONDict = None,
@@ -295,6 +307,7 @@ def __init__(
self.can_post_messages = can_post_messages
self.can_edit_messages = can_edit_messages
self.can_pin_messages = can_pin_messages
+ self.can_manage_topics = can_manage_topics
self.custom_title = custom_title
@@ -332,6 +345,9 @@ class ChatMemberRestricted(ChatMember):
in the chat. Supergroups only.
.. versionadded:: 13.7
+ .. versionchanged:: 20.0
+ The argument :paramref:`can_manage_topics` was added, which changes the position of the
+ optional argument :paramref:`until_date`.
Args:
user (:class:`telegram.User`): Information about the user.
@@ -353,6 +369,10 @@ class ChatMemberRestricted(ChatMember):
to send animations, games, stickers and use inline bots.
can_add_web_page_previews (:obj:`bool`): :obj:`True`, if the user is
allowed to add web page previews to their messages.
+ can_manage_topics (:obj:`bool`): :obj:`True`, if the user is allowed to create
+ forum topics.
+
+ .. versionadded:: 20.0
until_date (:class:`datetime.datetime`): Date when restrictions
will be lifted for this user.
@@ -378,6 +398,10 @@ class ChatMemberRestricted(ChatMember):
to send animations, games, stickers and use inline bots.
can_add_web_page_previews (:obj:`bool`): :obj:`True`, if the user is
allowed to add web page previews to their messages.
+ can_manage_topics (:obj:`bool`): :obj:`True`, if the user is allowed to create
+ forum topics.
+
+ .. versionadded:: 20.0
until_date (:class:`datetime.datetime`): Date when restrictions
will be lifted for this user.
@@ -393,6 +417,7 @@ class ChatMemberRestricted(ChatMember):
"can_send_polls",
"can_send_other_messages",
"can_add_web_page_previews",
+ "can_manage_topics",
"until_date",
)
@@ -408,6 +433,7 @@ def __init__(
can_send_polls: bool,
can_send_other_messages: bool,
can_add_web_page_previews: bool,
+ can_manage_topics: bool,
until_date: datetime.datetime,
*,
api_kwargs: JSONDict = None,
@@ -422,6 +448,7 @@ def __init__(
self.can_send_polls = can_send_polls
self.can_send_other_messages = can_send_other_messages
self.can_add_web_page_previews = can_add_web_page_previews
+ self.can_manage_topics = can_manage_topics
self.until_date = until_date
diff --git a/telegram/_chatpermissions.py b/telegram/_chatpermissions.py
index 416e3306a27..c05cfacbdd4 100644
--- a/telegram/_chatpermissions.py
+++ b/telegram/_chatpermissions.py
@@ -28,12 +28,17 @@ class ChatPermissions(TelegramObject):
Objects of this class are comparable in terms of equality. Two objects of this class are
considered equal, if their :attr:`can_send_messages`, :attr:`can_send_media_messages`,
:attr:`can_send_polls`, :attr:`can_send_other_messages`, :attr:`can_add_web_page_previews`,
- :attr:`can_change_info`, :attr:`can_invite_users` and :attr:`can_pin_messages` are equal.
+ :attr:`can_change_info`, :attr:`can_invite_users`, :attr:`can_pin_messages`, and
+ :attr:`can_manage_topics` are equal.
+
+ .. versionchanged:: 20.0
+ :attr:`can_manage_topics` is considered as well when comparing objects of
+ this type in terms of equality.
Note:
Though not stated explicitly in the official docs, Telegram changes not only the
permissions that are set, but also sets all the others to :obj:`False`. However, since not
- documented, this behaviour may change unbeknown to PTB.
+ documented, this behavior may change unbeknown to PTB.
Args:
can_send_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to send text
@@ -54,6 +59,11 @@ class ChatPermissions(TelegramObject):
users to the chat.
can_pin_messages (:obj:`bool`, optional): :obj:`True`, if the user is allowed to pin
messages. Ignored in public supergroups.
+ can_manage_topics (:obj:`bool`, optional): :obj:`True`, if the user is allowed
+ to create forum topics. If omitted defaults to the value of
+ :attr:`can_pin_messages`.
+
+ .. versionadded:: 20.0
Attributes:
can_send_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to send text
@@ -74,6 +84,11 @@ class ChatPermissions(TelegramObject):
new users to the chat.
can_pin_messages (:obj:`bool`): Optional. :obj:`True`, if the user is allowed to pin
messages. Ignored in public supergroups.
+ can_manage_topics (:obj:`bool`): Optional. :obj:`True`, if the user is allowed
+ to create forum topics. If omitted defaults to the value of
+ :attr:`can_pin_messages`.
+
+ .. versionadded:: 20.0
"""
@@ -86,6 +101,7 @@ class ChatPermissions(TelegramObject):
"can_change_info",
"can_pin_messages",
"can_add_web_page_previews",
+ "can_manage_topics",
)
def __init__(
@@ -98,6 +114,7 @@ def __init__(
can_change_info: bool = None,
can_invite_users: bool = None,
can_pin_messages: bool = None,
+ can_manage_topics: bool = None,
*,
api_kwargs: JSONDict = None,
):
@@ -111,6 +128,7 @@ def __init__(
self.can_change_info = can_change_info
self.can_invite_users = can_invite_users
self.can_pin_messages = can_pin_messages
+ self.can_manage_topics = can_manage_topics
self._id_attrs = (
self.can_send_messages,
@@ -121,6 +139,7 @@ def __init__(
self.can_change_info,
self.can_invite_users,
self.can_pin_messages,
+ self.can_manage_topics,
)
@classmethod
@@ -133,7 +152,7 @@ def all_permissions(cls) -> "ChatPermissions":
.. versionadded:: 20.0
"""
- return cls(True, True, True, True, True, True, True, True)
+ return cls(True, True, True, True, True, True, True, True, True)
@classmethod
def no_permissions(cls) -> "ChatPermissions":
@@ -143,4 +162,4 @@ def no_permissions(cls) -> "ChatPermissions":
.. versionadded:: 20.0
"""
- return cls(False, False, False, False, False, False, False, False)
+ return cls(False, False, False, False, False, False, False, False, False)
diff --git a/telegram/_forumtopic.py b/telegram/_forumtopic.py
new file mode 100644
index 00000000000..3cf3fe55be7
--- /dev/null
+++ b/telegram/_forumtopic.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+"""This module contains objects related to Telegram forum topics."""
+
+from telegram._telegramobject import TelegramObject
+from telegram._utils.types import JSONDict
+
+
+class ForumTopic(TelegramObject):
+ """
+ This object represents a forum topic.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`message_thread_id`, :attr:`name` and :attr:`icon_color`
+ are equal.
+
+ .. versionadded:: 20.0
+
+ Args:
+ message_thread_id (:obj:`int`): Unique identifier of the forum topic
+ name (:obj:`str`): Name of the topic
+ icon_color (:obj:`int`): Color of the topic icon in RGB format
+ icon_custom_emoji_id (:obj:`str`, optional): Unique identifier of the custom emoji shown
+ as the topic icon.
+
+ Attributes:
+ message_thread_id (:obj:`int`): Unique identifier of the forum topic
+ name (:obj:`str`): Name of the topic
+ icon_color (:obj:`int`): Color of the topic icon in RGB format
+ icon_custom_emoji_id (:obj:`str`): Optional. Unique identifier of the custom emoji shown
+ as the topic icon.
+ """
+
+ __slots__ = ("message_thread_id", "name", "icon_color", "icon_custom_emoji_id")
+
+ def __init__(
+ self,
+ message_thread_id: int,
+ name: str,
+ icon_color: int,
+ icon_custom_emoji_id: str = None,
+ *,
+ api_kwargs: JSONDict = None,
+ ):
+ super().__init__(api_kwargs=api_kwargs)
+ self.message_thread_id = message_thread_id
+ self.name = name
+ self.icon_color = icon_color
+ self.icon_custom_emoji_id = icon_custom_emoji_id
+
+ self._id_attrs = (self.message_thread_id, self.name, self.icon_color)
+
+
+class ForumTopicCreated(TelegramObject):
+ """
+ This object represents the content of a service message about a new forum topic created in
+ the chat.
+
+ Objects of this class are comparable in terms of equality. Two objects of this class are
+ considered equal, if their :attr:`name` and :attr:`icon_color` are equal.
+
+ .. versionadded:: 20.0
+
+ Args:
+ name (:obj:`str`): Name of the topic
+ icon_color (:obj:`int`): Color of the topic icon in RGB format
+ icon_custom_emoji_id (:obj:`str`, optional): Unique identifier of the custom emoji shown
+ as the topic icon.
+
+ Attributes:
+ name (:obj:`str`): Name of the topic
+ icon_color (:obj:`int`): Color of the topic icon in RGB format
+ icon_custom_emoji_id (:obj:`str`): Optional. Unique identifier of the custom emoji shown
+ as the topic icon.
+ """
+
+ __slots__ = ("name", "icon_color", "icon_custom_emoji_id")
+
+ def __init__(
+ self,
+ name: str,
+ icon_color: int,
+ icon_custom_emoji_id: str = None,
+ *,
+ api_kwargs: JSONDict = None,
+ ):
+ super().__init__(api_kwargs=api_kwargs)
+ self.name = name
+ self.icon_color = icon_color
+ self.icon_custom_emoji_id = icon_custom_emoji_id
+
+ self._id_attrs = (self.name, self.icon_color)
+
+
+class ForumTopicClosed(TelegramObject):
+ """
+ This object represents a service message about a forum topic closed in the chat.
+ Currently holds no information.
+
+ .. versionadded:: 20.0
+ """
+
+ __slots__ = ()
+
+
+class ForumTopicReopened(TelegramObject):
+ """
+ This object represents a service message about a forum topic reopened in the chat.
+ Currently holds no information.
+
+ .. versionadded:: 20.0
+ """
+
+ __slots__ = ()
diff --git a/telegram/_message.py b/telegram/_message.py
index 9aad571c8c6..3765566a023 100644
--- a/telegram/_message.py
+++ b/telegram/_message.py
@@ -36,6 +36,7 @@
from telegram._files.video import Video
from telegram._files.videonote import VideoNote
from telegram._files.voice import Voice
+from telegram._forumtopic import ForumTopicClosed, ForumTopicCreated, ForumTopicReopened
from telegram._games.game import Game
from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
from telegram._messageautodeletetimerchanged import MessageAutoDeleteTimerChanged
@@ -248,6 +249,26 @@ class Message(TelegramObject):
reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
to the message. :paramref:`~telegram.InlineKeyboardButton.login_url` buttons are
represented as ordinary url buttons.
+ is_topic_message (:obj:`bool`, optional): :obj:`True`, if the message is sent to a forum
+ topic.
+
+ .. versionadded:: 20.0
+ message_thread_id (:obj:`int`, optional): Unique identifier of a message thread to which
+ the message belongs; for supergroups only.
+
+ .. versionadded:: 20.0
+ forum_topic_created (:class:`telegram.ForumTopicCreated`, optional): Service message:
+ forum topic created
+
+ .. versionadded:: 20.0
+ forum_topic_closed (:class:`telegram.ForumTopicClosed`, optional): Service message:
+ forum topic closed
+
+ .. versionadded:: 20.0
+ forum_topic_reopened (:class:`telegram.ForumTopicReopened`, optional): Service message:
+ forum topic reopened
+
+ .. versionadded:: 20.0
Attributes:
message_id (:obj:`int`): Unique message identifier inside this chat.
@@ -400,6 +421,26 @@ class Message(TelegramObject):
reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
to the message. :paramref:`~telegram.InlineKeyboardButton.login_url` buttons are
represented as ordinary url buttons.
+ is_topic_message (:obj:`bool`): Optional. :obj:`True`, if the message is sent to a forum
+ topic.
+
+ .. versionadded:: 20.0
+ message_thread_id (:obj:`int`): Optional. Unique identifier of a message thread to which
+ the message belongs; for supergroups only.
+
+ .. versionadded:: 20.0
+ forum_topic_created (:class:`telegram.ForumTopicCreated`): Optional. Service message:
+ forum topic created
+
+ .. versionadded:: 20.0
+ forum_topic_closed (:class:`telegram.ForumTopicClosed`): Optional. Service message:
+ forum topic closed
+
+ .. versionadded:: 20.0
+ forum_topic_reopened (:class:`telegram.ForumTopicReopened`): Optional. Service message:
+ forum topic reopened
+
+ .. versionadded:: 20.0
.. |custom_emoji_formatting_note| replace:: Custom emoji entities will currently be ignored
by this function. Instead, the supplied replacement for the emoji will be used.
@@ -467,6 +508,11 @@ class Message(TelegramObject):
"is_automatic_forward",
"has_protected_content",
"web_app_data",
+ "is_topic_message",
+ "message_thread_id",
+ "forum_topic_created",
+ "forum_topic_closed",
+ "forum_topic_reopened",
)
def __init__(
@@ -530,6 +576,11 @@ def __init__(
is_automatic_forward: bool = None,
has_protected_content: bool = None,
web_app_data: WebAppData = None,
+ is_topic_message: bool = None,
+ message_thread_id: int = None,
+ forum_topic_created: ForumTopicCreated = None,
+ forum_topic_closed: ForumTopicClosed = None,
+ forum_topic_reopened: ForumTopicReopened = None,
*,
api_kwargs: JSONDict = None,
):
@@ -596,6 +647,11 @@ def __init__(
self.video_chat_participants_invited = video_chat_participants_invited
self.reply_markup = reply_markup
self.web_app_data = web_app_data
+ self.is_topic_message = is_topic_message
+ self.message_thread_id = message_thread_id
+ self.forum_topic_created = forum_topic_created
+ self.forum_topic_closed = forum_topic_closed
+ self.forum_topic_reopened = forum_topic_reopened
self._effective_attachment = DEFAULT_NONE
@@ -686,6 +742,13 @@ def de_json(cls, data: Optional[JSONDict], bot: "Bot") -> Optional["Message"]:
data.get("video_chat_participants_invited"), bot
)
data["web_app_data"] = WebAppData.de_json(data.get("web_app_data"), bot)
+ data["forum_topic_closed"] = ForumTopicClosed.de_json(data.get("forum_topic_closed"), bot)
+ data["forum_topic_created"] = ForumTopicCreated.de_json(
+ data.get("forum_topic_created"), bot
+ )
+ data["forum_topic_reopened"] = ForumTopicReopened.de_json(
+ data.get("forum_topic_reopened"), bot
+ )
return super().de_json(data=data, bot=bot)
@@ -784,6 +847,7 @@ async def reply_text(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
quote: bool = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -819,6 +883,7 @@ async def reply_text(
allow_sending_without_reply=allow_sending_without_reply,
entities=entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -836,6 +901,7 @@ async def reply_markdown(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
quote: bool = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -881,6 +947,7 @@ async def reply_markdown(
allow_sending_without_reply=allow_sending_without_reply,
entities=entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -898,6 +965,7 @@ async def reply_markdown_v2(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
quote: bool = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -939,6 +1007,7 @@ async def reply_markdown_v2(
allow_sending_without_reply=allow_sending_without_reply,
entities=entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -956,6 +1025,7 @@ async def reply_html(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
quote: bool = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -997,6 +1067,7 @@ async def reply_html(
allow_sending_without_reply=allow_sending_without_reply,
entities=entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1013,6 +1084,7 @@ async def reply_media_group(
reply_to_message_id: int = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
quote: bool = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1055,6 +1127,7 @@ async def reply_media_group(
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
caption=caption,
parse_mode=parse_mode,
caption_entities=caption_entities,
@@ -1071,6 +1144,7 @@ async def reply_photo(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
quote: bool = None,
@@ -1108,6 +1182,7 @@ async def reply_photo(
caption_entities=caption_entities,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1130,6 +1205,7 @@ async def reply_audio(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
quote: bool = None,
@@ -1171,6 +1247,7 @@ async def reply_audio(
caption_entities=caption_entities,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1191,6 +1268,7 @@ async def reply_document(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
quote: bool = None,
@@ -1235,6 +1313,7 @@ async def reply_document(
allow_sending_without_reply=allow_sending_without_reply,
caption_entities=caption_entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def reply_animation(
@@ -1252,6 +1331,7 @@ async def reply_animation(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
quote: bool = None,
@@ -1299,6 +1379,7 @@ async def reply_animation(
caption_entities=caption_entities,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def reply_sticker(
@@ -1309,6 +1390,7 @@ async def reply_sticker(
reply_markup: ReplyMarkup = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
quote: bool = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1346,6 +1428,7 @@ async def reply_sticker(
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def reply_video(
@@ -1364,6 +1447,7 @@ async def reply_video(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
quote: bool = None,
@@ -1411,6 +1495,7 @@ async def reply_video(
caption_entities=caption_entities,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def reply_video_note(
@@ -1424,6 +1509,7 @@ async def reply_video_note(
thumb: FileInput = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
quote: bool = None,
@@ -1467,6 +1553,7 @@ async def reply_video_note(
allow_sending_without_reply=allow_sending_without_reply,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def reply_voice(
@@ -1481,6 +1568,7 @@ async def reply_voice(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
quote: bool = None,
@@ -1525,6 +1613,7 @@ async def reply_voice(
caption_entities=caption_entities,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def reply_location(
@@ -1540,6 +1629,7 @@ async def reply_location(
proximity_alert_radius: int = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
location: Location = None,
quote: bool = None,
@@ -1584,6 +1674,7 @@ async def reply_location(
proximity_alert_radius=proximity_alert_radius,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def reply_venue(
@@ -1601,6 +1692,7 @@ async def reply_venue(
google_place_type: str = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
venue: Venue = None,
quote: bool = None,
@@ -1647,6 +1739,7 @@ async def reply_venue(
google_place_type=google_place_type,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def reply_contact(
@@ -1660,6 +1753,7 @@ async def reply_contact(
vcard: str = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
contact: Contact = None,
quote: bool = None,
@@ -1702,6 +1796,7 @@ async def reply_contact(
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def reply_poll(
@@ -1723,6 +1818,7 @@ async def reply_poll(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
explanation_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
quote: bool = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1771,6 +1867,7 @@ async def reply_poll(
allow_sending_without_reply=allow_sending_without_reply,
explanation_entities=explanation_entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def reply_dice(
@@ -1781,6 +1878,7 @@ async def reply_dice(
emoji: str = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
quote: bool = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1818,6 +1916,7 @@ async def reply_dice(
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def reply_chat_action(
@@ -1860,6 +1959,7 @@ async def reply_game(
reply_markup: "InlineKeyboardMarkup" = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
quote: bool = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1899,6 +1999,7 @@ async def reply_game(
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def reply_invoice(
@@ -1929,6 +2030,7 @@ async def reply_invoice(
max_tip_amount: int = None,
suggested_tip_amounts: List[int] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
quote: bool = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1998,6 +2100,7 @@ async def reply_invoice(
max_tip_amount=max_tip_amount,
suggested_tip_amounts=suggested_tip_amounts,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def forward(
@@ -2005,6 +2108,7 @@ async def forward(
chat_id: Union[int, str],
disable_notification: DVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2041,6 +2145,7 @@ async def forward(
message_id=self.message_id,
disable_notification=disable_notification,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -2059,6 +2164,7 @@ async def copy(
allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
reply_markup: ReplyMarkup = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2099,6 +2205,7 @@ async def copy(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def reply_copy(
@@ -2113,6 +2220,7 @@ async def reply_copy(
allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
reply_markup: ReplyMarkup = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
quote: bool = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2161,6 +2269,7 @@ async def reply_copy(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def edit_text(
@@ -2646,6 +2755,180 @@ async def unpin(
api_kwargs=api_kwargs,
)
+ async def edit_forum_topic(
+ self,
+ name: str,
+ icon_custom_emoji_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: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ await bot.edit_forum_topic(
+ chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args,
+ **kwargs
+ )
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.edit_forum_topic`.
+
+ .. versionadded:: 20.0
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+ """
+ return await self.get_bot().edit_forum_topic(
+ chat_id=self.chat_id,
+ message_thread_id=self.message_thread_id,
+ name=name,
+ icon_custom_emoji_id=icon_custom_emoji_id,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ async def close_forum_topic(
+ self,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ await bot.close_forum_topic(
+ chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args,
+ **kwargs
+ )
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.close_forum_topic`.
+
+ .. versionadded:: 20.0
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+ """
+ return await self.get_bot().close_forum_topic(
+ chat_id=self.chat_id,
+ message_thread_id=self.message_thread_id,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ async def reopen_forum_topic(
+ self,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ await bot.reopen_forum_topic(
+ chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args,
+ **kwargs
+ )
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.reopen_forum_topic`.
+
+ .. versionadded:: 20.0
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+ """
+ return await self.get_bot().reopen_forum_topic(
+ chat_id=self.chat_id,
+ message_thread_id=self.message_thread_id,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ async def delete_forum_topic(
+ self,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ await bot.delete_forum_topic(
+ chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args,
+ **kwargs
+ )
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.delete_forum_topic`.
+
+ .. versionadded:: 20.0
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+ """
+ return await self.get_bot().delete_forum_topic(
+ chat_id=self.chat_id,
+ message_thread_id=self.message_thread_id,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
+ async def unpin_all_forum_topic_messages(
+ self,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ ) -> bool:
+ """Shortcut for::
+
+ await bot.unpin_all_forum_topic_messages(
+ chat_id=message.chat_id, message_thread_id=message.message_thread_id, *args,
+ **kwargs
+ )
+
+ For the documentation of the arguments, please see
+ :meth:`telegram.Bot.unpin_all_forum_topic_messages`.
+
+ .. versionadded:: 20.0
+
+ Returns:
+ :obj:`bool`: On success, :obj:`True` is returned.
+ """
+ return await self.get_bot().unpin_all_forum_topic_messages(
+ chat_id=self.chat_id,
+ message_thread_id=self.message_thread_id,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=api_kwargs,
+ )
+
def parse_entity(self, entity: MessageEntity) -> str:
"""Returns the text from a given :class:`telegram.MessageEntity`.
diff --git a/telegram/_user.py b/telegram/_user.py
index 01bf0d93a87..015ec93234e 100644
--- a/telegram/_user.py
+++ b/telegram/_user.py
@@ -376,6 +376,7 @@ async def send_message(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -404,6 +405,7 @@ async def send_message(
allow_sending_without_reply=allow_sending_without_reply,
entities=entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -422,6 +424,7 @@ async def send_photo(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -452,6 +455,7 @@ async def send_photo(
caption_entities=caption_entities,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -468,6 +472,7 @@ async def send_media_group(
reply_to_message_id: int = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = 20,
@@ -500,6 +505,7 @@ async def send_media_group(
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
caption=caption,
parse_mode=parse_mode,
caption_entities=caption_entities,
@@ -520,6 +526,7 @@ async def send_audio(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -554,6 +561,7 @@ async def send_audio(
caption_entities=caption_entities,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -605,6 +613,7 @@ async def send_contact(
vcard: str = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
contact: "Contact" = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -640,6 +649,7 @@ async def send_contact(
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_dice(
@@ -650,6 +660,7 @@ async def send_dice(
emoji: str = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -680,6 +691,7 @@ async def send_dice(
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_document(
@@ -695,6 +707,7 @@ async def send_document(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -732,6 +745,7 @@ async def send_document(
allow_sending_without_reply=allow_sending_without_reply,
caption_entities=caption_entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_game(
@@ -742,6 +756,7 @@ async def send_game(
reply_markup: "InlineKeyboardMarkup" = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -772,6 +787,7 @@ async def send_game(
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_invoice(
@@ -802,6 +818,7 @@ async def send_invoice(
max_tip_amount: int = None,
suggested_tip_amounts: List[int] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -862,6 +879,7 @@ async def send_invoice(
max_tip_amount=max_tip_amount,
suggested_tip_amounts=suggested_tip_amounts,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_location(
@@ -877,6 +895,7 @@ async def send_location(
proximity_alert_radius: int = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
location: "Location" = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -914,6 +933,7 @@ async def send_location(
proximity_alert_radius=proximity_alert_radius,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_animation(
@@ -931,6 +951,7 @@ async def send_animation(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -970,6 +991,7 @@ async def send_animation(
caption_entities=caption_entities,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_sticker(
@@ -980,6 +1002,7 @@ async def send_sticker(
reply_markup: ReplyMarkup = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = 20,
@@ -1010,6 +1033,7 @@ async def send_sticker(
api_kwargs=api_kwargs,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_video(
@@ -1028,6 +1052,7 @@ async def send_video(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1068,6 +1093,7 @@ async def send_video(
caption_entities=caption_entities,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_venue(
@@ -1085,6 +1111,7 @@ async def send_venue(
google_place_type: str = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
venue: "Venue" = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1124,6 +1151,7 @@ async def send_venue(
google_place_type=google_place_type,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_video_note(
@@ -1137,6 +1165,7 @@ async def send_video_note(
thumb: FileInput = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1172,6 +1201,7 @@ async def send_video_note(
allow_sending_without_reply=allow_sending_without_reply,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_voice(
@@ -1186,6 +1216,7 @@ async def send_voice(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1222,6 +1253,7 @@ async def send_voice(
caption_entities=caption_entities,
filename=filename,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_poll(
@@ -1243,6 +1275,7 @@ async def send_poll(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
explanation_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1284,6 +1317,7 @@ async def send_poll(
allow_sending_without_reply=allow_sending_without_reply,
explanation_entities=explanation_entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def send_copy(
@@ -1298,6 +1332,7 @@ async def send_copy(
allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
reply_markup: ReplyMarkup = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1332,6 +1367,7 @@ async def send_copy(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def copy_message(
@@ -1346,6 +1382,7 @@ async def copy_message(
allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
reply_markup: ReplyMarkup = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1380,6 +1417,7 @@ async def copy_message(
pool_timeout=pool_timeout,
api_kwargs=api_kwargs,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
)
async def approve_join_request(
diff --git a/telegram/constants.py b/telegram/constants.py
index f5cd7b838f2..aba31dc4903 100644
--- a/telegram/constants.py
+++ b/telegram/constants.py
@@ -39,12 +39,15 @@
"ChatAction",
"ChatID",
"ChatInviteLinkLimit",
+ "ChatLimit",
"ChatMemberStatus",
"ChatType",
"CustomEmojiStickerLimit",
"DiceEmoji",
"FileSizeLimit",
"FloodLimit",
+ "ForumIconColor",
+ "ForumTopicLimit",
"InlineKeyboardMarkupLimit",
"InlineQueryLimit",
"InlineQueryResultType",
@@ -97,7 +100,7 @@ def __str__(self) -> str:
#: :data:`telegram.__bot_api_version_info__`.
#:
#: .. versionadded:: 20.0
-BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=2)
+BOT_API_VERSION_INFO = _BotAPIVersion(major=6, minor=3)
#: :obj:`str`: Telegram Bot API
#: version supported by this version of `python-telegram-bot`. Also available as
#: :data:`telegram.__bot_api_version__`.
@@ -237,6 +240,34 @@ class ChatInviteLinkLimit(IntEnum):
:meth:`telegram.Bot.create_chat_invite_link` and :meth:`telegram.Bot.edit_chat_invite_link`."""
+class ChatLimit(IntEnum):
+ """This enum contains limitations for
+ :meth:`telegram.Bot.set_chat_administrator_custom_title` and
+ :meth:`telegram.Bot.set_chat_title`.
+ The enum members of this enumeration are instances of :class:`int` and can be treated as such.
+
+ .. versionadded:: 20.0
+ """
+
+ __slots__ = ()
+
+ CHAT_ADMINISTRATOR_CUSTOM_TITLE_LENGTH = 16
+ """:obj:`int`: Maximum length of a :obj:`str` passed as the
+ :paramref:`~telegram.Bot.set_chat_administrator_custom_title.custom_title` parameter of
+ :meth:`telegram.Bot.set_chat_administrator_custom_title`.
+ """
+ MIN_CHAT_TITLE_LENGTH = 1
+ """:obj:`int`: Minimum length of a :obj:`str` passed as the
+ :paramref:`~telegram.Bot.set_chat_title.title` parameter of
+ :meth:`telegram.Bot.set_chat_title`.
+ """
+ MAX_CHAT_TITLE_LENGTH = 128
+ """:obj:`int`: Maximum length of a :obj:`str` passed as the
+ :paramref:`~telegram.Bot.set_chat_title.title` parameter of
+ :meth:`telegram.Bot.set_chat_title`.
+ """
+
+
class ChatMemberStatus(StringEnum):
"""This enum contains the available states for :class:`telegram.ChatMember`. The enum
members of this enumeration are instances of :class:`str` and can be treated as such.
@@ -370,6 +401,66 @@ class FloodLimit(IntEnum):
"""
+class ForumIconColor(IntEnum):
+ """This enum contains the available colors for use in
+ :paramref:`telegram.Bot.create_forum_topic.icon_color`. The enum members of this enumeration
+ are instances of :class:`int` and can be treated as such.
+
+ .. versionadded:: 20.0
+ """
+
+ __slots__ = ()
+
+ BLUE = 0x6FB9F0
+ """:obj:`int`: An icon with a color which corresponds to blue (``0x6FB9F0``).
+
+ .. raw:: html
+
+
+
+ """
+ YELLOW = 0xFFD67E
+ """:obj:`int`: An icon with a color which corresponds to yellow (``0xFFD67E``).
+
+ .. raw:: html
+
+
+
+ """
+ PURPLE = 0xCB86DB
+ """:obj:`int`: An icon with a color which corresponds to purple (``0xCB86DB``).
+
+ .. raw:: html
+
+
+
+ """
+ GREEN = 0x8EEE98
+ """:obj:`int`: An icon with a color which corresponds to green (``0x8EEE98``).
+
+ .. raw:: html
+
+
+
+ """
+ PINK = 0xFF93B2
+ """:obj:`int`: An icon with a color which corresponds to pink (``0xFF93B2``).
+
+ .. raw:: html
+
+
+
+ """
+ RED = 0xFB6F5F
+ """:obj:`int`: An icon with a color which corresponds to red (``0xFB6F5F``).
+
+ .. raw:: html
+
+
+
+ """
+
+
class InlineKeyboardMarkupLimit(IntEnum):
"""This enum contains limitations for :class:`telegram.InlineKeyboardMarkup`/
:meth:`telegram.Bot.send_message` & friends. The enum
@@ -901,3 +992,27 @@ class WebhookLimit(IntEnum):
""":obj:`int`: Minimum length of the secret token."""
MAX_SECRET_TOKEN_LENGTH = 256
""":obj:`int`: Maximum length of the secret token."""
+
+
+class ForumTopicLimit(IntEnum):
+ """This enum contains limitations for :paramref:`telegram.Bot.create_forum_topic.name` and
+ :paramref:`telegram.Bot.edit_forum_topic.name`.
+ The enum members of this enumeration are instances of :class:`int` and can be treated as such.
+
+ .. versionadded:: 20.0
+ """
+
+ __slots__ = ()
+
+ MIN_NAME_LENGTH = 1
+ """:obj:`int`: Minimum length of a :obj:`str` passed as the
+ :paramref:`~telegram.Bot.create_forum_topic.name` parameter of
+ :meth:`telegram.Bot.create_forum_topic` and :paramref:`~telegram.Bot.edit_forum_topic.name`
+ parameter of :meth:`telegram.Bot.edit_forum_topic`.
+ """
+ MAX_NAME_LENGTH = 128
+ """:obj:`int`: Maximum length of a :obj:`str` passed as the
+ :paramref:`~telegram.Bot.create_forum_topic.name` parameter of
+ :meth:`telegram.Bot.create_forum_topic` and :paramref:`~telegram.Bot.edit_forum_topic.name`
+ parameter of :meth:`telegram.Bot.edit_forum_topic`.
+ """
diff --git a/telegram/ext/_extbot.py b/telegram/ext/_extbot.py
index b6622108f02..0fd9713068b 100644
--- a/telegram/ext/_extbot.py
+++ b/telegram/ext/_extbot.py
@@ -53,6 +53,7 @@
Contact,
Document,
File,
+ ForumTopic,
GameHighScore,
InlineKeyboardMarkup,
InputMedia,
@@ -467,6 +468,7 @@ async def _send_message(
reply_markup: ReplyMarkup = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -484,6 +486,7 @@ async def _send_message(
reply_markup=self._replace_keyboard(reply_markup),
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -631,6 +634,7 @@ async def copy_message(
allow_sending_without_reply: DVInput[bool] = DEFAULT_NONE,
reply_markup: ReplyMarkup = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -652,6 +656,7 @@ async def copy_message(
allow_sending_without_reply=allow_sending_without_reply,
reply_markup=self._replace_keyboard(reply_markup),
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1103,6 +1108,28 @@ async def delete_chat_sticker_set(
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
+ async def delete_forum_topic(
+ self,
+ chat_id: Union[str, int],
+ message_thread_id: int,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ rate_limit_args: RLARGS = None,
+ ) -> bool:
+ return await super().delete_forum_topic(
+ chat_id=chat_id,
+ message_thread_id=message_thread_id,
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
+ )
+
async def delete_message(
self,
chat_id: Union[str, int],
@@ -1217,6 +1244,32 @@ async def edit_chat_invite_link(
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
+ async def edit_forum_topic(
+ self,
+ chat_id: Union[str, int],
+ message_thread_id: int,
+ name: str,
+ icon_custom_emoji_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: JSONDict = None,
+ rate_limit_args: RLARGS = None,
+ ) -> bool:
+ return await super().edit_forum_topic(
+ chat_id=chat_id,
+ message_thread_id=message_thread_id,
+ name=name,
+ icon_custom_emoji_id=icon_custom_emoji_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 edit_message_caption(
self,
chat_id: Union[str, int] = None,
@@ -1402,6 +1455,7 @@ async def forward_message(
message_id: int,
disable_notification: DVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1416,6 +1470,7 @@ async def forward_message(
message_id=message_id,
disable_notification=disable_notification,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1527,6 +1582,24 @@ async def get_file(
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
+ async def get_forum_topic_icon_stickers(
+ self,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ rate_limit_args: RLARGS = None,
+ ) -> List[Sticker]:
+ return await super().get_forum_topic_icon_stickers(
+ read_timeout=read_timeout,
+ write_timeout=write_timeout,
+ connect_timeout=connect_timeout,
+ pool_timeout=pool_timeout,
+ api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
+ )
+
async def get_game_high_scores(
self,
user_id: Union[int, str],
@@ -1751,6 +1824,54 @@ async def close(
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
+ async def close_forum_topic(
+ self,
+ chat_id: Union[str, int],
+ message_thread_id: int,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ rate_limit_args: RLARGS = None,
+ ) -> bool:
+ return await super().close_forum_topic(
+ chat_id=chat_id,
+ message_thread_id=message_thread_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 create_forum_topic(
+ self,
+ chat_id: Union[str, int],
+ name: str,
+ icon_color: int = None,
+ icon_custom_emoji_id: str = None,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ rate_limit_args: RLARGS = None,
+ ) -> ForumTopic:
+ return await super().create_forum_topic(
+ chat_id=chat_id,
+ name=name,
+ icon_color=icon_color,
+ icon_custom_emoji_id=icon_custom_emoji_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 pin_chat_message(
self,
chat_id: Union[str, int],
@@ -1790,6 +1911,7 @@ async def promote_chat_member(
is_anonymous: bool = None,
can_manage_chat: bool = None,
can_manage_video_chats: bool = None,
+ can_manage_topics: bool = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1812,6 +1934,29 @@ async def promote_chat_member(
is_anonymous=is_anonymous,
can_manage_chat=can_manage_chat,
can_manage_video_chats=can_manage_video_chats,
+ can_manage_topics=can_manage_topics,
+ 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 reopen_forum_topic(
+ self,
+ chat_id: Union[str, int],
+ message_thread_id: int,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ rate_limit_args: RLARGS = None,
+ ) -> bool:
+ return await super().reopen_forum_topic(
+ chat_id=chat_id,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -1883,6 +2028,7 @@ async def send_animation(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1907,6 +2053,7 @@ async def send_animation(
allow_sending_without_reply=allow_sending_without_reply,
caption_entities=caption_entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
filename=filename,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -1931,6 +2078,7 @@ async def send_audio(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -1955,6 +2103,7 @@ async def send_audio(
allow_sending_without_reply=allow_sending_without_reply,
caption_entities=caption_entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
filename=filename,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -1997,6 +2146,7 @@ async def send_contact(
vcard: str = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
contact: Contact = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2017,6 +2167,7 @@ async def send_contact(
vcard=vcard,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
contact=contact,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -2034,6 +2185,7 @@ async def send_dice(
emoji: str = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2050,6 +2202,7 @@ async def send_dice(
emoji=emoji,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -2071,6 +2224,7 @@ async def send_document(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2093,6 +2247,7 @@ async def send_document(
allow_sending_without_reply=allow_sending_without_reply,
caption_entities=caption_entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
filename=filename,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -2110,6 +2265,7 @@ async def send_game(
reply_markup: InlineKeyboardMarkup = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2126,6 +2282,7 @@ async def send_game(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -2162,6 +2319,7 @@ async def send_invoice(
max_tip_amount: int = None,
suggested_tip_amounts: List[int] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2198,6 +2356,7 @@ async def send_invoice(
max_tip_amount=max_tip_amount,
suggested_tip_amounts=suggested_tip_amounts,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -2219,6 +2378,7 @@ async def send_location(
proximity_alert_radius: int = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
location: Location = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2241,6 +2401,7 @@ async def send_location(
proximity_alert_radius=proximity_alert_radius,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
location=location,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -2259,6 +2420,7 @@ async def send_media_group(
reply_to_message_id: int = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = 20,
@@ -2277,6 +2439,7 @@ async def send_media_group(
reply_to_message_id=reply_to_message_id,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -2299,6 +2462,7 @@ async def send_message(
reply_to_message_id: int = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
reply_markup: ReplyMarkup = None,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2315,6 +2479,7 @@ async def send_message(
disable_web_page_preview=disable_web_page_preview,
disable_notification=disable_notification,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
reply_to_message_id=reply_to_message_id,
allow_sending_without_reply=allow_sending_without_reply,
reply_markup=reply_markup,
@@ -2337,6 +2502,7 @@ async def send_photo(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2357,6 +2523,7 @@ async def send_photo(
allow_sending_without_reply=allow_sending_without_reply,
caption_entities=caption_entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
filename=filename,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -2385,6 +2552,7 @@ async def send_poll(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
explanation_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2412,6 +2580,7 @@ async def send_poll(
allow_sending_without_reply=allow_sending_without_reply,
explanation_entities=explanation_entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -2428,6 +2597,7 @@ async def send_sticker(
reply_markup: ReplyMarkup = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
read_timeout: ODVInput[float] = DEFAULT_NONE,
write_timeout: ODVInput[float] = 20,
@@ -2444,6 +2614,7 @@ async def send_sticker(
reply_markup=reply_markup,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
read_timeout=read_timeout,
write_timeout=write_timeout,
connect_timeout=connect_timeout,
@@ -2467,6 +2638,7 @@ async def send_venue(
google_place_type: str = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
venue: Venue = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2491,6 +2663,7 @@ async def send_venue(
google_place_type=google_place_type,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
venue=venue,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -2516,6 +2689,7 @@ async def send_video(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2541,6 +2715,7 @@ async def send_video(
allow_sending_without_reply=allow_sending_without_reply,
caption_entities=caption_entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
filename=filename,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -2561,6 +2736,7 @@ async def send_video_note(
thumb: FileInput = None,
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2581,6 +2757,7 @@ async def send_video_note(
thumb=thumb,
allow_sending_without_reply=allow_sending_without_reply,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
filename=filename,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -2602,6 +2779,7 @@ async def send_voice(
allow_sending_without_reply: ODVInput[bool] = DEFAULT_NONE,
caption_entities: Union[List["MessageEntity"], Tuple["MessageEntity", ...]] = None,
protect_content: ODVInput[bool] = DEFAULT_NONE,
+ message_thread_id: int = None,
*,
filename: str = None,
read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -2623,6 +2801,7 @@ async def send_voice(
allow_sending_without_reply=allow_sending_without_reply,
caption_entities=caption_entities,
protect_content=protect_content,
+ message_thread_id=message_thread_id,
filename=filename,
read_timeout=read_timeout,
write_timeout=write_timeout,
@@ -3079,6 +3258,28 @@ async def unpin_chat_message(
api_kwargs=self._merge_api_rl_kwargs(api_kwargs, rate_limit_args),
)
+ async def unpin_all_forum_topic_messages(
+ self,
+ chat_id: Union[str, int],
+ message_thread_id: int,
+ *,
+ read_timeout: ODVInput[float] = DEFAULT_NONE,
+ write_timeout: ODVInput[float] = DEFAULT_NONE,
+ connect_timeout: ODVInput[float] = DEFAULT_NONE,
+ pool_timeout: ODVInput[float] = DEFAULT_NONE,
+ api_kwargs: JSONDict = None,
+ rate_limit_args: RLARGS = None,
+ ) -> bool:
+ return await super().unpin_all_forum_topic_messages(
+ chat_id=chat_id,
+ message_thread_id=message_thread_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 upload_sticker_file(
self,
user_id: Union[str, int],
@@ -3190,3 +3391,10 @@ async def upload_sticker_file(
getMyDefaultAdministratorRights = get_my_default_administrator_rights
setMyDefaultAdministratorRights = set_my_default_administrator_rights
createInvoiceLink = create_invoice_link
+ getForumTopicIconStickers = get_forum_topic_icon_stickers
+ createForumTopic = create_forum_topic
+ editForumTopic = edit_forum_topic
+ closeForumTopic = close_forum_topic
+ reopenForumTopic = reopen_forum_topic
+ deleteForumTopic = delete_forum_topic
+ unpinAllForumTopicMessages = unpin_all_forum_topic_messages
diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py
index 5ad236153cd..cdd4cf62ccc 100644
--- a/telegram/ext/filters.py
+++ b/telegram/ext/filters.py
@@ -61,6 +61,7 @@
"HAS_PROTECTED_CONTENT",
"INVOICE",
"IS_AUTOMATIC_FORWARD",
+ "IS_TOPIC_MESSAGE",
"LOCATION",
"Language",
"MessageFilter",
@@ -1422,6 +1423,20 @@ def filter(self, message: Message) -> bool:
"""
+class _IsTopicMessage(MessageFilter):
+ __slots__ = ()
+
+ def filter(self, message: Message) -> bool:
+ return bool(message.is_topic_message)
+
+
+IS_TOPIC_MESSAGE = _IsTopicMessage(name="filters.IS_TOPIC_MESSAGE")
+"""Messages that contain :attr:`telegram.Message.is_topic_message`.
+
+ .. versionadded:: 20.0
+"""
+
+
class Language(MessageFilter):
"""Filters messages to only allow those which are from users with a certain language code.
@@ -1705,6 +1720,9 @@ def filter(self, update: Update) -> bool:
or StatusUpdate.VIDEO_CHAT_ENDED.check_update(update)
or StatusUpdate.VIDEO_CHAT_PARTICIPANTS_INVITED.check_update(update)
or StatusUpdate.WEB_APP_DATA.check_update(update)
+ or StatusUpdate.FORUM_TOPIC_CREATED.check_update(update)
+ or StatusUpdate.FORUM_TOPIC_CLOSED.check_update(update)
+ or StatusUpdate.FORUM_TOPIC_REOPENED.check_update(update)
)
ALL = _All(name="filters.StatusUpdate.ALL")
@@ -1893,6 +1911,42 @@ def filter(self, message: Message) -> bool:
.. versionadded:: 20.0
"""
+ class _ForumTopicCreated(MessageFilter):
+ __slots__ = ()
+
+ def filter(self, message: Message) -> bool:
+ return bool(message.forum_topic_created)
+
+ FORUM_TOPIC_CREATED = _ForumTopicCreated(name="filters.StatusUpdate.FORUM_TOPIC_CREATED")
+ """Messages that contain :attr:`telegram.Message.forum_topic_created`.
+
+ .. versionadded:: 20.0
+ """
+
+ class _ForumTopicClosed(MessageFilter):
+ __slots__ = ()
+
+ def filter(self, message: Message) -> bool:
+ return bool(message.forum_topic_closed)
+
+ FORUM_TOPIC_CLOSED = _ForumTopicClosed(name="filters.StatusUpdate.FORUM_TOPIC_CLOSED")
+ """Messages that contain :attr:`telegram.Message.forum_topic_closed`.
+
+ .. versionadded:: 20.0
+ """
+
+ class _ForumTopicReopened(MessageFilter):
+ __slots__ = ()
+
+ def filter(self, message: Message) -> bool:
+ return bool(message.forum_topic_reopened)
+
+ FORUM_TOPIC_REOPENED = _ForumTopicReopened(name="filters.StatusUpdate.FORUM_TOPIC_REOPENED")
+ """Messages that contain :attr:`telegram.Message.forum_topic_reopened`.
+
+ .. versionadded:: 20.0
+ """
+
class Sticker:
"""Filters messages which contain a sticker.
diff --git a/tests/bots.py b/tests/bots.py
index 69927a32cdc..4ed5160a5ac 100644
--- a/tests/bots.py
+++ b/tests/bots.py
@@ -28,13 +28,14 @@
FALLBACKS = (
"W3sidG9rZW4iOiAiNTc5Njk0NzE0OkFBRnBLOHc2emtrVXJENHhTZVl3RjNNTzhlLTRHcm1jeTdjIiwgInBheW1lbnRfc"
"HJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6TmpRME5qWmxOekk1WWpKaSIsICJjaGF0X2lkIjogIjY3NTY2Nj"
- "IyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3J"
- "hbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAxIiwgImJvdF91c2VybmFtZSI6ICJAcHRi"
- "X2ZhbGxiYWNrXzFfYm90In0sIHsidG9rZW4iOiAiNTU4MTk0MDY2OkFBRndEUElGbHpHVWxDYVdIdFRPRVg0UkZyWDh1O"
- "URNcWZvIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOiAiMjg0Njg1MDYzOlRFU1Q6WWpFd09EUXdNVEZtTkRjeSIsIC"
- "JjaGF0X2lkIjogIjY3NTY2NjIyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTIyMTIxNjgzMCIsICJjaGFubmVsX2l"
- "kIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAyIiwgImJv"
- "dF91c2VybmFtZSI6ICJAcHRiX2ZhbGxiYWNrXzJfYm90In1d"
+ "IyNCIsICJzdXBlcl9ncm91cF9pZCI6ICItMTAwMTMxMDkxMTEzNSIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTYxOTE"
+ "1OTQwNCIsICJjaGFubmVsX2lkIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0"
+ "cyBmYWxsYmFjayAxIiwgImJvdF91c2VybmFtZSI6ICJAcHRiX2ZhbGxiYWNrXzFfYm90In0sIHsidG9rZW4iOiAiNTU4M"
+ "Tk0MDY2OkFBRndEUElGbHpHVWxDYVdIdFRPRVg0UkZyWDh1OURNcWZvIiwgInBheW1lbnRfcHJvdmlkZXJfdG9rZW4iOi"
+ "AiMjg0Njg1MDYzOlRFU1Q6WWpFd09EUXdNVEZtTkRjeSIsICJjaGF0X2lkIjogIjY3NTY2NjIyNCIsICJzdXBlcl9ncm9"
+ "1cF9pZCI6ICItMTAwMTIyMTIxNjgzMCIsICJmb3J1bV9ncm91cF9pZCI6ICItMTAwMTYxOTE1OTQwNCIsICJjaGFubmVs"
+ "X2lkIjogIkBweXRob250ZWxlZ3JhbWJvdHRlc3RzIiwgImJvdF9uYW1lIjogIlBUQiB0ZXN0cyBmYWxsYmFjayAyIiwgI"
+ "mJvdF91c2VybmFtZSI6ICJAcHRiX2ZhbGxiYWNrXzJfYm90In1d "
)
GITHUB_ACTION = os.getenv("GITHUB_ACTION", None)
diff --git a/tests/conftest.py b/tests/conftest.py
index 81b611b1da6..9d56aab7619 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -207,6 +207,11 @@ def super_group_id(bot_info):
return bot_info["super_group_id"]
+@pytest.fixture(scope="session")
+def forum_group_id(bot_info):
+ return int(bot_info["forum_group_id"])
+
+
@pytest.fixture(scope="session")
def channel_id(bot_info):
return bot_info["channel_id"]
diff --git a/tests/test_bot.py b/tests/test_bot.py
index ebfbb5b7c72..e042f67bbd3 100644
--- a/tests/test_bot.py
+++ b/tests/test_bot.py
@@ -2259,6 +2259,7 @@ async def test_promote_chat_member(self, bot, channel_id, monkeypatch):
can_promote_members=True,
can_manage_chat=True,
can_manage_video_chats=True,
+ can_manage_topics=True,
)
# Test that we pass the correct params to TG
@@ -2278,6 +2279,7 @@ async def make_assertion(*args, **_):
and data.get("can_promote_members") == 9
and data.get("can_manage_chat") == 10
and data.get("can_manage_video_chats") == 11
+ and data.get("can_manage_topics") == 12
)
monkeypatch.setattr(bot, "_post", make_assertion)
@@ -2295,6 +2297,7 @@ async def make_assertion(*args, **_):
can_promote_members=9,
can_manage_chat=10,
can_manage_video_chats=11,
+ can_manage_topics=12,
)
@pytest.mark.flaky(3, 1)
@@ -2565,6 +2568,9 @@ async def test_pin_and_unpin_message(self, bot, super_group_id):
# set_sticker_position_in_set, delete_sticker_from_set and get_custom_emoji_stickers
# are tested in the test_sticker module.
+ # get_forum_topic_icon_stickers, edit_forum_topic, etc...
+ # are tested in the test_forum module.
+
async def test_timeout_propagation_explicit(self, monkeypatch, bot, chat_id):
# Use BaseException that's not a subclass of Exception such that
# OkException should not be caught anywhere
@@ -2709,6 +2715,7 @@ async def test_get_set_my_default_administrator_rights(self, bot):
assert my_admin_rights_ch.can_promote_members is my_rights.can_promote_members
assert my_admin_rights_ch.can_restrict_members is my_rights.can_restrict_members
assert my_admin_rights_ch.can_pin_messages is None # Not returned for channels
+ assert my_admin_rights_ch.can_manage_topics is None # Not returned for channels
@pytest.mark.asyncio
async def test_get_set_chat_menu_button(self, bot, chat_id):
@@ -2822,6 +2829,7 @@ async def post(url, request_data: RequestData, *args, **kwargs):
data["caption_entities"]
== [MessageEntity(MessageEntity.BOLD, 0, 4).to_dict()],
data["protect_content"] is True,
+ data["message_thread_id"] == 1,
]
):
pytest.fail("I got wrong parameters in post")
@@ -2839,6 +2847,7 @@ async def post(url, request_data: RequestData, *args, **kwargs):
reply_markup=keyboard.to_json() if json_keyboard else keyboard,
disable_notification=True,
protect_content=True,
+ message_thread_id=1,
)
@pytest.mark.flaky(3, 1)
diff --git a/tests/test_chat.py b/tests/test_chat.py
index ddc9d3cdbd8..e8871deeedd 100644
--- a/tests/test_chat.py
+++ b/tests/test_chat.py
@@ -44,6 +44,9 @@ def chat(bot):
join_to_send_messages=True,
join_by_request=True,
has_restricted_voice_and_video_messages=True,
+ is_forum=True,
+ active_usernames=TestChat.active_usernames,
+ emoji_status_custom_emoji_id=TestChat.emoji_status_custom_emoji_id,
)
chat.set_bot(bot)
return chat
@@ -71,6 +74,9 @@ class TestChat:
join_to_send_messages = True
join_by_request = True
has_restricted_voice_and_video_messages = True
+ is_forum = True
+ active_usernames = ["These", "Are", "Usernames!"]
+ emoji_status_custom_emoji_id = "VeryUniqueCustomEmojiID"
def test_slot_behaviour(self, chat, mro_slots):
for attr in chat.__slots__:
@@ -98,6 +104,9 @@ def test_de_json(self, bot):
"has_restricted_voice_and_video_messages": (
self.has_restricted_voice_and_video_messages
),
+ "is_forum": self.is_forum,
+ "active_usernames": self.active_usernames,
+ "emoji_status_custom_emoji_id": self.emoji_status_custom_emoji_id,
}
chat = Chat.de_json(json_dict, bot)
@@ -124,6 +133,9 @@ def test_de_json(self, bot):
assert chat.api_kwargs == {
"all_members_are_administrators": self.all_members_are_administrators
}
+ assert chat.is_forum == self.is_forum
+ assert chat.active_usernames == self.active_usernames
+ assert chat.emoji_status_custom_emoji_id == self.emoji_status_custom_emoji_id
def test_to_dict(self, chat):
chat_dict = chat.to_dict()
@@ -146,6 +158,9 @@ def test_to_dict(self, chat):
chat_dict["has_restricted_voice_and_video_messages"]
== chat.has_restricted_voice_and_video_messages
)
+ assert chat_dict["is_forum"] == chat.is_forum
+ assert chat_dict["active_usernames"] == chat.active_usernames
+ assert chat_dict["emoji_status_custom_emoji_id"] == chat.emoji_status_custom_emoji_id
def test_enum_init(self):
chat = Chat(id=1, type="foo")
@@ -881,6 +896,128 @@ async def make_assertion(*_, **kwargs):
monkeypatch.setattr(chat.get_bot(), "decline_chat_join_request", make_assertion)
assert await chat.decline_join_request(user_id=42)
+ async def test_create_forum_topic(self, monkeypatch, chat):
+ async def make_assertion(*_, **kwargs):
+ return (
+ kwargs["chat_id"] == chat.id
+ and kwargs["name"] == "New Name"
+ and kwargs["icon_color"] == 0x6FB9F0
+ and kwargs["icon_custom_emoji_id"] == "12345"
+ )
+
+ assert check_shortcut_signature(
+ Chat.create_forum_topic, Bot.create_forum_topic, ["chat_id"], []
+ )
+ assert await check_shortcut_call(
+ chat.create_forum_topic,
+ chat.get_bot(),
+ "create_forum_topic",
+ shortcut_kwargs=["chat_id"],
+ )
+ assert await check_defaults_handling(chat.create_forum_topic, chat.get_bot())
+
+ monkeypatch.setattr(chat.get_bot(), "create_forum_topic", make_assertion)
+ assert await chat.create_forum_topic(
+ name="New Name", icon_color=0x6FB9F0, icon_custom_emoji_id="12345"
+ )
+
+ async def test_edit_forum_topic(self, monkeypatch, chat):
+ async def make_assertion(*_, **kwargs):
+ return (
+ kwargs["chat_id"] == chat.id
+ and kwargs["message_thread_id"] == 42
+ and kwargs["name"] == "New Name"
+ and kwargs["icon_custom_emoji_id"] == "12345"
+ )
+
+ assert check_shortcut_signature(
+ Chat.edit_forum_topic, Bot.edit_forum_topic, ["chat_id"], []
+ )
+ assert await check_shortcut_call(
+ chat.edit_forum_topic, chat.get_bot(), "edit_forum_topic", shortcut_kwargs=["chat_id"]
+ )
+ assert await check_defaults_handling(chat.edit_forum_topic, chat.get_bot())
+
+ monkeypatch.setattr(chat.get_bot(), "edit_forum_topic", make_assertion)
+ assert await chat.edit_forum_topic(
+ message_thread_id=42, name="New Name", icon_custom_emoji_id="12345"
+ )
+
+ async def test_close_forum_topic(self, monkeypatch, chat):
+ async def make_assertion(*_, **kwargs):
+ return kwargs["chat_id"] == chat.id and kwargs["message_thread_id"] == 42
+
+ assert check_shortcut_signature(
+ Chat.close_forum_topic, Bot.close_forum_topic, ["chat_id"], []
+ )
+ assert await check_shortcut_call(
+ chat.close_forum_topic,
+ chat.get_bot(),
+ "close_forum_topic",
+ shortcut_kwargs=["chat_id"],
+ )
+ assert await check_defaults_handling(chat.close_forum_topic, chat.get_bot())
+
+ monkeypatch.setattr(chat.get_bot(), "close_forum_topic", make_assertion)
+ assert await chat.close_forum_topic(message_thread_id=42)
+
+ async def test_reopen_forum_topic(self, monkeypatch, chat):
+ async def make_assertion(*_, **kwargs):
+ return kwargs["chat_id"] == chat.id and kwargs["message_thread_id"] == 42
+
+ assert check_shortcut_signature(
+ Chat.reopen_forum_topic, Bot.reopen_forum_topic, ["chat_id"], []
+ )
+ assert await check_shortcut_call(
+ chat.reopen_forum_topic,
+ chat.get_bot(),
+ "reopen_forum_topic",
+ shortcut_kwargs=["chat_id"],
+ )
+ assert await check_defaults_handling(chat.reopen_forum_topic, chat.get_bot())
+
+ monkeypatch.setattr(chat.get_bot(), "reopen_forum_topic", make_assertion)
+ assert await chat.reopen_forum_topic(message_thread_id=42)
+
+ async def test_delete_forum_topic(self, monkeypatch, chat):
+ async def make_assertion(*_, **kwargs):
+ return kwargs["chat_id"] == chat.id and kwargs["message_thread_id"] == 42
+
+ assert check_shortcut_signature(
+ Chat.delete_forum_topic, Bot.delete_forum_topic, ["chat_id"], []
+ )
+ assert await check_shortcut_call(
+ chat.delete_forum_topic,
+ chat.get_bot(),
+ "delete_forum_topic",
+ shortcut_kwargs=["chat_id"],
+ )
+ assert await check_defaults_handling(chat.delete_forum_topic, chat.get_bot())
+
+ monkeypatch.setattr(chat.get_bot(), "delete_forum_topic", make_assertion)
+ assert await chat.delete_forum_topic(message_thread_id=42)
+
+ async def test_unpin_all_forum_topic_messages(self, monkeypatch, chat):
+ async def make_assertion(*_, **kwargs):
+ return kwargs["chat_id"] == chat.id and kwargs["message_thread_id"] == 42
+
+ assert check_shortcut_signature(
+ Chat.unpin_all_forum_topic_messages,
+ Bot.unpin_all_forum_topic_messages,
+ ["chat_id"],
+ [],
+ )
+ assert await check_shortcut_call(
+ chat.unpin_all_forum_topic_messages,
+ chat.get_bot(),
+ "unpin_all_forum_topic_messages",
+ shortcut_kwargs=["chat_id"],
+ )
+ assert await check_defaults_handling(chat.unpin_all_forum_topic_messages, chat.get_bot())
+
+ monkeypatch.setattr(chat.get_bot(), "unpin_all_forum_topic_messages", make_assertion)
+ assert await chat.unpin_all_forum_topic_messages(message_thread_id=42)
+
def test_mention_html(self):
with pytest.raises(TypeError, match="Can not create a mention to a private group chat"):
chat = Chat(id=1, type="foo")
diff --git a/tests/test_chatadministratorrights.py b/tests/test_chatadministratorrights.py
index 16175f252a6..83826f74bac 100644
--- a/tests/test_chatadministratorrights.py
+++ b/tests/test_chatadministratorrights.py
@@ -34,6 +34,7 @@ def chat_admin_rights():
can_edit_messages=True,
can_manage_chat=True,
can_manage_video_chats=True,
+ can_manage_topics=True,
is_anonymous=True,
)
@@ -57,6 +58,7 @@ def test_de_json(self, bot, chat_admin_rights):
"can_edit_messages": True,
"can_manage_chat": True,
"can_manage_video_chats": True,
+ "can_manage_topics": True,
"is_anonymous": True,
}
chat_administrator_rights_de = ChatAdministratorRights.de_json(json_dict, bot)
@@ -80,13 +82,14 @@ def test_to_dict(self, chat_admin_rights):
assert admin_rights_dict["can_manage_chat"] == car.can_manage_chat
assert admin_rights_dict["is_anonymous"] == car.is_anonymous
assert admin_rights_dict["can_manage_video_chats"] == car.can_manage_video_chats
+ assert admin_rights_dict["can_manage_topics"] == car.can_manage_topics
def test_equality(self):
- a = ChatAdministratorRights(True, False, False, False, False, False, False, False)
- b = ChatAdministratorRights(True, False, False, False, False, False, False, False)
- c = ChatAdministratorRights(False, False, False, False, False, False, False, False)
- d = ChatAdministratorRights(True, True, False, False, False, False, False, False)
- e = ChatAdministratorRights(True, True, False, False, False, False, False, False)
+ a = ChatAdministratorRights(True, False, False, False, False, False, False, False, False)
+ b = ChatAdministratorRights(True, False, False, False, False, False, False, False, False)
+ c = ChatAdministratorRights(False, False, False, False, False, False, False, False, False)
+ d = ChatAdministratorRights(True, True, False, False, False, False, False, False, False)
+ e = ChatAdministratorRights(True, True, False, False, False, False, False, False, False)
assert a == b
assert hash(a) == hash(b)
@@ -102,7 +105,7 @@ def test_equality(self):
assert hash(d) == hash(e)
def test_all_rights(self):
- f = ChatAdministratorRights(True, True, True, True, True, True, True, True)
+ f = ChatAdministratorRights(True, True, True, True, True, True, True, True, True)
t = ChatAdministratorRights.all_rights()
# if the dirs are the same, the attributes will all be there
assert dir(f) == dir(t)
@@ -114,7 +117,7 @@ def test_all_rights(self):
assert f != t
def test_no_rights(self):
- f = ChatAdministratorRights(False, False, False, False, False, False, False, False)
+ f = ChatAdministratorRights(False, False, False, False, False, False, False, False, False)
t = ChatAdministratorRights.no_rights()
# if the dirs are the same, the attributes will all be there
assert dir(f) == dir(t)
diff --git a/tests/test_chatmember.py b/tests/test_chatmember.py
index f2a1d0854c8..ab7e7d23da2 100644
--- a/tests/test_chatmember.py
+++ b/tests/test_chatmember.py
@@ -60,6 +60,7 @@ class CMDefaults:
is_member: bool = True
can_manage_chat: bool = True
can_manage_video_chats: bool = True
+ can_manage_topics: bool = True
def chat_member_owner():
@@ -81,6 +82,7 @@ def chat_member_administrator():
CMDefaults.can_post_messages,
CMDefaults.can_edit_messages,
CMDefaults.can_pin_messages,
+ CMDefaults.can_manage_topics,
CMDefaults.custom_title,
)
@@ -101,6 +103,7 @@ def chat_member_restricted():
CMDefaults.can_send_polls,
CMDefaults.can_send_other_messages,
CMDefaults.can_add_web_page_previews,
+ CMDefaults.can_manage_topics,
CMDefaults.until_date,
)
diff --git a/tests/test_chatpermissions.py b/tests/test_chatpermissions.py
index 64f4da6c269..5498bae24ca 100644
--- a/tests/test_chatpermissions.py
+++ b/tests/test_chatpermissions.py
@@ -33,6 +33,7 @@ def chat_permissions():
can_change_info=True,
can_invite_users=True,
can_pin_messages=True,
+ can_manage_topics=True,
)
@@ -45,6 +46,7 @@ class TestChatPermissions:
can_change_info = False
can_invite_users = None
can_pin_messages = None
+ can_manage_topics = None
def test_slot_behaviour(self, chat_permissions, mro_slots):
inst = chat_permissions
@@ -62,6 +64,7 @@ def test_de_json(self, bot):
"can_change_info": self.can_change_info,
"can_invite_users": self.can_invite_users,
"can_pin_messages": self.can_pin_messages,
+ "can_manage_topics": self.can_manage_topics,
}
permissions = ChatPermissions.de_json(json_dict, bot)
assert permissions.api_kwargs == {}
@@ -74,6 +77,7 @@ def test_de_json(self, bot):
assert permissions.can_change_info == self.can_change_info
assert permissions.can_invite_users == self.can_invite_users
assert permissions.can_pin_messages == self.can_pin_messages
+ assert permissions.can_manage_topics == self.can_manage_topics
def test_to_dict(self, chat_permissions):
permissions_dict = chat_permissions.to_dict()
@@ -94,6 +98,7 @@ def test_to_dict(self, chat_permissions):
assert permissions_dict["can_change_info"] == chat_permissions.can_change_info
assert permissions_dict["can_invite_users"] == chat_permissions.can_invite_users
assert permissions_dict["can_pin_messages"] == chat_permissions.can_pin_messages
+ assert permissions_dict["can_manage_topics"] == chat_permissions.can_manage_topics
def test_equality(self):
a = ChatPermissions(
diff --git a/tests/test_filters.py b/tests/test_filters.py
index a3fcb5c3280..a79a7be0916 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -126,6 +126,22 @@ def filter_class(obj):
for attr in cls.__slots__:
assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}' for {name}"
+ def test__all__(self):
+ expected = {
+ key
+ for key, member in filters.__dict__.items()
+ if (
+ not key.startswith("_")
+ # exclude imported stuff
+ and getattr(member, "__module__", "unknown module") == "telegram.ext.filters"
+ and key != "sys"
+ )
+ }
+ actual = set(filters.__all__)
+ assert (
+ actual == expected
+ ), f"Members {expected - actual} were not listed in constants.__all__"
+
def test_filters_all(self, update):
assert filters.ALL.check_update(update)
@@ -980,6 +996,21 @@ def test_filters_status_update(self, update):
assert filters.StatusUpdate.WEB_APP_DATA.check_update(update)
update.message.web_app_data = None
+ update.message.forum_topic_created = "topic"
+ assert filters.StatusUpdate.ALL.check_update(update)
+ assert filters.StatusUpdate.FORUM_TOPIC_CREATED.check_update(update)
+ update.message.forum_topic_created = None
+
+ update.message.forum_topic_closed = "topic"
+ assert filters.StatusUpdate.ALL.check_update(update)
+ assert filters.StatusUpdate.FORUM_TOPIC_CLOSED.check_update(update)
+ update.message.forum_topic_closed = None
+
+ update.message.forum_topic_reopened = "topic"
+ assert filters.StatusUpdate.ALL.check_update(update)
+ assert filters.StatusUpdate.FORUM_TOPIC_REOPENED.check_update(update)
+ update.message.forum_topic_reopened = None
+
def test_filters_forwarded(self, update):
assert not filters.FORWARDED.check_update(update)
update.message.forward_date = datetime.datetime.utcnow()
@@ -1764,6 +1795,11 @@ def test_filters_is_automatic_forward(self, update):
update.message.is_automatic_forward = True
assert filters.IS_AUTOMATIC_FORWARD.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
+ assert filters.IS_TOPIC_MESSAGE.check_update(update)
+
def test_filters_has_protected_content(self, update):
assert not filters.HAS_PROTECTED_CONTENT.check_update(update)
update.message.has_protected_content = True
diff --git a/tests/test_forum.py b/tests/test_forum.py
new file mode 100644
index 00000000000..d903d3e3d1a
--- /dev/null
+++ b/tests/test_forum.py
@@ -0,0 +1,335 @@
+#!/usr/bin/env python
+#
+# A library that provides a Python interface to the Telegram Bot API
+# Copyright (C) 2015-2022
+# Leandro Toledo de Souza
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser Public License for more details.
+#
+# You should have received a copy of the GNU Lesser Public License
+# along with this program. If not, see [http://www.gnu.org/licenses/].
+import pytest
+
+from telegram import ForumTopic, ForumTopicClosed, ForumTopicCreated, ForumTopicReopened, Sticker
+
+TEST_MSG_TEXT = "Topics are forever"
+TEST_TOPIC_ICON_COLOR = 0x6FB9F0
+TEST_TOPIC_NAME = "Sad bot true: real stories"
+
+
+@pytest.fixture(scope="module")
+async def emoji_id(bot):
+ emoji_sticker_list = await bot.get_forum_topic_icon_stickers()
+ first_sticker = emoji_sticker_list[0]
+ return first_sticker.custom_emoji_id
+
+
+@pytest.fixture
+async def forum_topic_object(forum_group_id, emoji_id):
+ return ForumTopic(
+ message_thread_id=forum_group_id,
+ name=TEST_TOPIC_NAME,
+ icon_color=TEST_TOPIC_ICON_COLOR,
+ icon_custom_emoji_id=emoji_id,
+ )
+
+
+@pytest.fixture
+async def real_topic(bot, emoji_id, forum_group_id):
+ result = await bot.create_forum_topic(
+ chat_id=forum_group_id,
+ name=TEST_TOPIC_NAME,
+ icon_color=TEST_TOPIC_ICON_COLOR,
+ icon_custom_emoji_id=emoji_id,
+ )
+
+ yield result
+
+ result = await bot.delete_forum_topic(
+ chat_id=forum_group_id, message_thread_id=result.message_thread_id
+ )
+ assert result is True, "Topic was not deleted"
+
+
+class TestForumTopic:
+ def test_slot_behaviour(self, mro_slots, forum_topic_object):
+ for attr in forum_topic_object.__slots__:
+ assert getattr(forum_topic_object, attr, "err") != "err", f"got extra slot '{attr}'"
+ assert len(mro_slots(forum_topic_object)) == len(
+ set(mro_slots(forum_topic_object))
+ ), "duplicate slot"
+
+ async def test_expected_values(self, emoji_id, forum_group_id, forum_topic_object):
+ assert forum_topic_object.message_thread_id == forum_group_id
+ assert forum_topic_object.icon_color == TEST_TOPIC_ICON_COLOR
+ assert forum_topic_object.name == TEST_TOPIC_NAME
+ assert forum_topic_object.icon_custom_emoji_id == emoji_id
+
+ def test_de_json(self, bot, emoji_id, forum_group_id):
+ assert ForumTopic.de_json(None, bot=bot) is None
+
+ json_dict = {
+ "message_thread_id": forum_group_id,
+ "name": TEST_TOPIC_NAME,
+ "icon_color": TEST_TOPIC_ICON_COLOR,
+ "icon_custom_emoji_id": emoji_id,
+ }
+ topic = ForumTopic.de_json(json_dict, bot)
+ assert topic.api_kwargs == {}
+
+ assert topic.message_thread_id == forum_group_id
+ assert topic.icon_color == TEST_TOPIC_ICON_COLOR
+ assert topic.name == TEST_TOPIC_NAME
+ assert topic.icon_custom_emoji_id == emoji_id
+
+ def test_to_dict(self, emoji_id, forum_group_id, forum_topic_object):
+ topic_dict = forum_topic_object.to_dict()
+
+ assert isinstance(topic_dict, dict)
+ assert topic_dict["message_thread_id"] == forum_group_id
+ assert topic_dict["name"] == TEST_TOPIC_NAME
+ assert topic_dict["icon_color"] == TEST_TOPIC_ICON_COLOR
+ assert topic_dict["icon_custom_emoji_id"] == emoji_id
+
+ def test_equality(self, emoji_id, forum_group_id):
+ a = ForumTopic(
+ message_thread_id=forum_group_id,
+ name=TEST_TOPIC_NAME,
+ icon_color=TEST_TOPIC_ICON_COLOR,
+ )
+ b = ForumTopic(
+ message_thread_id=forum_group_id,
+ name=TEST_TOPIC_NAME,
+ icon_color=TEST_TOPIC_ICON_COLOR,
+ icon_custom_emoji_id=emoji_id,
+ )
+ c = ForumTopic(
+ message_thread_id=forum_group_id,
+ name=f"{TEST_TOPIC_NAME}!",
+ icon_color=TEST_TOPIC_ICON_COLOR,
+ )
+ d = ForumTopic(
+ message_thread_id=forum_group_id + 1,
+ name=TEST_TOPIC_NAME,
+ icon_color=TEST_TOPIC_ICON_COLOR,
+ )
+ e = ForumTopic(
+ message_thread_id=forum_group_id,
+ name=TEST_TOPIC_NAME,
+ icon_color=0xFFD67E,
+ )
+
+ assert a == b
+ assert hash(a) == hash(b)
+
+ assert a != c
+ assert hash(a) != hash(c)
+
+ assert a != d
+ assert hash(a) != hash(d)
+
+ assert a != e
+ assert hash(a) != hash(e)
+
+ @pytest.mark.flaky(3, 1)
+ async def test_create_forum_topic(self, real_topic):
+ result = real_topic
+ assert isinstance(result, ForumTopic)
+ assert result.name == TEST_TOPIC_NAME
+ assert result.message_thread_id
+ assert isinstance(result.icon_color, int)
+ assert isinstance(result.icon_custom_emoji_id, str)
+
+ async def test_create_forum_topic_with_only_required_args(self, bot, forum_group_id):
+ result = await bot.create_forum_topic(chat_id=forum_group_id, name=TEST_TOPIC_NAME)
+ assert isinstance(result, ForumTopic)
+ assert result.name == TEST_TOPIC_NAME
+ assert result.message_thread_id
+ assert isinstance(result.icon_color, int) # color is still there though it was not passed
+ assert result.icon_custom_emoji_id is None
+
+ result = await bot.delete_forum_topic(
+ chat_id=forum_group_id, message_thread_id=result.message_thread_id
+ )
+ assert result is True, "Failed to delete forum topic"
+
+ @pytest.mark.flaky(3, 1)
+ async def test_get_forum_topic_icon_stickers(self, bot):
+ emoji_sticker_list = await bot.get_forum_topic_icon_stickers()
+ first_sticker = emoji_sticker_list[0]
+
+ assert first_sticker.emoji == "📰"
+ assert first_sticker.height == 512
+ assert first_sticker.width == 512
+ assert first_sticker.is_animated
+ assert not first_sticker.is_video
+ assert first_sticker.set_name == "Topics"
+ assert first_sticker.type == Sticker.CUSTOM_EMOJI
+ assert first_sticker.thumb.width == 128
+ assert first_sticker.thumb.height == 128
+
+ # The following data of first item returned has changed in the past already,
+ # so check sizes loosely and ID's only by length of string
+ assert first_sticker.thumb.file_size in range(2000, 7000)
+ assert first_sticker.file_size in range(20000, 70000)
+ assert len(first_sticker.custom_emoji_id) == 19
+ assert len(first_sticker.thumb.file_unique_id) == 16
+ assert len(first_sticker.file_unique_id) == 15
+
+ async def test_edit_forum_topic(self, emoji_id, forum_group_id, bot, real_topic):
+ result = await bot.edit_forum_topic(
+ chat_id=forum_group_id,
+ message_thread_id=real_topic.message_thread_id,
+ name=f"{TEST_TOPIC_NAME}_EDITED",
+ icon_custom_emoji_id=emoji_id,
+ )
+ assert result is True, "Failed to edit forum topic"
+ # no way of checking the edited name, just the boolean result
+
+ @pytest.mark.flaky(3, 1)
+ async def test_send_message_to_topic(self, bot, forum_group_id, real_topic):
+ message_thread_id = real_topic.message_thread_id
+
+ message = await bot.send_message(
+ chat_id=forum_group_id, text=TEST_MSG_TEXT, message_thread_id=message_thread_id
+ )
+
+ assert message.text == TEST_MSG_TEXT
+ assert message.is_topic_message is True
+ assert message.message_thread_id == message_thread_id
+
+ async def test_close_and_reopen_forum_topic(self, bot, forum_group_id, real_topic):
+ message_thread_id = real_topic.message_thread_id
+
+ result = await bot.close_forum_topic(
+ chat_id=forum_group_id,
+ message_thread_id=message_thread_id,
+ )
+ assert result is True, "Failed to close forum topic"
+ # bot will still be able to send a message to a closed topic, so can't test anything like
+ # the inability to post to the topic
+
+ result = await bot.reopen_forum_topic(
+ chat_id=forum_group_id,
+ message_thread_id=message_thread_id,
+ )
+ assert result is True, "Failed to reopen forum topic"
+
+ @pytest.mark.xfail(reason="Can fail due to race conditions in GH actions CI")
+ async def test_unpin_all_forum_topic_messages(self, bot, forum_group_id, real_topic):
+ message_thread_id = real_topic.message_thread_id
+
+ msgs = [
+ await (
+ await bot.send_message(
+ chat_id=forum_group_id, text=TEST_MSG_TEXT, message_thread_id=message_thread_id
+ )
+ ).pin()
+ for _ in range(2)
+ ]
+
+ assert all(msgs) is True, "Message(s) were not pinned"
+
+ # We need 2 or more pinned msgs for this to work, else we get Chat_not_modified error
+ result = await bot.unpin_all_forum_topic_messages(
+ chat_id=forum_group_id, message_thread_id=message_thread_id
+ )
+ assert result is True, "Failed to unpin all the messages in forum topic"
+
+
+@pytest.fixture
+def topic_created():
+ return ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR)
+
+
+class TestForumTopicCreated:
+ def test_slot_behaviour(self, topic_created, mro_slots):
+ for attr in topic_created.__slots__:
+ assert getattr(topic_created, attr, "err") != "err", f"got extra slot '{attr}'"
+ assert len(mro_slots(topic_created)) == len(
+ set(mro_slots(topic_created))
+ ), "duplicate slot"
+
+ def test_expected_values(self, topic_created):
+ assert topic_created.icon_color == TEST_TOPIC_ICON_COLOR
+ assert topic_created.name == TEST_TOPIC_NAME
+
+ def test_de_json(self, bot):
+ assert ForumTopicCreated.de_json(None, bot=bot) is None
+
+ json_dict = {"icon_color": TEST_TOPIC_ICON_COLOR, "name": TEST_TOPIC_NAME}
+ action = ForumTopicCreated.de_json(json_dict, bot)
+ assert action.api_kwargs == {}
+
+ assert action.icon_color == TEST_TOPIC_ICON_COLOR
+ assert action.name == TEST_TOPIC_NAME
+
+ def test_to_dict(self, topic_created):
+ action_dict = topic_created.to_dict()
+
+ assert isinstance(action_dict, dict)
+ assert action_dict["name"] == TEST_TOPIC_NAME
+ assert action_dict["icon_color"] == TEST_TOPIC_ICON_COLOR
+
+ def test_equality(self, emoji_id):
+ a = ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=TEST_TOPIC_ICON_COLOR)
+ b = ForumTopicCreated(
+ name=TEST_TOPIC_NAME,
+ icon_color=TEST_TOPIC_ICON_COLOR,
+ icon_custom_emoji_id=emoji_id,
+ )
+ c = ForumTopicCreated(name=f"{TEST_TOPIC_NAME}!", icon_color=TEST_TOPIC_ICON_COLOR)
+ d = ForumTopicCreated(name=TEST_TOPIC_NAME, icon_color=0xFFD67E)
+
+ assert a == b
+ assert hash(a) == hash(b)
+
+ assert a != c
+ assert hash(a) != hash(c)
+
+ assert a != d
+ assert hash(a) != hash(d)
+
+
+class TestForumTopicClosed:
+ def test_slot_behaviour(self, mro_slots):
+ action = ForumTopicClosed()
+ 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"
+
+ def test_de_json(self):
+ action = ForumTopicClosed.de_json({}, None)
+ assert action.api_kwargs == {}
+ assert isinstance(action, ForumTopicClosed)
+
+ def test_to_dict(self):
+ action = ForumTopicClosed()
+ action_dict = action.to_dict()
+ assert action_dict == {}
+
+
+class TestForumTopicReopened:
+ def test_slot_behaviour(self, mro_slots):
+ action = ForumTopicReopened()
+ 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"
+
+ def test_de_json(self):
+ action = ForumTopicReopened.de_json({}, None)
+ assert action.api_kwargs == {}
+ assert isinstance(action, ForumTopicReopened)
+
+ def test_to_dict(self):
+ action = ForumTopicReopened()
+ action_dict = action.to_dict()
+ assert action_dict == {}
diff --git a/tests/test_inputmedia.py b/tests/test_inputmedia.py
index 897a9ebcd5e..2f261d19068 100644
--- a/tests/test_inputmedia.py
+++ b/tests/test_inputmedia.py
@@ -45,6 +45,9 @@
# noinspection PyUnresolvedReferences
from .test_document import document, document_file # noqa: F401
+# noinspection PyUnresolvedReferences
+from .test_forum import emoji_id, real_topic # noqa: F401
+
# noinspection PyUnresolvedReferences
from .test_photo import _photo, photo, photo_file, thumb # noqa: F401
@@ -465,6 +468,19 @@ async def test_send_media_group_photo(self, bot, chat_id, media_group):
mes.caption_entities == [MessageEntity(MessageEntity.BOLD, 0, 5)] for mes in messages
)
+ async def test_send_media_group_with_message_thread_id(
+ self, bot, real_topic, forum_group_id, media_group # noqa: F811
+ ):
+ messages = await bot.send_media_group(
+ forum_group_id,
+ media_group,
+ message_thread_id=real_topic.message_thread_id,
+ )
+ assert isinstance(messages, list)
+ assert len(messages) == 3
+ assert all(isinstance(mes, Message) for mes in messages)
+ assert all(i.message_thread_id == real_topic.message_thread_id for i in messages)
+
async def test_send_media_group_throws_error_with_group_caption_and_individual_captions(
self,
bot,
diff --git a/tests/test_message.py b/tests/test_message.py
index 3ad03e386cc..bb8aa8519c6 100644
--- a/tests/test_message.py
+++ b/tests/test_message.py
@@ -198,6 +198,7 @@ def message(bot):
]
},
{"web_app_data": WebAppData("some_data", "some_button_text")},
+ {"message_thread_id": 123},
],
ids=[
"forwarded_user",
@@ -251,6 +252,7 @@ def message(bot):
"has_protected_content",
"entities",
"web_app_data",
+ "message_thread_id",
],
)
def message_params(bot, request):
@@ -1693,6 +1695,124 @@ def test_default_quote(self, message):
finally:
message.get_bot()._defaults = None
+ async def test_edit_forum_topic(self, monkeypatch, message):
+ async def make_assertion(*_, **kwargs):
+ return (
+ kwargs["chat_id"] == message.chat_id
+ and kwargs["message_thread_id"] == message.message_thread_id
+ and kwargs["name"] == "New Name"
+ and kwargs["icon_custom_emoji_id"] == "12345"
+ )
+
+ assert check_shortcut_signature(
+ Message.edit_forum_topic, Bot.edit_forum_topic, ["chat_id", "message_thread_id"], []
+ )
+ assert await check_shortcut_call(
+ message.edit_forum_topic,
+ message.get_bot(),
+ "edit_forum_topic",
+ shortcut_kwargs=["chat_id", "message_thread_id"],
+ )
+ assert await check_defaults_handling(message.edit_forum_topic, message.get_bot())
+
+ monkeypatch.setattr(message.get_bot(), "edit_forum_topic", make_assertion)
+ assert await message.edit_forum_topic(name="New Name", icon_custom_emoji_id="12345")
+
+ async def test_close_forum_topic(self, monkeypatch, message):
+ async def make_assertion(*_, **kwargs):
+ return (
+ kwargs["chat_id"] == message.chat_id
+ and kwargs["message_thread_id"] == message.message_thread_id
+ )
+
+ assert check_shortcut_signature(
+ Message.close_forum_topic, Bot.close_forum_topic, ["chat_id", "message_thread_id"], []
+ )
+ assert await check_shortcut_call(
+ message.close_forum_topic,
+ message.get_bot(),
+ "close_forum_topic",
+ shortcut_kwargs=["chat_id", "message_thread_id"],
+ )
+ assert await check_defaults_handling(message.close_forum_topic, message.get_bot())
+
+ monkeypatch.setattr(message.get_bot(), "close_forum_topic", make_assertion)
+ assert await message.close_forum_topic()
+
+ async def test_reopen_forum_topic(self, monkeypatch, message):
+ async def make_assertion(*_, **kwargs):
+ return (
+ kwargs["chat_id"] == message.chat_id
+ and kwargs["message_thread_id"] == message.message_thread_id
+ )
+
+ assert check_shortcut_signature(
+ Message.reopen_forum_topic,
+ Bot.reopen_forum_topic,
+ ["chat_id", "message_thread_id"],
+ [],
+ )
+ assert await check_shortcut_call(
+ message.reopen_forum_topic,
+ message.get_bot(),
+ "reopen_forum_topic",
+ shortcut_kwargs=["chat_id", "message_thread_id"],
+ )
+ assert await check_defaults_handling(message.reopen_forum_topic, message.get_bot())
+
+ monkeypatch.setattr(message.get_bot(), "reopen_forum_topic", make_assertion)
+ assert await message.reopen_forum_topic()
+
+ async def test_delete_forum_topic(self, monkeypatch, message):
+ async def make_assertion(*_, **kwargs):
+ return (
+ kwargs["chat_id"] == message.chat_id
+ and kwargs["message_thread_id"] == message.message_thread_id
+ )
+
+ assert check_shortcut_signature(
+ Message.delete_forum_topic,
+ Bot.delete_forum_topic,
+ ["chat_id", "message_thread_id"],
+ [],
+ )
+ assert await check_shortcut_call(
+ message.delete_forum_topic,
+ message.get_bot(),
+ "delete_forum_topic",
+ shortcut_kwargs=["chat_id", "message_thread_id"],
+ )
+ assert await check_defaults_handling(message.delete_forum_topic, message.get_bot())
+
+ monkeypatch.setattr(message.get_bot(), "delete_forum_topic", make_assertion)
+ assert await message.delete_forum_topic()
+
+ async def test_unpin_all_forum_topic_messages(self, monkeypatch, message):
+ async def make_assertion(*_, **kwargs):
+ return (
+ kwargs["chat_id"] == message.chat_id
+ and kwargs["message_thread_id"] == message.message_thread_id
+ )
+
+ assert check_shortcut_signature(
+ Message.unpin_all_forum_topic_messages,
+ Bot.unpin_all_forum_topic_messages,
+ ["chat_id", "message_thread_id"],
+ [],
+ )
+ assert await check_shortcut_call(
+ message.unpin_all_forum_topic_messages,
+ message.get_bot(),
+ "unpin_all_forum_topic_messages",
+ shortcut_kwargs=["chat_id", "message_thread_id"],
+ )
+ assert await check_defaults_handling(
+ message.unpin_all_forum_topic_messages, message.get_bot()
+ )
+
+ monkeypatch.setattr(message.get_bot(), "unpin_all_forum_topic_messages", make_assertion)
+ assert await message.unpin_all_forum_topic_messages()
+
def test_equality(self):
id_ = 1
a = Message(