tg-сессия в комбайне + панель: ключи/сессия в шапке + фикс пустого radar_sources
- omni/config.py: tg_cfg() читает data/tg.json (вне git), env приоритетнее
- omni/tgsess.py: enabled()/fetch() подтягивают креды из config
- tools/tg_pick.py: перебор пула сессий, проверка is_user_authorized, личные аккаунты исключены
- omni claw pump: 2 канала -> 27 постов через сессию (проверено)
- omni/serve.py: /api/stats += keys{totals}, tg{enabled}
- web/index.html: строка статуса в шапке (ключей / сервисов / TG сессия)
- omni/cli.py radar --apply: пишет полный набор, а не только новые (повторный прогон обнулял файл)
- omni/cli.py radar --import-posts: --since-days + скип известных url до simhash
This commit is contained in:
36
omni/cli.py
36
omni/cli.py
@@ -1,5 +1,6 @@
|
|||||||
"""CLI: providers / search / fanout / cards / top / find / stats / export."""
|
"""CLI: providers / search / fanout / cards / top / find / stats / export."""
|
||||||
import argparse
|
import argparse
|
||||||
|
import datetime as _dt
|
||||||
import pathlib
|
import pathlib
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import json
|
import json
|
||||||
@@ -341,7 +342,15 @@ def cmd_radar(a):
|
|||||||
c = _sq.connect(db)
|
c = _sq.connect(db)
|
||||||
doms = c.execute("SELECT domain, score, info FROM domains ORDER BY score DESC").fetchall()
|
doms = c.execute("SELECT domain, score, info FROM domains ORDER BY score DESC").fetchall()
|
||||||
chans = c.execute("SELECT name, ok, fails FROM channels WHERE ok=1").fetchall()
|
chans = c.execute("SELECT name, ok, fails FROM channels WHERE ok=1").fetchall()
|
||||||
posts = c.execute("SELECT chan, mid, date, text FROM messages").fetchall() if a.import_posts else []
|
if a.import_posts:
|
||||||
|
if a.since_days:
|
||||||
|
cutoff = _dt.datetime.utcnow() - _dt.timedelta(days=a.since_days)
|
||||||
|
posts = c.execute("SELECT chan, mid, date, text FROM messages WHERE date >= ?",
|
||||||
|
(cutoff.strftime("%Y-%m-%d"),)).fetchall()
|
||||||
|
else:
|
||||||
|
posts = c.execute("SELECT chan, mid, date, text FROM messages").fetchall()
|
||||||
|
else:
|
||||||
|
posts = []
|
||||||
c.close()
|
c.close()
|
||||||
|
|
||||||
if a.import_posts:
|
if a.import_posts:
|
||||||
@@ -353,12 +362,19 @@ def cmd_radar(a):
|
|||||||
return ((txt or "").strip().splitlines() or [""])[0][:150]
|
return ((txt or "").strip().splitlines() or [""])[0][:150]
|
||||||
|
|
||||||
items, seen_u = [], set()
|
items, seen_u = [], set()
|
||||||
|
known = set()
|
||||||
|
try:
|
||||||
|
with store.db() as kc:
|
||||||
|
for (u,) in kc.execute("SELECT url FROM items WHERE url LIKE 'https://t.me/%'"):
|
||||||
|
known.add(u)
|
||||||
|
except Exception:
|
||||||
|
known = set()
|
||||||
for chan, mid, date, text in posts:
|
for chan, mid, date, text in posts:
|
||||||
text = (text or "").strip()
|
text = (text or "").strip()
|
||||||
if len(text) < 30:
|
if len(text) < 30:
|
||||||
continue
|
continue
|
||||||
url_u = "https://t.me/%s/%s" % (chan, mid)
|
url_u = "https://t.me/%s/%s" % (chan, mid)
|
||||||
if url_u in seen_u:
|
if url_u in seen_u or url_u in known:
|
||||||
continue
|
continue
|
||||||
seen_u.add(url_u)
|
seen_u.add(url_u)
|
||||||
items.append(Item("rad_" + chan, "post", url=url_u, title=head_line(text), text=text,
|
items.append(Item("rad_" + chan, "post", url=url_u, title=head_line(text), text=text,
|
||||||
@@ -376,20 +392,23 @@ def cmd_radar(a):
|
|||||||
have = {s.url.rstrip('/').lower() for s in _src.all_sources()}
|
have = {s.url.rstrip('/').lower() for s in _src.all_sources()}
|
||||||
have_names = {s.name for s in _src.all_sources()}
|
have_names = {s.name for s in _src.all_sources()}
|
||||||
|
|
||||||
|
full_dom, full_chan = [], []
|
||||||
new_dom, new_chan = [], []
|
new_dom, new_chan = [], []
|
||||||
for dom, score, info in doms:
|
for dom, score, info in doms:
|
||||||
if a.min_score and (score or 0) < a.min_score:
|
if a.min_score and (score or 0) < a.min_score:
|
||||||
continue
|
continue
|
||||||
url = "https://" + dom
|
|
||||||
if url.lower() in have or ("rad_" + dom.replace('.', '_').replace('-', '_')) in have_names:
|
|
||||||
continue
|
|
||||||
title = ""
|
title = ""
|
||||||
try:
|
try:
|
||||||
title = (_json.loads(info or "{}") or {}).get("title") or ""
|
title = (_json.loads(info or "{}") or {}).get("title") or ""
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
full_dom.append((dom, score, title))
|
||||||
|
url = "https://" + dom
|
||||||
|
if url.lower() in have or ("rad_" + dom.replace('.', '_').replace('-', '_')) in have_names:
|
||||||
|
continue
|
||||||
new_dom.append((dom, score, title))
|
new_dom.append((dom, score, title))
|
||||||
for name, ok, fails in chans:
|
for name, ok, fails in chans:
|
||||||
|
full_chan.append(name)
|
||||||
if ("tg_" + name) in have_names:
|
if ("tg_" + name) in have_names:
|
||||||
continue
|
continue
|
||||||
new_chan.append(name)
|
new_chan.append(name)
|
||||||
@@ -408,9 +427,11 @@ def cmd_radar(a):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
out = _P("data") / "radar_sources.json"
|
out = _P("data") / "radar_sources.json"
|
||||||
out.write_text(_json.dumps({"domains": new_dom, "channels": new_chan},
|
out.write_text(_json.dumps({"domains": full_dom, "channels": full_chan,
|
||||||
|
"new": len(new_dom)},
|
||||||
ensure_ascii=False, indent=1), encoding="utf-8")
|
ensure_ascii=False, indent=1), encoding="utf-8")
|
||||||
print("записано: %s (доменов %d, каналов %d)" % (out, len(new_dom), len(new_chan)))
|
print("записано: %s (доменов %d, каналов %d, из них новых %d)"
|
||||||
|
% (out, len(full_dom), len(full_chan), len(new_dom)))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -531,6 +552,7 @@ def main(argv=None):
|
|||||||
sp.add_argument("--apply", action="store_true", help="записать в реестр omni")
|
sp.add_argument("--apply", action="store_true", help="записать в реестр omni")
|
||||||
sp.add_argument("--min-score", type=int, default=20, help="минимальный score домена")
|
sp.add_argument("--min-score", type=int, default=20, help="минимальный score домена")
|
||||||
sp.add_argument("--import-posts", action="store_true", help="залить посты каналов радара в базу")
|
sp.add_argument("--import-posts", action="store_true", help="залить посты каналов радара в базу")
|
||||||
|
sp.add_argument("--since-days", type=int, default=0, help="только посты за N дней (0 = все)")
|
||||||
sp.add_argument("--limit", type=int, default=0)
|
sp.add_argument("--limit", type=int, default=0)
|
||||||
sp.set_defaults(fn=cmd_radar)
|
sp.set_defaults(fn=cmd_radar)
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ def load():
|
|||||||
cfg.update(json.loads(cf.read_text(encoding="utf-8")))
|
cfg.update(json.loads(cf.read_text(encoding="utf-8")))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
CONFIG = cfg
|
|
||||||
# cookies, снятые браузером (tools/cf_cookies.py) — файл вне git
|
# cookies, снятые браузером (tools/cf_cookies.py) — файл вне git
|
||||||
ck_file = DATA / "cookies.json"
|
ck_file = DATA / "cookies.json"
|
||||||
if ck_file.exists():
|
if ck_file.exists():
|
||||||
@@ -93,9 +92,38 @@ def load():
|
|||||||
if v:
|
if v:
|
||||||
KEYS[name] = v
|
KEYS[name] = v
|
||||||
DATA.mkdir(parents=True, exist_ok=True)
|
DATA.mkdir(parents=True, exist_ok=True)
|
||||||
|
CONFIG = cfg
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
def tg_cfg():
|
||||||
|
"""Креды Telegram-сессии: data/tg.json (вне git) или env. Возвращает dict.
|
||||||
|
|
||||||
|
Ключи: session (путь к .session без расширения), api_id, api_hash, limit.
|
||||||
|
env всегда приоритетнее файла.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
out = {"session": "", "api_id": "", "api_hash": "", "limit": "100"}
|
||||||
|
f = DATA / "tg.json"
|
||||||
|
if f.exists():
|
||||||
|
try:
|
||||||
|
d = json.loads(f.read_text(encoding="utf-8"))
|
||||||
|
for k in out:
|
||||||
|
if d.get(k):
|
||||||
|
out[k] = str(d[k])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
for env, key in (("OMNI_TG_SESSION", "session"), ("OMNI_TG_API_ID", "api_id"),
|
||||||
|
("OMNI_TG_API_HASH", "api_hash"), ("OMNI_TG_LIMIT", "limit")):
|
||||||
|
if os.environ.get(env):
|
||||||
|
out[key] = os.environ[env]
|
||||||
|
for env, key in (("OMNI_TG_SESSION", "session"), ("OMNI_TG_API_ID", "api_id"),
|
||||||
|
("OMNI_TG_API_HASH", "api_hash"), ("OMNI_TG_LIMIT", "limit")):
|
||||||
|
if out[key]:
|
||||||
|
os.environ.setdefault(env, out[key])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def proxies():
|
def proxies():
|
||||||
mode = CONFIG.get("proxy_mode", "off")
|
mode = CONFIG.get("proxy_mode", "off")
|
||||||
if mode == "env":
|
if mode == "env":
|
||||||
|
|||||||
@@ -219,8 +219,19 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
cats = _cat_health()
|
cats = _cat_health()
|
||||||
for c in cats:
|
for c in cats:
|
||||||
c["stored"] = sum(per_source.get(x.name, 0) for x in all_s if x.category == c["name"])
|
c["stored"] = sum(per_source.get(x.name, 0) for x in all_s if x.category == c["name"])
|
||||||
|
kv = {"services": 0, "keys": 0, "ready": 0}
|
||||||
|
tgst = {"enabled": False}
|
||||||
|
try:
|
||||||
|
from . import vault as _vault, tgsess as _tg
|
||||||
|
st_kv = _vault.stats()
|
||||||
|
kv = {"services": len(st_kv), "keys": sum(x["keys"] for x in st_kv),
|
||||||
|
"ready": sum(x["ready"] for x in st_kv)}
|
||||||
|
tgst = {"enabled": bool(_tg.enabled())}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return self._json({
|
return self._json({
|
||||||
"items": s["items"], "priced": s["priced"], "runs": s["runs"], "runs_ok": s["runs_ok"],
|
"items": s["items"], "priced": s["priced"], "runs": s["runs"], "runs_ok": s["runs_ok"],
|
||||||
|
"keys": kv, "tg": tgst,
|
||||||
"by_provider": [{"name": n, "count": c} for n, c in s["by_provider"][:12]],
|
"by_provider": [{"name": n, "count": c} for n, c in s["by_provider"][:12]],
|
||||||
"by_kind": [{"name": n, "count": c} for n, c in s["by_kind"]],
|
"by_kind": [{"name": n, "count": c} for n, c in s["by_kind"]],
|
||||||
"sources": len(all_s),
|
"sources": len(all_s),
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ SERVICE_RE = re.compile(
|
|||||||
|
|
||||||
|
|
||||||
def enabled():
|
def enabled():
|
||||||
|
try:
|
||||||
|
from . import config
|
||||||
|
config.tg_cfg()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return bool(os.environ.get("OMNI_TG_SESSION") and os.environ.get("OMNI_TG_API_ID")
|
return bool(os.environ.get("OMNI_TG_SESSION") and os.environ.get("OMNI_TG_API_ID")
|
||||||
and os.environ.get("OMNI_TG_API_HASH"))
|
and os.environ.get("OMNI_TG_API_HASH"))
|
||||||
|
|
||||||
@@ -75,6 +80,11 @@ def fetch(handles, limit=None):
|
|||||||
"""Список словарей {handle, title, id, posts[]} по каналам. Пусто, если нет env."""
|
"""Список словарей {handle, title, id, posts[]} по каналам. Пусто, если нет env."""
|
||||||
if not enabled():
|
if not enabled():
|
||||||
return []
|
return []
|
||||||
|
try:
|
||||||
|
from . import config
|
||||||
|
config.tg_cfg()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
session = os.environ["OMNI_TG_SESSION"]
|
session = os.environ["OMNI_TG_SESSION"]
|
||||||
api_id = int(os.environ["OMNI_TG_API_ID"])
|
api_id = int(os.environ["OMNI_TG_API_ID"])
|
||||||
api_hash = os.environ["OMNI_TG_API_HASH"]
|
api_hash = os.environ["OMNI_TG_API_HASH"]
|
||||||
|
|||||||
110
tools/tg_pick.py
Normal file
110
tools/tg_pick.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
"""Выбрать рабочую Telegram-сессию и прописать её в data/tg.json (вне git).
|
||||||
|
|
||||||
|
Перебирает кандидатов из пула, проверяет is_user_authorized и права на чтение,
|
||||||
|
берёт первую живую. Личные аккаунты (@reformboss, @R3fIex) исключены всегда.
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
python tools/tg_pick.py # пул по умолчанию
|
||||||
|
python tools/tg_pick.py --file my.json # свой список путей
|
||||||
|
python tools/tg_pick.py --check # проверить, что уже прописано
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
OUT = ROOT / "data" / "tg.json"
|
||||||
|
DEFAULT_POOL = Path("C:/Users/User/tmp/tg_chat_grow/alive_sessions.json")
|
||||||
|
BANNED = {"reformboss", "r3flex", "r3fiex"}
|
||||||
|
API_ID = "23926822"
|
||||||
|
API_HASH = "f383e16e2f55bc0857291510fd513002"
|
||||||
|
|
||||||
|
|
||||||
|
def candidates(pool_file):
|
||||||
|
try:
|
||||||
|
data = json.loads(Path(pool_file).read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
paths = data if isinstance(data, list) else list(data.values())
|
||||||
|
out = []
|
||||||
|
for p in paths:
|
||||||
|
if not isinstance(p, str):
|
||||||
|
continue
|
||||||
|
low = Path(p).name.lower()
|
||||||
|
if any(b in low for b in BANNED):
|
||||||
|
continue
|
||||||
|
stem = p[:-8] if p.lower().endswith(".session") else p
|
||||||
|
if Path(stem + ".session").exists():
|
||||||
|
out.append(stem)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
async def probe(stem, timeout=25):
|
||||||
|
from telethon import TelegramClient
|
||||||
|
|
||||||
|
client = TelegramClient(stem, int(API_ID), API_HASH, connection_retries=1, timeout=12)
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(client.connect(), timeout)
|
||||||
|
ok = await asyncio.wait_for(client.is_user_authorized(), timeout)
|
||||||
|
if not ok:
|
||||||
|
return None
|
||||||
|
me = await asyncio.wait_for(client.get_me(), timeout)
|
||||||
|
if not me or (me.id in (7448683285, 809951394)):
|
||||||
|
return None
|
||||||
|
dialogs = 0
|
||||||
|
try:
|
||||||
|
async for _ in client.iter_dialogs(limit=200):
|
||||||
|
dialogs += 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"stem": stem, "uid": me.id, "username": me.username or "",
|
||||||
|
"phone": (me.phone or "")[:6] + "…", "dialogs": dialogs}
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(client.disconnect(), 8)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("-n", "--need", type=int, default=12, help="сколько кандидатов проверить")
|
||||||
|
ap.add_argument("--file", default=str(DEFAULT_POOL))
|
||||||
|
ap.add_argument("--check", action="store_true")
|
||||||
|
a = ap.parse_args()
|
||||||
|
|
||||||
|
if a.check:
|
||||||
|
if not OUT.exists():
|
||||||
|
print("не прописано")
|
||||||
|
return 1
|
||||||
|
d = json.loads(OUT.read_text(encoding="utf-8"))
|
||||||
|
print("прописано:", d.get("session"), "| limit", d.get("limit"))
|
||||||
|
rec = await probe(d["session"]) if d.get("session") else None
|
||||||
|
print("состояние:", "жива, uid %s, диалогов %s" % (rec["uid"], rec["dialogs"]) if rec else "не отвечает")
|
||||||
|
return 0 if rec else 2
|
||||||
|
|
||||||
|
cands = candidates(a.file)[: a.need]
|
||||||
|
print("кандидатов проверяю:", len(cands))
|
||||||
|
for stem in cands:
|
||||||
|
rec = await probe(stem)
|
||||||
|
print(" %-58s %s" % (Path(stem).name[:58], ("uid %s, диалогов %s" % (rec["uid"], rec["dialogs"])) if rec else "нет"))
|
||||||
|
if rec and rec["dialogs"] >= 3:
|
||||||
|
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
OUT.write_text(json.dumps({"session": stem, "api_id": API_ID, "api_hash": API_HASH,
|
||||||
|
"limit": "100"}, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||||
|
print("\nзаписано в data/tg.json: uid %s (@%s), диалогов %s"
|
||||||
|
% (rec["uid"], rec["username"] or "-", rec["dialogs"]))
|
||||||
|
return 0
|
||||||
|
print("\nживых не нашёл — подключи сессию вручную в data/tg.json")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(asyncio.run(main()))
|
||||||
@@ -231,6 +231,9 @@ select:focus,input:focus{border-color:rgba(124,108,244,.6)}
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---- меню v2 ---- */
|
/* ---- меню v2 ---- */
|
||||||
|
.stack{display:flex;gap:6px;flex-wrap:wrap;padding:2px 6px 10px}
|
||||||
|
.stack em{font:10px var(--mono);color:var(--dim2);letter-spacing:.05em;text-transform:uppercase;background:#131822;border:1px solid var(--line);border-radius:20px;padding:2px 7px;font-style:normal}
|
||||||
|
.stack em.on{color:#7fe3a1;border-color:#1f4a33;background:#0f1d16}
|
||||||
.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}
|
.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);
|
.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}
|
background:#11151d;color:var(--dim);border-radius:11px;font:12.5px inherit;cursor:pointer;transition:.15s}
|
||||||
@@ -284,6 +287,7 @@ select:focus,input:focus{border-color:rgba(124,108,244,.6)}
|
|||||||
<div><b>omni</b><span>source radar</span></div>
|
<div><b>omni</b><span>source radar</span></div>
|
||||||
<span class="ver" id="brand-n">—</span>
|
<span class="ver" id="brand-n">—</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="stack" id="brand-k"></div>
|
||||||
|
|
||||||
<button class="menu-btn" id="btn-palette"><i>⌘</i>Меню<span class="kbd">Ctrl K</span></button>
|
<button class="menu-btn" id="btn-palette"><i>⌘</i>Меню<span class="kbd">Ctrl K</span></button>
|
||||||
|
|
||||||
@@ -547,6 +551,12 @@ async function loadStats() {
|
|||||||
$('#nav-src-n').textContent = fmt(s.sources);
|
$('#nav-src-n').textContent = fmt(s.sources);
|
||||||
$('#nav-runs-n').textContent = fmt(s.runs);
|
$('#nav-runs-n').textContent = fmt(s.runs);
|
||||||
$('#brand-n').textContent = fmt(s.alive) + '/' + fmt(s.sources);
|
$('#brand-n').textContent = fmt(s.alive) + '/' + fmt(s.sources);
|
||||||
|
const _bk = $('#brand-k');
|
||||||
|
if (_bk && s.keys) {
|
||||||
|
const tge = !!(s.tg && s.tg.enabled);
|
||||||
|
_bk.innerHTML = `<em>${fmt(s.keys.keys)} ключей</em><em>${fmt(s.keys.services)} сервисов</em>` +
|
||||||
|
`<em class="${tge ? 'on' : ''}">TG ${tge ? 'сессия' : 'веб-превью'}</em>`;
|
||||||
|
}
|
||||||
$('#aside-foot').innerHTML =
|
$('#aside-foot').innerHTML =
|
||||||
`<b>${fmt(s.sources)}</b> источников · <b>${fmt(s.sources_enabled)}</b> вкл<br>` +
|
`<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.alive)}</b> живых · <b>${fmt(s.tested)}</b> проверено<br>` +
|
||||||
|
|||||||
Reference in New Issue
Block a user