#!/usr/bin/env python3 """Участники группы с аватарками (владелец 2026-10-18): /mnt/backup/tg/members/_<название>/ members.json и members.csv (id, username, имя, бот/премиум, статус, есть ли фото), avatars/.jpg (маленькие). sudo -u tgexport sh -c '. /etc/tg-export/env; /opt/tg-export/venv/bin/python /opt/tg-export/tg_members.py @cyfasttrack' ⚠️ Не запускать одновременно с tg-export.service (одна сессия). Если админы скрыли список участников — Telegram отдаст только админов. Это персональные данные участников: в поиск/индекс не идут. """ import asyncio import csv import json import os import sys from pathlib import Path from telethon import TelegramClient, errors import export as ex OUT = Path(os.environ.get("TG_OUT", "/mnt/backup/tg")) / "members" def status_of(u): s = getattr(u, "status", None) if s is None: return "" name = type(s).__name__.replace("UserStatus", "") when = getattr(s, "was_online", None) return f"{name} {when.isoformat()}" if when else name async def main(chat): client = TelegramClient("/var/lib/tg-export/tg.session", int(os.environ["TG_API_ID"]), os.environ["TG_API_HASH"]) client.flood_sleep_threshold = 3600 await client.start() try: ent = await client.get_entity(chat) folder = OUT / f"-100{ent.id}_{ex.slugify(getattr(ent, 'title', chat))}" av = folder / "avatars" av.mkdir(parents=True, exist_ok=True) people = await client.get_participants(ent, aggressive=True) rows = [] for i, u in enumerate(people, 1): photo = "" if getattr(u, "photo", None): dest = av / f"{u.id}.jpg" if not dest.exists(): try: await client.download_profile_photo(u, file=str(dest), download_big=False) except errors.FloodWaitError as e: await asyncio.sleep(e.seconds + 1) except Exception as e: # фото недоступно — пропускаем print(f" {u.id}: фото не скачалось ({type(e).__name__})", flush=True) photo = dest.name if dest.exists() else "" p = getattr(u, "participant", None) rows.append({"id": u.id, "username": u.username or "", "first_name": u.first_name or "", "last_name": u.last_name or "", "bot": bool(u.bot), "premium": bool(getattr(u, "premium", False)), "deleted": bool(u.deleted), "status": status_of(u), "role": type(p).__name__.replace("ChannelParticipant", "") if p else "", "joined": p.date.isoformat() if getattr(p, "date", None) else "", "avatar": photo}) if i % 100 == 0: print(f" {i}/{len(people)}", flush=True) (folder / "members.json").write_text(json.dumps({"chat": getattr(ent, "title", chat), "id": f"-100{ent.id}", "count": len(rows), "members": rows}, ensure_ascii=False, indent=1)) with (folder / "members.csv").open("w", newline="", encoding="utf-8") as f: w = csv.DictWriter(f, fieldnames=list(rows[0].keys()) if rows else ["id"]) w.writeheader() w.writerows(rows) total = getattr(await client.get_participants(ent, limit=0), "total", None) print(f"{folder.name}: участников получено {len(rows)} (в группе всего {total}), аватарок {sum(1 for r in rows if r['avatar'])}") finally: await client.disconnect() asyncio.run(main(sys.argv[1]))