From 2cf6f5d5af75a0fd844df399d2a4613a7cb46138 Mon Sep 17 00:00:00 2001 From: Arash Hatami Date: Wed, 25 Feb 2026 20:38:50 +0330 Subject: [PATCH 1/4] feat(returners): support thread id for Telegram --- salt/returners/telegram_return.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/salt/returners/telegram_return.py b/salt/returners/telegram_return.py index d80ebdde6e24..8b3d9951de0c 100644 --- a/salt/returners/telegram_return.py +++ b/salt/returners/telegram_return.py @@ -5,6 +5,7 @@ telegram.chat_id (required) telegram.token (required) + telegram.thread_id (optional) Telegram settings may also be configured as: @@ -13,6 +14,11 @@ telegram: chat_id: 000000000 token: 000000000:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + thread_id: 0000 + +The ``telegram.thread_id`` is optional and can be used to specify a thread + in a Telegram chat to send the message to. To send messages to the main + chat, simply omit the ``telegram.thread_id``. To use the Telegram return, append '--return telegram' to the salt command. @@ -48,7 +54,7 @@ def _get_options(ret=None): :return: Dictionary containing the data and options needed to send them to telegram. """ - attrs = {"chat_id": "chat_id", "token": "token"} + attrs = {"chat_id": "chat_id", "token": "token", "thread_id": "thread_id"} _options = salt.returners.get_returner_options( __virtualname__, ret, attrs, __salt__=__salt__, __opts__=__opts__ @@ -68,6 +74,7 @@ def returner(ret): chat_id = _options.get("chat_id") token = _options.get("token") + thread_id = _options.get("thread_id") if not chat_id: log.error("telegram.chat_id not defined in salt config") @@ -80,4 +87,6 @@ def returner(ret): ret.get("id"), ret.get("fun"), ret.get("fun_args"), ret.get("jid"), returns ) - return __salt__["telegram.post_message"](message, chat_id=chat_id, token=token) + return __salt__["telegram.post_message"]( + message, chat_id=chat_id, token=token, thread_id=thread_id + ) From af6a81008cd31a983f5b3a7a96019fcd6a300ad0 Mon Sep 17 00:00:00 2001 From: Arash Hatami Date: Wed, 25 Feb 2026 20:38:59 +0330 Subject: [PATCH 2/4] feat(modules): support thread id for Telegram --- salt/modules/telegram.py | 41 +++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/salt/modules/telegram.py b/salt/modules/telegram.py index 3d1e73cca4d5..bf41c936ebce 100644 --- a/salt/modules/telegram.py +++ b/salt/modules/telegram.py @@ -6,8 +6,13 @@ in the pillar. Some sample configs might look like:: telegram.chat_id: '123456789' + telegram.thread_id: '2' telegram.token: '00000000:xxxxxxxxxxxxxxxxxxxxxxxx' + The ``telegram.thread_id`` is optional and can be used to specify a thread + in a Telegram chat to send the message to. To send messages to the main + chat, simply omit the ``telegram.thread_id``. + """ import logging @@ -67,14 +72,28 @@ def _get_token(): return token -def post_message(message, chat_id=None, token=None): +def _get_thread_id(): + """ + Retrieves and return the Telegram's configured thread id + + :return: String: the thread id string + """ + thread_id = __salt__["config.get"]("telegram:thread_id") or __salt__["config.get"]( + "telegram.thread_id" + ) + + return thread_id + + +def post_message(message, chat_id=None, token=None, thread_id=None): """ Send a message to a Telegram chat. - :param message: The message to send to the Telegram chat. - :param chat_id: (optional) The Telegram chat id. - :param token: (optional) The Telegram API token. - :return: Boolean if message was sent successfully. + :param message: The message to send to the Telegram chat. + :param chat_id: (optional) The Telegram chat id. + :param token: (optional) The Telegram API token. + :param thread_id: (optional) The Telegram thread id. + :return: Boolean if message was sent successfully. CLI Example: @@ -86,22 +105,28 @@ def post_message(message, chat_id=None, token=None): if not chat_id: chat_id = _get_chat_id() + if not thread_id: + thread_id = _get_thread_id() + if not token: token = _get_token() if not message: log.error("message is a required option.") - return _post_message(message=message, chat_id=chat_id, token=token) + return _post_message( + message=message, chat_id=chat_id, token=token, thread_id=thread_id + ) -def _post_message(message, chat_id, token): +def _post_message(message, chat_id, token, thread_id=None): """ Send a message to a Telegram chat. :param chat_id: The chat id. :param message: The message to send to the telegram chat. :param token: The Telegram API token. + :param thread_id: (optional) The Telegram thread id. :return: Boolean if message was sent successfully. """ url = f"https://api.telegram.org/bot{token}/sendMessage" @@ -111,6 +136,8 @@ def _post_message(message, chat_id, token): parameters["chat_id"] = chat_id if message: parameters["text"] = message + if thread_id: + parameters["message_thread_id"] = thread_id try: response = requests.post(url, data=parameters, timeout=120) From dc80c9ab89e86b7bf3b24e337bcf36efe27d1e72 Mon Sep 17 00:00:00 2001 From: Arash Hatami Date: Wed, 25 Feb 2026 21:22:15 +0330 Subject: [PATCH 3/4] docs(changelog): add #68771 --- changelog/68771.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/68771.added.md diff --git a/changelog/68771.added.md b/changelog/68771.added.md new file mode 100644 index 000000000000..1b205defa6c2 --- /dev/null +++ b/changelog/68771.added.md @@ -0,0 +1 @@ +Support Telegram topics by adding an optional thread_id configuration option to the Telegram returner and module. From 2c4cbca0ca72797acdd961ca905f68ffc3b8ab77 Mon Sep 17 00:00:00 2001 From: Arash Hatami Date: Thu, 26 Feb 2026 10:22:50 +0330 Subject: [PATCH 4/4] tests: add thread_id for telegram --- tests/pytests/unit/modules/test_telegram.py | 1 + .../unit/returners/test_telegram_return.py | 47 +++++++++++++++---- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/tests/pytests/unit/modules/test_telegram.py b/tests/pytests/unit/modules/test_telegram.py index 02679df4d1ac..35a00aa59ace 100644 --- a/tests/pytests/unit/modules/test_telegram.py +++ b/tests/pytests/unit/modules/test_telegram.py @@ -60,6 +60,7 @@ def configure_loader_modules(): "telegram": { "chat_id": "123456789", "token": "000000000:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "thread_id": "2", } } ), diff --git a/tests/pytests/unit/returners/test_telegram_return.py b/tests/pytests/unit/returners/test_telegram_return.py index 23cbec36ea52..9700358857d5 100644 --- a/tests/pytests/unit/returners/test_telegram_return.py +++ b/tests/pytests/unit/returners/test_telegram_return.py @@ -19,20 +19,49 @@ def test_returner(): """ Test to see if the Telegram returner sends a message """ - ret = { + ret: dict[str, str] = { "id": "12345", "fun": "mytest.func", "fun_args": "myfunc args", "jid": "54321", "return": "The room is on fire as shes fixing her hair", } - options = {"chat_id": "", "token": ""} - - with patch( - "salt.returners.telegram_return._get_options", - MagicMock(return_value=options), - ), patch.dict( - "salt.returners.telegram_return.__salt__", - {"telegram.post_message": MagicMock(return_value=True)}, + options: dict[str, str] = {"chat_id": "", "token": ""} + + with ( + patch( + "salt.returners.telegram_return._get_options", + MagicMock(return_value=options), + ), + patch.dict( + "salt.returners.telegram_return.__salt__", + {"telegram.post_message": MagicMock(return_value=True)}, + ), + ): + assert telegram.returner(ret) is True + + +def test_returner_topics(): + """ + Test to see if the Telegram returner sends a message to specific topic + """ + ret: dict[str, str] = { + "id": "12345", + "fun": "mytest.func", + "fun_args": "myfunc args", + "jid": "54321", + "return": "The room is on fire as shes fixing her hair", + } + options: dict[str, str] = {"chat_id": "", "token": "", "thread_id": ""} + + with ( + patch( + "salt.returners.telegram_return._get_options", + MagicMock(return_value=options), + ), + patch.dict( + "salt.returners.telegram_return.__salt__", + {"telegram.post_message": MagicMock(return_value=True)}, + ), ): assert telegram.returner(ret) is True