新增 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: 类型定义
1022 lines
29 KiB
Python
1022 lines
29 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from enum import StrEnum
|
|
|
|
from .config import _apply_env_overrides, _dict_to_account
|
|
from .errors import classify_error
|
|
from .utils import chunk_text, get_nio, sanitize_matrix_text
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class OutboundDeliveryMode(StrEnum):
|
|
DIRECT = "direct"
|
|
GATEWAY = "gateway"
|
|
HYBRID = "hybrid"
|
|
|
|
|
|
delivery_mode = OutboundDeliveryMode.DIRECT
|
|
chunker_mode = None
|
|
text_chunk_limit = 4000
|
|
|
|
presentation_capabilities = None
|
|
delivery_capabilities = None
|
|
|
|
|
|
def chunker(text: str, limit: int, ctx=None) -> list[str]:
|
|
return chunk_text(text, max(limit, 100))
|
|
|
|
|
|
def sanitize_text(text: str, payload=None) -> str:
|
|
return sanitize_matrix_text(text)
|
|
|
|
|
|
async def send_text(
|
|
target_id: str,
|
|
content: str,
|
|
*,
|
|
reply_to_id: str | None = None,
|
|
thread_id: str | None = None,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
|
|
try:
|
|
body = {
|
|
"msgtype": "m.text",
|
|
"body": content,
|
|
"format": "org.matrix.custom.html",
|
|
"formatted_body": content,
|
|
}
|
|
if reply_to_id:
|
|
body["m.relates_to"] = {
|
|
"m.in_reply_to": {"event_id": reply_to_id},
|
|
}
|
|
if thread_id:
|
|
relates = body.setdefault("m.relates_to", {})
|
|
relates["rel_type"] = "m.thread"
|
|
relates["event_id"] = thread_id
|
|
|
|
resp = await client.room_send(target_id, "m.room.message", body)
|
|
return {"event_id": resp.event_id, "room_id": target_id}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix send_text failed: severity=%s retry_after=%dms error=%s",
|
|
matrix_err.severity,
|
|
matrix_err.retry_after_ms,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def send_media(
|
|
target_id: str,
|
|
media_url: str,
|
|
media_type: str = "file",
|
|
*,
|
|
reply_to_id: str | None = None,
|
|
thread_id: str | None = None,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
|
|
msgtype_map = {
|
|
"image": "m.image",
|
|
"file": "m.file",
|
|
"audio": "m.audio",
|
|
"voice": "m.audio",
|
|
"video": "m.video",
|
|
}
|
|
|
|
content_type_map = {
|
|
"image": "image/png",
|
|
"file": "application/octet-stream",
|
|
"audio": "audio/ogg",
|
|
"voice": "audio/ogg",
|
|
"video": "video/mp4",
|
|
}
|
|
|
|
try:
|
|
mime_type = content_type_map.get(media_type, "application/octet-stream")
|
|
mxc_uri, _ = await client.upload(media_url, content_type=mime_type)
|
|
body = {
|
|
"msgtype": msgtype_map.get(media_type, "m.file"),
|
|
"body": media_url.split("/")[-1] if "/" in media_url else "file",
|
|
"url": mxc_uri,
|
|
}
|
|
if reply_to_id:
|
|
body["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_to_id}}
|
|
if thread_id:
|
|
relates = body.setdefault("m.relates_to", {})
|
|
relates["rel_type"] = "m.thread"
|
|
relates["event_id"] = thread_id
|
|
|
|
resp = await client.room_send(target_id, "m.room.message", body)
|
|
return {"event_id": resp.event_id, "room_id": target_id, "mxc_uri": mxc_uri}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix send_media failed: severity=%s retry_after=%dms error=%s",
|
|
matrix_err.severity,
|
|
matrix_err.retry_after_ms,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def edit_message(
|
|
target_id: str,
|
|
message_id: str,
|
|
content: str,
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
|
|
try:
|
|
body = {
|
|
"msgtype": "m.text",
|
|
"body": content,
|
|
"m.new_content": {
|
|
"msgtype": "m.text",
|
|
"body": content,
|
|
},
|
|
"m.relates_to": {
|
|
"rel_type": "m.replace",
|
|
"event_id": message_id,
|
|
},
|
|
}
|
|
resp = await client.room_send(target_id, "m.room.message", body)
|
|
return {"event_id": resp.event_id, "room_id": target_id}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix edit_message failed: severity=%s retry_after=%dms error=%s",
|
|
matrix_err.severity,
|
|
matrix_err.retry_after_ms,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def unsend_message(
|
|
target_id: str,
|
|
message_id: str,
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
|
|
try:
|
|
resp = await client.room_redact(target_id, message_id)
|
|
return {"event_id": resp.event_id, "room_id": target_id}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix unsend_message failed: severity=%s retry_after=%dms error=%s",
|
|
matrix_err.severity,
|
|
matrix_err.retry_after_ms,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def send_reaction(
|
|
target_id: str,
|
|
message_id: str,
|
|
emoji: str,
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
|
|
try:
|
|
body = {
|
|
"m.relates_to": {
|
|
"rel_type": "m.annotation",
|
|
"event_id": message_id,
|
|
"key": emoji,
|
|
}
|
|
}
|
|
resp = await client.room_send(target_id, "m.reaction", body)
|
|
return {"event_id": resp.event_id, "room_id": target_id}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix send_reaction failed: severity=%s retry_after=%dms error=%s",
|
|
matrix_err.severity,
|
|
matrix_err.retry_after_ms,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def send_typing(
|
|
target_id: str, typing_state: bool, *, account_id: str | None = None, config: dict | None = None, client: object = None
|
|
) -> None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
await client.room_typing(target_id, typing_state=typing_state)
|
|
except Exception:
|
|
logger.debug("Matrix send_typing failed for room %s", target_id)
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def send_read_receipt(
|
|
target_id: str,
|
|
event_id: str,
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
await client.room_read_markers(target_id, fully_read_event=event_id, read_event=event_id)
|
|
except Exception:
|
|
logger.debug("Matrix send_read_receipt failed for room %s", target_id)
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def send_poll(
|
|
target_id: str,
|
|
question: str,
|
|
options: list[str],
|
|
*,
|
|
is_anonymous: bool = True,
|
|
allows_multiple_answers: bool = False,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
|
|
try:
|
|
kind = "org.matrix.msc3381.poll.disclosed"
|
|
poll_content = {
|
|
"org.matrix.msc3381.poll.start": {
|
|
"kind": kind,
|
|
"max_selections": len(options) if allows_multiple_answers else 1,
|
|
"question": {
|
|
"org.matrix.msc1767.text": question,
|
|
"body": question,
|
|
},
|
|
"answers": [
|
|
{
|
|
"id": f"opt_{i}",
|
|
"org.matrix.msc1767.text": opt,
|
|
}
|
|
for i, opt in enumerate(options)
|
|
],
|
|
},
|
|
"org.matrix.msc1767.text": question,
|
|
}
|
|
resp = await client.room_send(target_id, "m.poll.start", poll_content)
|
|
return {"event_id": resp.event_id, "room_id": target_id}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix send_poll failed: severity=%s retry_after=%dms error=%s",
|
|
matrix_err.severity,
|
|
matrix_err.retry_after_ms,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
poll_max_options = 10
|
|
supports_poll_duration_seconds = False
|
|
supports_anonymous_polls = True
|
|
|
|
|
|
def _resolve_account(config: dict | None, account_id: str | None):
|
|
if config is None:
|
|
config = {}
|
|
aid = account_id or "default"
|
|
account_data = config.get("accounts", {}).get(aid, {})
|
|
account = _dict_to_account(account_data)
|
|
return _apply_env_overrides(account)
|
|
|
|
|
|
def _create_client(account, nio):
|
|
client = nio.AsyncClient(
|
|
homeserver=account.homeserver,
|
|
user=account.user_id,
|
|
device_id=account.device_id,
|
|
)
|
|
client.access_token = account.access_token
|
|
return client
|
|
|
|
|
|
async def _with_client(
|
|
config: dict | None,
|
|
account_id: str | None,
|
|
client: object | None,
|
|
operation_name: str,
|
|
operation_fn,
|
|
):
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
return await operation_fn(nio, client)
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix %s failed: severity=%s retry_after=%dms error=%s",
|
|
operation_name,
|
|
matrix_err.severity,
|
|
matrix_err.retry_after_ms,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def send_notice(
|
|
target_id: str,
|
|
content: str,
|
|
*,
|
|
reply_to_id: str | None = None,
|
|
thread_id: str | None = None,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
async def _op(nio, client):
|
|
body = {"msgtype": "m.notice", "body": content}
|
|
if reply_to_id:
|
|
body["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_to_id}}
|
|
if thread_id:
|
|
relates = body.setdefault("m.relates_to", {})
|
|
relates["rel_type"] = "m.thread"
|
|
relates["event_id"] = thread_id
|
|
resp = await client.room_send(target_id, "m.room.message", body)
|
|
return {"event_id": resp.event_id, "room_id": target_id}
|
|
|
|
return await _with_client(config, account_id, client, "send_notice", _op)
|
|
|
|
|
|
async def create_room(
|
|
name: str | None = None,
|
|
topic: str | None = None,
|
|
invite_users: list[str] | None = None,
|
|
*,
|
|
is_direct: bool = False,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
async def _op(nio, client):
|
|
resp = await client.room_create(
|
|
name=name, topic=topic, invite=invite_users or [], is_direct=is_direct
|
|
)
|
|
return {"room_id": resp.room_id}
|
|
|
|
return await _with_client(config, account_id, client, "create_room", _op)
|
|
|
|
|
|
async def join_room(
|
|
room_id_or_alias: str,
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
async def _op(nio, client):
|
|
resp = await client.join(room_id_or_alias)
|
|
return {"room_id": resp.room_id}
|
|
|
|
return await _with_client(config, account_id, client, "join_room", _op)
|
|
|
|
|
|
async def leave_room(
|
|
room_id: str,
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
await client.room_leave(room_id)
|
|
return {"ok": True}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix leave_room failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def invite_user(
|
|
room_id: str,
|
|
user_id: str,
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
await client.room_invite(room_id, user_id)
|
|
return {"ok": True}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix invite_user failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def kick_user(
|
|
room_id: str,
|
|
user_id: str,
|
|
reason: str = "",
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
await client.room_kick(room_id, user_id, reason=reason)
|
|
return {"ok": True}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix kick_user failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def ban_user(
|
|
room_id: str,
|
|
user_id: str,
|
|
reason: str = "",
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
await client.room_ban(room_id, user_id, reason=reason)
|
|
return {"ok": True}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix ban_user failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def set_display_name(
|
|
display_name: str,
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
await client.set_displayname(display_name)
|
|
return {"ok": True}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix set_display_name failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def set_avatar(
|
|
avatar_url: str,
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
await client.set_avatar(avatar_url)
|
|
return {"ok": True}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix set_avatar failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def send_emote(
|
|
target_id: str,
|
|
content: str,
|
|
*,
|
|
reply_to_id: str | None = None,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
body = {"msgtype": "m.emote", "body": content}
|
|
if reply_to_id:
|
|
body["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_to_id}}
|
|
resp = await client.room_send(target_id, "m.room.message", body)
|
|
return {"event_id": resp.event_id, "room_id": target_id}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix send_emote failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def set_room_name(
|
|
room_id: str,
|
|
name: str,
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
await client.room_put_state(room_id, "m.room.name", {"name": name})
|
|
return {"ok": True}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix set_room_name failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def set_room_topic(
|
|
room_id: str,
|
|
topic: str,
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
await client.room_put_state(room_id, "m.room.topic", {"topic": topic})
|
|
return {"ok": True}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix set_room_topic failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def get_room_members(
|
|
room_id: str,
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> list[dict] | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
resp = await client.joined_members(room_id)
|
|
members_data = resp.members if hasattr(resp, "members") else {}
|
|
return [
|
|
{"user_id": uid, "display_name": info.display_name, "avatar_url": info.avatar_url}
|
|
for uid, info in (members_data.items() if isinstance(members_data, dict) else [])
|
|
]
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix get_room_members failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return None
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def update_dm_mapping(
|
|
user_id: str,
|
|
room_id: str,
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
resp = await client.get_account_data("m.direct")
|
|
dm_map = resp.content if hasattr(resp, "content") else {}
|
|
dm_map = dict(dm_map) if isinstance(dm_map, dict) else {}
|
|
existing = dm_map.get(user_id, [])
|
|
if room_id not in existing:
|
|
dm_map[user_id] = existing + [room_id]
|
|
await client.set_account_data("m.direct", dm_map)
|
|
return {"ok": True}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix update_dm_mapping failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def send_location(
|
|
location_url: str,
|
|
body: str = "",
|
|
geo_uri: str = "",
|
|
*,
|
|
target_id: str = "",
|
|
reply_to_id: str | None = None,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
location_content = {"body": body or "Location", "geo_uri": geo_uri, "msgtype": "m.location"}
|
|
if reply_to_id:
|
|
location_content["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_to_id}}
|
|
resp = await client.room_send(target_id, "m.room.message", location_content)
|
|
return {"event_id": resp.event_id, "room_id": target_id}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix send_location failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def send_sticker(
|
|
target_id: str,
|
|
sticker_url: str,
|
|
*,
|
|
body: str = "Sticker",
|
|
reply_to_id: str | None = None,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
sticker_content = {"body": body, "url": sticker_url, "msgtype": "m.sticker"}
|
|
if reply_to_id:
|
|
sticker_content["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_to_id}}
|
|
resp = await client.room_send(target_id, "m.sticker", sticker_content)
|
|
return {"event_id": resp.event_id, "room_id": target_id}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix send_sticker failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def send_voice(
|
|
target_id: str,
|
|
audio_url: str,
|
|
*,
|
|
body: str = "Voice message",
|
|
reply_to_id: str | None = None,
|
|
duration_ms: int = 0,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
voice_content = {
|
|
"body": body,
|
|
"msgtype": "m.voice",
|
|
"url": audio_url,
|
|
"org.matrix.msc3245.voice": {},
|
|
}
|
|
if duration_ms > 0:
|
|
voice_content["org.matrix.msc3245.voice"]["duration"] = duration_ms
|
|
if reply_to_id:
|
|
voice_content["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_to_id}}
|
|
resp = await client.room_send(target_id, "m.room.message", voice_content)
|
|
return {"event_id": resp.event_id, "room_id": target_id}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix send_voice failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def set_room_avatar(
|
|
room_id: str,
|
|
avatar_url: str,
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
await client.room_put_state(room_id, "m.room.avatar", {"url": avatar_url})
|
|
return {"ok": True}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix set_room_avatar failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def get_user_profile(
|
|
user_id: str,
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
resp = await client.get_profile(user_id)
|
|
return {
|
|
"user_id": user_id,
|
|
"displayname": getattr(resp, "displayname", None),
|
|
"avatar_url": getattr(resp, "avatar_url", None),
|
|
}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix get_user_profile failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return None
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def get_room_tags(
|
|
room_id: str,
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
resp = await client.get_account_data(f"m.tag/{room_id}")
|
|
return getattr(resp, "content", {}).get("tags", {}) if hasattr(resp, "content") else {}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
if "M_NOT_FOUND" not in str(e):
|
|
logger.error(
|
|
"Matrix get_room_tags failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return {}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|
|
|
|
|
|
async def set_room_tags(
|
|
room_id: str,
|
|
tags: dict[str, dict | None],
|
|
*,
|
|
account_id: str | None = None,
|
|
config: dict | None = None,
|
|
client: object = None,
|
|
) -> dict | None:
|
|
nio = get_nio()
|
|
account = _resolve_account(config, account_id)
|
|
_should_close = client is None
|
|
if client is None:
|
|
client = _create_client(account, nio)
|
|
try:
|
|
await client.set_account_data(f"m.tag/{room_id}", {"tags": tags})
|
|
return {"ok": True}
|
|
except Exception as e:
|
|
matrix_err = classify_error(e)
|
|
logger.error(
|
|
"Matrix set_room_tags failed: severity=%s error=%s",
|
|
matrix_err.severity,
|
|
e,
|
|
)
|
|
return {"error": str(e)}
|
|
finally:
|
|
if _should_close:
|
|
await client.close()
|