fix: complete send_message MEDIA delivery salvage

- prevent raw MEDIA tag leakage outside the gateway pipeline
- make extract_media handle quoted/backticked paths and optional whitespace
- send Telegram media natively with explicit error/warning handling
- add regression tests for Telegram media dispatch and MEDIA parsing
This commit is contained in:
teknium1 2026-03-14 04:01:46 -07:00
parent 50d6659392
commit 5c9a84219d
4 changed files with 309 additions and 44 deletions

View file

@ -14,11 +14,10 @@ import time
logger = logging.getLogger(__name__)
_TELEGRAM_TOPIC_TARGET_RE = re.compile(r"^\s*(-?\d+)(?::(\d+))?\s*$")
_IMAGE_EXTS = {'.jpg', '.jpeg', '.png', '.webp', '.gif'}
_VIDEO_EXTS = {'.mp4', '.mov', '.avi', '.mkv', '.3gp'}
_AUDIO_EXTS = {'.ogg', '.opus', '.mp3', '.wav', '.m4a'}
_VOICE_EXTS = {'.ogg', '.opus'}
_IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
_VIDEO_EXTS = {".mp4", ".mov", ".avi", ".mkv", ".3gp"}
_AUDIO_EXTS = {".ogg", ".opus", ".mp3", ".wav", ".m4a"}
_VOICE_EXTS = {".ogg", ".opus"}
SEND_MESSAGE_SCHEMA = {
@ -135,6 +134,11 @@ def _handle_send(args):
if not pconfig or not pconfig.enabled:
return json.dumps({"error": f"Platform '{platform_name}' is not configured. Set up credentials in ~/.hermes/gateway.json or environment variables."})
from gateway.platforms.base import BasePlatformAdapter
media_files, cleaned_message = BasePlatformAdapter.extract_media(message)
mirror_text = cleaned_message.strip() or _describe_media_for_mirror(media_files)
used_home_channel = False
if not chat_id:
home = config.get_home_channel(platform)
@ -150,16 +154,25 @@ def _handle_send(args):
try:
from model_tools import _run_async
result = _run_async(_send_to_platform(platform, pconfig, chat_id, message, thread_id=thread_id))
result = _run_async(
_send_to_platform(
platform,
pconfig,
chat_id,
cleaned_message,
thread_id=thread_id,
media_files=media_files,
)
)
if used_home_channel and isinstance(result, dict) and result.get("success"):
result["note"] = f"Sent to {platform_name} home channel (chat_id: {chat_id})"
# Mirror the sent message into the target's gateway session
if isinstance(result, dict) and result.get("success"):
if isinstance(result, dict) and result.get("success") and mirror_text:
try:
from gateway.mirror import mirror_to_session
source_label = os.getenv("HERMES_SESSION_PLATFORM", "cli")
if mirror_to_session(platform_name, chat_id, message, source_label=source_label, thread_id=thread_id):
if mirror_to_session(platform_name, chat_id, mirror_text, source_label=source_label, thread_id=thread_id):
result["mirrored"] = True
except Exception:
pass
@ -180,48 +193,97 @@ def _parse_target_ref(platform_name: str, target_ref: str):
return None, None, False
async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None):
def _describe_media_for_mirror(media_files):
"""Return a human-readable mirror summary when a message only contains media."""
if not media_files:
return ""
if len(media_files) == 1:
media_path, is_voice = media_files[0]
ext = os.path.splitext(media_path)[1].lower()
if is_voice and ext in _VOICE_EXTS:
return "[Sent voice message]"
if ext in _IMAGE_EXTS:
return "[Sent image attachment]"
if ext in _VIDEO_EXTS:
return "[Sent video attachment]"
if ext in _AUDIO_EXTS:
return "[Sent audio attachment]"
return "[Sent document attachment]"
return f"[Sent {len(media_files)} media attachments]"
async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None, media_files=None):
"""Route a message to the appropriate platform sender."""
from gateway.config import Platform
media_files = media_files or []
if platform == Platform.TELEGRAM:
return await _send_telegram(pconfig.token, chat_id, message, thread_id=thread_id)
elif platform == Platform.DISCORD:
return await _send_discord(pconfig.token, chat_id, message)
return await _send_telegram(
pconfig.token,
chat_id,
message,
media_files=media_files,
thread_id=thread_id,
)
if media_files and not message.strip():
return {
"error": (
f"send_message MEDIA delivery is currently only supported for telegram; "
f"target {platform.value} had only media attachments"
)
}
warning = None
if media_files:
warning = (
f"MEDIA attachments were omitted for {platform.value}; "
"native send_message media delivery is currently only supported for telegram"
)
if platform == Platform.DISCORD:
result = await _send_discord(pconfig.token, chat_id, message)
elif platform == Platform.SLACK:
return await _send_slack(pconfig.token, chat_id, message)
result = await _send_slack(pconfig.token, chat_id, message)
elif platform == Platform.SIGNAL:
return await _send_signal(pconfig.extra, chat_id, message)
result = await _send_signal(pconfig.extra, chat_id, message)
elif platform == Platform.EMAIL:
return await _send_email(pconfig.extra, chat_id, message)
return {"error": f"Direct sending not yet implemented for {platform.value}"}
result = await _send_email(pconfig.extra, chat_id, message)
else:
result = {"error": f"Direct sending not yet implemented for {platform.value}"}
if warning and isinstance(result, dict) and result.get("success"):
warnings = list(result.get("warnings", []))
warnings.append(warning)
result["warnings"] = warnings
return result
async def _send_telegram(token, chat_id, message, thread_id=None):
async def _send_telegram(token, chat_id, message, media_files=None, thread_id=None):
"""Send via Telegram Bot API (one-shot, no polling needed)."""
try:
from telegram import Bot
from gateway.platforms.base import BasePlatformAdapter
bot = Bot(token=token)
int_chat_id = int(chat_id)
media_files = media_files or []
thread_kwargs = {}
if thread_id is not None:
thread_kwargs["message_thread_id"] = int(thread_id)
# Extract MEDIA:<path> tags and send files natively
media_files, cleaned = BasePlatformAdapter.extract_media(message)
last_msg = None
# Send text portion if any remains
if cleaned.strip():
warnings = []
if message.strip():
last_msg = await bot.send_message(
chat_id=int_chat_id, text=cleaned, **thread_kwargs
chat_id=int_chat_id, text=message, **thread_kwargs
)
# Send extracted media files
for media_path, is_voice in media_files:
if not os.path.exists(media_path):
logger.warning("Media file not found, skipping: %s", media_path)
warning = f"Media file not found, skipping: {media_path}"
logger.warning(warning)
warnings.append(warning)
continue
ext = os.path.splitext(media_path)[1].lower()
try:
with open(media_path, "rb") as f:
@ -246,15 +308,25 @@ async def _send_telegram(token, chat_id, message, thread_id=None):
chat_id=int_chat_id, document=f, **thread_kwargs
)
except Exception as e:
logger.error("Failed to send media %s: %s", media_path, e)
warning = f"Failed to send media {media_path}: {e}"
logger.error(warning)
warnings.append(warning)
# If no text and no media sent, send cleaned text as fallback
if last_msg is None:
last_msg = await bot.send_message(
chat_id=int_chat_id, text=cleaned if cleaned.strip() else message, **thread_kwargs
)
error = "No deliverable text or media remained after processing MEDIA tags"
if warnings:
return {"error": error, "warnings": warnings}
return {"error": error}
return {"success": True, "platform": "telegram", "chat_id": chat_id, "message_id": str(last_msg.message_id)}
result = {
"success": True,
"platform": "telegram",
"chat_id": chat_id,
"message_id": str(last_msg.message_id),
}
if warnings:
result["warnings"] = warnings
return result
except ImportError:
return {"error": "python-telegram-bot not installed. Run: pip install python-telegram-bot"}
except Exception as e: