From 83c9a1513b1b5dc04fd060bd8aa0a385e6516aba Mon Sep 17 00:00:00 2001 From: Mikhail Putilovskij Date: Mon, 20 Apr 2026 16:26:37 +0300 Subject: [PATCH] feat: parse matrix staged attachment commands --- adapter/matrix/converter.py | 48 ++++++++++++++---- tests/adapter/matrix/test_converter.py | 69 +++++++++++++++++++++++--- 2 files changed, 100 insertions(+), 17 deletions(-) diff --git a/adapter/matrix/converter.py b/adapter/matrix/converter.py index 00fcdc4..f8edd78 100644 --- a/adapter/matrix/converter.py +++ b/adapter/matrix/converter.py @@ -14,42 +14,52 @@ PLATFORM = "matrix" def extract_attachments(event: Any) -> list[Attachment]: + content = getattr(event, "content", {}) or {} msgtype = getattr(event, "msgtype", None) if msgtype is None: - content = getattr(event, "content", {}) or {} msgtype = content.get("msgtype") + url = content.get("url") or getattr(event, "url", None) + filename = content.get("body") or getattr(event, "body", None) + mime_type = content.get("mimetype") or getattr(event, "mimetype", None) + if mime_type is None: + info = content.get("info") or {} + if isinstance(info, dict): + mime_type = info.get("mimetype") if msgtype == "m.image": return [ Attachment( type="image", - url=getattr(event, "url", None), - mime_type=getattr(event, "mimetype", None), + url=url, + filename=filename, + mime_type=mime_type, ) ] if msgtype == "m.file": return [ Attachment( type="document", - url=getattr(event, "url", None), - filename=getattr(event, "body", None), - mime_type=getattr(event, "mimetype", None), + url=url, + filename=filename, + mime_type=mime_type, ) ] if msgtype == "m.audio": return [ Attachment( type="audio", - url=getattr(event, "url", None), - mime_type=getattr(event, "mimetype", None), + url=url, + filename=filename, + mime_type=mime_type, ) ] if msgtype == "m.video": return [ Attachment( type="video", - url=getattr(event, "url", None), - mime_type=getattr(event, "mimetype", None), + url=url, + filename=filename, + mime_type=mime_type, ) ] return [] @@ -75,6 +85,24 @@ def from_command(body: str, sender: str, chat_id: str, room_id: str | None = Non }, ) + if command == "list" and not args: + return IncomingCommand( + user_id=sender, + platform=PLATFORM, + chat_id=chat_id, + command="matrix_list_attachments", + args=[], + ) + + if command == "remove" and len(args) == 1: + return IncomingCommand( + user_id=sender, + platform=PLATFORM, + chat_id=chat_id, + command="matrix_remove_attachment", + args=[args[0]], + ) + aliases = { "skills": "settings_skills", "connectors": "settings_connectors", diff --git a/tests/adapter/matrix/test_converter.py b/tests/adapter/matrix/test_converter.py index ecaecdc..a6b75fb 100644 --- a/tests/adapter/matrix/test_converter.py +++ b/tests/adapter/matrix/test_converter.py @@ -37,7 +37,23 @@ def image_event(url: str = "mxc://x/img", mime: str = "image/jpeg"): ) -async def test_plain_text_to_incoming_message(): +def content_file_event(): + return SimpleNamespace( + sender="@a:m.org", + body="doc.pdf", + event_id="$e4", + msgtype=None, + replyto_event_id=None, + content={ + "msgtype": "m.file", + "body": "nested.pdf", + "url": "mxc://x/nested", + "info": {"mimetype": "application/pdf"}, + }, + ) + + +def test_plain_text_to_incoming_message(): result = from_room_event(text_event("Hello"), room_id="!r:m.org", chat_id="C1") assert isinstance(result, IncomingMessage) assert result.text == "Hello" @@ -46,20 +62,48 @@ async def test_plain_text_to_incoming_message(): assert result.attachments == [] -async def test_bang_command_to_incoming_command(): +def test_bang_command_to_incoming_command(): result = from_room_event(text_event("!new Analysis"), room_id="!r:m.org", chat_id="C1") assert isinstance(result, IncomingCommand) assert result.command == "new" assert result.args == ["Analysis"] -async def test_skills_alias_to_settings_command(): +def test_list_command_maps_to_matrix_list_attachments(): + result = from_room_event(text_event("!list"), room_id="!r:m.org", chat_id="C1") + assert isinstance(result, IncomingCommand) + assert result.command == "matrix_list_attachments" + assert result.args == [] + + +def test_remove_all_maps_to_matrix_remove_attachment(): + result = from_room_event(text_event("!remove all"), room_id="!r:m.org", chat_id="C1") + assert isinstance(result, IncomingCommand) + assert result.command == "matrix_remove_attachment" + assert result.args == ["all"] + + +def test_remove_index_maps_to_matrix_remove_attachment(): + result = from_room_event(text_event("!remove 2"), room_id="!r:m.org", chat_id="C1") + assert isinstance(result, IncomingCommand) + assert result.command == "matrix_remove_attachment" + assert result.args == ["2"] + + +def test_remove_arbitrary_index_maps_to_matrix_remove_attachment(): + result = from_room_event(text_event("!remove 99"), room_id="!r:m.org", chat_id="C1") + assert isinstance(result, IncomingCommand) + assert result.command == "matrix_remove_attachment" + assert result.args == ["99"] + + +def test_skills_alias_to_settings_command(): result = from_command("!skills", sender="@a:m.org", chat_id="C1") assert isinstance(result, IncomingCommand) assert result.command == "settings_skills" -async def test_yes_to_callback(): +def test_yes_to_callback(): result = from_room_event(text_event("!yes"), room_id="!room:example.org", chat_id="C7") assert isinstance(result, IncomingCallback) assert result.action == "confirm" @@ -67,7 +111,7 @@ async def test_yes_to_callback(): assert result.payload["room_id"] == "!room:example.org" -async def test_no_to_callback(): +def test_no_to_callback(): result = from_room_event(text_event("!no"), room_id="!room:example.org", chat_id="C7") assert isinstance(result, IncomingCallback) assert result.action == "cancel" @@ -75,7 +119,7 @@ async def test_no_to_callback(): assert result.payload["room_id"] == "!room:example.org" -async def test_file_attachment(): +def test_file_attachment(): result = from_room_event(file_event(), room_id="!r:m.org", chat_id="C1") assert isinstance(result, IncomingMessage) assert len(result.attachments) == 1 @@ -86,11 +130,22 @@ async def test_file_attachment(): assert a.mime_type == "application/pdf" -async def test_image_attachment(): +def test_image_attachment(): result = from_room_event(image_event(), room_id="!r:m.org", chat_id="C1") assert result.attachments[0].type == "image" + assert result.attachments[0].filename == "img.jpg" assert result.attachments[0].mime_type == "image/jpeg" +def test_attachment_falls_back_to_content_payload(): + result = from_room_event(content_file_event(), room_id="!r:m.org", chat_id="C1") + assert isinstance(result, IncomingMessage) + a = result.attachments[0] + assert a.type == "document" + assert a.url == "mxc://x/nested" + assert a.filename == "nested.pdf" + assert a.mime_type == "application/pdf" + + def test_converter_module_does_not_expose_reaction_callbacks(): assert not hasattr(converter, "from_reaction")