ForcePilot/backend/package/yuxi/channels/adapters/mattermost/target_resolution.py

173 lines
5.0 KiB
Python
Raw Normal View History

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