реестр: дедуп источников строго по url (2 падавших теста зелёные); инкрементальный abuse-индекс + tgsearch из предыдущей сессии
This commit is contained in:
127
omni/abuse.py
127
omni/abuse.py
@@ -39,6 +39,46 @@ RULES = [
|
||||
r"продаю", r"продам", r"куплю\s+аккаунт", r"for\s+sale", r"цена\s*[:$]", r"\$\d+\s?/\s?(мес|mo|1k|1000)",
|
||||
r"реселлер", r"resell",
|
||||
]),
|
||||
("credits", "кредиты/квоты", 22, [
|
||||
r"free\s+(?:api\s+)?credits?", r"\b\d+\s?(?:k|m)?\s?tokens?\s+(?:free|бесплатн)", r"free\s+quota",
|
||||
r"бесплатн\w*\s+(?:лимит|квот|токен)", r"кредит\w*\s+(?:бесплатн|в\s+подарок)", r"赠金", r"额度",
|
||||
r"unlimited\s+(?:api|access|tokens|usage)", r"безлимит\w*\s+(?:api|ключ|доступ|токен)",
|
||||
]),
|
||||
("invite", "инвайты/беты", 16, [
|
||||
r"\binvite\s?(?:code|link|only)\b", r"инвайт", r"\bwaitlist\b", r"лист\s+ожидан", r"early\s+access",
|
||||
r"\bbeta\s+(?:access|invite|key)", r"закрыт\w*\s+бет", r"приглашени",
|
||||
]),
|
||||
("ltd", "lifetime deal", 18, [
|
||||
r"lifetime\s+deal", r"\bltd\b", r"пожизненн\w*\s+(?:доступ|лицензи)", r"пакет\s+навсегда",
|
||||
]),
|
||||
("coupon", "купоны/скидки", 12, [
|
||||
r"\bcoupon\b", r"\bdiscount\s?(?:code)?\b", r"\d{1,2}%\s?off", r"дешёв\w*\s+чем", r"\bcheaper\b",
|
||||
]),
|
||||
("combo", "комбо/базы", 20, [
|
||||
r"[\w.\-]+@[\w.\-]+\.[a-z]{2,}\s?:\s?[^\s]{4,}", r"\bcombolist\b", r"\bcombos?\b\s+(?:list|base)",
|
||||
r"\b\d{3,}\s*(?:k|к|тыс)?\s*(?:строк|lines|аккаунтов|accounts)\b",
|
||||
]),
|
||||
("crack", "кряки/патчи", 16, [
|
||||
r"\bkeygen\b", r"\bpatcher\b", r"\bкряк\b", r"\bcrack(?:ed)?\s+(?:version|by)", r"\bcracked\b",
|
||||
r"\bnulled\b", r"\bactivated\s+version\b",
|
||||
]),
|
||||
("selfhost", "селфхост/опенсорс", 8, [
|
||||
r"self[\s-]?host(?:ed|ing)?", r"open[\s-]?source\s+alternative", r"альтернатив\w*\s+(?:для|к)\s+\w+",
|
||||
r"docker\s+(?:run|compose)", r"\bна\s+своём\s+сервер",
|
||||
]),
|
||||
("freeapi", "бесплатные API", 24, [
|
||||
r"free\s+(?:llm\s+)?api", r"бесплатн\w*\s+api", r"api\s+(?:без|without)\s+(?:ключа|ключа\s+не\s+нужно|key)",
|
||||
r"no\s+(?:api\s+)?key\s+(?:required|needed)", r"keyless", r"openai[\s-]?compatible\s+(?:endpoint|api)",
|
||||
r"/v1/chat/completions", r"new[\s-]?api\s+(?:panel|панел)", r"openai\s+proxy",
|
||||
]),
|
||||
("nokyc", "без KYC/карты", 14, [
|
||||
r"no\s+kyc", r"без\s+kyc", r"no\s+card\s+(?:required|needed)", r"без\s+карт\w*", r"без\s+депозит",
|
||||
r"no\s+sign[\s-]?up",
|
||||
]),
|
||||
("infra", "прокси/номера/карты", 10, [
|
||||
r"\bпрокси\b|\bproxies\b", r"\bsms\s?(?:activ|receive)", r"\bвиртуальн\w*\s+карт", r"\bvcc\b",
|
||||
r"\bноутбук\s+номер", r"\bномер\w*\s+для\s+смс",
|
||||
]),
|
||||
("news", "новости", 6, [
|
||||
r"анонс", r"выпустил", r"released?", r"launch", r"обновлени", r"release notes",
|
||||
r"новая модель", r"new model",
|
||||
@@ -47,7 +87,7 @@ RULES = [
|
||||
|
||||
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"}
|
||||
STRONG = {"keys", "promo", "trial", "resale", "credits", "freeapi", "ltd", "combo"}
|
||||
# на специализированных абуз-площадках хватает одного признака: сам источник тематический
|
||||
ABUSE_SRC_SC = 14
|
||||
# категории-источники, которые вообще имеют смысл для абузов
|
||||
@@ -106,7 +146,7 @@ def _qualifies(source, title, text, tags):
|
||||
return 0, []
|
||||
if cat is None:
|
||||
# старые записи хранят категорию вместо имени источника
|
||||
if not raw_key and not str(source or "").startswith(("ab_", "tg_", "reddit_", "discourse_", "xenforo")):
|
||||
if not raw_key and not str(source or "").startswith(("ab_", "tg_", "rad_", "reddit_", "discourse_", "xenforo")):
|
||||
return 0, []
|
||||
elif cat not in ABUSE_CATS and not raw_key:
|
||||
return 0, []
|
||||
@@ -119,42 +159,89 @@ def _qualifies(source, title, text, tags):
|
||||
return 0, []
|
||||
|
||||
|
||||
def _connect():
|
||||
con = sqlite3.connect(str(DB), timeout=60)
|
||||
con.execute("PRAGMA busy_timeout=60000")
|
||||
con.execute("CREATE TABLE IF NOT EXISTS abuse_idx "
|
||||
"(id INTEGER PRIMARY KEY, score INTEGER, tags TEXT)")
|
||||
return con
|
||||
|
||||
|
||||
def rebuild(full=False, chunk=4000):
|
||||
"""Инкрементальный индекс абузов: classify по новым items, без полного перещёлкивания.
|
||||
|
||||
full=True (или OMNI_ABUSE_REBUILD=1) — пересчитать всю базу после правки правил.
|
||||
Возвращает число записей, попавших в индекс на этом проходе.
|
||||
"""
|
||||
import os
|
||||
if os.environ.get("OMNI_ABUSE_REBUILD") == "1":
|
||||
full = True
|
||||
con = _connect()
|
||||
try:
|
||||
if full:
|
||||
con.execute("DELETE FROM abuse_idx")
|
||||
con.commit()
|
||||
last = con.execute("SELECT COALESCE(MAX(id), 0) FROM abuse_idx").fetchone()[0]
|
||||
added = 0
|
||||
while True:
|
||||
rows = con.execute(
|
||||
"SELECT id, source, url, title, text, tags FROM items WHERE id > ? ORDER BY id LIMIT ?",
|
||||
(last, chunk)).fetchall()
|
||||
if not rows:
|
||||
break
|
||||
batch = []
|
||||
for r in rows:
|
||||
sc, hits = _qualifies(r[1], r[3], r[4], r[5] or "")
|
||||
if sc:
|
||||
batch.append((r[0], sc, ",".join(hits)))
|
||||
last = r[0]
|
||||
if batch:
|
||||
con.executemany("INSERT OR REPLACE INTO abuse_idx(id, score, tags) VALUES (?,?,?)", batch)
|
||||
added += len(batch)
|
||||
con.commit()
|
||||
return added
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
|
||||
def _rows(limit=400, tag=None, q=None, since_days=None):
|
||||
sql = "SELECT id, source, url, title, text, published, tags, score FROM items"
|
||||
"""Читает из индекса abuse_idx; индекс добивается свежими записями перед чтением."""
|
||||
try:
|
||||
rebuild()
|
||||
except Exception:
|
||||
pass
|
||||
sql = ("SELECT i.id, i.source, i.url, i.title, i.text, i.published, a.tags, i.score, a.score "
|
||||
"FROM abuse_idx a JOIN items i ON i.id = a.id")
|
||||
where, args = [], []
|
||||
if tag:
|
||||
where.append("(',' || a.tags || ',') LIKE ?")
|
||||
args.append("%%," + tag + ",%%")
|
||||
if q:
|
||||
where.append("(title LIKE ? OR text LIKE ? OR source LIKE ?)")
|
||||
where.append("(i.title LIKE ? OR i.text LIKE ? OR i.source LIKE ?)")
|
||||
args += ["%%%s%%" % q, "%%%s%%" % q, "%%%s%%" % q]
|
||||
if since_days:
|
||||
where.append("published >= date('now', ?)")
|
||||
where.append("i.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)
|
||||
sql += " ORDER BY a.score DESC, i.id DESC LIMIT ?"
|
||||
args.append(int(limit))
|
||||
con = _connect()
|
||||
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
|
||||
for r in con.execute(sql, args):
|
||||
hits = [t for t in (r[6] or "").split(",") if t]
|
||||
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
|
||||
"tags": hits, "abuse_score": r[8], "rank": r[7]})
|
||||
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)
|
||||
"""limit — сколько отдать; индекс уже отсортирован по весy, window не режет."""
|
||||
items = _rows(limit=max(int(limit), 400), 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:
|
||||
|
||||
Reference in New Issue
Block a user