127 lines
4.6 KiB
Python
127 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
from .shared import *
|
|
|
|
class FeishuSenderResolutionMixin:
|
|
@staticmethod
|
|
def _build_mention_prefix(*, open_id: Optional[str], display_name: Optional[str]) -> str:
|
|
user_open_id = str(open_id or "").strip()
|
|
if not user_open_id:
|
|
return ""
|
|
safe_name = str(display_name or "你").replace("<", "").replace(">", "").strip()[:24] or "你"
|
|
return f'<at user_id="{user_open_id}">{safe_name}</at> '
|
|
|
|
async def _resolve_sender_display_name(
|
|
self,
|
|
*,
|
|
sender: Dict[str, Any],
|
|
sender_id: Dict[str, Any],
|
|
) -> str:
|
|
sender_name = (sender.get("sender_name") or "").strip()
|
|
if sender_name:
|
|
return sender_name
|
|
for key in ("name", "en_name"):
|
|
value = (sender.get(key) or "").strip()
|
|
if value:
|
|
return value
|
|
|
|
open_id = (sender_id.get("open_id") or "").strip()
|
|
if open_id:
|
|
remote_name = await self._fetch_user_name_by_open_id(open_id)
|
|
if remote_name:
|
|
return remote_name
|
|
return f"飞书用户({open_id[:8]})"
|
|
|
|
user_id = (sender_id.get("user_id") or "").strip()
|
|
if user_id:
|
|
return f"飞书用户({user_id[:8]})"
|
|
return "未知用户"
|
|
|
|
async def _fetch_user_name_by_open_id(self, open_id: str) -> Optional[str]:
|
|
safe_open_id = (open_id or "").strip()
|
|
if not safe_open_id:
|
|
return None
|
|
try:
|
|
access_token = await self._get_tenant_access_token()
|
|
except Exception as exc:
|
|
print(f"[Feishu] fetch user name skipped: token unavailable: {exc}")
|
|
return None
|
|
|
|
url = f"https://open.feishu.cn/open-apis/contact/v3/users/{safe_open_id}"
|
|
headers = {"Authorization": f"Bearer {access_token}"}
|
|
params = {"user_id_type": "open_id"}
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0, trust_env=False) as client:
|
|
resp = await client.get(url, headers=headers, params=params)
|
|
resp.raise_for_status()
|
|
payload = resp.json() or {}
|
|
if payload.get("code") != 0:
|
|
print(
|
|
"[Feishu] fetch user name failed: "
|
|
f"code={payload.get('code')} msg={payload.get('msg')}"
|
|
)
|
|
return None
|
|
user = payload.get("data", {}).get("user", {}) if isinstance(payload.get("data", {}), dict) else {}
|
|
name = (user.get("name") or user.get("en_name") or "").strip()
|
|
return name or None
|
|
except Exception as exc:
|
|
print(f"[Feishu] fetch user name failed: {exc}")
|
|
return None
|
|
|
|
@staticmethod
|
|
def _extract_message_datetime(message: Dict[str, Any]) -> Optional[datetime]:
|
|
if not isinstance(message, dict):
|
|
return None
|
|
raw = message.get("create_time") or message.get("update_time")
|
|
if raw is None:
|
|
return None
|
|
try:
|
|
ts = int(str(raw).strip())
|
|
if ts > 10_000_000_000:
|
|
ts = ts / 1000.0
|
|
return datetime.fromtimestamp(ts, tz=CN_TZ)
|
|
except Exception:
|
|
return None
|
|
|
|
@staticmethod
|
|
def _extract_mentioned_bot_names(message: Dict[str, Any]) -> List[str]:
|
|
if not isinstance(message, dict):
|
|
return []
|
|
mentions = message.get("mentions", [])
|
|
names: List[str] = []
|
|
if isinstance(mentions, list):
|
|
for item in mentions:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
name = str(item.get("name") or "").strip()
|
|
if not name:
|
|
continue
|
|
if name not in names:
|
|
names.append(name)
|
|
|
|
content = message.get("content")
|
|
text = ""
|
|
if isinstance(content, dict):
|
|
text = str(content.get("text") or "")
|
|
elif isinstance(content, str):
|
|
try:
|
|
parsed = json.loads(content)
|
|
if isinstance(parsed, dict):
|
|
text = str(parsed.get("text") or "")
|
|
else:
|
|
text = content
|
|
except Exception:
|
|
text = content
|
|
|
|
if text:
|
|
for raw in re.findall(r"<at[^>]*>([^<]+)</at>", text, flags=re.IGNORECASE):
|
|
name = str(raw or "").strip()
|
|
if name and name not in names:
|
|
names.append(name)
|
|
for raw in re.findall(r"@([^\s@]+)", text):
|
|
name = str(raw or "").strip()
|
|
if name and name not in names:
|
|
names.append(name)
|
|
return names
|
|
|