# Telegram Bot API клиент: httpx + SOCKS5-прокси. # TG из РФ заблокирован -> все запросы через TG_PROXY (socks5://127.0.0.1:1080). import httpx from .config import settings API = settings.api_base TIMEOUT = 20.0 class TelegramError(Exception): """Ошибка Bot API / сети. Код для HTTP-ответа сервиса.""" def __init__(self, message: str, http_code: int = 502, detail: str = ""): super().__init__(message) self.message = message self.http_code = http_code # какой HTTP-статус вернуть вызывающему self.detail = detail def _client() -> httpx.Client: if not settings.bot_token: raise TelegramError("VESTI_BOT_TOKEN не задан", http_code=500, detail="missing token") return httpx.Client(proxy=settings.tg_proxy, timeout=TIMEOUT) def _url(method: str) -> str: return f"{API}/bot{settings.bot_token}/{method}" def _call(method: str, **params) -> dict: """Выполняет метод Bot API. Возвращает result. Ошибки -> TelegramError.""" try: with _client() as c: r = c.post(_url(method), json=params, timeout=TIMEOUT) except Exception as e: raise TelegramError(f"Сеть/прокси до Bot API: {e}", http_code=502) from e if r.status_code != 200: # 401 bad token, 403 forbidden (бот не админ) и т.п. try: body = r.json() desc = body.get("description", "") except Exception: desc = r.text[:200] raise TelegramError( f"Bot API {method}: HTTP {r.status_code} {desc}", http_code=403 if r.status_code == 403 else 502, detail=desc, ) data = r.json() if not data.get("ok"): desc = data.get("description", "") raise TelegramError(f"Bot API {method}: {desc}", http_code=502, detail=desc) return data.get("result", {}) def send_message(chat_id: str, text: str, parse_mode: str | None = None) -> int: """Отправляет текст. По умолчанию — plain (карточка репоста: исходный текст поста с сырыми символами <,>,&; ссылки Telegram распознаёт сам). link_preview_options={is_disabled:True} — НЕ показывать предпросмотр ссылки (чтобы канал не выглядел как агрегатор ссылок с превью).""" params = { "chat_id": chat_id, "text": text, "link_preview_options": {"is_disabled": True}, } if parse_mode: params["parse_mode"] = parse_mode return _call("sendMessage", **params)["message_id"] def send_photo(chat_id: str, photo_path: str, caption: str = "") -> int: """Отправляет фото (медиа первым). Возвращает message_id.""" if not photo_path: raise TelegramError("photo_path пуст", http_code=400) try: with _client() as c, open(photo_path, "rb") as f: r = c.post( _url("sendPhoto"), data={"chat_id": chat_id, "caption": caption[:1024]}, files={"photo": f}, timeout=TIMEOUT, ) except TelegramError: raise except Exception as e: raise TelegramError(f"sendPhoto сеть: {e}", http_code=502) from e if r.status_code != 200: try: desc = r.json().get("description", "") except Exception: desc = r.text[:200] raise TelegramError(f"sendPhoto: HTTP {r.status_code} {desc}", http_code=403 if r.status_code == 403 else 502) data = r.json() if not data.get("ok"): raise TelegramError(f"sendPhoto: {data.get('description','')}", http_code=502) return data["result"]["message_id"] def send_video(chat_id: str, video_path: str, caption: str = "") -> int: """Отправляет видео (медиа первым). Возвращает message_id.""" if not video_path: raise TelegramError("video_path пуст", http_code=400) try: with _client() as c, open(video_path, "rb") as f: r = c.post( _url("sendVideo"), data={"chat_id": chat_id, "caption": caption[:1024]}, files={"video": f}, timeout=TIMEOUT, ) except TelegramError: raise except Exception as e: raise TelegramError(f"sendVideo сеть: {e}", http_code=502) from e if r.status_code != 200: try: desc = r.json().get("description", "") except Exception: desc = r.text[:200] raise TelegramError(f"sendVideo: HTTP {r.status_code} {desc}", http_code=403 if r.status_code == 403 else 502) data = r.json() if not data.get("ok"): raise TelegramError(f"sendVideo: {data.get('description','')}", http_code=502) return data["result"]["message_id"] def send_media(chat_id: str, media_path: str, caption: str = "") -> int: """Отправляет медиа по расширению: фото или видео. Возвращает message_id.""" if str(media_path).lower().endswith((".mp4", ".mov", ".avi", ".mkv")): return send_video(chat_id, media_path, caption) return send_photo(chat_id, media_path, caption) def get_views(chat_id: str, message_id: int) -> int: """Просмотры опубликованного сообщения (getMessage → views).""" try: r = _call("getMessage", chat_id=chat_id, message_id=message_id) return int(r.get("views") or r.get("forwards") or 0) except TelegramError: return 0 def get_me() -> dict: return _call("getMe") def delete_message(chat_id: str, message_id: int) -> bool: """Удаляет сообщение из канала. True если успешно.""" try: _call("deleteMessage", chat_id=chat_id, message_id=message_id) return True except TelegramError: return False def get_chat(chat_id: str) -> dict: return _call("getChat", chat_id=chat_id)