ForcePilot/backend/package/yuxi/channel/extensions/matrix/utils.py
Kris 4e9c6dd8ab feat(channel): 添加 Matrix 渠道扩展
新增 Matrix 渠道扩展,支持在 Yuxi 平台中集成 Matrix 去中心化通讯协议。

包含以下功能模块:
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- crypto: 端到端加密
- dedupe: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- session: 会话管理
- room_resolver: 房间解析
- dm_tracker: 私聊追踪
- rate_limiter: 速率限制
- actions: 动作处理
- constants: 常量定义
- utils: 工具函数
- types: 类型定义
2026-05-21 11:18:13 +08:00

106 lines
2.9 KiB
Python

from __future__ import annotations
import logging
import re
from .constants import MXC_URI_REGEX, MXID_REGEX, ROOM_ALIAS_REGEX, ROOM_ID_REGEX
logger = logging.getLogger(__name__)
_NIO_IMPORT_ERROR = "matrix-nio is not installed. Install with: pip install matrix-nio python-olm peewee"
def get_nio():
try:
import nio
return nio
except ImportError:
raise ImportError(_NIO_IMPORT_ERROR)
def parse_mxc_uri(uri: str) -> tuple[str, str] | None:
match = re.match(MXC_URI_REGEX, uri)
if not match:
return None
return match.group(1), match.group(2)
def resolve_mxc_download_url(homeserver: str, mxc_uri: str) -> str:
parsed = parse_mxc_uri(mxc_uri)
if not parsed:
raise ValueError(f"Invalid MXC URI: {mxc_uri}")
server, media_id = parsed
return f"{homeserver.rstrip('/')}/_matrix/media/v3/download/{server}/{media_id}"
def is_matrix_user_id(value: str) -> bool:
return bool(re.match(MXID_REGEX, value))
def is_room_id(value: str) -> bool:
return bool(re.match(ROOM_ID_REGEX, value))
def is_room_alias(value: str) -> bool:
return bool(re.match(ROOM_ALIAS_REGEX, value))
def normalize_user_id(user_id: str) -> str:
return user_id.strip().lower()
def extract_mention_user_ids(body: str) -> list[str]:
return re.findall(r"@[^\s,:!?]+:[\w.\-]+", body)
def extract_room_aliases(text: str) -> list[str]:
return re.findall(r"#[^\s:]+:[^\s]+", text)
def build_dedupe_key(account_id: str, event_id: str) -> str:
return f"matrix:{account_id}:{event_id}"
def strip_html_tags(html: str) -> str:
return re.sub(r"<[^>]+>", "", html)
def sanitize_matrix_text(text: str, limit: int = 4000) -> str:
if len(text) <= limit:
return text
return text[: limit - 3] + "..."
def chunk_text(text: str, limit: int = 4000) -> list[str]:
chunks = []
remaining = text
while len(remaining) > limit:
split_at = remaining.rfind("\n", 0, limit)
if split_at == -1:
split_at = remaining.rfind(" ", 0, limit)
if split_at == -1:
split_at = limit
chunks.append(remaining[:split_at])
remaining = remaining[split_at:].lstrip()
if remaining:
chunks.append(remaining)
return chunks
async def download_media(homeserver: str, access_token: str, mxc_uri: str) -> bytes | None:
import aiohttp
url = resolve_mxc_download_url(homeserver, mxc_uri)
headers = {"Authorization": f"Bearer {access_token}"}
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as resp:
if resp.status == 200:
return await resp.read()
logger.warning("Media download failed: HTTP %s for %s", resp.status, mxc_uri)
return None
except Exception:
logger.exception("Media download error for %s", mxc_uri)
return None