# VESTI — извлечение потенциальных источников из текста поста (без LLM, regex). import re from urllib.parse import urlparse _MAX_PER_POST = 5 # t.me/handle, t.me/s/handle, telegram.me/handle _TG_RE = re.compile( r"(?:https?://)?(?:www\.)?(?:t\.me|telegram\.me)/(?:s/)?([a-zA-Z][a-zA-Z0-9_]{3,31})" ) # @handle (не email, не на конце слова) _AT_RE = re.compile(r"(?\"']+)", re.I) _TG_DOMAINS = ("t.me", "telegram.me") def _domain_of(url: str) -> str: return (urlparse(url if "://" in url else "https://" + url).netloc or "").lower() def extract_sources_from_text(text: str, exclude_tg_url: str | None = None) -> list[dict]: """Ищет TG-каналы и внешние сайты в тексте поста. exclude_tg_url — url самого поста (t.me/channel/postid): ссылку на него отбрасываем (это адрес поста, а не источник). Возвращает список dict: {"kind": "telegram"|"site", "address": str, "slug_hint": str|None, "domain": str|None} максимум _MAX_PER_POST элементов. """ out: list[dict] = [] seen: set[tuple] = set() def add(kind: str, address: str, slug_hint: str | None, domain: str | None): key = (kind, slug_hint or domain or address) if key in seen: return seen.add(key) out.append({"kind": kind, "address": address, "slug_hint": slug_hint, "domain": domain}) text = (text or "")[:4000] for m in _TG_RE.finditer(text): ch = m.group(1) if exclude_tg_url and f"/{ch}/" in exclude_tg_url: continue # ссылка на сам пост add("telegram", f"https://t.me/{ch}", ch, None) if len(out) >= _MAX_PER_POST: break for m in _AT_RE.finditer(text): add("telegram", f"https://t.me/{m.group(1)}", m.group(1), None) if len(out) >= _MAX_PER_POST: break for m in _URL_RE.finditer(text): u = m.group(1).rstrip(".,;:!?)]}\"'") dom = _domain_of(u) if not dom: continue if dom in _TG_DOMAINS or dom.endswith(".t.me"): continue # уже обработано выше add("site", u, None, dom) if len(out) >= _MAX_PER_POST: break return out[:_MAX_PER_POST]