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

217
omni/bot.py Normal file
View File

@@ -0,0 +1,217 @@
"""Telegram-бот поверх парсера: лента, абузы, источники, прогон, автодобавление.
Запуск: OMNI_BOT_TOKEN=... python -m omni.bot
Доступ: OMNI_BOT_OWNERS=7448683285,123 (если пусто — только первый написавший)
Бот не постит никуда сам: только отвечает в личный чат тем, кто в списке.
Команды:
/new [n] последние записи
/abuse [тег] абузы: promo trial keys leak tool resale
/find <текст> поиск по базе
/src <текст> поиск источника
/add <url> добавить источник (сначала реальная проверка фида)
/run <категория> прогнать категорию
/auto [n] автодобавление новых фидов
/stats цифры
"""
from __future__ import annotations
import html
import json
import os
import re
import time
import urllib.parse
import urllib.request
from . import abuse as abuse_mod
from . import discover as discover_mod
from . import feeds, net, source_state, sources, store
API = "https://api.telegram.org/bot%s/" % os.environ.get("OMNI_BOT_TOKEN", "")
TOKEN = os.environ.get("OMNI_BOT_TOKEN", "")
OWNERS = {int(x) for x in re.findall(r"\d{5,}", os.environ.get("OMNI_BOT_OWNERS", ""))}
MAX_LEN = 3600
def _api(method, **params):
if not TOKEN:
raise SystemExit("OMNI_BOT_TOKEN не задан")
url = API + method
data = urllib.parse.urlencode(params).encode()
r = net.get(url, headers={"Content-Type": "application/x-www-form-urlencoded"}) if False else None
req = urllib.request.Request(url, data=data)
with urllib.request.urlopen(req, timeout=40) as resp:
return json.loads(resp.read().decode("utf-8"))
def _send(chat_id, text, preview=True):
text = text[:MAX_LEN]
return _api("sendMessage", chat_id=chat_id, text=text, parse_mode="HTML",
disable_web_page_preview="false" if preview else "true")
def _esc(s):
return html.escape(str(s or ""), quote=False)
def fmt_item(it):
title = _esc((it.get("title") or "")[:150])
src = _esc(it.get("source") or "")
url = it.get("url") or ""
line = "<b>%s</b>\n<code>%s</code>" % (title, src)
if url:
line += ' · <a href="%s">открыть</a>' % _esc(url)
return line
def cmd_start(chat_id, arg):
_send(chat_id, "<b>omni</b> — парсер-радар\n\n"
"/new 5 — последние записи\n"
"/abuse promo — абузы по тегу\n"
"/find claude — поиск\n"
"/src reddit — источник\n"
"/add &lt;url&gt; — подключить фид\n"
"/run abuse — прогнать категорию\n"
"/auto 10 — автодобавление\n"
"/stats — цифры")
def cmd_new(chat_id, arg):
n = int(arg) if arg.strip().isdigit() else 8
rows = store.browse2(limit=min(n, 20), sort="fresh")
items = rows.get("items", rows) if isinstance(rows, dict) else rows
if not items:
return _send(chat_id, "пусто")
_send(chat_id, "последние %d:\n\n" % len(items) + "\n\n".join(fmt_item(i) for i in items))
def cmd_abuse(chat_id, arg):
tag = arg.strip().split()[0] if arg.strip() else None
d = abuse_mod.scan(limit=10, tag=tag)
if not d["items"]:
return _send(chat_id, "абузов не видно")
head = "абузы · %s · %d\n\n" % ("все" if not tag else tag, d["total"])
body = []
for it in d["items"]:
body.append("<b>%s</b>\n<code>%s</code> · %s" % (
_esc(it["title"][:120]), _esc(it["source"]), _esc(",".join(it["tags"]))))
_send(chat_id, head + "\n\n".join(body))
def cmd_find(chat_id, arg):
q = arg.strip()
if not q:
return _send(chat_id, "что искать?")
rows = store.search(q, limit=8)
items = rows.get("items", rows) if isinstance(rows, dict) else rows
if not items:
return _send(chat_id, "нет совпадений")
_send(chat_id, "поиск: %s\n\n" % _esc(q) + "\n\n".join(fmt_item(i) for i in items))
def cmd_src(chat_id, arg):
q = arg.strip().lower()
all_s = sources.all_sources()
hits = [s for s in all_s if q and (q in s.name.lower() or q in s.url.lower() or q in s.category.lower())]
if not hits:
return _send(chat_id, "нет такого источника")
out = ["<b>%s</b> · %s · %s" % (_esc(s.name), _esc(s.category), _esc(s.adapter)) for s in hits[:12]]
_send(chat_id, "источники (%d):\n\n" % len(hits) + "\n".join(out))
def cmd_add(chat_id, arg):
url = arg.strip()
if not url.startswith("http"):
return _send(chat_id, "нужен url фида")
ok, n, ad = discover_mod.probe(url, "community", limit=6)
if not ok:
return _send(chat_id, "фид пустой или недоступен: %s" % _esc(ad))
name = "manual_" + re.sub(r"[^a-z0-9]+", "_", urllib.parse.urlsplit(url).netloc.lower())[:32]
source_state.add_custom(name, "community", url, kind="rss" if ad == "rss" else "html",
adapter=ad, tags=["manual"], limit=15)
_send(chat_id, "добавлен <code>%s</code> · %s · %d записей" % (_esc(name), _esc(ad), n))
def cmd_run(chat_id, arg):
cat = arg.strip() or "abuse"
srcs = [s for s in sources.all_sources() if s.category == cat]
if not srcs:
return _send(chat_id, "нет категории %s" % _esc(cat))
_send(chat_id, "прогон %s (%d источников)…" % (_esc(cat), len(srcs)))
got = new = 0
for s in srcs[:80]:
try:
items = feeds.fetch(s, 10)
except Exception:
items = []
got += len(items)
if items:
new += store.add(items)
_send(chat_id, "готово: собрано %d, новых %d" % (got, new))
def cmd_auto(chat_id, arg):
n = int(arg) if arg.strip().isdigit() else 15
res = discover_mod.discover(limit_hosts=30, workers=8, max_add=min(n, 30))
_send(chat_id, "автодобавление: кандидатов %d, живых %d\n%s" % (
res["found"], res["alive"], _esc(", ".join(res["added"][:10]))))
def cmd_stats(chat_id, arg):
s = store.stats()
a = sources.all_sources()
alive = source_state.alive_count() if hasattr(source_state, "alive_count") else "?"
_send(chat_id, "<b>omni</b>\nзаписей %s · источников %d · живых %s\nкатегорий %d · прогонов %s · с ценой %s" % (
s.get("items"), len(a), alive, len(sources.CATEGORIES), s.get("runs"), s.get("priced")))
CMDS = {"/start": cmd_start, "/help": cmd_start, "/new": cmd_new, "/abuse": cmd_abuse,
"/find": cmd_find, "/src": cmd_src, "/add": cmd_add, "/run": cmd_run,
"/auto": cmd_auto, "/stats": cmd_stats}
def handle(update):
msg = update.get("message") or update.get("edited_message") or {}
chat = msg.get("chat") or {}
chat_id = chat.get("id")
text = (msg.get("text") or "").strip()
uid = (msg.get("from") or {}).get("id")
if not chat_id or not text:
return
if OWNERS and uid not in OWNERS:
return
if not OWNERS:
OWNERS.add(uid) # первый написавший становится владельцем до рестарта
parts = text.split(None, 1)
cmd = parts[0].lower().split("@")[0]
arg = parts[1] if len(parts) > 1 else ""
fn = CMDS.get(cmd)
if not fn:
return _send(chat_id, "не знаю такую команду. /help")
try:
fn(chat_id, arg)
except Exception as e:
_send(chat_id, "ошибка: %s" % _esc(type(e).__name__ + ": " + str(e)[:160]))
def run_forever():
if not TOKEN:
raise SystemExit("нужен OMNI_BOT_TOKEN")
offset = 0
print("bot up", flush=True)
while True:
try:
r = _api("getUpdates", offset=offset, timeout=30)
except Exception as e:
print("poll fail", e, flush=True)
time.sleep(5)
continue
for up in r.get("result", []):
offset = up["update_id"] + 1
handle(up)
time.sleep(0.3)
if __name__ == "__main__":
run_forever()