新增 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: 类型定义
284 lines
10 KiB
Python
284 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
|
|
from .types import MatrixAccount
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
ENV_MAP = {
|
|
"MATRIX_HOMESERVER": "homeserver",
|
|
"MATRIX_ACCESS_TOKEN": "access_token",
|
|
"MATRIX_USER_ID": "user_id",
|
|
"MATRIX_PASSWORD": "password",
|
|
"MATRIX_DEVICE_ID": "device_id",
|
|
"MATRIX_DEVICE_NAME": "device_name",
|
|
"MATRIX_RECOVERY_KEY": "recovery_key",
|
|
}
|
|
|
|
|
|
def _apply_env_overrides(account: MatrixAccount) -> MatrixAccount:
|
|
for env_key, attr_name in ENV_MAP.items():
|
|
env_val = os.environ.get(env_key)
|
|
if env_val is not None:
|
|
setattr(account, attr_name, env_val)
|
|
env_dm_policy = os.environ.get("MATRIX_DM_POLICY")
|
|
if env_dm_policy:
|
|
account.dm_policy = env_dm_policy
|
|
env_group_policy = os.environ.get("MATRIX_GROUP_POLICY")
|
|
if env_group_policy:
|
|
account.group_policy = env_group_policy
|
|
return account
|
|
|
|
|
|
def _dict_to_account(data: dict) -> MatrixAccount:
|
|
return MatrixAccount(
|
|
account_id=data.get("account_id", "default"),
|
|
enabled=data.get("enabled", True),
|
|
homeserver=data.get("homeserver", ""),
|
|
allow_private_network=data.get("allow_private_network", False),
|
|
access_token=data.get("access_token", ""),
|
|
user_id=data.get("user_id", ""),
|
|
password=data.get("password", ""),
|
|
device_id=data.get("device_id", ""),
|
|
device_name=data.get("device_name", "ForcePilot Gateway"),
|
|
encryption=data.get("encryption", False),
|
|
recovery_key=data.get("recovery_key", ""),
|
|
initial_sync_limit=data.get("initial_sync_limit", 50),
|
|
dm_policy=data.get("dm_policy", "pairing"),
|
|
dm_allow_from=data.get("dm_allow_from", []),
|
|
dm_session_scope=data.get("dm_session_scope", "per-user"),
|
|
group_policy=data.get("group_policy", "allowlist"),
|
|
group_allow_from=data.get("group_allow_from", []),
|
|
rooms=data.get("rooms", {}),
|
|
auto_join=data.get("auto_join", "off"),
|
|
auto_join_allowlist=data.get("auto_join_allowlist", []),
|
|
streaming=data.get("streaming", "partial"),
|
|
block_streaming=data.get("block_streaming", False),
|
|
text_chunk_limit=data.get("text_chunk_limit", 4000),
|
|
media_max_mb=data.get("media_max_mb", 20),
|
|
thread_replies=data.get("thread_replies", "inbound"),
|
|
reply_to_mode=data.get("reply_to_mode", "off"),
|
|
ack_reaction=data.get("ack_reaction", ""),
|
|
reaction_notifications=data.get("reaction_notifications", "own"),
|
|
)
|
|
|
|
|
|
def list_account_ids(config: dict) -> list[str]:
|
|
accounts = config.get("accounts", {})
|
|
if not accounts:
|
|
return ["default"]
|
|
return list(accounts.keys())
|
|
|
|
|
|
def resolve_account(account_id: str, config: dict | None = None) -> dict:
|
|
if config is None:
|
|
config = {}
|
|
accounts = config.get("accounts", {})
|
|
data = accounts.get(account_id, {})
|
|
if not data:
|
|
data = {"account_id": account_id}
|
|
elif "account_id" not in data:
|
|
data = {**data, "account_id": account_id}
|
|
return data
|
|
|
|
|
|
def is_configured(account: dict) -> bool:
|
|
acct = _dict_to_account(account)
|
|
acct = _apply_env_overrides(acct)
|
|
return bool(acct.homeserver and acct.access_token and acct.user_id)
|
|
|
|
|
|
def is_enabled(account: dict, config: dict | None = None) -> bool:
|
|
return account.get("enabled", True)
|
|
|
|
|
|
def describe_account(account: dict, config: dict | None = None) -> dict:
|
|
acct = _dict_to_account(account)
|
|
acct = _apply_env_overrides(acct)
|
|
return {
|
|
"account_id": acct.account_id,
|
|
"homeserver": acct.homeserver,
|
|
"user_id": acct.user_id,
|
|
"device_id": acct.device_id,
|
|
"device_name": acct.device_name,
|
|
"encryption": acct.encryption,
|
|
"dm_policy": acct.dm_policy,
|
|
"group_policy": acct.group_policy,
|
|
"enabled": acct.enabled,
|
|
}
|
|
|
|
|
|
def inspect_account(config: dict, account_id: str | None = None) -> dict:
|
|
aid = account_id or "default"
|
|
accounts = config.get("accounts", {})
|
|
data = accounts.get(aid, {})
|
|
acct = _dict_to_account(data)
|
|
acct = _apply_env_overrides(acct)
|
|
return {
|
|
"account_id": acct.account_id,
|
|
"homeserver": acct.homeserver,
|
|
"user_id": acct.user_id,
|
|
"configured": bool(acct.homeserver and acct.access_token and acct.user_id),
|
|
"encryption": acct.encryption,
|
|
}
|
|
|
|
|
|
def default_account_id(config: dict) -> str:
|
|
return "default"
|
|
|
|
|
|
def set_account_enabled(config: dict, account_id: str, enabled: bool) -> dict:
|
|
accounts = config.setdefault("accounts", {})
|
|
acct = accounts.setdefault(account_id, {})
|
|
acct["enabled"] = enabled
|
|
return config
|
|
|
|
|
|
def delete_account(config: dict, account_id: str) -> dict:
|
|
accounts = config.get("accounts", {})
|
|
accounts.pop(account_id, None)
|
|
return config
|
|
|
|
|
|
def disabled_reason(account: dict, config: dict | None = None) -> str:
|
|
if account.get("enabled", True):
|
|
return ""
|
|
return "Account is disabled"
|
|
|
|
|
|
def unconfigured_reason(account: dict, config: dict | None = None) -> str:
|
|
acct = _dict_to_account(account)
|
|
acct = _apply_env_overrides(acct)
|
|
reasons = []
|
|
if not acct.homeserver:
|
|
reasons.append("homeserver not set")
|
|
if not acct.access_token:
|
|
reasons.append("access_token not set")
|
|
if not acct.user_id:
|
|
reasons.append("user_id not set")
|
|
return "; ".join(reasons) if reasons else ""
|
|
|
|
|
|
def resolve_allow_from(config: dict, account_id: str | None = None) -> list[str] | None:
|
|
aid = account_id or "default"
|
|
accounts = config.get("accounts", {})
|
|
data = accounts.get(aid, {})
|
|
acct = _dict_to_account(data)
|
|
acct = _apply_env_overrides(acct)
|
|
return list(acct.dm_allow_from)
|
|
|
|
|
|
def format_allow_from(config: dict, account_id: str | None, allow_from: list[str]) -> list[str]:
|
|
return allow_from
|
|
|
|
|
|
def has_configured_state(config: dict) -> bool:
|
|
accounts = config.get("accounts", {})
|
|
for data in accounts.values():
|
|
if is_configured(data):
|
|
return True
|
|
return False
|
|
|
|
|
|
def has_persisted_auth_state(config: dict) -> bool:
|
|
return has_configured_state(config)
|
|
|
|
|
|
def resolve_default_to(config: dict, account_id: str | None = None) -> str | None:
|
|
return None
|
|
|
|
|
|
def config_schema() -> dict:
|
|
return {
|
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
"type": "object",
|
|
"properties": {
|
|
"accounts": {
|
|
"type": "object",
|
|
"default": {"default": {}},
|
|
"additionalProperties": {
|
|
"type": "object",
|
|
"properties": {
|
|
"account_id": {"type": "string", "default": "default"},
|
|
"enabled": {"type": "boolean", "default": True},
|
|
"homeserver": {
|
|
"type": "string",
|
|
"description": "Matrix Homeserver URL",
|
|
"placeholder": "https://matrix.example.org",
|
|
},
|
|
"access_token": {"type": "string", "description": "Matrix Access Token"},
|
|
"user_id": {"type": "string", "description": "Matrix User ID (@bot:example.org)"},
|
|
"password": {"type": "string", "description": "Login password (fallback)"},
|
|
"device_id": {"type": "string"},
|
|
"device_name": {"type": "string", "default": "ForcePilot Gateway"},
|
|
"encryption": {"type": "boolean", "default": False},
|
|
"recovery_key": {"type": "string"},
|
|
"sync_token": {"type": "string", "description": "Last sync token for incremental sync"},
|
|
"dm_policy": {
|
|
"type": "string",
|
|
"enum": ["pairing", "allowlist", "open", "disabled"],
|
|
"default": "pairing",
|
|
},
|
|
"dm_allow_from": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"default": [],
|
|
},
|
|
"dm_session_scope": {
|
|
"type": "string",
|
|
"enum": ["per-user", "per-room"],
|
|
"default": "per-user",
|
|
},
|
|
"group_policy": {
|
|
"type": "string",
|
|
"enum": ["open", "allowlist", "disabled"],
|
|
"default": "allowlist",
|
|
},
|
|
"group_allow_from": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"default": [],
|
|
},
|
|
"auto_join": {
|
|
"type": "string",
|
|
"enum": ["off", "allowlist", "all"],
|
|
"default": "off",
|
|
},
|
|
"auto_join_allowlist": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"default": [],
|
|
},
|
|
"streaming": {
|
|
"type": "string",
|
|
"enum": ["partial", "quiet", "off"],
|
|
"default": "partial",
|
|
},
|
|
"block_streaming": {"type": "boolean", "default": False},
|
|
"text_chunk_limit": {"type": "integer", "default": 4000},
|
|
"media_max_mb": {"type": "integer", "default": 20},
|
|
"thread_replies": {
|
|
"type": "string",
|
|
"enum": ["off", "inbound", "always"],
|
|
"default": "inbound",
|
|
},
|
|
"reply_to_mode": {
|
|
"type": "string",
|
|
"enum": ["off", "inbound"],
|
|
"default": "off",
|
|
},
|
|
"ack_reaction": {"type": "string", "default": ""},
|
|
"reaction_notifications": {
|
|
"type": "string",
|
|
"enum": ["off", "own", "all"],
|
|
"default": "own",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
}
|