mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 18:05:03 +00:00
161 lines
6.1 KiB
Python
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) |