"""Обработчики для работы с постами.""" from datetime import datetime from logging import getLogger import re from typing import Dict, Any, Optional, cast, Union from telegram import ( Update, Message, CallbackQuery, ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton ) from telegram.ext import ( ContextTypes, ConversationHandler ) from telegram.helpers import escape_markdown from telegram.constants import ChatAction, ParseMode from telegram.error import BadRequest, Forbidden, TelegramError from ..session import UserSession, SessionStore from ..states import BotStates from ..keyboards import KbBuilder from app.models.post import Post, PostType from app.services.template import TemplateService from app.services.channels import ChannelService from app.services.telegram import PostService from ..messages import MessageType logger = getLogger(__name__) def parse_key_value_lines(text: str) -> Dict[str, str]: """Парсит строки в формате 'ключ = значение' в словарь.""" if not text: return {} result = {} for line in text.split('\n'): if '=' not in line: continue key, value = map(str.strip, line.split('=', 1)) if key: result[key] = value return result logger = getLogger(__name__) logger = getLogger(__name__) async def newpost(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Начало создания нового поста.""" message = update.effective_message user = update.effective_user if not message or not user: return ConversationHandler.END try: # Создаем новую сессию session = SessionStore.get_instance().get(user.id) session.clear() # Загружаем список каналов пользователя channels = await ChannelService.get_user_channels(user.id) if not channels: await message.reply_text( "У вас нет добавленных каналов. Используйте /add_channel чтобы добавить." ) return ConversationHandler.END kb = KbBuilder.channels(channels) await message.reply_text( "Выберите канал для публикации:", reply_markup=kb ) return BotStates.CHOOSE_CHANNEL except Exception as e: logger.error(f"Error in newpost: {e}") await message.reply_text("Произошла ошибка при создании поста") return ConversationHandler.END async def choose_channel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Обработка выбора канала.""" query = update.callback_query if not query or not query.from_user: return ConversationHandler.END await query.answer() try: message = cast(Message, query.message) if not query.data: await message.edit_text("Неверный формат данных") return ConversationHandler.END channel_id = int(query.data.replace("channel:", "")) # Проверяем существование канала channel = await ChannelService.get_channel(channel_id) if not channel: await message.edit_text("Канал не найден") return ConversationHandler.END session = SessionStore.get_instance().get(query.from_user.id) session.channel_id = channel_id kb = KbBuilder.post_types() await message.edit_text( "Выберите тип поста:", reply_markup=kb ) return BotStates.CHOOSE_TYPE except Exception as e: logger.error(f"Error in choose_channel: {e}") if query.message: await query.message.edit_text("Произошла ошибка при выборе канала") return ConversationHandler.END async def choose_type(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Обработка выбора типа поста.""" query = update.callback_query if not query or not query.from_user: return ConversationHandler.END await query.answer() try: message = cast(Message, query.message) if not query.data: await message.edit_text("Неверный формат данных") return ConversationHandler.END post_type = PostType(query.data.replace("type:", "")) session = SessionStore.get_instance().get(query.from_user.id) session.type = post_type kb = KbBuilder.parse_modes() await message.edit_text( "Выберите формат текста:", reply_markup=kb ) return BotStates.CHOOSE_FORMAT except Exception as e: logger.error(f"Error in choose_type: {e}") if query.message: await query.message.edit_text("Произошла ошибка при выборе типа поста") return ConversationHandler.END async def choose_format(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Обработка выбора формата текста.""" query = update.callback_query if not query or not query.from_user: return ConversationHandler.END await query.answer() try: message = cast(Message, query.message) if not query.data: await message.edit_text("Неверный формат данных") return ConversationHandler.END parse_mode = query.data.replace("fmt:", "") if parse_mode not in [ParseMode.HTML, ParseMode.MARKDOWN_V2]: await message.edit_text("Неизвестный формат текста") return ConversationHandler.END session = SessionStore.get_instance().get(query.from_user.id) session.parse_mode = parse_mode kb = KbBuilder.text_input_options() await message.edit_text( "Введите текст поста или выберите шаблон:", reply_markup=kb ) return BotStates.ENTER_TEXT except Exception as e: logger.error(f"Error in choose_format: {e}") if query.message: await query.message.edit_text("Произошла ошибка при выборе формата") return ConversationHandler.END async def enter_text(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Обработка ввода текста поста.""" message = update.effective_message user = update.effective_user if not message or not user: return ConversationHandler.END text = message.text if not text: await message.reply_text("Пожалуйста, введите текст поста") return BotStates.ENTER_TEXT try: session = SessionStore.get_instance().get(user.id) session.text = text if session.type == MessageType.TEXT: await message.reply_text( "Введите клавиатуру в формате:\n" "текст кнопки = ссылка\n\n" "Или отправьте 'skip' чтобы пропустить" ) return BotStates.EDIT_KEYBOARD await message.reply_text( "Отправьте фото/видео/gif для поста" ) return BotStates.ENTER_MEDIA except Exception as e: logger.error(f"Error in enter_text: {e}") await message.reply_text("Произошла ошибка при сохранении текста") return ConversationHandler.END async def choose_template_open(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Открытие выбора шаблона.""" query = update.callback_query if not query or not query.from_user: return ConversationHandler.END await query.answer() try: message = cast(Message, query.message) user_id = query.from_user.id templates = await TemplateService.list_user_templates(user_id) if not templates: await message.edit_text( "У вас нет шаблонов. Создайте новый с помощью /newtemplate" ) return BotStates.ENTER_TEXT total = len(templates) if total == 0: await message.edit_text("Список шаблонов пуст") return BotStates.ENTER_TEXT user_data = context.user_data if not user_data: user_data = {} context.user_data = user_data page = user_data.get("tpl_page", 0) items = templates[page * KbBuilder.PAGE_SIZE:(page + 1) * KbBuilder.PAGE_SIZE] kb = KbBuilder.templates_list(items, page, total) await message.edit_text( "Выберите шаблон:", reply_markup=kb ) return BotStates.SELECT_TEMPLATE except Exception as e: logger.error(f"Error in choose_template_open: {e}") if query.message: await query.message.edit_text("Произошла ошибка при загрузке шаблонов") return ConversationHandler.END async def choose_template_apply(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Применение выбранного шаблона.""" query = update.callback_query if not query or not query.from_user: return ConversationHandler.END await query.answer() try: message = cast(Message, query.message) if not query.data: await message.edit_text("Неверный формат данных") return BotStates.SELECT_TEMPLATE template_id = query.data.replace("tpluse:", "") template = await TemplateService.get_template(template_id) if not template: await message.edit_text("Шаблон не найден") return BotStates.ENTER_TEXT session = SessionStore.get_instance().get(query.from_user.id) session.template_id = template_id session.text = template.content if "{" in template.content and "}" in template.content: # Шаблон содержит переменные await message.edit_text( "Введите значения для переменных в формате:\n" "переменная = значение" ) return BotStates.PREVIEW_VARS # Нет переменных, можно сразу показать предпросмотр kb = KbBuilder.preview_confirm() post_data = session.to_dict() await PostService.preview_post(message, post_data) await message.reply_text( "Предпросмотр поста. Выберите действие:", reply_markup=kb ) return BotStates.PREVIEW_CONFIRM except Exception as e: logger.error(f"Error in choose_template_apply: {e}") if query.message: await query.message.edit_text( "Произошла ошибка при применении шаблона" ) return BotStates.ENTER_TEXT except Exception as e: logger.error(f"Ошибка при применении шаблона: {e}") await query.message.edit_text( f"Ошибка при применении шаблона: {str(e)}" ) return BotStates.ENTER_TEXT async def choose_template_preview(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Предпросмотр шаблона.""" query = update.callback_query if not query or not query.from_user: return ConversationHandler.END await query.answer() try: message = cast(Message, query.message) if not query.data: await message.edit_text("Неверный формат данных") return BotStates.SELECT_TEMPLATE template_id = query.data.replace("tplprev:", "") template = await TemplateService.get_template(template_id) if not template: await message.edit_text("Шаблон не найден") return BotStates.SELECT_TEMPLATE await message.edit_text( f"Предпросмотр шаблона:\n\n{template.content}", parse_mode=template.parse_mode ) return BotStates.SELECT_TEMPLATE except Exception as e: logger.error(f"Error in choose_template_preview: {e}") if query.message: await query.message.edit_text( "Произошла ошибка при предпросмотре шаблона" ) return BotStates.SELECT_TEMPLATE async def choose_template_navigate(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Навигация по страницам шаблонов.""" query = update.callback_query if not query or not query.from_user: return ConversationHandler.END await query.answer() try: message = cast(Message, query.message) if not query.data: return BotStates.SELECT_TEMPLATE # Получаем номер страницы page = int(query.data.replace("tplpage:", "")) user_data = context.user_data if not user_data: user_data = {} context.user_data = user_data user_data["tpl_page"] = page # Перестраиваем список для новой страницы templates = await TemplateService.list_user_templates(query.from_user.id) total = len(templates) items = templates[page * KbBuilder.PAGE_SIZE:(page + 1) * KbBuilder.PAGE_SIZE] kb = KbBuilder.templates_list(items, page, total) await message.edit_reply_markup(reply_markup=kb) return BotStates.SELECT_TEMPLATE except Exception as e: logger.error(f"Error in choose_template_navigate: {e}") if query.message: await query.message.edit_text("Произошла ошибка при смене страницы") return ConversationHandler.END async def choose_template_cancel(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Отмена выбора шаблона.""" query = update.callback_query if not query: return ConversationHandler.END await query.answer() await query.message.edit_text( "Введите текст поста:" ) return BotStates.ENTER_TEXT async def preview_collect_vars(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Сбор значений переменных для шаблона.""" message = update.message if not message or not message.from_user or not message.text: return ConversationHandler.END try: variables = parse_key_value_lines(message.text) session = SessionStore.get_instance().get(message.from_user.id) if not session.template_id: await message.reply_text("Шаблон не выбран") return BotStates.ENTER_TEXT template = await TemplateService.get_template(session.template_id) if not template: await message.reply_text("Шаблон не найден") return BotStates.ENTER_TEXT # Подставляем значения переменных text = template.content for var, value in variables.items(): text = text.replace(f"{{{var}}}", value) session.text = text post_data = session.to_dict() kb = KbBuilder.preview_confirm() await PostService.preview_post(message, post_data) await message.reply_text( "Предпросмотр поста. Выберите действие:", reply_markup=kb ) return BotStates.PREVIEW_CONFIRM except ValueError as e: await message.reply_text( f"Ошибка в формате переменных: {str(e)}\n" "Используйте формат:\n" "переменная = значение" ) return BotStates.PREVIEW_VARS except Exception as e: logger.error(f"Error in preview_collect_vars: {e}") await message.reply_text( "Произошла ошибка при обработке переменных" ) return BotStates.PREVIEW_VARS async def preview_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Подтверждение предпросмотра поста.""" query = update.callback_query if not query or not query.from_user: return ConversationHandler.END await query.answer() try: message = cast(Message, query.message) if not query.data: await message.edit_text("Неверный формат данных") return BotStates.PREVIEW_CONFIRM action = query.data.replace("pv:", "") if action == "edit": await message.edit_text( "Введите текст поста:" ) return BotStates.ENTER_TEXT session = SessionStore.get_instance().get(query.from_user.id) if not session.type: await message.edit_text("Ошибка: не выбран тип поста") return ConversationHandler.END if session.type == MessageType.TEXT: await message.edit_text( "Введите клавиатуру в формате:\n" "текст кнопки = ссылка\n\n" "Или отправьте 'skip' чтобы пропустить" ) return BotStates.EDIT_KEYBOARD await message.edit_text( "Отправьте фото/видео/gif для поста" ) return BotStates.ENTER_MEDIA except Exception as e: logger.error(f"Error in preview_confirm: {e}") if query.message: await query.message.edit_text( "Произошла ошибка при обработке предпросмотра" ) return ConversationHandler.END async def enter_media(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Обработка медиафайла.""" message = update.message if not message or not message.from_user: return ConversationHandler.END try: session = SessionStore.get_instance().get(message.from_user.id) if message.photo: session.media_file_id = message.photo[-1].file_id elif message.video: session.media_file_id = message.video.file_id elif message.animation: session.media_file_id = message.animation.file_id elif message.document: session.media_file_id = message.document.file_id else: await message.reply_text( "Пожалуйста, отправьте фото, видео или GIF" ) return BotStates.ENTER_MEDIA # Показываем предпросмотр kb = KbBuilder.preview_confirm() post_data = session.to_dict() await PostService.preview_post(message, post_data) await message.reply_text( "Предпросмотр поста. Выберите действие:", reply_markup=kb ) return BotStates.PREVIEW_CONFIRM except Exception as e: logger.error(f"Error in enter_media: {e}") await message.reply_text( "Произошла ошибка при обработке файла" ) return ConversationHandler.END session.media_id = message.animation.file_id await message.reply_text( "Введите клавиатуру в формате:\n" "текст кнопки = ссылка\n\n" "Или отправьте 'skip' чтобы пропустить" ) return BotStates.EDIT_KEYBOARD async def edit_keyboard(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Обработка клавиатуры поста.""" message = update.message if not message or not message.from_user or not message.text: return ConversationHandler.END try: kb_text = message.text.strip() session = SessionStore.get_instance().get(message.from_user.id) if kb_text.lower() != "skip": keyboard = parse_key_value_lines(kb_text) session.keyboard = {"rows": []} for text, url in keyboard.items(): session.keyboard["rows"].append([{"text": text, "url": url}]) # Показываем предпросмотр поста post_data = session.to_dict() await PostService.preview_post(message, post_data) keyboard = InlineKeyboardMarkup([ [ InlineKeyboardButton("Отправить", callback_data="send:now"), InlineKeyboardButton("Отложить", callback_data="send:schedule") ] ]) await message.reply_text( "Выберите действие:", reply_markup=keyboard ) return BotStates.CONFIRM_SEND except ValueError as e: await message.reply_text( f"Ошибка в формате клавиатуры: {e}\n" "Используйте формат:\n" "текст кнопки = ссылка" ) return BotStates.EDIT_KEYBOARD except Exception as e: logger.error(f"Error in edit_keyboard: {e}") await message.reply_text( "Произошла ошибка при обработке клавиатуры" ) return ConversationHandler.END async def confirm_send(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Подтверждение отправки поста.""" query = update.callback_query if not query or not query.from_user: return ConversationHandler.END await query.answer() try: message = cast(Message, query.message) if not query.data: await message.edit_text("Неверный формат данных") return BotStates.CONFIRM_SEND action = query.data.replace("send:", "") if action == "schedule": await message.edit_text( "Введите дату и время для отложенной публикации в формате:\n" "ДД.ММ.ГГГГ ЧЧ:ММ" ) return BotStates.ENTER_SCHEDULE session = SessionStore.get_instance().get(query.from_user.id) # Отправляем пост сейчас post_data = session.to_dict() message = query.message if not message: return BotStates.PREVIEW_CONFIRM if not session.channel_id: await context.bot.send_message( chat_id=message.chat.id, text="Канал не выбран", reply_markup=KbBuilder.go_back() ) return BotStates.PREVIEW_CONFIRM post = await PostService.create_post(context.bot, session.channel_id, post_data) if post: await context.bot.edit_message_text( chat_id=message.chat.id, message_id=message.message_id, text="Пост успешно отправлен!" ) SessionStore.get_instance().drop(query.from_user.id) return ConversationHandler.END await context.bot.send_message( chat_id=message.chat.id, text="Ошибка при отправке поста. Попробуйте позже.", reply_markup=KbBuilder.go_back() ) return BotStates.PREVIEW_CONFIRM except Exception as e: logger.error(f"Error in confirm_send: {e}") message = query.message if message: await context.bot.edit_message_text( chat_id=message.chat.id, message_id=message.message_id, text="Произошла ошибка при отправке поста" ) return BotStates.PREVIEW_CONFIRM return ConversationHandler.END session = get_session_store().get_or_create(query.from_user.id) try: # Отправляем пост await schedule_post(session, schedule_time=None) await query.message.edit_text("Пост успешно отправлен!") # Очищаем сессию get_session_store().drop(query.from_user.id) return ConversationHandler.END except Exception as e: logger.error(f"Ошибка при отправке поста: {e}") await query.message.edit_text( f"Ошибка при отправке поста: {str(e)}" ) return BotStates.CONFIRM_SEND async def enter_schedule(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: """Обработка времени для отложенной публикации.""" message = update.message if not message or not message.from_user or not message.text: return ConversationHandler.END try: schedule_text = message.text.strip() if not schedule_text: await message.reply_text( "Некорректный формат даты.\n" "Используйте формат: ДД.ММ.ГГГГ ЧЧ:ММ" ) return BotStates.ENTER_SCHEDULE try: schedule_time = datetime.strptime(schedule_text, "%d.%m.%Y %H:%M") if schedule_time <= datetime.now(): await message.reply_text( "Нельзя указать время в прошлом.\n" "Введите время в будущем." ) return BotStates.ENTER_SCHEDULE session = SessionStore.get_instance().get(message.from_user.id) if not session.channel_id: await message.reply_text("Не выбран канал для публикации") return ConversationHandler.END # Отправляем отложенный пост post_data = session.to_dict() post_data["schedule_time"] = schedule_time await PostService.create_post(context.bot, session.channel_id, post_data) await message.reply_text("Пост запланирован!") # Очищаем сессию SessionStore.get_instance().drop(message.from_user.id) return ConversationHandler.END except ValueError: await message.reply_text( "Некорректный формат даты.\n" "Используйте формат: ДД.ММ.ГГГГ ЧЧ:ММ" ) return BotStates.ENTER_SCHEDULE except Exception as e: logger.error(f"Error in enter_schedule: {e}") await message.reply_text( "Произошла ошибка при обработке времени публикации" ) return ConversationHandler.END