Files

161 lines
6.1 KiB
Python

# 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)