ForcePilot/backend/package/yuxi/channel/extensions/signal/access_policy.py
Kris 6713dc2d0d feat(channel): 添加 Signal 渠道扩展
新增 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 重连机制
2026-05-21 11:41:55 +08:00

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()