实现了完整的Flock渠道接入能力,包含消息收发、Webhook事件监听、账号配置、安全校验、媒体文件处理等功能,支持私聊和群组聊天,适配ForcePilot插件规范。
208 lines
8.4 KiB
Python
208 lines
8.4 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
|
|
from .types import FlockAccount
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ENV_MAP = {
|
|
"FLOCK_BOT_TOKEN": "bot_token",
|
|
"FLOCK_APP_TOKEN": "app_token",
|
|
"FLOCK_APP_SECRET": "app_secret",
|
|
"FLOCK_OUTGOING_WEBHOOK_TOKEN": "outgoing_webhook_token",
|
|
"FLOCK_EVENT_LISTENER_TOKEN": "event_listener_token",
|
|
"FLOCK_INCOMING_WEBHOOK_URL": "incoming_webhook_url",
|
|
"FLOCK_DM_POLICY": "dm_policy",
|
|
"FLOCK_GROUP_POLICY": "group_policy",
|
|
"FLOCK_TEXT_CHUNK_LIMIT": "text_chunk_limit",
|
|
}
|
|
|
|
|
|
def _apply_env_overrides(account: FlockAccount) -> FlockAccount:
|
|
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)
|
|
return account
|
|
|
|
|
|
def _dict_to_account(data: dict) -> FlockAccount:
|
|
return FlockAccount(
|
|
account_id=data.get("account_id", "default"),
|
|
enabled=data.get("enabled", True),
|
|
bot_token=data.get("bot_token", ""),
|
|
app_token=data.get("app_token", ""),
|
|
app_secret=data.get("app_secret", ""),
|
|
outgoing_webhook_token=data.get("outgoing_webhook_token", ""),
|
|
event_listener_token=data.get("event_listener_token", ""),
|
|
incoming_webhook_url=data.get("incoming_webhook_url", ""),
|
|
dm_policy=data.get("dm_policy", "open"),
|
|
dm_allow_from=data.get("dm_allow_from", []),
|
|
group_policy=data.get("group_policy", "open"),
|
|
group_allow_from=data.get("group_allow_from", []),
|
|
require_mention=data.get("require_mention", False),
|
|
text_chunk_limit=data.get("text_chunk_limit", 7000),
|
|
media_max_mb=data.get("media_max_mb", 100),
|
|
thread_replies=data.get("thread_replies", "inbound"),
|
|
block_streaming=data.get("block_streaming", True),
|
|
)
|
|
|
|
|
|
def _resolve_account_data(raw_config: dict, account_id: str) -> FlockAccount:
|
|
accounts = raw_config.get("accounts", {})
|
|
data = accounts.get(account_id, {})
|
|
if "account_id" not in data:
|
|
data = {**data, "account_id": account_id}
|
|
account = _dict_to_account(data)
|
|
return _apply_env_overrides(account)
|
|
|
|
|
|
class FlockConfigAdapter:
|
|
def __init__(self, raw_config: dict | None = None):
|
|
self._raw_config: dict = raw_config or {}
|
|
|
|
def set_config(self, config: dict) -> None:
|
|
self._raw_config = config
|
|
|
|
def list_account_ids(self, config: dict | None = None) -> list[str]:
|
|
cfg = config if config is not None else self._raw_config
|
|
accounts = cfg.get("accounts", {})
|
|
if not accounts:
|
|
return ["default"]
|
|
return list(accounts.keys())
|
|
|
|
def resolve_account(self, account_id: str) -> dict:
|
|
accounts = self._raw_config.get("accounts", {})
|
|
data = accounts.get(account_id, {})
|
|
if not data:
|
|
return {"account_id": account_id}
|
|
if "account_id" not in data:
|
|
return {**data, "account_id": account_id}
|
|
return data
|
|
|
|
def resolve_account_object(self, account_id: str) -> FlockAccount:
|
|
return _resolve_account_data(self._raw_config, account_id)
|
|
|
|
def is_configured(self, account: dict) -> bool:
|
|
acct = _dict_to_account(account)
|
|
acct = _apply_env_overrides(acct)
|
|
return bool(acct.bot_token or acct.incoming_webhook_url)
|
|
|
|
def is_enabled(self, account: dict, config: dict | None = None) -> bool:
|
|
return account.get("enabled", True)
|
|
|
|
def describe_account(self, account: dict, config: dict | None = None) -> dict:
|
|
acct = _dict_to_account(account)
|
|
acct = _apply_env_overrides(acct)
|
|
return {
|
|
"account_id": acct.account_id,
|
|
"enabled": acct.enabled,
|
|
"configured": bool(acct.bot_token or acct.incoming_webhook_url),
|
|
"dm_policy": acct.dm_policy,
|
|
"group_policy": acct.group_policy,
|
|
}
|
|
|
|
def disabled_reason(self, account: dict, config: dict | None = None) -> str:
|
|
if account.get("enabled", True):
|
|
return ""
|
|
return "Account is disabled"
|
|
|
|
def unconfigured_reason(self, account: dict, config: dict | None = None) -> str:
|
|
acct = _dict_to_account(account)
|
|
acct = _apply_env_overrides(acct)
|
|
if not acct.bot_token and not acct.incoming_webhook_url:
|
|
return "bot_token and incoming_webhook_url not set"
|
|
return ""
|
|
|
|
def default_account_id(self, config: dict) -> str:
|
|
return "default"
|
|
|
|
def resolve_allow_from(self, 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(self, config: dict, account_id: str | None, allow_from: list[str]) -> list[str]:
|
|
return allow_from
|
|
|
|
def has_configured_state(self, config: dict) -> bool:
|
|
accounts = config.get("accounts", {})
|
|
for data in accounts.values():
|
|
if self.is_configured(data):
|
|
return True
|
|
return False
|
|
|
|
def has_persisted_auth_state(self, config: dict) -> bool:
|
|
return self.has_configured_state(config)
|
|
|
|
def config_schema(self) -> 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},
|
|
"bot_token": {"type": "string", "description": "Flock Bot Token"},
|
|
"app_token": {"type": "string", "description": "Flock App Token (备选)"},
|
|
"app_secret": {
|
|
"type": "string",
|
|
"description": "Flock App Secret (Event Token HMAC 签名验证)",
|
|
},
|
|
"outgoing_webhook_token": {
|
|
"type": "string",
|
|
"description": "Outgoing Webhook 验证 Token",
|
|
},
|
|
"event_listener_token": {
|
|
"type": "string",
|
|
"description": "Event Listener 验证 Token",
|
|
},
|
|
"incoming_webhook_url": {
|
|
"type": "string",
|
|
"description": "Incoming Webhook URL (备选发送方式)",
|
|
},
|
|
"dm_policy": {
|
|
"type": "string",
|
|
"enum": ["open", "pairing", "allowlist", "disabled"],
|
|
"default": "open",
|
|
},
|
|
"dm_allow_from": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"default": [],
|
|
},
|
|
"group_policy": {
|
|
"type": "string",
|
|
"enum": ["open", "allowlist", "disabled"],
|
|
"default": "open",
|
|
},
|
|
"group_allow_from": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"default": [],
|
|
},
|
|
"require_mention": {"type": "boolean", "default": False},
|
|
"text_chunk_limit": {"type": "integer", "default": 7000},
|
|
"media_max_mb": {"type": "integer", "default": 100},
|
|
"thread_replies": {
|
|
"type": "string",
|
|
"enum": ["off", "inbound", "always"],
|
|
"default": "inbound",
|
|
},
|
|
"block_streaming": {"type": "boolean", "default": True},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
}
|