fix(telegram): group attachment detection, typing lifecycle, per-chat session isolation
- Fix group @mention with attachments being silently dropped: filter now checks caption / caption_entities for media messages - Fix typing indicator cancelled prematurely on inline updates (break_loop=false): typing and reply_to cleanup moved to process_chain_end's finally block - Fix same user's group and private chat interfering with each other: _map_key now includes chat_id for per-chat context isolation - Refactor _make_handler to use direct function references instead of string-based getattr
keyboardstaff committed
Mar 23, 2026 at 07:18 UTC
24eb76296317ebdb5cb41fb325046c05907bc222
4 files changed
+33
-30
plugins/_telegram_integration/extensions/python/job_loop/_10_telegram_bot.py
+7
-9
@@ -64,11 +64,11 @@ class TelegramBotManager(Extension):
64
65
try:
66
# Create handler closures that capture bot_name and config
67
- _on_start = partial(_make_handler("handle_start"), bot_name=name, bot_cfg=bot_cfg)
68
- _on_clear = partial(_make_handler("handle_clear"), bot_name=name, bot_cfg=bot_cfg)
69
- _on_message = partial(_make_handler("handle_message"), bot_name=name, bot_cfg=bot_cfg)
70
- _on_callback = partial(_make_handler("handle_callback_query"), bot_name=name, bot_cfg=bot_cfg)
71
- _on_new_members = partial(_make_handler("handle_new_members"), bot_name=name, bot_cfg=bot_cfg)
67
+ _on_start = partial(_make_handler(handle_start), bot_name=name, bot_cfg=bot_cfg)
68
+ _on_clear = partial(_make_handler(handle_clear), bot_name=name, bot_cfg=bot_cfg)
69
+ _on_message = partial(_make_handler(handle_message), bot_name=name, bot_cfg=bot_cfg)
70
+ _on_callback = partial(_make_handler(handle_callback_query), bot_name=name, bot_cfg=bot_cfg)
71
+ _on_new_members = partial(_make_handler(handle_new_members), bot_name=name, bot_cfg=bot_cfg)
72
73
instance = create_bot(
74
name=name,
@@ -115,10 +115,8 @@ def _get_current_bot_cfg(bot_name: str) -> dict:
115
return {}
116
117
118
-def _make_handler(handler_func):
118
+def _make_handler(handler_fn):
119
"""Create a wrapper that resolves fresh bot config on every call."""
120
async def _wrapped(event, bot_name: str, bot_cfg: dict):
121
- from plugins._telegram_integration.helpers import handler
122
- fn = getattr(handler, handler_func)
123
- await fn(event, bot_name, _get_current_bot_cfg(bot_name) or bot_cfg)
121
+ await handler_fn(event, bot_name, _get_current_bot_cfg(bot_name) or bot_cfg)
122
return _wrapped
plugins/_telegram_integration/extensions/python/process_chain_end/_55_telegram_reply.py
+10
-1
@@ -2,7 +2,10 @@ from helpers.extension import Extension
2
from helpers.print_style import PrintStyle
3
from helpers.errors import format_error
4
from agent import AgentContext, LoopData, UserMessage
5
-from plugins._telegram_integration.helpers.handler import CTX_TG_BOT, CTX_TG_ATTACHMENTS, CTX_TG_KEYBOARD
5
+from plugins._telegram_integration.helpers.handler import (
6
+ CTX_TG_BOT, CTX_TG_ATTACHMENTS, CTX_TG_KEYBOARD,
7
+ CTX_TG_TYPING_STOP, CTX_TG_REPLY_TO,
8
+)
9
10
MAX_SEND_RETRIES: int = 2
11
CTX_SEND_FAILURES: str = "_telegram_send_failures"
@@ -29,6 +32,12 @@ class TelegramAutoReply(Extension):
32
await self._send_reply(context, response_text, attachments, keyboard)
33
except Exception as e:
34
PrintStyle.error(f"Telegram auto-reply error: {format_error(e)}")
35
+ finally:
36
+ # Cancel typing and clean up reply_to after final send
37
+ typing_stop = context.data.pop(CTX_TG_TYPING_STOP, None)
38
+ if typing_stop:
39
+ typing_stop.set()
40
+ context.data.pop(CTX_TG_REPLY_TO, None)
41
42
async def _send_reply(
43
self,
plugins/_telegram_integration/helpers/bot_manager.py
+11
-9
@@ -115,19 +115,21 @@ def _make_group_mention_filter(handler: Callable, bot: Bot):
115
await handler(message)
116
return
117
118
- # Check for @mention in text
119
- if message.text and f"@{bot_username}" in message.text:
118
+ # Check for @mention in text or caption (media messages use caption)
119
+ text = message.text or message.caption or ""
120
+ entities = message.entities or message.caption_entities or []
121
+
122
+ if text and f"@{bot_username}" in text:
123
await handler(message)
124
return
125
126
# Check entities for mention
124
- if message.entities:
125
- for entity in message.entities:
126
- if entity.type == "mention":
127
- mention_text = message.text[entity.offset:entity.offset + entity.length]
128
- if mention_text.lower() == f"@{bot_username.lower()}":
129
- await handler(message)
130
- return
127
+ for entity in entities:
128
+ if entity.type == "mention":
129
+ mention_text = text[entity.offset:entity.offset + entity.length]
130
+ if mention_text.lower() == f"@{bot_username.lower()}":
131
+ await handler(message)
132
+ return
133
134
_group_handler.__name__ = f"_group_handler_{id(handler)}"
135
return _group_handler
plugins/_telegram_integration/helpers/handler.py
+5
-11
@@ -61,8 +61,8 @@ def _save_state(state: dict):
61
files.write_file(path, json.dumps(state))
62
63
64
-def _map_key(bot_name: str, user_id: int) -> str:
65
- return f"{bot_name}:{user_id}"
64
+def _map_key(bot_name: str, user_id: int, chat_id: int) -> str:
65
+ return f"{bot_name}:{user_id}:{chat_id}"
66
67
68
def cleanup_old_attachments():
@@ -160,7 +160,7 @@ async def handle_clear(message: TgMessage, bot_name: str, bot_cfg: dict):
160
if not _is_allowed(bot_cfg, user.id, user.username):
161
return
162
163
- key = _map_key(bot_name, user.id)
163
+ key = _map_key(bot_name, user.id, message.chat.id)
164
165
with _chat_map_lock:
166
state = _load_state()
@@ -349,7 +349,7 @@ async def _get_or_create_context_from_user(
349
username: str | None,
350
chat_id: int,
351
) -> AgentContext | None:
352
- key = _map_key(bot_name, user_id)
352
+ key = _map_key(bot_name, user_id, chat_id)
353
354
with _chat_map_lock:
355
state = _load_state()
@@ -360,7 +360,6 @@ async def _get_or_create_context_from_user(
360
if ctx_id:
361
ctx = AgentContext.get(ctx_id)
362
if ctx:
363
- ctx.data[CTX_TG_CHAT_ID] = chat_id # always track latest chat
363
return ctx
364
# Context was garbage collected, remove stale mapping
365
chats.pop(key, None)
@@ -490,12 +489,7 @@ async def send_telegram_reply(
489
if not chat_id:
490
return "No chat_id on context"
491
493
- # Cancel persistent typing indicator
494
- typing_stop = context.data.pop(CTX_TG_TYPING_STOP, None)
495
- if typing_stop:
496
- typing_stop.set()
497
-
498
- reply_to = context.data.pop(CTX_TG_REPLY_TO, None)
492
+ reply_to = context.data.get(CTX_TG_REPLY_TO)
493
494
try:
495
async with _temp_bot(instance.bot.token, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) as reply_bot: