rpi: tg_members.py — участники группы с аватарками (CY Fast Track Talks)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
gilfanovr
2026-10-18 20:18:07 +03:00
co-authored by Claude Opus 5.5
parent 15a9e1d2de
commit 1a525e9073
2 changed files with 80 additions and 0 deletions
+4
View File
@@ -63,3 +63,7 @@ sudo systemctl enable --now tg-export.timer
сообщения после последней ежечасной докачки уйдут в следующий рекап, без дыр), до 60 тыс. знаков (больше — последние),
Codex пишет темы дня и «Полезное» со ссылками → очередь outbox. Сейчас: CY Fast Track Talks (@cyfasttrack).
`tg digest run --dry --hours 24` — посмотреть.
- **Участники группы с аватарками** — `app/tg_members.py` (`/opt/tg-export/tg_members.py @чат`, от tgexport, ⚠️ не параллельно
с `tg-export.service` — одна сессия): `/mnt/backup/tg/members/<id>_<название>/members.{json,csv}` + `avatars/<id>.jpg`
(маленькие). Персональные данные участников — в поиск/индекс не идут. 2026-10-18: CY Fast Track Talks — 2774 из 2774,
аватарок 1619. Разовая выгрузка, не по таймеру.
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Участники группы с аватарками (владелец 2026-10-18): /mnt/backup/tg/members/<id>_<название>/
members.json и members.csv (id, username, имя, бот/премиум, статус, есть ли фото), avatars/<id>.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]))