107 lines
5.5 KiB
Python
107 lines
5.5 KiB
Python
#!/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]))
|