"""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 добавить источник (сначала реальная проверка фида) /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") or os.environ.get("OMNI_BOT_ALLOW") or "")} MAX_LEN = 3600 def _api(method, **params): if not TOKEN: raise SystemExit("OMNI_BOT_TOKEN не задан") url = API + method data = urllib.parse.urlencode(params).encode() 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 = "%s\n%s" % (title, src) if url: line += ' · открыть' % _esc(url) return line def cmd_start(chat_id, arg): _send(chat_id, "omni — парсер-радар\n\n" "/new 5 — последние записи\n" "/abuse promo — абузы по тегу\n" "/find claude — поиск\n" "/src reddit — источник\n" "/add <url> — подключить фид\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("%s\n%s · %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 = ["%s · %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, "добавлен %s · %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, "omni\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()