新增 Mattermost 渠道完整实现,包含适配器核心、消息处理、交互回调、命令支持、安全校验、多账号管理等功能,支持机器人消息发送、交互按钮、命令注册、投票功能以及配置动态修改等特性。
132 lines
4.1 KiB
Python
132 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
ChatMode = str
|
|
MODE_ONCALL: ChatMode = "oncall"
|
|
MODE_ONMESSAGE: ChatMode = "onmessage"
|
|
MODE_ONCHAR: ChatMode = "onchar"
|
|
CHAT_MODES = frozenset({MODE_ONCALL, MODE_ONMESSAGE, MODE_ONCHAR})
|
|
|
|
DEFAULT_ONCHAR_PREFIXES = frozenset({">", "!"})
|
|
|
|
|
|
@dataclass
|
|
class MentionGateConfig:
|
|
chat_mode: ChatMode = MODE_ONMESSAGE
|
|
require_mention: bool = True
|
|
onchar_prefixes: set[str] = field(default_factory=lambda: set(DEFAULT_ONCHAR_PREFIXES))
|
|
|
|
@classmethod
|
|
def from_config(cls, config: dict) -> MentionGateConfig:
|
|
chat_mode = config.get("chatmode", MODE_ONMESSAGE)
|
|
if chat_mode not in CHAT_MODES:
|
|
chat_mode = MODE_ONMESSAGE
|
|
|
|
prefixes_raw = config.get("onchar_prefixes", [])
|
|
if not prefixes_raw:
|
|
prefixes_raw = list(DEFAULT_ONCHAR_PREFIXES)
|
|
prefixes = {str(p).strip() for p in prefixes_raw if str(p).strip()}
|
|
if not prefixes:
|
|
prefixes = set(DEFAULT_ONCHAR_PREFIXES)
|
|
|
|
require_mention = bool(config.get("require_mention", True))
|
|
|
|
return cls(
|
|
chat_mode=chat_mode,
|
|
require_mention=require_mention,
|
|
onchar_prefixes=frozenset(prefixes),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class MentionGateResult:
|
|
should_respond: bool
|
|
reason: str = ""
|
|
|
|
|
|
class MentionGate:
|
|
"""消息响应门控 — 决定是否响应入站消息。
|
|
|
|
支持三种模式:
|
|
- oncall: 仅响应明确呼叫(不自动触发)
|
|
- onmessage: 响应所有消息(结合 requireMention 过滤)
|
|
- onchar: 仅响应以特定前缀开头的消息
|
|
"""
|
|
|
|
def __init__(self, config: dict | None = None):
|
|
self._config = MentionGateConfig.from_config(config or {})
|
|
|
|
@property
|
|
def chat_mode(self) -> str:
|
|
return self._config.chat_mode
|
|
|
|
@property
|
|
def require_mention(self) -> bool:
|
|
return self._config.require_mention
|
|
|
|
def check(
|
|
self,
|
|
chat_type: str,
|
|
text: str,
|
|
bot_mentioned: bool,
|
|
) -> MentionGateResult:
|
|
mode = self._config.chat_mode
|
|
|
|
if chat_type == "direct":
|
|
return self._check_dm(text, mode)
|
|
|
|
return self._check_group_or_channel(text, bot_mentioned, mode)
|
|
|
|
def _check_dm(self, text: str, mode: ChatMode) -> MentionGateResult:
|
|
if mode == MODE_ONCALL:
|
|
return MentionGateResult(True)
|
|
|
|
if mode == MODE_ONCHAR:
|
|
if text and any(text.lstrip().startswith(p) for p in self._config.onchar_prefixes):
|
|
return MentionGateResult(True)
|
|
return MentionGateResult(False, "DM message does not match onchar prefix")
|
|
|
|
return MentionGateResult(True)
|
|
|
|
def _check_group_or_channel(self, text: str, bot_mentioned: bool, mode: ChatMode) -> MentionGateResult:
|
|
if mode == MODE_ONCALL:
|
|
return MentionGateResult(False, "oncall mode does not respond in groups without explicit mention")
|
|
|
|
if mode == MODE_ONMESSAGE:
|
|
if self._config.require_mention and not bot_mentioned:
|
|
return MentionGateResult(False, "Bot not mentioned and requireMention is enabled")
|
|
return MentionGateResult(True)
|
|
|
|
if mode == MODE_ONCHAR:
|
|
if text and any(text.lstrip().startswith(p) for p in self._config.onchar_prefixes):
|
|
return MentionGateResult(True)
|
|
return MentionGateResult(False, "Message does not match onchar prefix")
|
|
|
|
return MentionGateResult(False, f"Unknown chat mode: {mode}")
|
|
|
|
|
|
CONTROL_COMMAND_MARKERS = frozenset({"/restart", "/sudo", "/exec", "/delete", "/config", "/approve", "/deny"})
|
|
|
|
|
|
def is_control_command(text: str) -> bool:
|
|
text_stripped = text.strip()
|
|
return any(text_stripped.startswith(cmd) for cmd in CONTROL_COMMAND_MARKERS)
|
|
|
|
|
|
def resolve_control_command_gate(
|
|
text: str,
|
|
chat_type: str,
|
|
bot_mentioned: bool = False,
|
|
) -> bool:
|
|
"""控制命令门控 — 控制命令总是通过(仅 DM/mention 场景),
|
|
由后续的 authorize_command_invocation 做进一步授权检查。
|
|
"""
|
|
if not is_control_command(text):
|
|
return False
|
|
|
|
if chat_type == "direct":
|
|
return True
|
|
|
|
return bot_mentioned
|