Files
MessageGateway/app/core/telegram_client.py
Sergey Antropov b90def35ed Initial commit: Message Gateway project
- FastAPI приложение для отправки мониторинговых алертов в мессенджеры
- Поддержка Telegram и MAX/VK
- Интеграция с Grafana, Zabbix, AlertManager
- Автоматическое создание тикетов в Jira
- Управление группами мессенджеров через API
- Декораторы для авторизации и скрытия эндпоинтов
- Подробная документация в папке docs/

Автор: Сергей Антропов
Сайт: https://devops.org.ru
2025-11-12 20:25:11 +03:00

346 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Клиент для работы с 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()