新增 Signal 渠道扩展,支持在 Yuxi 平台中集成 Signal 加密即时通讯渠道。 包含以下功能模块: - client: Signal 客户端封装 - daemon: signald 守护进程管理 - config_schema: 配置模式 - send: 消息发送 - accounts: 账户管理 - account_management: 账户综合管理 - access_policy: 访问策略 - identity: 身份管理 - profiles: 用户资料 - groups: 群组管理 - format: 消息格式转换 - normalize: 消息规范化 - dedupe: 消息去重 - monitor: 渠道状态监控 - probe: 健康探测 - sse_reconnect: SSE 重连机制
132 lines
3.8 KiB
Python
132 lines
3.8 KiB
Python
import logging
|
|
import random
|
|
import string
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DM_POLICIES = {"open", "pairing", "allowlist", "disabled"}
|
|
GROUP_POLICIES = {"open", "allowlist", "disabled"}
|
|
DEFAULT_DM_POLICY = "pairing"
|
|
DEFAULT_GROUP_POLICY = "allowlist"
|
|
|
|
_group_policy_warnings: set[str] = set()
|
|
|
|
|
|
def resolve_dm_policy(config: dict) -> str:
|
|
policy = config.get("dm_policy", DEFAULT_DM_POLICY)
|
|
if policy not in DM_POLICIES:
|
|
return DEFAULT_DM_POLICY
|
|
return policy
|
|
|
|
|
|
def resolve_group_policy(config: dict, account_key: str = "") -> str:
|
|
policy = config.get("group_policy")
|
|
if policy and policy in GROUP_POLICIES:
|
|
return policy
|
|
|
|
if policy is not None and policy not in GROUP_POLICIES:
|
|
if account_key not in _group_policy_warnings:
|
|
_group_policy_warnings.add(account_key)
|
|
logger.warning(
|
|
"Invalid group_policy '%s' for account '%s', falling back to '%s'",
|
|
policy,
|
|
account_key or "default",
|
|
DEFAULT_GROUP_POLICY,
|
|
)
|
|
return DEFAULT_GROUP_POLICY
|
|
|
|
if account_key not in _group_policy_warnings:
|
|
_group_policy_warnings.add(account_key)
|
|
logger.warning(
|
|
"No group_policy configured for account '%s', falling back to default '%s'",
|
|
account_key or "default",
|
|
DEFAULT_GROUP_POLICY,
|
|
)
|
|
return DEFAULT_GROUP_POLICY
|
|
|
|
|
|
def check_dm_access(
|
|
config: dict,
|
|
sender_id: str,
|
|
allow_from: list[str] | None = None,
|
|
pairing_store: dict | None = None,
|
|
) -> tuple[str, str | None]:
|
|
"""返回 (status, reason) status ∈ {allow, deny, pairing}"""
|
|
policy = resolve_dm_policy(config)
|
|
|
|
if policy == "disabled":
|
|
return ("deny", "DM is disabled")
|
|
|
|
if policy == "open":
|
|
return ("allow", None)
|
|
|
|
allow_list = allow_from or config.get("allow_from", [])
|
|
if "*" in allow_list:
|
|
return ("allow", None)
|
|
|
|
from yuxi.channel.extensions.signal.identity import match_signal_allow_entry
|
|
|
|
for entry in allow_list:
|
|
if match_signal_allow_entry(sender_id, entry):
|
|
return ("allow", None)
|
|
|
|
if policy == "allowlist":
|
|
return ("deny", "Sender not in allowlist")
|
|
|
|
if policy == "pairing":
|
|
store = pairing_store or {}
|
|
if sender_id in store:
|
|
return ("allow", None)
|
|
return ("pairing", "Pairing required")
|
|
|
|
return ("deny", f"Unknown policy: {policy}")
|
|
|
|
|
|
def check_group_access(
|
|
config: dict,
|
|
sender_id: str,
|
|
group_allow_from: list[str] | None = None,
|
|
account_key: str = "",
|
|
) -> tuple[str, str | None]:
|
|
"""返回 (status, reason); status ∈ {allow, deny}"""
|
|
policy = resolve_group_policy(config, account_key)
|
|
|
|
if policy == "disabled":
|
|
return ("deny", "Group messages are disabled")
|
|
|
|
if policy == "open":
|
|
return ("allow", None)
|
|
|
|
allow_list = group_allow_from or config.get("group_allow_from", []) or config.get("allow_from", [])
|
|
if "*" in allow_list:
|
|
return ("allow", None)
|
|
|
|
from yuxi.channel.extensions.signal.identity import match_signal_allow_entry
|
|
|
|
for entry in allow_list:
|
|
if match_signal_allow_entry(sender_id, entry):
|
|
return ("allow", None)
|
|
|
|
return ("deny", "Sender not in group allowlist")
|
|
|
|
|
|
def resolve_group_config(config: dict, group_id: str) -> dict:
|
|
groups = config.get("groups", {})
|
|
group_cfg = groups.get(group_id, groups.get("*", {}))
|
|
return {
|
|
"require_mention": group_cfg.get("require_mention", True),
|
|
"ingest": group_cfg.get("ingest", False),
|
|
}
|
|
|
|
|
|
PAIRING_CODE_CHARS = string.digits
|
|
PAIRING_CODE_LENGTH = 4
|
|
|
|
|
|
def generate_pairing_code() -> str:
|
|
return "".join(random.choices(PAIRING_CODE_CHARS, k=PAIRING_CODE_LENGTH))
|
|
|
|
|
|
def verify_pairing_code(generated: str, submitted: str) -> bool:
|
|
return generated.strip() == submitted.strip()
|