feat(tg): своя сессия Telethon — omni/tgsess.py + omni pump (глубина 200 постов вместо 20, 20 каналов -> 2832 поста, 2592 новых), CVE-ID в заголовке (471), tg_alive_audit из 856 сессий нашёл 121 живую; тесты 22
This commit is contained in:
24
README.md
24
README.md
@@ -24,7 +24,7 @@ python -m omni.bot
|
||||
|
||||
## Состояние на сейчас
|
||||
|
||||
- **источников: 869** в 11 категориях (662 базовых + 207 из реестра абузов + 19 Telegram-каналов, дубли вырезаны), **записей в базе: 7295** (251 с ценой, 120 прогонов), из них 3135 из реестра абузов и 374 из Telegram-каналов
|
||||
- **источников: 869** в 11 категориях (662 базовых + 207 из реестра абузов + 19 Telegram-каналов, дубли вырезаны), **записей в базе: 9889** (421 с ценой, 120 прогонов), из них 3135 из реестра абузов и 2966 из Telegram (превью + своя сессия)
|
||||
- **типы записей: 14** — каждая запись размечена по смыслу: новость 3693 · софт 516 · уязвимость 402 · релиз 361 · модель 331 · вопрос 265 · цена 205 · гайд 194 · датасет 126 · слив 90 · триал 41 · промо 34 · вакансия 21 · перепродажа 10
|
||||
- **X/Twitter: 30 аккаунтов** читаются без API-ключей через `api.fxtwitter.com` (313 твитов в базе)
|
||||
- вкладки панели: `Лента` · `Источники` · `Прогоны` · `Абузы`; полоса чипов «тип», фильтр «только с ценой», сортировка `score/fresh/price`, экспорт CSV
|
||||
@@ -220,6 +220,28 @@ API: `GET /api/stats`, `GET /api/items?category=&q=&limit=&offset=`, `GET /api/s
|
||||
|
||||
Один прогон этих каналов: 324 поста, 0 пустых источников.
|
||||
|
||||
### Своя сессия: глубина вместо 20 постов
|
||||
|
||||
Веб-превью отдаёт ~20 последних постов. Свой аккаунт (Telethon) по тем же каналам
|
||||
берёт 200: прогон 20 каналов дал **2832 поста, 2592 новых записи** (остальное дубли).
|
||||
Каналы с выключенным превью тоже читаются, но пустые не оживают: у `leakbase`
|
||||
и через сессию 4 поста — только инвайты.
|
||||
|
||||
Секретов в репозитории нет, всё через окружение:
|
||||
|
||||
```bash
|
||||
export OMNI_TG_SESSION=/path/to/account # .session без расширения
|
||||
export OMNI_TG_API_ID=... OMNI_TG_API_HASH=...
|
||||
python -m omni pump thehackernews,cvefeed --limit 200 --apply
|
||||
```
|
||||
|
||||
Без env команда честно говорит, что сессии нет, и парсер продолжает работать
|
||||
на веб-превью. Аудит пула сессий — `tg_alive_audit.py`: из 856 файлов живыми
|
||||
оказались 121 (остальные unauthorized/битые), их и берём.
|
||||
|
||||
Ещё по ходу: у CVE-каналов тело advisory без идентификатора в заголовке —
|
||||
теперь `CVE-2026-97254 · Improper Neutralization...`, таких заголовков 471.
|
||||
|
||||
## X / Twitter без ключей
|
||||
|
||||
- `omni/providers/x.py` → `profile(handle)` тянет ленту через `api.fxtwitter.com/2/profile/<handle>/statuses`: полный текст, метрики (лайки, репосты, просмотры), дата, язык.
|
||||
|
||||
32
REPORT.md
32
REPORT.md
@@ -1,35 +1,35 @@
|
||||
# omni-parse · автоотчёт
|
||||
|
||||
<!-- STATS:START -->
|
||||
_обновлено 2026-10-26T18:05:21+00:00_
|
||||
_обновлено 2026-10-26T19:42:10+00:00_
|
||||
|
||||
| метрика | значение |
|
||||
|---|---|
|
||||
| источников | 869 |
|
||||
| категорий | 11 |
|
||||
| записей | 7295 |
|
||||
| с ценой | 251 |
|
||||
| типизировано | 7295 |
|
||||
| записей | 9889 |
|
||||
| с ценой | 421 |
|
||||
| типизировано | 9889 |
|
||||
| прогонов | 120 |
|
||||
| пустых источников | 238 |
|
||||
| пустых источников | 237 |
|
||||
|
||||
типы: новость 5126 · софт 592 · уязвимость 491 · релиз 378 · модель 337 · вопрос 277 · цена 259 · гайд 219
|
||||
типы: новость 6223 · уязвимость 1354 · софт 921 · релиз 563 · цена 437 · модель 399 · вопрос 347 · гайд 301
|
||||
|
||||
| источник | записей |
|
||||
|---|---|
|
||||
| arxiv_cs_ai | 280 |
|
||||
| tg_vulnwatch | 220 |
|
||||
| tg_bleepingcomputer | 220 |
|
||||
| tg_ransomware_news | 218 |
|
||||
| community | 215 |
|
||||
| reddit_machinelearning | 208 |
|
||||
| hf_trending_models | 156 |
|
||||
| ab_niflheim_world | 123 |
|
||||
| ab_cpamafia_pro | 121 |
|
||||
| ab_zhyk_org | 120 |
|
||||
| ab_hardforum_com | 120 |
|
||||
| ab_carders_biz | 120 |
|
||||
| ab_ascarding_net | 119 |
|
||||
| ab_altenens_is | 119 |
|
||||
| ab_afflift_com | 119 |
|
||||
| tg_breachradar | 202 |
|
||||
| tg_thehackernews | 200 |
|
||||
| tg_poc_in_github | 200 |
|
||||
| tg_cvefeed | 200 |
|
||||
| tg_xakep_ru | 199 |
|
||||
| tg_alstack | 198 |
|
||||
|
||||
тесты: 19 passed in 8.07s
|
||||
тесты: 20 passed in 2.30s
|
||||
самоаудит: OK
|
||||
<!-- STATS:END -->
|
||||
|
||||
36
omni/cli.py
36
omni/cli.py
@@ -8,6 +8,7 @@ import sys
|
||||
from . import config, rank, store
|
||||
from . import feeds, sources
|
||||
from . import filter as flt
|
||||
from .model import Item
|
||||
from .providers import FUNCS, MODULES, get, status
|
||||
|
||||
ALL_FREE = ["reddit", "x", "forums", "ddg", "mojeek", "bing"]
|
||||
@@ -268,6 +269,35 @@ def cmd_dedupe(a):
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_pump(a):
|
||||
"""История Telegram-каналов через свою сессию (нужны OMNI_TG_SESSION/API_ID/API_HASH)."""
|
||||
from . import tgsess
|
||||
if not tgsess.enabled():
|
||||
print("нет сессии: задай OMNI_TG_SESSION, OMNI_TG_API_ID, OMNI_TG_API_HASH")
|
||||
return 2
|
||||
handles = [h.strip() for h in a.channels.split(",") if h.strip()]
|
||||
recs = tgsess.fetch(handles, a.limit)
|
||||
if not recs:
|
||||
print("пусто")
|
||||
return 1
|
||||
items = []
|
||||
for r in recs:
|
||||
for post in r.get("posts") or []:
|
||||
if len(post["text"]) < 12:
|
||||
continue
|
||||
items.append(Item("tg_" + r["handle"], "post", url=post["url"], title=post["title"],
|
||||
text=post["text"], author="@" + r["handle"],
|
||||
published=post["date"], source="tg_" + r["handle"],
|
||||
extra={"views": post.get("views"), "via": "session"}))
|
||||
print("каналов %d, постов %d" % (len(recs), len(items)))
|
||||
if not a.apply:
|
||||
print("dry-run: добавь --apply")
|
||||
return 0
|
||||
new, dup = store.add(items)
|
||||
print("новых %d, дублей %d" % (new, dup))
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
@@ -369,6 +399,12 @@ def main(argv=None):
|
||||
sp.add_argument("--apply", action="store_true", help="реально удалить лишние строки")
|
||||
sp.set_defaults(fn=cmd_dedupe)
|
||||
|
||||
sp = sub.add_parser("pump", help="история Telegram-каналов через свою сессию")
|
||||
sp.add_argument("channels", help="каналы через запятую")
|
||||
sp.add_argument("--limit", type=int, default=100)
|
||||
sp.add_argument("--apply", action="store_true")
|
||||
sp.set_defaults(fn=cmd_pump)
|
||||
|
||||
args = p.parse_args(argv)
|
||||
return args.fn(args)
|
||||
|
||||
|
||||
@@ -33,6 +33,11 @@ def _item(src, kind, url, title, text="", author="", published="", extra=None):
|
||||
cand = [ln for ln in body.splitlines() if sum(ch.isalpha() for ch in ln) >= 12]
|
||||
if cand:
|
||||
t = max(cand, key=len)[:140]
|
||||
# CVE-каналы постят тело advisory без идентификатора в заголовке — выносим его вперёд
|
||||
if "CVE-" not in t:
|
||||
m = re.search(r"CVE-\d{4}-\d{4,7}", body or "")
|
||||
if m:
|
||||
t = "%s · %s" % (m.group(0), t[:120])
|
||||
return Item(src.name, kind, url=str(url or ""), title=t[:300], text=body[:6000],
|
||||
author=_txt(author)[:80], published=str(published or ""), source=src.name,
|
||||
extra=ex)
|
||||
|
||||
82
omni/tgsess.py
Normal file
82
omni/tgsess.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Telegram через свою сессию (Telethon): глубина и закрытые каналы.
|
||||
|
||||
Зачем, если есть веб-превью t.me/s: превью отдаёт ~20 последних постов и ничего
|
||||
не отдаёт для каналов с выключенным превью. Сессия тянет 200+ постов и читает
|
||||
каналы/группы, куда аккаунт уже вступил.
|
||||
|
||||
Конфиг только через env (секретов в репозитории нет):
|
||||
OMNI_TG_SESSION путь к .session без расширения
|
||||
OMNI_TG_API_ID api_id (my.telegram.org)
|
||||
OMNI_TG_API_HASH api_hash
|
||||
OMNI_TG_LIMIT сколько постов тянуть на канал (по умолчанию 100)
|
||||
|
||||
Если env не задан — модуль молча выключен, парсер работает на веб-превью.
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
|
||||
SERVICE_RE = re.compile(
|
||||
r"^(channel (created|photo|name|description|username|deleted)|pinned|message pinned|"
|
||||
r"channel photo (updated|removed)|joined|left|invite link)", re.I)
|
||||
|
||||
|
||||
def enabled():
|
||||
return bool(os.environ.get("OMNI_TG_SESSION") and os.environ.get("OMNI_TG_API_ID")
|
||||
and os.environ.get("OMNI_TG_API_HASH"))
|
||||
|
||||
|
||||
def _key(handle):
|
||||
return handle.strip().strip("/").split("/")[-1].lstrip("@")
|
||||
|
||||
|
||||
async def _pull(handles, limit, session, api_id, api_hash, timeout=40):
|
||||
from telethon import TelegramClient
|
||||
|
||||
client = TelegramClient(session, api_id, api_hash, connection_retries=1, timeout=timeout / 2)
|
||||
out = []
|
||||
await asyncio.wait_for(client.connect(), timeout)
|
||||
try:
|
||||
for h in handles:
|
||||
hh = _key(h)
|
||||
rec = {"handle": hh, "posts": []}
|
||||
try:
|
||||
ent = await asyncio.wait_for(client.get_entity(hh), timeout)
|
||||
msgs = await asyncio.wait_for(client.get_messages(ent, limit=limit), timeout)
|
||||
rec["title"] = getattr(ent, "title", "") or hh
|
||||
rec["id"] = getattr(ent, "id", None)
|
||||
for m in msgs:
|
||||
text = (getattr(m, "message", "") or "").strip()
|
||||
if not text or SERVICE_RE.match(text):
|
||||
continue
|
||||
rec["posts"].append({
|
||||
"id": getattr(m, "id", None),
|
||||
"url": "https://t.me/%s/%s" % (hh, getattr(m, "id", "")),
|
||||
"title": text.splitlines()[0][:300],
|
||||
"text": text[:6000],
|
||||
"views": getattr(m, "views", None),
|
||||
"date": (m.date.astimezone(timezone.utc).isoformat()
|
||||
if isinstance(getattr(m, "date", None), datetime) else ""),
|
||||
"channel": hh,
|
||||
})
|
||||
except Exception as e:
|
||||
rec["error"] = type(e).__name__
|
||||
out.append(rec)
|
||||
finally:
|
||||
try:
|
||||
await asyncio.wait_for(client.disconnect(), 10)
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def fetch(handles, limit=None):
|
||||
"""Список словарей {handle, title, id, posts[]} по каналам. Пусто, если нет env."""
|
||||
if not enabled():
|
||||
return []
|
||||
session = os.environ["OMNI_TG_SESSION"]
|
||||
api_id = int(os.environ["OMNI_TG_API_ID"])
|
||||
api_hash = os.environ["OMNI_TG_API_HASH"]
|
||||
lim = int(limit or os.environ.get("OMNI_TG_LIMIT") or 100)
|
||||
return asyncio.run(_pull(list(handles), lim, session, api_id, api_hash))
|
||||
@@ -182,3 +182,23 @@ def test_tag_command_readonly(monkeypatch):
|
||||
bot.handle({"message": {"chat": {"id": 1}, "from": {"id": 1}, "text": "/tag"}})
|
||||
assert sent and "типы записей" in sent[0], sent[:1]
|
||||
assert "/tag" in bot.CMDS
|
||||
|
||||
|
||||
|
||||
# ---------- 12. сессия Telegram: выключена без env, CVE в заголовке ----------
|
||||
def test_tgsess_disabled_without_env(monkeypatch):
|
||||
from omni import tgsess
|
||||
for k in ("OMNI_TG_SESSION", "OMNI_TG_API_ID", "OMNI_TG_API_HASH"):
|
||||
monkeypatch.delenv(k, raising=False)
|
||||
assert tgsess.enabled() is False
|
||||
assert tgsess.fetch(["thehackernews"], 5) == []
|
||||
assert tgsess._key("@SomeChan/") == "SomeChan"
|
||||
|
||||
|
||||
def test_item_cve_prefix():
|
||||
from types import SimpleNamespace
|
||||
from omni.feeds import _item
|
||||
src = SimpleNamespace(name="tg_cvefeed", tags=["cve"])
|
||||
it = _item(src, "post", "https://t.me/cvefeed/1", "Improper neutralization of input",
|
||||
"Details: CVE-2026-12345 affects the parser badly.")
|
||||
assert it.title.startswith("CVE-2026-12345"), it.title
|
||||
|
||||
84
tools/tgpump.py
Normal file
84
tools/tgpump.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Заливка истории Telegram-каналов через свою сессию (Telethon) в базу omni.
|
||||
|
||||
Веб-превью t.me/s даёт ~20 постов, сессия — 200+. Скрипт берёт живую сессию из
|
||||
пула аудита, тянет каналы и складывает посты как обычные записи (source = tg_<канал>),
|
||||
чтобы они получили типы, скор и попали в панель/бот.
|
||||
|
||||
Запуск:
|
||||
python tools/tgpump.py --pool /c/Users/User/tmp/tg_chat_grow/alive_orca.json \
|
||||
--channels thehackernews,cvefeed,true_secator --limit 200 [--apply]
|
||||
|
||||
Без --apply только считает (dry-run). Секретов в коде нет: api_id/hash из env
|
||||
TG_API_ID/TG_API_HASH (по умолчанию официальные desktop-значения), путь к сессии —
|
||||
из запуска.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from omni import store, tgsess # noqa: E402
|
||||
from omni.feeds import _item # noqa: E402
|
||||
|
||||
|
||||
def pick_session(pool_path, skip=()):
|
||||
rows = [r for r in json.loads(Path(pool_path).read_text(encoding="utf-8"))
|
||||
if r.get("state") == "alive" and r["session"] not in skip]
|
||||
if not rows:
|
||||
raise SystemExit("в пуле нет живых сессий")
|
||||
return rows[0]["session"]
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--pool", default="/c/Users/User/tmp/tg_chat_grow/alive_orca.json")
|
||||
ap.add_argument("--sessions-root", default=str(Path.home() / ".orca" / "sessions"))
|
||||
ap.add_argument("--channels", required=True, help="через запятую или @файл")
|
||||
ap.add_argument("--limit", type=int, default=200)
|
||||
ap.add_argument("--apply", action="store_true")
|
||||
a = ap.parse_args()
|
||||
|
||||
chans = a.channels
|
||||
if chans.startswith("@"):
|
||||
chans = Path(chans[1:]).read_text(encoding="utf-8").replace("\n", ",")
|
||||
handles = [c.strip() for c in chans.split(",") if c.strip()]
|
||||
|
||||
sess = pick_session(a.pool)
|
||||
os.environ["OMNI_TG_SESSION"] = str(Path(a.sessions_root) / sess)[:-8]
|
||||
os.environ.setdefault("OMNI_TG_API_ID", os.environ.get("TG_API_ID", "2040"))
|
||||
os.environ.setdefault("OMNI_TG_API_HASH",
|
||||
os.environ.get("TG_API_HASH", "b18441a1ff607e10a989891a5462e627"))
|
||||
os.environ["OMNI_TG_LIMIT"] = str(a.limit)
|
||||
|
||||
print(f"сессия {sess} | каналов {len(handles)} | лимит {a.limit}")
|
||||
res = tgsess.fetch(handles, a.limit)
|
||||
total_posts, items, per = 0, [], []
|
||||
for rec in res:
|
||||
h = rec["handle"]
|
||||
posts = rec.get("posts") or []
|
||||
total_posts += len(posts)
|
||||
per.append((h, len(posts), rec.get("error", "")))
|
||||
src = SimpleNamespace(name="tg_" + h.replace("-", "_"), tags=["abuse", "tg"])
|
||||
for p in posts:
|
||||
if len(p["text"]) < 12:
|
||||
continue
|
||||
items.append(_item(src, "post", p["url"], p["title"], p["text"],
|
||||
author="@" + h, published=p["date"],
|
||||
extra={"views": p["views"], "handle": h, "via": "session"}))
|
||||
print("постов получено:", total_posts, "| к записи:", len(items))
|
||||
for h, n, err in per:
|
||||
print(f" {h:24} {n:4} {err}")
|
||||
|
||||
if not a.apply:
|
||||
print("dry-run. добавь --apply чтобы записать в базу")
|
||||
return
|
||||
new, dup = store.add(items)
|
||||
print(f"записано новых: {new}, дублей: {dup}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user