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