feat(send_message): add media delivery support for Signal

Cherry-picked from PR #13159 by @cdanis.

Adds native media attachment delivery to Signal via signal-cli JSON-RPC
attachments param. Signal messages with media now follow the same
early-return pattern as Telegram/Discord/Matrix — attachments are sent
only with the last chunk to avoid duplicates.

Follow-up fixes on top of the original PR:
- Moved Signal into its own early-return block above the restriction
  check (matches Telegram/Discord/Matrix pattern)
- Fixed media_files being sent on every chunk in the generic loop
- Restored restriction/warning guards to simple form (Signal exits early)
- Fixed non-hermetic test writing to /tmp instead of tmp_path
This commit is contained in:
cdanis 2026-04-20 13:11:03 -07:00 committed by Teknium
parent 761c113427
commit 4a424f1fbb
3 changed files with 252 additions and 6 deletions

View file

@ -514,11 +514,27 @@ async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None,
last_result = result
return last_result
# --- Non-Telegram/Discord platforms ---
# --- Signal: native attachment support via JSON-RPC attachments param ---
if platform == Platform.SIGNAL and media_files:
last_result = None
for i, chunk in enumerate(chunks):
is_last = (i == len(chunks) - 1)
result = await _send_signal(
pconfig.extra,
chat_id,
chunk,
media_files=media_files if is_last else [],
)
if isinstance(result, dict) and result.get("error"):
return result
last_result = result
return last_result
# --- Non-media platforms ---
if media_files and not message.strip():
return {
"error": (
f"send_message MEDIA delivery is currently only supported for telegram, discord, matrix, and weixin; "
f"send_message MEDIA delivery is currently only supported for telegram, discord, matrix, weixin, and signal; "
f"target {platform.value} had only media attachments"
)
}
@ -526,7 +542,7 @@ async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None,
if media_files:
warning = (
f"MEDIA attachments were omitted for {platform.value}; "
"native send_message media delivery is currently only supported for telegram, discord, matrix, and weixin"
"native send_message media delivery is currently only supported for telegram, discord, matrix, weixin, and signal"
)
last_result = None
@ -972,8 +988,12 @@ async def _send_whatsapp(extra, chat_id, message):
return _error(f"WhatsApp send failed: {e}")
async def _send_signal(extra, chat_id, message):
"""Send via signal-cli JSON-RPC API."""
async def _send_signal(extra, chat_id, message, media_files=None):
"""Send via signal-cli JSON-RPC API.
Supports both text-only and text-with-attachments (images/audio/documents).
Attachments are sent as an 'attachments' array in the JSON-RPC params.
"""
try:
import httpx
except ImportError:
@ -990,6 +1010,18 @@ async def _send_signal(extra, chat_id, message):
else:
params["recipient"] = [chat_id]
# Add attachments if media_files are present
valid_media = media_files or []
attachment_paths = []
for media_path, _is_voice in valid_media:
if os.path.exists(media_path):
attachment_paths.append(media_path)
else:
logger.warning("Signal media file not found, skipping: %s", media_path)
if attachment_paths:
params["attachments"] = attachment_paths
payload = {
"jsonrpc": "2.0",
"method": "send",
@ -1003,7 +1035,12 @@ async def _send_signal(extra, chat_id, message):
data = resp.json()
if "error" in data:
return _error(f"Signal RPC error: {data['error']}")
return {"success": True, "platform": "signal", "chat_id": chat_id}
# Return warning for any skipped media files
result = {"success": True, "platform": "signal", "chat_id": chat_id}
if len(attachment_paths) < len(valid_media):
result["warnings"] = [f"Some media files were skipped (not found on disk)"]
return result
except Exception as e:
return _error(f"Signal send failed: {e}")