bot works as echo (adding templates s functional)

tpl_list dont
This commit is contained in:
2025-08-19 05:13:16 +09:00
parent a8d860ed87
commit 18a92ca526
7 changed files with 274 additions and 47 deletions

View File

@@ -1,13 +1,27 @@
"""Обработчики для работы с шаблонами.""" """Обработчики для работы с шаблонами."""
from typing import Optional, Dict, Any import logging
from telegram import Update, Message 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.ext import ContextTypes, ConversationHandler
from telegram.error import BadRequest
from app.bots.editor.states import BotStates from app.bots.editor.states import BotStates
from app.bots.editor.session import get_session_store 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.parsers import parse_key_value_lines
from ..utils.validation import validate_template_name 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: 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("Произошла непредвиденная ошибка при создании шаблона") await message.reply_text("Произошла непредвиденная ошибка при создании шаблона")
return BotStates.TPL_NEW_KB 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: 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 return BotStates.CONVERSATION_END
try:
message = update.message message = update.message
user_id = message.from_user.id 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)
templates = await get_user_templates(user_id)
if not templates: if not templates:
await message.reply_text("У вас пока нет шаблонов") await message.reply_text("Нет доступных шаблонов")
return BotStates.CONVERSATION_END return BotStates.CONVERSATION_END
page = context.user_data.get("tpl_page", 0) # Сохраняем шаблоны в контексте
keyboard = get_templates_keyboard(templates, page) context.user_data['templates'] = {str(t.id): t for t in templates}
# Создаем клавиатуру с шаблонами
keyboard = get_templates_keyboard(templates)
await message.reply_text( await message.reply_text(
"Выберите шаблон:", "Выберите шаблон для использования:",
reply_markup=keyboard reply_markup=keyboard
) )
return BotStates.TPL_SELECT 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
await query.answer()
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

View File

@@ -10,7 +10,27 @@ def template_type_keyboard() -> InlineKeyboardMarkup:
def get_templates_keyboard(templates: List[Any], page: int = 0) -> 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: class KbBuilder:

View File

@@ -12,7 +12,10 @@ from .handlers.templates import (
handle_template_name, handle_template_name,
handle_template_text, handle_template_text,
handle_template_keyboard, handle_template_keyboard,
list_templates list_templates,
handle_template_selection,
handle_template_vars,
handle_preview_confirmation
) )
from .handlers.posts import ( from .handlers.posts import (
newpost, newpost,
@@ -41,7 +44,10 @@ def register_handlers(app: Application) -> None:
# Шаблоны # Шаблоны
template_handler = ConversationHandler( template_handler = ConversationHandler(
entry_points=[CommandHandler("newtemplate", start_template_creation)], entry_points=[
CommandHandler("newtemplate", start_template_creation),
CommandHandler("tpl_list", list_templates)
],
states={ states={
BotStates.TPL_TYPE: [ BotStates.TPL_TYPE: [
CallbackQueryHandler(handle_template_type) CallbackQueryHandler(handle_template_type)
@@ -54,6 +60,15 @@ def register_handlers(app: Application) -> None:
], ],
BotStates.TPL_NEW_KB: [ BotStates.TPL_NEW_KB: [
MessageHandler(filters.TEXT & ~filters.COMMAND, handle_template_keyboard) 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)] fallbacks=[CommandHandler("cancel", cancel)]

View File

@@ -23,7 +23,7 @@ class BotStates(IntEnum):
TEMPLATE_PREVIEW = 19 TEMPLATE_PREVIEW = 19
TEMPLATE_VARS = 20 TEMPLATE_VARS = 20
# Состояния создания поста # Состояния работы с шаблонами и создания поста
CREATE_POST = 30 CREATE_POST = 30
CHOOSE_CHANNEL = 31 # Выбор канала CHOOSE_CHANNEL = 31 # Выбор канала
CHOOSE_TYPE = 32 # Выбор типа поста (текст/фото/видео/gif) CHOOSE_TYPE = 32 # Выбор типа поста (текст/фото/видео/gif)
@@ -33,9 +33,9 @@ class BotStates(IntEnum):
EDIT_KEYBOARD = 36 # Редактирование клавиатуры EDIT_KEYBOARD = 36 # Редактирование клавиатуры
CONFIRM_SEND = 37 # Подтверждение отправки CONFIRM_SEND = 37 # Подтверждение отправки
ENTER_SCHEDULE = 38 # Ввод времени для отложенной публикации ENTER_SCHEDULE = 38 # Ввод времени для отложенной публикации
SELECT_TEMPLATE = 39 # Выбор шаблона SELECT_TEMPLATE = 25 # Выбор шаблона
PREVIEW_VARS = 40 # Ввод значений для переменных PREVIEW_VARS = 40 # Ввод значений для переменных
PREVIEW_CONFIRM = 41 # Подтверждение предпросмотра PREVIEW_CONFIRM = 26 # Подтверждение предпросмотра
# Состояния работы с каналами # Состояния работы с каналами
CHANNEL_NAME = 50 CHANNEL_NAME = 50

View File

@@ -29,7 +29,7 @@ DEFAULT_TTL = 3600 # 1 час
# Настройка логирования # Настройка логирования
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
async def main(): async def run_bot():
"""Запуск бота.""" """Запуск бота."""
app = Application.builder().token(settings.editor_bot_token).build() 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) await app.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__": def main():
import asyncio """Основная функция запуска."""
try: try:
asyncio.run(main()) asyncio.run(run_bot())
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info("Бот остановлен") logger.info("Бот остановлен")
except Exception as e: except Exception as e:
logger.error(f"Ошибка: {e}", exc_info=True) logger.error(f"Ошибка: {e}", exc_info=True)
finally:
# Убедимся, что все циклы закрыты if __name__ == "__main__":
loop = asyncio.get_event_loop() main()
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 app.core.config import settings
from .editor.session import SessionStore from .editor.session import SessionStore
@@ -67,12 +61,7 @@ from .editor.handlers.templates import (
handle_template_keyboard, handle_template_keyboard,
list_templates list_templates
) )
from .editor.handlers.posts import ( from .editor.handlers.posts import newpost
newpost,
handle_post_template,
handle_post_channel,
handle_post_schedule
)
from .editor.states import BotStates from .editor.states import BotStates
# Настройка логирования # Настройка логирования
@@ -624,7 +613,7 @@ async def tpl_new_content(update: Update, context: Context) -> int:
"title": session.template_name, "title": session.template_name,
"content": content, "content": content,
"type": session.type, "type": session.type,
"owner_id": user.id, "tg_user_id": user.id, # Передаем telegram user id вместо owner_id
"parse_mode": session.parse_mode, "parse_mode": session.parse_mode,
"keyboard_tpl": session.keyboard "keyboard_tpl": session.keyboard
} }
@@ -633,8 +622,9 @@ async def tpl_new_content(update: Update, context: Context) -> int:
await create_template(template_data) await create_template(template_data)
await message.reply_text("Шаблон успешно сохранен") await message.reply_text("Шаблон успешно сохранен")
return ConversationHandler.END return ConversationHandler.END
except ValueError as e: except Exception as e:
await message.reply_text(f"Ошибка создания шаблона: {e}") logger.error(f"Ошибка создания шаблона: {e}", exc_info=True)
await message.reply_text(f"Ошибка создания шаблона. Пожалуйста, попробуйте позже.")
return BotStates.TPL_NEW_CONTENT return BotStates.TPL_NEW_CONTENT
async def list_user_templates(update: Update, context: Context) -> None: async def list_user_templates(update: Update, context: Context) -> None:

View File

@@ -35,9 +35,9 @@ async def list_templates(owner_id: Optional[int] = None, limit: Optional[int] =
List[Template]: Список шаблонов List[Template]: Список шаблонов
""" """
async with async_session_maker() as session: async with async_session_maker() as session:
query = Template.__table__.select() query = select(Template)
if owner_id is not None: 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: if offset is not None:
query = query.offset(offset) query = query.offset(offset)
if limit is not None: 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) result = await session.execute(query)
return list(result.scalars()) return list(result.scalars())
from app.services.users import get_or_create_user
async def create_template(template_data: Dict[str, Any]) -> Template: 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: Returns:
Template: Созданный шаблон 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: async with async_session_maker() as session:
template = Template(**template_data) template = Template(**template_data)
session.add(template) session.add(template)

30
app/services/users.py Normal file
View File

@@ -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