"""Matrix messaging: send text, notices, media, edits, redactions, reactions. All functions operate on a mautrix Client and are async. Designed to work with the MatrixClient wrapper as well as standalone usage. """ from __future__ import annotations import logging import os from dataclasses import dataclass from typing import Any, Optional logger = logging.getLogger(__name__) # ── Data classes ──────────────────────────────────────────────────────────── @dataclass class SendResult: """Result of sending a Matrix message.""" success: bool """True if the message was sent.""" event_id: str = "" """The event ID of the sent message.""" error: str = "" """Error message if sending failed.""" # ── Core messaging ───────────────────────────────────────────────────────── async def send_text( client: Any, room_id: str, body: str, html: str = "", thread_root: str = "", reply_to: str = "", ) -> SendResult: """Send a plain-text message to a Matrix room. Args: client: A connected mautrix Client or MatrixClient. room_id: Target room ID. body: Plain-text message body. html: Optional formatted HTML body. thread_root: Optional event ID to thread under. reply_to: Optional event ID to reply to (mutually exclusive with thread_root). Returns: SendResult with success status and event ID. """ try: kwargs: dict[str, Any] = {} if reply_to: kwargs["relates_to"] = {"m.in_reply_to": {"event_id": reply_to}} elif thread_root: kwargs["relates_to"] = { "rel_type": "m.thread", "event_id": thread_root, } event_id = await client.send_text( room_id, text=body, html=html or None, **kwargs, ) return SendResult(success=True, event_id=str(event_id)) except Exception as exc: logger.error("Failed to send text: %s", exc) return SendResult(success=False, error=str(exc)) async def send_notice( client: Any, room_id: str, body: str, html: str = "", ) -> SendResult: """Send a notice message (m.notice — less intrusive than m.text). Args: client: A connected mautrix Client or MatrixClient. room_id: Target room ID. body: Notice body text. html: Optional formatted HTML body. Returns: SendResult with success status and event ID. """ try: from mautrix.types import MessageType event_id = await client.send_message( room_id, { "msgtype": MessageType.NOTICE.value, "body": body, **({"format": "org.matrix.custom.html", "formatted_body": html} if html else {}), }, ) return SendResult(success=True, event_id=str(event_id)) except Exception as exc: logger.error("Failed to send notice: %s", exc) return SendResult(success=False, error=str(exc)) async def send_emote( client: Any, room_id: str, body: str, ) -> SendResult: """Send an emote message (m.emote — /me style). Args: client: A connected mautrix Client or MatrixClient. room_id: Target room ID. body: Emote body text. Returns: SendResult with success status and event ID. """ try: event_id = await client.send_emote(room_id, body) return SendResult(success=True, event_id=str(event_id)) except Exception as exc: logger.error("Failed to send emote: %s", exc) return SendResult(success=False, error=str(exc)) async def send_image( client: Any, room_id: str, image_path: str, caption: str = "", thread_root: str = "", ) -> SendResult: """Send an image file to a Matrix room. Args: client: A connected mautrix Client or MatrixClient. room_id: Target room ID. image_path: Local filesystem path to the image. caption: Optional caption text. thread_root: Optional event ID to thread under. Returns: SendResult with success status and event ID. """ try: event_id = await client.send_image( room_id, image_path, caption=caption or None, thread_root=thread_root or None, ) return SendResult(success=True, event_id=str(event_id)) except Exception as exc: logger.error("Failed to send image: %s", exc) return SendResult(success=False, error=str(exc)) async def send_file( client: Any, room_id: str, file_path: str, filename: str = "", caption: str = "", thread_root: str = "", ) -> SendResult: """Send a generic file to a Matrix room. Args: client: A connected mautrix Client or MatrixClient. room_id: Target room ID. file_path: Local filesystem path to the file. filename: Override filename (default: basename of file_path). caption: Optional caption text. thread_root: Optional event ID to thread under. Returns: SendResult with success status and event ID. """ try: event_id = await client.send_file( room_id, file_path, filename=filename or os.path.basename(file_path), caption=caption or None, thread_root=thread_root or None, ) return SendResult(success=True, event_id=str(event_id)) except Exception as exc: logger.error("Failed to send file: %s", exc) return SendResult(success=False, error=str(exc)) async def send_sticker( client: Any, room_id: str, image_path: str, ) -> SendResult: """Send a sticker to a Matrix room. Args: client: A connected mautrix Client or MatrixClient. room_id: Target room ID. image_path: Local path to the sticker image. Returns: SendResult with success status and event ID. """ try: event_id = await client.send_sticker(room_id, image_path) return SendResult(success=True, event_id=str(event_id)) except Exception as exc: logger.error("Failed to send sticker: %s", exc) return SendResult(success=False, error=str(exc)) # ── Message editing & redaction ───────────────────────────────────────────── async def edit_message( client: Any, room_id: str, event_id: str, new_body: str, new_html: str = "", ) -> SendResult: """Edit a previously sent message. Sends an m.replace event that updates the original message body. Args: client: A connected mautrix Client or MatrixClient. room_id: Room ID of the message. event_id: Event ID of the message to edit. new_body: Replacement plain-text body (use "* text" for clear edits). new_html: Optional replacement formatted body. Returns: SendResult with the edit event's ID. """ try: from mautrix.types import ( ContentURI, EventType, RelatesTo, TextMessageEventContent, ) content: dict[str, Any] = { "msgtype": "m.text", "body": f" * {new_body}", "m.new_content": { "msgtype": "m.text", "body": new_body, }, "m.relates_to": { "rel_type": "m.replace", "event_id": event_id, }, } if new_html: content["m.new_content"]["format"] = "org.matrix.custom.html" content["m.new_content"]["formatted_body"] = new_html edit_event_id = await client.send_message_event( room_id, EventType.ROOM_MESSAGE, content, ) return SendResult(success=True, event_id=str(edit_event_id)) except Exception as exc: logger.error("Failed to edit message: %s", exc) return SendResult(success=False, error=str(exc)) async def redact_message( client: Any, room_id: str, event_id: str, reason: str = "", ) -> bool: """Redact (delete) a message from a Matrix room. Args: client: A connected mautrix Client or MatrixClient. room_id: Room ID of the message. event_id: Event ID to redact. reason: Optional reason for the redaction. Returns: True if redacted successfully. """ try: await client.redact(room_id, event_id, reason=reason or None) logger.info("Redacted event %s in %s", event_id, room_id) return True except Exception as exc: logger.error("Failed to redact event %s: %s", event_id, exc) return False # ── Reactions ─────────────────────────────────────────────────────────────── async def send_reaction( client: Any, room_id: str, event_id: str, key: str, ) -> bool: """Send a reaction to a message. Standard reactions: 👍, 👎, ❤️, 🎉, 😂, 😢, 😡, etc. Args: client: A connected mautrix Client or MatrixClient. room_id: Room ID. event_id: Event ID to react to. key: Reaction key string (emoji). Returns: True if reaction was sent successfully. """ try: await client.react(room_id, event_id, key) logger.info("Reacted to %s with %s", event_id, key) return True except Exception as exc: logger.error("Failed to send reaction: %s", exc) return False # ── Typing & read receipts ────────────────────────────────────────────────── async def send_typing( client: Any, room_id: str, typing: bool = True, timeout: int = 5000, ) -> bool: """Send a typing notification to a room. Args: client: A connected mautrix Client or MatrixClient. room_id: Room ID. typing: True to indicate typing, False to stop. timeout: Typing timeout in milliseconds (default: 5000ms). Returns: True if the notification was sent. """ try: await client.set_typing(room_id, typing=typing, timeout=timeout) return True except Exception as exc: logger.error("Failed to send typing notification: %s", exc) return False async def send_read_receipt( client: Any, room_id: str, event_id: str, ) -> bool: """Send a read receipt for a room event. Args: client: A connected mautrix Client or MatrixClient. room_id: Room ID. event_id: Event ID to mark as read. Returns: True if the receipt was sent. """ try: await client.send_receipt(room_id, event_id, "m.read") return True except Exception as exc: logger.error("Failed to send read receipt: %s", exc) return False async def set_fully_read( client: Any, room_id: str, event_id: str, ) -> bool: """Set the fully-read marker in a room. Args: client: A connected mautrix Client or MatrixClient. room_id: Room ID. event_id: Event ID to mark as fully read. Returns: True if set successfully. """ try: await client.set_fully_read_marker(room_id, event_id) return True except Exception as exc: logger.error("Failed to set fully-read marker: %s", exc) return False