parser: ETag/304-кэш, 61 авто-источник из 81 кандидата (+RSS AI/security), правила farm/weights, тесты sources_auto
This commit is contained in:
13
README.md
13
README.md
@@ -198,7 +198,7 @@ python -m omni export --format jsonl --out data/dump.jsonl
|
|||||||
|
|
||||||
## Реестр источников
|
## Реестр источников
|
||||||
|
|
||||||
`omni/sources.py` + `omni/sources_ext.py` + `omni/sources_auto.py` — **1072 источника в 11 категориях**, у каждого свой адаптер и флаг вкл/выкл. Из них **467 abuse** (включая 46 авто-подключённых `aa_`) и **33 в активной авто-группе `aa_`** (остальные 13 авто-кандидатов совпали по url с уже существующими источниками и были схлопнуты дедупом):
|
`omni/sources.py` + `omni/sources_ext.py` + `omni/sources_auto.py` — **1072 источника в 11 категориях**, у каждого свой адаптер и флаг вкл/выкл. Из них **467 abuse** (включая 61 авто-подключённый `aa_`) и **37 в активной авто-группе `aa_`** (остальные 24 авто-кандидата совпали по url с уже существующими источниками и были схлопнуты дедупом):
|
||||||
|
|
||||||
- **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
|
- **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
|
- **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
|
||||||
@@ -229,20 +229,27 @@ python tools/auto_cycle.py # полный цикл: probe →
|
|||||||
|
|
||||||
Универсальный `generic_json` понимает описание в `extra`: `rows` (путь к списку через точку), `title_key`, `url_key`, `text_key`, `date_key`, `base` + `slug_key` (собрать ссылку из слага), `deref` (спуститься в подобъект). Поэтому большинство новых JSON-источников — одна строка в `data/source_candidates.json`, без нового кода. Отдельные адаптеры только там, где схема нестандартная: `bsky_search` (Bluesky public API, без токена), `mastodon_tag`, `tg_web` (`t.me/s/<channel>`), `discourse` (`latest.json`).
|
Универсальный `generic_json` понимает описание в `extra`: `rows` (путь к списку через точку), `title_key`, `url_key`, `text_key`, `date_key`, `base` + `slug_key` (собрать ссылку из слага), `deref` (спуститься в подобъект). Поэтому большинство новых JSON-источников — одна строка в `data/source_candidates.json`, без нового кода. Отдельные адаптеры только там, где схема нестандартная: `bsky_search` (Bluesky public API, без токена), `mastodon_tag`, `tg_web` (`t.me/s/<channel>`), `discourse` (`latest.json`).
|
||||||
|
|
||||||
Подключено в этом апдейте (46 живых из 52 проверенных, `пусто 0/33 | собрано 458, новых 408`):
|
Подключено в этом апдейте (61 живой из 81 проверенного; отбор `aa_` = 37 источников после дедупа по url, `пусто 0/37 | собрано 673, новых 234`):
|
||||||
|
|
||||||
- **JSON без ключа** — dev.to, Lobsters, npm search (llm / mcp / free api keys), crates.io, Docker Hub, Stack Overflow (openai / prompt-injection), GitLab (new/llm), Hugging Face spaces + models trending
|
- **JSON без ключа** — dev.to, Lobsters, npm search (llm / mcp / free api keys), crates.io, Docker Hub, Stack Overflow (openai / prompt-injection), GitLab (new/llm), Hugging Face spaces + models trending
|
||||||
- **Reddit** — через `api.pullpush.io` (архив Reddit, без OAuth): LocalLLaMA, FreeAI, OpenAIAPI, ChatGPT, selfhosted
|
- **Reddit** — через `api.pullpush.io` (архив Reddit, без OAuth): LocalLLaMA, FreeAI, OpenAIAPI, ChatGPT, selfhosted
|
||||||
- **Bluesky** — `public.api.bsky.app` searchPosts: free tokens api, openrouter, free api, ai keys, llm api
|
- **Bluesky** — `public.api.bsky.app` searchPosts: free tokens api, openrouter, free api, ai keys, llm api
|
||||||
- **Mastodon** — теги ai / openai / llm
|
- **Mastodon** — теги ai / openai / llm
|
||||||
- **Discourse** — community.openai, discuss.huggingface, forum.cursor, discuss.python, discuss.fly, forum.obsidian
|
- **Discourse** — community.openai, discuss.huggingface, forum.cursor, discuss.python, discuss.fly, forum.obsidian
|
||||||
- **RSS** — Habr, The Decoder, MarkTechPost, OpenAI news, Simon Willison, HF blog, TechCrunch AI, 52pojie
|
- **RSS** — Habr, The Decoder, MarkTechPost, OpenAI news, Simon Willison, HF blog, TechCrunch AI, 52pojie, Latent Space, Interconnects, Import AI, TLDR AI, smol.ai, Snyk, JFrog, Wiz, UpGuard, SecurityWeek, Cloudflare, Bleeping, The Hacker News
|
||||||
|
- **Проверка живости** — `tools/auto_expand.py --probe` (items>0 обязателен), здоровье в `data/source_health.json`, три нуля подряд → кандидат выкидывается из генерации
|
||||||
- **Telegram-превью** — ai_newz, data_secrets, neurohive, gpt4telegram, dataleak, denissexy
|
- **Telegram-превью** — ai_newz, data_secrets, neurohive, gpt4telegram, dataleak, denissexy
|
||||||
|
|
||||||
`--only` в `omni feed` понимает префикс: `python -m omni feed --only aa_` берёт всю авто-группу. Для `github_search` поднимается лимит, если в `.env` есть `GITHUB_TOKEN` (анонимно 10 запросов/мин, с токеном 30).
|
`--only` в `omni feed` понимает префикс: `python -m omni feed --only aa_` берёт всю авто-группу. Для `github_search` поднимается лимит, если в `.env` есть `GITHUB_TOKEN` (анонимно 10 запросов/мин, с токеном 30).
|
||||||
|
|
||||||
Крон (каждые 6 часов, `tools/auto_cycle.py`): проверка кандидатов → пересборка `sources_auto.py` → докачка только при изменениях → пересборка abuse-индекса → коммит `auto: источники N live` и пуш в gitea/github. Лог — `data/auto_cycle.log`.
|
Крон (каждые 6 часов, `tools/auto_cycle.py`): проверка кандидатов → пересборка `sources_auto.py` → докачка только при изменениях → пересборка abuse-индекса → коммит `auto: источники N live` и пуш в gitea/github. Лог — `data/auto_cycle.log`.
|
||||||
|
|
||||||
|
Отбор из public-apis (`--discover`) фильтруется по категориям (AI / ML / Security / News / Open Data / Development / Text Analysis / Cryptocurrency) и чёрному списку мусора (waifu/anime/cat facts/insurance/…): из 40 «бесплатных» эндпоинтов публичного каталога реально отдают данные единицы, остальное — витрины без API. Поэтому основной прирост идёт не из каталогов, а из конкретных известных эндпоинтов (`--add`).
|
||||||
|
|
||||||
|
**Условный GET (ETag/Last-Modified)** — в `omni/net.py`: ответ с `ETag`/`Last-Modified` кладётся в `data/http_cache/`, повторный запрос уходит с `If-None-Match`/`If-Modified-Since`, `304` отдаётся из кэша. Замер на `huggingface.co/blog/feed.xml`: `1.73 с` → `0.74 с` при идентичном теле (262 549 байт). Прогон по 60+ RSS/HTML-источникам ускоряется без потери свежести — новые записи приходят, как только меняется сам документ.
|
||||||
|
|
||||||
|
**Новые правила абуз-разбора** (`omni/abuse.py`): форматы ключей `sk-ws-*`, `ek_live_*`, `abl_*`, `xpl_*`, `AIza*`, `ak_*`, `sk-or-v1-*`; теги `farm` (авторег, фермы, пул прокси, ротация ip) и `weights` (abliterated/uncensored/GGUF); в `tool` добавлены `new-api`/`one-api`/`sub2api`/`chat2api`/`gpt-load`/`shim`/`openai-compatible`/`reverse proxy`; в `credits` — «$N free credits». Проверка классификатора: панель new-api → 202 (`trial/tool/credits/freeapi/infra`), авторег-ферма → 96 (`tool/resale/farm/infra`), «погода» → 0.
|
||||||
|
|
||||||
## Панель и команды
|
## Панель и команды
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ RULES = [
|
|||||||
r"sk-[A-Za-z0-9_\-]{20,}", r"gh[pousr]_[A-Za-z0-9]{20,}", r"xox[baprs]-[A-Za-z0-9\-]{10,}",
|
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"(разда|выда|дарю|бесплатн|free|giveaway)[^.]{0,40}(ключ|key|токен|token)",
|
||||||
r"(ключ|key|токен)[^.]{0,25}(бесплатн|free|дарю|раздач|giveaway)",
|
r"(ключ|key|токен)[^.]{0,25}(бесплатн|free|дарю|раздач|giveaway)",
|
||||||
|
r"sk-ws-[A-Za-z0-9_\-]{16,}", r"ek_live_[A-Za-z0-9_\-]{10,}", r"abl_[A-Za-z0-9]{16,}",
|
||||||
|
r"xpl_[A-Za-z0-9]{16,}", r"AIza[0-9A-Za-z_\-]{30,}", r"ak_[A-Za-z0-9]{20,}",
|
||||||
|
r"kr/claude", r"sk-or-v1-[A-Za-z0-9]{20,}",
|
||||||
]),
|
]),
|
||||||
("promo", "промо", 26, [
|
("promo", "промо", 26, [
|
||||||
r"промокод", r"promo\s?code", r"промо[\s-]?код", r"скидк[аи]\s+\d{1,3}", r"\d{1,3}\s?%\s?(off|скидк)",
|
r"промокод", r"promo\s?code", r"промо[\s-]?код", r"скидк[аи]\s+\d{1,3}", r"\d{1,3}\s?%\s?(off|скидк)",
|
||||||
@@ -37,6 +40,8 @@ RULES = [
|
|||||||
("tool", "софт", 18, [
|
("tool", "софт", 18, [
|
||||||
r"софт", r"кряк", r"crack(ed)?", r"bypass", r"обход\s+(блокиров|бан|защит)",
|
r"софт", r"кряк", r"crack(ed)?", r"bypass", r"обход\s+(блокиров|бан|защит)",
|
||||||
r"авторег", r"autoreg", r"panel", r"панел[ьи]\s", r"рассылк", r"sender", r"скрипт",
|
r"авторег", r"autoreg", r"panel", r"панел[ьи]\s", r"рассылк", r"sender", r"скрипт",
|
||||||
|
r"new-?api", r"one-?api", r"sub2api", r"chat2api", r"gpt-?load", r"api\s*(?:реселл|resell)",
|
||||||
|
r"openai[\s-]?compatib", r"reverse\s+proxy", r"shim",
|
||||||
]),
|
]),
|
||||||
("resale", "продажа", 14, [
|
("resale", "продажа", 14, [
|
||||||
r"продаю", r"продам", r"куплю\s+аккаунт", r"\bfor\s+sale\b", r"цена\s*[:$]", r"\$\d+\s?/\s?(мес|mo|1k|1000)",
|
r"продаю", r"продам", r"куплю\s+аккаунт", r"\bfor\s+sale\b", r"цена\s*[:$]", r"\$\d+\s?/\s?(мес|mo|1k|1000)",
|
||||||
@@ -54,6 +59,15 @@ RULES = [
|
|||||||
r"free\s+(?:api\s+)?credits?", r"\b\d+\s?(?:k|m)?\s?tokens?\s+(?:free|бесплатн)", r"free\s+quota",
|
r"free\s+(?:api\s+)?credits?", r"\b\d+\s?(?:k|m)?\s?tokens?\s+(?:free|бесплатн)", r"free\s+quota",
|
||||||
r"бесплатн\w*\s+(?:лимит|квот|токен)", r"кредит\w*\s+(?:бесплатн|в\s+подарок)", r"赠金", r"额度",
|
r"бесплатн\w*\s+(?:лимит|квот|токен)", r"кредит\w*\s+(?:бесплатн|в\s+подарок)", r"赠金", r"额度",
|
||||||
r"unlimited\s+(?:api|access|tokens|usage)", r"безлимит\w*\s+(?:api|ключ|доступ|токен)",
|
r"unlimited\s+(?:api|access|tokens|usage)", r"безлимит\w*\s+(?:api|ключ|доступ|токен)",
|
||||||
|
r"\$\d{2,4}\s+(?:in\s+)?(?:free\s+)?credits?", r"credits?\s+\$\d{2,4}", r"\$\d{2,4}\s*(?:free|в\s+подарок)",
|
||||||
|
]),
|
||||||
|
("farm", "фермы/автоматизация", 18, [
|
||||||
|
r"авторег", r"autoreg", r"farm(?:er|ing)?", r"много\s*аккаунт", r"массов\w+\s+регистрац",
|
||||||
|
r"аккаунт\w*\s+оптом", r"прокси[\s-]?пул", r"proxy\s+pool", r"ротаци\w*\s+(?:ip|прокси)",
|
||||||
|
]),
|
||||||
|
("weights", "модели/веса", 20, [
|
||||||
|
r"abliterated", r"uncensored", r"GGUF", r"weights?[^.]{0,25}(?:download|слив|huggingface)",
|
||||||
|
r"без\s+цензур", r"обход\s+модераци",
|
||||||
]),
|
]),
|
||||||
("invite", "инвайты/беты", 16, [
|
("invite", "инвайты/беты", 16, [
|
||||||
r"\binvite\s?(?:code|link|only)\b", r"инвайт", r"\bwaitlist\b", r"лист\s+ожидан", r"early\s+access",
|
r"\binvite\s?(?:code|link|only)\b", r"инвайт", r"\bwaitlist\b", r"лист\s+ожидан", r"early\s+access",
|
||||||
@@ -98,7 +112,8 @@ RULES = [
|
|||||||
|
|
||||||
COMPILED = [(tag, label, w, [re.compile(p, re.I) for p in pats]) for tag, label, w, pats in RULES]
|
COMPILED = [(tag, label, w, [re.compile(p, re.I) for p in pats]) for tag, label, w, pats in RULES]
|
||||||
TAG_LABEL = {t: l for t, l, _, _ in RULES}
|
TAG_LABEL = {t: l for t, l, _, _ in RULES}
|
||||||
STRONG = {"keys", "promo", "trial", "resale", "accs", "credits", "freeapi", "ltd", "combo"}
|
STRONG = {"keys", "promo", "trial", "resale", "accs", "credits", "freeapi", "ltd", "combo",
|
||||||
|
"farm", "weights"}
|
||||||
# на специализированных абуз-площадках хватает одного признака: сам источник тематический
|
# на специализированных абуз-площадках хватает одного признака: сам источник тематический
|
||||||
ABUSE_SRC_SC = 14
|
ABUSE_SRC_SC = 14
|
||||||
# категории-источники, которые вообще имеют смысл для абузов
|
# категории-источники, которые вообще имеют смысл для абузов
|
||||||
|
|||||||
110
omni/net.py
110
omni/net.py
@@ -10,6 +10,78 @@ _cfg = config.load()
|
|||||||
_lock = threading.Lock()
|
_lock = threading.Lock()
|
||||||
_last = {} # host -> ts
|
_last = {} # host -> ts
|
||||||
|
|
||||||
|
# ---------- условный GET: ETag / If-Modified-Since ----------
|
||||||
|
_DATA = __import__("pathlib").Path(__file__).resolve().parents[1] / "data"
|
||||||
|
_COND = _DATA / "http_cond.json"
|
||||||
|
_BODIES = _DATA / "http_cache"
|
||||||
|
_COND_DB = None
|
||||||
|
_cond_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _cond_load():
|
||||||
|
global _COND_DB
|
||||||
|
if _COND_DB is None:
|
||||||
|
try:
|
||||||
|
import json as _j
|
||||||
|
_COND_DB = _j.loads(_COND.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
_COND_DB = {}
|
||||||
|
return _COND_DB
|
||||||
|
|
||||||
|
|
||||||
|
def _cond_save():
|
||||||
|
try:
|
||||||
|
import json as _j
|
||||||
|
with _cond_lock:
|
||||||
|
_BODIES.mkdir(parents=True, exist_ok=True)
|
||||||
|
_COND.write_text(_j.dumps(_COND_DB, ensure_ascii=False), encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _body_path(key):
|
||||||
|
import hashlib
|
||||||
|
return _BODIES / (hashlib.sha1(key.encode("utf-8", "replace")).hexdigest() + ".body")
|
||||||
|
|
||||||
|
|
||||||
|
def _cond_ask(key, h):
|
||||||
|
rec = _cond_load().get(key) or {}
|
||||||
|
if rec.get("etag"):
|
||||||
|
h["If-None-Match"] = rec["etag"]
|
||||||
|
if rec.get("lm"):
|
||||||
|
h["If-Modified-Since"] = rec["lm"]
|
||||||
|
return bool(rec.get("etag") or rec.get("lm"))
|
||||||
|
|
||||||
|
|
||||||
|
def _cond_store(key, resp):
|
||||||
|
if resp.status != 200 or not resp.text or len(resp.text) > 1_500_000:
|
||||||
|
return
|
||||||
|
hdr = {str(k).lower(): v for k, v in (resp.headers or {}).items()}
|
||||||
|
etag, lm = hdr.get("etag"), hdr.get("last-modified")
|
||||||
|
if not etag and not lm:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
_BODIES.mkdir(parents=True, exist_ok=True)
|
||||||
|
p = _body_path(key)
|
||||||
|
p.write_text(resp.text, encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
_cond_load()[key] = {"etag": etag, "lm": lm, "body": p.name,
|
||||||
|
"ts": int(time.time()), "size": len(resp.text)}
|
||||||
|
_cond_save()
|
||||||
|
|
||||||
|
|
||||||
|
def _cond_hit(key):
|
||||||
|
rec = _cond_load().get(key)
|
||||||
|
if not rec or not rec.get("body"):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
txt = (_BODIES / rec["body"]).read_text(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return Resp(key.split("|")[0], 200, txt, "cache-304", headers={"x-omni-cache": "304"})
|
||||||
|
|
||||||
|
|
||||||
BLOCK_MARKS = ("just a moment", "cf-browser-verification", "attention required",
|
BLOCK_MARKS = ("just a moment", "cf-browser-verification", "attention required",
|
||||||
"ddos-guard", "checking your browser", "enable javascript and cookies")
|
"ddos-guard", "checking your browser", "enable javascript and cookies")
|
||||||
|
|
||||||
@@ -118,9 +190,11 @@ def post_json(url, payload, headers=None, timeout=None):
|
|||||||
|
|
||||||
|
|
||||||
def get(url, headers=None, timeout=None, json_mode=False, proxy=None):
|
def get(url, headers=None, timeout=None, json_mode=False, proxy=None):
|
||||||
"""Лестница: httpx -> curl_cffi -> curl_cffi с прокси. Возвращает первый осмысленный ответ.
|
"""Лестница: httpx -> curl_cffi -> curl_cffi с прокси + условный GET (ETag/304).
|
||||||
|
|
||||||
proxy=... — принудительный прокси для конкретного источника (напр. CF-форумы через DE).
|
proxy=... — принудительный прокси для конкретного источника (напр. CF-форумы через DE).
|
||||||
|
Повторный запрос неизменного документа отдаётся из data/http_cache (304), это экономит
|
||||||
|
и трафик, и время прогона (RSS/HTML отдают ETag/Last-Modified почти всегда).
|
||||||
"""
|
"""
|
||||||
timeout = timeout or _cfg.get("timeout", 25)
|
timeout = timeout or _cfg.get("timeout", 25)
|
||||||
host = urllib.parse.urlsplit(url).netloc
|
host = urllib.parse.urlsplit(url).netloc
|
||||||
@@ -135,23 +209,43 @@ def get(url, headers=None, timeout=None, json_mode=False, proxy=None):
|
|||||||
if ck:
|
if ck:
|
||||||
h["Cookie"] = ck
|
h["Cookie"] = ck
|
||||||
|
|
||||||
|
key = url + ("|" + proxy if proxy else "")
|
||||||
|
conditional = _cond_ask(key, h)
|
||||||
|
|
||||||
|
def finish(resp):
|
||||||
|
if conditional and resp.status in (304,):
|
||||||
|
hit = _cond_hit(key)
|
||||||
|
if hit:
|
||||||
|
return hit
|
||||||
|
if resp.status == 200:
|
||||||
|
_cond_store(key, resp)
|
||||||
|
return resp
|
||||||
|
|
||||||
if proxy:
|
if proxy:
|
||||||
forced = _try_curl(url, h, timeout, proxy)
|
forced = _try_curl(url, h, timeout, proxy)
|
||||||
if forced.ok and not forced.blocked:
|
if forced.ok and not forced.blocked:
|
||||||
return forced
|
return finish(forced)
|
||||||
alt = _try_httpx(url, h, timeout, proxy)
|
alt = _try_httpx(url, h, timeout, proxy)
|
||||||
return alt if alt.status == 200 else forced
|
return finish(alt if alt.status == 200 else forced)
|
||||||
|
|
||||||
first = _try_httpx(url, h, timeout, None)
|
first = _try_httpx(url, h, timeout, None)
|
||||||
|
if conditional and first.status == 304:
|
||||||
|
hit = _cond_hit(key)
|
||||||
|
if hit:
|
||||||
|
return hit
|
||||||
if first.ok and not first.blocked:
|
if first.ok and not first.blocked:
|
||||||
return first
|
return finish(first)
|
||||||
second = _try_curl(url, h, timeout, None)
|
second = _try_curl(url, h, timeout, None)
|
||||||
|
if conditional and second.status == 304:
|
||||||
|
hit = _cond_hit(key)
|
||||||
|
if hit:
|
||||||
|
return hit
|
||||||
if second.ok and not second.blocked:
|
if second.ok and not second.blocked:
|
||||||
return second
|
return finish(second)
|
||||||
proxy = _pick_proxy()
|
proxy = _pick_proxy()
|
||||||
if proxy:
|
if proxy:
|
||||||
third = _try_curl(url, h, timeout, proxy)
|
third = _try_curl(url, h, timeout, proxy)
|
||||||
if third.status == 200:
|
if third.status == 200:
|
||||||
return third
|
return finish(third)
|
||||||
return third
|
return finish(third)
|
||||||
return second if second.status else first
|
return finish(second if second.status else first)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
Файл генерируется `tools/auto_expand.py` из `data/source_candidates.json`
|
Файл генерируется `tools/auto_expand.py` из `data/source_candidates.json`
|
||||||
(живые кандидаты) — правки руками перезапишутся, добавляй кандидатов в JSON.
|
(живые кандидаты) — правки руками перезапишутся, добавляй кандидатов в JSON.
|
||||||
|
|
||||||
Обновлено: 2026-10-27 16:41 UTC · живых 46 из 52
|
Обновлено: 2026-10-27 17:19 UTC · живых 61 из 81
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ AUTO = [
|
|||||||
A("aa_gitlab_llm", "json", "https://gitlab.com/api/v4/projects?search=llm&sort=desc&order_by=created_at&per_page=25", "generic_json", ("abuse"), extra={"url_key": "web_url"}),
|
A("aa_gitlab_llm", "json", "https://gitlab.com/api/v4/projects?search=llm&sort=desc&order_by=created_at&per_page=25", "generic_json", ("abuse"), extra={"url_key": "web_url"}),
|
||||||
A("aa_npm_free_api", "json", "https://registry.npmjs.org/-/v1/search?text=free%20api%20keys&size=25", "generic_json", ("abuse"), extra={"rows": "objects", "deref": "package", "title_key": "name", "text_key": "description", "base": "https://www.npmjs.com/package/", "slug_key": "name"}),
|
A("aa_npm_free_api", "json", "https://registry.npmjs.org/-/v1/search?text=free%20api%20keys&size=25", "generic_json", ("abuse"), extra={"rows": "objects", "deref": "package", "title_key": "name", "text_key": "description", "base": "https://www.npmjs.com/package/", "slug_key": "name"}),
|
||||||
A("aa_crates_proxy", "json", "https://crates.io/api/v1/crates?q=openai%20proxy&per_page=20", "generic_json", ("abuse"), extra={"rows": "crates", "base": "https://crates.io/crates/", "slug_key": "name"}),
|
A("aa_crates_proxy", "json", "https://crates.io/api/v1/crates?q=openai%20proxy&per_page=20", "generic_json", ("abuse"), extra={"rows": "crates", "base": "https://crates.io/crates/", "slug_key": "name"}),
|
||||||
|
A("aa_json_gh_advisories", "json", "https://api.github.com/advisories?per_page=30", "generic_json", ("abuse"), extra={"url_key": "html_url", "title_key": "summary", "date_key": "published_at"}),
|
||||||
|
|
||||||
# ---------- Reddit через pullpush.io (без ключа) ----------
|
# ---------- Reddit через pullpush.io (без ключа) ----------
|
||||||
A("aa_pp_freeai", "json", "https://api.pullpush.io/reddit/search/submission/?subreddit=FreeAI&size=25", "generic_json", ("abuse"), extra={"rows": "data", "url_key": "url", "title_key": "title", "text_key": "selftext", "date_key": "created_utc"}),
|
A("aa_pp_freeai", "json", "https://api.pullpush.io/reddit/search/submission/?subreddit=FreeAI&size=25", "generic_json", ("abuse"), extra={"rows": "data", "url_key": "url", "title_key": "title", "text_key": "selftext", "date_key": "created_utc"}),
|
||||||
@@ -63,6 +64,20 @@ AUTO = [
|
|||||||
A("aa_rss_hf_blog", "rss", "https://huggingface.co/blog/feed.xml", "rss", ("abuse")),
|
A("aa_rss_hf_blog", "rss", "https://huggingface.co/blog/feed.xml", "rss", ("abuse")),
|
||||||
A("aa_rss_tc_ai", "rss", "https://techcrunch.com/category/artificial-intelligence/feed/", "rss", ("abuse")),
|
A("aa_rss_tc_ai", "rss", "https://techcrunch.com/category/artificial-intelligence/feed/", "rss", ("abuse")),
|
||||||
A("aa_rss_52pojie", "rss", "https://www.52pojie.cn/forum.php?mod=rss", "rss", ("abuse")),
|
A("aa_rss_52pojie", "rss", "https://www.52pojie.cn/forum.php?mod=rss", "rss", ("abuse")),
|
||||||
|
A("aa_rss_latentspace", "rss", "https://www.latent.space/feed", "rss", ("abuse")),
|
||||||
|
A("aa_rss_interconnects", "rss", "https://www.interconnects.ai/feed", "rss", ("abuse")),
|
||||||
|
A("aa_rss_importai", "rss", "https://importai.substack.com/feed", "rss", ("abuse")),
|
||||||
|
A("aa_rss_tldr_ai", "rss", "https://tldr.tech/api/rss/ai", "rss", ("abuse")),
|
||||||
|
A("aa_rss_smol", "rss", "https://news.smol.ai/rss.xml", "rss", ("abuse")),
|
||||||
|
A("aa_rss_aisupremacy", "rss", "https://aisupremacy.substack.com/feed", "rss", ("abuse")),
|
||||||
|
A("aa_rss_snyk", "rss", "https://snyk.io/blog/feed/", "rss", ("abuse")),
|
||||||
|
A("aa_rss_jfrog", "rss", "https://jfrog.com/blog/feed/", "rss", ("abuse")),
|
||||||
|
A("aa_rss_wiz", "rss", "https://www.wiz.io/blog/rss.xml", "rss", ("abuse")),
|
||||||
|
A("aa_rss_upguard", "rss", "https://www.upguard.com/blog/rss.xml", "rss", ("abuse")),
|
||||||
|
A("aa_rss_securityweek", "rss", "https://www.securityweek.com/feed/", "rss", ("abuse")),
|
||||||
|
A("aa_rss_cloudflare", "rss", "https://blog.cloudflare.com/rss/", "rss", ("abuse")),
|
||||||
|
A("aa_rss_bleeping", "rss", "https://www.bleepingcomputer.com/feed/", "rss", ("abuse")),
|
||||||
|
A("aa_rss_thn", "rss", "https://feeds.feedburner.com/TheHackersNews", "rss", ("abuse")),
|
||||||
|
|
||||||
# ---------- Telegram-превью ----------
|
# ---------- Telegram-превью ----------
|
||||||
A("aa_tg_ai_newz", "html", "https://t.me/s/ai_newz", "tg_web", ("abuse", "tg")),
|
A("aa_tg_ai_newz", "html", "https://t.me/s/ai_newz", "tg_web", ("abuse", "tg")),
|
||||||
|
|||||||
53
tests/test_sources_auto.py
Normal file
53
tests/test_sources_auto.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"""Санити-тесты авто-реестра источников (без сети).
|
||||||
|
|
||||||
|
Проверяют, что сгенерированный `omni/sources_auto.py` не битый: адаптеры существуют,
|
||||||
|
имена/url не дублируются, обязательные поля на месте. Сами фиды тут не дергаются —
|
||||||
|
живость проверяет `tools/auto_expand.py --probe`.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from omni import feeds, sources
|
||||||
|
from omni.sources_auto import AUTO
|
||||||
|
|
||||||
|
|
||||||
|
def test_auto_not_empty():
|
||||||
|
assert AUTO, "sources_auto.py пустой — прогони tools/auto_expand.py --probe --write"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("entry", AUTO, ids=[d.get("name", "?") for d in AUTO])
|
||||||
|
def test_entry_shape(entry):
|
||||||
|
for key in ("name", "url", "adapter", "kind"):
|
||||||
|
assert entry.get(key), "у записи нет поля %s: %r" % (key, entry)
|
||||||
|
assert entry["url"].startswith("http"), entry["url"]
|
||||||
|
assert entry["adapter"] in feeds.ADAPTERS, "неизвестный адаптер: %s" % entry["adapter"]
|
||||||
|
assert entry.get("category", "abuse") == "abuse"
|
||||||
|
|
||||||
|
|
||||||
|
def test_names_and_urls_unique():
|
||||||
|
names = [d["name"] for d in AUTO]
|
||||||
|
urls = [d["url"] for d in AUTO]
|
||||||
|
assert len(names) == len(set(names)), "дубли имён в AUTO"
|
||||||
|
assert len(urls) == len(set(urls)), "дубли url в AUTO"
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_auto_registered_in_sources():
|
||||||
|
"""Каждая живая запись должна попасть в общий реестр (или быть схлопнутой дедупом по url)."""
|
||||||
|
registered = {s.name for s in sources.SOURCES if s.name.startswith("aa_")}
|
||||||
|
existing_urls = {s.url for s in sources.SOURCES}
|
||||||
|
missing = [d["name"] for d in AUTO if d["name"] not in registered and d["url"] not in existing_urls]
|
||||||
|
assert not missing, "не попали в SOURCES: %s" % missing[:5]
|
||||||
|
|
||||||
|
|
||||||
|
def test_generated_file_is_marked_generated():
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import omni
|
||||||
|
|
||||||
|
path = Path(omni.__file__).parent / "sources_auto.py"
|
||||||
|
head = path.read_text(encoding="utf-8")[:400]
|
||||||
|
assert "генерируется" in head, "шапка файла должна предупреждать, что он генерируется"
|
||||||
|
assert re.search(r"AUTO = \[", path.read_text(encoding="utf-8")), "нет списка AUTO"
|
||||||
@@ -27,6 +27,8 @@ CAND = ROOT / "data" / "source_candidates.json"
|
|||||||
HEALTH = ROOT / "data" / "source_health.json"
|
HEALTH = ROOT / "data" / "source_health.json"
|
||||||
OUT = ROOT / "omni" / "sources_auto.py"
|
OUT = ROOT / "omni" / "sources_auto.py"
|
||||||
PUBLIC_APIS = "https://raw.githubusercontent.com/public-apis/public-apis/master/README.md"
|
PUBLIC_APIS = "https://raw.githubusercontent.com/public-apis/public-apis/master/README.md"
|
||||||
|
RE_URL = __import__("re").compile(r"\((https?://[^)\s]+)\)")
|
||||||
|
RE_LINK = __import__("re").compile(r"\[([^\]]+)\]\([^)]+\)")
|
||||||
|
|
||||||
HEADER = '''"""Авто-расширение: источники на публичных API без ключа.
|
HEADER = '''"""Авто-расширение: источники на публичных API без ключа.
|
||||||
|
|
||||||
@@ -144,25 +146,48 @@ def write_module(cands, results):
|
|||||||
print("\nwrote %s: %d live of %d" % (OUT.name, live, len(cands)))
|
print("\nwrote %s: %d live of %d" % (OUT.name, live, len(cands)))
|
||||||
|
|
||||||
|
|
||||||
|
CAT_OK = ("Artificial Intelligence", "Machine Learning", "Security", "News", "Open Data",
|
||||||
|
"Development", "Text Analysis", "Data Validation", "Sports", "Cryptocurrency")
|
||||||
|
KEEP_ANY_CAT = ("llm", "gpt", "openai", "threat", "cve", "vuln", "leak", "hack", "malware",
|
||||||
|
"phish", "proxy", "temp mail", "sms", "telegram", "scrap", "dns", "whois")
|
||||||
|
BAD = ("waifu", "anime", "cat fact", "dog", "joke", "meme", "pokemon", "quote", "trivia",
|
||||||
|
"recipe", "cocktail", "game", "3d", "texture", "insurance", "crypto price")
|
||||||
|
KW_OK = ("ai", "llm", "gpt", "model", "secur", "threat", "cve", "vuln", "leak", "hack", "news",
|
||||||
|
"feed", "telegram", "proxy", "mail", "sms", "text", "translate", "scrap", "search",
|
||||||
|
"crypto", "token", "domain", "dns", "ip", "geo")
|
||||||
|
|
||||||
|
|
||||||
def discover(limit=40):
|
def discover(limit=40):
|
||||||
"""Добрать бесплатные (Auth=No) API из public-apis."""
|
"""Добрать бесплатные (Auth=No) API из public-apis: только релевантные категории/слова."""
|
||||||
req = urllib.request.Request(PUBLIC_APIS, headers={"User-Agent": "omni-parse/1.0"})
|
req = urllib.request.Request(PUBLIC_APIS, headers={"User-Agent": "omni-parse/1.0"})
|
||||||
md = urllib.request.urlopen(req, timeout=40).read().decode("utf-8", "replace")
|
md = urllib.request.urlopen(req, timeout=40).read().decode("utf-8", "replace")
|
||||||
rows = []
|
rows, cat = [], ""
|
||||||
for line in md.splitlines():
|
for line in md.splitlines():
|
||||||
|
if line.startswith("### "):
|
||||||
|
cat = line[4:].strip()
|
||||||
|
continue
|
||||||
if not line.startswith("|") or "http" not in line:
|
if not line.startswith("|") or "http" not in line:
|
||||||
continue
|
continue
|
||||||
cells = [c.strip() for c in line.strip("|").split("|")]
|
cells = [c.strip() for c in line.strip("|").split("|")]
|
||||||
if len(cells) < 6:
|
if len(cells) < 5:
|
||||||
continue
|
continue
|
||||||
url, auth, https = cells[2], cells[4], cells[5]
|
m = RE_URL.search(cells[0])
|
||||||
if "No" not in auth or "http" not in url:
|
if not m:
|
||||||
continue
|
continue
|
||||||
if any(b in url for b in ("github.com/public-apis", "localhost")):
|
url = m.group(1)
|
||||||
|
title = RE_LINK.sub(r"\1", cells[0]).strip()
|
||||||
|
desc, auth = cells[1], cells[2]
|
||||||
|
if "No" not in auth:
|
||||||
continue
|
continue
|
||||||
rows.append({"name": "dis_" + url.split("//")[-1].split("/")[0].replace("www.", "").replace(".", "_")[:24],
|
blob = (title + " " + desc).lower()
|
||||||
"url": url, "adapter": "generic_json", "kind": "json",
|
if cat not in CAT_OK and not any(k in blob for k in KEEP_ANY_CAT):
|
||||||
"tags": ["abuse", "public_api"], "group": "json", "limit": 15})
|
continue
|
||||||
|
if any(b in blob for b in BAD):
|
||||||
|
continue
|
||||||
|
host = url.split("//")[-1].split("/")[0].replace("www.", "").replace(".", "_").replace("-", "_")
|
||||||
|
rows.append({"name": "dis_" + host[:26], "url": url, "adapter": "generic_json",
|
||||||
|
"kind": "json", "tags": ["abuse", "public_api"], "group": "json", "limit": 15,
|
||||||
|
"note": (title + " — " + desc)[:120], "cat": cat})
|
||||||
uniq = {}
|
uniq = {}
|
||||||
for r in rows:
|
for r in rows:
|
||||||
uniq.setdefault(r["name"], r)
|
uniq.setdefault(r["name"], r)
|
||||||
@@ -172,6 +197,8 @@ def discover(limit=40):
|
|||||||
cands.extend(new)
|
cands.extend(new)
|
||||||
save(CAND, cands)
|
save(CAND, cands)
|
||||||
print("discover: +%d кандидатов (всего %d)" % (len(new), len(cands)))
|
print("discover: +%d кандидатов (всего %d)" % (len(new), len(cands)))
|
||||||
|
for r in new[:12]:
|
||||||
|
print(" + %-30s %s" % (r["name"], r["note"][:80]))
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user