Skip to content
Open
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
102 changes: 102 additions & 0 deletions tests/tools/test_send_message_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,108 @@ def test_whatsapp_routes_via_local_bridge_sender(self):
async_mock.assert_awaited_once_with({"bridge_port": 3000}, chat_id, "hello from hermes")


class TestSendToPlatformPluginMedia:
@staticmethod
def _register_plugin_platform(name="testmedia"):
from gateway.platform_registry import PlatformEntry, platform_registry

platform_registry.register(
PlatformEntry(
name=name,
label="Test Media",
adapter_factory=lambda _cfg: None,
check_fn=lambda: True,
source="plugin",
)
)
return Platform(name)

@staticmethod
def _unregister_plugin_platform(platform):
from gateway.platform_registry import platform_registry

platform_registry.unregister(platform.value)
Platform._value2member_map_.pop(platform.value, None)
Platform._member_map_.pop(platform.name, None)

def test_plugin_platform_routes_media_through_live_adapter(self, tmp_path, monkeypatch):
platform = self._register_plugin_platform()
try:
image = tmp_path / "chart.png"
audio = tmp_path / "voice.mp3"
doc = tmp_path / "report.pdf"
image.write_bytes(b"png")
audio.write_bytes(b"mp3")
doc.write_bytes(b"pdf")

adapter = SimpleNamespace(
send=AsyncMock(return_value=SimpleNamespace(success=True, message_id="text-1")),
send_image_file=AsyncMock(return_value=SimpleNamespace(success=True, message_id="image-1")),
send_voice=AsyncMock(return_value=SimpleNamespace(success=True, message_id="voice-1")),
send_document=AsyncMock(return_value=SimpleNamespace(success=True, message_id="doc-1")),
)
runner = SimpleNamespace(adapters={platform: adapter})

monkeypatch.setattr("gateway.run._gateway_runner_ref", lambda: runner)

result = asyncio.run(
_send_to_platform(
platform,
SimpleNamespace(enabled=True, token=None, extra={}),
"room-1",
"attached",
media_files=[
(str(image), False),
(str(audio), False),
(str(doc), False),
],
)
)

assert result == {
"success": True,
"platform": "testmedia",
"chat_id": "room-1",
"message_id": "doc-1",
}
adapter.send.assert_awaited_once_with(chat_id="room-1", content="attached")
adapter.send_image_file.assert_awaited_once_with("room-1", str(image))
adapter.send_voice.assert_awaited_once_with("room-1", str(audio))
adapter.send_document.assert_awaited_once_with("room-1", str(doc))
finally:
self._unregister_plugin_platform(platform)

def test_plugin_platform_allows_media_only_send(self, tmp_path, monkeypatch):
platform = self._register_plugin_platform("testmediaonly")
try:
image = tmp_path / "chart.png"
image.write_bytes(b"png")
adapter = SimpleNamespace(
send=AsyncMock(return_value=SimpleNamespace(success=True, message_id="text-1")),
send_image_file=AsyncMock(return_value=SimpleNamespace(success=True, message_id="image-1")),
)
runner = SimpleNamespace(adapters={platform.value: adapter})

monkeypatch.setattr("gateway.run._gateway_runner_ref", lambda: runner)

result = asyncio.run(
_send_to_platform(
platform,
SimpleNamespace(enabled=True, token=None, extra={}),
"room-1",
"",
media_files=[(str(image), False)],
)
)

assert result["success"] is True
assert result["message_id"] == "image-1"
adapter.send.assert_not_awaited()
adapter.send_image_file.assert_awaited_once_with("room-1", str(image))
finally:
self._unregister_plugin_platform(platform)


class TestSendTelegramHtmlDetection:
"""Verify that messages containing HTML tags are sent with parse_mode=HTML
and that plain / markdown messages use MarkdownV2."""
Expand Down
75 changes: 75 additions & 0 deletions tools/send_message_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,65 @@ async def _send_via_adapter(platform, pconfig, chat_id, chunk):
return {"error": f"No live adapter for platform '{platform.value}'. Is the gateway running with this platform connected?"}


def _is_plugin_platform(platform) -> bool:
try:
from gateway.platform_registry import platform_registry

entry = platform_registry.get(platform.value)
return bool(entry and entry.source == "plugin")
except Exception:
return False


async def _send_plugin_media_via_adapter(platform, chat_id, chunk, media_files):
"""Send text/media for a plugin platform through its live gateway adapter."""
try:
from gateway.run import _gateway_runner_ref

runner = _gateway_runner_ref()
adapter = runner.adapters.get(platform) if runner else None
if adapter is None and runner:
adapter = runner.adapters.get(platform.value)
if adapter is None:
return {
"error": (
f"No live adapter for platform '{platform.value}'. "
"Is the gateway running with this platform connected?"
)
}

last_result = None
if chunk.strip():
text_result = await adapter.send(chat_id=chat_id, content=chunk)
if not text_result.success:
return {"error": f"Plugin text send failed: {text_result.error}"}
last_result = text_result

for media_path, is_voice in media_files or []:
ext = os.path.splitext(media_path)[1].lower()
if ext in _IMAGE_EXTS:
result = await adapter.send_image_file(chat_id, media_path)
elif ext in _VOICE_EXTS and is_voice:
result = await adapter.send_voice(chat_id, media_path)
elif ext in _AUDIO_EXTS:
result = await adapter.send_voice(chat_id, media_path)
else:
result = await adapter.send_document(chat_id, media_path)

if not result.success:
return {"error": f"Plugin media send failed: {result.error}"}
last_result = result

return {
"success": True,
"platform": platform.value,
"chat_id": chat_id,
"message_id": getattr(last_result, "message_id", None),
}
except Exception as e:
return {"error": f"Plugin media send failed: {e}"}


async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None, media_files=None):
"""Route a message to the appropriate platform sender.

Expand Down Expand Up @@ -588,6 +647,22 @@ async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None,
last_result = result
return last_result

# --- Plugin platforms: route media through the live adapter interface ---
if media_files and _is_plugin_platform(platform):
last_result = None
for i, chunk in enumerate(chunks):
is_last = (i == len(chunks) - 1)
result = await _send_plugin_media_via_adapter(
platform,
chat_id,
chunk,
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 {
Expand Down
Loading