omni: абузы (31 живой источник + классификатор), авто-добавление фидов, Telegram-бот, вкладка абузов; 589 источников

This commit is contained in:
reform
2026-10-26 12:25:24 +03:00
parent 81f3e556c4
commit ec61ec21f0
12 changed files with 1362 additions and 35 deletions

149
omni/abuse.py Normal file
View File

@@ -0,0 +1,149 @@
"""Разбор абузов: промо, триалы, ключи, сливы, софт, реселл.
Работает поверх уже собранной базы (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}