mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
fix(telegram): preserve DM topic routing via reply fallback
This commit is contained in:
parent
28b5bd7e93
commit
b3239572f0
6 changed files with 1331 additions and 152 deletions
|
|
@ -361,6 +361,63 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
thread_id = metadata.get("thread_id") or metadata.get("message_thread_id")
|
||||
return str(thread_id) if thread_id is not None else None
|
||||
|
||||
@classmethod
|
||||
def _metadata_direct_messages_topic_id(cls, metadata: Optional[Dict[str, Any]]) -> Optional[str]:
|
||||
if not metadata:
|
||||
return None
|
||||
topic_id = metadata.get("direct_messages_topic_id") or metadata.get("telegram_direct_messages_topic_id")
|
||||
return str(topic_id) if topic_id is not None else None
|
||||
|
||||
@classmethod
|
||||
def _metadata_reply_to_message_id(cls, metadata: Optional[Dict[str, Any]]) -> Optional[int]:
|
||||
if not metadata:
|
||||
return None
|
||||
reply_to = metadata.get("telegram_reply_to_message_id")
|
||||
return int(reply_to) if reply_to is not None else None
|
||||
|
||||
@classmethod
|
||||
def _reply_to_message_id_for_send(
|
||||
cls,
|
||||
reply_to: Optional[str],
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> Optional[int]:
|
||||
if reply_to:
|
||||
return int(reply_to)
|
||||
if metadata and metadata.get("telegram_dm_topic_reply_fallback"):
|
||||
return cls._metadata_reply_to_message_id(metadata)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _thread_kwargs_for_send(
|
||||
cls,
|
||||
chat_id: str,
|
||||
thread_id: Optional[str],
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
reply_to_message_id: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Return Telegram send kwargs for forum and direct-message topic routing.
|
||||
|
||||
Supergroup/forum topics use ``message_thread_id``. True Bot API Direct
|
||||
Messages topics can opt in with explicit ``direct_messages_topic_id``
|
||||
metadata. Hermes-created private-chat topic lanes are marked with
|
||||
``telegram_dm_topic_reply_fallback`` and must send the private topic
|
||||
thread id together with a reply anchor. Live testing showed that either
|
||||
parameter alone can render outside the visible lane.
|
||||
"""
|
||||
if metadata and metadata.get("telegram_dm_topic_reply_fallback"):
|
||||
if reply_to_message_id is None:
|
||||
reply_to_message_id = cls._metadata_reply_to_message_id(metadata)
|
||||
if reply_to_message_id is None:
|
||||
return {}
|
||||
return {"message_thread_id": cls._message_thread_id_for_send(thread_id)}
|
||||
direct_topic_id = cls._metadata_direct_messages_topic_id(metadata)
|
||||
if direct_topic_id is not None:
|
||||
return {
|
||||
"message_thread_id": None,
|
||||
"direct_messages_topic_id": int(direct_topic_id),
|
||||
}
|
||||
return {"message_thread_id": cls._message_thread_id_for_send(thread_id)}
|
||||
|
||||
@classmethod
|
||||
def _message_thread_id_for_send(cls, thread_id: Optional[str]) -> Optional[int]:
|
||||
if not thread_id or str(thread_id) == cls._GENERAL_TOPIC_THREAD_ID:
|
||||
|
|
@ -384,6 +441,65 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
def _is_thread_not_found_error(error: Exception) -> bool:
|
||||
return "thread not found" in str(error).lower()
|
||||
|
||||
@staticmethod
|
||||
def _is_bad_request_error(error: Exception) -> bool:
|
||||
name = error.__class__.__name__.lower()
|
||||
if name == "badrequest" or name.endswith("badrequest"):
|
||||
return True
|
||||
try:
|
||||
from telegram.error import BadRequest
|
||||
return isinstance(error, BadRequest)
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _should_retry_without_dm_topic_reply_anchor(
|
||||
cls,
|
||||
error: Exception,
|
||||
metadata: Optional[Dict[str, Any]],
|
||||
reply_to_message_id: Optional[int],
|
||||
) -> bool:
|
||||
return (
|
||||
bool(metadata and metadata.get("telegram_dm_topic_reply_fallback"))
|
||||
and reply_to_message_id is not None
|
||||
and cls._is_bad_request_error(error)
|
||||
and "message to be replied not found" in str(error).lower()
|
||||
)
|
||||
|
||||
async def _send_with_dm_topic_reply_anchor_retry(
|
||||
self,
|
||||
send_fn: Any,
|
||||
send_kwargs: Dict[str, Any],
|
||||
metadata: Optional[Dict[str, Any]],
|
||||
reply_to_message_id: Optional[int],
|
||||
media_label: str,
|
||||
reset_media: Optional[Any] = None,
|
||||
) -> Any:
|
||||
"""Retry stale private-topic media replies once without the topic anchor."""
|
||||
try:
|
||||
return await send_fn(**send_kwargs)
|
||||
except Exception as send_err:
|
||||
if not self._should_retry_without_dm_topic_reply_anchor(
|
||||
send_err,
|
||||
metadata,
|
||||
reply_to_message_id,
|
||||
):
|
||||
raise
|
||||
logger.warning(
|
||||
"[%s] Reply target deleted for Telegram %s, "
|
||||
"retrying without reply/topic anchor: %s",
|
||||
self.name,
|
||||
media_label,
|
||||
send_err,
|
||||
)
|
||||
if reset_media is not None:
|
||||
reset_media()
|
||||
retry_kwargs = dict(send_kwargs)
|
||||
retry_kwargs["reply_to_message_id"] = None
|
||||
retry_kwargs.pop("message_thread_id", None)
|
||||
retry_kwargs.pop("direct_messages_topic_id", None)
|
||||
return await send_fn(**retry_kwargs)
|
||||
|
||||
def _fallback_ips(self) -> list[str]:
|
||||
"""Return validated fallback IPs from config (populated by _apply_env_overrides)."""
|
||||
configured = self.config.extra.get("fallback_ips", []) if getattr(self.config, "extra", None) else []
|
||||
|
|
@ -1254,9 +1370,23 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
_TimedOut = None # type: ignore[assignment,misc]
|
||||
|
||||
for i, chunk in enumerate(chunks):
|
||||
should_thread = self._should_thread_reply(reply_to, i)
|
||||
reply_to_id = int(reply_to) if should_thread else None
|
||||
effective_thread_id = self._message_thread_id_for_send(thread_id)
|
||||
metadata_reply_to = self._metadata_reply_to_message_id(metadata)
|
||||
reply_to_source = reply_to or (
|
||||
str(metadata_reply_to)
|
||||
if metadata and metadata.get("telegram_dm_topic_reply_fallback") and metadata_reply_to is not None else None
|
||||
)
|
||||
if metadata and metadata.get("telegram_dm_topic_reply_fallback"):
|
||||
should_thread = reply_to_source is not None
|
||||
else:
|
||||
should_thread = self._should_thread_reply(reply_to_source, i)
|
||||
reply_to_id = int(reply_to_source) if should_thread and reply_to_source else None
|
||||
thread_kwargs = self._thread_kwargs_for_send(
|
||||
chat_id,
|
||||
thread_id,
|
||||
metadata,
|
||||
reply_to_message_id=reply_to_id,
|
||||
)
|
||||
effective_thread_id = thread_kwargs.get("message_thread_id")
|
||||
|
||||
msg = None
|
||||
for _send_attempt in range(3):
|
||||
|
|
@ -1268,7 +1398,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
text=chunk,
|
||||
parse_mode=ParseMode.MARKDOWN_V2,
|
||||
reply_to_message_id=reply_to_id,
|
||||
message_thread_id=effective_thread_id,
|
||||
**thread_kwargs,
|
||||
**self._link_preview_kwargs(),
|
||||
)
|
||||
except Exception as md_error:
|
||||
|
|
@ -1281,7 +1411,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
text=plain_chunk,
|
||||
parse_mode=None,
|
||||
reply_to_message_id=reply_to_id,
|
||||
message_thread_id=effective_thread_id,
|
||||
**thread_kwargs,
|
||||
**self._link_preview_kwargs(),
|
||||
)
|
||||
else:
|
||||
|
|
@ -1302,17 +1432,30 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
self.name, effective_thread_id,
|
||||
)
|
||||
effective_thread_id = None
|
||||
thread_kwargs = {"message_thread_id": None}
|
||||
continue
|
||||
err_lower = str(send_err).lower()
|
||||
if "message to be replied not found" in err_lower and reply_to_id is not None:
|
||||
# Original message was deleted before we
|
||||
# could reply — clear reply target and retry
|
||||
# so the response is still delivered.
|
||||
# could reply. For private-topic fallback
|
||||
# sends, message_thread_id is only valid with
|
||||
# the reply anchor, so drop both together.
|
||||
logger.warning(
|
||||
"[%s] Reply target deleted, retrying without reply_to: %s",
|
||||
self.name, send_err,
|
||||
)
|
||||
reply_to_id = None
|
||||
if metadata and metadata.get("telegram_dm_topic_reply_fallback"):
|
||||
thread_kwargs = {}
|
||||
effective_thread_id = None
|
||||
else:
|
||||
thread_kwargs = self._thread_kwargs_for_send(
|
||||
chat_id,
|
||||
thread_id,
|
||||
metadata,
|
||||
reply_to_message_id=reply_to_id,
|
||||
)
|
||||
effective_thread_id = thread_kwargs.get("message_thread_id")
|
||||
continue
|
||||
# Other BadRequest errors are permanent — don't retry
|
||||
raise
|
||||
|
|
@ -1494,13 +1637,19 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
]
|
||||
])
|
||||
thread_id = self._metadata_thread_id(metadata)
|
||||
message_thread_id = self._message_thread_id_for_send(thread_id)
|
||||
reply_to_id = self._reply_to_message_id_for_send(None, metadata)
|
||||
msg = await self._bot.send_message(
|
||||
chat_id=int(chat_id),
|
||||
text=text,
|
||||
parse_mode=ParseMode.MARKDOWN,
|
||||
reply_markup=keyboard,
|
||||
message_thread_id=message_thread_id,
|
||||
reply_to_message_id=reply_to_id,
|
||||
**self._thread_kwargs_for_send(
|
||||
chat_id,
|
||||
thread_id,
|
||||
metadata,
|
||||
reply_to_message_id=reply_to_id,
|
||||
),
|
||||
**self._link_preview_kwargs(),
|
||||
)
|
||||
return SendResult(success=True, message_id=str(msg.message_id))
|
||||
|
|
@ -1558,9 +1707,16 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
"reply_markup": keyboard,
|
||||
**self._link_preview_kwargs(),
|
||||
}
|
||||
message_thread_id = self._message_thread_id_for_send(thread_id)
|
||||
if message_thread_id is not None:
|
||||
kwargs["message_thread_id"] = message_thread_id
|
||||
reply_to_id = self._reply_to_message_id_for_send(None, metadata)
|
||||
kwargs["reply_to_message_id"] = reply_to_id
|
||||
kwargs.update(
|
||||
self._thread_kwargs_for_send(
|
||||
chat_id,
|
||||
thread_id,
|
||||
metadata,
|
||||
reply_to_message_id=reply_to_id,
|
||||
)
|
||||
)
|
||||
|
||||
msg = await self._bot.send_message(**kwargs)
|
||||
|
||||
|
|
@ -1603,9 +1759,16 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
"reply_markup": keyboard,
|
||||
**self._link_preview_kwargs(),
|
||||
}
|
||||
message_thread_id = self._message_thread_id_for_send(thread_id)
|
||||
if message_thread_id is not None:
|
||||
kwargs["message_thread_id"] = message_thread_id
|
||||
reply_to_id = self._reply_to_message_id_for_send(None, metadata)
|
||||
kwargs["reply_to_message_id"] = reply_to_id
|
||||
kwargs.update(
|
||||
self._thread_kwargs_for_send(
|
||||
chat_id,
|
||||
thread_id,
|
||||
metadata,
|
||||
reply_to_message_id=reply_to_id,
|
||||
)
|
||||
)
|
||||
|
||||
msg = await self._bot.send_message(**kwargs)
|
||||
self._slash_confirm_state[confirm_id] = session_key
|
||||
|
|
@ -1664,12 +1827,19 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
)
|
||||
|
||||
thread_id = metadata.get("thread_id") if metadata else None
|
||||
reply_to_id = self._reply_to_message_id_for_send(None, metadata)
|
||||
msg = await self._bot.send_message(
|
||||
chat_id=int(chat_id),
|
||||
text=text,
|
||||
parse_mode=ParseMode.MARKDOWN,
|
||||
reply_markup=keyboard,
|
||||
message_thread_id=int(thread_id) if thread_id else None,
|
||||
reply_to_message_id=reply_to_id,
|
||||
**self._thread_kwargs_for_send(
|
||||
chat_id,
|
||||
thread_id,
|
||||
metadata,
|
||||
reply_to_message_id=reply_to_id,
|
||||
),
|
||||
**self._link_preview_kwargs(),
|
||||
)
|
||||
|
||||
|
|
@ -2046,17 +2216,47 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
session_key, confirm_id, choice,
|
||||
)
|
||||
if result_text and query.message:
|
||||
# Inherit the prompt message's thread so the reply
|
||||
# lands in the same supergroup topic / reply chain.
|
||||
# Inherit the prompt message's topic. Supergroup forums
|
||||
# use message_thread_id; Telegram private DM-topic lanes
|
||||
# need both the private topic id and the prompt reply anchor.
|
||||
thread_id = getattr(query.message, "message_thread_id", None)
|
||||
chat = getattr(query.message, "chat", None)
|
||||
chat_type = getattr(chat, "type", None)
|
||||
prompt_message_id = getattr(query.message, "message_id", None)
|
||||
send_kwargs: Dict[str, Any] = {
|
||||
"chat_id": int(query.message.chat_id),
|
||||
"text": result_text,
|
||||
"parse_mode": ParseMode.MARKDOWN,
|
||||
**self._link_preview_kwargs(),
|
||||
}
|
||||
if thread_id is not None:
|
||||
send_kwargs["message_thread_id"] = thread_id
|
||||
chat_type_value = getattr(chat_type, "value", chat_type)
|
||||
is_private_chat = str(chat_type_value).lower() in {
|
||||
"private",
|
||||
str(ChatType.PRIVATE).lower(),
|
||||
str(getattr(ChatType.PRIVATE, "value", ChatType.PRIVATE)).lower(),
|
||||
}
|
||||
if thread_id is not None and is_private_chat and prompt_message_id is not None:
|
||||
reply_to_id = int(prompt_message_id)
|
||||
send_kwargs["reply_to_message_id"] = reply_to_id
|
||||
send_kwargs.update(
|
||||
self._thread_kwargs_for_send(
|
||||
str(query.message.chat_id),
|
||||
str(thread_id),
|
||||
{
|
||||
"thread_id": str(thread_id),
|
||||
"telegram_dm_topic_reply_fallback": True,
|
||||
},
|
||||
reply_to_message_id=reply_to_id,
|
||||
)
|
||||
)
|
||||
elif thread_id is not None:
|
||||
send_kwargs.update(
|
||||
self._thread_kwargs_for_send(
|
||||
str(query.message.chat_id),
|
||||
str(thread_id),
|
||||
{"thread_id": str(thread_id)},
|
||||
)
|
||||
)
|
||||
await self._bot.send_message(**send_kwargs)
|
||||
except Exception as exc:
|
||||
logger.error("[%s] slash-confirm callback failed: %s", self.name, exc, exc_info=True)
|
||||
|
|
@ -2137,22 +2337,50 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
# .ogg / .opus files -> send as voice (round playable bubble)
|
||||
if ext in (".ogg", ".opus"):
|
||||
_voice_thread = self._metadata_thread_id(metadata)
|
||||
msg = await self._bot.send_voice(
|
||||
chat_id=int(chat_id),
|
||||
voice=audio_file,
|
||||
caption=caption[:1024] if caption else None,
|
||||
reply_to_message_id=int(reply_to) if reply_to else None,
|
||||
message_thread_id=self._message_thread_id_for_send(_voice_thread),
|
||||
reply_to_id = self._reply_to_message_id_for_send(reply_to, metadata)
|
||||
voice_thread_kwargs = self._thread_kwargs_for_send(
|
||||
chat_id,
|
||||
_voice_thread,
|
||||
metadata,
|
||||
reply_to_message_id=reply_to_id,
|
||||
)
|
||||
msg = await self._send_with_dm_topic_reply_anchor_retry(
|
||||
self._bot.send_voice,
|
||||
{
|
||||
"chat_id": int(chat_id),
|
||||
"voice": audio_file,
|
||||
"caption": caption[:1024] if caption else None,
|
||||
"reply_to_message_id": reply_to_id,
|
||||
**voice_thread_kwargs,
|
||||
},
|
||||
metadata,
|
||||
reply_to_id,
|
||||
"voice",
|
||||
reset_media=lambda: audio_file.seek(0),
|
||||
)
|
||||
elif ext in (".mp3", ".m4a"):
|
||||
# Telegram's Bot API sendAudio only accepts MP3 / M4A.
|
||||
_audio_thread = self._metadata_thread_id(metadata)
|
||||
msg = await self._bot.send_audio(
|
||||
chat_id=int(chat_id),
|
||||
audio=audio_file,
|
||||
caption=caption[:1024] if caption else None,
|
||||
reply_to_message_id=int(reply_to) if reply_to else None,
|
||||
message_thread_id=self._message_thread_id_for_send(_audio_thread),
|
||||
reply_to_id = self._reply_to_message_id_for_send(reply_to, metadata)
|
||||
audio_thread_kwargs = self._thread_kwargs_for_send(
|
||||
chat_id,
|
||||
_audio_thread,
|
||||
metadata,
|
||||
reply_to_message_id=reply_to_id,
|
||||
)
|
||||
msg = await self._send_with_dm_topic_reply_anchor_retry(
|
||||
self._bot.send_audio,
|
||||
{
|
||||
"chat_id": int(chat_id),
|
||||
"audio": audio_file,
|
||||
"caption": caption[:1024] if caption else None,
|
||||
"reply_to_message_id": reply_to_id,
|
||||
**audio_thread_kwargs,
|
||||
},
|
||||
metadata,
|
||||
reply_to_id,
|
||||
"audio",
|
||||
reset_media=lambda: audio_file.seek(0),
|
||||
)
|
||||
else:
|
||||
# Formats Telegram can't play natively (.wav, .flac, ...)
|
||||
|
|
@ -2172,7 +2400,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
return await super().send_voice(chat_id, audio_path, caption, reply_to)
|
||||
return await super().send_voice(chat_id, audio_path, caption, reply_to, metadata=metadata)
|
||||
|
||||
async def send_multiple_images(
|
||||
self,
|
||||
|
|
@ -2227,7 +2455,6 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
|
||||
from urllib.parse import unquote as _unquote
|
||||
_thread = self._metadata_thread_id(metadata)
|
||||
_thread_id = self._message_thread_id_for_send(_thread)
|
||||
|
||||
# Chunk into groups of 10 (Telegram's album limit)
|
||||
CHUNK = 10
|
||||
|
|
@ -2263,10 +2490,33 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
"[%s] Sending media group of %d photo(s) (chunk %d/%d)",
|
||||
self.name, len(media), chunk_idx + 1, len(chunks),
|
||||
)
|
||||
await self._bot.send_media_group(
|
||||
chat_id=int(chat_id),
|
||||
media=media,
|
||||
message_thread_id=_thread_id,
|
||||
reply_to_id = self._reply_to_message_id_for_send(None, metadata)
|
||||
thread_kwargs = self._thread_kwargs_for_send(
|
||||
chat_id,
|
||||
_thread,
|
||||
metadata,
|
||||
reply_to_message_id=reply_to_id,
|
||||
)
|
||||
|
||||
def _reset_opened_files() -> None:
|
||||
for fh in opened_files:
|
||||
try:
|
||||
fh.seek(0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await self._send_with_dm_topic_reply_anchor_retry(
|
||||
self._bot.send_media_group,
|
||||
{
|
||||
"chat_id": int(chat_id),
|
||||
"media": media,
|
||||
"reply_to_message_id": reply_to_id,
|
||||
**thread_kwargs,
|
||||
},
|
||||
metadata,
|
||||
reply_to_id,
|
||||
"media group",
|
||||
reset_media=_reset_opened_files,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
|
|
@ -2303,13 +2553,27 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
return SendResult(success=False, error=self._missing_media_path_error("Image", image_path))
|
||||
|
||||
_thread = self._metadata_thread_id(metadata)
|
||||
reply_to_id = self._reply_to_message_id_for_send(reply_to, metadata)
|
||||
thread_kwargs = self._thread_kwargs_for_send(
|
||||
chat_id,
|
||||
_thread,
|
||||
metadata,
|
||||
reply_to_message_id=reply_to_id,
|
||||
)
|
||||
with open(image_path, "rb") as image_file:
|
||||
msg = await self._bot.send_photo(
|
||||
chat_id=int(chat_id),
|
||||
photo=image_file,
|
||||
caption=caption[:1024] if caption else None,
|
||||
reply_to_message_id=int(reply_to) if reply_to else None,
|
||||
message_thread_id=self._message_thread_id_for_send(_thread),
|
||||
msg = await self._send_with_dm_topic_reply_anchor_retry(
|
||||
self._bot.send_photo,
|
||||
{
|
||||
"chat_id": int(chat_id),
|
||||
"photo": image_file,
|
||||
"caption": caption[:1024] if caption else None,
|
||||
"reply_to_message_id": reply_to_id,
|
||||
**thread_kwargs,
|
||||
},
|
||||
metadata,
|
||||
reply_to_id,
|
||||
"photo",
|
||||
reset_media=lambda: image_file.seek(0),
|
||||
)
|
||||
return SendResult(success=True, message_id=str(msg.message_id))
|
||||
except Exception as e:
|
||||
|
|
@ -2360,7 +2624,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
doc_err,
|
||||
exc_info=True,
|
||||
)
|
||||
return await super().send_image_file(chat_id, image_path, caption, reply_to)
|
||||
return await super().send_image_file(chat_id, image_path, caption, reply_to, metadata=metadata)
|
||||
|
||||
async def send_document(
|
||||
self,
|
||||
|
|
@ -2382,20 +2646,34 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
|
||||
display_name = file_name or os.path.basename(file_path)
|
||||
_thread = self._metadata_thread_id(metadata)
|
||||
reply_to_id = self._reply_to_message_id_for_send(reply_to, metadata)
|
||||
thread_kwargs = self._thread_kwargs_for_send(
|
||||
chat_id,
|
||||
_thread,
|
||||
metadata,
|
||||
reply_to_message_id=reply_to_id,
|
||||
)
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
msg = await self._bot.send_document(
|
||||
chat_id=int(chat_id),
|
||||
document=f,
|
||||
filename=display_name,
|
||||
caption=caption[:1024] if caption else None,
|
||||
reply_to_message_id=int(reply_to) if reply_to else None,
|
||||
message_thread_id=self._message_thread_id_for_send(_thread),
|
||||
msg = await self._send_with_dm_topic_reply_anchor_retry(
|
||||
self._bot.send_document,
|
||||
{
|
||||
"chat_id": int(chat_id),
|
||||
"document": f,
|
||||
"filename": display_name,
|
||||
"caption": caption[:1024] if caption else None,
|
||||
"reply_to_message_id": reply_to_id,
|
||||
**thread_kwargs,
|
||||
},
|
||||
metadata,
|
||||
reply_to_id,
|
||||
"document",
|
||||
reset_media=lambda: f.seek(0),
|
||||
)
|
||||
return SendResult(success=True, message_id=str(msg.message_id))
|
||||
except Exception as e:
|
||||
print(f"[{self.name}] Failed to send document: {e}")
|
||||
return await super().send_document(chat_id, file_path, caption, file_name, reply_to)
|
||||
return await super().send_document(chat_id, file_path, caption, file_name, reply_to, metadata=metadata)
|
||||
|
||||
async def send_video(
|
||||
self,
|
||||
|
|
@ -2415,18 +2693,32 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
return SendResult(success=False, error=self._missing_media_path_error("Video", video_path))
|
||||
|
||||
_thread = self._metadata_thread_id(metadata)
|
||||
reply_to_id = self._reply_to_message_id_for_send(reply_to, metadata)
|
||||
thread_kwargs = self._thread_kwargs_for_send(
|
||||
chat_id,
|
||||
_thread,
|
||||
metadata,
|
||||
reply_to_message_id=reply_to_id,
|
||||
)
|
||||
with open(video_path, "rb") as f:
|
||||
msg = await self._bot.send_video(
|
||||
chat_id=int(chat_id),
|
||||
video=f,
|
||||
caption=caption[:1024] if caption else None,
|
||||
reply_to_message_id=int(reply_to) if reply_to else None,
|
||||
message_thread_id=self._message_thread_id_for_send(_thread),
|
||||
msg = await self._send_with_dm_topic_reply_anchor_retry(
|
||||
self._bot.send_video,
|
||||
{
|
||||
"chat_id": int(chat_id),
|
||||
"video": f,
|
||||
"caption": caption[:1024] if caption else None,
|
||||
"reply_to_message_id": reply_to_id,
|
||||
**thread_kwargs,
|
||||
},
|
||||
metadata,
|
||||
reply_to_id,
|
||||
"video",
|
||||
reset_media=lambda: f.seek(0),
|
||||
)
|
||||
return SendResult(success=True, message_id=str(msg.message_id))
|
||||
except Exception as e:
|
||||
print(f"[{self.name}] Failed to send video: {e}")
|
||||
return await super().send_video(chat_id, video_path, caption, reply_to)
|
||||
return await super().send_video(chat_id, video_path, caption, reply_to, metadata=metadata)
|
||||
|
||||
async def send_image(
|
||||
self,
|
||||
|
|
@ -2452,12 +2744,25 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
try:
|
||||
# Telegram can send photos directly from URLs (up to ~5MB)
|
||||
_photo_thread = self._metadata_thread_id(metadata)
|
||||
msg = await self._bot.send_photo(
|
||||
chat_id=int(chat_id),
|
||||
photo=image_url,
|
||||
caption=caption[:1024] if caption else None, # Telegram caption limit
|
||||
reply_to_message_id=int(reply_to) if reply_to else None,
|
||||
message_thread_id=self._message_thread_id_for_send(_photo_thread),
|
||||
reply_to_id = self._reply_to_message_id_for_send(reply_to, metadata)
|
||||
photo_thread_kwargs = self._thread_kwargs_for_send(
|
||||
chat_id,
|
||||
_photo_thread,
|
||||
metadata,
|
||||
reply_to_message_id=reply_to_id,
|
||||
)
|
||||
msg = await self._send_with_dm_topic_reply_anchor_retry(
|
||||
self._bot.send_photo,
|
||||
{
|
||||
"chat_id": int(chat_id),
|
||||
"photo": image_url,
|
||||
"caption": caption[:1024] if caption else None,
|
||||
"reply_to_message_id": reply_to_id,
|
||||
**photo_thread_kwargs,
|
||||
},
|
||||
metadata,
|
||||
reply_to_id,
|
||||
"URL photo",
|
||||
)
|
||||
return SendResult(success=True, message_id=str(msg.message_id))
|
||||
except Exception as e:
|
||||
|
|
@ -2474,13 +2779,25 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
resp = await client.get(image_url)
|
||||
resp.raise_for_status()
|
||||
image_data = resp.content
|
||||
|
||||
msg = await self._bot.send_photo(
|
||||
chat_id=int(chat_id),
|
||||
photo=image_data,
|
||||
caption=caption[:1024] if caption else None,
|
||||
reply_to_message_id=int(reply_to) if reply_to else None,
|
||||
message_thread_id=self._message_thread_id_for_send(_photo_thread),
|
||||
|
||||
upload_thread_kwargs = self._thread_kwargs_for_send(
|
||||
chat_id,
|
||||
_photo_thread,
|
||||
metadata,
|
||||
reply_to_message_id=reply_to_id,
|
||||
)
|
||||
msg = await self._send_with_dm_topic_reply_anchor_retry(
|
||||
self._bot.send_photo,
|
||||
{
|
||||
"chat_id": int(chat_id),
|
||||
"photo": image_data,
|
||||
"caption": caption[:1024] if caption else None,
|
||||
"reply_to_message_id": reply_to_id,
|
||||
**upload_thread_kwargs,
|
||||
},
|
||||
metadata,
|
||||
reply_to_id,
|
||||
"uploaded photo",
|
||||
)
|
||||
return SendResult(success=True, message_id=str(msg.message_id))
|
||||
except Exception as e2:
|
||||
|
|
@ -2491,7 +2808,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
exc_info=True,
|
||||
)
|
||||
# Final fallback: send URL as text
|
||||
return await super().send_image(chat_id, image_url, caption, reply_to)
|
||||
return await super().send_image(chat_id, image_url, caption, reply_to, metadata=metadata)
|
||||
|
||||
async def send_animation(
|
||||
self,
|
||||
|
|
@ -2507,12 +2824,25 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
|
||||
try:
|
||||
_anim_thread = self._metadata_thread_id(metadata)
|
||||
msg = await self._bot.send_animation(
|
||||
chat_id=int(chat_id),
|
||||
animation=animation_url,
|
||||
caption=caption[:1024] if caption else None,
|
||||
reply_to_message_id=int(reply_to) if reply_to else None,
|
||||
message_thread_id=self._message_thread_id_for_send(_anim_thread),
|
||||
reply_to_id = self._reply_to_message_id_for_send(reply_to, metadata)
|
||||
animation_thread_kwargs = self._thread_kwargs_for_send(
|
||||
chat_id,
|
||||
_anim_thread,
|
||||
metadata,
|
||||
reply_to_message_id=reply_to_id,
|
||||
)
|
||||
msg = await self._send_with_dm_topic_reply_anchor_retry(
|
||||
self._bot.send_animation,
|
||||
{
|
||||
"chat_id": int(chat_id),
|
||||
"animation": animation_url,
|
||||
"caption": caption[:1024] if caption else None,
|
||||
"reply_to_message_id": reply_to_id,
|
||||
**animation_thread_kwargs,
|
||||
},
|
||||
metadata,
|
||||
reply_to_id,
|
||||
"animation",
|
||||
)
|
||||
return SendResult(success=True, message_id=str(msg.message_id))
|
||||
except Exception as e:
|
||||
|
|
@ -2523,7 +2853,7 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
exc_info=True,
|
||||
)
|
||||
# Fallback: try as a regular photo
|
||||
return await self.send_image(chat_id, animation_url, caption, reply_to)
|
||||
return await self.send_image(chat_id, animation_url, caption, reply_to, metadata=metadata)
|
||||
|
||||
async def send_typing(self, chat_id: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
"""Send typing indicator."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue