diff --git a/app/handlers/admin/polls.py b/app/handlers/admin/polls.py
index 97352ec1..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 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
@@ -34,227 +33,6 @@ from app.utils.validators import get_html_help_text, validate_html_tags
logger = logging.getLogger(__name__)
-def _safe_format_price(amount_kopeks: int) -> str:
- try:
- return settings.format_price(amount_kopeks)
- except Exception as error: # pragma: no cover - defensive logging
- logger.error("Не удалось отформатировать сумму %s: %s", amount_kopeks, error)
- return f"{amount_kopeks / 100:.2f} ₽"
-
-
-async def _safe_delete_message(message: types.Message) -> None:
- try:
- await message.delete()
- except TelegramBadRequest as error:
- if "message to delete not found" in str(error).lower():
- logger.debug("Сообщение уже удалено: %s", error)
- else:
- logger.warning("Не удалось удалить сообщение %s: %s", message.message_id, error)
-
-
-async def _edit_creation_message(
- bot: Bot,
- state_data: dict,
- text: str,
- *,
- reply_markup: types.InlineKeyboardMarkup | None = None,
- parse_mode: str | None = "HTML",
-) -> bool:
- chat_id = state_data.get("form_chat_id")
- message_id = state_data.get("form_message_id")
-
- if not chat_id or not message_id:
- return False
-
- try:
- await bot.edit_message_text(
- chat_id=chat_id,
- message_id=message_id,
- text=text,
- reply_markup=reply_markup,
- parse_mode=parse_mode,
- )
- return True
- except TelegramBadRequest as error:
- error_text = str(error).lower()
- if "message is not modified" in error_text:
- return True
- log_method = (
- logger.debug
- if "there is no text in the message to edit" in error_text
- else logger.warning
- )
- log_method(
- "Не удалось обновить сообщение создания опроса %s: %s",
- message_id,
- error,
- )
- except Exception as error: # pragma: no cover - defensive logging
- logger.error(
- "Непредвиденная ошибка при обновлении сообщения создания опроса %s: %s",
- message_id,
- error,
- )
- return False
-
-
-async def _send_creation_message(
- message: types.Message,
- state: FSMContext,
- text: str,
- *,
- reply_markup: types.InlineKeyboardMarkup | None = None,
- parse_mode: str | None = "HTML",
-) -> types.Message:
- state_data = await state.get_data()
- chat_id = state_data.get("form_chat_id")
- message_id = state_data.get("form_message_id")
-
- if chat_id and message_id:
- try:
- await message.bot.delete_message(chat_id, message_id)
- except TelegramBadRequest as error:
- error_text = str(error).lower()
- if "message to delete not found" in error_text:
- logger.debug("Сообщение уже удалено: %s", error)
- else:
- logger.warning(
- "Не удалось удалить сообщение создания опроса %s: %s",
- message_id,
- error,
- )
- except Exception as error: # pragma: no cover - defensive logging
- logger.error(
- "Непредвиденная ошибка при удалении сообщения создания опроса %s: %s",
- message_id,
- error,
- )
-
- sent_message = await message.bot.send_message(
- chat_id=message.chat.id,
- text=text,
- reply_markup=reply_markup,
- parse_mode=parse_mode,
- )
-
- await state.update_data(
- form_chat_id=sent_message.chat.id,
- form_message_id=sent_message.message_id,
- )
-
- return sent_message
-
-
-def _render_creation_progress(
- texts,
- data: dict,
- next_step: str,
- *,
- status_message: str | None = None,
- error_message: str | None = None,
-) -> str:
- lines: list[str] = ["🗳️ Создание опроса"]
-
- title_prompt = texts.t(
- "ADMIN_POLLS_CREATION_TITLE_PROMPT",
- "Введите заголовок опроса:",
- )
- lines.append("")
- lines.append(title_prompt)
-
- title = data.get("title")
- if title:
- lines.append(f"• {html.escape(title)}")
-
- if next_step == "title":
- if error_message:
- lines.append("")
- lines.append(error_message)
- return "\n".join(lines)
-
- description_prompt = texts.t(
- "ADMIN_POLLS_CREATION_DESCRIPTION_PROMPT",
- "Введите описание опроса. HTML разрешён.\nОтправьте /skip, чтобы пропустить.",
- )
-
- lines.append("")
- lines.append(description_prompt)
-
- if "description" in data:
- description = data.get("description")
- if description:
- lines.append(f"• {description}")
- else:
- lines.append(
- "• "
- + texts.t(
- "ADMIN_POLLS_CREATION_DESCRIPTION_SKIPPED",
- "Описание пропущено.",
- )
- )
- else:
- lines.append("")
- lines.append(get_html_help_text())
-
- if next_step == "description":
- if error_message:
- lines.append("")
- lines.append(error_message)
- return "\n".join(lines)
-
- reward_prompt = texts.t(
- "ADMIN_POLLS_CREATION_REWARD_PROMPT",
- "Введите сумму награды в рублях. Отправьте 0 чтобы отключить награду.",
- )
-
- lines.append("")
- lines.append(reward_prompt)
-
- if "reward_enabled" in data:
- if data.get("reward_enabled"):
- amount = data.get("reward_amount_kopeks", 0)
- lines.append(f"• {_safe_format_price(amount)}")
- else:
- lines.append(texts.t("ADMIN_POLLS_REWARD_DISABLED", "Награда отключена"))
-
- if next_step == "reward":
- if error_message:
- lines.append("")
- lines.append(error_message)
- return "\n".join(lines)
-
- question_prompt = texts.t(
- "ADMIN_POLLS_CREATION_QUESTION_PROMPT",
- (
- "Введите вопрос и варианты ответов.\n"
- "Каждая строка — отдельный вариант.\n"
- "Первая строка — текст вопроса.\n"
- "Отправьте /done, когда вопросы будут добавлены."
- ),
- )
-
- lines.append("")
- lines.append(question_prompt)
-
- questions = data.get("questions", [])
- if questions:
- lines.append("")
- for idx, question in enumerate(questions, start=1):
- lines.append(f"{idx}. {html.escape(question['text'])}")
- for option in question["options"]:
- lines.append(f" • {html.escape(option)}")
-
- if status_message:
- lines.append("")
- lines.append(status_message)
-
- if error_message:
- lines.append("")
- lines.append(error_message)
-
- return "\n".join(lines)
-
-
class PollCreationStates(StatesGroup):
waiting_for_title = State()
waiting_for_description = State()
@@ -499,14 +277,13 @@ async def start_poll_creation(
texts = get_texts(db_user.language)
await state.clear()
await state.set_state(PollCreationStates.waiting_for_title)
- await _safe_delete_message(callback.message)
await state.update_data(questions=[])
- form_text = _render_creation_progress(texts, await state.get_data(), "title")
- await _send_creation_message(
- callback.message,
- state,
- form_text,
+ await callback.message.edit_text(
+ texts.t(
+ "ADMIN_POLLS_CREATION_TITLE_PROMPT",
+ "🗳️ Создание опроса\n\nВведите заголовок опроса:",
+ ),
parse_mode="HTML",
)
await callback.answer()
@@ -520,66 +297,31 @@ async def process_poll_title(
state: FSMContext,
db: AsyncSession,
):
- texts = get_texts(db_user.language)
- state_data = await state.get_data()
-
if message.text == "/cancel":
- await _safe_delete_message(message)
- cancel_text = texts.t(
- "ADMIN_POLLS_CREATION_CANCELLED",
- "❌ Создание опроса отменено.",
- )
- keyboard = get_admin_communications_submenu_keyboard(db_user.language)
- updated = await _edit_creation_message(
- message.bot,
- state_data,
- cancel_text,
- reply_markup=keyboard,
- parse_mode="HTML",
- )
- if not updated:
- await _send_creation_message(
- message,
- state,
- cancel_text,
- reply_markup=keyboard,
- parse_mode="HTML",
- )
await state.clear()
+ await message.answer(
+ get_texts(db_user.language).t("ADMIN_POLLS_CREATION_CANCELLED", "❌ Создание опроса отменено."),
+ reply_markup=get_admin_communications_submenu_keyboard(db_user.language),
+ )
return
- title = (message.text or "").strip()
- await _safe_delete_message(message)
-
+ title = message.text.strip()
if not title:
- error_text = texts.t(
- "ADMIN_POLLS_CREATION_TITLE_EMPTY",
- "❌ Заголовок не может быть пустым. Попробуйте снова.",
- )
- form_text = _render_creation_progress(texts, state_data, "title", error_message=error_text)
- updated = await _edit_creation_message(message.bot, state_data, form_text)
- if not updated:
- await _send_creation_message(
- message,
- state,
- form_text,
- parse_mode="HTML",
- )
+ await message.answer("❌ Заголовок не может быть пустым. Попробуйте снова.")
return
await state.update_data(title=title)
await state.set_state(PollCreationStates.waiting_for_description)
- new_data = await state.get_data()
- form_text = _render_creation_progress(texts, new_data, "description")
- updated = await _edit_creation_message(message.bot, new_data, form_text)
- if not updated:
- await _send_creation_message(
- message,
- state,
- form_text,
- parse_mode="HTML",
+ 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()}",
+ parse_mode="HTML",
+ )
@admin_required
@@ -591,71 +333,36 @@ async def process_poll_description(
db: AsyncSession,
):
texts = get_texts(db_user.language)
- state_data = await state.get_data()
if message.text == "/cancel":
- await _safe_delete_message(message)
- cancel_text = texts.t(
- "ADMIN_POLLS_CREATION_CANCELLED",
- "❌ Создание опроса отменено.",
- )
- keyboard = get_admin_communications_submenu_keyboard(db_user.language)
- updated = await _edit_creation_message(
- message.bot,
- state_data,
- cancel_text,
- reply_markup=keyboard,
- parse_mode="HTML",
- )
- if not updated:
- await _send_creation_message(
- message,
- state,
- cancel_text,
- reply_markup=keyboard,
- parse_mode="HTML",
- )
await state.clear()
+ await message.answer(
+ texts.t("ADMIN_POLLS_CREATION_CANCELLED", "❌ Создание опроса отменено."),
+ reply_markup=get_admin_communications_submenu_keyboard(db_user.language),
+ )
return
description: Optional[str]
- message_text = message.text or ""
- await _safe_delete_message(message)
-
- if message_text == "/skip":
+ if message.text == "/skip":
description = None
else:
- description = message_text.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)
- form_text = _render_creation_progress(texts, state_data, "description", error_message=error_text)
- updated = await _edit_creation_message(message.bot, state_data, form_text)
- if not updated:
- await _send_creation_message(
- message,
- state,
- form_text,
- parse_mode="HTML",
- )
+ await message.answer(
+ texts.t("ADMIN_POLLS_CREATION_INVALID_HTML", "❌ Ошибка в HTML: {error}").format(error=error_message)
+ )
return
await state.update_data(description=description)
await state.set_state(PollCreationStates.waiting_for_reward)
- new_data = await state.get_data()
- form_text = _render_creation_progress(texts, new_data, "reward")
- updated = await _edit_creation_message(message.bot, new_data, form_text)
- if not updated:
- await _send_creation_message(
- message,
- state,
- form_text,
- parse_mode="HTML",
+ await message.answer(
+ texts.t(
+ "ADMIN_POLLS_CREATION_REWARD_PROMPT",
+ "Введите сумму награды в рублях. Отправьте 0 чтобы отключить награду.",
)
+ )
def _parse_reward_amount(message_text: str) -> int | None:
@@ -681,49 +388,18 @@ async def process_poll_reward(
db: AsyncSession,
):
texts = get_texts(db_user.language)
- state_data = await state.get_data()
if message.text == "/cancel":
- await _safe_delete_message(message)
- cancel_text = texts.t(
- "ADMIN_POLLS_CREATION_CANCELLED",
- "❌ Создание опроса отменено.",
- )
- keyboard = get_admin_communications_submenu_keyboard(db_user.language)
- updated = await _edit_creation_message(
- message.bot,
- state_data,
- cancel_text,
- reply_markup=keyboard,
- parse_mode="HTML",
- )
- if not updated:
- await _send_creation_message(
- message,
- state,
- cancel_text,
- reply_markup=keyboard,
- parse_mode="HTML",
- )
await state.clear()
+ await message.answer(
+ texts.t("ADMIN_POLLS_CREATION_CANCELLED", "❌ Создание опроса отменено."),
+ reply_markup=get_admin_communications_submenu_keyboard(db_user.language),
+ )
return
- reward_kopeks = _parse_reward_amount(message.text or "")
- await _safe_delete_message(message)
+ reward_kopeks = _parse_reward_amount(message.text)
if reward_kopeks is None:
- error_text = texts.t(
- "ADMIN_POLLS_CREATION_REWARD_INVALID",
- "❌ Некорректная сумма. Попробуйте ещё раз.",
- )
- form_text = _render_creation_progress(texts, state_data, "reward", error_message=error_text)
- updated = await _edit_creation_message(message.bot, state_data, form_text)
- if not updated:
- await _send_creation_message(
- message,
- state,
- form_text,
- parse_mode="HTML",
- )
+ await message.answer(texts.t("ADMIN_POLLS_CREATION_REWARD_INVALID", "❌ Некорректная сумма. Попробуйте ещё раз."))
return
reward_enabled = reward_kopeks > 0
@@ -733,16 +409,16 @@ async def process_poll_reward(
)
await state.set_state(PollCreationStates.waiting_for_questions)
- new_data = await state.get_data()
- form_text = _render_creation_progress(texts, new_data, "questions")
- updated = await _edit_creation_message(message.bot, new_data, form_text)
- if not updated:
- await _send_creation_message(
- message,
- state,
- form_text,
- parse_mode="HTML",
- )
+ prompt = texts.t(
+ "ADMIN_POLLS_CREATION_QUESTION_PROMPT",
+ (
+ "Введите вопрос и варианты ответов.\n"
+ "Каждая строка — отдельный вариант.\n"
+ "Первая строка — текст вопроса.\n"
+ "Отправьте /done, когда вопросы будут добавлены."
+ ),
+ )
+ await message.answer(prompt)
@admin_required
@@ -754,51 +430,21 @@ async def process_poll_question(
db: AsyncSession,
):
texts = get_texts(db_user.language)
- state_data = await state.get_data()
-
if message.text == "/cancel":
- await _safe_delete_message(message)
- cancel_text = texts.t(
- "ADMIN_POLLS_CREATION_CANCELLED",
- "❌ Создание опроса отменено.",
- )
- keyboard = get_admin_communications_submenu_keyboard(db_user.language)
- updated = await _edit_creation_message(
- message.bot,
- state_data,
- cancel_text,
- reply_markup=keyboard,
- parse_mode="HTML",
- )
- if not updated:
- await _send_creation_message(
- message,
- state,
- cancel_text,
- reply_markup=keyboard,
- parse_mode="HTML",
- )
await state.clear()
+ await message.answer(
+ texts.t("ADMIN_POLLS_CREATION_CANCELLED", "❌ Создание опроса отменено."),
+ reply_markup=get_admin_communications_submenu_keyboard(db_user.language),
+ )
return
if message.text == "/done":
- await _safe_delete_message(message)
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", "❌ Добавьте хотя бы один вопрос."),
)
- form_text = _render_creation_progress(texts, data, "questions", error_message=error_text)
- updated = await _edit_creation_message(message.bot, data, form_text)
- if not updated:
- await _send_creation_message(
- message,
- state,
- form_text,
- parse_mode="HTML",
- )
return
title = data.get("title")
@@ -806,8 +452,6 @@ async def process_poll_question(
reward_enabled = data.get("reward_enabled", False)
reward_amount = data.get("reward_amount_kopeks", 0)
- form_data = data.copy()
-
poll = await create_poll(
db,
title=title,
@@ -818,55 +462,31 @@ async def process_poll_question(
questions=questions,
)
- reward_text = _format_reward_text(poll, db_user.language)
- result_text = texts.t(
- "ADMIN_POLLS_CREATION_FINISHED",
- (
- "✅ Опрос «{title}» создан!\n"
- "Вопросов: {count}\n"
- "{reward}"
- ),
- ).format(
- title=html.escape(poll.title),
- count=len(poll.questions),
- reward=reward_text,
- )
+ await state.clear()
- keyboard = _build_polls_keyboard(await list_polls(db), db_user.language)
- updated = await _edit_creation_message(
- message.bot,
- form_data,
- result_text,
- reply_markup=keyboard,
+ reward_text = _format_reward_text(poll, db_user.language)
+ 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",
)
- if not updated:
- await _send_creation_message(
- message,
- state,
- result_text,
- reply_markup=keyboard,
- parse_mode="HTML",
- )
- await state.clear()
return
- lines = [line.strip() for line in (message.text or "").splitlines() if line.strip()]
- await _safe_delete_message(message)
+ 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",
- "❌ Нужен вопрос и минимум два варианта ответа.",
- )
- form_text = _render_creation_progress(texts, state_data, "questions", error_message=error_text)
- updated = await _edit_creation_message(message.bot, state_data, form_text)
- if not updated:
- await _send_creation_message(
- message,
- state,
- form_text,
- parse_mode="HTML",
+ await message.answer(
+ texts.t(
+ "ADMIN_POLLS_CREATION_MIN_OPTIONS",
+ "❌ Нужен вопрос и минимум два варианта ответа.",
)
+ )
return
question_text = lines[0]
@@ -876,26 +496,13 @@ async def process_poll_question(
questions.append({"text": question_text, "options": options})
await state.update_data(questions=questions)
- new_data = await state.get_data()
- status_message = texts.t(
- "ADMIN_POLLS_CREATION_ADDED_QUESTION",
- "Вопрос добавлен: «{question}». Добавьте следующий вопрос или отправьте /done.",
- ).format(question=html.escape(question_text))
-
- form_text = _render_creation_progress(
- texts,
- new_data,
- "questions",
- status_message=status_message,
+ await message.answer(
+ texts.t(
+ "ADMIN_POLLS_CREATION_ADDED_QUESTION",
+ "Вопрос добавлен: «{question}». Добавьте следующий вопрос или отправьте /done.",
+ ).format(question=question_text),
+ parse_mode="HTML",
)
- updated = await _edit_creation_message(message.bot, new_data, form_text)
- if not updated:
- await _send_creation_message(
- message,
- state,
- form_text,
- parse_mode="HTML",
- )
async def _render_poll_details(poll: Poll, language: str) -> str:
@@ -1062,15 +669,12 @@ async def confirm_poll_send(
await callback.answer("❌ Опрос не найден", show_alert=True)
return
- poll_id_value = poll.id
-
if target.startswith("custom_"):
users = await get_custom_users(db, target.replace("custom_", ""))
else:
users = await get_target_users(db, target)
- user_language = db_user.language
- texts = get_texts(user_language)
+ texts = get_texts(db_user.language)
await callback.message.edit_text(
texts.t("ADMIN_POLLS_SENDING", "📤 Запускаю отправку опроса..."),
parse_mode="HTML",
@@ -1085,7 +689,7 @@ async def confirm_poll_send(
await callback.message.edit_text(
result_text,
- reply_markup=_build_poll_details_keyboard(poll_id_value, user_language),
+ reply_markup=_build_poll_details_keyboard(poll.id, db_user.language),
parse_mode="HTML",
)
await callback.answer()
diff --git a/app/handlers/polls.py b/app/handlers/polls.py
index 94fcbcf0..2572b38b 100644
--- a/app/handlers/polls.py
+++ b/app/handlers/polls.py
@@ -3,7 +3,6 @@ import logging
from datetime import datetime
from aiogram import Dispatcher, F, types
-from aiogram.exceptions import TelegramBadRequest
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
@@ -42,44 +41,6 @@ async def _render_question_text(
return "\n".join(lines)
-async def _update_poll_message(
- message: types.Message,
- text: str,
- *,
- reply_markup: types.InlineKeyboardMarkup | None = None,
- parse_mode: str | None = "HTML",
-) -> bool:
- try:
- await message.edit_text(
- text,
- reply_markup=reply_markup,
- parse_mode=parse_mode,
- )
- return True
- except TelegramBadRequest as error:
- error_text = str(error).lower()
- if "message is not modified" in error_text:
- logger.debug(
- "Опросное сообщение уже актуально, пропускаем обновление: %s",
- error,
- )
- return True
-
- logger.warning(
- "Не удалось обновить сообщение опроса %s: %s",
- message.message_id,
- error,
- )
- except Exception as error: # pragma: no cover - defensive logging
- logger.exception(
- "Непредвиденная ошибка при обновлении сообщения опроса %s: %s",
- message.message_id,
- error,
- )
-
- return False
-
-
def _build_options_keyboard(response_id: int, question: PollQuestion) -> types.InlineKeyboardMarkup:
buttons: list[list[types.InlineKeyboardButton]] = []
for option in sorted(question.options, key=lambda o: o.order):
@@ -137,13 +98,11 @@ async def handle_poll_start(
db_user.language,
)
- if not await _update_poll_message(
- callback.message,
+ await callback.message.edit_text(
question_text,
reply_markup=_build_options_keyboard(response.id, question),
- ):
- await callback.answer(texts.t("POLL_ERROR", "Не удалось показать вопрос."), show_alert=True)
- return
+ parse_mode="HTML",
+ )
await callback.answer()
@@ -204,13 +163,11 @@ async def handle_poll_answer(
len(response.poll.questions),
db_user.language,
)
- if not await _update_poll_message(
- callback.message,
+ await callback.message.edit_text(
question_text,
reply_markup=_build_options_keyboard(response.id, next_question),
- ):
- await callback.answer(texts.t("POLL_ERROR", "Не удалось показать вопрос."), show_alert=True)
- return
+ parse_mode="HTML",
+ )
await callback.answer()
return
@@ -228,12 +185,7 @@ async def handle_poll_answer(
).format(amount=settings.format_price(reward_amount))
)
- if not await _update_poll_message(
- callback.message,
- "\n\n".join(thanks_lines),
- ):
- await callback.answer(texts.t("POLL_COMPLETED", "🙏 Спасибо за участие в опросе!"))
- return
+ await callback.message.edit_text("\n\n".join(thanks_lines), parse_mode="HTML")
asyncio.create_task(
_delete_message_later(callback.bot, callback.message.chat.id, callback.message.message_id)
)
diff --git a/app/services/poll_service.py b/app/services/poll_service.py
index fc3962e8..5e8b6187 100644
--- a/app/services/poll_service.py
+++ b/app/services/poll_service.py
@@ -1,10 +1,8 @@
import asyncio
import logging
-from types import SimpleNamespace
from typing import Iterable
from aiogram import Bot
-from aiogram.exceptions import TelegramBadRequest
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -24,8 +22,8 @@ from app.localization.texts import get_texts
logger = logging.getLogger(__name__)
-def _build_poll_invitation_text(poll: Poll, language: str) -> str:
- texts = get_texts(language)
+def _build_poll_invitation_text(poll: Poll, user: User) -> str:
+ texts = get_texts(user.language)
lines: list[str] = [f"🗳️ {poll.title}"]
if poll.description:
@@ -72,28 +70,11 @@ async def send_poll_to_users(
failed = 0
skipped = 0
- poll_id = poll.id
- poll_snapshot = SimpleNamespace(
- title=poll.title,
- description=poll.description,
- reward_enabled=poll.reward_enabled,
- reward_amount_kopeks=poll.reward_amount_kopeks,
- )
-
- user_snapshots = [
- SimpleNamespace(
- id=user.id,
- telegram_id=user.telegram_id,
- language=user.language,
- )
- for user in users
- ]
-
- for index, user in enumerate(user_snapshots, start=1):
+ for index, user in enumerate(users, start=1):
existing_response = await db.execute(
select(PollResponse.id).where(
and_(
- PollResponse.poll_id == poll_id,
+ PollResponse.poll_id == poll.id,
PollResponse.user_id == user.id,
)
)
@@ -103,7 +84,7 @@ async def send_poll_to_users(
continue
response = PollResponse(
- poll_id=poll_id,
+ poll_id=poll.id,
user_id=user.id,
)
db.add(response)
@@ -111,7 +92,7 @@ async def send_poll_to_users(
try:
await db.flush()
- text = _build_poll_invitation_text(poll_snapshot, user.language)
+ text = _build_poll_invitation_text(poll, user)
keyboard = build_start_keyboard(response.id, user.language)
await bot.send_message(
@@ -127,30 +108,11 @@ async def send_poll_to_users(
if index % 20 == 0:
await asyncio.sleep(1)
- except TelegramBadRequest as error:
- error_text = str(error).lower()
- if "chat not found" in error_text or "bot was blocked by the user" in error_text:
- skipped += 1
- logger.info(
- "ℹ️ Пропуск пользователя %s при отправке опроса %s: %s",
- user.telegram_id,
- poll_id,
- error,
- )
- else: # pragma: no cover - unexpected telegram error
- failed += 1
- logger.error(
- "❌ Ошибка отправки опроса %s пользователю %s: %s",
- poll_id,
- user.telegram_id,
- error,
- )
- await db.rollback()
except Exception as error: # pragma: no cover - defensive logging
failed += 1
logger.error(
"❌ Ошибка отправки опроса %s пользователю %s: %s",
- poll_id,
+ poll.id,
user.telegram_id,
error,
)