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'{safe_name} '
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"]*>([^<]+)", 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