feat(matrix): reply in threads with fallback relations
Propagate Matrix thread metadata from inbound events and attach m.relates_to (rel_type=m.thread, m.in_reply_to, is_falling_back=true) to outbound messages including attachments. Add tests for thread metadata and thread replies.
This commit is contained in:
committed by
Alexander Minges
parent
6a40665753
commit
705d5738e3
@@ -480,8 +480,20 @@ class MatrixChannel(BaseChannel):
|
||||
return 0
|
||||
return min(local_limit, server_limit)
|
||||
|
||||
def _configured_media_limit_bytes(self) -> int:
|
||||
"""Resolve the configured local media limit with backward compatibility."""
|
||||
for name in ("max_inbound_media_bytes", "max_media_bytes"):
|
||||
value = getattr(self.config, name, None)
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
return 0
|
||||
|
||||
async def _upload_and_send_attachment(
|
||||
self, room_id: str, path: Path, limit_bytes: int
|
||||
self,
|
||||
room_id: str,
|
||||
path: Path,
|
||||
limit_bytes: int,
|
||||
relates_to: dict[str, Any] | None = None,
|
||||
) -> str | None:
|
||||
"""Upload one local file to Matrix and send it as a media message."""
|
||||
if not self.client:
|
||||
@@ -578,6 +590,8 @@ class MatrixChannel(BaseChannel):
|
||||
mxc_url=mxc_url,
|
||||
encryption_info=encryption_info,
|
||||
)
|
||||
if relates_to:
|
||||
content["m.relates_to"] = relates_to
|
||||
try:
|
||||
await self._send_room_content(room_id, content)
|
||||
except Exception as e:
|
||||
@@ -596,6 +610,7 @@ class MatrixChannel(BaseChannel):
|
||||
|
||||
text = msg.content or ""
|
||||
candidates = self._collect_outbound_media_candidates(msg.media)
|
||||
relates_to = self._build_thread_relates_to(msg.metadata)
|
||||
|
||||
try:
|
||||
failures: list[str] = []
|
||||
@@ -607,6 +622,7 @@ class MatrixChannel(BaseChannel):
|
||||
room_id=msg.chat_id,
|
||||
path=path,
|
||||
limit_bytes=limit_bytes,
|
||||
relates_to=relates_to,
|
||||
)
|
||||
if failure_marker:
|
||||
failures.append(failure_marker)
|
||||
@@ -618,7 +634,10 @@ class MatrixChannel(BaseChannel):
|
||||
text = "\n".join(failures)
|
||||
|
||||
if text or not candidates:
|
||||
await self._send_room_content(msg.chat_id, _build_matrix_text_content(text))
|
||||
content = _build_matrix_text_content(text)
|
||||
if relates_to:
|
||||
content["m.relates_to"] = relates_to
|
||||
await self._send_room_content(msg.chat_id, content)
|
||||
finally:
|
||||
await self._stop_typing_keepalive(msg.chat_id, clear_typing=True)
|
||||
|
||||
@@ -793,7 +812,7 @@ class MatrixChannel(BaseChannel):
|
||||
content = source.get("content")
|
||||
return content if isinstance(content, dict) else {}
|
||||
|
||||
def _event_thread_root_id(self, event: RoomMessage) -> str | None:
|
||||
def _event_thread_root_id(self, event: Any) -> str | None:
|
||||
"""Return thread root event_id if this message is inside a thread."""
|
||||
content = self._event_source_content(event)
|
||||
relates_to = content.get("m.relates_to")
|
||||
@@ -804,7 +823,7 @@ class MatrixChannel(BaseChannel):
|
||||
root_id = relates_to.get("event_id")
|
||||
return root_id if isinstance(root_id, str) and root_id else None
|
||||
|
||||
def _thread_metadata(self, event: RoomMessage) -> dict[str, str] | None:
|
||||
def _thread_metadata(self, event: Any) -> dict[str, str] | None:
|
||||
"""Build metadata used to reply within a thread."""
|
||||
root_id = self._event_thread_root_id(event)
|
||||
if not root_id:
|
||||
@@ -833,7 +852,7 @@ class MatrixChannel(BaseChannel):
|
||||
"is_falling_back": True,
|
||||
}
|
||||
|
||||
def _event_attachment_type(self, event: MatrixMediaEvent) -> str:
|
||||
def _event_attachment_type(self, event: Any) -> str:
|
||||
"""Map Matrix event payload/type to a stable attachment kind."""
|
||||
msgtype = self._event_source_content(event).get("msgtype")
|
||||
if msgtype == "m.image":
|
||||
@@ -1073,11 +1092,20 @@ class MatrixChannel(BaseChannel):
|
||||
|
||||
await self._start_typing_keepalive(room.room_id)
|
||||
try:
|
||||
metadata: dict[str, Any] = {
|
||||
"room": getattr(room, "display_name", room.room_id),
|
||||
}
|
||||
event_id = getattr(event, "event_id", None)
|
||||
if isinstance(event_id, str) and event_id:
|
||||
metadata["event_id"] = event_id
|
||||
thread_meta = self._thread_metadata(event)
|
||||
if thread_meta:
|
||||
metadata.update(thread_meta)
|
||||
await self._handle_message(
|
||||
sender_id=event.sender,
|
||||
chat_id=room.room_id,
|
||||
content=event.body,
|
||||
metadata={"room": getattr(room, "display_name", room.room_id)},
|
||||
metadata=metadata,
|
||||
)
|
||||
except Exception:
|
||||
await self._stop_typing_keepalive(room.room_id, clear_typing=True)
|
||||
@@ -1107,15 +1135,22 @@ class MatrixChannel(BaseChannel):
|
||||
|
||||
await self._start_typing_keepalive(room.room_id)
|
||||
try:
|
||||
metadata: dict[str, Any] = {
|
||||
"room": getattr(room, "display_name", room.room_id),
|
||||
"attachments": attachments,
|
||||
}
|
||||
event_id = getattr(event, "event_id", None)
|
||||
if isinstance(event_id, str) and event_id:
|
||||
metadata["event_id"] = event_id
|
||||
thread_meta = self._thread_metadata(event)
|
||||
if thread_meta:
|
||||
metadata.update(thread_meta)
|
||||
await self._handle_message(
|
||||
sender_id=event.sender,
|
||||
chat_id=room.room_id,
|
||||
content="\n".join(content_parts),
|
||||
media=media_paths,
|
||||
metadata={
|
||||
"room": getattr(room, "display_name", room.room_id),
|
||||
"attachments": attachments,
|
||||
},
|
||||
metadata=metadata,
|
||||
)
|
||||
except Exception:
|
||||
await self._stop_typing_keepalive(room.room_id, clear_typing=True)
|
||||
|
||||
Reference in New Issue
Block a user