From 2fa84f6dc94f7ea1c45cdc3bcd1b86912a5108e3 Mon Sep 17 00:00:00 2001 From: Egor Date: Thu, 23 Oct 2025 07:39:20 +0300 Subject: [PATCH] Revert "Handle redundant poll message edits gracefully" --- app/handlers/admin/polls.py | 560 +++++------------------------------ app/handlers/polls.py | 62 +--- app/services/poll_service.py | 52 +--- 3 files changed, 96 insertions(+), 578 deletions(-) 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, )