Skip to content

Commit e59bd71

Browse files
authored
Merge pull request freqtrade#11143 from freqtrade/feat/telegram_group_topics
Support Telegram group topics, add /tg_info command
2 parents 215b648 + 1810a91 commit e59bd71

File tree

6 files changed

+98
-13
lines changed

6 files changed

+98
-13
lines changed

build_helpers/schema.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,11 @@
601601
"type": "string"
602602
},
603603
"chat_id": {
604-
"description": "Telegram chat ID",
604+
"description": "Telegram chat or group ID",
605+
"type": "string"
606+
},
607+
"topic_id": {
608+
"description": "Telegram topic ID - only applicable for group chats",
605609
"type": "string"
606610
},
607611
"allow_custom_messages": {

docs/telegram-usage.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,22 @@ Get your "Id", you will use it for the config parameter `chat_id`.
4545

4646
#### Use Group id
4747

48-
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:
48+
To get the group ID, you can add the bot to the group, start freqtrade, and issue a `/tg_info` command.
49+
This will return the group id to you, without having to use some random bot.
50+
While "chat_id" is still required, it doesn't need to be set to this particular group id for this command.
51+
52+
The response will also contain the "topic_id" if necessary - both in a format ready to copy/paste into your configuration.
4953

5054
``` json
51-
"chat":{
52-
"id":-1001332619709
55+
{
56+
"enabled": true,
57+
"token": "********",
58+
"chat_id": "-1001332619709",
59+
"topic_id": "122"
5360
}
5461
```
5562

56-
For the Freqtrade configuration, you can then use the full value (including `-` if it's there) as string:
63+
For the Freqtrade configuration, you can then use the full value (including `-` ) as string:
5764

5865
```json
5966
"chat_id": "-1001332619709"
@@ -62,6 +69,18 @@ For the Freqtrade configuration, you can then use the full value (including `-`
6269
!!! Warning "Using telegram groups"
6370
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.
6471

72+
##### Group Topic ID
73+
74+
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.
75+
Without this, the bot will always respond to the general channel in the group if topics are enabled for a group chat.
76+
77+
```json
78+
"chat_id": "-1001332619709",
79+
"topic_id": "3"
80+
```
81+
82+
Similar to the group-id - you can use `/tg_info` from the topic/thread to get the correct topic-id.
83+
6584
## Control telegram noise
6685

6786
Freqtrade provides means to control the verbosity of your telegram bot.

freqtrade/configuration/config_schema.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,11 @@
460460
},
461461
"token": {"description": "Telegram bot token.", "type": "string"},
462462
"chat_id": {
463-
"description": "Telegram chat ID",
463+
"description": "Telegram chat or group ID",
464+
"type": "string",
465+
},
466+
"topic_id": {
467+
"description": "Telegram topic ID - only applicable for group chats",
464468
"type": "string",
465469
},
466470
"allow_custom_messages": {

freqtrade/rpc/telegram.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ class TimeunitMappings:
9090
def authorized_only(command_handler: Callable[..., Coroutine[Any, Any, None]]):
9191
"""
9292
Decorator to check if the message comes from the correct chat_id
93+
can only be used with Telegram Class to decorate instance methods.
9394
:param command_handler: Telegram CommandHandler
9495
:return: decorated function
9596
"""
@@ -102,13 +103,21 @@ async def wrapper(self, *args, **kwargs):
102103
# Reject unauthorized messages
103104
if update.callback_query:
104105
cchat_id = int(update.callback_query.message.chat.id)
106+
ctopic_id = update.callback_query.message.message_thread_id
105107
else:
106108
cchat_id = int(update.message.chat_id)
109+
ctopic_id = update.message.message_thread_id
107110

108111
chat_id = int(self._config["telegram"]["chat_id"])
109112
if cchat_id != chat_id:
110-
logger.info(f"Rejected unauthorized message from: {update.message.chat_id}")
111-
return wrapper
113+
logger.info(f"Rejected unauthorized message from: {cchat_id}")
114+
return None
115+
if (topic_id := self._config["telegram"].get("topic_id")) is not None:
116+
if str(ctopic_id) != topic_id:
117+
# This can be quite common in multi-topic environments.
118+
logger.debug(f"Rejected message from wrong channel: {cchat_id}, {ctopic_id}")
119+
return None
120+
112121
# Rollback session to avoid getting data stored in a transaction.
113122
Trade.rollback()
114123
logger.debug("Executing handler: %s for chat_id: %s", command_handler.__name__, chat_id)
@@ -291,6 +300,7 @@ def _init(self) -> None:
291300
CommandHandler("marketdir", self._changemarketdir),
292301
CommandHandler("order", self._order),
293302
CommandHandler("list_custom_data", self._list_custom_data),
303+
CommandHandler("tg_info", self._tg_info),
294304
]
295305
callbacks = [
296306
CallbackQueryHandler(self._status_table, pattern="update_status_table"),
@@ -2054,6 +2064,7 @@ async def _send_msg(
20542064
parse_mode=parse_mode,
20552065
reply_markup=reply_markup,
20562066
disable_notification=disable_notification,
2067+
message_thread_id=self._config["telegram"].get("topic_id"),
20572068
)
20582069
except NetworkError as network_err:
20592070
# Sometimes the telegram server resets the current connection,
@@ -2067,6 +2078,7 @@ async def _send_msg(
20672078
parse_mode=parse_mode,
20682079
reply_markup=reply_markup,
20692080
disable_notification=disable_notification,
2081+
message_thread_id=self._config["telegram"].get("topic_id"),
20702082
)
20712083
except TelegramError as telegram_err:
20722084
logger.warning("TelegramError: %s! Giving up on that message.", telegram_err.message)
@@ -2112,3 +2124,37 @@ async def _changemarketdir(self, update: Update, context: CallbackContext) -> No
21122124
"Invalid usage of command /marketdir. \n"
21132125
"Usage: */marketdir [short | long | even | none]*"
21142126
)
2127+
2128+
async def _tg_info(self, update: Update, context: CallbackContext) -> None:
2129+
"""
2130+
Intentionally unauthenticated Handler for /tg_info.
2131+
Returns information about the current telegram chat - even if chat_id does not
2132+
correspond to this chat.
2133+
2134+
:param update: message update
2135+
:return: None
2136+
"""
2137+
if not update.message:
2138+
return
2139+
chat_id = update.message.chat_id
2140+
topic_id = update.message.message_thread_id
2141+
2142+
msg = f"""Freqtrade Bot Info:
2143+
```json
2144+
{{
2145+
"enabled": true,
2146+
"token": "********",
2147+
"chat_id": "{chat_id}",
2148+
{f'"topic_id": "{topic_id}"' if topic_id else ""}
2149+
}}
2150+
```
2151+
"""
2152+
try:
2153+
await context.bot.send_message(
2154+
chat_id=chat_id,
2155+
text=msg,
2156+
parse_mode=ParseMode.MARKDOWN_V2,
2157+
message_thread_id=topic_id,
2158+
)
2159+
except TelegramError as telegram_err:
2160+
logger.warning("TelegramError: %s! Giving up on that message.", telegram_err.message)

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,7 @@ def get_default_conf(testdatadir):
625625
"telegram": {
626626
"enabled": False,
627627
"token": "token",
628-
"chat_id": "0",
628+
"chat_id": "1235",
629629
"notification_settings": {},
630630
},
631631
"datadir": Path(testdatadir),

tests/rpc/test_rpc_telegram.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def default_conf(default_conf) -> dict:
6767

6868
@pytest.fixture
6969
def update():
70-
message = Message(0, datetime.now(timezone.utc), Chat(0, 0))
70+
message = Message(0, datetime.now(timezone.utc), Chat(1235, 0))
7171
_update = Update(0, message=message)
7272

7373
return _update
@@ -167,7 +167,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
167167
"['stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], "
168168
"['bl_delete', 'blacklist_delete'], "
169169
"['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir'], "
170-
"['order'], ['list_custom_data']]"
170+
"['order'], ['list_custom_data'], ['tg_info']]"
171171
)
172172

173173
assert log_has(message_str, caplog)
@@ -224,8 +224,8 @@ async def test_authorized_only(default_conf, mocker, caplog, update) -> None:
224224
patch_get_signal(bot)
225225
await dummy.dummy_handler(update=update, context=MagicMock())
226226
assert dummy.state["called"] is True
227-
assert log_has("Executing handler: dummy_handler for chat_id: 0", caplog)
228-
assert not log_has("Rejected unauthorized message from: 0", caplog)
227+
assert log_has("Executing handler: dummy_handler for chat_id: 1235", caplog)
228+
assert not log_has("Rejected unauthorized message from: 1235", caplog)
229229
assert not log_has("Exception occurred within Telegram module", caplog)
230230

231231

@@ -2967,3 +2967,15 @@ def test_noficiation_settings(default_conf_usdt, mocker):
29672967
assert loudness({"type": RPCMessageType.EXIT, "exit_reason": "roi"}) == "off"
29682968
assert loudness({"type": RPCMessageType.EXIT, "exit_reason": "partial_exit"}) == "off"
29692969
assert loudness({"type": RPCMessageType.EXIT, "exit_reason": "cust_exit112"}) == "off"
2970+
2971+
2972+
async def test__tg_info(default_conf_usdt, mocker, update):
2973+
(telegram, _, _) = get_telegram_testobject(mocker, default_conf_usdt)
2974+
context = AsyncMock()
2975+
2976+
await telegram._tg_info(update, context)
2977+
2978+
assert context.bot.send_message.call_count == 1
2979+
content = context.bot.send_message.call_args[1]["text"]
2980+
assert "Freqtrade Bot Info:\n" in content
2981+
assert '"chat_id": "1235"' in content

0 commit comments

Comments
 (0)