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 = "" 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: 或 # - channel-name: → 通过名称查找频道 - user: 或 @ - <26位 ID> → 尝试推测为 channel ID - <53位 DM ID> → 识别为 DM channel ID - mattermost: """ 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