Files
infra/stacks/rpi-tg-export/app/tg_bios.py
T

107 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Ссылки на группы/каналы в профилях участников (владелец 2026-10-18): по members.json группы — bio и «личный канал»
каждого участника (GetFullUser, ~1 запрос/с), затем размер каждой найденной группы/канала → топ по участникам.
sudo -u tgexport sh -c '. /etc/tg-export/env; /opt/tg-export/venv/bin/python /opt/tg-export/tg_bios.py <папка members>'
Результат в той же папке: bios.json (id → bio, личный канал; докачка с места остановки), links.json (группа/канал →
кто ссылается, размер), top.txt. ⚠️ Не параллельно с tg-export.service (одна сессия).
"""
import asyncio
import json
import os
import re
import sys
from pathlib import Path
from telethon import TelegramClient, errors
from telethon.tl.functions.channels import GetFullChannelRequest
from telethon.tl.functions.users import GetFullUserRequest
LINK = re.compile(r"(?:https?://)?(?:t\.me|telegram\.me)/(?:s/)?([A-Za-z][\w]{3,31})(?![\w/])|(?<![\w@])@([A-Za-z][\w]{3,31})")
SKIP = {"joinchat", "addstickers", "share", "proxy", "socks", "iv", "c", "addlist", "boost"}
async def main(folder):
folder = Path(folder)
members = json.loads((folder / "members.json").read_text())["members"]
bios_f = folder / "bios.json"
bios = json.loads(bios_f.read_text()) if bios_f.exists() else {}
c = TelegramClient("/var/lib/tg-export/tg.session", int(os.environ["TG_API_ID"]), os.environ["TG_API_HASH"])
c.flood_sleep_threshold = 0 # FloodWait ловим сами, с логом
await c.start()
try:
todo = [m for m in members if not m["bot"] and not m["deleted"] and str(m["id"]) not in bios]
print(f"профилей: {len(members)}, уже есть {len(bios)}, осталось {len(todo)}", flush=True)
for i, m in enumerate(todo, 1):
while True:
try:
full = await c(GetFullUserRequest(m["id"]))
break
except errors.FloodWaitError as e:
print(f" FloodWait {e.seconds} с на {i}/{len(todo)}", flush=True)
await asyncio.sleep(e.seconds + 2)
except Exception as e:
full = None
print(f" {m['id']}: {type(e).__name__}", flush=True)
break
rec = {"username": m["username"], "bio": "", "channel": None}
if full:
rec["bio"] = full.full_user.about or ""
pc = getattr(full.full_user, "personal_channel_id", None)
if pc:
ch = next((x for x in full.chats if x.id == pc), None)
rec["channel"] = {"id": pc, "username": getattr(ch, "username", None), "title": getattr(ch, "title", None)}
bios[str(m["id"])] = rec
if i % 50 == 0:
bios_f.write_text(json.dumps(bios, ensure_ascii=False))
print(f" {i}/{len(todo)}", flush=True)
await asyncio.sleep(1.0)
bios_f.write_text(json.dumps(bios, ensure_ascii=False))
# ссылки: username → кто ссылается
refs = {}
for uid, r in bios.items():
found = set()
for a, b in LINK.findall(r["bio"] or ""):
u = (a or b).lower()
if u not in SKIP:
found.add(u)
if r["channel"] and r["channel"].get("username"):
found.add(r["channel"]["username"].lower())
for u in found:
refs.setdefault(u, set()).add(r["username"] or uid)
print(f"уникальных ссылок: {len(refs)}", flush=True)
links = {}
for i, (u, who) in enumerate(sorted(refs.items()), 1):
info = {"refs": sorted(who), "kind": None, "title": None, "members": None}
while True:
try:
e = await c.get_entity(u)
if getattr(e, "broadcast", None) is not None or getattr(e, "megagroup", None) is not None:
full = await c(GetFullChannelRequest(e))
info.update(kind="channel" if e.broadcast else "group", title=e.title,
members=full.full_chat.participants_count)
else:
info["kind"] = "user" if hasattr(e, "first_name") else type(e).__name__
break
except errors.FloodWaitError as ex:
print(f" FloodWait {ex.seconds} с на ссылке {i}", flush=True)
await asyncio.sleep(ex.seconds + 2)
except Exception as ex:
info["kind"] = f"error:{type(ex).__name__}"
break
links[u] = info
await asyncio.sleep(1.5)
(folder / "links.json").write_text(json.dumps(links, ensure_ascii=False, indent=1))
top = sorted(((u, x) for u, x in links.items() if x["members"]), key=lambda t: -t[1]["members"])
lines = [f"{n}. {x['title']} (@{u}) — {x['kind']}, {x['members']} участников; в профилях: {', '.join('@' + w if not w.isdigit() else w for w in x['refs'][:5])}"
for n, (u, x) in enumerate(top[:30], 1)]
(folder / "top.txt").write_text("\n".join(lines) + "\n")
print("\n".join(lines[:10]))
finally:
await c.disconnect()
asyncio.run(main(sys.argv[1]))