from __future__ import annotations from typing import TYPE_CHECKING from yuxi.channels.models import ChannelResponse, DeliveryResult from yuxi.utils.logging_config import logger from .formatter import markdown_to_matrix_html from .location import build_outbound_location from .sticker import build_outbound_sticker if TYPE_CHECKING: from nio import AsyncClient async def send_formatted_text( client: AsyncClient, room_id: str, response: ChannelResponse, reply_to_mode: str = "all", ) -> DeliveryResult: content: dict = { "msgtype": "m.text", "body": response.content, } if response.content: html_body = markdown_to_matrix_html(response.content) content["format"] = "org.matrix.custom.html" content["formatted_body"] = html_body if response.reply_to_message_id: if reply_to_mode != "off": content["m.relates_to"] = { "m.in_reply_to": {"event_id": response.reply_to_message_id}, } try: resp = await client.room_send( room_id=room_id, message_type="m.room.message", content=content, ) return DeliveryResult(success=True, message_id=resp.event_id) except Exception as e: return DeliveryResult(success=False, error=str(e)) async def send_formatted_media( client: AsyncClient, room_id: str, media_type: str, media_data: bytes, mime_type: str, filename: str = "", caption: str = "", reply_to: str | None = None, ) -> DeliveryResult: try: upload_resp, _ = await client.upload( media_data, content_type=mime_type, filename=filename or "file", ) except Exception as e: return DeliveryResult(success=False, error=f"Media upload failed: {e}") mxc_url = upload_resp.content_uri logger.debug(f"Matrix media uploaded: {mxc_url}") msgtype_map = { "image": "m.image", "video": "m.video", "audio": "m.audio", "file": "m.file", } msgtype = msgtype_map.get(media_type, "m.file") content: dict = { "msgtype": msgtype, "body": filename or caption or "media", "url": mxc_url, "info": {"mimetype": mime_type}, } if caption: content["body"] = caption if reply_to: content["m.relates_to"] = { "m.in_reply_to": {"event_id": reply_to}, } try: resp = await client.room_send( room_id=room_id, message_type="m.room.message", content=content, ) return DeliveryResult(success=True, message_id=resp.event_id) except Exception as e: return DeliveryResult(success=False, error=str(e)) async def send_reaction( client: AsyncClient, room_id: str, target_event_id: str, reaction: str, ) -> DeliveryResult: content = { "m.relates_to": { "rel_type": "m.annotation", "event_id": target_event_id, "key": reaction, }, } try: resp = await client.room_send( room_id=room_id, message_type="m.reaction", content=content, ) return DeliveryResult(success=True, message_id=resp.event_id) except Exception as e: return DeliveryResult(success=False, error=str(e)) async def send_thread_message( client: AsyncClient, room_id: str, root_event_id: str, text: str, ) -> DeliveryResult: html_body = markdown_to_matrix_html(text) msg_content = { "msgtype": "m.text", "body": text, "format": "org.matrix.custom.html", "formatted_body": html_body, "m.relates_to": { "rel_type": "m.thread", "event_id": root_event_id, "is_falling_back": False, }, } try: resp = await client.room_send( room_id=room_id, message_type="m.room.message", content=msg_content, ) return DeliveryResult(success=True, message_id=resp.event_id) except Exception as e: return DeliveryResult(success=False, error=str(e)) async def send_location( client: AsyncClient, room_id: str, lat: float, lon: float, body: str = "", description: str = "", ) -> DeliveryResult: content = build_outbound_location(body, lat, lon, description) try: resp = await client.room_send( room_id=room_id, message_type="m.room.message", content=content, ) return DeliveryResult(success=True, message_id=resp.event_id) except Exception as e: return DeliveryResult(success=False, error=str(e)) async def send_sticker( client: AsyncClient, room_id: str, sticker_url: str, body: str = "", mimetype: str = "image/png", ) -> DeliveryResult: content = build_outbound_sticker(sticker_url, body, mimetype) try: resp = await client.room_send( room_id=room_id, message_type="m.sticker", content=content, ) return DeliveryResult(success=True, message_id=resp.event_id) except Exception as e: return DeliveryResult(success=False, error=str(e)) _STREAM_SENT_LENGTHS: dict[str, int] = {} _AUTH_ERROR_CODES = frozenset({"M_UNKNOWN_TOKEN", "M_FORBIDDEN", "M_MISSING_TOKEN"}) _RATE_LIMIT_STATUS = 429 def classify_send_error(exc: Exception) -> tuple[str, int | None]: """返回 (error_type, retry_after_ms_or_none) error_type: 'auth' | 'rate_limit' | 'other' """ status_code = getattr(exc, "status_code", None) errcode = getattr(exc, "errcode", None) or getattr(exc, "message", "") if errcode in _AUTH_ERROR_CODES: return "auth", None if status_code == _RATE_LIMIT_STATUS: retry_after = getattr(exc, "retry_after_ms", None) if retry_after is None: retry_after = 5000 return "rate_limit", retry_after return "other", None async def send_stream_chunk( client: AsyncClient, room_id: str, message_id: str, chunk_text: str, finished: bool = False, stream_state: dict[str, int] | None = None, ) -> DeliveryResult: html_body = markdown_to_matrix_html(chunk_text) stream_key = f"{room_id}:{message_id}" lengths = stream_state if stream_state is not None else _STREAM_SENT_LENGTHS prev_length = lengths.get(stream_key, 0) if prev_length > 0 and prev_length < len(chunk_text): delta_text = chunk_text[prev_length:] delta_html = markdown_to_matrix_html(delta_text) else: delta_text = chunk_text delta_html = html_body content = { "msgtype": "m.text", "body": delta_text, "format": "org.matrix.custom.html", "formatted_body": delta_html, "m.new_content": { "msgtype": "m.text", "body": chunk_text, "format": "org.matrix.custom.html", "formatted_body": html_body, }, "m.relates_to": { "rel_type": "m.replace", "event_id": message_id, }, } if finished: lengths.pop(stream_key, None) else: lengths[stream_key] = len(chunk_text) try: resp = await client.room_send( room_id=room_id, message_type="m.room.message", content=content, ) return DeliveryResult(success=True, message_id=resp.event_id) except Exception as e: return DeliveryResult(success=False, error=str(e))