Files
postbot/app/bots/editor/handlers/posts.py

745 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Обработчики для работы с постами."""
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