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

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ data/
*.sqlite3*
__pycache__/
*.pyc
.bot_token

View File

@@ -4,6 +4,36 @@
Ставится без ключей. Платные провайдеры включаются сами, если в окружении есть ключ.
## Состояние на сейчас
- **источников: 589** в 11 категориях, **записей в базе: 3411**
- вкладки панели: `Лента` · `Источники` · `Прогоны` · `Абузы`
- health-аудит: живой/мёртвый/непроверенный по каждому источнику, причина в примечании
- Telegram-бот на той же базе: `/new`, `/abuse`, `/find`, `/src`, `/run`, `/add`, `/auto`, `/stats`
| категория | источников |
|---|---|
| `community` | 123 |
| `company` | 119 |
| `security` | 72 |
| `papers` | 68 |
| `forum` | 50 |
| `models` | 35 |
| `company_cn` | 34 |
| `abuse` | 31 |
| `release` | 30 |
| `benchmark` | 16 |
| `company_kr` | 11 |
## Панель
```bash
python -m omni serve --port 8077
# http://127.0.0.1:8077/?view=abuse
```
`?view=feed|src|runs|abuse`, горячие клавиши `1..4`, палитра `Ctrl K`, категории в URL (`?cat=`), кнопка «авто-добавление» ищет новые фиды на уже подключённых хостах и сохраняет только те, что реально отдали записи.
## Что умеет
| Провайдер | Что тянет | Ключ |
@@ -44,7 +74,7 @@ python -m omni search "scraping twitter without api" --providers reddit,forums,d
python -m omni sources
python -m omni sources --category papers,security --list
# обход реестра БЕЗ запроса: 450 источников -> база
# обход реестра БЕЗ запроса: 589 источников -> база
python -m omni feed --category community --limit 8
python -m omni feed --limit 15 # всё подряд
python -m omni feed --only arxiv_cs_ai,hn_top,reddit_localllama
@@ -66,7 +96,7 @@ python -m omni export --format jsonl --out data/dump.jsonl
## Реестр источников
`omni/sources.py` + `omni/sources_ext.py` — **450 источников в 9 категориях**, у каждого свой адаптер и флаг вкл/выкл:
`omni/sources.py` + `omni/sources_ext.py` — **589 источников в 9 категориях**, у каждого свой адаптер и флаг вкл/выкл:
- **papers 49** — arxiv по 25+ разделам (cs.AI/LG/CL/CV/RO/IR/CR/SE/NE/MA/DC/DB/HC/SD/CY, stat.ML, eess.AS/SP/IV, math.OC, q-bio.NC, cs.GR, cs.AR, cs.PF), HF daily papers, OpenAlex, Semantic Scholar, Papers with Code, Connected Papers
- **company 95** — ленты OpenAI, Google AI/Research/DeepMind, Microsoft Research, NVIDIA, Apple ML, Amazon Science, HF, Meta AI, Mistral, Cohere, Together, Perplexity, Groq, Cerebras, Stability, Databricks, Anyscale, Modal, W&B, LangChain, LlamaIndex, vLLM, Replicate, Fireworks, SambaNova, Scale, xAI, Salesforce, AWS ML, Azure AI, Anthropic, AI2, EleutherAI, LAION, Runway, ElevenLabs, Deepgram, AssemblyAI, Pinecone, Weaviate, Qdrant, Chroma, Unsloth, Axolotl
@@ -93,7 +123,7 @@ python -m omni digest --out data/digest.md
1. **лента** — счётчики (записей, с ценой, прогонов, источников, включённых), поиск по базе, фильтр по категориям, кнопка «собрать ленту» с прогрессом и разбивкой fetched/new/dup/empty.
2. **источники** — менеджер реестра:
- таблица всех 450 источников: переключатель вкл/выкл, категория, тип, url, статус (жив / мёртв / не проверен), число записей;
- таблица всех 589 источников: переключатель вкл/выкл, категория, тип, url, статус (жив / мёртв / не проверен), число записей;
- фильтр по имени/url и по категории;
- **подключить источник** — форма (имя, url, категория): тип и адаптер определяются по url автоматически (`/rss` → rss, `.json`/`/api/` → generic_json, `github.com/org` → github_org, `t.me/s/` → tg_web), запись живёт в SQLite и сразу участвует в обходах;
- **проверить всё / проверить категорию** — фоновой джоб: реальный фетч каждого источника, запись `status/items/note/checked`, прогресс `done/total, живых/мёртвых`;
@@ -143,3 +173,20 @@ python -m omni stats
## Лицензия
MIT.
## Абузы, авто-добавление и Telegram-бот
- **абузы**: категория `abuse` — 31 живых площадок (XenForo/Discourse/Telegram/RSS), классификатор `omni/abuse.py` раскладывает записи по тегам `ключи / промо / триалы / сливы / софт / продажа / новости`. В панели — отдельная вкладка (`?view=abuse`), в боте — `/abuse [тег]`.
- **авто-добавление**: `omni/discover.py` ищет фиды на уже подключённых хостах и каталогах, проверяет их теми же адаптерами и сохраняет только те, что реально отдали записи. В панели — кнопка «авто-добавление», в API — `POST /api/discover?hosts=40&add=25`.
- **новости**: любые RSS/Atom/JSON-ленты (редакции, релизы, сообщества) подключаются как обычный источник или через `/add <url>` в боте — отдельной схемы не требуется.
### Telegram-бот
```bash
set OMNI_BOT_TOKEN=<токен BotFather>
set OMNI_BOT_ALLOW=<твой numeric id>
python -m omni.bot
```
Команды: `/new [N]`, `/abuse [тег]`, `/find <слово>`, `/src <слово>`, `/run <категория>`, `/add <url>`, `/stats`. Бот работает на той же базе и том же реестре, что панель; отвечает только id из `OMNI_BOT_ALLOW`.

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}

237
omni/abuse_sources.py Normal file
View File

@@ -0,0 +1,237 @@
"""Абузы: форумы, каналы и площадки с промо/сливами/триалами/ключами.
Сгенерировано из реестров парсера абузов; адаптер выведен из движка сайта,
каждая строка проходит реальную проверку fetch перед включением.
"""
def A(name, category, kind, url, adapter, tags=(), limit=20):
return {"name": name, "category": category, "kind": kind, "url": url,
"adapter": adapter, "tags": tuple(tags), "limit": limit, "detail": False, "extra": {}}
ABUSE = [
# ---- abuse (222) ----
A("ab_lolz_live", "abuse", "html", "https://lolz.live/tags/rassylka/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_crackingx_com", "abuse", "html", "https://crackingx.com/forums/5/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_xreactor_org", "abuse", "html", "https://xreactor.org/forums/combolists.152/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_crdpro_cc", "abuse", "html", "https://crdpro.cc/tags/tools/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_trafficultras_com", "abuse", "html", "https://trafficultras.com/forum/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_blackhatworld_com", "abuse", "html", "https://www.blackhatworld.com/seo/up-to-date-list-of-specialized-software-for-telegram.1642970/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_addset_ru", "abuse", "html", "https://addset.ru/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_fb_killa_pro", "abuse", "html", "https://fb-killa.pro/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_forum_x_com", "abuse", "html", "https://forum-x.com/forum/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_afflift_com", "abuse", "html", "https://afflift.com/f/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_cracked_io", "abuse", "html", "https://cracked.io/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_altenens_is", "abuse", "html", "https://altenens.is/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_cardvilla_cc", "abuse", "html", "https://cardvilla.cc/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_cybercarders_eu", "abuse", "html", "https://cybercarders.eu/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_shadowcarders_com", "abuse", "html", "https://shadowcarders.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_carders_biz", "abuse", "html", "https://carders.biz/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_ascarding_net", "abuse", "html", "https://ascarding.net/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_xakerplus_com", "abuse", "html", "https://xakerplus.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_lozerix_com", "abuse", "html", "https://lozerix.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_zhyk_org", "abuse", "html", "https://zhyk.org/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_zenno_club", "abuse", "html", "https://zenno.club/discussion/tags/arbitrazh-trafika/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_partnerkin_com", "abuse", "html", "https://partnerkin.com/blog/articles", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_afftimes_com", "abuse", "html", "https://afftimes.com/articles/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_cpa_rip", "abuse", "html", "https://cpa.rip/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_cpa_live", "abuse", "html", "https://cpa.live/articles/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_gdetraffic_com", "abuse", "html", "https://gdetraffic.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_affy_group", "abuse", "html", "https://affy.group/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_conversion_im", "abuse", "html", "https://conversion.im/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_warriorforum_com", "abuse", "html", "https://www.warriorforum.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_forums_digitalpoint_com", "abuse", "html", "https://forums.digitalpoint.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_cpaelites_com", "abuse", "html", "https://www.cpaelites.com/forum/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_hackforums_net", "abuse", "html", "https://hackforums.net/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_cracking_org", "abuse", "html", "https://cracking.org/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_crackingsoul_com", "abuse", "html", "https://crackingsoul.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_tele_sender_com", "abuse", "html", "https://tele-sender.com/blog/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_crmchat_ai", "abuse", "html", "https://crmchat.ai/blog/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_teleboost_app", "abuse", "html", "https://teleboost.app/blog/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_reddit_com", "abuse", "html", "https://www.reddit.com/r/TelegramBots/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_lzt_market", "abuse", "html", "https://lzt.market/telegram/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_sourceforge_net", "abuse", "html", "https://sourceforge.net/directory/telegram-bots/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_t_me", "abuse", "html", "https://t.me/s/Abuz_ai_soft", "tg_web", ('abuse', 'abuse'), limit=20),
A("ab_habr_com", "abuse", "html", "https://habr.com/ru/search/?q=telegram+парсинг+софт&target_type=posts&order=date", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_teletype_in", "abuse", "html", "https://teletype.in/search?query=telegram+софт", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_youtube_com", "abuse", "html", "https://www.youtube.com/results?search_query=telegram+marketing+software+2026&sp=CAISBAgCEAE%253D", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_medium_com", "abuse", "html", "https://medium.com/search?q=telegram+bot+automation+2026", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_tgstat_ru", "abuse", "html", "https://tgstat.ru/ratings/chats/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_carder_su", "abuse", "html", "https://carder.su/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_procarders_com", "abuse", "html", "https://procarders.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_darkpro_net", "abuse", "html", "https://darkpro.net/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_tuts4you_com", "abuse", "html", "https://tuts4you.com/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_forum_tuts4you_com", "abuse", "html", "https://forum.tuts4you.com/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_forum_exetools_com", "abuse", "html", "https://forum.exetools.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_tlgrm_eu", "abuse", "html", "https://tlgrm.eu/channels", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_telemetr_me", "abuse", "html", "https://telemetr.me/channels", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_cpamafia_pro", "abuse", "html", "https://cpamafia.pro/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_cpamafia_top", "abuse", "html", "https://cpamafia.top/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_m_vk_com", "abuse", "html", "https://m.vk.com/search?c%5Bq%5D=продвижение+telegram+софт&c%5Bsection%5D=auto", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_forobeta_com", "abuse", "html", "https://forobeta.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_forexfactory_com", "abuse", "html", "https://www.forexfactory.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_gpwa_org", "abuse", "html", "https://www.gpwa.org/forum/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_casinomeister_com", "abuse", "html", "https://www.casinomeister.com/forums/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_forums_babypips_com", "abuse", "html", "https://forums.babypips.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_github_com", "abuse", "html", "https://github.com/ofmtools/awesome-ofm-tools", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_searchengines_guru", "abuse", "html", "https://searchengines.guru/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_hardforum_com", "abuse", "html", "https://hardforum.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_bitcointalk_org", "abuse", "html", "https://bitcointalk.org/index.php?board=238.0", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_sendpulse_com", "abuse", "html", "https://sendpulse.com/blog/telegram-marketing-tools", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_softwaresuggest_com", "abuse", "html", "https://www.softwaresuggest.com/telegram-marketing-software", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_tacticaldatamanager_com", "abuse", "html", "https://tacticaldatamanager.com/blog", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_statiko_io", "abuse", "html", "https://statiko.io/blog/telegram-anti-spam-bot", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_tghero_pro", "abuse", "html", "https://tghero.pro/telegram-marketing-tools/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_enreach_ai", "abuse", "html", "https://www.enreach.ai/insights/best-telegram-outreach-tools", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_nulledbb_com", "abuse", "html", "https://nulledbb.com/syndication.php?limit=50", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_cracking_forums_com", "abuse", "html", "http://www.cracking-forums.com/syndication.php?limit=50", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_eleaks_to", "abuse", "html", "https://eleaks.to/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_crackiansleaks_com", "abuse", "html", "https://crackiansleaks.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_niflheim_world", "abuse", "html", "https://niflheim.world/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_nodeloc_com", "abuse", "json", "https://www.nodeloc.com/latest.json", "discourse", ('abuse', 'abuse'), limit=20),
A("ab_v2ex_com", "abuse", "json", "https://www.v2ex.com/api/topics/hot.json", "generic_json", ('abuse', 'abuse'), limit=20),
A("ab_hostloc_com", "abuse", "rss", "https://hostloc.com/forum.php?mod=rss", "rss", ('abuse', 'abuse'), limit=20),
A("ab_findaicredits_com", "abuse", "html", "https://www.findaicredits.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_bhf_io", "abuse", "html", "https://bhf.io/en-gb", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_leak_sx", "abuse", "html", "https://leak.sx/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_forum_hashkiller_io", "abuse", "html", "https://forum.hashkiller.io/index.php", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_linux_do", "abuse", "json", "https://linux.do/latest.json", "discourse", ('abuse', 'abuse'), limit=20),
A("ab_xss_is", "abuse", "html", "https://xss.is/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_qqshill_com", "abuse", "html", "https://qqshill.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_telegrammassdm_com", "abuse", "html", "https://telegrammassdm.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_mavibot_ai", "abuse", "html", "https://mavibot.ai/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_kenzabyte_com", "abuse", "html", "https://www.kenzabyte.com/telegram-auto-account-creator-version-history/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_ltesocks_io", "abuse", "html", "https://ltesocks.io/blog/specific-examples-of-mobile-proxies-en-en/top-programs-for-automatic-telegram-account-registration/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_dicloak_com", "abuse", "html", "https://dicloak.com/blog-detail/the-ultimate-guide-to-using-a-telegram-bulk-account-creator-in-2026", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_multilogin_com", "abuse", "html", "https://multilogin.com/multiple-accounting/create-multiple-telegram-accounts/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_quackr_io", "abuse", "html", "https://quackr.io/blog/create-unlimited-telegram-accounts", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_genfarmer_com", "abuse", "html", "https://genfarmer.com/tool-for-mass-telegram-registration-and-automatic-telegram-account-nurturing-en/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_like_tg", "abuse", "html", "https://www.like.tg/blog/telegram-bulk-registration-tool-best-two-thousand-twenty-six-stable-solution-captcha-guide", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_dexpose_io", "abuse", "html", "https://www.dexpose.io/dark-web-forums/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_cybelangel_com", "abuse", "html", "https://cybelangel.com/blog/dark-web-telegram-channels-attackers-use-most-2026/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_proxyscrape_com", "abuse", "html", "https://proxyscrape.com/online-proxy-checker", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_localtonet_com", "abuse", "html", "https://localtonet.com/tools/proxy-check", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_proxy_seller_com", "abuse", "html", "https://proxy-seller.com/tools/proxy-checker/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_gologin_com", "abuse", "html", "https://gologin.com/proxy-checker/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_proxyway_com", "abuse", "html", "https://proxyway.com/tools/free-online-proxy-checker", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_chromewebstore_google_com", "abuse", "html", "https://chromewebstore.google.com/detail/tg-member-extractor/gphncajfmfojhafagcemjjacbekahdoh", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_apify_com", "abuse", "html", "https://apify.com/agentx/telegram-member-scraper", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_energent_ai", "abuse", "html", "https://www.energent.ai/use-cases/en/telegram-scraper", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_thunderbit_com", "abuse", "html", "https://thunderbit.com/template/telegram-scraper", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_axiom_ai", "abuse", "html", "https://axiom.ai/scrape/telegram/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_flozic_ai", "abuse", "html", "https://www.flozic.ai/agents/conversational/telegram-bot", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_manybot_io", "abuse", "html", "https://manybot.io/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_botmother_com", "abuse", "html", "https://botmother.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_chatloom_app", "abuse", "html", "https://chatloom.app/en/channels/telegram", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_chatbotbuilder_net", "abuse", "html", "https://chatbotbuilder.net/telegram", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_saascounter_com", "abuse", "html", "https://www.saascounter.com/telegram-marketing-software", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_schedulala_com", "abuse", "html", "https://schedulala.com/blog/telegram-marketing-guide", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_technologycounter_com", "abuse", "html", "https://technologycounter.com/telegram-marketing-software", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_omisoft_net", "abuse", "html", "https://omisoft.net/blog/telegram-mini-apps-for-business/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_adsly_pro", "abuse", "html", "https://adsly.pro/guides/managed-service-vs-saas-cost/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_optimum_web_com", "abuse", "html", "https://www.optimum-web.com/blog/telegram-ai-bot-for-business-2026/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_telegramsender_com", "abuse", "html", "https://telegramsender.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_xreacher_com", "abuse", "html", "https://xreacher.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_zapier_com", "abuse", "html", "https://zapier.com/apps/outreach/integrations/telegram", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_n8n_io", "abuse", "html", "https://n8n.io/workflows/13732-run-email-outreach-campaigns-from-telegram-with-gmail-and-google-sheets/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_albato_com", "abuse", "html", "https://albato.com/connect/outreach-with-telegramprivate", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_entergram_com", "abuse", "html", "https://www.entergram.com/blog/telegram-lead-management-complete-guide", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_breakcold_com", "abuse", "html", "https://www.breakcold.com/blog/crm-integration-with-telegram", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_kommo_com", "abuse", "html", "https://www.kommo.com/integrations/telegram/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_gramsales_com", "abuse", "html", "https://gramsales.com/blog/best-crm-tools-for-telegram", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_synchubai_com", "abuse", "html", "https://synchubai.com/blog/telegram-messaging-crm/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_messaggio_com", "abuse", "html", "https://messaggio.com/messaging-in-telegram/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_vc_ru", "abuse", "html", "https://vc.ru/id5905098/2879981-deskgram-2-moschnyj-soft-dlja-telegram-spama", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_unisender_com", "abuse", "html", "https://www.unisender.com/ru/blog/servisy-dlya-rassylki-v-telegram/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_socradar_io", "abuse", "html", "https://socradar.io/blog/top-10-deep-web-and-dark-web-forums/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_nordlayer_com", "abuse", "html", "https://nordlayer.com/blog/top-dark-web-telegram-groups-and-channels/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_play_google_com", "abuse", "html", "https://play.google.com/store/apps/details?id=com.boardgames.bd.mtproto", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_proxyempire_io", "abuse", "html", "https://proxyempire.io/proxy-checker/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_proxybros_com", "abuse", "html", "https://proxybros.com/proxies/proxy-for-telegram/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_echo_tg", "abuse", "html", "https://echo.tg/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_adfasten_com", "abuse", "html", "https://www.adfasten.com/telegram-marketing", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_chatimize_com", "abuse", "html", "https://chatimize.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_tg_on_com", "abuse", "html", "https://tg-on.com/articles/top-programm-rassylka-telegram.html", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_botract_com", "abuse", "html", "https://www.botract.com/blog/how-to-choose-telegram-bot-builder", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_respond_io", "abuse", "html", "https://respond.io/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_sendgecko_io", "abuse", "html", "https://sendgecko.io/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_community_thermaltake_com", "abuse", "html", "https://community.thermaltake.com/index.php?/topic/692572-the-ultimate-2026-guide-to-mass-telegram-account-creation-zero-bans/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_discuss_flarum_org", "abuse", "html", "https://discuss.flarum.org/d/39670-what-forum-spam-looks-like-in-2026-summer-edition", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_texapi_net", "abuse", "html", "https://texapi.net/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_pixelscan_net", "abuse", "html", "https://pixelscan.net/blog/best-telegram-batch-registration-software-in-2026/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_pipedream_com", "abuse", "html", "https://pipedream.com/docs", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_p1sms_kz", "abuse", "html", "https://p1sms.kz/telegram-rassylki", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_nodemaven_com", "abuse", "html", "https://nodemaven.com/tools/proxy-bandwidth-checker/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_infatica_io", "abuse", "html", "https://infatica.io/proxy-checker/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_flowxo_com", "abuse", "html", "https://flowxo.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_pas7_com_ua", "abuse", "html", "https://pas7.com.ua/blog/en/telegram-bot-development-cost-2026", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_telelog_org", "abuse", "html", "https://telelog.org/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_help_nuelink_com", "abuse", "html", "https://help.nuelink.com/article/228-best-telegram-scheduling-tools", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_wooxy_com", "abuse", "html", "https://wooxy.com/telegram", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_chatbots_studio", "abuse", "html", "https://chatbots.studio/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_app_flow_ai", "abuse", "html", "https://app.flow.ai", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_kelacyber_com", "abuse", "html", "https://www.kelacyber.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_infobip_com", "abuse", "html", "https://www.infobip.com/blog/best-marketing-automation-platforms", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_atompark_com", "abuse", "html", "https://www.atompark.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_foresiet_com", "abuse", "html", "https://foresiet.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_vovsoft_com", "abuse", "html", "https://vovsoft.com/software/proxy-checker/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_pocodot_ai", "abuse", "html", "https://pocodot.ai/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_richads_com", "abuse", "html", "https://richads.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_proxyline_net", "abuse", "html", "https://proxyline.net/proxy-checker", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_pypi_org", "abuse", "html", "https://pypi.org/project/TGSessionsConverter/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_opentele_readthedocs_io", "abuse", "html", "https://opentele.readthedocs.io/en/latest/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_growity_ai", "abuse", "html", "https://growity.ai/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_pantheon_run", "abuse", "html", "https://pantheon.run/learn/best-telegram-crm", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_platform_openai_com", "abuse", "html", "https://platform.openai.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_dev_to", "abuse", "html", "https://dev.to/scofieldidehen/if-you-use-telegram-in-2026-read-this-now-4770", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_codester_com", "abuse", "html", "https://www.codester.com/items/54880/telegram-marketing-tools-bot", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_greasyfork_org", "abuse", "html", "https://greasyfork.org/en/scripts/538176-telegram-scraper-menu-commands-v2-3-3-human-like-defaults", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_medevel_com", "abuse", "html", "https://medevel.com/telegram-scraper-20/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_blog_checkpoint_com", "abuse", "html", "https://blog.checkpoint.com/research/telegrams-crackdown-in-2026-and-why-cyber-criminals-are-still-winning/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_make_com", "abuse", "html", "https://www.make.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_gopulsar_io", "abuse", "html", "https://gopulsar.io/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_tgproxy_io", "abuse", "html", "https://tgproxy.io/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_texsender_net", "abuse", "html", "https://texsender.net/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_communicat_o_com", "abuse", "html", "https://www.communicat-o.com/crm-integration-for-telegram/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_telegramscraper_shop", "abuse", "html", "https://telegramscraper.shop/blog/best-telegram-scraper-2026", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_proxyhorizon_com", "abuse", "html", "https://www.proxyhorizon.com/blog/proxies-for-telegram", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_ctiacademy_io", "abuse", "html", "https://ctiacademy.io/blog/telegram-cybercrime-marketplace", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_tgsender_wadesk_io", "abuse", "html", "https://tgsender.wadesk.io/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_coronium_io", "abuse", "html", "https://www.coronium.io/partners/software/telegram-prime-review", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_stackoverflow_com", "abuse", "html", "https://stackoverflow.com/questions/61657596/telethon-first-steps-in-python", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_tgninja_com", "abuse", "html", "https://tgninja.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_malware_news", "abuse", "html", "https://malware.news/t/top-dark-web-telegram-groups-channels-2026/104664", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_telega_io", "abuse", "html", "https://telega.io/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_fiverr_com", "abuse", "html", "https://www.fiverr.com/gigs/telegram-invite", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_quora_com", "abuse", "html", "https://www.quora.com/What-bulk-messaging-software-is-best-for-Telegram-Line-and-WhatsApp", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_bolster_ai", "abuse", "html", "https://bolster.ai/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_capterra_com", "abuse", "html", "https://www.capterra.com/p/180347/Telegram/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_manychat_com", "abuse", "html", "https://manychat.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_outpost24_com", "abuse", "html", "https://outpost24.com/blog/carding-ecosystem-fall-financial-cybercrime/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_telegramvpn_org", "abuse", "html", "https://telegramvpn.org/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_iricrm_com", "abuse", "html", "https://iricrm.com/telegram-crm/?lang=en", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_deepstrike_io", "abuse", "html", "https://deepstrike.io/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_stealthmole_com", "abuse", "html", "https://www.stealthmole.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_f6s_com", "abuse", "html", "https://www.f6s.com/software/category/no-code-telegram-bot-builder", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_exploit_in", "abuse", "html", "https://exploit.in/challenge/?return=/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_checkpoint_com", "abuse", "html", "https://www.checkpoint.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_seochao_ru", "abuse", "html", "https://seochao.ru/forums/arbitrazh-trafika.85/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_blog_invitemember_com", "abuse", "html", "https://blog.invitemember.com/5-essential-telegram-bots-for-creators/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_tracexlabs_com", "abuse", "html", "https://tracexlabs.com/knowledge-base/top-10-deep-web-and-dark-web-forums.html", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_proxies_sx", "abuse", "html", "https://www.proxies.sx/partners/automation-platforms/telegram-expert", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_checkproxy_org", "abuse", "html", "https://checkproxy.org/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_automatedclientacquisition_com", "abuse", "html", "https://www.automatedclientacquisition.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_stormycloud_org", "abuse", "html", "https://stormycloud.org/mtproto/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_bot_market_net", "abuse", "html", "https://bot-market.net/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_telebotbuilder_com", "abuse", "html", "https://telebotbuilder.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_cavyro_com", "abuse", "html", "https://cavyro.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_positechglobal_com", "abuse", "html", "https://positechglobal.com/en/research/analytics/cybercriminal-market-in-telegram/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_trypromptlab_com", "abuse", "html", "https://trypromptlab.com/blog/best-crm-with-telegram-integration", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_flowcastle_ai", "abuse", "html", "https://flowcastle.ai/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_telegramsoftware_pro", "abuse", "html", "https://telegramsoftware.pro/blog/telegram-session-to-tdata-converter/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_darkbase_cc", "abuse", "html", "https://darkbase.cc/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_nocturne_cards", "abuse", "html", "https://nocturne.cards/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_ads_telegram_org", "abuse", "html", "https://ads.telegram.org/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_teleform_io", "abuse", "html", "https://www.teleform.io/", "html_listing", ('abuse', 'abuse'), limit=20),
]

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()

164
omni/discover.py Normal file
View File

@@ -0,0 +1,164 @@
"""Автодобавление источников: находит фиды по домашним страницам и каталогам.
Логика: берём затравки (хосты уже подключённых источников + известные каталоги
сообществ), вытаскиваем ссылки на фиды (link rel=alternate, /feed, /rss,
/latest.json, подфорумы), прогоняем каждый кандидат реальным адаптером и
добавляем только те, что отдали записи. Всё добавленное помечается auto и
попадает в панель как обычный источник.
"""
from __future__ import annotations
import json
import re
import time
import urllib.parse
from concurrent.futures import ThreadPoolExecutor
from bs4 import BeautifulSoup
from . import feeds, net, source_state, sources
FEED_PATHS = ("/feed", "/rss", "/rss.xml", "/atom.xml", "/feed.xml", "/index.xml",
"/latest.json", "/?feed=rss2", "/feed/", "/blog/feed")
SEED_CATALOGS = [
# каталоги сообществ и подфорумов, откуда удобно цеплять новые фиды
"https://www.reddit.com/r/LocalLLaMA/about.json",
"https://news.ycombinator.com/",
"https://lobste.rs/",
]
UA = getattr(feeds, "UA", {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"})
def _hosts():
seen = {}
for s in sources.all_sources():
try:
h = urllib.parse.urlsplit(s.url).netloc.lower()
except Exception:
continue
if not h or h in seen:
continue
seen[h] = s.category
return seen
def _feed_links(html, base):
out = []
soup = BeautifulSoup(html, "lxml")
for ln in soup.find_all("link", href=True):
t = (ln.get("type") or "").lower()
rel = " ".join(ln.get("rel") or []).lower()
if "alternate" in rel and ("rss" in t or "atom" in t or "json" in t):
out.append(urllib.parse.urljoin(base, ln["href"]))
for a in soup.find_all("a", href=True):
href = str(a["href"])
if re.search(r"(/feed|/rss|\.rss|atom\.xml|/latest\.json)$", href):
out.append(urllib.parse.urljoin(base, href))
return out
def candidates(limit_hosts=60, extra_seeds=()):
hosts = _hosts()
seeds = []
for h, cat in list(hosts.items())[:limit_hosts]:
seeds.append(("https://%s/" % h, cat))
for u in list(extra_seeds) + SEED_CATALOGS:
seeds.append((u, "auto"))
return seeds
def probe(url, category="auto", limit=6):
"""Проверка одного URL реальным адаптером. Возвращает (ok, n, adapter)."""
low = url.lower()
if "t.me/s/" in low:
ad, kind = "tg_web", "html"
elif low.endswith(".json") or "/latest.json" in low:
ad, kind = ("discourse", "json") if "/latest.json" in low else ("generic_json", "json")
elif any(x in low for x in (".rss", "/rss", "/feed", "atom.xml", "feed.xml", "index.xml")):
ad, kind = "rss", "rss"
else:
ad, kind = "html_listing", "html"
src = sources.Source(name="probe", category=category, kind=kind, url=url,
adapter=ad, tags=("auto",), limit=limit, detail=False, extra={})
fn = feeds.ADAPTERS.get(ad)
if not fn:
return False, 0, ad
try:
items = fn(src, limit) or []
except Exception:
return False, 0, ad
return len(items) > 0, len(items), ad
def discover(limit_hosts=40, workers=8, max_add=40, dry=False):
seeds = candidates(limit_hosts=limit_hosts)
found = []
def scan(seed):
base, cat = seed
try:
r = net.get(base, headers=UA)
if r.status != 200:
return []
except Exception:
return []
links = _feed_links(r.text[:400000], base)
dom = urllib.parse.urlsplit(base)
for p in FEED_PATHS:
links.append("%s://%s%s" % (dom.scheme, dom.netloc, p))
# подфорумы Discourse/Reddit
if "reddit.com" in base:
for sub in ("LocalLLaMA", "MachineLearning", "mcp", "selfhosted"):
links.append("https://www.reddit.com/r/%s/.rss" % sub)
return [(u, cat) for u in dict.fromkeys(links)]
with ThreadPoolExecutor(max_workers=workers) as ex:
for res in ex.map(scan, seeds):
found.extend(res)
print("кандидатов найдено:", len(found), flush=True)
known = {s.url for s in sources.all_sources()}
found = [(u, c) for u, c in found if u not in known]
print("после дедупа:", len(found), flush=True)
def check(pair):
url, cat = pair
ok, n, ad = probe(url, cat)
return url, cat, ok, n, ad
# только машинные фиды: html-листинги на автодобавлении дают мусор
best = {}
with ThreadPoolExecutor(max_workers=workers) as ex:
for url, cat, ok, n, ad in ex.map(check, found):
if not ok or ad == "html_listing" or n < 3:
continue
dom = urllib.parse.urlsplit(url).netloc.lower()
key = (dom, ad)
if key not in best or n > best[key][2]:
best[key] = (url, cat, n, ad)
added = []
for (dom, ad), (url, cat, n, _ad) in sorted(best.items(), key=lambda kv: -kv[1][2]):
name = "auto_" + re.sub(r"[^a-z0-9]+", "_", dom).strip("_")[:34]
if any(a["name"] == name for a in added):
continue
rec = {"name": name, "category": cat if cat in ("abuse", "forum", "security") else "community",
"kind": "rss" if ad == "rss" else ("json" if ad in ("generic_json", "discourse") else "html"),
"url": url, "adapter": ad, "tags": ["auto"], "lim": 15, "detail": False}
added.append(rec)
if len(added) >= max_add:
break
print("живых фидов:", len(added), flush=True)
if not dry:
for rec in added:
source_state.add_custom(rec["name"], rec["category"], rec["url"], kind=rec["kind"],
adapter=rec["adapter"], tags=rec["tags"], limit=rec["lim"],
detail=rec["detail"])
return {"found": len(found), "alive": len(added), "added": added if dry else [a["name"] for a in added]}
if __name__ == "__main__":
import sys
lim = int(sys.argv[1]) if len(sys.argv) > 1 else 30
res = discover(limit_hosts=lim)
print(json.dumps({"found": res["found"], "alive": res["alive"], "added": res["added"][:20]},
ensure_ascii=False, indent=1))

View File

@@ -336,7 +336,8 @@ def _xenforo_latest(src, limit):
if len(title) < 12 or href.rstrip("/").endswith("/threads"):
continue
if not href.startswith("http"):
href = "https://www.blackhatworld.com" + href
parts = urllib.parse.urlsplit(src.url)
href = "%s://%s%s" % (parts.scheme, parts.netloc, href)
out.append(_item(src, "thread", href, title))
if len(out) >= limit:
break

View File

@@ -190,6 +190,42 @@ class Handler(BaseHTTPRequestHandler):
"categories": cats,
})
if path == "/api/runs":
try:
limit = max(1, min(500, int((q.get("limit") or ["80"])[0])))
except ValueError:
limit = 80
import time as _t
con = store._con() if hasattr(store, "_con") else None
rows = []
try:
import sqlite3 as _s3
from .config import DB as _DB
db = _s3.connect(str(_DB), timeout=30)
db.row_factory = _s3.Row
for r in db.execute("SELECT id, ts, provider, source, query, got, kept, status, bytes "
"FROM runs ORDER BY id DESC LIMIT ?", (limit,)):
d = dict(r)
ts = d.get("ts") or 0
d["when"] = _t.strftime("%Y-%m-%d %H:%M", _t.localtime(ts)) if ts else ""
rows.append(d)
total = db.execute("SELECT count(*) FROM runs").fetchone()[0]
db.close()
except Exception as e:
return self._json({"error": "runs: %s" % e})
return self._json({"items": rows, "total": total, "shown": len(rows)})
if path == "/api/abuse":
from . import abuse as _ab
tag = (q.get("tag") or [""])[0].strip() or None
if tag in ("all", "все"):
tag = None
try:
lim = max(1, min(200, int((q.get("limit") or ["40"])[0])))
except ValueError:
lim = 40
return self._json(_ab.scan(limit=lim, tag=tag))
if path == "/api/sources":
cat = (q.get("category") or [None])[0]
query = (q.get("q") or [""])[0].strip().lower()
@@ -259,6 +295,15 @@ class Handler(BaseHTTPRequestHandler):
except Exception:
body = {}
if u.path == "/api/discover":
from . import discover as _d
q = urllib.parse.parse_qs(u.query)
hosts = max(5, min(120, int((q.get("hosts") or ["40"])[0])))
add = max(1, min(60, int((q.get("add") or ["25"])[0])))
dry = (q.get("dry") or ["0"])[0] not in ("0", "false", "")
res = _d.discover(limit_hosts=hosts, workers=8, max_add=add, dry=dry)
return self._json(res)
if u.path == "/api/feed":
jid = _jid("feed")
_job(jid, state="running")

View File

@@ -12,6 +12,8 @@ from dataclasses import dataclass, field
from .sources_ext import EXTRA
from .sources_ext2 import EXTRA2
from .sources_ext3 import EXTRA3
from .sources_abuse import ABUSE_LIVE
@dataclass
@@ -188,7 +190,7 @@ def _dedup(rows):
return out
SOURCES = _dedup(SOURCES_BASE + [Source(**d) for d in EXTRA] + [Source(**d) for d in EXTRA2])
SOURCES = _dedup(SOURCES_BASE + [Source(**d) for d in EXTRA] + [Source(**d) for d in EXTRA2] + [Source(**d) for d in EXTRA3] + [Source(**d) for d in ABUSE_LIVE])
BY_NAME = {s.name: s for s in SOURCES}
CATEGORIES = sorted({s.category for s in SOURCES})

45
omni/sources_abuse.py Normal file
View File

@@ -0,0 +1,45 @@
"""Абузы (живые): форумы, каналы и площадки с промо/сливами/софтом.
Только структурные движки (RSS/Discourse/XenForo/Telegram) и HTML-форумы,
у которых реально нашлись темы (>=5 ссылок на треды), а не навигация.
"""
def A(name, category, kind, url, adapter, tags=(), limit=20):
return {"name": name, "category": category, "kind": kind, "url": url,
"adapter": adapter, "tags": tuple(tags), "limit": limit, "detail": False, "extra": {}}
ABUSE_LIVE = [
A("ab_lolz_live", "abuse", "html", "https://lolz.live/tags/rassylka/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_crackingx_com", "abuse", "html", "https://crackingx.com/forums/5/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_xreactor_org", "abuse", "html", "https://xreactor.org/forums/combolists.152/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_crdpro_cc", "abuse", "html", "https://crdpro.cc/tags/tools/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_blackhatworld_com", "abuse", "html", "https://www.blackhatworld.com/seo/up-to-date-list-of-specialized-software-for-telegram.1642970/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_cracked_io", "abuse", "html", "https://cracked.io/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_t_me", "abuse", "html", "https://t.me/s/Abuz_ai_soft", "tg_web", ('abuse', 'abuse'), limit=20),
A("ab_tuts4you_com", "abuse", "html", "https://tuts4you.com/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_forum_tuts4you_com", "abuse", "html", "https://forum.tuts4you.com/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_nodeloc_com", "abuse", "json", "https://www.nodeloc.com/latest.json", "discourse", ('abuse', 'abuse'), limit=20),
A("ab_v2ex_com", "abuse", "json", "https://www.v2ex.com/api/topics/hot.json", "generic_json", ('abuse', 'abuse'), limit=20),
A("ab_hostloc_com", "abuse", "rss", "https://hostloc.com/forum.php?mod=rss", "rss", ('abuse', 'abuse'), limit=20),
A("ab_linux_do", "abuse", "json", "https://linux.do/latest.json", "discourse", ('abuse', 'abuse'), limit=20),
A("ab_xss_is", "abuse", "html", "https://xss.is/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_exploit_in", "abuse", "html", "https://exploit.in/challenge/?return=/", "xenforo_latest", ('abuse', 'abuse'), limit=20),
A("ab_fb_killa_pro", "abuse", "html", "https://fb-killa.pro/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_afflift_com", "abuse", "html", "https://afflift.com/f/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_altenens_is", "abuse", "html", "https://altenens.is/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_carders_biz", "abuse", "html", "https://carders.biz/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_ascarding_net", "abuse", "html", "https://ascarding.net/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_zhyk_org", "abuse", "html", "https://zhyk.org/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_zenno_club", "abuse", "html", "https://zenno.club/discussion/tags/arbitrazh-trafika/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_carder_su", "abuse", "html", "https://carder.su/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_cpamafia_pro", "abuse", "html", "https://cpamafia.pro/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_forexfactory_com", "abuse", "html", "https://www.forexfactory.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_casinomeister_com", "abuse", "html", "https://www.casinomeister.com/forums/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_hardforum_com", "abuse", "html", "https://hardforum.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_niflheim_world", "abuse", "html", "https://niflheim.world/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_telegrammassdm_com", "abuse", "html", "https://telegrammassdm.com/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_echo_tg", "abuse", "html", "https://echo.tg/", "html_listing", ('abuse', 'abuse'), limit=20),
A("ab_seochao_ru", "abuse", "html", "https://seochao.ru/forums/arbitrazh-trafika.85/", "html_listing", ('abuse', 'abuse'), limit=20),
]

112
omni/sources_ext3.py Normal file
View File

@@ -0,0 +1,112 @@
"""Волна 4 (только живые после реального fetch): релизы репозиториев, рассылки,
Discourse-форумы, подреддиты, CN/JP блоги, HF-задачи. Каждая строка проверена: items > 0."""
def S(name, category, kind, url, adapter="rss", tags=(), limit=20, detail=False, extra=None):
return {"name": name, "category": category, "kind": kind, "url": url, "adapter": adapter,
"tags": tuple(tags), "limit": limit, "detail": detail, "extra": extra or {}}
EXTRA3 = [
# ---- release ----
S("gh_rel_ollama", "release", "atom", "https://github.com/ollama/ollama/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_llamacpp", "release", "atom", "https://github.com/ggml-org/llama.cpp/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_vllm", "release", "atom", "https://github.com/vllm-project/vllm/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_comfyui", "release", "atom", "https://github.com/comfyanonymous/ComfyUI/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_dify", "release", "atom", "https://github.com/langgenius/dify/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_n8n", "release", "atom", "https://github.com/n8n-io/n8n/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_langchain", "release", "atom", "https://github.com/langchain-ai/langchain/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_autogen", "release", "atom", "https://github.com/microsoft/autogen/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_litellm", "release", "atom", "https://github.com/BerriAI/litellm/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_openwebui", "release", "atom", "https://github.com/open-webui/open-webui/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_ragflow", "release", "atom", "https://github.com/infiniflow/ragflow/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_browseruse", "release", "atom", "https://github.com/browser-use/browser-use/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_playwrightmcp", "release", "atom", "https://github.com/microsoft/playwright-mcp/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_mcpservers", "release", "atom", "https://github.com/modelcontextprotocol/servers/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_dspy", "release", "atom", "https://github.com/stanfordnlp/dspy/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_transformers", "release", "atom", "https://github.com/huggingface/transformers/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_trl", "release", "atom", "https://github.com/huggingface/trl/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_sglang", "release", "atom", "https://github.com/sgl-project/sglang/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_unsloth", "release", "atom", "https://github.com/unslothai/unsloth/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_axolotl", "release", "atom", "https://github.com/axolotl-ai-cloud/axolotl/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_markitdown", "release", "atom", "https://github.com/microsoft/markitdown/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_openai_sdk", "release", "atom", "https://github.com/openai/openai-python/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_anthropic_sdk", "release", "atom", "https://github.com/anthropics/anthropic-sdk-python/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_gemini_sdk", "release", "atom", "https://github.com/google-gemini/generative-ai-python/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_crewai", "release", "atom", "https://github.com/crewAIInc/crewAI/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_agno", "release", "atom", "https://github.com/agno-agi/agno/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_livekit_agents", "release", "atom", "https://github.com/livekit/agents/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_mem0", "release", "atom", "https://github.com/mem0ai/mem0/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_qdrant", "release", "atom", "https://github.com/qdrant/qdrant/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
S("gh_rel_chroma", "release", "atom", "https://github.com/chroma-core/chroma/releases.atom", "rss", ('release', 'github', 'update'), limit=10, detail=False),
# ---- community ----
S("lesswrong", "community", "rss", "https://www.lesswrong.com/feed.xml?view=curated-rss", "rss", ('ai', 'alignment'), limit=20, detail=False),
S("alignmentforum", "community", "rss", "https://www.alignmentforum.org/feed.xml?view=curated-rss", "rss", ('ai', 'alignment'), limit=20, detail=False),
S("gwern", "community", "rss", "https://gwern.net/feed", "rss", ('ai', 'longform'), limit=20, detail=False),
S("importai", "community", "rss", "https://importai.substack.com/feed", "rss", ('ai', 'newsletter'), limit=20, detail=False),
S("thezvi", "community", "rss", "https://thezvi.substack.com/feed", "rss", ('ai', 'newsletter'), limit=20, detail=False),
S("latentspace", "community", "rss", "https://www.latent.space/feed", "rss", ('ai', 'podcast'), limit=20, detail=False),
S("aisupremacy", "community", "rss", "https://aisupremacy.substack.com/feed", "rss", ('ai', 'newsletter'), limit=20, detail=False),
S("oneusefulthing", "community", "rss", "https://www.oneusefulthing.org/feed", "rss", ('ai', 'practices'), limit=20, detail=False),
S("chinatalk", "community", "rss", "https://www.chinatalk.media/feed", "rss", ('ai', 'china'), limit=20, detail=False),
S("reddit_claudeai", "community", "rss", "https://old.reddit.com/r/ClaudeAI/.rss", "reddit_listing", ('reddit', 'claude'), limit=20, detail=False),
S("reddit_bard", "community", "rss", "https://old.reddit.com/r/Bard/.rss", "reddit_listing", ('reddit', 'gemini'), limit=20, detail=False),
S("reddit_singularity", "community", "rss", "https://old.reddit.com/r/singularity/.rss", "reddit_listing", ('reddit', 'ai'), limit=20, detail=False),
S("reddit_openai_dev", "community", "rss", "https://old.reddit.com/r/OpenAIDev/.rss", "reddit_listing", ('reddit', 'openai'), limit=20, detail=False),
S("reddit_mcp", "community", "rss", "https://old.reddit.com/r/mcp/.rss", "reddit_listing", ('reddit', 'mcp'), limit=20, detail=False),
S("reddit_ai_agents", "community", "rss", "https://old.reddit.com/r/AI_Agents/.rss", "reddit_listing", ('reddit', 'agents'), limit=20, detail=False),
S("reddit_bugbounty", "community", "rss", "https://old.reddit.com/r/bugbounty/.rss", "reddit_listing", ('reddit', 'bugbounty'), limit=20, detail=False),
S("reddit_netsecstudents", "community", "rss", "https://old.reddit.com/r/netsecstudents/.rss", "reddit_listing", ('reddit', 'security'), limit=20, detail=False),
S("reddit_blueteamsec", "community", "rss", "https://old.reddit.com/r/blueteamsec/.rss", "reddit_listing", ('reddit', 'security'), limit=20, detail=False),
S("reddit_prompteng", "community", "rss", "https://old.reddit.com/r/PromptEngineering/.rss", "reddit_listing", ('reddit', 'prompts'), limit=20, detail=False),
S("reddit_oobabooga", "community", "rss", "https://old.reddit.com/r/Oobabooga/.rss", "reddit_listing", ('reddit', 'llm'), limit=20, detail=False),
S("reddit_unsloth", "community", "rss", "https://old.reddit.com/r/unsloth/.rss", "reddit_listing", ('reddit', 'finetune'), limit=20, detail=False),
S("reddit_mlops", "community", "rss", "https://old.reddit.com/r/mlops/.rss", "reddit_listing", ('reddit', 'mlops'), limit=20, detail=False),
S("reddit_devops2", "community", "rss", "https://old.reddit.com/r/devops/.rss", "reddit_listing", ('reddit', 'devops'), limit=20, detail=False),
S("reddit_homelab2", "community", "rss", "https://old.reddit.com/r/homelab/.rss", "reddit_listing", ('reddit', 'homelab'), limit=20, detail=False),
S("reddit_grok2", "community", "rss", "https://old.reddit.com/r/grok/.rss", "reddit_listing", ('reddit', 'grok'), limit=20, detail=False),
# ---- security ----
S("sysdig", "security", "rss", "https://sysdig.com/feed/", "rss", ('security', 'cloud'), limit=20, detail=False),
S("permiso", "security", "rss", "https://permiso.io/blog/rss.xml", "rss", ('security', 'cloud'), limit=20, detail=False),
S("aquasec", "security", "rss", "https://www.aquasec.com/feed/", "rss", ('security', 'cloud'), limit=20, detail=False),
S("infosecmatter", "security", "rss", "https://www.infosecmatter.com/feed/", "rss", ('security', 'tools'), limit=20, detail=False),
S("cvefeed", "security", "rss", "https://cvefeed.io/rssfeed/latest.xml", "rss", ('security', 'cve'), limit=20, detail=False),
S("graynoise", "security", "rss", "https://www.greynoise.io/blog/rss.xml", "rss", ('security', 'intel'), limit=20, detail=False),
S("recordedfuture", "security", "rss", "https://www.recordedfuture.com/feed", "rss", ('security', 'intel'), limit=20, detail=False),
S("cisa_news", "security", "rss", "https://www.cisa.gov/news.xml", "rss", ('security', 'gov'), limit=20, detail=False),
S("msrc", "security", "rss", "https://api.msrc.microsoft.com/update-guide/rss", "rss", ('security', 'microsoft', 'cve'), limit=20, detail=False),
# ---- forum ----
S("forum_openai", "forum", "json", "https://community.openai.com/latest.json", "discourse", ('forum', 'openai'), limit=20, detail=False),
S("forum_fly", "forum", "json", "https://community.fly.io/latest.json", "discourse", ('forum', 'infra'), limit=20, detail=False),
S("forum_bitwarden", "forum", "json", "https://community.bitwarden.com/latest.json", "discourse", ('forum', 'security'), limit=20, detail=False),
S("forum_cursor", "forum", "json", "https://forum.cursor.com/latest.json", "discourse", ('forum', 'ai', 'ide'), limit=20, detail=False),
S("forum_manjaro", "forum", "json", "https://forum.manjaro.org/latest.json", "discourse", ('forum', 'linux'), limit=20, detail=False),
S("forum_arduino", "forum", "json", "https://forum.arduino.cc/latest.json", "discourse", ('forum', 'hardware'), limit=20, detail=False),
S("forum_kde", "forum", "json", "https://discuss.kde.org/latest.json", "discourse", ('forum', 'linux'), limit=20, detail=False),
S("forum_ray", "forum", "json", "https://discuss.ray.io/latest.json", "discourse", ('forum', 'ml'), limit=20, detail=False),
S("forum_wandb", "forum", "json", "https://community.wandb.ai/latest.json", "discourse", ('forum', 'ml'), limit=20, detail=False),
S("forum_grafana2", "forum", "json", "https://community.grafana.com/latest.json", "discourse", ('forum', 'observability'), limit=20, detail=False),
S("forum_traefik2", "forum", "json", "https://community.traefik.io/latest.json", "discourse", ('forum', 'infra'), limit=20, detail=False),
S("forum_nvidia", "forum", "json", "https://forums.developer.nvidia.com/latest.json", "discourse", ('forum', 'nvidia'), limit=20, detail=False),
# ---- company_cn ----
S("qbitai", "company_cn", "rss", "https://www.qbitai.com/feed", "rss", ('cn', 'ai', 'news'), limit=20, detail=False),
S("infoq_cn", "company_cn", "rss", "https://www.infoq.cn/feed", "rss", ('cn', 'dev'), limit=20, detail=False),
S("leiphone", "company_cn", "rss", "https://www.leiphone.com/feed", "rss", ('cn', 'tech'), limit=20, detail=False),
# ---- models ----
S("hf_models_summary", "models", "json", "https://huggingface.co/api/models?pipeline_tag=summarization&sort=trendingScore&direction=-1&limit=20", "hf_models", ('model', 'hf'), limit=20, detail=False),
S("hf_models_translate", "models", "json", "https://huggingface.co/api/models?pipeline_tag=translation&sort=trendingScore&direction=-1&limit=20", "hf_models", ('model', 'hf'), limit=20, detail=False),
S("hf_models_similarity", "models", "json", "https://huggingface.co/api/models?pipeline_tag=sentence-similarity&sort=trendingScore&direction=-1&limit=20", "hf_models", ('model', 'hf', 'embed'), limit=20, detail=False),
S("hf_models_tokencls", "models", "json", "https://huggingface.co/api/models?pipeline_tag=token-classification&sort=trendingScore&direction=-1&limit=20", "hf_models", ('model', 'hf'), limit=20, detail=False),
S("hf_models_zeroshot", "models", "json", "https://huggingface.co/api/models?pipeline_tag=zero-shot-classification&sort=trendingScore&direction=-1&limit=20", "hf_models", ('model', 'hf'), limit=20, detail=False),
S("hf_models_segment", "models", "json", "https://huggingface.co/api/models?pipeline_tag=image-segmentation&sort=trendingScore&direction=-1&limit=20", "hf_models", ('model', 'hf', 'vision'), limit=20, detail=False),
S("hf_models_objectdet", "models", "json", "https://huggingface.co/api/models?pipeline_tag=object-detection&sort=trendingScore&direction=-1&limit=20", "hf_models", ('model', 'hf', 'vision'), limit=20, detail=False),
S("hf_models_depth", "models", "json", "https://huggingface.co/api/models?pipeline_tag=depth-estimation&sort=trendingScore&direction=-1&limit=20", "hf_models", ('model', 'hf', 'vision'), limit=20, detail=False),
S("hf_models_timeseries", "models", "json", "https://huggingface.co/api/models?pipeline_tag=time-series-forecasting&sort=trendingScore&direction=-1&limit=20", "hf_models", ('model', 'hf'), limit=20, detail=False),
S("hf_models_rl", "models", "json", "https://huggingface.co/api/models?pipeline_tag=reinforcement-learning&sort=trendingScore&direction=-1&limit=20", "hf_models", ('model', 'hf', 'rl'), limit=20, detail=False),
S("hf_models_qa", "models", "json", "https://huggingface.co/api/models?pipeline_tag=question-answering&sort=trendingScore&direction=-1&limit=20", "hf_models", ('model', 'hf'), limit=20, detail=False),
S("hf_models_img2text", "models", "json", "https://huggingface.co/api/models?pipeline_tag=image-to-text&sort=trendingScore&direction=-1&limit=20", "hf_models", ('model', 'hf', 'vlm'), limit=20, detail=False),
S("hf_models_rerank2", "models", "json", "https://huggingface.co/api/models?pipeline_tag=text-ranking&sort=trendingScore&direction=-1&limit=20", "hf_models", ('model', 'hf', 'rag'), limit=20, detail=False),
# ---- benchmark ----
S("aider_board", "benchmark", "rss", "https://aider.chat/feed.xml", "rss", ('benchmark', 'code'), limit=20, detail=False),
S("arcprize", "benchmark", "rss", "https://arcprize.org/feed.xml", "rss", ('benchmark', 'reasoning'), limit=20, detail=False),
]

View File

@@ -229,37 +229,93 @@ select:focus,input:focus{border-color:rgba(124,108,244,.6)}
.thead,.row{grid-template-columns:38px minmax(140px,1fr) 84px 80px 64px}
.thead span:nth-child(4),.row .catpill{display:none}
}
/* ---- меню v2 ---- */
.brand .ver{margin-left:auto;font:10.5px var(--mono);color:var(--dim2);background:#151a24;border:1px solid var(--line);border-radius:20px;padding:2px 8px}
.menu-btn{display:flex;align-items:center;gap:9px;margin:0 0 12px;padding:9px 11px;width:100%;border:1px solid var(--line);
background:#11151d;color:var(--dim);border-radius:11px;font:12.5px inherit;cursor:pointer;transition:.15s}
.menu-btn:hover{border-color:var(--line2);color:var(--txt);background:#161c26}
.menu-btn i{font-style:normal;color:var(--acc2)}
.menu-btn .kbd{margin-left:auto}
.kbd{font:10px var(--mono);color:var(--dim2);border:1px solid var(--line2);border-radius:5px;padding:1px 5px;background:#0d1117}
.nav button .kbd{margin-left:auto;margin-right:6px;opacity:.55}
.nav button:hover .kbd{opacity:1}
.sect{display:flex;align-items:center;justify-content:space-between;gap:8px}
.sect .mini{border:0;background:transparent;color:var(--dim2);cursor:pointer;font-size:11px;padding:0 2px}
.sect .mini:hover{color:var(--txt)}
.cats.collapsed{display:none}
.quick{display:grid;gap:6px;margin:2px 0 10px}
.quick button{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:9px;border:1px solid var(--line);
background:#0f131a;color:var(--dim);font:12.5px inherit;cursor:pointer;text-align:left;transition:.14s}
.quick button:hover{border-color:var(--line2);color:var(--txt);background:#151b25;transform:translateX(2px)}
.quick button i{font-style:normal;color:var(--acc);width:12px;text-align:center}
.ttlwrap{min-width:0}
.crumbs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:5px}
.crumbs span{font:10.5px var(--mono);color:var(--dim2);border:1px solid var(--line);border-radius:20px;padding:2px 8px;background:#0d1117}
.crumbs span.on{color:#cfd6ec;border-color:#3b3660;background:#181633}
.palette{position:fixed;inset:0;background:rgba(4,5,8,.72);backdrop-filter:blur(6px);display:none;z-index:80;padding-top:14vh;justify-content:center}
.palette.on{display:flex}
.pal-box{width:min(620px,92vw);background:#0f131b;border:1px solid var(--line2);border-radius:16px;
box-shadow:0 30px 80px rgba(0,0,0,.6);overflow:hidden;align-self:flex-start}
.pal-box input{width:100%;border:0;border-bottom:1px solid var(--line);background:transparent;color:var(--txt);
padding:15px 18px;font:14px inherit;outline:none}
.pal-list{max-height:52vh;overflow:auto;padding:6px}
.pal-item{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:9px;cursor:pointer;color:var(--dim);font-size:13px}
.pal-item .tag{font:10px var(--mono);color:var(--dim2);border:1px solid var(--line);border-radius:5px;padding:1px 6px}
.pal-item .cnt{margin-left:auto;font:11px var(--mono);color:var(--dim2)}
.pal-item.sel,.pal-item:hover{background:#171d29;color:var(--txt)}
.pal-foot{display:flex;gap:6px;align-items:center;padding:9px 14px;border-top:1px solid var(--line);font:10.5px var(--mono);color:var(--dim2)}
.thead.runs,.row.runs{grid-template-columns:132px minmax(120px,1fr) minmax(120px,1.2fr) 90px 100px 110px}
.runs-time{font:11.5px var(--mono);color:var(--dim2)}
.chip b{font:10.5px var(--mono);color:var(--dim2);font-weight:500;margin-left:3px}
.chip.on b{color:#cbc4ff}
</style>
</head>
<body>
<div class="app">
<aside>
<aside id="side">
<div class="brand">
<div class="mark"></div>
<div><b>omni</b><span>source radar</span></div>
<span class="ver" id="brand-n">—</span>
</div>
<button class="menu-btn" id="btn-palette"><i>⌘</i>Меню<span class="kbd">Ctrl K</span></button>
<div class="nav">
<button id="nav-feed" class="on"><i>▤</i>Лента<b id="nav-feed-n">—</b></button>
<button id="nav-src"><i>◈</i>Источники<b id="nav-src-n">—</b></button>
<button id="nav-feed" class="on"><i>▤</i>Лента<span class="kbd">1</span><b id="nav-feed-n">—</b></button>
<button id="nav-src"><i>◈</i>Источники<span class="kbd">2</span><b id="nav-src-n">—</b></button>
<button id="nav-runs"><i>≡</i>Прогоны<span class="kbd">3</span><b id="nav-runs-n">—</b></button>
<button id="nav-abuse"><i>◭</i>Абузы<span class="kbd">4</span><b id="nav-abuse-n">—</b></button>
</div>
<div class="sect">категории</div>
<div class="sect"><span>категории</span><button class="mini" id="cats-toggle" title="свернуть">▾</button></div>
<div class="cats" id="cats"></div>
<div class="legend"><span><i class="lg-a"></i>живые</span><span><i class="lg-d"></i>мёртвые</span><span><i class="lg-u"></i>не проверены</span></div>
<div class="sect"><span>действия</span></div>
<div class="quick">
<button id="q-feed"><i>↻</i>Собрать ленту</button>
<button id="q-check"><i>✓</i>Проверить всё</button>
<button id="q-add"><i>+</i>Источник</button>
</div>
<div class="aside-foot" id="aside-foot">загрузка…</div>
</aside>
<main>
<div class="top">
<div class="ttlwrap">
<div class="crumbs" id="crumbs"></div>
<h1 id="ttl">Лента<small>свежие записи со всех источников</small></h1>
</div>
<div class="spacer"></div>
<div class="search">
<span style="color:#5d6779">⌕</span>
<span class="sicon">⌕</span>
<input id="q" placeholder="поиск по базе">
<kbd>/</kbd>
</div>
<button class="btn" id="btn-palette2" title="меню (Ctrl K)">⌘ Меню</button>
<button class="btn primary" id="btn-feed">Собрать ленту</button>
</div>
@@ -273,7 +329,17 @@ select:focus,input:focus{border-color:rgba(124,108,244,.6)}
</section>
<!-- ИСТОЧНИКИ -->
<section id="view-abuse" style="display:none">
<div class="strip" id="abuse-strip"></div>
<div class="filters" id="abuse-tags"></div>
<div class="cards" id="abuse-cards"></div>
</section>
<section id="view-src" style="display:none">
<div class="filters" id="src-tools">
<button class="btn" id="btn-discover" title="искать фиды на хостах и подключать">авто-добавление</button>
<span class="kv" id="discover-note"></span>
</div>
<div class="strip" id="src-strip"></div>
<div class="filters">
<select id="f-cat"><option value="all">все категории</option></select>
@@ -300,10 +366,30 @@ select:focus,input:focus{border-color:rgba(124,108,244,.6)}
<div id="rows"></div>
</div>
</section>
<!-- ПРОГОНЫ -->
<section id="view-runs" style="display:none">
<div class="strip" id="runs-strip"></div>
<div class="table">
<div class="thead runs">
<span>время</span><span>источник</span><span>запрос</span>
<span class="num">получено</span><span class="num">сохранено</span><span>статус</span>
</div>
<div id="runs-rows"></div>
</div>
</section>
</div>
</main>
</div>
<div class="palette" id="palette">
<div class="pal-box">
<input id="pal-q" placeholder="куда идём: категория, действие, источник…">
<div class="pal-list" id="pal-list"></div>
<div class="pal-foot"><kbd>↑</kbd><kbd>↓</kbd> выбор <kbd>↵</kbd> открыть <kbd>esc</kbd> закрыть</div>
</div>
</div>
<div class="prog" id="prog">
<span id="prog-txt">работаю…</span>
<div class="track"><i id="prog-bar"></i></div>
@@ -392,6 +478,12 @@ function setCat(name) {
S.cat = name; S.offset = 0;
renderCats(S.stats ? S.stats.categories : []);
syncChips();
crumb();
try {
const u = new URL(location.href);
name && name !== 'all' ? u.searchParams.set('cat', name) : u.searchParams.delete('cat');
history.replaceState(null, '', u.toString());
} catch (e) {}
if (S.view === 'feed') loadItems(true); else loadSources();
$('#f-cat').value = name;
}
@@ -403,11 +495,14 @@ async function loadStats() {
S.stats = s;
$('#nav-feed-n').textContent = fmt(s.items);
$('#nav-src-n').textContent = fmt(s.sources);
$('#nav-runs-n').textContent = fmt(s.runs);
$('#brand-n').textContent = fmt(s.alive) + '/' + fmt(s.sources);
$('#aside-foot').innerHTML =
`<b>${fmt(s.sources)}</b> источников · <b>${fmt(s.sources_enabled)}</b> вкл<br>` +
`<b>${fmt(s.alive)}</b> живых · <b>${fmt(s.tested)}</b> проверено<br>` +
`<b>${fmt(s.runs)}</b> прогонов · <b>${fmt(s.priced)}</b> с ценой`;
renderCats(s.categories);
buildChips(s.categories);
const ks = [
['записей', s.items, 'в sqlite + fts5', ''],
['источников', s.sources, `${s.sources_enabled} включено`, ''],
@@ -555,21 +650,144 @@ async function check(category, name) {
}
}
/* ---------- прогоны ---------- */
async function loadRuns() {
const rows = $('#runs-rows');
rows.innerHTML = '<div class="skel"></div><div class="skel"></div><div class="skel"></div>';
const r = await api('/api/runs?limit=100');
if (r.error) { rows.innerHTML = ''; return toast(r.error, 'err'); }
const items = r.items || [];
const ok = items.filter(x => String(x.status || '').toLowerCase().startsWith('ok')).length;
const got = items.reduce((a, x) => a + (x.got || 0), 0);
const kept = items.reduce((a, x) => a + (x.kept || 0), 0);
$('#runs-strip').innerHTML =
`<div>всего прогонов<b>${fmt(r.total || items.length)}</b></div>` +
`<div class="g">успешных<b>${fmt(ok)}</b></div>` +
`<div class="r">пустых<b>${fmt(items.length - ok)}</b></div>` +
`<div>получено<b>${fmt(got)}</b></div>` +
`<div>сохранено<b>${fmt(kept)}</b></div>` +
`<div>конверсия<b>${got ? Math.round(kept / got * 100) : 0}%</b></div>`;
rows.innerHTML = '';
if (!items.length) { rows.innerHTML = '<div class="empty">прогонов нет. нажми «Собрать ленту».</div>'; return; }
items.forEach(x => {
const d = el('div', 'row runs');
const st = String(x.status || '').toLowerCase();
d.innerHTML =
`<span class="runs-time">${esc(x.when || '')}</span>` +
`<span>${esc(x.source || x.provider || '—')}</span>` +
`<span class="mono">${esc(String(x.query || '—').slice(0, 62))}</span>` +
`<span class="num">${fmt(x.got || 0)}</span>` +
`<span class="num">${fmt(x.kept || 0)}</span>` +
`<span class="st ${st.startsWith('ok') ? 'ok' : 'bad'}">${esc(x.status || '—')}</span>`;
rows.appendChild(d);
});
}
function crumb() {
const c = $('#crumbs');
if (!c) return;
const v = {feed: 'лента', src: 'источники', runs: 'прогоны'}[S.view] || S.view;
let h = `<span class="on">${esc(v)}</span>`;
if (S.cat && S.cat !== 'all') h += `<span>${esc(S.cat)}</span>`;
if (S.q) h += `<span>поиск: ${esc(S.q.slice(0, 22))}</span>`;
if (S.view === 'feed') h += `<span>${S.sort === 'score' ? 'по score' : 'по свежести'}</span>`;
if (S.view === 'src' && S.state) h += `<span>${esc(S.state)}</span>`;
c.innerHTML = h;
}
const VIEWMETA = {
feed: ['Лента', 'свежие записи со всех источников'],
src: ['Источники', 'подключение, здоровье, включение'],
runs: ['Прогоны', 'история запусков парсера'],
abuse: ['Абузы', 'промо, триалы, ключи, сливы, софт']
};
/* ---------- абузы ---------- */
const ABUSE_LABELS = {keys: 'ключи', promo: 'промо', trial: 'триалы', leak: 'сливы', tool: 'софт', resale: 'продажа', news: 'новости'};
S.abuseTag = null; S.abuse = null;
async function loadAbuse(tag) {
if (tag !== undefined) S.abuseTag = tag;
const q = new URLSearchParams({limit: 60});
if (S.abuseTag) q.set('tag', S.abuseTag);
const [d, st] = await Promise.all([api('/api/abuse?' + q), S.stats ? Promise.resolve(S.stats) : api('/api/stats')]);
S.abuse = d;
const tags = Object.entries(d.tags || {}).sort((a, b) => b[1] - a[1]);
const strip = $('#abuse-strip');
if (strip) strip.innerHTML =
`<span class="kv"><b>${d.total}</b> находок</span>` +
`<span class="kv"><b>${tags.length}</b> тегов</span>` +
`<span class="kv">в базе <b>${fmt(st.items)}</b></span>` +
`<span class="kv">источников <b>${fmt(st.sources)}</b></span>`;
const box = $('#abuse-tags');
if (box) {
box.innerHTML = `<button class="chip${!S.abuseTag ? ' on' : ''}" data-t="all">всё</button>` +
tags.map(([k, v]) => `<button class="chip${S.abuseTag === k ? ' on' : ''}" data-t="${esc(k)}">${esc(ABUSE_LABELS[k] || k)} <b>${v}</b></button>`).join('');
box.querySelectorAll('.chip').forEach(b => b.onclick = () => loadAbuse(b.dataset.t === 'all' ? null : b.dataset.t));
}
const cards = $('#abuse-cards');
if (!cards) return;
if (!d.items.length) { cards.innerHTML = '<div class="empty">пусто — запусти прогон категории abuse</div>'; return; }
cards.innerHTML = d.items.map(it => `
<div class="card">
<div class="card-h"><span class="src">${esc(it.source)}</span><span class="dots"></span>
<span class="score">${it.abuse_score}</span></div>
<a class="card-t" href="${esc(it.url)}" target="_blank" rel="noreferrer">${esc(it.title)}</a>
<div class="card-m"><span class="tags">${it.tags.map(t => `<i>${esc(ABUSE_LABELS[t] || t)}</i>`).join('')}</span>
<span class="date">${esc((it.published || '').slice(0, 10))}</span></div>
</div>`).join('');
}
$('#btn-discover') && ($('#btn-discover').onclick = async (e) => {
const b = e.currentTarget; const t0 = b.textContent;
b.disabled = true; b.textContent = 'ищу фиды…';
try {
const r = await api('/api/discover?hosts=40&add=25', {method: 'POST'});
b.textContent = `+${(r.added || []).length} подключено`;
const nt = $('#discover-note'); if (nt) nt.textContent = `кандидатов ${r.found || 0} · живых ${r.alive || 0}`;
setTimeout(() => { b.disabled = false; b.textContent = t0; loadStats(); }, 4000);
} catch (err) { b.textContent = 'ошибка: ' + err.message; b.disabled = false; setTimeout(() => b.textContent = t0, 4000); }
});
/* ---------- view switching ---------- */
function setView(v) {
if (!VIEWMETA[v]) v = 'feed';
S.view = v;
$('#nav-feed').classList.toggle('on', v === 'feed');
$('#nav-src').classList.toggle('on', v === 'src');
$('#view-feed').style.display = v === 'feed' ? '' : 'none';
$('#view-src').style.display = v === 'src' ? '' : 'none';
['feed', 'src', 'runs', 'abuse'].forEach(k => {
const b = $('#nav-' + k); if (b) b.classList.toggle('on', k === v);
const sec = $('#view-' + k); if (sec) sec.style.display = k === v ? '' : 'none';
});
$('#btn-feed').style.display = v === 'feed' ? '' : 'none';
$('#feed-tools').style.display = v === 'feed' ? '' : 'none';
if (v === 'feed') loadItems(true); else loadSources();
const ft = $('#feed-tools'); if (ft) ft.style.display = v === 'feed' ? '' : 'none';
if (v === 'abuse') loadAbuse().catch(e => console.error('abuse', e));
const m = VIEWMETA[v];
$('#ttl').innerHTML = `${m[0]}<small>${m[1]}</small>`;
crumb();
try {
const u = new URL(location.href);
u.searchParams.set('view', v);
S.cat && S.cat !== 'all' ? u.searchParams.set('cat', S.cat) : u.searchParams.delete('cat');
S.q ? u.searchParams.set('q', S.q) : u.searchParams.delete('q');
history.replaceState(null, '', u.toString());
} catch (e) {}
if (v === 'feed') loadItems(true);
else if (v === 'src') loadSources();
else loadRuns();
}
/* ---------- wire ---------- */
$('#nav-feed').onclick = () => setView('feed');
$('#nav-src').onclick = () => setView('src');
$('#nav-runs').onclick = () => setView('runs');
$('#nav-abuse').onclick = () => { setView('abuse'); loadAbuse(); };
$('#q-feed').onclick = runFeed;
$('#q-check').onclick = () => check(null, null);
$('#q-add').onclick = () => $('#modal').classList.add('on');
$('#cats-toggle').onclick = e => {
const c = $('#cats'); const hid = c.classList.toggle('collapsed');
e.target.textContent = hid ? '▸' : '▾';
try { localStorage.setItem('omni.cats', hid ? '0' : '1'); } catch (er) {}
};
$('#btn-feed').onclick = runFeed;
$('#btn-more').onclick = () => loadItems(false);
$('#btn-check-all').onclick = () => check(null, null);
@@ -588,8 +806,17 @@ $('#q').oninput = e => {
}, 320);
};
document.addEventListener('keydown', e => {
if (e.key === '/' && document.activeElement !== $('#q')) { e.preventDefault(); $('#q').focus(); }
if (e.key === 'Escape') $('#modal').classList.remove('on');
const typing = /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement.tagName);
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); openPalette(); return; }
if (e.key === '/' && !typing) { e.preventDefault(); $('#q').focus(); return; }
if (e.key === 'Escape') { $('#modal').classList.remove('on'); $('#palette').classList.remove('on'); return; }
if (!typing && !e.ctrlKey && !e.metaKey) {
if (e.key === '1') setView('feed');
if (e.key === '2') setView('src');
if (e.key === '3') setView('runs');
if (e.key === '4') { setView('abuse'); loadAbuse(); }
if (e.key === 'r' && !e.shiftKey) runFeed();
}
});
$('#btn-add').onclick = () => $('#modal').classList.add('on');
@@ -608,27 +835,107 @@ $('#a-save').onclick = async () => {
loadSources(); loadStats();
};
/* feed toolbar */
$('#feed-tools').innerHTML =
['all', 'community', 'papers', 'security', 'company', 'forum', 'models', 'benchmark']
.map(c => `<button class="chip" data-k="cat" data-cat="${c}">${c === 'all' ? 'всё' : c}</button>`).join('') +
/* feed toolbar: чипы строятся из живого реестра категорий */
function buildChips(cats) {
const box = $('#feed-tools');
if (!box) return;
const list = cats || (S.stats && S.stats.categories) || [];
const tot = {}; list.forEach(c => { tot[c.name] = c.total; });
box.innerHTML =
`<button class="chip" data-k="cat" data-cat="all">всё <b>${fmt(S.stats ? S.stats.sources : 0)}</b></button>` +
list.map(c => `<button class="chip" data-k="cat" data-cat="${esc(c.name)}">${esc(c.name)} <b>${fmt(c.total)}</b></button>`).join('') +
'<span style="flex:1"></span>' +
'<button class="chip" data-k="sort" data-sort="score">по score</button>' +
'<button class="chip" data-k="sort" data-sort="fresh">по свежести</button>';
document.querySelectorAll('#feed-tools .chip').forEach(b => {
box.querySelectorAll('.chip').forEach(b => {
b.onclick = () => {
if (b.dataset.k === 'sort') { S.sort = b.dataset.sort; syncChips(); loadItems(true); }
else setCat(b.dataset.cat);
};
});
syncChips();
});
syncChips();
}
/* ---------- палитра ---------- */
let PAL = [], palSel = 0;
function palItems() {
const out = [
{tag: 'вид', label: 'Лента', hint: 'свежие записи', run: () => setView('feed')},
{tag: 'вид', label: 'Источники', hint: 'менеджер источников', run: () => setView('src')},
{tag: 'вид', label: 'Прогоны', hint: 'история запусков', run: () => setView('runs')},
{tag: 'вид', label: 'Абузы', hint: 'промо/триалы/ключи', run: () => { setView('abuse'); loadAbuse(); }},
{tag: 'действие', label: 'Собрать ленту', hint: 'опрашивает включённые источники', run: runFeed},
{tag: 'действие', label: 'Проверить все источники', hint: 'health-check', run: () => check(null, null)},
{tag: 'действие', label: 'Добавить источник', hint: 'своя лента или API', run: () => $('#modal').classList.add('on')},
{tag: 'действие', label: 'Включить всё в выборке', hint: 'bulk enable', run: async () => { await post('/api/sources/toggle_all', {enabled: true, category: S.cat}); toast('все включены', 'ok'); loadSources(); loadStats(); }},
{tag: 'действие', label: 'Выключить всё в выборке', hint: 'bulk disable', run: async () => { await post('/api/sources/toggle_all', {enabled: false, category: S.cat}); toast('все выключены', 'ok'); loadSources(); loadStats(); }}
];
((S.stats && S.stats.categories) || []).forEach(c => out.push({
tag: 'категория', label: c.name, hint: `${c.alive}/${c.total} живых`, q: c.name,
run: () => { setView('feed'); setCat(c.name); }
}));
(S.sources || []).forEach(x => out.push({
tag: 'источник', label: x.name,
hint: `${x.category} · ${x.stored || 0} записей` + (x.note ? ' · ' + x.note : ''),
hay: `${x.name} ${x.category} ${(x.tags || []).join(' ')}`,
run: () => { S.srcQ = x.name; setView('src'); $('#q').value = x.name; S.q = x.name; loadSources(); }
}));
return out;
}
function renderPal(q) {
const box = $('#pal-list');
PAL = palItems();
const needle = (q || '').toLowerCase().trim();
const list = needle ? PAL.filter(x => (x.label + ' ' + x.tag + ' ' + (x.hint || '') + ' ' + (x.hay || '')).toLowerCase().includes(needle)) : PAL;
PAL = list.slice(0, 60);
if (palSel >= PAL.length) palSel = 0;
box.innerHTML = PAL.length ? '' : '<div class="empty">ничего не нашлось</div>';
PAL.forEach((x, i) => {
const d = el('div', 'pal-item' + (i === palSel ? ' sel' : ''));
d.innerHTML = `<span class="tag">${esc(x.tag)}</span><span>${esc(x.label)}</span><span class="cnt">${esc(x.hint || '')}</span>`;
d.onclick = () => { $('#palette').classList.remove('on'); x.run(); };
box.appendChild(d);
});
}
async function openPalette() {
$('#palette').classList.add('on');
palSel = 0;
$('#pal-q').value = '';
renderPal('');
setTimeout(() => $('#pal-q').focus(), 30);
if (!(S.sources || []).length) {
try {
const r = await api('/api/sources?category=all');
if (r && r.items) { S.sources = r.items; renderPal($('#pal-q').value); }
} catch (e) {}
}
}
$('#btn-palette').onclick = openPalette;
$('#btn-palette2').onclick = openPalette;
$('#palette').onclick = e => { if (e.target.id === 'palette') $('#palette').classList.remove('on'); };
$('#pal-q').oninput = e => { palSel = 0; renderPal(e.target.value); };
$('#pal-q').onkeydown = e => {
if (e.key === 'ArrowDown') { e.preventDefault(); palSel = Math.min(palSel + 1, PAL.length - 1); renderPal($('#pal-q').value); }
if (e.key === 'ArrowUp') { e.preventDefault(); palSel = Math.max(palSel - 1, 0); renderPal($('#pal-q').value); }
if (e.key === 'Enter' && PAL[palSel]) { $('#palette').classList.remove('on'); PAL[palSel].run(); }
};
try { if (localStorage.getItem('omni.cats') === '0') { $('#cats').classList.add('collapsed'); $('#cats-toggle').textContent = '▸'; } } catch (e) {}
(async () => {
const p = new URLSearchParams(location.search);
if (p.get('cat')) S.cat = p.get('cat');
if (p.get('q')) { S.q = p.get('q'); $('#q').value = S.q; }
await loadStats();
if (p.get('view') === 'src') { setView('src'); } else { await loadItems(true); }
const v = p.get('view');
if (v === 'abuse') { setView('abuse'); setTimeout(() => loadAbuse().catch(e => console.error('abuse', e)), 300); }
else if (v === 'src' || v === 'runs') setView(v);
else { setView('feed'); }
if (p.get('pal')) setTimeout(openPalette, 500);
})();
</script>
</body>