From 4a256ed7ca55859d195d374370666af5429bfb48 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Dec 2024 19:57:05 +0100 Subject: [PATCH 01/10] chore: failed messages shouldn't return the wrapper but None --- freqtrade/rpc/telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 1b5653b1d..70f3d26c1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -90,6 +90,7 @@ class TimeunitMappings: def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]): """ Decorator to check if the message comes from the correct chat_id + can only be used with Telegram Class to decorate instance methods. :param command_handler: Telegram CommandHandler :return: decorated function """ @@ -108,7 +109,7 @@ def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]): chat_id = int(self._config["telegram"]["chat_id"]) if cchat_id != chat_id: logger.info(f"Rejected unauthorized message from: {update.message.chat_id}") - return wrapper + return None # Rollback session to avoid getting data stored in a transaction. Trade.rollback() logger.debug("Executing handler: %s for chat_id: %s", command_handler.__name__, chat_id) From 6cc0c2be9168044b961fb695b8cca4edf28d77ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Dec 2024 20:11:24 +0100 Subject: [PATCH 02/10] feat: add support for "topic_id" in telegram notifications --- freqtrade/rpc/telegram.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 70f3d26c1..262976d89 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -103,13 +103,20 @@ def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]): # Reject unauthorized messages if update.callback_query: cchat_id = int(update.callback_query.message.chat.id) + ctopic_id = update.callback_query.message.message_thread_id else: cchat_id = int(update.message.chat_id) + ctopic_id = update.message.message_thread_id chat_id = int(self._config["telegram"]["chat_id"]) if cchat_id != chat_id: - logger.info(f"Rejected unauthorized message from: {update.message.chat_id}") + logger.info(f"Rejected unauthorized message from: {cchat_id}") return None + if (topic_id := self._config["telegram"].get("topic_id")) is not None: + if str(ctopic_id) != topic_id: + logger.info(f"Rejected message from wrong channel: {cchat_id}, {ctopic_id}") + return None + # Rollback session to avoid getting data stored in a transaction. Trade.rollback() logger.debug("Executing handler: %s for chat_id: %s", command_handler.__name__, chat_id) @@ -2055,6 +2062,7 @@ class Telegram(RPCHandler): parse_mode=parse_mode, reply_markup=reply_markup, disable_notification=disable_notification, + message_thread_id=self._config["telegram"].get("topic_id"), ) except NetworkError as network_err: # Sometimes the telegram server resets the current connection, @@ -2068,6 +2076,7 @@ class Telegram(RPCHandler): parse_mode=parse_mode, reply_markup=reply_markup, disable_notification=disable_notification, + message_thread_id=self._config["telegram"].get("topic_id"), ) except TelegramError as telegram_err: logger.warning("TelegramError: %s! Giving up on that message.", telegram_err.message) From 0663335011fc46c4b5d6e75da240c3a852b73303 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Dec 2024 20:12:21 +0100 Subject: [PATCH 03/10] chore: update config_schema with topic_id attribute --- build_helpers/schema.json | 6 +++++- freqtrade/configuration/config_schema.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/build_helpers/schema.json b/build_helpers/schema.json index 73c06ba46..5c9844975 100644 --- a/build_helpers/schema.json +++ b/build_helpers/schema.json @@ -601,7 +601,11 @@ "type": "string" }, "chat_id": { - "description": "Telegram chat ID", + "description": "Telegram chat or group ID", + "type": "string" + }, + "topic_id": { + "description": "Telegram topic ID - only applicable for group chats", "type": "string" }, "allow_custom_messages": { diff --git a/freqtrade/configuration/config_schema.py b/freqtrade/configuration/config_schema.py index d31069e7b..9af3f0950 100644 --- a/freqtrade/configuration/config_schema.py +++ b/freqtrade/configuration/config_schema.py @@ -460,7 +460,11 @@ CONF_SCHEMA = { }, "token": {"description": "Telegram bot token.", "type": "string"}, "chat_id": { - "description": "Telegram chat ID", + "description": "Telegram chat or group ID", + "type": "string", + }, + "topic_id": { + "description": "Telegram topic ID - only applicable for group chats", "type": "string", }, "allow_custom_messages": { From b75ac1243a5bec1301a36e8e522c935ed1e4afe9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2024 12:17:15 +0100 Subject: [PATCH 04/10] docs: document group-topic-id --- docs/telegram-usage.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 3fc76d58f..0bd270748 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -62,6 +62,17 @@ For the Freqtrade configuration, you can then use the full value (including `-` !!! Warning "Using telegram groups" When using telegram groups, you're giving every member of the telegram group access to your freqtrade bot and to all commands possible via telegram. Please make sure that you can trust everyone in the telegram group to avoid unpleasant surprises. +##### Group Topic ID + +To use a specific topic in a group, you can use the `topic_id` parameter in the configuration. This will allow you to use the bot in a specific topic in a group. +Without this, the bot will always respond to the general channel in the group if topics are enabled for a group chat. + +```json + "chat_id": "-1001332619709", + "topic_id": "3" +``` + + ## Control telegram noise Freqtrade provides means to control the verbosity of your telegram bot. From cef21860eb32099012aa7ab2daedd58d2d43715b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2024 13:25:59 +0100 Subject: [PATCH 05/10] feat: add _tg_info endpoint for easier information gathering --- freqtrade/rpc/telegram.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 262976d89..106659f1c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -299,6 +299,7 @@ class Telegram(RPCHandler): CommandHandler("marketdir", self._changemarketdir), CommandHandler("order", self._order), CommandHandler("list_custom_data", self._list_custom_data), + CommandHandler("tg_info", self._tg_info), ] callbacks = [ CallbackQueryHandler(self._status_table, pattern="update_status_table"), @@ -2122,3 +2123,35 @@ class Telegram(RPCHandler): "Invalid usage of command /marketdir. \n" "Usage: */marketdir [short | long | even | none]*" ) + + async def _tg_info(self, update: Update, context: CallbackContext) -> None: + """ + Intentionally unauthenticated Handler for /tg_info. + Returns information about the current telegram chat - even if chat_id does not + correspond to this chat. + + :param update: message update + :return: None + """ + chat_id = update.message.chat_id + topic_id = update.message.message_thread_id + + msg = f"""Freqtrade Bot Info: + ```json + {{ + "enabled": true, + "token": "********", + "chat_id": "{chat_id}", + {f'"topic_id": "{topic_id}"' if topic_id else ""} + }} + ``` + """ + try: + await context.bot.send_message( + chat_id=chat_id, + text=msg, + parse_mode=ParseMode.MARKDOWN_V2, + message_thread_id=topic_id, + ) + except TelegramError as telegram_err: + logger.warning("TelegramError: %s! Giving up on that message.", telegram_err.message) From 6ef8291b2e08af93a1606dab1ec4f95757f8113c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2024 13:38:34 +0100 Subject: [PATCH 06/10] test: add test for tg_info, update chat_id to be not 0 --- tests/conftest.py | 2 +- tests/rpc/test_rpc_telegram.py | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 69ea8878b..a66bd3430 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -625,7 +625,7 @@ def get_default_conf(testdatadir): "telegram": { "enabled": False, "token": "token", - "chat_id": "0", + "chat_id": "1235", "notification_settings": {}, }, "datadir": Path(testdatadir), diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index b71b3975e..5cf689da9 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -67,7 +67,7 @@ def default_conf(default_conf) -> dict: @pytest.fixture def update(): - message = Message(0, datetime.now(timezone.utc), Chat(0, 0)) + message = Message(0, datetime.now(timezone.utc), Chat(1235, 0)) _update = Update(0, message=message) return _update @@ -167,7 +167,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: "['stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], " "['bl_delete', 'blacklist_delete'], " "['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir'], " - "['order'], ['list_custom_data']]" + "['order'], ['list_custom_data'], ['tg_info']]" ) assert log_has(message_str, caplog) @@ -224,8 +224,8 @@ async def test_authorized_only(default_conf, mocker, caplog, update) -> None: patch_get_signal(bot) await dummy.dummy_handler(update=update, context=MagicMock()) assert dummy.state["called"] is True - assert log_has("Executing handler: dummy_handler for chat_id: 0", caplog) - assert not log_has("Rejected unauthorized message from: 0", caplog) + assert log_has("Executing handler: dummy_handler for chat_id: 1235", caplog) + assert not log_has("Rejected unauthorized message from: 1235", caplog) assert not log_has("Exception occurred within Telegram module", caplog) @@ -2967,3 +2967,15 @@ def test_noficiation_settings(default_conf_usdt, mocker): assert loudness({"type": RPCMessageType.EXIT, "exit_reason": "roi"}) == "off" assert loudness({"type": RPCMessageType.EXIT, "exit_reason": "partial_exit"}) == "off" assert loudness({"type": RPCMessageType.EXIT, "exit_reason": "cust_exit112"}) == "off" + + +async def test__tg_info(default_conf_usdt, mocker, update): + (telegram, _, _) = get_telegram_testobject(mocker, default_conf_usdt) + context = AsyncMock() + + await telegram._tg_info(update, context) + + assert context.bot.send_message.call_count == 1 + content = context.bot.send_message.call_args[1]["text"] + assert "Freqtrade Bot Info:\n" in content + assert '"chat_id": "1235"' in content From 010b2826795884fdc7c30dfe4cd2096537123b8d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2024 13:46:29 +0100 Subject: [PATCH 07/10] docs: document `/tg_info` command --- docs/telegram-usage.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 0bd270748..99824b521 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -45,15 +45,22 @@ Get your "Id", you will use it for the config parameter `chat_id`. #### Use Group id -You can use bots in telegram groups by just adding them to the group. You can find the group id by first adding a [RawDataBot](https://telegram.me/rawdatabot) to your group. The Group id is shown as id in the `"chat"` section, which the RawDataBot will send to you: +To get the group ID, you can add the bot to the group, start freqtrade, and issue a `/tg_info` command. +This will return the group id to you, without having to use some random bot. +While "chat_id" is still required, it doesn't need to be set to this particular group id for this command. + +The response will also contain the "topic_id" if necessary - both in a format ready to copy/paste into your configuration. ``` json -"chat":{ - "id":-1001332619709 + { + "enabled": true, + "token": "********", + "chat_id": "-1002403829695", + "topic_id": "122" } ``` -For the Freqtrade configuration, you can then use the full value (including `-` if it's there) as string: +For the Freqtrade configuration, you can then use the full value (including `-` ) as string: ```json "chat_id": "-1001332619709" @@ -72,6 +79,7 @@ Without this, the bot will always respond to the general channel in the group if "topic_id": "3" ``` +Similar to the group-id - you can use `/tg_info` from the topic/thread to get the correct topic-id. ## Control telegram noise From d73f01701e19a1ec8a2e798322b207fa681113b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2024 13:52:51 +0100 Subject: [PATCH 08/10] chore: reduce verbosity for topic_id wrong check --- freqtrade/rpc/telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 106659f1c..30fdd7e01 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -114,7 +114,8 @@ def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]): return None if (topic_id := self._config["telegram"].get("topic_id")) is not None: if str(ctopic_id) != topic_id: - logger.info(f"Rejected message from wrong channel: {cchat_id}, {ctopic_id}") + # This can be quite common in multi-topic environments. + logger.debug(f"Rejected message from wrong channel: {cchat_id}, {ctopic_id}") return None # Rollback session to avoid getting data stored in a transaction. From 51ae0f037751b53dfb4cbfbb69cb4d48e536290a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2024 14:05:17 +0100 Subject: [PATCH 09/10] chore: check for update.message being present --- freqtrade/rpc/telegram.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 30fdd7e01..70cf6fe72 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -2134,6 +2134,8 @@ class Telegram(RPCHandler): :param update: message update :return: None """ + if not update.message: + return chat_id = update.message.chat_id topic_id = update.message.message_thread_id From 1810a911d411f9452d0e040d8920ec648eb07cf6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2024 14:42:39 +0100 Subject: [PATCH 10/10] chore: update chatID to be identical throughout docs. --- docs/telegram-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 99824b521..43ab66cbf 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -55,7 +55,7 @@ The response will also contain the "topic_id" if necessary - both in a format re { "enabled": true, "token": "********", - "chat_id": "-1002403829695", + "chat_id": "-1001332619709", "topic_id": "122" } ```