Revert "Fix poll delivery session usage and tidy poll creation prompts"

This commit is contained in:
Egor
2025-10-23 06:31:20 +03:00
committed by GitHub
parent fe63ef5fc5
commit 104d98542e
2 changed files with 69 additions and 327 deletions

View File

@@ -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", "🗳️ <b>Создание опроса</b>")
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",
"🗳️ <b>Создание опроса</b>\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()

View File

@@ -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"🗳️ <b>{poll.title}</b>"]
@@ -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,
)