From 18a92ca526e69dd19be23a5dfd3828fc019d19c3 Mon Sep 17 00:00:00 2001 From: "Andrey K. Choi" Date: Tue, 19 Aug 2025 05:13:16 +0900 Subject: [PATCH] bot works as echo (adding templates s functional) tpl_list dont --- app/bots/editor/handlers/templates.py | 198 +++++++++++++++++++++++--- app/bots/editor/keyboards.py | 22 ++- app/bots/editor/router.py | 19 ++- app/bots/editor/states.py | 6 +- app/bots/editor_bot.py | 34 ++--- app/services/template.py | 12 +- app/services/users.py | 30 ++++ 7 files changed, 274 insertions(+), 47 deletions(-) create mode 100644 app/services/users.py diff --git a/app/bots/editor/handlers/templates.py b/app/bots/editor/handlers/templates.py index a731198..bd305fc 100644 --- a/app/bots/editor/handlers/templates.py +++ b/app/bots/editor/handlers/templates.py @@ -1,13 +1,27 @@ """Обработчики для работы с шаблонами.""" -from typing import Optional, Dict, Any -from telegram import Update, Message +import logging +import logging +import re +import logging +from typing import Optional, Dict, Any, cast +from telegram import Update, Message, CallbackQuery from telegram.ext import ContextTypes, ConversationHandler +from telegram.error import BadRequest from app.bots.editor.states import BotStates from app.bots.editor.session import get_session_store -from ..keyboards import template_type_keyboard, get_templates_keyboard +from ..keyboards import ( + template_type_keyboard, + get_templates_keyboard, + get_preview_keyboard +) from ..utils.parsers import parse_key_value_lines from ..utils.validation import validate_template_name +from app.services.users import get_or_create_user +from app.services.template import list_templates as get_user_templates_list +from app.services.template import create_template + +logger = logging.getLogger(__name__) async def start_template_creation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> BotStates: """Начало создания шаблона.""" @@ -123,24 +137,174 @@ async def handle_template_keyboard(update: Update, context: ContextTypes.DEFAULT await message.reply_text("Произошла непредвиденная ошибка при создании шаблона") return BotStates.TPL_NEW_KB +def extract_template_vars(content: str) -> list[str]: + """Извлекает переменные из текста шаблона.""" + import re + pattern = r'\{([^}]+)\}' + return list(set(re.findall(pattern, content))) + async def list_templates(update: Update, context: ContextTypes.DEFAULT_TYPE) -> BotStates: """Список шаблонов.""" - if not update.message: + if not update.message or not update.message.from_user: return BotStates.CONVERSATION_END - message = update.message - user_id = message.from_user.id - - templates = await get_user_templates(user_id) - if not templates: - await message.reply_text("У вас пока нет шаблонов") + try: + message = update.message + tg_user = message.from_user + + # Получаем или создаем пользователя + user = await get_or_create_user(tg_user_id=tg_user.id, username=tg_user.username) + + # Получаем шаблоны пользователя + templates = await get_user_templates_list(owner_id=user.id) + + if not templates: + await message.reply_text("Нет доступных шаблонов") + return BotStates.CONVERSATION_END + + # Сохраняем шаблоны в контексте + context.user_data['templates'] = {str(t.id): t for t in templates} + + # Создаем клавиатуру с шаблонами + keyboard = get_templates_keyboard(templates) + + await message.reply_text( + "Выберите шаблон для использования:", + reply_markup=keyboard + ) + return BotStates.SELECT_TEMPLATE + + except Exception as e: + logger.error(f"Ошибка загрузки шаблонов: {e}", exc_info=True) + if update.message: + await update.message.reply_text(f"Ошибка загрузки шаблонов: {e}") + return BotStates.CONVERSATION_END + +async def handle_template_selection(update: Update, context: ContextTypes.DEFAULT_TYPE) -> BotStates: + """Обработка выбора шаблона.""" + query = update.callback_query + if not query: return BotStates.CONVERSATION_END - page = context.user_data.get("tpl_page", 0) - keyboard = get_templates_keyboard(templates, page) + await query.answer() - await message.reply_text( - "Выберите шаблон:", - reply_markup=keyboard - ) - return BotStates.TPL_SELECT + try: + template_id = query.data.split(":")[1] + template = context.user_data['templates'].get(template_id) + + if not template: + await query.message.edit_text("Шаблон не найден") + return BotStates.CONVERSATION_END + + # Сохраняем выбранный шаблон + context.user_data['current_template'] = template + + # Извлекаем переменные из шаблона + vars_needed = extract_template_vars(template.content) + + if not vars_needed: + # Если переменных нет, сразу показываем предпросмотр + rendered_text = template.content + keyboard = get_preview_keyboard() + await query.message.edit_text( + f"Предпросмотр:\n\n{rendered_text}", + reply_markup=keyboard, + parse_mode=template.parse_mode + ) + return BotStates.PREVIEW_CONFIRM + + # Сохраняем список необходимых переменных + context.user_data['vars_needed'] = vars_needed + context.user_data['template_vars'] = {} + + # Запрашиваем первую переменную + await query.message.edit_text( + f"Введите значение для переменной {vars_needed[0]}:" + ) + return BotStates.TEMPLATE_VARS + + except Exception as e: + logger.error(f"Ошибка при выборе шаблона: {e}", exc_info=True) + await query.message.edit_text(f"Произошла ошибка: {e}") + return BotStates.CONVERSATION_END + +async def handle_template_vars(update: Update, context: ContextTypes.DEFAULT_TYPE) -> BotStates: + """Обработка ввода значений переменных шаблона.""" + if not update.message or not update.message.text: + return BotStates.CONVERSATION_END + + try: + vars_needed = context.user_data.get('vars_needed', []) + template_vars = context.user_data.get('template_vars', {}) + template = context.user_data.get('current_template') + + if not vars_needed or not template: + await update.message.reply_text("Ошибка: данные шаблона потеряны") + return BotStates.CONVERSATION_END + + # Сохраняем введенное значение + current_var = vars_needed[len(template_vars)] + template_vars[current_var] = update.message.text + context.user_data['template_vars'] = template_vars + + # Проверяем, все ли переменные заполнены + if len(template_vars) == len(vars_needed): + # Формируем предпросмотр + rendered_text = template.content + for var, value in template_vars.items(): + rendered_text = rendered_text.replace(f"{{{var}}}", value) + + keyboard = get_preview_keyboard() + await update.message.reply_text( + f"Предпросмотр:\n\n{rendered_text}", + reply_markup=keyboard, + parse_mode=template.parse_mode + ) + return BotStates.PREVIEW_CONFIRM + + # Запрашиваем следующую переменную + next_var = vars_needed[len(template_vars)] + await update.message.reply_text( + f"Введите значение для переменной {next_var}:" + ) + return BotStates.TEMPLATE_VARS + + except Exception as e: + logger.error(f"Ошибка при обработке переменных: {e}", exc_info=True) + await update.message.reply_text(f"Произошла ошибка: {e}") + return BotStates.CONVERSATION_END + +async def handle_preview_confirmation(update: Update, context: ContextTypes.DEFAULT_TYPE) -> BotStates: + """Обработка подтверждения предпросмотра.""" + query = update.callback_query + if not query: + return BotStates.CONVERSATION_END + + await query.answer() + + try: + action = query.data.split(":")[1] + + if action == "cancel": + await query.message.edit_text("Отправка отменена") + return BotStates.CONVERSATION_END + + if action == "send": + template = context.user_data.get('current_template') + template_vars = context.user_data.get('template_vars', {}) + + if not template: + await query.message.edit_text("Ошибка: данные шаблона потеряны") + return BotStates.CONVERSATION_END + + # TODO: Добавить логику отправки в канал + await query.message.edit_text( + "Пост успешно отправлен\n\n" + + "(Здесь будет реальная отправка в канал)" + ) + return BotStates.CONVERSATION_END + + except Exception as e: + logger.error(f"Ошибка при подтверждении: {e}", exc_info=True) + await query.message.edit_text(f"Произошла ошибка: {e}") + return BotStates.CONVERSATION_END diff --git a/app/bots/editor/keyboards.py b/app/bots/editor/keyboards.py index 1a12e62..d442844 100644 --- a/app/bots/editor/keyboards.py +++ b/app/bots/editor/keyboards.py @@ -10,7 +10,27 @@ def template_type_keyboard() -> InlineKeyboardMarkup: def get_templates_keyboard(templates: List[Any], page: int = 0) -> InlineKeyboardMarkup: """Возвращает клавиатуру со списком шаблонов.""" - return KbBuilder.get_templates_keyboard(templates, page) + keyboard = [] + + for template in templates: + keyboard.append([ + InlineKeyboardButton( + template.name, + callback_data=f"template:{template.id}" + ) + ]) + + return InlineKeyboardMarkup(keyboard) + +def get_preview_keyboard() -> InlineKeyboardMarkup: + """Возвращает клавиатуру для предпросмотра.""" + keyboard = [ + [ + InlineKeyboardButton("✅ Отправить", callback_data="preview:send"), + InlineKeyboardButton("❌ Отмена", callback_data="preview:cancel") + ] + ] + return InlineKeyboardMarkup(keyboard) class KbBuilder: diff --git a/app/bots/editor/router.py b/app/bots/editor/router.py index 61ad11d..c32275c 100644 --- a/app/bots/editor/router.py +++ b/app/bots/editor/router.py @@ -12,7 +12,10 @@ from .handlers.templates import ( handle_template_name, handle_template_text, handle_template_keyboard, - list_templates + list_templates, + handle_template_selection, + handle_template_vars, + handle_preview_confirmation ) from .handlers.posts import ( newpost, @@ -41,7 +44,10 @@ def register_handlers(app: Application) -> None: # Шаблоны template_handler = ConversationHandler( - entry_points=[CommandHandler("newtemplate", start_template_creation)], + entry_points=[ + CommandHandler("newtemplate", start_template_creation), + CommandHandler("tpl_list", list_templates) + ], states={ BotStates.TPL_TYPE: [ CallbackQueryHandler(handle_template_type) @@ -54,6 +60,15 @@ def register_handlers(app: Application) -> None: ], BotStates.TPL_NEW_KB: [ MessageHandler(filters.TEXT & ~filters.COMMAND, handle_template_keyboard) + ], + BotStates.SELECT_TEMPLATE: [ + CallbackQueryHandler(handle_template_selection, pattern=r"^template:") + ], + BotStates.TEMPLATE_VARS: [ + MessageHandler(filters.TEXT & ~filters.COMMAND, handle_template_vars) + ], + BotStates.PREVIEW_CONFIRM: [ + CallbackQueryHandler(handle_preview_confirmation, pattern=r"^preview:") ] }, fallbacks=[CommandHandler("cancel", cancel)] diff --git a/app/bots/editor/states.py b/app/bots/editor/states.py index 9a2757b..e8fdcb5 100644 --- a/app/bots/editor/states.py +++ b/app/bots/editor/states.py @@ -23,7 +23,7 @@ class BotStates(IntEnum): TEMPLATE_PREVIEW = 19 TEMPLATE_VARS = 20 - # Состояния создания поста + # Состояния работы с шаблонами и создания поста CREATE_POST = 30 CHOOSE_CHANNEL = 31 # Выбор канала CHOOSE_TYPE = 32 # Выбор типа поста (текст/фото/видео/gif) @@ -33,9 +33,9 @@ class BotStates(IntEnum): EDIT_KEYBOARD = 36 # Редактирование клавиатуры CONFIRM_SEND = 37 # Подтверждение отправки ENTER_SCHEDULE = 38 # Ввод времени для отложенной публикации - SELECT_TEMPLATE = 39 # Выбор шаблона + SELECT_TEMPLATE = 25 # Выбор шаблона PREVIEW_VARS = 40 # Ввод значений для переменных - PREVIEW_CONFIRM = 41 # Подтверждение предпросмотра + PREVIEW_CONFIRM = 26 # Подтверждение предпросмотра # Состояния работы с каналами CHANNEL_NAME = 50 diff --git a/app/bots/editor_bot.py b/app/bots/editor_bot.py index 46a20c5..10d4cdb 100644 --- a/app/bots/editor_bot.py +++ b/app/bots/editor_bot.py @@ -29,7 +29,7 @@ DEFAULT_TTL = 3600 # 1 час # Настройка логирования logger = logging.getLogger(__name__) -async def main(): +async def run_bot(): """Запуск бота.""" app = Application.builder().token(settings.editor_bot_token).build() @@ -39,23 +39,17 @@ async def main(): # Запуск бота await app.run_polling(allowed_updates=Update.ALL_TYPES) -if __name__ == "__main__": - import asyncio +def main(): + """Основная функция запуска.""" try: - asyncio.run(main()) + asyncio.run(run_bot()) 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) + +if __name__ == "__main__": + main() from app.core.config import settings from .editor.session import SessionStore @@ -67,12 +61,7 @@ from .editor.handlers.templates import ( handle_template_keyboard, list_templates ) -from .editor.handlers.posts import ( - newpost, - handle_post_template, - handle_post_channel, - handle_post_schedule -) +from .editor.handlers.posts import newpost from .editor.states import BotStates # Настройка логирования @@ -624,7 +613,7 @@ async def tpl_new_content(update: Update, context: Context) -> int: "title": session.template_name, "content": content, "type": session.type, - "owner_id": user.id, + "tg_user_id": user.id, # Передаем telegram user id вместо owner_id "parse_mode": session.parse_mode, "keyboard_tpl": session.keyboard } @@ -633,8 +622,9 @@ async def tpl_new_content(update: Update, context: Context) -> int: await create_template(template_data) await message.reply_text("Шаблон успешно сохранен") return ConversationHandler.END - except ValueError as e: - await message.reply_text(f"Ошибка создания шаблона: {e}") + except Exception as e: + logger.error(f"Ошибка создания шаблона: {e}", exc_info=True) + await message.reply_text(f"Ошибка создания шаблона. Пожалуйста, попробуйте позже.") return BotStates.TPL_NEW_CONTENT async def list_user_templates(update: Update, context: Context) -> None: diff --git a/app/services/template.py b/app/services/template.py index 7f7e031..f06175c 100644 --- a/app/services/template.py +++ b/app/services/template.py @@ -35,9 +35,9 @@ async def list_templates(owner_id: Optional[int] = None, limit: Optional[int] = List[Template]: Список шаблонов """ async with async_session_maker() as session: - query = Template.__table__.select() + query = select(Template) if owner_id is not None: - query = query.where(Template.__table__.c.owner_id == owner_id) + query = query.where(Template.owner_id == owner_id) if offset is not None: query = query.offset(offset) if limit is not None: @@ -45,6 +45,8 @@ async def list_templates(owner_id: Optional[int] = None, limit: Optional[int] = result = await session.execute(query) return list(result.scalars()) +from app.services.users import get_or_create_user + async def create_template(template_data: Dict[str, Any]) -> Template: """Создать новый шаблон. @@ -54,6 +56,12 @@ async def create_template(template_data: Dict[str, Any]) -> Template: Returns: Template: Созданный шаблон """ + # Проверяем owner_id и создаем пользователя если нужно + tg_user_id = template_data.pop("tg_user_id", None) + if tg_user_id: + user = await get_or_create_user(tg_user_id) + template_data["owner_id"] = user.id + async with async_session_maker() as session: template = Template(**template_data) session.add(template) diff --git a/app/services/users.py b/app/services/users.py new file mode 100644 index 0000000..929c58b --- /dev/null +++ b/app/services/users.py @@ -0,0 +1,30 @@ +"""Сервис для работы с пользователями.""" +from typing import Optional +from sqlalchemy import select +from app.db.session import async_session_maker +from app.models.user import User + +async def get_or_create_user(tg_user_id: int, username: Optional[str] = None) -> User: + """Получить или создать пользователя. + + Args: + tg_user_id: ID пользователя в Telegram + username: Имя пользователя в Telegram + + Returns: + User: Объект пользователя + """ + async with async_session_maker() as session: + # Пробуем найти пользователя + query = select(User).where(User.tg_user_id == tg_user_id) + result = await session.execute(query) + user = result.scalar_one_or_none() + + if not user: + # Создаем нового пользователя + user = User(tg_user_id=tg_user_id, username=username) + session.add(user) + await session.commit() + await session.refresh(user) + + return user