新增 Mattermost 渠道完整实现,包含适配器核心、消息处理、交互回调、命令支持、安全校验、多账号管理等功能,支持机器人消息发送、交互按钮、命令注册、投票功能以及配置动态修改等特性。
173 lines
5.0 KiB
Python
173 lines
5.0 KiB
Python
from __future__ import annotations
|
||
|
||
import re
|
||
from dataclasses import dataclass
|
||
from typing import Any
|
||
|
||
TARGET_CHANNEL_RE = re.compile(r"^(?:channel:|#)(\S+)$", re.IGNORECASE)
|
||
TARGET_USER_RE = re.compile(r"^(?:user:|@)(\S+)$", re.IGNORECASE)
|
||
TARGET_MATTERMOST_RE = re.compile(r"^mattermost:(\S+)$", re.IGNORECASE)
|
||
TARGET_CHANNEL_NAME_RE = re.compile(r"^channel-name:(.+)$", re.IGNORECASE)
|
||
TARGET_DM_CHANNEL_ID_RE = re.compile(r"^[a-z0-9]{26}__[a-z0-9]{26}$", re.IGNORECASE)
|
||
NICK_USER_HOST_RE = re.compile(r"^([^!]+)!([^@]+)@(.+)$")
|
||
|
||
|
||
@dataclass
|
||
class ResolvedTarget:
|
||
target_type: str # "channel" | "user" | "unknown"
|
||
target_id: str
|
||
display_name: str = ""
|
||
|
||
@property
|
||
def is_channel(self) -> bool:
|
||
return self.target_type == "channel"
|
||
|
||
@property
|
||
def is_user(self) -> bool:
|
||
return self.target_type == "user"
|
||
|
||
|
||
TARGET_HINT = "<channelId|user:ID|channel:ID|channel-name:name|@username>"
|
||
|
||
|
||
def looks_like_mattermost_target_id(raw: str) -> bool:
|
||
if not raw:
|
||
return False
|
||
target = raw.strip()
|
||
if TARGET_DM_CHANNEL_ID_RE.match(target):
|
||
return True
|
||
if len(target) == 26 and target.isalnum():
|
||
return True
|
||
if TARGET_CHANNEL_RE.match(target):
|
||
return True
|
||
if TARGET_USER_RE.match(target):
|
||
return True
|
||
if TARGET_CHANNEL_NAME_RE.match(target):
|
||
return True
|
||
return False
|
||
|
||
|
||
def get_target_format_hint() -> str:
|
||
return TARGET_HINT
|
||
|
||
|
||
def parse_target(raw: str) -> ResolvedTarget:
|
||
"""解析目标字符串为结构化目标。
|
||
|
||
支持格式:
|
||
- channel:<id> 或 #<channel_name>
|
||
- channel-name:<name> → 通过名称查找频道
|
||
- user:<id> 或 @<username>
|
||
- <26位 ID> → 尝试推测为 channel ID
|
||
- <53位 DM ID> → 识别为 DM channel ID
|
||
- mattermost:<id>
|
||
"""
|
||
if not raw or not raw.strip():
|
||
return ResolvedTarget("unknown", "")
|
||
|
||
target = raw.strip()
|
||
|
||
m = TARGET_CHANNEL_RE.match(target)
|
||
if m:
|
||
return ResolvedTarget("channel", m.group(1), display_name=f"#{m.group(1)}")
|
||
|
||
m = TARGET_CHANNEL_NAME_RE.match(target)
|
||
if m:
|
||
return ResolvedTarget("channel", m.group(1), display_name=f"#{m.group(1)}")
|
||
|
||
m = TARGET_USER_RE.match(target)
|
||
if m:
|
||
return ResolvedTarget("user", m.group(1), display_name=f"@{m.group(1)}")
|
||
|
||
m = TARGET_MATTERMOST_RE.match(target)
|
||
if m:
|
||
return parse_target(m.group(1))
|
||
|
||
m = NICK_USER_HOST_RE.match(target)
|
||
if m:
|
||
return ResolvedTarget("user", m.group(1), display_name=m.group(1))
|
||
|
||
if TARGET_DM_CHANNEL_ID_RE.match(target):
|
||
return ResolvedTarget("channel", target, display_name=f"dm:{target[:8]}...")
|
||
|
||
if len(target) == 26 and target.isalnum():
|
||
return ResolvedTarget("channel", target, display_name=f"channel:{target[:8]}...")
|
||
|
||
return ResolvedTarget("unknown", target, display_name=target)
|
||
|
||
|
||
async def resolve_mattermost_opaque_target(
|
||
raw: str,
|
||
adapter: Any = None,
|
||
known_channels: dict[str, str] | None = None,
|
||
known_users: dict[str, str] | None = None,
|
||
) -> tuple[str | None, str]:
|
||
"""解析模糊目标为 (chat_id, chat_type)。
|
||
|
||
返回 (chat_id, chat_type),chat_type 为 "direct" 或 "channel"。
|
||
"""
|
||
target = parse_target(raw)
|
||
|
||
if target.target_type == "user":
|
||
uid = target.target_id
|
||
if known_users:
|
||
for kid, kname in known_users.items():
|
||
if kname.casefold() == uid.casefold():
|
||
uid = kid
|
||
break
|
||
return uid, "direct"
|
||
|
||
if target.target_type == "channel":
|
||
cid = target.target_id
|
||
if known_channels:
|
||
for kcid, kcname in known_channels.items():
|
||
if kcname.casefold() == cid.casefold():
|
||
cid = kcid
|
||
break
|
||
|
||
if adapter and hasattr(adapter, "_channel_cache"):
|
||
cached = adapter._channel_cache.channel_by_name.get(cid)
|
||
if cached:
|
||
return str(cached), "channel"
|
||
|
||
return cid, "channel"
|
||
|
||
return None, "unknown"
|
||
|
||
|
||
def resolve_channel_target(channel_name_or_id: str, known_channels: dict[str, str] | None = None) -> str | None:
|
||
"""将频道名解析为 channel_id。"""
|
||
if not channel_name_or_id:
|
||
return None
|
||
|
||
target = parse_target(channel_name_or_id)
|
||
if target.target_type == "channel":
|
||
return target.target_id
|
||
|
||
if known_channels:
|
||
for cid, cname in known_channels.items():
|
||
if cname.casefold() == target.target_id.casefold():
|
||
return cid
|
||
|
||
return target.target_id
|
||
|
||
|
||
def resolve_user_target(
|
||
user_ref: str,
|
||
known_users: dict[str, str] | None = None,
|
||
) -> str | None:
|
||
"""将用户引用解析为 user_id。"""
|
||
if not user_ref:
|
||
return None
|
||
|
||
target = parse_target(user_ref)
|
||
if target.target_type == "user":
|
||
uid = target.target_id
|
||
if known_users and uid not in known_users:
|
||
for kid, kname in known_users.items():
|
||
if kname.casefold() == uid.casefold():
|
||
return kid
|
||
return uid
|
||
|
||
return target.target_id
|