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
|