- FastAPI приложение для отправки мониторинговых алертов в мессенджеры - Поддержка Telegram и MAX/VK - Интеграция с Grafana, Zabbix, AlertManager - Автоматическое создание тикетов в Jira - Управление группами мессенджеров через API - Декораторы для авторизации и скрытия эндпоинтов - Подробная документация в папке docs/ Автор: Сергей Антропов Сайт: https://devops.org.ru
346 lines
14 KiB
Python
346 lines
14 KiB
Python
"""
|
||
Клиент для работы с Telegram Bot API.
|
||
|
||
Автор: Сергей Антропов
|
||
Сайт: https://devops.org.ru
|
||
"""
|
||
import logging
|
||
import io
|
||
from typing import Optional, Union
|
||
from telegram import Bot, InlineKeyboardMarkup
|
||
from telegram.error import TelegramError
|
||
import httpx
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class TelegramClient:
|
||
"""Клиент для отправки сообщений в Telegram."""
|
||
|
||
def __init__(self, bot_token: Optional[str] = None):
|
||
"""
|
||
Инициализация клиента Telegram.
|
||
|
||
Args:
|
||
bot_token: Токен бота Telegram. Если не указан, используется из настроек.
|
||
|
||
Raises:
|
||
ValueError: Если токен не указан.
|
||
"""
|
||
# Импортируем settings здесь, чтобы избежать циклических зависимостей
|
||
from app.core.config import get_settings
|
||
settings = get_settings()
|
||
self.bot_token = bot_token or settings.telegram_bot_token
|
||
if not self.bot_token:
|
||
raise ValueError("TELEGRAM_BOT_TOKEN не установлен")
|
||
self._bot: Optional[Bot] = None
|
||
|
||
async def get_bot(self) -> Bot:
|
||
"""
|
||
Получить экземпляр бота (создается при первом обращении).
|
||
|
||
Returns:
|
||
Экземпляр Bot.
|
||
"""
|
||
if self._bot is None:
|
||
self._bot = Bot(token=self.bot_token)
|
||
return self._bot
|
||
|
||
async def send_message(
|
||
self,
|
||
chat_id: int,
|
||
text: str,
|
||
message_thread_id: Optional[int] = None,
|
||
reply_markup: Optional[InlineKeyboardMarkup] = None,
|
||
disable_web_page_preview: bool = True,
|
||
parse_mode: str = "HTML"
|
||
) -> bool:
|
||
"""
|
||
Отправить сообщение в Telegram.
|
||
|
||
Args:
|
||
chat_id: ID чата или группы.
|
||
text: Текст сообщения.
|
||
message_thread_id: ID треда в группе (опционально).
|
||
reply_markup: Клавиатура с кнопками (опционально).
|
||
disable_web_page_preview: Отключить превью ссылок.
|
||
parse_mode: Режим парсинга (HTML, Markdown).
|
||
|
||
Returns:
|
||
True если сообщение отправлено успешно, False в противном случае.
|
||
"""
|
||
try:
|
||
bot = await self.get_bot()
|
||
await bot.send_message(
|
||
chat_id=chat_id,
|
||
text=text,
|
||
message_thread_id=message_thread_id,
|
||
disable_web_page_preview=disable_web_page_preview,
|
||
parse_mode=parse_mode,
|
||
reply_markup=reply_markup
|
||
)
|
||
logger.info(f"Сообщение отправлено в чат {chat_id}, тред {message_thread_id}")
|
||
return True
|
||
except TelegramError as e:
|
||
logger.error(f"Ошибка отправки сообщения в Telegram: {e}")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"Неожиданная ошибка при отправке сообщения: {e}")
|
||
return False
|
||
|
||
async def send_photo(
|
||
self,
|
||
chat_id: int,
|
||
photo: Union[str, bytes, io.BytesIO],
|
||
caption: Optional[str] = None,
|
||
message_thread_id: Optional[int] = None,
|
||
parse_mode: str = "HTML"
|
||
) -> bool:
|
||
"""
|
||
Отправить фото в Telegram.
|
||
|
||
Args:
|
||
chat_id: ID чата или группы.
|
||
photo: Путь к файлу, URL, bytes или BytesIO объект с фото.
|
||
caption: Подпись к фото (опционально).
|
||
message_thread_id: ID треда в группе (опционально).
|
||
parse_mode: Режим парсинга (HTML, Markdown).
|
||
|
||
Returns:
|
||
True если фото отправлено успешно, False в противном случае.
|
||
"""
|
||
try:
|
||
bot = await self.get_bot()
|
||
|
||
# Если photo - это URL, скачиваем файл
|
||
if isinstance(photo, str) and (photo.startswith('http://') or photo.startswith('https://')):
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.get(photo)
|
||
photo = io.BytesIO(response.content)
|
||
|
||
# Если photo - это bytes, преобразуем в BytesIO
|
||
if isinstance(photo, bytes):
|
||
photo = io.BytesIO(photo)
|
||
|
||
await bot.send_photo(
|
||
chat_id=chat_id,
|
||
photo=photo,
|
||
caption=caption,
|
||
message_thread_id=message_thread_id,
|
||
parse_mode=parse_mode
|
||
)
|
||
logger.info(f"Фото отправлено в чат {chat_id}, тред {message_thread_id}")
|
||
return True
|
||
except TelegramError as e:
|
||
logger.error(f"Ошибка отправки фото в Telegram: {e}")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"Неожиданная ошибка при отправке фото: {e}")
|
||
return False
|
||
|
||
async def send_video(
|
||
self,
|
||
chat_id: int,
|
||
video: Union[str, bytes, io.BytesIO],
|
||
caption: Optional[str] = None,
|
||
message_thread_id: Optional[int] = None,
|
||
parse_mode: str = "HTML",
|
||
duration: Optional[int] = None,
|
||
width: Optional[int] = None,
|
||
height: Optional[int] = None
|
||
) -> bool:
|
||
"""
|
||
Отправить видео в Telegram.
|
||
|
||
Args:
|
||
chat_id: ID чата или группы.
|
||
video: Путь к файлу, URL, bytes или BytesIO объект с видео.
|
||
caption: Подпись к видео (опционально).
|
||
message_thread_id: ID треда в группе (опционально).
|
||
parse_mode: Режим парсинга (HTML, Markdown).
|
||
duration: Длительность видео в секундах (опционально).
|
||
width: Ширина видео (опционально).
|
||
height: Высота видео (опционально).
|
||
|
||
Returns:
|
||
True если видео отправлено успешно, False в противном случае.
|
||
"""
|
||
try:
|
||
bot = await self.get_bot()
|
||
|
||
# Если video - это URL, скачиваем файл
|
||
if isinstance(video, str) and (video.startswith('http://') or video.startswith('https://')):
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.get(video)
|
||
video = io.BytesIO(response.content)
|
||
|
||
# Если video - это bytes, преобразуем в BytesIO
|
||
if isinstance(video, bytes):
|
||
video = io.BytesIO(video)
|
||
|
||
await bot.send_video(
|
||
chat_id=chat_id,
|
||
video=video,
|
||
caption=caption,
|
||
message_thread_id=message_thread_id,
|
||
parse_mode=parse_mode,
|
||
duration=duration,
|
||
width=width,
|
||
height=height
|
||
)
|
||
logger.info(f"Видео отправлено в чат {chat_id}, тред {message_thread_id}")
|
||
return True
|
||
except TelegramError as e:
|
||
logger.error(f"Ошибка отправки видео в Telegram: {e}")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"Неожиданная ошибка при отправке видео: {e}")
|
||
return False
|
||
|
||
async def send_audio(
|
||
self,
|
||
chat_id: int,
|
||
audio: Union[str, bytes, io.BytesIO],
|
||
caption: Optional[str] = None,
|
||
message_thread_id: Optional[int] = None,
|
||
parse_mode: str = "HTML",
|
||
duration: Optional[int] = None,
|
||
performer: Optional[str] = None,
|
||
title: Optional[str] = None
|
||
) -> bool:
|
||
"""
|
||
Отправить аудио в Telegram.
|
||
|
||
Args:
|
||
chat_id: ID чата или группы.
|
||
audio: Путь к файлу, URL, bytes или BytesIO объект с аудио.
|
||
caption: Подпись к аудио (опционально).
|
||
message_thread_id: ID треда в группе (опционально).
|
||
parse_mode: Режим парсинга (HTML, Markdown).
|
||
duration: Длительность аудио в секундах (опционально).
|
||
performer: Исполнитель (опционально).
|
||
title: Название трека (опционально).
|
||
|
||
Returns:
|
||
True если аудио отправлено успешно, False в противном случае.
|
||
"""
|
||
try:
|
||
bot = await self.get_bot()
|
||
|
||
# Если audio - это URL, скачиваем файл
|
||
if isinstance(audio, str) and (audio.startswith('http://') or audio.startswith('https://')):
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.get(audio)
|
||
audio = io.BytesIO(response.content)
|
||
|
||
# Если audio - это bytes, преобразуем в BytesIO
|
||
if isinstance(audio, bytes):
|
||
audio = io.BytesIO(audio)
|
||
|
||
await bot.send_audio(
|
||
chat_id=chat_id,
|
||
audio=audio,
|
||
caption=caption,
|
||
message_thread_id=message_thread_id,
|
||
parse_mode=parse_mode,
|
||
duration=duration,
|
||
performer=performer,
|
||
title=title
|
||
)
|
||
logger.info(f"Аудио отправлено в чат {chat_id}, тред {message_thread_id}")
|
||
return True
|
||
except TelegramError as e:
|
||
logger.error(f"Ошибка отправки аудио в Telegram: {e}")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"Неожиданная ошибка при отправке аудио: {e}")
|
||
return False
|
||
|
||
async def send_document(
|
||
self,
|
||
chat_id: int,
|
||
document: Union[str, bytes, io.BytesIO],
|
||
caption: Optional[str] = None,
|
||
message_thread_id: Optional[int] = None,
|
||
parse_mode: str = "HTML",
|
||
filename: Optional[str] = None
|
||
) -> bool:
|
||
"""
|
||
Отправить документ в Telegram.
|
||
|
||
Args:
|
||
chat_id: ID чата или группы.
|
||
document: Путь к файлу, URL, bytes или BytesIO объект с документом.
|
||
caption: Подпись к документу (опционально).
|
||
message_thread_id: ID треда в группе (опционально).
|
||
parse_mode: Режим парсинга (HTML, Markdown).
|
||
filename: Имя файла (опционально).
|
||
|
||
Returns:
|
||
True если документ отправлен успешно, False в противном случае.
|
||
"""
|
||
try:
|
||
bot = await self.get_bot()
|
||
document_url = None
|
||
|
||
# Если document - это URL, скачиваем файл
|
||
if isinstance(document, str) and (document.startswith('http://') or document.startswith('https://')):
|
||
document_url = document
|
||
async with httpx.AsyncClient() as client:
|
||
response = await client.get(document)
|
||
document = io.BytesIO(response.content)
|
||
if not filename:
|
||
# Пытаемся извлечь имя файла из URL
|
||
filename = document_url.split('/')[-1].split('?')[0]
|
||
|
||
# Если document - это bytes, преобразуем в BytesIO
|
||
if isinstance(document, bytes):
|
||
document = io.BytesIO(document)
|
||
|
||
await bot.send_document(
|
||
chat_id=chat_id,
|
||
document=document,
|
||
caption=caption,
|
||
message_thread_id=message_thread_id,
|
||
parse_mode=parse_mode,
|
||
filename=filename
|
||
)
|
||
logger.info(f"Документ отправлен в чат {chat_id}, тред {message_thread_id}")
|
||
return True
|
||
except TelegramError as e:
|
||
logger.error(f"Ошибка отправки документа в Telegram: {e}")
|
||
return False
|
||
except Exception as e:
|
||
logger.error(f"Неожиданная ошибка при отправке документа: {e}")
|
||
return False
|
||
|
||
|
||
# Глобальный экземпляр клиента (lazy initialization)
|
||
_telegram_client_instance: Optional[TelegramClient] = None
|
||
|
||
|
||
def get_telegram_client() -> TelegramClient:
|
||
"""
|
||
Получить экземпляр клиента Telegram (lazy initialization).
|
||
|
||
Returns:
|
||
Экземпляр TelegramClient.
|
||
"""
|
||
global _telegram_client_instance
|
||
if _telegram_client_instance is None:
|
||
_telegram_client_instance = TelegramClient()
|
||
return _telegram_client_instance
|
||
|
||
|
||
# Для обратной совместимости (lazy initialization)
|
||
# telegram_client будет создан при первом использовании
|
||
class _TelegramClientProxy:
|
||
"""Прокси для ленивой инициализации telegram_client."""
|
||
|
||
def __getattr__(self, name):
|
||
"""Получить атрибут из telegram_client."""
|
||
return getattr(get_telegram_client(), name)
|
||
|
||
|
||
telegram_client = _TelegramClientProxy()
|