新增了完整的Signal渠道适配器实现,包含RPC客户端、守护进程管理、安全策略、消息处理、安装配置工具等全套功能,支持通过signal-cli与Signal网络进行通信,包含账户管理、消息收发、反应处理、媒体分析、健康检查等能力。
181 lines
6.1 KiB
Python
181 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from enum import StrEnum
|
|
from typing import Any
|
|
|
|
E164_MIN_LENGTH = 5
|
|
E164_MAX_LENGTH = 15
|
|
E164_PATTERN = re.compile(r"^\+\d{%d,%d}$" % (E164_MIN_LENGTH, E164_MAX_LENGTH))
|
|
|
|
|
|
class SetupStep(StrEnum):
|
|
STATUS = "status"
|
|
PREPARE = "prepare"
|
|
CLI_PATH = "cli_path"
|
|
SIGNAL_NUMBER = "signal_number"
|
|
ALLOW_FROM = "allow_from"
|
|
COMPLETION = "completion"
|
|
|
|
|
|
class SetupWizard:
|
|
def __init__(self, config: dict[str, Any] | None = None):
|
|
self.config = config or {}
|
|
self._current_step: SetupStep = SetupStep.STATUS
|
|
|
|
@property
|
|
def current_step(self) -> str:
|
|
return self._current_step.value
|
|
|
|
def get_step_result(self) -> dict:
|
|
match self._current_step:
|
|
case SetupStep.STATUS:
|
|
return self._status_step()
|
|
case SetupStep.PREPARE:
|
|
return self._prepare_step()
|
|
case SetupStep.CLI_PATH:
|
|
return self._cli_path_step()
|
|
case SetupStep.SIGNAL_NUMBER:
|
|
return self._signal_number_step()
|
|
case SetupStep.ALLOW_FROM:
|
|
return self._allow_from_step()
|
|
case SetupStep.COMPLETION:
|
|
return self._completion_step()
|
|
case _:
|
|
return {"step": self._current_step.value, "status": "unknown"}
|
|
|
|
def advance(self, data: dict[str, Any] | None = None) -> dict:
|
|
step_order = list(SetupStep)
|
|
current_idx = step_order.index(self._current_step)
|
|
if current_idx < len(step_order) - 1:
|
|
self._current_step = step_order[current_idx + 1]
|
|
return self.get_step_result()
|
|
|
|
def _status_step(self) -> dict:
|
|
return {
|
|
"step": "status",
|
|
"title": "Signal Setup Wizard",
|
|
"description": "Configure your Signal channel step by step",
|
|
"total_steps": len(SetupStep),
|
|
}
|
|
|
|
def _prepare_step(self) -> dict:
|
|
from yuxi.channels.adapters.signal.install import check_java_installed, check_signal_cli_installed
|
|
|
|
java_ok, _ = check_java_installed()
|
|
cli_ok, cli_version = check_signal_cli_installed(self.config.get("cli_path", "signal-cli"))
|
|
|
|
result = {
|
|
"step": "prepare",
|
|
"title": "Environment Check",
|
|
"fields": [
|
|
{"key": "java_installed", "label": "Java 17+", "ok": java_ok},
|
|
{"key": "signal_cli_installed", "label": "signal-cli", "ok": cli_ok, "version": cli_version or ""},
|
|
],
|
|
"ready": java_ok and cli_ok,
|
|
}
|
|
|
|
if not cli_ok and java_ok:
|
|
result["can_auto_install"] = True
|
|
result["auto_install_hint"] = "signal-cli can be auto-installed in the next step"
|
|
|
|
return result
|
|
|
|
def _cli_path_step(self) -> dict:
|
|
current_path = self.config.get("cli_path", "signal-cli")
|
|
return {
|
|
"step": "cli_path",
|
|
"title": "signal-cli Path",
|
|
"description": "Path to the signal-cli binary",
|
|
"fields": [
|
|
{
|
|
"key": "cli_path",
|
|
"label": "Binary Path",
|
|
"type": "string",
|
|
"default": current_path,
|
|
"required": True,
|
|
}
|
|
],
|
|
}
|
|
|
|
def _signal_number_step(self) -> dict:
|
|
current_number = self.config.get("signal_number", "")
|
|
return {
|
|
"step": "signal_number",
|
|
"title": "Signal Phone Number",
|
|
"description": "E.164 format phone number (e.g. +1234567890)",
|
|
"fields": [
|
|
{
|
|
"key": "signal_number",
|
|
"label": "Phone Number",
|
|
"type": "string",
|
|
"default": current_number,
|
|
"required": True,
|
|
"pattern": f"^\\+\\d{{{E164_MIN_LENGTH},{E164_MAX_LENGTH}}}$",
|
|
}
|
|
],
|
|
}
|
|
|
|
def _completion_step(self) -> dict:
|
|
return {
|
|
"step": "completion",
|
|
"title": "Setup Complete",
|
|
"description": "Signal channel is ready to connect",
|
|
"next_steps": [
|
|
"1. Register your number: signal-cli -a <number> register",
|
|
"2. Verify: signal-cli -a <number> verify <CODE>",
|
|
"3. Add an account_uuid config for loop prevention",
|
|
],
|
|
}
|
|
|
|
def _allow_from_step(self) -> dict:
|
|
security = self.config.get("security", {})
|
|
current_allow_from = security.get("allow_from", [])
|
|
return {
|
|
"step": "allow_from",
|
|
"title": "Access Control - Allow From",
|
|
"description": "Comma-separated E.164 numbers, UUIDs, or '*' to allow DM from (leave empty for pairing mode)",
|
|
"fields": [
|
|
{
|
|
"key": "allow_from",
|
|
"label": "Allowed Senders",
|
|
"type": "string",
|
|
"default": ", ".join(current_allow_from) if current_allow_from else "",
|
|
"placeholder": "+8613800138000, uuid:abc123, *",
|
|
}
|
|
],
|
|
}
|
|
|
|
def auto_install(self) -> dict:
|
|
import asyncio
|
|
from yuxi.channels.adapters.signal.install import auto_install_signal_cli
|
|
|
|
try:
|
|
target_dir = self.config.get("install_dir")
|
|
success = asyncio.run(auto_install_signal_cli(target_dir))
|
|
return {
|
|
"step": "prepare",
|
|
"auto_install_success": success,
|
|
"message": "signal-cli installed successfully" if success else "auto-install failed",
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"step": "prepare",
|
|
"auto_install_success": False,
|
|
"message": str(e),
|
|
}
|
|
|
|
|
|
def validate_e164(number: str) -> bool:
|
|
return bool(E164_PATTERN.match(number.strip()))
|
|
|
|
|
|
def format_e164(raw: str) -> str:
|
|
stripped = raw.strip()
|
|
if E164_PATTERN.match(stripped):
|
|
return stripped
|
|
digits = re.sub(r"[^\d]", "", stripped)
|
|
if E164_MIN_LENGTH <= len(digits) <= E164_MAX_LENGTH:
|
|
return f"+{digits}"
|
|
raise ValueError(f"Invalid E.164 number: {raw}")
|