"""Разбор абузов: промо, триалы, ключи, сливы, софт, реселл. Работает поверх уже собранной базы (items) и поверх новых абуз-источников: каждая запись получает набор тегов и вес, панель показывает только то, что реально пахнет абузом, а не весь поток. """ from __future__ import annotations import re import sqlite3 from .config import DB # тег -> (человеческое имя, вес, регулярки) RULES = [ ("keys", "ключи", 40, [ r"sk-[A-Za-z0-9_\-]{20,}", r"gh[pousr]_[A-Za-z0-9]{20,}", r"xox[baprs]-[A-Za-z0-9\-]{10,}", r"(разда|выда|дарю|бесплатн|free|giveaway)[^.]{0,40}(ключ|key|токен|token)", r"(ключ|key|токен)[^.]{0,25}(бесплатн|free|дарю|раздач|giveaway)", ]), ("promo", "промо", 26, [ r"промокод", r"promo\s?code", r"промо[\s-]?код", r"скидк[аи]\s+\d{1,3}", r"\d{1,3}\s?%\s?(off|скидк)", r"redeem\s?(code|код)", r"invite\s?code", r"реф(еральн|код)", r"бонус\s+на\s+сч[её]т", r"кэшб[еэ]к|cashback", ]), ("trial", "триалы", 24, [ r"trial", r"триал", r"free\s+credits?", r"бесплатн\w*\s+(кредит|токен|баланс|доступ|период)", r"free\s+tier", r"\$\d+\s+free", r"试用", r"免费", r"пробн\w+\s+период", r"без\s+депозита", r"no\s+deposit", ]), ("leak", "сливы", 26, [ r"слив(?!\s*(сок|ки|очн|тест|трафик))", r"leaked\s+(database|db|dump|combo)", r"db\s?dump", r"combo[\s-]?list", r"stealer\s?log", r"утечк\w*\s+баз", r"база\s+(пассов|почт|данных)", r"пароли\s+от\s+\w+\.(com|ru|net)", ]), ("tool", "софт", 18, [ r"софт", r"кряк", r"crack(ed)?", r"bypass", r"обход\s+(блокиров|бан|защит)", r"авторег", r"autoreg", r"panel", r"панел[ьи]\s", r"рассылк", r"sender", r"скрипт", ]), ("resale", "продажа", 14, [ r"продаю", r"продам", r"куплю\s+аккаунт", r"for\s+sale", r"цена\s*[:$]", r"\$\d+\s?/\s?(мес|mo|1k|1000)", r"реселлер", r"resell", ]), ("news", "новости", 6, [ r"анонс", r"выпустил", r"released?", r"launch", r"обновлени", r"release notes", r"новая модель", r"new model", ]), ] COMPILED = [(tag, label, w, [re.compile(p, re.I) for p in pats]) for tag, label, w, pats in RULES] TAG_LABEL = {t: l for t, l, _, _ in RULES} STRONG = {"keys", "promo", "trial", "resale"} # категории-источники, которые вообще имеют смысл для абузов ABUSE_CATS = {"abuse", "forum", "community", "company_cn"} # «статьи»: слова про ключи и утечки там — текст, а не раздача NOISE = re.compile(r"(blog|devto|medium|substack|newsletter|docs?|wiki)", re.I) def _source_meta(): try: from . import sources as _s return {x.name: x.category for x in _s.all_sources()} except Exception: return {} SRC_CAT = _source_meta() def classify(title: str, text: str, tags: str = ""): blob = (" ".join([title or "", text or "", tags or ""]))[:4000] hits = [] score = 0 for tag, label, w, pats in COMPILED: n = 0 for p in pats: m = p.search(blob) if m: n += 1 if n: hits.append(tag) score += w * min(n, 3) return score, hits def _qualifies(source, title, text, tags): sc, hits = classify(title, text, tags) if not hits: return 0, [] strong = bool(set(hits) & STRONG) cat = SRC_CAT.get(source) blob = (title or "") + " " + (text or "") raw_key = bool(re.search(r"(sk-[A-Za-z0-9_\-]{20,}|gh[pousr]_[A-Za-z0-9]{20,})", blob)) if NOISE.search(source or "") and not raw_key: return 0, [] if cat is None: # старые записи хранят категорию вместо имени источника if not raw_key and not str(source or "").startswith(("ab_", "tg_", "reddit_", "discourse_", "xenforo")): return 0, [] elif cat not in ABUSE_CATS and not raw_key: return 0, [] if strong and sc >= 24: return sc, hits if len(hits) >= 2 and sc >= 40: return sc, hits return 0, [] def _rows(limit=400, tag=None, q=None, since_days=None): sql = "SELECT id, source, url, title, text, published, tags, score FROM items" where, args = [], [] if q: where.append("(title LIKE ? OR text LIKE ? OR source LIKE ?)") args += ["%%%s%%" % q, "%%%s%%" % q, "%%%s%%" % q] if since_days: where.append("published >= date('now', ?)") args.append("-%d day" % int(since_days)) if where: sql += " WHERE " + " AND ".join(where) sql += " ORDER BY id DESC LIMIT ?" args.append(int(limit) * 6) con = sqlite3.connect(str(DB), timeout=30) try: cur = con.execute(sql, args) out = [] for r in cur: sc, hits = _qualifies(r[1], r[3], r[4], r[6] or "") if not sc: continue if tag and tag not in hits: continue out.append({"id": r[0], "source": r[1], "url": r[2], "title": r[3], "text": (r[4] or "")[:600], "published": r[5], "tags": hits, "abuse_score": sc, "rank": r[7]}) if len(out) >= limit: break return out finally: con.close() def scan(limit=120, tag=None, q=None, since_days=None, window=6000): """limit — сколько отдать, window — сколько последних записей просканировать.""" items = _rows(limit=window, tag=tag, q=q, since_days=since_days) items.sort(key=lambda x: (-x["abuse_score"], -(x["id"] or 0))) counts = {} for it in items: for t in it["tags"]: counts[t] = counts.get(t, 0) + 1 return {"items": items[:limit], "total": len(items), "tags": counts, "labels": TAG_LABEL}