Introduce background scheduling and migrations for analytics ingest, expand parser management and map toolbar UX, and ignore local data/session files from version control. Co-authored-by: Cursor <cursoragent@cursor.com>
123 lines
3.9 KiB
Python
123 lines
3.9 KiB
Python
"""Telegram Client API (Telethon) для чтения постов канала."""
|
||
|
||
import os
|
||
import re
|
||
from pathlib import Path
|
||
|
||
from telethon import TelegramClient
|
||
from telethon.errors import AuthKeyUnregisteredError, SessionPasswordNeededError
|
||
|
||
from workers.models import TelegramPost
|
||
from workers.sources.telegram_settings import create_client, get_api_credentials
|
||
|
||
DEFAULT_SESSION_PATH = "/data/telegram.session"
|
||
MAX_POSTS = 500
|
||
|
||
|
||
class TelegramConfigError(Exception):
|
||
pass
|
||
|
||
|
||
class TelegramAuthError(Exception):
|
||
pass
|
||
|
||
|
||
def _get_config() -> tuple[int, str, str]:
|
||
api_id, api_hash = get_api_credentials()
|
||
session_path = os.environ.get("TELEGRAM_SESSION_PATH", DEFAULT_SESSION_PATH)
|
||
return api_id, api_hash, session_path
|
||
|
||
|
||
def normalize_channel(raw: str) -> str:
|
||
value = raw.strip()
|
||
if not value:
|
||
raise ValueError("Укажите канал Telegram")
|
||
|
||
value = value.replace("https://", "").replace("http://", "")
|
||
value = re.sub(r"^web\.telegram\.org/k/#@", "", value)
|
||
value = re.sub(r"^t\.me/", "", value)
|
||
value = value.lstrip("@").split("/")[0].split("?")[0]
|
||
if not value:
|
||
raise ValueError("Некорректное имя канала")
|
||
return value
|
||
|
||
|
||
def _build_post_url(channel: str, message_id: int) -> str:
|
||
username = channel.lstrip("@")
|
||
return f"https://t.me/{username}/{message_id}"
|
||
|
||
|
||
def message_to_post(message, channel: str) -> TelegramPost | None:
|
||
text = (message.text or message.message or "").strip()
|
||
if not text:
|
||
return None
|
||
username = normalize_channel(channel)
|
||
return TelegramPost(
|
||
id=message.id,
|
||
text=text,
|
||
date=message.date,
|
||
url=_build_post_url(username, message.id),
|
||
channel=username,
|
||
)
|
||
|
||
|
||
async def fetch_channel_posts(
|
||
channel: str,
|
||
limit: int = 100,
|
||
*,
|
||
client: TelegramClient | None = None,
|
||
) -> list[TelegramPost]:
|
||
limit = max(1, min(limit, MAX_POSTS))
|
||
username = normalize_channel(channel)
|
||
own_client = client is None
|
||
|
||
if own_client:
|
||
try:
|
||
api_id, api_hash, session_path = _get_config()
|
||
except ValueError as exc:
|
||
raise TelegramConfigError(str(exc)) from exc
|
||
|
||
if not Path(session_path).exists():
|
||
raise TelegramAuthError(
|
||
f"Файл сессии не найден: {session_path}. "
|
||
"Выполните: python scripts/telegram_auth.py"
|
||
)
|
||
|
||
client = create_client(session_path, api_id, api_hash)
|
||
posts: list[TelegramPost] = []
|
||
|
||
try:
|
||
await client.connect()
|
||
if not await client.is_user_authorized():
|
||
raise TelegramAuthError(
|
||
"Telegram-сессия не авторизована. Выполните: python scripts/telegram_auth.py"
|
||
)
|
||
|
||
entity = await client.get_entity(username)
|
||
async for message in client.iter_messages(entity, limit=limit):
|
||
post = message_to_post(message, username)
|
||
if post:
|
||
posts.append(post)
|
||
except AuthKeyUnregisteredError as exc:
|
||
raise TelegramAuthError(
|
||
"Сессия Telegram недействительна. Переавторизуйтесь: python scripts/telegram_auth.py"
|
||
) from exc
|
||
except SessionPasswordNeededError as exc:
|
||
raise TelegramAuthError(
|
||
"Для аккаунта включена двухфакторная аутентификация. "
|
||
"Авторизуйтесь через scripts/telegram_auth.py с паролем 2FA."
|
||
) from exc
|
||
finally:
|
||
await client.disconnect()
|
||
|
||
return posts
|
||
|
||
assert client is not None
|
||
posts = []
|
||
entity = await client.get_entity(username)
|
||
async for message in client.iter_messages(entity, limit=limit):
|
||
post = message_to_post(message, username)
|
||
if post:
|
||
posts.append(post)
|
||
return posts
|