feat(tg): своя сессия Telethon — omni/tgsess.py + omni pump (глубина 200 постов вместо 20, 20 каналов -> 2832 поста, 2592 новых), CVE-ID в заголовке (471), tg_alive_audit из 856 сессий нашёл 121 живую; тесты 22
Some checks failed
ci / test (3.10) (push) Has been cancelled
ci / test (3.11) (push) Has been cancelled
ci / test (3.12) (push) Has been cancelled

This commit is contained in:
Vlad Gothbreach
2026-10-26 22:43:21 +03:00
parent 74d968ab2d
commit aa417d635b
7 changed files with 266 additions and 17 deletions

View File

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

View File

@@ -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
View 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))