diff --git a/tests/tools/test_send_message_tool.py b/tests/tools/test_send_message_tool.py index b9d99e2b3d..9a277d5980 100644 --- a/tests/tools/test_send_message_tool.py +++ b/tests/tools/test_send_message_tool.py @@ -886,3 +886,39 @@ class TestSendToPlatformDiscordThread: send_mock.assert_awaited_once() _, call_kwargs = send_mock.await_args assert call_kwargs["thread_id"] is None + + +class TestSendMatrixUrlEncoding: + """_send_matrix URL-encodes Matrix room IDs in the API path.""" + + def test_room_id_is_percent_encoded_in_url(self): + """Matrix room IDs with ! and : are percent-encoded in the PUT URL.""" + import aiohttp + + mock_resp = MagicMock() + mock_resp.status = 200 + mock_resp.json = AsyncMock(return_value={"event_id": "$evt123"}) + mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) + mock_resp.__aexit__ = AsyncMock(return_value=None) + + mock_session = MagicMock() + mock_session.put = MagicMock(return_value=mock_resp) + mock_session.__aenter__ = AsyncMock(return_value=mock_session) + mock_session.__aexit__ = AsyncMock(return_value=None) + + with patch("aiohttp.ClientSession", return_value=mock_session): + from tools.send_message_tool import _send_matrix + result = asyncio.get_event_loop().run_until_complete( + _send_matrix( + "test_token", + {"homeserver": "https://matrix.example.org"}, + "!HLOQwxYGgFPMPJUSNR:matrix.org", + "hello", + ) + ) + + assert result["success"] is True + # Verify the URL was called with percent-encoded room ID + put_url = mock_session.put.call_args[0][0] + assert "%21HLOQwxYGgFPMPJUSNR%3Amatrix.org" in put_url + assert "!HLOQwxYGgFPMPJUSNR:matrix.org" not in put_url diff --git a/tools/send_message_tool.py b/tools/send_message_tool.py index f99bcdaf46..6c9632b2c3 100644 --- a/tools/send_message_tool.py +++ b/tools/send_message_tool.py @@ -68,7 +68,7 @@ SEND_MESSAGE_SCHEMA = { }, "target": { "type": "string", - "description": "Delivery target. Format: 'platform' (uses home channel), 'platform:#channel-name', 'platform:chat_id', or 'platform:chat_id:thread_id' for Telegram topics and Discord threads. Examples: 'telegram', 'telegram:-1001234567890:17585', 'discord:999888777:555444333', 'discord:#bot-home', 'slack:#engineering', 'signal:+155****4567'" + "description": "Delivery target. Format: 'platform' (uses home channel), 'platform:#channel-name', 'platform:chat_id', or 'platform:chat_id:thread_id' for Telegram topics and Discord threads. Examples: 'telegram', 'telegram:-1001234567890:17585', 'discord:999888777:555444333', 'discord:#bot-home', 'slack:#engineering', 'signal:+155****4567', 'matrix:!roomid:server.org', 'matrix:@user:server.org'" }, "message": { "type": "string", @@ -819,7 +819,9 @@ async def _send_matrix(token, extra, chat_id, message): 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)}_{os.urandom(4).hex()}" - url = f"{homeserver}/_matrix/client/v3/rooms/{chat_id}/send/m.room.message/{txn_id}" + from urllib.parse import quote + encoded_room = quote(chat_id, safe="") + url = f"{homeserver}/_matrix/client/v3/rooms/{encoded_room}/send/m.room.message/{txn_id}" headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} # Build message payload with optional HTML formatted_body.