from __future__ import annotations import re import unicodedata def normalize_open_id(open_id: str) -> str: return open_id.strip().lower() if open_id else "" def normalize_chat_id(chat_id: str) -> str: return chat_id.strip() if chat_id else "" def safe_filename(name: str) -> str: name = re.sub(r"[\x00-\x1f\x7f-\x9f]", "", name) name = name.replace('"', "'").replace("\\", "_") return name.strip() or "file" def recover_latin1_filename(value: str) -> str: try: return value.encode("latin-1").decode("utf-8") except (UnicodeEncodeError, UnicodeDecodeError): return value def build_sequential_key(chat_id: str, account_id: str) -> str: return f"{account_id}:{chat_id}" def parse_feishu_allow_entry(entry: str) -> tuple[str, str]: entry = entry.strip() if entry == "*": return ("wildcard", "*") if ":" in entry: prefix, value = entry.split(":", 1) return (prefix.strip().lower(), value.strip()) if entry.startswith("oc_"): return ("chat", entry) if entry.startswith("ou_"): return ("user", entry) return ("unknown", entry) def is_broadcast_mention(mention_key: str) -> bool: return mention_key in ("@all", "@_all", "@everyone") def extract_image_keys(text: str) -> list[str]: return re.findall(r"img_v2_[a-zA-Z0-9]+", text) def sanitize_markdown_escape(text: str) -> str: special_chars = r"\*_{}[]()#+-!|>~" result = [] for ch in text: if ch in special_chars: result.append("\\" + ch) else: result.append(ch) return "".join(result) def ellipsize(text: str, max_len: int = 100) -> str: if len(text) <= max_len: return text return text[: max_len - 1] + "…" def strip_control_chars(text: str) -> str: return "".join(ch for ch in text if unicodedata.category(ch)[0] != "C" or ch in ("\n", "\r", "\t"))