Files
omni-parse/omni/bot.py

220 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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") 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]
if os.environ.get("OMNI_BOT_DRY"):
print("[DRY] " + text.replace("</b>", "").replace("<b>", "")[:1200])
return {"ok": True}
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
items, _total = store.browse2(limit=min(n, 20), sort="fresh")
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)
keys = ("provider", "kind", "source", "title", "score", "url")
items = [dict(zip(keys, r)) if not isinstance(r, dict) else r for r in 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()
fl = source_state.flags()
alive = sum(1 for x in a if (fl.get(x.name, {}).get("items") or 0) > 0)
_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()