ForcePilot/backend/package/yuxi/channels/adapters/mattermost/target_resolution.py
Kris 002d601d1b feat(mattermost): 实现完整的 Mattermost 适配器模块
新增 Mattermost 渠道完整实现,包含适配器核心、消息处理、交互回调、命令支持、安全校验、多账号管理等功能,支持机器人消息发送、交互按钮、命令注册、投票功能以及配置动态修改等特性。
2026-05-12 00:46:12 +08:00

173 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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