"""Главный модуль бота редактора.""" import time import shlex import logging from typing import cast, Optional, Dict, Any from urllib.parse import urlparse from datetime import datetime import asyncio from typing import Optional, Dict, Any from apscheduler.schedulers.asyncio import AsyncIOScheduler from telegram import Update, Message, CallbackQuery, User, InlineKeyboardMarkup, InlineKeyboardButton from telegram.ext import ( Application, CommandHandler, MessageHandler, CallbackQueryHandler, ContextTypes, ConversationHandler, filters ) from app.core.config import settings from .editor.states import BotStates from .editor.session import get_session_store, SessionStore from .editor.router import register_handlers from app.services.template import count_templates # Определение типа Context Context = ContextTypes.DEFAULT_TYPE # Константы DEFAULT_TTL = 3600 # 1 час # Настройка логирования logger = logging.getLogger(__name__) async def main(): """Запуск бота.""" app = Application.builder().token(settings.editor_bot_token).build() # Регистрация обработчиков register_handlers(app) # Запуск бота await app.run_polling(allowed_updates=Update.ALL_TYPES) if __name__ == "__main__": import asyncio try: asyncio.run(main()) except KeyboardInterrupt: logger.info("Бот остановлен") except Exception as e: logger.error(f"Ошибка: {e}", exc_info=True) finally: # Убедимся, что все циклы закрыты loop = asyncio.get_event_loop() if loop.is_running(): loop.stop() if not loop.is_closed(): loop.close() except Exception as e: logger.error(f"Ошибка: {e}", exc_info=True) from app.core.config import settings from .editor.session import SessionStore from .editor.handlers.templates import ( start_template_creation, handle_template_type, handle_template_name, handle_template_text, handle_template_keyboard, list_templates ) from .editor.handlers.posts import ( newpost, handle_post_template, handle_post_channel, handle_post_schedule ) from .editor.states import BotStates # Настройка логирования logger = logging.getLogger(__name__) # Константы SESSION_TIMEOUT = 3600 # 1 час async def cancel_handler(update: Update, context: Context) -> int: """Отмена текущей операции.""" message = cast(Message, update.message) if not message or not message.from_user: return BotStates.CONVERSATION_END user_id = message.from_user.id session_store = get_session_store() session_store.drop(user_id) await message.reply_text("Операция отменена") return BotStates.CONVERSATION_END def cleanup_old_sessions(): """Очистка старых сессий.""" try: now = time.time() session_store = get_session_store() for uid, session in list(session_store._data.items()): if now - session.last_activity > DEFAULT_TTL: del session_store._data[uid] except Exception as e: logger.error(f"Error cleaning up sessions: {e}") Context = ContextTypes.DEFAULT_TYPE PREVIEW_VARS = BotStates.TEMPLATE_VARS # Convert state constants from BotStates CONVERSATION_END = BotStates.CONVERSATION_END START = BotStates.START MAIN_MENU = BotStates.MAIN_MENU SELECT_TEMPLATE = BotStates.SELECT_TEMPLATE TEMPLATE_PREVIEW = BotStates.TEMPLATE_PREVIEW TEMPLATE_VARS = BotStates.TEMPLATE_VARS PREVIEW_CONFIRM = BotStates.PREVIEW_CONFIRM TPL_NEW_NAME = BotStates.TPL_NEW_NAME TPL_NEW_TYPE = BotStates.TPL_NEW_TYPE TPL_NEW_FORMAT = BotStates.TPL_NEW_FORMAT CREATE_POST = BotStates.CREATE_POST CHOOSE_CHANNEL = BotStates.CHOOSE_CHANNEL CHOOSE_TYPE = BotStates.CHOOSE_TYPE CHOOSE_FORMAT = BotStates.CHOOSE_FORMAT ENTER_TEXT = BotStates.ENTER_TEXT ENTER_MEDIA = BotStates.ENTER_MEDIA EDIT_KEYBOARD = BotStates.EDIT_KEYBOARD CONFIRM_SEND = BotStates.CONFIRM_SEND ENTER_SCHEDULE = BotStates.ENTER_SCHEDULE BOT_TOKEN = BotStates.BOT_TOKEN CHANNEL_ID = BotStates.CHANNEL_ID CHANNEL_TITLE = BotStates.CHANNEL_TITLE CHANNEL_SELECT_BOT = BotStates.CHANNEL_SELECT_BOT TPL_NEW_CONTENT = BotStates.TPL_NEW_CONTENT TPL_NEW_KB = BotStates.TPL_NEW_KB from telegram.ext import ( filters, CallbackContext ) from telegram.error import TelegramError from typing import List, Optional from celery import shared_task from sqlalchemy import select from app.models.templates import Template from app.models.channel import Channel from app.models.post import PostType from app.db.session import async_session_maker from .editor.session import SessionStore, UserSession from .editor.messages import MessageType from .editor.router import register_handlers from .editor.states import BotStates from app.services.template import render_template_by_name, list_templates, create_template # Константы пагинации PAGE_SIZE = 5 # Количество элементов на странице from typing import Optional from app.core.config import settings from app.tasks.celery_app import celery_app from .editor.session import SessionStore from .editor.router import register_handlers from .editor.template import render_template_by_name from .editor.states import BotStates # Константы PAGE_SIZE = 5 # Количество элементов на странице MAX_MESSAGE_LENGTH = 4096 # Глобальное хранилище сессий _session_store: Optional[SessionStore] = None def get_session_store() -> SessionStore: """Получить глобальное хранилище сессий""" global _session_store if _session_store is None: _session_store = SessionStore() return _session_store SESSION_TIMEOUT = 3600 # 1 час ALLOWED_URL_SCHEMES = {'http', 'https'} # Настройка логирования logger = logging.getLogger(__name__) def validate_message_length(text: str) -> bool: """Проверка длины сообщения согласно лимитам Telegram. Args: text: Текст для проверки Returns: bool: True если длина в пределах лимита """ return len(text) <= MAX_MESSAGE_LENGTH def validate_url(url: str) -> bool: """Проверяет URL на допустимость. Args: url: URL для проверки Returns: bool: True если URL допустимый """ try: result = urlparse(url) return bool(result.scheme and result.netloc and result.scheme in ALLOWED_URL_SCHEMES) except Exception: return False def parse_template_invocation(s: str) -> tuple[str, dict]: """Разбор строки вызова шаблона. Args: s: Строка в формате #template_name key1=value1 key2=value2 Returns: tuple: (имя_шаблона, словарь_параметров) Raises: ValueError: Если неверный формат строки """ s = s.strip() if not s.startswith("#"): raise ValueError("Имя шаблона должно начинаться с #") parts = shlex.split(s) if not parts: raise ValueError("Пустой шаблон") name = parts[0][1:] args: dict[str, str] = {} for tok in parts[1:]: if "=" in tok: k, v = tok.split("=", 1) args[k.strip()] = v.strip() return name, args def parse_key_value_lines(text: str) -> dict: """Парсинг строк формата key=value. Args: text: Строки в формате key=value или key="quoted value" Returns: dict: Словарь параметров """ text = (text or "").strip() if not text: return {} out = {} if "\n" in text: for line in text.splitlines(): if "=" in line: k, v = line.split("=", 1) v = v.strip().strip('"') if k.strip(): # Проверка на пустой ключ out[k.strip()] = v else: try: for tok in shlex.split(text): if "=" in tok: k, v = tok.split("=", 1) if k.strip(): # Проверка на пустой ключ out[k.strip()] = v except ValueError as e: logger.warning(f"Error parsing key-value line: {e}") return out async def choose_type(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Обработчик выбора типа поста.""" query = cast(CallbackQuery, update.callback_query) if not query or not query.data: return ConversationHandler.END user_id = query.from_user.id type_choice = query.data.split(":")[1] session = get_session_store().get(user_id) session.clear() session.type = MessageType(type_choice) session.touch() await query.edit_message_text("Выберите формат сообщения") return BotStates.CHOOSE_FORMAT async def choose_format(update: Update, context: Context) -> int: """Обработчик выбора формата сообщения.""" query = cast(CallbackQuery, update.callback_query) if not query or not query.data: return ConversationHandler.END user_id = query.from_user.id format_choice = query.data.split(":")[1] session = get_session_store().get(user_id) session.parse_mode = format_choice session.touch() await query.edit_message_text("Введите текст сообщения") return BotStates.ENTER_TEXT async def enter_text(update: Update, context: Context) -> int: """Обработчик ввода текста сообщения.""" message = cast(Message, update.message) if not message or not message.text: return ConversationHandler.END if not message.from_user: return ConversationHandler.END user_id = message.from_user.id text = message.text if not validate_message_length(text): await message.reply_text("Слишком длинное сообщение") return BotStates.ENTER_TEXT session = get_session_store().get(user_id) session.text = text session.touch() await message.reply_text("Текст сохранен. Введите ID медиафайла или пропустите") return BotStates.ENTER_MEDIA async def enter_media(update: Update, context: Context) -> int: """Обработчик ввода медиафайла.""" message = cast(Message, update.message) if not message or not message.text: return ConversationHandler.END user = cast(User, message.from_user) user_id = user.id media_id = message.text session = get_session_store().get(user_id) session.media_file_id = media_id if media_id != "skip" else None session.touch() await message.reply_text("Введите клавиатуру или пропустите") return BotStates.EDIT_KEYBOARD async def edit_keyboard(update: Update, context: Context) -> int: """Обработчик редактирования клавиатуры.""" message = cast(Message, update.message) if not message or not message.text: return ConversationHandler.END user = cast(User, message.from_user) user_id = user.id keyboard_text = message.text session = get_session_store().get(user_id) if keyboard_text != "skip": try: keyboard_data = parse_key_value_lines(keyboard_text) if keyboard_text else {} session.keyboard = keyboard_data session.touch() except ValueError as e: await message.reply_text(f"Ошибка разбора клавиатуры: {e}") return BotStates.EDIT_KEYBOARD await message.reply_text("Подтвердите отправку") return BotStates.CONFIRM_SEND async def confirm_send(update: Update, context: Context) -> int: """Обработчик подтверждения отправки.""" query = cast(CallbackQuery, update.callback_query) if not query or not query.data or not query.message: return ConversationHandler.END user = update.effective_user if not user: await query.answer("Ошибка: пользователь не определен") return ConversationHandler.END try: choice = query.data.split(":", 1)[1] if choice == "yes": session = get_session_store().get(user.id) if not session: await query.edit_message_text("Ошибка: сессия потеряна") return ConversationHandler.END if not session.is_complete(): await query.edit_message_text("Ошибка: не все поля заполнены") return ConversationHandler.END try: payload = session.to_dict() # Отправляем задачу через Celery task = celery_app.send_task( 'app.tasks.senders.send_post_task', args=[payload] ) await query.edit_message_text( f"Пост поставлен в очередь\nID задачи: {task.id}" ) except ValueError as e: await query.edit_message_text(f"Ошибка валидации: {e}") return ConversationHandler.END except Exception as e: logger.error(f"Ошибка при отправке поста: {e}") await query.edit_message_text("Произошла ошибка при отправке поста") return ConversationHandler.END else: await query.edit_message_text("Отправка отменена") # Очищаем сессию пользователя if user := update.effective_user: session = get_session_store().get(user.id) if session: session.clear() return ConversationHandler.END except Exception as e: logger.error(f"Ошибка при подтверждении отправки: {e}") await query.answer("Произошла ошибка") return ConversationHandler.END async def preview_collect_vars(update: Update, context: Context) -> int: """Сбор переменных для предпросмотра шаблона.""" message = cast(Message, update.message) if not message or not message.text: return ConversationHandler.END if not message.from_user: return ConversationHandler.END user_id = message.from_user.id vars_text = message.text try: template_vars = parse_key_value_lines(vars_text) session = get_session_store().get(user_id) session.template_vars = template_vars session.touch() await message.reply_text("Переменные сохранены. Подтвердите предпросмотр") return BotStates.PREVIEW_CONFIRM except ValueError as e: await message.reply_text(f"Ошибка разбора переменных: {e}") return BotStates.TEMPLATE_VARS async def preview_confirm(update: Update, context: Context) -> int: """Подтверждение предпросмотра шаблона.""" query = cast(CallbackQuery, update.callback_query) if not query or not query.data or not query.message: return ConversationHandler.END user_id = query.from_user.id choice = query.data.split(":")[1] session = get_session_store().get(user_id) if not session: await query.edit_message_text("Ошибка: сессия потеряна") return ConversationHandler.END if choice == "use": try: template_name = session.template_name template_vars = session.template_vars if not template_name: raise ValueError("Имя шаблона не задано") # Создаем контекст для шаблонизатора template_context = { "user_id": user_id, "vars": template_vars, "context": dict(context.user_data) if context.user_data else {} } # Рендерим и обновляем сессию rendered = await render_template_by_name(template_name, template_vars, template_context) session.update(rendered) await query.edit_message_text("Шаблон применен. Проверьте параметры отправки") return BotStates.CONFIRM_SEND except Exception as e: logger.error(f"Ошибка при применении шаблона: {e}") await query.edit_message_text(f"Ошибка применения шаблона: {e}") return ConversationHandler.END else: await query.edit_message_text("Редактирование отменено") return ConversationHandler.END def create_template_dict(data: dict, user_id: int) -> dict: """Создание словаря параметров для создания шаблона. Args: data: Исходные данные user_id: ID пользователя Returns: dict: Подготовленные данные для создания шаблона """ template_data = { "owner_id": user_id, "name": data.get("name"), "title": data.get("name"), "content": data.get("content"), "keyboard_tpl": data.get("keyboard"), "type_": data.get("type"), "parse_mode": data.get("format") } # Проверяем обязательные поля required_fields = ["name", "content", "type_"] missing_fields = [f for f in required_fields if not template_data.get(f)] if missing_fields: raise ValueError(f"Не хватает обязательных полей: {', '.join(missing_fields)}") return template_data async def tpl_new_start(update: Update, context: Context) -> int: """Начало создания нового шаблона.""" message = cast(Message, update.message) if not message or not message.from_user: return ConversationHandler.END user_id = message.from_user.id session = get_session_store().get(user_id) session.clear() await message.reply_text("Введите имя нового шаблона") return BotStates.TPL_NEW_NAME async def tpl_new_name(update: Update, context: Context) -> int: """Ввод имени нового шаблона.""" message = cast(Message, update.message) if not message or not message.text or not message.from_user: return ConversationHandler.END user_id = message.from_user.id name = message.text.strip() if not name or " " in name: await message.reply_text("Недопустимое имя шаблона") return BotStates.TPL_NEW_NAME session = get_session_store().get(user_id) session.template_name = name keyboard = [] for post_type in PostType: button = InlineKeyboardButton( text=post_type.value.capitalize(), callback_data=f"type:{post_type.value}" ) keyboard.append([button]) markup = InlineKeyboardMarkup(keyboard) await message.reply_text("Выберите тип шаблона", reply_markup=markup) return BotStates.TPL_NEW_TYPE async def tpl_new_type(update: Update, context: Context) -> int: """Выбор типа нового шаблона.""" query = cast(CallbackQuery, update.callback_query) if not query or not query.data: return ConversationHandler.END user = cast(User, query.from_user) type_choice = query.data.split(":")[1] session = get_session_store().get(user.id) session.type = PostType(type_choice) session.touch() keyboard = [ [InlineKeyboardButton("HTML", callback_data="format:HTML")], [InlineKeyboardButton("Markdown", callback_data="format:MarkdownV2")] ] markup = InlineKeyboardMarkup(keyboard) await query.edit_message_text("Выберите формат сообщения", reply_markup=markup) return BotStates.TPL_NEW_FORMAT async def tpl_new_format(update: Update, context: Context) -> int: """Выбор формата нового шаблона.""" query = cast(CallbackQuery, update.callback_query) if not query or not query.data: return ConversationHandler.END user = cast(User, query.from_user) format_choice = query.data.split(":")[1] session = get_session_store().get(user.id) session.parse_mode = format_choice session.touch() await query.edit_message_text( "Теперь введите содержимое шаблона.\n\n" f"Формат: {format_choice}\n" "Поддерживаются переменные в формате {variable_name}" ) session.touch() await query.edit_message_text("Введите содержимое шаблона") return BotStates.TPL_NEW_CONTENT async def tpl_new_content(update: Update, context: Context) -> int: """Ввод содержимого нового шаблона.""" message = cast(Message, update.message) if not message or not message.text or not message.from_user: return ConversationHandler.END user = cast(User, message.from_user) content = message.text if len(content) > 4096: # Максимальная длина сообщения в Telegram await message.reply_text("Слишком длинный шаблон. Максимальная длина: 4096 символов") return BotStates.TPL_NEW_CONTENT session = get_session_store().get(user.id) template_data = { "name": session.template_name, "title": session.template_name, "content": content, "type": session.type, "owner_id": user.id, "parse_mode": session.parse_mode, "keyboard_tpl": session.keyboard } try: await create_template(template_data) await message.reply_text("Шаблон успешно сохранен") return ConversationHandler.END except ValueError as e: await message.reply_text(f"Ошибка создания шаблона: {e}") return BotStates.TPL_NEW_CONTENT async def list_user_templates(update: Update, context: Context) -> None: """Вывод списка доступных шаблонов.""" message = cast(Message, update.message) if not message or not message.from_user: return None try: user_id = message.from_user.id if context and hasattr(context, 'user_data'): if not isinstance(context.user_data, dict): context.user_data = {} if context.user_data is not None: context.user_data["tpl_page"] = 0 templates = await list_templates(owner_id=user_id) if not templates: await message.reply_text("Нет доступных шаблонов") return None text = "Доступные шаблоны:\n\n" for tpl in templates: # Используем явное приведение типа для доступа к атрибутам tpl_name = getattr(tpl, 'name', 'Без имени') tpl_type = getattr(tpl, 'type', 'Неизвестный тип') text += f"• {tpl_name} ({tpl_type})\n" await message.reply_text(text) except Exception as e: logger.error(f"Ошибка при загрузке шаблонов: {e}") await message.reply_text("Ошибка при загрузке списка шаблонов") @shared_task def send_post_async(data: dict) -> None: """Отправка поста из данных сессии в фоновом режиме. Args: data: Данные поста """ post_type = data.get("type") if not post_type or not isinstance(post_type, str): raise ValueError("Неверный тип поста") payload = build_payload( ptype=post_type, text=data.get("text"), media_file_id=data.get("media_id"), parse_mode=data.get("format"), keyboard=data.get("keyboard"), ) celery_app.send_task('app.tasks.senders.send_post_task', args=[payload]) @shared_task(bind=True) def send_post(self, data: dict) -> None: """Отправка поста из данных сессии. Args: data: Данные сессии """ try: post_type = data.get("type") if not post_type or not isinstance(post_type, str): raise ValueError("Неверный тип поста") payload = build_payload( ptype=post_type, text=data.get("text"), media_file_id=data.get("media_id"), parse_mode=data.get("format"), keyboard=data.get("keyboard"), ) celery_app.send_task('app.tasks.senders.send_post_task', args=[payload]) except Exception as e: logger.error(f"Ошибка при отправке поста: {e}") raise async def create_template_with_data(data: dict, user_id: int) -> None: """Создание нового шаблона из данных сессии. Args: data: Данные сессии user_id: ID пользователя """ name = data.get("name") content = data.get("content") keyboard = data.get("keyboard") tpl_type = data.get("type") parse_mode = data.get("format") if not name or not content or not tpl_type: raise ValueError("Не хватает обязательных данных для шаблона") template_data = { "owner_id": user_id, "name": name, "title": name, "content": content, "type": PostType(tpl_type), "parse_mode": parse_mode or "HTML", "keyboard_tpl": keyboard } await create_template(template_data) async def handle_template_kb(update: Update, context: Context) -> int: """Обработка клавиатуры шаблона.""" message = cast(Message, update.message) if not message or not message.text: return ConversationHandler.END user = cast(User, message.from_user) user_id = user.id kb_text = message.text session_store = get_session_store() data = cast(UserSession, session_store.get(user_id)) if not data: await message.reply_text("Ошибка: сессия потеряна") return ConversationHandler.END if kb_text != "skip": try: keyboard = parse_key_value_lines(kb_text) if kb_text else {} data.keyboard = keyboard except ValueError as e: await message.reply_text(f"Ошибка разбора клавиатуры: {e}") return BotStates.TPL_NEW_KB try: if not data.template_name or not data.text: await message.reply_text("Отсутствуют обязательные данные шаблона") return BotStates.TPL_NEW_KB template_data = { "owner_id": user_id, "name": data.template_name, "title": data.template_name, "content": data.text, "keyboard_tpl": data.keyboard, "type_": str(data.type) if data.type else "text", "parse_mode": data.parse_mode if data.parse_mode else "html" # html по умолчанию } await create_template(template_data) await message.reply_text("Шаблон создан успешно") except ValueError as e: await message.reply_text(f"Ошибка создания шаблона: {e}") return TPL_NEW_KB except Exception as e: logger.error(f"Неожиданная ошибка при создании шаблона: {e}") await message.reply_text("Произошла ошибка при создании шаблона") return BotStates.TPL_NEW_KB session_store = get_session_store() session_store.drop(user_id) return ConversationHandler.END async def handle_template_pagination(update: Update, context: Context) -> int: """Обработка пагинации в списке шаблонов.""" if not update.callback_query or not update.effective_user: return BotStates.ENTER_TEXT query = cast(CallbackQuery, update.callback_query) if not query or not isinstance(query.data, str): return ConversationHandler.END try: parts = query.data.split(":", 1) if len(parts) != 2: await query.answer("Некорректный формат страницы") return ConversationHandler.END try: page = max(0, int(parts[1])) except ValueError: await query.answer("Некорректный номер страницы") return BotStates.CONVERSATION_END except Exception as e: logger.error(f"Error in handle_template_pagination: {e}") return BotStates.CONVERSATION_END return BotStates.CONVERSATION_END user = update.effective_user if not user or not user.id: await query.answer("Ошибка: пользователь не определен") return None # Получаем шаблоны пользователя templates = await list_templates(owner_id=user.id) if not templates: msg = cast(Optional[Message], query.message) if msg and hasattr(msg, 'edit_text'): await msg.edit_text("У вас нет шаблонов") else: await query.answer("У вас нет шаблонов") return None # Вычисляем диапазон для текущей страницы start = page * PAGE_SIZE end = start + PAGE_SIZE page_templates = templates[start:end] if not page_templates: text = "Нет шаблонов на этой странице" else: text = "Доступные шаблоны:\n\n" for tpl in page_templates: tpl_name = getattr(tpl, 'name', 'Без имени') tpl_type = getattr(tpl, 'type', 'Неизвестный тип') text += f"• {tpl_name} ({tpl_type})\n" # Создаем кнопки навигации keyboard: List[List[InlineKeyboardButton]] = [[]] if page > 0: keyboard[0].append(InlineKeyboardButton("⬅️", callback_data=f"page:{page-1}")) if len(templates) > end: keyboard[0].append(InlineKeyboardButton("➡️", callback_data=f"page:{page+1}")) markup = InlineKeyboardMarkup(keyboard) msg = cast(Optional[Message], query.message) if not msg or not hasattr(msg, 'edit_text'): await query.answer("Не удалось обновить сообщение") return None try: await msg.edit_text(text=text, reply_markup=markup) await query.answer() except TelegramError as e: logger.error(f"Ошибка при обновлении сообщения: {e}", exc_info=True) await query.answer("Не удалось обновить список") except Exception as e: logger.error(f"Ошибка при обработке пагинации: {e}") await query.answer("Произошла ошибка") async def tpl_new_kb(update: Update, context: Context) -> int: """Ввод клавиатуры для нового шаблона.""" message = cast(Message, update.message) if not message or not message.text: return ConversationHandler.END user = cast(User, message.from_user) kb_text = message.text session = get_session_store().get(user.id) if not session.type or not session.template_name: await message.reply_text("Ошибка: сессия потеряна") return ConversationHandler.END if kb_text != "skip": try: keyboard = parse_key_value_lines(kb_text) session.keyboard = keyboard except ValueError as e: await message.reply_text(f"Ошибка разбора клавиатуры: {e}") return BotStates.TPL_NEW_KB try: template_data = { "owner_id": user.id, "name": session.template_name, "title": session.template_name, "content": session.text, "type": session.type, "parse_mode": session.parse_mode or "HTML", "keyboard_tpl": session.keyboard } await create_template(template_data) await message.reply_text("Шаблон успешно создан") # Очищаем сессию после успешного создания session_store = get_session_store() session_store.drop(user.id) return BotStates.CONVERSATION_END except ValueError as e: await message.reply_text(f"Ошибка создания шаблона: {e}") return BotStates.TPL_NEW_KB except Exception as e: logger.error(f"Неожиданная ошибка при создании шаблона: {e}") await message.reply_text("Произошла непредвиденная ошибка при создании шаблона") return BotStates.TPL_NEW_KB async def tpl_list(update: Update, context: Context) -> BotStates: """Вывод списка доступных шаблонов.""" if not update.message: return BotStates.CONVERSATION_END try: templates = await list_templates() if not templates: await update.message.reply_text("Нет доступных шаблонов") return BotStates.CONVERSATION_END text = "Доступные шаблоны:\n\n" for tpl in templates: text += f"• {tpl.name} ({tpl.type})\n" await update.message.reply_text(text) return BotStates.MAIN_MENU except Exception as e: await update.message.reply_text(f"Ошибка загрузки шаблонов: {e}") return BotStates.CONVERSATION_END # -------- Команды верхнего уровня --------- async def start(update: Update, context: Context) -> None: """Обработчик команды /start.""" if not update.effective_user or not update.message: return session = get_session_store().get(update.effective_user.id) session.clear() await update.message.reply_text( "Привет! Я редактор. Команды:\n" "/newpost — мастер поста\n" "/tpl_new — создать шаблон\n" "/tpl_list — список шаблонов" ) async def newpost(update: Update, context: Context) -> int: """Начало создания нового поста.""" if not update.effective_user or not update.message: return ConversationHandler.END uid = update.effective_user.id session = get_session_store().get(uid) session.touch() try: async with async_session_maker() as s: res = await s.execute(select(Channel).where(Channel.owner_id == uid).limit(50)) channels = list(res.scalars()) if not channels: await update.message.reply_text( "Пока нет каналов. Добавь через админку или команду /add_channel (в разработке)." ) return ConversationHandler.END kb = [ [InlineKeyboardButton(ch.title or str(ch.chat_id), callback_data=f"channel:{ch.id}")] for ch in channels ] await update.message.reply_text( "Выбери канал для публикации:", reply_markup=InlineKeyboardMarkup(kb) ) return BotStates.CHOOSE_CHANNEL except Exception as e: logger.error(f"Error in newpost: {e}") await update.message.reply_text("Произошла ошибка. Попробуйте позже.") return ConversationHandler.END async def choose_channel(update: Update, context: Context) -> int: """Обработка выбора канала.""" if not update.callback_query or not update.effective_user: return ConversationHandler.END await update.callback_query.answer() uid = update.effective_user.id session = get_session_store().get(uid) if session: session.touch() # Получаем необходимые объекты query = cast(Optional[CallbackQuery], update.callback_query) user = update.effective_user if not query or not isinstance(query.data, str) or not user or not user.id: return ConversationHandler.END msg = cast(Optional[Message], query.message) try: # Парсим данные из callback parts = query.data.split(":", 1) if len(parts) != 2: if msg and hasattr(msg, 'edit_text'): await msg.edit_text("Ошибка: неверный формат данных") return ConversationHandler.END # Проверяем корректность ID канала try: ch_id = int(parts[1]) except ValueError: if msg and hasattr(msg, 'edit_text'): await msg.edit_text("Ошибка: некорректный ID канала") return ConversationHandler.END # Сохраняем в сессию session = get_session_store().get(user.id) session.channel_id = ch_id # Создаем клавиатуру выбора типа поста kb = [ [ InlineKeyboardButton("Текст", callback_data="type:text"), InlineKeyboardButton("Фото", callback_data="type:photo") ], [ InlineKeyboardButton("Видео", callback_data="type:video"), InlineKeyboardButton("GIF", callback_data="type:animation") ], ] # Отображаем сообщение с выбором типа if msg and hasattr(msg, 'edit_text'): await msg.edit_text( "Выберите тип поста:", reply_markup=InlineKeyboardMarkup(kb) ) return BotStates.CHOOSE_TYPE except Exception as e: logger.error(f"Ошибка при выборе канала: {e}", exc_info=True) if msg and hasattr(msg, 'edit_text'): await msg.edit_text("Произошла ошибка. Попробуйте заново.") return ConversationHandler.END # ... [Остальные функции обновляются аналогично] ... async def enter_schedule(update: Update, context: Context) -> int: """Обработка ввода времени для отложенной публикации.""" if not update.effective_user or not update.message or not update.message.text: return ConversationHandler.END uid = update.effective_user.id session = get_session_store().get(uid) if session: session.touch() try: when = datetime.strptime(update.message.text.strip(), "%Y-%m-%d %H:%M") if when < datetime.now(): await update.message.reply_text( "Дата должна быть в будущем. Укажите корректное время в формате YYYY-MM-DD HH:MM" ) return BotStates.ENTER_SCHEDULE await _dispatch_with_eta(uid, when) await update.message.reply_text("Задача запланирована.") return ConversationHandler.END except ValueError: await update.message.reply_text( "Неверный формат даты. Используйте формат YYYY-MM-DD HH:MM" ) return BotStates.ENTER_SCHEDULE except Exception as e: logger.error(f"Error scheduling post: {e}") await update.message.reply_text("Ошибка планирования. Попробуйте позже.") return ConversationHandler.END 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: 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 _dispatch_with_eta(uid: int, when: datetime) -> None: """Отправка отложенного поста.""" session_store = get_session_store() data = session_store.get(uid) if not data: raise ValueError("Сессия потеряна") token = settings.editor_bot_token try: payload = build_payload( ptype=str(data.type) if data.type else "text", text=data.text, media_file_id=data.media_file_id, parse_mode=data.parse_mode or "HTML", keyboard=data.keyboard, ) # Проверка длины сообщения if not validate_message_length(payload.get("text", "")): raise ValueError("Превышен максимальный размер сообщения") # Проверка URL в клавиатуре if keyboard := payload.get("keyboard"): for row in keyboard.get("rows", []): for btn in row: if "url" in btn and not validate_url(btn["url"]): raise ValueError(f"Небезопасный URL: {btn['url']}") celery_app.send_task( 'app.tasks.senders.send_post_task', args=[token, data.channel_id, payload], eta=when ) except Exception as e: logger.error(f"Error in _dispatch_with_eta: {e}") raise def init_application(): """Инициализация приложения и регистрация обработчиков.""" # Настройка логирования logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) # Инициализация бота app = Application.builder().token(settings.editor_bot_token).build() # Создание обработчика постов post_conv = ConversationHandler( entry_points=[CommandHandler("newpost", newpost)], states={ BotStates.CHOOSE_CHANNEL: [CallbackQueryHandler(choose_channel, pattern=r"^channel:")], BotStates.CHOOSE_TYPE: [CallbackQueryHandler(choose_type, pattern=r"^type:")], BotStates.CHOOSE_FORMAT: [CallbackQueryHandler(choose_format, pattern=r"^fmt:")], BotStates.ENTER_TEXT: [ MessageHandler(filters.TEXT & ~filters.COMMAND, enter_text), CallbackQueryHandler(choose_template_open, pattern=r"^tpl:choose$"), ], BotStates.SELECT_TEMPLATE: [ CallbackQueryHandler(choose_template_apply, pattern=r"^tpluse:"), CallbackQueryHandler(choose_template_preview, pattern=r"^tplprev:"), CallbackQueryHandler(choose_template_navigate, pattern=r"^tplpage:"), CallbackQueryHandler(choose_template_cancel, pattern=r"^tpl:cancel$"), ], BotStates.TEMPLATE_VARS: [ MessageHandler(filters.TEXT & ~filters.COMMAND, preview_collect_vars) ], BotStates.PREVIEW_CONFIRM: [ CallbackQueryHandler(preview_confirm, pattern=r"^pv:(use|edit)$"), CallbackQueryHandler(choose_template_cancel, pattern=r"^tpl:cancel$"), ], BotStates.ENTER_MEDIA: [ MessageHandler(filters.TEXT & ~filters.COMMAND, enter_media) ], BotStates.EDIT_KEYBOARD: [ MessageHandler(filters.TEXT & ~filters.COMMAND, edit_keyboard) ], BotStates.CONFIRM_SEND: [ CallbackQueryHandler(confirm_send, pattern=r"^send:") ], BotStates.ENTER_SCHEDULE: [ MessageHandler(filters.TEXT & ~filters.COMMAND, enter_schedule) ], }, fallbacks=[CommandHandler("start", start)], ) # Создание обработчика шаблонов tpl_conv = ConversationHandler( entry_points=[CommandHandler("tpl_new", tpl_new_start)], states={ BotStates.TPL_NEW_NAME: [ MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_name) ], BotStates.TPL_NEW_TYPE: [ CallbackQueryHandler(tpl_new_type, pattern=r"^type:") ], BotStates.TPL_NEW_FORMAT: [ CallbackQueryHandler(tpl_new_format, pattern=r"^format:") ], BotStates.TPL_NEW_CONTENT: [ MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_content) ], BotStates.TPL_NEW_KB: [ MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_kb) ], }, fallbacks=[CommandHandler("cancel", cancel_handler)], ) # Регистрация всех обработчиков app.add_handler(CommandHandler("start", start)) app.add_handler(post_conv) app.add_handler(tpl_conv) app.add_handler(CommandHandler("tpl_list", tpl_list)) return app # -------- Вспомогательные функции для шаблонов --------- async def _render_tpl_list_message(message: Message, uid: int, page: int) -> int: """Отображение списка шаблонов с пагинацией через сообщение.""" try: total = await count_templates(uid) offset = page * PAGE_SIZE tpls = await list_templates(uid, limit=PAGE_SIZE, offset=offset) if not tpls: if page == 0: await message.reply_text("Шаблонов пока нет. Создай через /tpl_new.") return BotStates.ENTER_TEXT else: return await _render_tpl_list_message(message, uid, 0) kb = [] for t in tpls: kb.append([ InlineKeyboardButton((t.title or t.name), callback_data=f"tpluse:{t.name}"), InlineKeyboardButton("👁 Предпросмотр", callback_data=f"tplprev:{t.name}") ]) nav = [] if page > 0: nav.append(InlineKeyboardButton("◀️ Назад", callback_data=f"tplpage:{page-1}")) if offset + PAGE_SIZE < total: nav.append(InlineKeyboardButton("Вперёд ▶️", callback_data=f"tplpage:{page+1}")) if nav: kb.append(nav) text = "📜 Список шаблонов:\n\n" if total > PAGE_SIZE: text += f"(Страница {page + 1})" await message.reply_text(text, reply_markup=InlineKeyboardMarkup(kb)) return ENTER_TEXT except Exception as e: logger.error(f"Error rendering template list for message: {e}") await message.reply_text("Ошибка при загрузке списка шаблонов") return BotStates.ENTER_TEXT async def _render_tpl_list_query(query: CallbackQuery, uid: int, page: int) -> int: """Отображение списка шаблонов с пагинацией через callback query.""" try: total = await count_templates(uid) offset = page * PAGE_SIZE tpls = await list_templates(uid, limit=PAGE_SIZE, offset=offset) if not tpls: if page == 0: await query.edit_message_text("Шаблонов пока нет. Создай через /tpl_new.") return BotStates.ENTER_TEXT else: return await _render_tpl_list_query(query, uid, 0) kb = [] for t in tpls: kb.append([ InlineKeyboardButton((t.title or t.name), callback_data=f"tpluse:{t.name}"), InlineKeyboardButton("👁 Предпросмотр", callback_data=f"tplprev:{t.name}") ]) nav = [] if page > 0: nav.append(InlineKeyboardButton("◀️ Назад", callback_data=f"tplpage:{page-1}")) if offset + PAGE_SIZE < total: nav.append(InlineKeyboardButton("Вперёд ▶️", callback_data=f"tplpage:{page+1}")) if nav: kb.append(nav) text = "📜 Список шаблонов:\n\n" if total > PAGE_SIZE: text += f"(Страница {page + 1})" await query.edit_message_text(text, reply_markup=InlineKeyboardMarkup(kb)) return ENTER_TEXT except Exception as e: logger.error(f"Error rendering template list for query: {e}") await query.edit_message_text("Ошибка при загрузке списка шаблонов") return BotStates.ENTER_TEXT async def _render_tpl_list(update: Update, uid: int, page: int) -> int: """Отображение списка шаблонов с пагинацией.""" try: offset = page * PAGE_SIZE total = await count_templates(uid) tpls = await list_templates(uid, offset=offset, limit=PAGE_SIZE) kb = [] for t in tpls: kb.append([ InlineKeyboardButton((t.title or t.name), callback_data=f"tpluse:{t.name}"), InlineKeyboardButton("👁 Предпросмотр", callback_data=f"tplprev:{t.name}") ]) nav = [] if page > 0: nav.append(InlineKeyboardButton("◀️ Назад", callback_data=f"tplpage:{page-1}")) if offset + PAGE_SIZE < total: nav.append(InlineKeyboardButton("Вперёд ▶️", callback_data=f"tplpage:{page+1}")) if nav: kb.append(nav) kb.append([InlineKeyboardButton("Отмена", callback_data="tpl:cancel")]) text = f"Шаблоны (стр. {page+1}/{(total-1)//PAGE_SIZE + 1}):" msg = update.effective_message if isinstance(msg, Message): if isinstance(update.callback_query, CallbackQuery): await msg.edit_text(text, reply_markup=InlineKeyboardMarkup(kb)) else: await msg.reply_text(text, reply_markup=InlineKeyboardMarkup(kb)) return BotStates.SELECT_TEMPLATE except Exception as e: logger.error(f"Error rendering template list: {e}") if update.effective_message: await update.effective_message.reply_text("Ошибка при загрузке списка шаблонов") return BotStates.ENTER_TEXT async def _apply_template_and_confirm_message(message: Message, uid: int, name: str, ctx_vars: dict) -> int: """Применение шаблона к текущему посту через сообщение.""" try: rendered = await render_template_by_name(name=name, template_vars=ctx_vars, context={"user_id": uid}) session = get_session_store().get(uid) session.type = rendered["type"] session.text = rendered["text"] session.keyboard = rendered.get("keyboard") session.parse_mode = rendered.get("parse_mode", "HTML") session.touch() kb = [ [InlineKeyboardButton("Отправить сейчас", callback_data="send:now")], [InlineKeyboardButton("Запланировать", callback_data="send:schedule")] ] markup = InlineKeyboardMarkup(kb) await message.reply_text( "Шаблон применён. Как публикуем?", reply_markup=markup ) return BotStates.CONFIRM_SEND except Exception as e: logger.error(f"Error applying template via message: {e}") await message.reply_text("Ошибка при применении шаблона") return ConversationHandler.END async def _apply_template_and_confirm_query(query: CallbackQuery, uid: int, name: str, ctx_vars: dict) -> int: """Применение шаблона к текущему посту через callback query.""" try: rendered = await render_template_by_name(name=name, template_vars=ctx_vars, context={"user_id": uid}) session = get_session_store().get(uid) session.type = rendered["type"] session.text = rendered["text"] session.keyboard = rendered.get("keyboard") session.parse_mode = rendered.get("parse_mode", "HTML") session.touch() kb = [ [InlineKeyboardButton("Отправить сейчас", callback_data="send:now")], [InlineKeyboardButton("Запланировать", callback_data="send:schedule")] ] markup = InlineKeyboardMarkup(kb) await query.edit_message_text( "Шаблон применён. Как публикуем?", reply_markup=markup ) return BotStates.CONFIRM_SEND except Exception as e: logger.error(f"Error applying template via query: {e}") await query.edit_message_text("Ошибка при применении шаблона") return ConversationHandler.END async def _apply_template_and_confirm(update: Update, uid: int, name: str, ctx_vars: dict) -> int: """Применение шаблона к текущему посту.""" try: if update.callback_query: return await _apply_template_and_confirm_query(update.callback_query, uid, name, ctx_vars) elif update.message: return await _apply_template_and_confirm_message(update.message, uid, name, ctx_vars) else: logger.error("Neither callback_query nor message found in update") return ConversationHandler.END except Exception as e: logger.error(f"Error in _apply_template_and_confirm: {e}") return ConversationHandler.END async def _render_preview_and_confirm_message(message: Message, uid: int, name: str, ctx_vars: dict) -> int: """Рендеринг предпросмотра шаблона через сообщение.""" try: rendered = await render_template_by_name(name=name, template_vars=ctx_vars, context={"user_id": uid}) text = rendered["text"] user_session = get_session_store().get(uid) parse_mode = rendered.get("parse_mode") or "HTML" preview_text = f"Предпросмотр:\n\n{text[:3500]}" await message.reply_text(preview_text, parse_mode=parse_mode) kb = [ [InlineKeyboardButton("✅ Использовать", callback_data="pv:use")], [InlineKeyboardButton("✏️ Изменить переменные", callback_data="pv:edit")], [InlineKeyboardButton("Отмена", callback_data="tpl:cancel")] ] markup = InlineKeyboardMarkup(kb) await message.reply_text("Что дальше?", reply_markup=markup) return BotStates.PREVIEW_CONFIRM except Exception as e: logger.error(f"Error in preview render via message: {e}") await message.reply_text("Ошибка при рендеринге шаблона") return ConversationHandler.END async def _render_preview_and_confirm_query(query: CallbackQuery, uid: int, name: str, ctx_vars: dict) -> int: """Рендеринг предпросмотра шаблона через callback query.""" try: rendered = await render_template_by_name(name=name, template_vars=ctx_vars, context={"user_id": uid}) text = rendered["text"] session_store = get_session_store() user_session = session_store.get(uid) parse_mode = rendered.get("parse_mode") or (user_session.parse_mode if user_session else None) or "HTML" preview_text = f"Предпросмотр:\n\n{text[:3500]}" await query.edit_message_text(preview_text, parse_mode=parse_mode) kb = [ [InlineKeyboardButton("✅ Использовать", callback_data="pv:use")], [InlineKeyboardButton("✏️ Изменить переменные", callback_data="pv:edit")], [InlineKeyboardButton("Отмена", callback_data="tpl:cancel")] ] markup = InlineKeyboardMarkup(kb) if query.message: message = query.message if isinstance(message, Message): await message.edit_text("Что дальше?", reply_markup=markup) return BotStates.PREVIEW_CONFIRM except Exception as e: logger.error(f"Error in preview render via query: {e}") await query.edit_message_text("Ошибка при рендеринге шаблона") return ConversationHandler.END async def _render_preview_and_confirm(update: Update, uid: int, name: str, ctx_vars: dict) -> int: """Рендеринг предпросмотра шаблона.""" try: if update.callback_query: return await _render_preview_and_confirm_query(update.callback_query, uid, name, ctx_vars) elif update.message: return await _render_preview_and_confirm_message(update.message, uid, name, ctx_vars) else: logger.error("Neither callback_query nor message found in update") return ConversationHandler.END except Exception as e: logger.error(f"Error in _render_preview_and_confirm: {e}") return ConversationHandler.END # -------- Обработчики шаблонов --------- async def choose_template_open(update: Update, context: Context) -> int: """Открытие списка шаблонов.""" query = cast(Optional[CallbackQuery], update.callback_query) if not query: return ConversationHandler.END await query.answer() user = update.effective_user if not user or not user.id: msg = cast(Optional[Message], query.message) if msg and hasattr(msg, 'edit_text'): await msg.edit_text("Ошибка: пользователь не определен") return ConversationHandler.END if hasattr(context, 'user_data') and context.user_data is not None: context.user_data["tpl_page"] = 0 return await _render_tpl_list(update, user.id, page=0) async def choose_template_navigate(update: Update, context: Context) -> int: """Навигация по списку шаблонов.""" query = cast(Optional[CallbackQuery], update.callback_query) user = update.effective_user if not query or not query.data or not user or not user.id: return ConversationHandler.END try: await query.answer() msg = cast(Optional[Message], query.message) # Получаем номер страницы из callback data parts = query.data.split(":", 1) if len(parts) != 2: if msg and hasattr(msg, 'edit_text'): await msg.edit_text("Ошибка: неверный формат данных") return ConversationHandler.END try: page = max(0, int(parts[1])) except ValueError: if msg and hasattr(msg, 'edit_text'): await msg.edit_text("Ошибка: некорректный номер страницы") return ConversationHandler.END if context and context.user_data is not None: context.user_data["tpl_page"] = page return await _render_tpl_list(update, user.id, page) except Exception as e: logger.error(f"Ошибка при навигации по шаблонам: {e}", exc_info=True) msg = cast(Optional[Message], query.message) if msg and hasattr(msg, 'edit_text'): await msg.edit_text("Произошла ошибка при навигации") return ConversationHandler.END page = int(page_s) context.user_data["tpl_page"] = page return await _render_tpl_list(q, uid, page) async def choose_template_apply(update: Update, context: Context) -> int: """Применение выбранного шаблона.""" query = cast(Optional[CallbackQuery], update.callback_query) user = update.effective_user if not query or not isinstance(query.data, str) or not user or not user.id: return ConversationHandler.END try: await query.answer() msg = cast(Optional[Message], query.message) parts = query.data.split(":", 1) if len(parts) != 2: if msg and hasattr(msg, 'edit_text'): await msg.edit_text("Ошибка: неверный формат данных") return ConversationHandler.END name = parts[1] # Получаем шаблон и проверяем переменные tpl = await render_template_by_name(name=name, template_vars={}, context={"user_id": user.id}) if not tpl: if msg and hasattr(msg, 'edit_text'): await msg.edit_text("Ошибка: шаблон не найден") return ConversationHandler.END required = set(tpl.get("_required", [])) # Сохраняем данные в контекст if context and context.user_data is not None: context.user_data["preview"] = { "name": name, "provided": {}, "missing": list(required) } # Если есть обязательные переменные, запрашиваем их заполнение if required: if msg and hasattr(msg, 'edit_text'): next_var = list(required)[0] await msg.edit_text( f"Для этого шаблона требуются параметры:\n" f"{', '.join(sorted(required))}\n\n" f"Пожалуйста, введите значение для параметра {next_var}:" ) return BotStates.TEMPLATE_VARS # Если переменных нет, применяем шаблон сразу return await _apply_template_and_confirm(update, user.id, name, {}) except Exception as e: logger.error(f"Ошибка при применении шаблона: {e}", exc_info=True) msg = cast(Optional[Message], query.message if query else None) if msg and hasattr(msg, 'edit_text'): await msg.edit_text("Ошибка при применении шаблона") return ConversationHandler.END async def choose_template_preview(update: Update, context: Context) -> int: """Предпросмотр шаблона.""" if (not update.callback_query or not update.effective_user or not context.user_data or not update.callback_query.data): return ConversationHandler.END await update.callback_query.answer() uid = update.effective_user.id try: name = update.callback_query.data.split(":")[1] tpl = await render_template_by_name(name=name, template_vars={}, context={"user_id": uid}) required = set(tpl.get("_required", [])) context.user_data["preview"] = { "name": name, "provided": {}, "missing": list(required) } if required: await update.callback_query.edit_message_text( "Для предпросмотра нужны переменные: " + ", ".join(sorted(required)) + "\nПришли значения в формате key=value (по строкам или в одну строку)." ) return PREVIEW_VARS return await _render_preview_and_confirm(update, uid, name, {}) except Exception as e: logger.error(f"Error previewing template: {e}") await update.callback_query.edit_message_text("Ошибка при предпросмотре шаблона") return ConversationHandler.END async def choose_template_cancel(update: Update, context: Context) -> int: """Отмена выбора шаблона.""" if not update.callback_query: return ConversationHandler.END await update.callback_query.answer() await update.callback_query.edit_message_text( "Отправь текст сообщения или введи #имя для шаблона." ) return ENTER_TEXT import asyncio import signal from contextlib import suppress class BotApplication: """Класс для управления жизненным циклом бота и планировщика.""" def __init__(self): self.app = None self.scheduler = None self.loop = None self._shutdown = False def signal_handler(self, signum, frame): """Обработчик сигналов для корректного завершения.""" self._shutdown = True if self.loop: self.loop.stop() async def shutdown(self): """Корректное завершение работы всех компонентов.""" if self.scheduler: with suppress(Exception): self.scheduler.shutdown() if self.app: with suppress(Exception): await self.app.stop() with suppress(Exception): await self.app.shutdown() async def run_bot(self): """Запуск бота и планировщика.""" try: # Инициализация приложения self.app = init_application() if not self.app: logger.critical("Failed to initialize application") return # Настройка планировщика self.scheduler = AsyncIOScheduler() self.scheduler.add_job(cleanup_old_sessions, 'interval', minutes=30) # Инициализация приложения await self.app.initialize() await self.app.start() # Запуск планировщика self.scheduler.start() # Запуск бота while not self._shutdown: try: if self.app and self.app.updater: await self.app.updater.start_polling() else: logger.error("Application or updater is not initialized") await asyncio.sleep(1) except Exception as e: if not self._shutdown: logger.error(f"Polling error: {e}") await asyncio.sleep(1) else: break except Exception as e: logger.critical(f"Critical error: {e}") finally: await self.shutdown() async def init_bot(): """Инициализация и настройка бота.""" try: application = init_application() if not application: logger.critical("Не удалось инициализировать приложение") return None return application except Exception as e: logger.critical(f"Ошибка при инициализации бота: {e}") return None def start_bot(): """Запуск бота с правильным управлением циклом событий.""" loop = None try: # Создаем и настраиваем цикл событий loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) # Инициализируем бота application = loop.run_until_complete(init_bot()) if not application: return # Запускаем бота с правильным управлением циклом событий from app.bots.bot_runner import run_bot run_bot(application, cleanup_old_sessions) except Exception as e: logger.critical(f"Критическая ошибка при запуске бота: {e}") raise finally: if loop: try: loop.close() except Exception as e: logger.error(f"Ошибка при закрытии цикла событий: {e}") try: # Создаем и настраиваем цикл событий loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) # Инициализируем бота application = loop.run_until_complete(init_bot()) if not application: return # Запускаем бота с правильным управлением циклом событий from app.bots.bot_runner import run_bot run_bot(application, cleanup_old_sessions) except Exception as e: logger.critical(f"Критическая ошибка при запуске бота: {e}") raise finally: if loop: try: loop.close() except Exception as e: logger.error(f"Ошибка при закрытии цикла событий: {e}") if __name__ == "__main__": start_bot()