275 lines
7.4 KiB
Python
275 lines
7.4 KiB
Python
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))
|