mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-21 11:51:06 +00:00
Merge pull request #1469 from Fr1ngg/56bjws-bedolaga/fix-poll-sending-error-in-bot
Stabilize poll creation flow and poll delivery
This commit is contained in:
@@ -3,7 +3,8 @@ import logging
|
||||
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
||||
from typing import Optional
|
||||
|
||||
from aiogram import Dispatcher, F, types
|
||||
from aiogram import Bot, Dispatcher, F, types
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -33,6 +34,209 @@ 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: FSMContext,
|
||||
text: str,
|
||||
*,
|
||||
reply_markup: types.InlineKeyboardMarkup | None = None,
|
||||
parse_mode: str | None = "HTML",
|
||||
) -> bool:
|
||||
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 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
|
||||
|
||||
logger.warning(
|
||||
"Не удалось обновить сообщение создания опроса %s: %s",
|
||||
message_id,
|
||||
error,
|
||||
)
|
||||
|
||||
try:
|
||||
await bot.delete_message(chat_id=chat_id, message_id=message_id)
|
||||
except TelegramBadRequest:
|
||||
# Сообщение уже удалено или нельзя удалить — продолжаем без ошибок
|
||||
pass
|
||||
except Exception as delete_error: # pragma: no cover - defensive logging
|
||||
logger.debug(
|
||||
"Не удалось удалить старое сообщение создания опроса %s: %s",
|
||||
message_id,
|
||||
delete_error,
|
||||
)
|
||||
except Exception as error: # pragma: no cover - defensive logging
|
||||
logger.error(
|
||||
"Непредвиденная ошибка при обновлении сообщения создания опроса %s: %s",
|
||||
message_id,
|
||||
error,
|
||||
)
|
||||
|
||||
if not chat_id:
|
||||
return False
|
||||
|
||||
try:
|
||||
new_message = await bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text=text,
|
||||
reply_markup=reply_markup,
|
||||
parse_mode=parse_mode,
|
||||
)
|
||||
await state.update_data(
|
||||
form_chat_id=new_message.chat.id,
|
||||
form_message_id=new_message.message_id,
|
||||
)
|
||||
return True
|
||||
except Exception as error: # pragma: no cover - defensive logging
|
||||
logger.error(
|
||||
"Не удалось отправить сообщение создания опроса в чат %s: %s",
|
||||
chat_id,
|
||||
error,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _render_creation_progress(
|
||||
texts,
|
||||
data: dict,
|
||||
next_step: str,
|
||||
*,
|
||||
status_message: str | None = None,
|
||||
error_message: str | None = None,
|
||||
) -> str:
|
||||
lines: list[str] = ["🗳️ <b>Создание опроса</b>"]
|
||||
|
||||
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()
|
||||
@@ -277,15 +481,24 @@ 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 state.update_data(
|
||||
questions=[],
|
||||
form_chat_id=callback.message.chat.id,
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
texts.t(
|
||||
"ADMIN_POLLS_CREATION_TITLE_PROMPT",
|
||||
"🗳️ <b>Создание опроса</b>\n\nВведите заголовок опроса:",
|
||||
),
|
||||
state_data = await state.get_data()
|
||||
form_text = _render_creation_progress(texts, state_data, "title")
|
||||
|
||||
form_message = await callback.message.answer(
|
||||
form_text,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await state.update_data(
|
||||
form_chat_id=form_message.chat.id,
|
||||
form_message_id=form_message.message_id,
|
||||
)
|
||||
|
||||
await _safe_delete_message(callback.message)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@@ -297,31 +510,50 @@ async def process_poll_title(
|
||||
state: FSMContext,
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
|
||||
if message.text == "/cancel":
|
||||
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),
|
||||
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,
|
||||
cancel_text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
if not updated:
|
||||
await message.answer(cancel_text, reply_markup=keyboard, parse_mode="HTML")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
title = message.text.strip()
|
||||
title = (message.text or "").strip()
|
||||
await _safe_delete_message(message)
|
||||
|
||||
state_data = await state.get_data()
|
||||
if not title:
|
||||
await message.answer("❌ Заголовок не может быть пустым. Попробуйте снова.")
|
||||
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, form_text)
|
||||
if not updated:
|
||||
await message.answer(error_text)
|
||||
return
|
||||
|
||||
await state.update_data(title=title)
|
||||
await state.set_state(PollCreationStates.waiting_for_description)
|
||||
|
||||
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",
|
||||
)
|
||||
new_data = await state.get_data()
|
||||
form_text = _render_creation_progress(texts, new_data, "description")
|
||||
updated = await _edit_creation_message(message.bot, state, form_text)
|
||||
if not updated:
|
||||
await message.answer(form_text, parse_mode="HTML")
|
||||
|
||||
|
||||
@admin_required
|
||||
@@ -333,36 +565,55 @@ async def process_poll_description(
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
state_data = await state.get_data()
|
||||
|
||||
if message.text == "/cancel":
|
||||
await state.clear()
|
||||
await message.answer(
|
||||
texts.t("ADMIN_POLLS_CREATION_CANCELLED", "❌ Создание опроса отменено."),
|
||||
reply_markup=get_admin_communications_submenu_keyboard(db_user.language),
|
||||
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,
|
||||
cancel_text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
if not updated:
|
||||
await message.answer(cancel_text, reply_markup=keyboard, parse_mode="HTML")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
description: Optional[str]
|
||||
if message.text == "/skip":
|
||||
message_text = message.text or ""
|
||||
await _safe_delete_message(message)
|
||||
|
||||
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:
|
||||
await message.answer(
|
||||
texts.t("ADMIN_POLLS_CREATION_INVALID_HTML", "❌ Ошибка в HTML: {error}").format(error=error_message)
|
||||
)
|
||||
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, form_text)
|
||||
if not updated:
|
||||
await message.answer(error_text)
|
||||
return
|
||||
|
||||
await state.update_data(description=description)
|
||||
await state.set_state(PollCreationStates.waiting_for_reward)
|
||||
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"ADMIN_POLLS_CREATION_REWARD_PROMPT",
|
||||
"Введите сумму награды в рублях. Отправьте 0 чтобы отключить награду.",
|
||||
)
|
||||
)
|
||||
new_data = await state.get_data()
|
||||
form_text = _render_creation_progress(texts, new_data, "reward")
|
||||
updated = await _edit_creation_message(message.bot, state, form_text)
|
||||
if not updated:
|
||||
await message.answer(form_text, parse_mode="HTML")
|
||||
|
||||
|
||||
def _parse_reward_amount(message_text: str) -> int | None:
|
||||
@@ -388,18 +639,38 @@ async def process_poll_reward(
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
state_data = await state.get_data()
|
||||
|
||||
if message.text == "/cancel":
|
||||
await state.clear()
|
||||
await message.answer(
|
||||
texts.t("ADMIN_POLLS_CREATION_CANCELLED", "❌ Создание опроса отменено."),
|
||||
reply_markup=get_admin_communications_submenu_keyboard(db_user.language),
|
||||
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,
|
||||
cancel_text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
if not updated:
|
||||
await message.answer(cancel_text, reply_markup=keyboard, parse_mode="HTML")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
reward_kopeks = _parse_reward_amount(message.text)
|
||||
reward_kopeks = _parse_reward_amount(message.text or "")
|
||||
await _safe_delete_message(message)
|
||||
if reward_kopeks is None:
|
||||
await message.answer(texts.t("ADMIN_POLLS_CREATION_REWARD_INVALID", "❌ Некорректная сумма. Попробуйте ещё раз."))
|
||||
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, form_text)
|
||||
if not updated:
|
||||
await message.answer(error_text)
|
||||
return
|
||||
|
||||
reward_enabled = reward_kopeks > 0
|
||||
@@ -409,16 +680,11 @@ async def process_poll_reward(
|
||||
)
|
||||
await state.set_state(PollCreationStates.waiting_for_questions)
|
||||
|
||||
prompt = texts.t(
|
||||
"ADMIN_POLLS_CREATION_QUESTION_PROMPT",
|
||||
(
|
||||
"Введите вопрос и варианты ответов.\n"
|
||||
"Каждая строка — отдельный вариант.\n"
|
||||
"Первая строка — текст вопроса.\n"
|
||||
"Отправьте /done, когда вопросы будут добавлены."
|
||||
),
|
||||
)
|
||||
await message.answer(prompt)
|
||||
new_data = await state.get_data()
|
||||
form_text = _render_creation_progress(texts, new_data, "questions")
|
||||
updated = await _edit_creation_message(message.bot, state, form_text)
|
||||
if not updated:
|
||||
await message.answer(form_text, parse_mode="HTML")
|
||||
|
||||
|
||||
@admin_required
|
||||
@@ -430,21 +696,40 @@ async def process_poll_question(
|
||||
db: AsyncSession,
|
||||
):
|
||||
texts = get_texts(db_user.language)
|
||||
state_data = await state.get_data()
|
||||
|
||||
if message.text == "/cancel":
|
||||
await state.clear()
|
||||
await message.answer(
|
||||
texts.t("ADMIN_POLLS_CREATION_CANCELLED", "❌ Создание опроса отменено."),
|
||||
reply_markup=get_admin_communications_submenu_keyboard(db_user.language),
|
||||
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,
|
||||
cancel_text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
if not updated:
|
||||
await message.answer(cancel_text, reply_markup=keyboard, parse_mode="HTML")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
if message.text == "/done":
|
||||
await _safe_delete_message(message)
|
||||
data = await state.get_data()
|
||||
questions = data.get("questions", [])
|
||||
if not questions:
|
||||
await message.answer(
|
||||
texts.t("ADMIN_POLLS_CREATION_NEEDS_QUESTION", "❌ Добавьте хотя бы один вопрос."),
|
||||
error_text = 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, state, form_text)
|
||||
if not updated:
|
||||
await message.answer(error_text)
|
||||
return
|
||||
|
||||
title = data.get("title")
|
||||
@@ -462,31 +747,48 @@ async def process_poll_question(
|
||||
questions=questions,
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
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,
|
||||
result_text = texts.t(
|
||||
"ADMIN_POLLS_CREATION_FINISHED",
|
||||
(
|
||||
"✅ Опрос «{title}» создан!\n"
|
||||
"Вопросов: {count}\n"
|
||||
"{reward}"
|
||||
),
|
||||
reply_markup=_build_polls_keyboard(await list_polls(db), db_user.language),
|
||||
).format(
|
||||
title=html.escape(poll.title),
|
||||
count=len(poll.questions),
|
||||
reward=reward_text,
|
||||
)
|
||||
|
||||
keyboard = _build_polls_keyboard(await list_polls(db), db_user.language)
|
||||
updated = await _edit_creation_message(
|
||||
message.bot,
|
||||
state,
|
||||
result_text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
if not updated:
|
||||
await message.answer(
|
||||
result_text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
lines = [line.strip() for line in message.text.splitlines() if line.strip()]
|
||||
lines = [line.strip() for line in (message.text or "").splitlines() if line.strip()]
|
||||
await _safe_delete_message(message)
|
||||
if len(lines) < 3:
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"ADMIN_POLLS_CREATION_MIN_OPTIONS",
|
||||
"❌ Нужен вопрос и минимум два варианта ответа.",
|
||||
)
|
||||
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, form_text)
|
||||
if not updated:
|
||||
await message.answer(error_text)
|
||||
return
|
||||
|
||||
question_text = lines[0]
|
||||
@@ -496,13 +798,21 @@ async def process_poll_question(
|
||||
questions.append({"text": question_text, "options": options})
|
||||
await state.update_data(questions=questions)
|
||||
|
||||
await message.answer(
|
||||
texts.t(
|
||||
"ADMIN_POLLS_CREATION_ADDED_QUESTION",
|
||||
"Вопрос добавлен: «{question}». Добавьте следующий вопрос или отправьте /done.",
|
||||
).format(question=question_text),
|
||||
parse_mode="HTML",
|
||||
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,
|
||||
)
|
||||
updated = await _edit_creation_message(message.bot, state, form_text)
|
||||
if not updated:
|
||||
await message.answer(status_message, parse_mode="HTML")
|
||||
|
||||
|
||||
async def _render_poll_details(poll: Poll, language: str) -> str:
|
||||
@@ -669,6 +979,8 @@ 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:
|
||||
@@ -689,7 +1001,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_value, db_user.language),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from types import SimpleNamespace
|
||||
from typing import Iterable
|
||||
|
||||
from aiogram import Bot
|
||||
@@ -17,6 +18,7 @@ from app.database.models import (
|
||||
TransactionType,
|
||||
User,
|
||||
)
|
||||
from app.localization.loader import DEFAULT_LANGUAGE
|
||||
from app.localization.texts import get_texts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -70,11 +72,42 @@ async def send_poll_to_users(
|
||||
failed = 0
|
||||
skipped = 0
|
||||
|
||||
for index, user in enumerate(users, start=1):
|
||||
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: list[SimpleNamespace] = []
|
||||
for raw_user in users:
|
||||
try:
|
||||
user_id = getattr(raw_user, "id")
|
||||
telegram_id = getattr(raw_user, "telegram_id")
|
||||
except AttributeError as error: # pragma: no cover - defensive logging
|
||||
logger.warning("Пропускаем пользователя без обязательных полей: %s", error)
|
||||
continue
|
||||
|
||||
language = getattr(raw_user, "language", None) or DEFAULT_LANGUAGE
|
||||
user_snapshots.append(
|
||||
SimpleNamespace(id=user_id, telegram_id=telegram_id, language=language)
|
||||
)
|
||||
|
||||
for index, user in enumerate(user_snapshots, start=1):
|
||||
if not user.telegram_id:
|
||||
failed += 1
|
||||
logger.error(
|
||||
"❌ Ошибка отправки опроса %s: у пользователя %s отсутствует telegram_id",
|
||||
poll_id,
|
||||
user.id,
|
||||
)
|
||||
continue
|
||||
|
||||
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,
|
||||
)
|
||||
)
|
||||
@@ -84,7 +117,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)
|
||||
@@ -92,7 +125,7 @@ async def send_poll_to_users(
|
||||
try:
|
||||
await db.flush()
|
||||
|
||||
text = _build_poll_invitation_text(poll, user)
|
||||
text = _build_poll_invitation_text(poll_snapshot, user)
|
||||
keyboard = build_start_keyboard(response.id, user.language)
|
||||
|
||||
await bot.send_message(
|
||||
@@ -112,7 +145,7 @@ async def send_poll_to_users(
|
||||
failed += 1
|
||||
logger.error(
|
||||
"❌ Ошибка отправки опроса %s пользователю %s: %s",
|
||||
poll.id,
|
||||
poll_id,
|
||||
user.telegram_id,
|
||||
error,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user