175 lines
6.7 KiB
Python
175 lines
6.7 KiB
Python
"""Telegram через свою сессию (Telethon): глубина и закрытые каналы.
|
||
|
||
Зачем, если есть веб-превью t.me/s: превью отдаёт ~20 последних постов и ничего
|
||
не отдаёт для каналов с выключенным превью. Сессия тянет 200+ постов и читает
|
||
каналы/группы, куда аккаунт уже вступил.
|
||
|
||
Конфиг только через env (секретов в репозитории нет):
|
||
OMNI_TG_SESSION путь к .session без расширения
|
||
OMNI_TG_API_ID api_id (my.telegram.org)
|
||
OMNI_TG_API_HASH api_hash
|
||
OMNI_TG_LIMIT сколько постов тянуть на канал (по умолчанию 100)
|
||
|
||
Если env не задан — модуль молча выключен, парсер работает на веб-превью.
|
||
"""
|
||
import asyncio
|
||
import os
|
||
import re
|
||
from datetime import datetime, timezone
|
||
|
||
SERVICE_RE = re.compile(
|
||
r"^(channel (created|photo|name|description|username|deleted)|pinned|message pinned|"
|
||
r"channel photo (updated|removed)|joined|left|invite link)", re.I)
|
||
|
||
|
||
def enabled():
|
||
try:
|
||
from . import config
|
||
config.tg_cfg()
|
||
except Exception:
|
||
pass
|
||
return bool(os.environ.get("OMNI_TG_SESSION") and os.environ.get("OMNI_TG_API_ID")
|
||
and os.environ.get("OMNI_TG_API_HASH"))
|
||
|
||
|
||
def _key(handle):
|
||
return handle.strip().strip("/").split("/")[-1].lstrip("@")
|
||
|
||
|
||
async def _pull(handles, limit, session, api_id, api_hash, timeout=40):
|
||
from telethon import TelegramClient
|
||
|
||
client = TelegramClient(session, api_id, api_hash, connection_retries=1, timeout=timeout / 2)
|
||
out = []
|
||
await asyncio.wait_for(client.connect(), timeout)
|
||
try:
|
||
for h in handles:
|
||
hh = _key(h)
|
||
rec = {"handle": hh, "posts": []}
|
||
try:
|
||
ent = await asyncio.wait_for(client.get_entity(hh), timeout)
|
||
msgs = await asyncio.wait_for(client.get_messages(ent, limit=limit), timeout)
|
||
rec["title"] = getattr(ent, "title", "") or hh
|
||
rec["id"] = getattr(ent, "id", None)
|
||
for m in msgs:
|
||
text = (getattr(m, "message", "") or "").strip()
|
||
if not text or SERVICE_RE.match(text):
|
||
continue
|
||
rec["posts"].append({
|
||
"id": getattr(m, "id", None),
|
||
"url": "https://t.me/%s/%s" % (hh, getattr(m, "id", "")),
|
||
"title": text.splitlines()[0][:300],
|
||
"text": text[:6000],
|
||
"views": getattr(m, "views", None),
|
||
"date": (m.date.astimezone(timezone.utc).isoformat()
|
||
if isinstance(getattr(m, "date", None), datetime) else ""),
|
||
"channel": hh,
|
||
})
|
||
except Exception as e:
|
||
rec["error"] = type(e).__name__
|
||
out.append(rec)
|
||
finally:
|
||
try:
|
||
await asyncio.wait_for(client.disconnect(), 10)
|
||
except Exception:
|
||
pass
|
||
return out
|
||
|
||
|
||
def fetch(handles, limit=None):
|
||
"""Список словарей {handle, title, id, posts[]} по каналам. Пусто, если нет env."""
|
||
if not enabled():
|
||
return []
|
||
try:
|
||
from . import config
|
||
config.tg_cfg()
|
||
except Exception:
|
||
pass
|
||
session = os.environ["OMNI_TG_SESSION"]
|
||
api_id = int(os.environ["OMNI_TG_API_ID"])
|
||
api_hash = os.environ["OMNI_TG_API_HASH"]
|
||
lim = int(limit or os.environ.get("OMNI_TG_LIMIT") or 100)
|
||
return asyncio.run(_pull(list(handles), lim, session, api_id, api_hash))
|
||
|
||
async def _search(query, limit, session, api_id, api_hash, timeout=60):
|
||
"""Глобальный поиск каналов/групп по запросу (contacts.SearchRequest)."""
|
||
from telethon import TelegramClient
|
||
from telethon.tl.functions.contacts import SearchRequest
|
||
|
||
client = TelegramClient(session, api_id, api_hash, connection_retries=1, timeout=timeout / 2)
|
||
await asyncio.wait_for(client.connect(), timeout)
|
||
try:
|
||
res = await asyncio.wait_for(client(SearchRequest(q=str(query), limit=int(limit))), timeout)
|
||
out = []
|
||
for ch in getattr(res, "chats", []) or []:
|
||
uname = getattr(ch, "username", None)
|
||
if not uname:
|
||
continue
|
||
out.append({"username": uname, "title": getattr(ch, "title", "") or "",
|
||
"id": getattr(ch, "id", None),
|
||
"broadcast": bool(getattr(ch, "broadcast", False)),
|
||
"megagroup": bool(getattr(ch, "megagroup", False)),
|
||
"participants": getattr(ch, "participants_count", None) or 0,
|
||
"query": str(query)})
|
||
return out
|
||
finally:
|
||
try:
|
||
await asyncio.wait_for(client.disconnect(), 10)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def search(query, limit=40):
|
||
"""Каналы по ключевому слову. Пусто, если сессия не настроена."""
|
||
if not enabled():
|
||
return []
|
||
try:
|
||
from . import config
|
||
config.tg_cfg()
|
||
except Exception:
|
||
pass
|
||
session = os.environ["OMNI_TG_SESSION"]
|
||
api_id = int(os.environ["OMNI_TG_API_ID"])
|
||
api_hash = os.environ["OMNI_TG_API_HASH"]
|
||
return asyncio.run(_search(query, limit, session, api_id, api_hash))
|
||
|
||
|
||
_ME = {}
|
||
|
||
|
||
async def _me(session, api_id, api_hash, timeout=40):
|
||
from telethon import TelegramClient
|
||
|
||
client = TelegramClient(session, api_id, api_hash, connection_retries=1, timeout=timeout / 2)
|
||
await asyncio.wait_for(client.connect(), timeout)
|
||
try:
|
||
u = await asyncio.wait_for(client.get_me(), timeout)
|
||
if not u:
|
||
return {}
|
||
return {"id": getattr(u, "id", None), "username": getattr(u, "username", None),
|
||
"first_name": getattr(u, "first_name", None), "premium": bool(getattr(u, "premium", False)),
|
||
"phone_tail": (str(getattr(u, "phone", "") or "")[-4:] or None)}
|
||
finally:
|
||
try:
|
||
await asyncio.wait_for(client.disconnect(), 10)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def me():
|
||
"""Аккаунт сессии: {id, username, first_name, premium}. Кэш на процесс."""
|
||
if _ME:
|
||
return _ME
|
||
if not enabled():
|
||
return {}
|
||
try:
|
||
from . import config
|
||
config.tg_cfg()
|
||
except Exception:
|
||
pass
|
||
session = os.environ["OMNI_TG_SESSION"]
|
||
api_id = int(os.environ["OMNI_TG_API_ID"])
|
||
api_hash = os.environ["OMNI_TG_API_HASH"]
|
||
_ME.update(asyncio.run(_me(session, api_id, api_hash)) or {})
|
||
return _ME
|