rpi: tg_members.py — участники группы с аватарками (CY Fast Track Talks)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
15a9e1d2de
commit
1a525e9073
@@ -63,3 +63,7 @@ sudo systemctl enable --now tg-export.timer
|
|||||||
сообщения после последней ежечасной докачки уйдут в следующий рекап, без дыр), до 60 тыс. знаков (больше — последние),
|
сообщения после последней ежечасной докачки уйдут в следующий рекап, без дыр), до 60 тыс. знаков (больше — последние),
|
||||||
Codex пишет темы дня и «Полезное» со ссылками → очередь outbox. Сейчас: CY Fast Track Talks (@cyfasttrack).
|
Codex пишет темы дня и «Полезное» со ссылками → очередь outbox. Сейчас: CY Fast Track Talks (@cyfasttrack).
|
||||||
`tg digest run --dry --hours 24` — посмотреть.
|
`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. Разовая выгрузка, не по таймеру.
|
||||||
|
|||||||
@@ -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]))
|
||||||
Reference in New Issue
Block a user