From 104d98542e4fe28ec4f91584f5069fcfec6ebbd0 Mon Sep 17 00:00:00 2001 From: Egor Date: Thu, 23 Oct 2025 06:31:20 +0300 Subject: [PATCH] Revert "Fix poll delivery session usage and tidy poll creation prompts" --- app/handlers/admin/polls.py | 368 ++++++----------------------------- app/services/poll_service.py | 28 +-- 2 files changed, 69 insertions(+), 327 deletions(-) diff --git a/app/handlers/admin/polls.py b/app/handlers/admin/polls.py index 89c9a867..1220cae4 100644 --- a/app/handlers/admin/polls.py +++ b/app/handlers/admin/polls.py @@ -3,8 +3,7 @@ import logging from decimal import Decimal, InvalidOperation, ROUND_HALF_UP from typing import Optional -from aiogram import Bot, Dispatcher, F, types -from aiogram.exceptions import MessageNotModified, TelegramBadRequest +from aiogram import Dispatcher, F, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from sqlalchemy.ext.asyncio import AsyncSession @@ -41,65 +40,6 @@ class PollCreationStates(StatesGroup): waiting_for_questions = State() -def _get_creation_header(texts) -> str: - return texts.t("ADMIN_POLLS_CREATION_HEADER", "🗳️ Создание опроса") - - -def _format_creation_prompt(texts, body: str, error: str | None = None) -> str: - header = _get_creation_header(texts) - body_content = body.strip() - if body_content.startswith(header): - body_content = body_content[len(header) :].lstrip("\n") - - sections = [header] - if error: - sections.append(error) - if body_content: - sections.append(body_content) - - return "\n\n".join(sections) - - -async def _delete_user_message(message: types.Message) -> None: - try: - await message.delete() - except TelegramBadRequest as error: - logger.debug("Failed to delete poll creation input: %s", error) - - -async def _update_creation_message( - bot: Bot, - chat_id: int, - message_id: int | None, - text: str, - *, - reply_markup: types.InlineKeyboardMarkup | None = None, - parse_mode: str | None = None, -) -> int: - if message_id: - try: - await bot.edit_message_text( - text=text, - chat_id=chat_id, - message_id=message_id, - reply_markup=reply_markup, - parse_mode=parse_mode, - ) - return message_id - except MessageNotModified: - return message_id - except TelegramBadRequest as error: - logger.debug("Failed to edit poll creation prompt: %s", error) - - new_message = await bot.send_message( - chat_id=chat_id, - text=text, - reply_markup=reply_markup, - parse_mode=parse_mode, - ) - return new_message.message_id - - def _build_polls_keyboard(polls: list[Poll], language: str) -> types.InlineKeyboardMarkup: texts = get_texts(language) keyboard: list[list[types.InlineKeyboardButton]] = [] @@ -337,6 +277,7 @@ async def start_poll_creation( texts = get_texts(db_user.language) await state.clear() await state.set_state(PollCreationStates.waiting_for_title) + await state.update_data(questions=[]) await callback.message.edit_text( texts.t( @@ -345,11 +286,6 @@ async def start_poll_creation( ), parse_mode="HTML", ) - await state.update_data( - questions=[], - prompt_message_id=callback.message.message_id, - prompt_chat_id=callback.message.chat.id, - ) await callback.answer() @@ -361,66 +297,31 @@ async def process_poll_title( state: FSMContext, db: AsyncSession, ): - texts = get_texts(db_user.language) - data = await state.get_data() - prompt_message_id = data.get("prompt_message_id") - prompt_chat_id = data.get("prompt_chat_id", message.chat.id) - - user_input = (message.text or "").strip() - - if user_input == "/cancel": + if message.text == "/cancel": await state.clear() - await _delete_user_message(message) - cancel_text = texts.t("ADMIN_POLLS_CREATION_CANCELLED", "❌ Создание опроса отменено.") - await _update_creation_message( - message.bot, - prompt_chat_id, - prompt_message_id, - cancel_text, + await message.answer( + get_texts(db_user.language).t("ADMIN_POLLS_CREATION_CANCELLED", "❌ Создание опроса отменено."), reply_markup=get_admin_communications_submenu_keyboard(db_user.language), - parse_mode="HTML", ) return - await _delete_user_message(message) - - if not user_input: - error_text = texts.t( - "ADMIN_POLLS_CREATION_TITLE_EMPTY", - "❌ Заголовок не может быть пустым. Попробуйте снова.", - ) - prompt_body = texts.t( - "ADMIN_POLLS_CREATION_TITLE_PROMPT", - "🗳️ Создание опроса\n\nВведите заголовок опроса:", - ) - new_message_id = await _update_creation_message( - message.bot, - prompt_chat_id, - prompt_message_id, - _format_creation_prompt(texts, prompt_body, error_text), - parse_mode="HTML", - ) - await state.update_data(prompt_message_id=new_message_id, prompt_chat_id=prompt_chat_id) + title = message.text.strip() + if not title: + await message.answer("❌ Заголовок не может быть пустым. Попробуйте снова.") return - await state.update_data(title=user_input) + await state.update_data(title=title) await state.set_state(PollCreationStates.waiting_for_description) - prompt_body = ( + texts = get_texts(db_user.language) + await message.answer( texts.t( "ADMIN_POLLS_CREATION_DESCRIPTION_PROMPT", "Введите описание опроса. HTML разрешён.\nОтправьте /skip, чтобы пропустить.", ) - + f"\n\n{get_html_help_text()}" - ) - new_message_id = await _update_creation_message( - message.bot, - prompt_chat_id, - prompt_message_id, - _format_creation_prompt(texts, prompt_body), + + f"\n\n{get_html_help_text()}", parse_mode="HTML", ) - await state.update_data(prompt_message_id=new_message_id, prompt_chat_id=prompt_chat_id) @admin_required @@ -432,74 +333,36 @@ async def process_poll_description( db: AsyncSession, ): texts = get_texts(db_user.language) - data = await state.get_data() - prompt_message_id = data.get("prompt_message_id") - prompt_chat_id = data.get("prompt_chat_id", message.chat.id) - user_input = message.text or "" - - if user_input == "/cancel": + if message.text == "/cancel": await state.clear() - await _delete_user_message(message) - cancel_text = texts.t("ADMIN_POLLS_CREATION_CANCELLED", "❌ Создание опроса отменено.") - await _update_creation_message( - message.bot, - prompt_chat_id, - prompt_message_id, - cancel_text, + await message.answer( + texts.t("ADMIN_POLLS_CREATION_CANCELLED", "❌ Создание опроса отменено."), reply_markup=get_admin_communications_submenu_keyboard(db_user.language), - parse_mode="HTML", ) return - await _delete_user_message(message) - - if user_input == "/skip": - description: Optional[str] = None + description: Optional[str] + if message.text == "/skip": + description = None else: - description = user_input.strip() + description = message.text.strip() is_valid, error_message = validate_html_tags(description) if not is_valid: - error_text = texts.t( - "ADMIN_POLLS_CREATION_INVALID_HTML", - "❌ Ошибка в HTML: {error}", - ).format(error=error_message) - prompt_body = ( - texts.t( - "ADMIN_POLLS_CREATION_DESCRIPTION_PROMPT", - "Введите описание опроса. HTML разрешён.\nОтправьте /skip, чтобы пропустить.", - ) - + f"\n\n{get_html_help_text()}" + await message.answer( + texts.t("ADMIN_POLLS_CREATION_INVALID_HTML", "❌ Ошибка в HTML: {error}").format(error=error_message) ) - new_message_id = await _update_creation_message( - message.bot, - prompt_chat_id, - prompt_message_id, - _format_creation_prompt(texts, prompt_body, error_text), - parse_mode="HTML", - ) - await state.update_data(prompt_message_id=new_message_id, prompt_chat_id=prompt_chat_id) return await state.update_data(description=description) await state.set_state(PollCreationStates.waiting_for_reward) - prompt_body = texts.t( - "ADMIN_POLLS_CREATION_REWARD_PROMPT", - ( - "Укажите награду за прохождение опроса (в рублях).\n" - "0 — без награды. Можно использовать дробные значения.\n" - "Например: 0, 0.5, 10" - ), + await message.answer( + texts.t( + "ADMIN_POLLS_CREATION_REWARD_PROMPT", + "Введите сумму награды в рублях. Отправьте 0 чтобы отключить награду.", + ) ) - new_message_id = await _update_creation_message( - message.bot, - prompt_chat_id, - prompt_message_id, - _format_creation_prompt(texts, prompt_body), - parse_mode="HTML", - ) - await state.update_data(prompt_message_id=new_message_id, prompt_chat_id=prompt_chat_id) def _parse_reward_amount(message_text: str) -> int | None: @@ -525,50 +388,18 @@ async def process_poll_reward( db: AsyncSession, ): texts = get_texts(db_user.language) - data = await state.get_data() - prompt_message_id = data.get("prompt_message_id") - prompt_chat_id = data.get("prompt_chat_id", message.chat.id) - user_input = message.text or "" - - if user_input == "/cancel": + if message.text == "/cancel": await state.clear() - await _delete_user_message(message) - cancel_text = texts.t("ADMIN_POLLS_CREATION_CANCELLED", "❌ Создание опроса отменено.") - await _update_creation_message( - message.bot, - prompt_chat_id, - prompt_message_id, - cancel_text, + await message.answer( + texts.t("ADMIN_POLLS_CREATION_CANCELLED", "❌ Создание опроса отменено."), reply_markup=get_admin_communications_submenu_keyboard(db_user.language), - parse_mode="HTML", ) return - await _delete_user_message(message) - - reward_kopeks = _parse_reward_amount(user_input) + reward_kopeks = _parse_reward_amount(message.text) if reward_kopeks is None: - error_text = texts.t( - "ADMIN_POLLS_CREATION_REWARD_INVALID", - "❌ Некорректная сумма. Попробуйте ещё раз.", - ) - prompt_body = texts.t( - "ADMIN_POLLS_CREATION_REWARD_PROMPT", - ( - "Укажите награду за прохождение опроса (в рублях).\n" - "0 — без награды. Можно использовать дробные значения.\n" - "Например: 0, 0.5, 10" - ), - ) - new_message_id = await _update_creation_message( - message.bot, - prompt_chat_id, - prompt_message_id, - _format_creation_prompt(texts, prompt_body, error_text), - parse_mode="HTML", - ) - await state.update_data(prompt_message_id=new_message_id, prompt_chat_id=prompt_chat_id) + await message.answer(texts.t("ADMIN_POLLS_CREATION_REWARD_INVALID", "❌ Некорректная сумма. Попробуйте ещё раз.")) return reward_enabled = reward_kopeks > 0 @@ -587,14 +418,7 @@ async def process_poll_reward( "Отправьте /done, когда вопросы будут добавлены." ), ) - new_message_id = await _update_creation_message( - message.bot, - prompt_chat_id, - prompt_message_id, - _format_creation_prompt(texts, prompt), - parse_mode="HTML", - ) - await state.update_data(prompt_message_id=new_message_id, prompt_chat_id=prompt_chat_id) + await message.answer(prompt) @admin_required @@ -606,52 +430,21 @@ async def process_poll_question( db: AsyncSession, ): texts = get_texts(db_user.language) - data = await state.get_data() - prompt_message_id = data.get("prompt_message_id") - prompt_chat_id = data.get("prompt_chat_id", message.chat.id) - - user_input = message.text or "" - - if user_input == "/cancel": + if message.text == "/cancel": await state.clear() - await _delete_user_message(message) - cancel_text = texts.t("ADMIN_POLLS_CREATION_CANCELLED", "❌ Создание опроса отменено.") - await _update_creation_message( - message.bot, - prompt_chat_id, - prompt_message_id, - cancel_text, + await message.answer( + texts.t("ADMIN_POLLS_CREATION_CANCELLED", "❌ Создание опроса отменено."), reply_markup=get_admin_communications_submenu_keyboard(db_user.language), - parse_mode="HTML", ) return - await _delete_user_message(message) - - if user_input == "/done": + if message.text == "/done": + data = await state.get_data() questions = data.get("questions", []) if not questions: - error_text = texts.t( - "ADMIN_POLLS_CREATION_NEEDS_QUESTION", - "❌ Добавьте хотя бы один вопрос.", + await message.answer( + texts.t("ADMIN_POLLS_CREATION_NEEDS_QUESTION", "❌ Добавьте хотя бы один вопрос."), ) - prompt = texts.t( - "ADMIN_POLLS_CREATION_QUESTION_PROMPT", - ( - "Введите вопрос и варианты ответов.\n" - "Каждая строка — отдельный вариант.\n" - "Первая строка — текст вопроса.\n" - "Отправьте /done, когда вопросы будут добавлены." - ), - ) - new_message_id = await _update_creation_message( - message.bot, - prompt_chat_id, - prompt_message_id, - _format_creation_prompt(texts, prompt, error_text), - parse_mode="HTML", - ) - await state.update_data(prompt_message_id=new_message_id, prompt_chat_id=prompt_chat_id) return title = data.get("title") @@ -669,80 +462,47 @@ async def process_poll_question( questions=questions, ) + await state.clear() + reward_text = _format_reward_text(poll, db_user.language) - final_text = texts.t( - "ADMIN_POLLS_CREATION_FINISHED", - "✅ Опрос «{title}» создан. Вопросов: {count}. {reward}", - ).format( - title=html.escape(poll.title), - count=len(poll.questions), - reward=reward_text, - ) - polls_keyboard = _build_polls_keyboard(await list_polls(db), db_user.language) - await _update_creation_message( - message.bot, - prompt_chat_id, - prompt_message_id, - final_text, - reply_markup=polls_keyboard, + await message.answer( + texts.t( + "ADMIN_POLLS_CREATION_FINISHED", + "✅ Опрос «{title}» создан. Вопросов: {count}. {reward}", + ).format( + title=poll.title, + count=len(poll.questions), + reward=reward_text, + ), + reply_markup=_build_polls_keyboard(await list_polls(db), db_user.language), parse_mode="HTML", ) - await state.clear() return - lines = [line.strip() for line in user_input.splitlines() if line.strip()] + lines = [line.strip() for line in message.text.splitlines() if line.strip()] if len(lines) < 3: - error_text = texts.t( - "ADMIN_POLLS_CREATION_MIN_OPTIONS", - "❌ Нужен вопрос и минимум два варианта ответа.", + await message.answer( + texts.t( + "ADMIN_POLLS_CREATION_MIN_OPTIONS", + "❌ Нужен вопрос и минимум два варианта ответа.", + ) ) - prompt = texts.t( - "ADMIN_POLLS_CREATION_QUESTION_PROMPT", - ( - "Введите вопрос и варианты ответов.\n" - "Каждая строка — отдельный вариант.\n" - "Первая строка — текст вопроса.\n" - "Отправьте /done, когда вопросы будут добавлены." - ), - ) - new_message_id = await _update_creation_message( - message.bot, - prompt_chat_id, - prompt_message_id, - _format_creation_prompt(texts, prompt, error_text), - parse_mode="HTML", - ) - await state.update_data(prompt_message_id=new_message_id, prompt_chat_id=prompt_chat_id) return question_text = lines[0] options = lines[1:] + data = await state.get_data() questions = data.get("questions", []) questions.append({"text": question_text, "options": options}) await state.update_data(questions=questions) - prompt = texts.t( - "ADMIN_POLLS_CREATION_QUESTION_PROMPT", - ( - "Введите вопрос и варианты ответов.\n" - "Каждая строка — отдельный вариант.\n" - "Первая строка — текст вопроса.\n" - "Отправьте /done, когда вопросы будут добавлены." - ), - ) - confirmation = texts.t( - "ADMIN_POLLS_CREATION_ADDED_QUESTION", - "Вопрос добавлен: «{question}». Добавьте следующий вопрос или отправьте /done.", - ).format(question=html.escape(question_text)) - body = f"{confirmation}\n\n{prompt}" - new_message_id = await _update_creation_message( - message.bot, - prompt_chat_id, - prompt_message_id, - _format_creation_prompt(texts, body), + await message.answer( + texts.t( + "ADMIN_POLLS_CREATION_ADDED_QUESTION", + "Вопрос добавлен: «{question}». Добавьте следующий вопрос или отправьте /done.", + ).format(question=question_text), parse_mode="HTML", ) - await state.update_data(prompt_message_id=new_message_id, prompt_chat_id=prompt_chat_id) async def _render_poll_details(poll: Poll, language: str) -> str: @@ -929,7 +689,7 @@ async def confirm_poll_send( await callback.message.edit_text( result_text, - reply_markup=_build_poll_details_keyboard(poll_id, db_user.language), + reply_markup=_build_poll_details_keyboard(poll.id, db_user.language), parse_mode="HTML", ) await callback.answer() diff --git a/app/services/poll_service.py b/app/services/poll_service.py index b6862e43..5e8b6187 100644 --- a/app/services/poll_service.py +++ b/app/services/poll_service.py @@ -1,6 +1,5 @@ import asyncio import logging -from dataclasses import dataclass from typing import Iterable from aiogram import Bot @@ -23,16 +22,7 @@ from app.localization.texts import get_texts logger = logging.getLogger(__name__) -@dataclass(frozen=True) -class PollSnapshot: - id: int - title: str - description: str | None - reward_enabled: bool - reward_amount_kopeks: int - - -def _build_poll_invitation_text(poll: PollSnapshot, user: User) -> str: +def _build_poll_invitation_text(poll: Poll, user: User) -> str: texts = get_texts(user.language) lines: list[str] = [f"🗳️ {poll.title}"] @@ -76,14 +66,6 @@ async def send_poll_to_users( poll: Poll, users: Iterable[User], ) -> dict: - poll_info = PollSnapshot( - id=poll.id, - title=poll.title, - description=poll.description, - reward_enabled=poll.reward_enabled, - reward_amount_kopeks=poll.reward_amount_kopeks, - ) - sent = 0 failed = 0 skipped = 0 @@ -92,7 +74,7 @@ async def send_poll_to_users( existing_response = await db.execute( select(PollResponse.id).where( and_( - PollResponse.poll_id == poll_info.id, + PollResponse.poll_id == poll.id, PollResponse.user_id == user.id, ) ) @@ -102,7 +84,7 @@ async def send_poll_to_users( continue response = PollResponse( - poll_id=poll_info.id, + poll_id=poll.id, user_id=user.id, ) db.add(response) @@ -110,7 +92,7 @@ async def send_poll_to_users( try: await db.flush() - text = _build_poll_invitation_text(poll_info, user) + text = _build_poll_invitation_text(poll, user) keyboard = build_start_keyboard(response.id, user.language) await bot.send_message( @@ -130,7 +112,7 @@ async def send_poll_to_users( failed += 1 logger.error( "❌ Ошибка отправки опроса %s пользователю %s: %s", - poll_info.id, + poll.id, user.telegram_id, error, )