新增 Nextcloud Talk 渠道扩展,支持在 Yuxi 平台中集成 Nextcloud Talk 即时通讯渠道。 包含以下功能模块: - accounts: 账户管理 - bot_admin: Bot 管理 - chat_api: 聊天 API 封装 - config_schema: 配置模式 - conversation: 会话管理 - gateway: SSE/WebSocket 网关接入 - inbound: 入站消息处理 - send: 消息发送 - webhook_server: Webhook 服务 - format: 消息格式转换 - normalize: 消息规范化 - poll_api: 投票 API - reaction_api: 表情反应 API - policy: 安全策略 - probe: 健康探测 - replay_guard: 重放防护 - signature: 签名验证 - room_info: 房间信息 - types: 类型定义
155 lines
5.7 KiB
Python
155 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
import logging
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from urllib.parse import urljoin
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
OCS_CAPABILITIES_PATH = "/ocs/v2.php/cloud/capabilities"
|
|
PROBE_TIMEOUT = 10.0
|
|
|
|
|
|
@dataclass
|
|
class NextcloudTalkProbeResult:
|
|
ok: bool = False
|
|
base_url: str = ""
|
|
reachable: bool = False
|
|
latency_ms: float | None = None
|
|
nextcloud_version: str | None = None
|
|
talk_app_installed: bool = False
|
|
bots_v1_available: bool = False
|
|
reactions_available: bool = False
|
|
api_credentials_valid: bool | None = None
|
|
# --feature response cannot be verified without a valid room token
|
|
feature_response_checkable: bool = False
|
|
warnings: list[str] = field(default_factory=list)
|
|
error: str | None = None
|
|
|
|
|
|
async def probe_nextcloud_talk_connectivity(base_url: str) -> dict:
|
|
if not base_url:
|
|
return {"reachable": False, "error": "base_url is not configured"}
|
|
|
|
url = urljoin(base_url.rstrip("/") + "/", OCS_CAPABILITIES_PATH)
|
|
start = time.monotonic()
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=PROBE_TIMEOUT) as client:
|
|
resp = await client.get(
|
|
url,
|
|
headers={
|
|
"OCS-APIRequest": "true",
|
|
"Accept": "application/json",
|
|
},
|
|
)
|
|
latency_ms = (time.monotonic() - start) * 1000
|
|
|
|
if resp.status_code != 200:
|
|
return {"reachable": False, "latency_ms": latency_ms, "error": f"HTTP {resp.status_code}"}
|
|
|
|
data = resp.json()
|
|
ocs_data = data.get("ocs", {}).get("data", {})
|
|
version = ocs_data.get("version", {}).get("string")
|
|
capabilities = ocs_data.get("capabilities", {})
|
|
talk_installed = "spreed" in capabilities
|
|
talk_capabilities = capabilities.get("spreed", {}).get("features", [])
|
|
|
|
return {
|
|
"reachable": True,
|
|
"latency_ms": round(latency_ms, 1),
|
|
"nextcloud_version": version,
|
|
"talk_app_installed": talk_installed,
|
|
"bots_v1_available": "bots-v1" in talk_capabilities,
|
|
"reactions_available": "reactions" in talk_capabilities,
|
|
}
|
|
except httpx.TimeoutException:
|
|
return {"reachable": False, "error": "connection timeout"}
|
|
except httpx.ConnectError as e:
|
|
return {"reachable": False, "error": f"connection failed: {e}"}
|
|
except Exception as e:
|
|
return {"reachable": False, "error": str(e)}
|
|
|
|
|
|
async def probe_nextcloud_talk_api_credentials(base_url: str, api_user: str, api_password: str) -> dict:
|
|
if not api_user or not api_password:
|
|
return {"valid": None, "error": "API credentials not configured"}
|
|
|
|
url = urljoin(base_url.rstrip("/") + "/", OCS_CAPABILITIES_PATH)
|
|
auth = base64.b64encode(f"{api_user}:{api_password}".encode()).decode()
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=PROBE_TIMEOUT) as client:
|
|
resp = await client.get(
|
|
url,
|
|
headers={
|
|
"Authorization": f"Basic {auth}",
|
|
"OCS-APIRequest": "true",
|
|
"Accept": "application/json",
|
|
},
|
|
)
|
|
if resp.status_code == 200:
|
|
return {"valid": True}
|
|
if resp.status_code == 401:
|
|
return {"valid": False, "error": "API credentials invalid (401)"}
|
|
return {"valid": False, "error": f"HTTP {resp.status_code}"}
|
|
except Exception as e:
|
|
return {"valid": False, "error": str(e)}
|
|
|
|
|
|
async def probe_nextcloud_talk(account: dict) -> NextcloudTalkProbeResult:
|
|
base_url = account.get("base_url", "")
|
|
api_user = account.get("api_user")
|
|
api_password = account.get("api_password", "")
|
|
bot_secret = account.get("bot_secret", "")
|
|
|
|
result = NextcloudTalkProbeResult(base_url=base_url)
|
|
|
|
if not base_url:
|
|
result.error = "base_url not configured"
|
|
result.warnings.append("base_url is required")
|
|
return result
|
|
|
|
conn_result = await probe_nextcloud_talk_connectivity(base_url)
|
|
result.reachable = conn_result["reachable"]
|
|
result.latency_ms = conn_result.get("latency_ms")
|
|
result.nextcloud_version = conn_result.get("nextcloud_version")
|
|
result.talk_app_installed = conn_result.get("talk_app_installed", False)
|
|
result.bots_v1_available = conn_result.get("bots_v1_available", False)
|
|
result.reactions_available = conn_result.get("reactions_available", False)
|
|
|
|
if not result.reachable:
|
|
result.error = conn_result.get("error", "unreachable")
|
|
result.warnings.append(f"Nextcloud instance unreachable: {result.error}")
|
|
return result
|
|
|
|
if not result.talk_app_installed:
|
|
result.warnings.append("Nextcloud Talk app not detected on this instance")
|
|
|
|
if not result.bots_v1_available:
|
|
result.warnings.append("bots-v1 capability not available - Nextcloud Talk may be too old for Bot API")
|
|
|
|
if api_user and api_password:
|
|
cred_result = await probe_nextcloud_talk_api_credentials(base_url, api_user, api_password)
|
|
result.api_credentials_valid = cred_result["valid"]
|
|
if not cred_result["valid"]:
|
|
result.warnings.append(f"API credentials: {cred_result.get('error', 'unknown error')}")
|
|
|
|
if not bot_secret:
|
|
result.warnings.append("bot_secret not configured - webhook signature verification will fail")
|
|
|
|
result.feature_response_checkable = bool(bot_secret)
|
|
if not result.feature_response_checkable:
|
|
result.warnings.append(
|
|
"Cannot verify --feature response: bot_secret not configured. "
|
|
"Ensure 'occ talk:bot:install --feature response' was used."
|
|
)
|
|
|
|
result.ok = result.reachable and not any(w for w in result.warnings if "unreachable" in w.lower())
|
|
|
|
return result
|