fix(tools): implement send_message routing for Matrix, Mattermost, HomeAssistant, DingTalk (#3796)
* fix(tools): implement send_message routing for Matrix, Mattermost, HomeAssistant, DingTalk Matrix, Mattermost, HomeAssistant, and DingTalk were present in platform_map but fell through to the "not yet implemented" else branch, causing send_message tool calls to silently fail on these platforms. Add four async sender functions: - _send_mattermost: POST /api/v4/posts via Mattermost REST API - _send_matrix: PUT /_matrix/client/v3/rooms/.../send via Matrix CS API - _send_homeassistant: POST /api/services/notify/notify via HA REST API - _send_dingtalk: POST to session webhook URL Add routing in _send_to_platform() and 17 unit tests covering success, HTTP errors, missing config, env var fallback, and Matrix txn_id uniqueness. * fix: pass platform tokens explicitly to Mattermost/Matrix/HA senders The original PR passed pconfig.extra to sender functions, but tokens live at pconfig.token (not in extra). This caused the senders to always fall through to env var lookup instead of using the gateway-resolved token. Changes: - Mattermost/Matrix/HA: accept token as first arg, matching the Telegram/Discord/Slack sender pattern - DingTalk: add DINGTALK_WEBHOOK_URL env var fallback + docstring explaining the session-webhook vs robot-webhook difference - Tests updated for new signatures + new DingTalk env var test --------- Co-authored-by: sprmn24 <oncuevtv@gmail.com>
This commit is contained in:
@@ -343,6 +343,14 @@ async def _send_to_platform(platform, pconfig, chat_id, message, thread_id=None,
|
||||
result = await _send_email(pconfig.extra, chat_id, chunk)
|
||||
elif platform == Platform.SMS:
|
||||
result = await _send_sms(pconfig.api_key, chat_id, chunk)
|
||||
elif platform == Platform.MATTERMOST:
|
||||
result = await _send_mattermost(pconfig.token, pconfig.extra, chat_id, chunk)
|
||||
elif platform == Platform.MATRIX:
|
||||
result = await _send_matrix(pconfig.token, pconfig.extra, chat_id, chunk)
|
||||
elif platform == Platform.HOMEASSISTANT:
|
||||
result = await _send_homeassistant(pconfig.token, pconfig.extra, chat_id, chunk)
|
||||
elif platform == Platform.DINGTALK:
|
||||
result = await _send_dingtalk(pconfig.extra, chat_id, chunk)
|
||||
else:
|
||||
result = {"error": f"Direct sending not yet implemented for {platform.value}"}
|
||||
|
||||
@@ -666,6 +674,109 @@ async def _send_sms(auth_token, chat_id, message):
|
||||
return {"error": f"SMS send failed: {e}"}
|
||||
|
||||
|
||||
async def _send_mattermost(token, extra, chat_id, message):
|
||||
"""Send via Mattermost REST API."""
|
||||
try:
|
||||
import aiohttp
|
||||
except ImportError:
|
||||
return {"error": "aiohttp not installed. Run: pip install aiohttp"}
|
||||
try:
|
||||
base_url = (extra.get("url") or os.getenv("MATTERMOST_URL", "")).rstrip("/")
|
||||
token = token or os.getenv("MATTERMOST_TOKEN", "")
|
||||
if not base_url or not token:
|
||||
return {"error": "Mattermost not configured (MATTERMOST_URL, MATTERMOST_TOKEN required)"}
|
||||
url = f"{base_url}/api/v4/posts"
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as session:
|
||||
async with session.post(url, headers=headers, json={"channel_id": chat_id, "message": message}) as resp:
|
||||
if resp.status not in (200, 201):
|
||||
body = await resp.text()
|
||||
return {"error": f"Mattermost API error ({resp.status}): {body}"}
|
||||
data = await resp.json()
|
||||
return {"success": True, "platform": "mattermost", "chat_id": chat_id, "message_id": data.get("id")}
|
||||
except Exception as e:
|
||||
return {"error": f"Mattermost send failed: {e}"}
|
||||
|
||||
|
||||
async def _send_matrix(token, extra, chat_id, message):
|
||||
"""Send via Matrix Client-Server API."""
|
||||
try:
|
||||
import aiohttp
|
||||
except ImportError:
|
||||
return {"error": "aiohttp not installed. Run: pip install aiohttp"}
|
||||
try:
|
||||
homeserver = (extra.get("homeserver") or os.getenv("MATRIX_HOMESERVER", "")).rstrip("/")
|
||||
token = token or os.getenv("MATRIX_ACCESS_TOKEN", "")
|
||||
if not homeserver or not token:
|
||||
return {"error": "Matrix not configured (MATRIX_HOMESERVER, MATRIX_ACCESS_TOKEN required)"}
|
||||
txn_id = f"hermes_{int(time.time() * 1000)}"
|
||||
url = f"{homeserver}/_matrix/client/v3/rooms/{chat_id}/send/m.room.message/{txn_id}"
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as session:
|
||||
async with session.put(url, headers=headers, json={"msgtype": "m.text", "body": message}) as resp:
|
||||
if resp.status not in (200, 201):
|
||||
body = await resp.text()
|
||||
return {"error": f"Matrix API error ({resp.status}): {body}"}
|
||||
data = await resp.json()
|
||||
return {"success": True, "platform": "matrix", "chat_id": chat_id, "message_id": data.get("event_id")}
|
||||
except Exception as e:
|
||||
return {"error": f"Matrix send failed: {e}"}
|
||||
|
||||
|
||||
async def _send_homeassistant(token, extra, chat_id, message):
|
||||
"""Send via Home Assistant notify service."""
|
||||
try:
|
||||
import aiohttp
|
||||
except ImportError:
|
||||
return {"error": "aiohttp not installed. Run: pip install aiohttp"}
|
||||
try:
|
||||
hass_url = (extra.get("url") or os.getenv("HASS_URL", "")).rstrip("/")
|
||||
token = token or os.getenv("HASS_TOKEN", "")
|
||||
if not hass_url or not token:
|
||||
return {"error": "Home Assistant not configured (HASS_URL, HASS_TOKEN required)"}
|
||||
url = f"{hass_url}/api/services/notify/notify"
|
||||
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as session:
|
||||
async with session.post(url, headers=headers, json={"message": message, "target": chat_id}) as resp:
|
||||
if resp.status not in (200, 201):
|
||||
body = await resp.text()
|
||||
return {"error": f"Home Assistant API error ({resp.status}): {body}"}
|
||||
return {"success": True, "platform": "homeassistant", "chat_id": chat_id}
|
||||
except Exception as e:
|
||||
return {"error": f"Home Assistant send failed: {e}"}
|
||||
|
||||
|
||||
async def _send_dingtalk(extra, chat_id, message):
|
||||
"""Send via DingTalk robot webhook.
|
||||
|
||||
Note: The gateway's DingTalk adapter uses per-session webhook URLs from
|
||||
incoming messages (dingtalk-stream SDK). For cross-platform send_message
|
||||
delivery we use a static robot webhook URL instead, which must be
|
||||
configured via ``DINGTALK_WEBHOOK_URL`` env var or ``webhook_url`` in the
|
||||
platform's extra config.
|
||||
"""
|
||||
try:
|
||||
import httpx
|
||||
except ImportError:
|
||||
return {"error": "httpx not installed"}
|
||||
try:
|
||||
webhook_url = extra.get("webhook_url") or os.getenv("DINGTALK_WEBHOOK_URL", "")
|
||||
if not webhook_url:
|
||||
return {"error": "DingTalk not configured. Set DINGTALK_WEBHOOK_URL env var or webhook_url in dingtalk platform extra config."}
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
resp = await client.post(
|
||||
webhook_url,
|
||||
json={"msgtype": "text", "text": {"content": message}},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if data.get("errcode", 0) != 0:
|
||||
return {"error": f"DingTalk API error: {data.get('errmsg', 'unknown')}"}
|
||||
return {"success": True, "platform": "dingtalk", "chat_id": chat_id}
|
||||
except Exception as e:
|
||||
return {"error": f"DingTalk send failed: {e}"}
|
||||
|
||||
|
||||
def _check_send_message():
|
||||
"""Gate send_message on gateway running (always available on messaging platforms)."""
|
||||
platform = os.getenv("HERMES_SESSION_PLATFORM", "")
|
||||
|
||||
Reference in New Issue
Block a user