Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/tools/test_send_message_missing_platforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def test_success(self):
session.put.assert_called_once()
call_kwargs = session.put.call_args
url = call_kwargs[0][0]
assert url.startswith("https://matrix.example.com/_matrix/client/v3/rooms/!room:example.com/send/m.room.message/")
assert url.startswith("https://matrix.example.com/_matrix/client/v3/rooms/%21room%3Aexample.com/send/m.room.message/")
assert call_kwargs[1]["headers"]["Authorization"] == "Bearer syt_tok"
payload = call_kwargs[1]["json"]
assert payload["msgtype"] == "m.text"
Expand Down
79 changes: 79 additions & 0 deletions tests/tools/test_send_message_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from tools.send_message_tool import (
_parse_target_ref,
_send_discord,
_send_matrix_via_adapter,
_send_telegram,
_send_to_platform,
send_message_tool,
Expand Down Expand Up @@ -594,6 +595,84 @@ async def fake_send(token, chat_id, message, media_files=None, thread_id=None):
assert all(call == [] for call in sent_calls[:-1])
assert sent_calls[-1] == media

def test_matrix_media_uses_native_adapter_helper(self):

doc_path = Path("/tmp/test-send-message-matrix.pdf")
doc_path.write_bytes(b"%PDF-1.4 test")

try:
helper = AsyncMock(return_value={"success": True, "platform": "matrix", "chat_id": "!room:example.com", "message_id": "$evt"})
with patch("tools.send_message_tool._send_matrix_via_adapter", helper):
result = asyncio.run(
_send_to_platform(
Platform.MATRIX,
SimpleNamespace(enabled=True, token="tok", extra={"homeserver": "https://matrix.example.com"}),
"!room:example.com",
"here you go",
media_files=[(str(doc_path), False)],
)
)

assert result["success"] is True
helper.assert_awaited_once()
call = helper.await_args
assert call.args[1] == "!room:example.com"
assert call.args[2] == "here you go"
assert call.kwargs["media_files"] == [(str(doc_path), False)]
finally:
doc_path.unlink(missing_ok=True)

def test_send_matrix_via_adapter_sends_document(self, tmp_path):
file_path = tmp_path / "report.pdf"
file_path.write_bytes(b"%PDF-1.4 test")

calls = []

class FakeAdapter:
def __init__(self, _config):
self.connected = False

async def connect(self):
self.connected = True
calls.append(("connect",))
return True

async def send(self, chat_id, message, metadata=None):
calls.append(("send", chat_id, message, metadata))
return SimpleNamespace(success=True, message_id="$text")

async def send_document(self, chat_id, file_path, metadata=None):
calls.append(("send_document", chat_id, file_path, metadata))
return SimpleNamespace(success=True, message_id="$file")

async def disconnect(self):
calls.append(("disconnect",))

fake_module = SimpleNamespace(MatrixAdapter=FakeAdapter)

with patch.dict(sys.modules, {"gateway.platforms.matrix": fake_module}):
result = asyncio.run(
_send_matrix_via_adapter(
SimpleNamespace(enabled=True, token="tok", extra={"homeserver": "https://matrix.example.com"}),
"!room:example.com",
"report attached",
media_files=[(str(file_path), False)],
)
)

assert result == {
"success": True,
"platform": "matrix",
"chat_id": "!room:example.com",
"message_id": "$file",
}
assert calls == [
("connect",),
("send", "!room:example.com", "report attached", None),
("send_document", "!room:example.com", str(file_path), None),
("disconnect",),
]


# ---------------------------------------------------------------------------
# HTML auto-detection in Telegram send
Expand Down
81 changes: 79 additions & 2 deletions tools/send_message_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,19 +404,36 @@ async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None,
last_result = result
return last_result

# --- Matrix: use the native adapter helper for text + media ---
if platform == Platform.MATRIX:
last_result = None
for i, chunk in enumerate(chunks):
is_last = (i == len(chunks) - 1)
result = await _send_matrix_via_adapter(
pconfig,
chat_id,
chunk,
media_files=media_files if is_last else [],
thread_id=thread_id,
)
if isinstance(result, dict) and result.get("error"):
return result
last_result = result
return last_result

# --- Non-Telegram/Discord platforms ---
if media_files and not message.strip():
return {
"error": (
f"send_message MEDIA delivery is currently only supported for telegram, discord, and weixin; "
f"send_message MEDIA delivery is currently only supported for telegram, discord, matrix, and weixin; "
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, discord, and weixin"
"native send_message media delivery is currently only supported for telegram, discord, matrix, and weixin"
)

last_result = None
Expand Down Expand Up @@ -907,6 +924,66 @@ async def _send_matrix(token, extra, chat_id, message):
return _error(f"Matrix send failed: {e}")


async def _send_matrix_via_adapter(pconfig, chat_id, message, media_files=None, thread_id=None):
"""Send via the Matrix adapter so native Matrix media uploads are preserved."""
try:
from gateway.platforms.matrix import MatrixAdapter
except ImportError:
return {"error": "Matrix dependencies not installed. Run: pip install 'mautrix[encryption]'"}

media_files = media_files or []

try:
adapter = MatrixAdapter(pconfig)
connected = await adapter.connect()
if not connected:
return _error("Matrix connect failed")

metadata = {"thread_id": thread_id} if thread_id else None
last_result = None

if message.strip():
last_result = await adapter.send(chat_id, message, metadata=metadata)
if not last_result.success:
return _error(f"Matrix send failed: {last_result.error}")

for media_path, is_voice in media_files:
if not os.path.exists(media_path):
return _error(f"Media file not found: {media_path}")

ext = os.path.splitext(media_path)[1].lower()
if ext in _IMAGE_EXTS:
last_result = await adapter.send_image_file(chat_id, media_path, metadata=metadata)
elif ext in _VIDEO_EXTS:
last_result = await adapter.send_video(chat_id, media_path, metadata=metadata)
elif ext in _VOICE_EXTS and is_voice:
last_result = await adapter.send_voice(chat_id, media_path, metadata=metadata)
elif ext in _AUDIO_EXTS:
last_result = await adapter.send_voice(chat_id, media_path, metadata=metadata)
else:
last_result = await adapter.send_document(chat_id, media_path, metadata=metadata)

if not last_result.success:
return _error(f"Matrix media send failed: {last_result.error}")

if last_result is None:
return {"error": "No deliverable text or media remained after processing MEDIA tags"}

return {
"success": True,
"platform": "matrix",
"chat_id": chat_id,
"message_id": last_result.message_id,
}
except Exception as e:
return _error(f"Matrix send failed: {e}")
finally:
try:
await adapter.disconnect()
except Exception:
pass


async def _send_homeassistant(token, extra, chat_id, message):
"""Send via Home Assistant notify service."""
try:
Expand Down
Loading