from __future__ import annotations import logging from typing import Dict, Any, Optional, Iterable, Tuple from telegram import Bot, Message, InlineKeyboardMarkup from telegram.error import InvalidToken, TelegramError logger = logging.getLogger(__name__) def make_keyboard_payload(buttons: Optional[Iterable[Tuple[str, str]]]) -> Optional[Dict]: """ Создает структуру inline-клавиатуры для API Telegram. Args: buttons: Список кнопок в формате [(text, url), ...] Returns: Dict в формате {"rows": [[{"text": text, "url": url}], ...]} """ if not buttons: return None rows = [[{"text": t, "url": u}] for t, u in buttons] return {"rows": rows} def build_payload( ptype: str, text: Optional[str] = None, media_file_id: Optional[str] = None, parse_mode: Optional[str] = None, keyboard: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """ Строит payload для отправки поста. Args: ptype: Тип поста (text/photo/video/animation) text: Текст сообщения media_file_id: ID медиафайла в Telegram parse_mode: Формат разметки (HTML/MarkdownV2) keyboard: Inline клавиатура Returns: Dict содержащий все необходимые поля для отправки """ payload: Dict[str, Any] = { "type": str(ptype), "text": text if text is not None else "", "parse_mode": str(parse_mode) if parse_mode is not None else "html", "keyboard": keyboard if keyboard is not None else {}, } if media_file_id: payload["media_file_id"] = media_file_id return payload async def validate_bot_token(token: str) -> Tuple[bool, Optional[str], Optional[int]]: """ Проверяет валидность токена бота и возвращает его username и ID. Args: token: Токен бота для проверки Returns: tuple[bool, Optional[str], Optional[int]]: (is_valid, username, bot_id) """ try: bot = Bot(token) me = await bot.get_me() return True, me.username, me.id except InvalidToken: logger.warning(f"Invalid bot token provided: {token[:10]}...") return False, None, None except TelegramError as e: logger.error(f"Telegram error while validating bot token: {e}") return False, None, None except Exception as e: logger.exception(f"Unexpected error while validating bot token: {e}") return False, None, None finally: if 'bot' in locals(): await bot.close() def validate_message_length(text: str) -> bool: """ Проверяет длину сообщения на соответствие лимитам Telegram. Args: text: Текст для проверки Returns: bool: True если длина в пределах лимита """ return len(text) <= 4096 # Максимальная длина текста в Telegram def is_valid_webhook_url(url: str) -> bool: """Проверяет соответствие URL требованиям Telegram для вебхуков. Args: url: URL для проверки Returns: bool: True если URL валидный, иначе False """ if not url: return False return True # TODO: implement proper validation class PostService: """Сервис для работы с постами.""" @staticmethod async def preview_post(message: Message, post_data: Dict[str, Any]) -> None: """Показывает предпросмотр поста. Args: message (Message): Telegram сообщение post_data (Dict[str, Any]): Данные поста из сессии """ text = post_data.get('text', '') parse_mode = post_data.get('parse_mode', 'HTML') keyboard = post_data.get('keyboard') if keyboard: # Создаем разметку клавиатуры rows = keyboard.get('rows', []) markup = InlineKeyboardMarkup(rows) if rows else None else: markup = None media_file_id = post_data.get('media_file_id') if media_file_id: # Отправляем медиафайл с подписью try: await message.reply_photo( photo=media_file_id, caption=text, parse_mode=parse_mode, reply_markup=markup ) except TelegramError as e: # В случае ошибки отправляем только текст logger.error(f"Error sending photo preview: {e}") await message.reply_text( text=text, parse_mode=parse_mode, reply_markup=markup ) else: # Отправляем только текст await message.reply_text( text=text, parse_mode=parse_mode, reply_markup=markup ) @staticmethod async def create_post(bot: Bot, chat_id: int, post_data: Dict[str, Any]) -> bool: """Создает новый пост в канале. Args: bot (Bot): Экземпляр бота chat_id (int): ID канала post_data (Dict[str, Any]): Данные поста Returns: bool: Успешность создания """ try: text = post_data.get('text', '') parse_mode = post_data.get('parse_mode', 'HTML') keyboard = post_data.get('keyboard') if keyboard: rows = keyboard.get('rows', []) markup = InlineKeyboardMarkup(rows) if rows else None else: markup = None media_file_id = post_data.get('media_file_id') if media_file_id: await bot.send_photo( chat_id=chat_id, photo=media_file_id, caption=text, parse_mode=parse_mode, reply_markup=markup ) else: await bot.send_message( chat_id=chat_id, text=text, parse_mode=parse_mode, reply_markup=markup ) return True except TelegramError as e: logger.error(f"Error creating post: {e}") return False def validate_url(url: str) -> bool: """Проверяет соответствие URL требованиям. Args: url (str): URL для проверки Returns: bool: True если URL соответствует требованиям """ return ( url.startswith("https://") and not url.startswith("https://telegram.org") and len(url) <= 512 )