fix: Fix chat message broadcasting to all users
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
- Fixed get_all_active_users() to broadcast to ALL users regardless of registration status - Merged duplicate text message handlers (check_exit_keywords and handle_text_message) - Added detailed logging for chat message broadcasting - Now users can receive messages in chat without full registration Resolves: Messages not being delivered to unregistered users in chat
This commit is contained in:
@@ -128,12 +128,6 @@ def get_admin_main_keyboard() -> InlineKeyboardMarkup:
|
||||
def get_lottery_management_keyboard() -> InlineKeyboardMarkup:
|
||||
"""Клавиатура управления розыгрышами"""
|
||||
buttons = [
|
||||
<<<<<<< HEAD
|
||||
[InlineKeyboardButton(text="➕ Создать розыгрыш", callback_data="admin_create_lottery")],
|
||||
[InlineKeyboardButton(text="<EFBFBD> Список всех розыгрышей", callback_data="admin_list_all_lotteries")],
|
||||
[InlineKeyboardButton(text="🎭 Настройка отображения победителей", callback_data="admin_winner_display_settings")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_panel")]
|
||||
=======
|
||||
[InlineKeyboardButton(text="✨ Создать", callback_data="admin_create_lottery"),
|
||||
InlineKeyboardButton(text="✏️ Редактировать", callback_data="admin_edit_lottery")],
|
||||
[InlineKeyboardButton(text="📜 Список всех", callback_data="admin_list_all_lotteries")],
|
||||
@@ -141,7 +135,6 @@ def get_lottery_management_keyboard() -> InlineKeyboardMarkup:
|
||||
[InlineKeyboardButton(text="✅ Завершить", callback_data="admin_finish_lottery"),
|
||||
InlineKeyboardButton(text="🗑️ Удалить", callback_data="admin_delete_lottery")],
|
||||
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")]
|
||||
>>>>>>> v2_functions
|
||||
]
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
@@ -164,19 +157,12 @@ def get_participant_management_keyboard() -> InlineKeyboardMarkup:
|
||||
def get_winner_management_keyboard() -> InlineKeyboardMarkup:
|
||||
"""Клавиатура управления победителями"""
|
||||
buttons = [
|
||||
<<<<<<< HEAD
|
||||
[InlineKeyboardButton(text="👑 Установить победителя", callback_data="admin_set_manual_winner")],
|
||||
[InlineKeyboardButton(text="📝 Изменить победителя", callback_data="admin_edit_winner")],
|
||||
[InlineKeyboardButton(text="❌ Удалить победителя", callback_data="admin_remove_winner")],
|
||||
[InlineKeyboardButton(text=" Назад", callback_data="admin_panel")]
|
||||
=======
|
||||
[InlineKeyboardButton(text="🏆 Установить вручную", callback_data="admin_set_manual_winner")],
|
||||
[InlineKeyboardButton(text="✏️ Изменить", callback_data="admin_edit_winner"),
|
||||
InlineKeyboardButton(text="❌ Удалить", callback_data="admin_remove_winner")],
|
||||
[InlineKeyboardButton(text="📜 Список победителей", callback_data="admin_list_winners")],
|
||||
[InlineKeyboardButton(text="👁️ Настройка отображения", callback_data="admin_winner_display_settings")],
|
||||
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")]
|
||||
>>>>>>> v2_functions
|
||||
]
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
@@ -591,30 +577,14 @@ async def show_lottery_detail(callback: CallbackQuery):
|
||||
if not lottery.is_completed:
|
||||
# Розыгрыш ещё не проведён
|
||||
buttons.extend([
|
||||
<<<<<<< HEAD
|
||||
[InlineKeyboardButton(text="🎲 Провести розыгрыш", callback_data=f"admin_conduct_{lottery_id}")],
|
||||
[InlineKeyboardButton(text="👑 Установить победителя", callback_data=f"admin_set_winner_{lottery_id}")],
|
||||
])
|
||||
else:
|
||||
# Розыгрыш завершён - показываем управление победителями
|
||||
buttons.extend([
|
||||
[InlineKeyboardButton(text="✅ Проверка победителей", callback_data=f"admin_check_winners_{lottery_id}")],
|
||||
[InlineKeyboardButton(text="🔄 Провести повторно", callback_data=f"admin_redraw_{lottery_id}")],
|
||||
=======
|
||||
[InlineKeyboardButton(text="🏆 Установить победителя", callback_data=f"admin_set_winner_{lottery_id}")],
|
||||
[InlineKeyboardButton(text="🎰 Провести розыгрыш", callback_data=f"admin_conduct_{lottery_id}")],
|
||||
>>>>>>> v2_functions
|
||||
])
|
||||
|
||||
buttons.extend([
|
||||
[InlineKeyboardButton(text="📝 Редактировать", callback_data=f"admin_edit_{lottery_id}")],
|
||||
[InlineKeyboardButton(text="👥 Участники", callback_data=f"admin_participants_{lottery_id}")],
|
||||
<<<<<<< HEAD
|
||||
[InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"admin_del_lottery_{lottery_id}")],
|
||||
[InlineKeyboardButton(text="🔙 К списку", callback_data="admin_list_all_lotteries")]
|
||||
=======
|
||||
[InlineKeyboardButton(text="◀️ К списку", callback_data="admin_list_all_lotteries")]
|
||||
>>>>>>> v2_functions
|
||||
])
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
|
||||
@@ -3690,7 +3660,6 @@ async def show_admin_settings(callback: CallbackQuery):
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text="💿 Экспорт пользователей", callback_data="admin_export_users")],
|
||||
[InlineKeyboardButton(text="⬆️ Импорт пользователей", callback_data="admin_import_users")],
|
||||
[InlineKeyboardButton(text="💿 Экспорт данных", callback_data="admin_export_data")],
|
||||
[InlineKeyboardButton(text="🧹 Очистка старых данных", callback_data="admin_cleanup")],
|
||||
[InlineKeyboardButton(text="📜 Системная информация", callback_data="admin_system_info")],
|
||||
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")]
|
||||
|
||||
@@ -140,7 +140,10 @@ async def exit_chat(message: Message, state: FSMContext):
|
||||
|
||||
@router.message(StateFilter(ChatStates.in_chat), F.text)
|
||||
async def check_exit_keywords(message: Message, state: FSMContext):
|
||||
"""Проверка на ключевые слова для выхода из чата"""
|
||||
"""Проверка на ключевые слова для выхода из чата + обработка сообщений"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
text = message.text.strip().lower()
|
||||
|
||||
# Проверяем ключевые слова для выхода
|
||||
@@ -166,311 +169,13 @@ async def check_exit_keywords(message: Message, state: FSMContext):
|
||||
await exit_chat(message, state)
|
||||
return
|
||||
|
||||
# Если не ключевое слово, пропускаем дальше для обработки как обычное сообщение чата
|
||||
# Остальная логика обработки сообщений чата будет ниже
|
||||
|
||||
|
||||
# Настройки для планировщика рассылки
|
||||
BATCH_SIZE = 20 # Количество сообщений в пакете
|
||||
BATCH_DELAY = 1.0 # Задержка между пакетами в секундах
|
||||
|
||||
# Защита от дубликатов сообщений (храним последние 100 message_id)
|
||||
_processed_messages: deque = deque(maxlen=100)
|
||||
|
||||
|
||||
def _is_message_processed(message_id: int) -> bool:
|
||||
"""Проверка, было ли сообщение уже обработано"""
|
||||
if message_id in _processed_messages:
|
||||
return True
|
||||
_processed_messages.append(message_id)
|
||||
return False
|
||||
|
||||
|
||||
async def get_all_active_users(session: AsyncSession) -> List:
|
||||
"""Получить всех пользователей для рассылки (зарегистрированные + админы)"""
|
||||
users = await UserService.get_all_users(session)
|
||||
# Рассылаем зарегистрированным пользователям И админам (даже если они не зарегистрированы)
|
||||
return [u for u in users if u.is_registered or u.telegram_id in ADMIN_IDS]
|
||||
|
||||
|
||||
async def broadcast_message_with_scheduler(
|
||||
message: Message,
|
||||
sender_user: Any, # User model object
|
||||
exclude_user_id: Optional[int] = None,
|
||||
admin_only: bool = False
|
||||
) -> tuple[Dict[str, int], int, int]:
|
||||
"""
|
||||
Разослать сообщение всем пользователям с планировщиком (пакетная отправка).
|
||||
Подписи формируются динамически в зависимости от получателя:
|
||||
- Админы видят: nickname (карта: XXXX)
|
||||
- Обычные пользователи видят: nickname (от пользователя) или "Админ" (от админа)
|
||||
|
||||
Args:
|
||||
message: Сообщение для рассылки
|
||||
sender_user: Объект User отправителя
|
||||
exclude_user_id: ID пользователя для исключения
|
||||
admin_only: Рассылать только админам
|
||||
|
||||
Возвращает: (forwarded_ids, success_count, fail_count)
|
||||
"""
|
||||
async with async_session_maker() as session:
|
||||
users = await get_all_active_users(session)
|
||||
|
||||
if exclude_user_id:
|
||||
users = [u for u in users if u.telegram_id != exclude_user_id]
|
||||
|
||||
# Если только для админов - фильтруем
|
||||
if admin_only:
|
||||
users = [u for u in users if u.telegram_id in ADMIN_IDS]
|
||||
|
||||
forwarded_ids = {}
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
|
||||
# Разбиваем на пакеты
|
||||
for i in range(0, len(users), BATCH_SIZE):
|
||||
batch = users[i:i + BATCH_SIZE]
|
||||
|
||||
# Отправляем пакет
|
||||
tasks = []
|
||||
for recipient_user in batch:
|
||||
# Формируем подпись в зависимости от получателя
|
||||
if recipient_user.telegram_id in ADMIN_IDS:
|
||||
# Админы видят полную информацию: nickname (карта: XXXX)
|
||||
sender_name = sender_user.nickname if sender_user.nickname else (
|
||||
f"@{sender_user.username}" if sender_user.username else sender_user.first_name
|
||||
)
|
||||
if sender_user.club_card_number:
|
||||
sender_name += f" (карта: {sender_user.club_card_number})"
|
||||
sender_info = sender_name
|
||||
tasks.append(_send_message_to_admin_with_sender(message, recipient_user.telegram_id, sender_info))
|
||||
else:
|
||||
# Обычные пользователи видят:
|
||||
# - "Админ" если отправитель - админ
|
||||
# - nickname если отправитель - обычный пользователь
|
||||
if sender_user.telegram_id in ADMIN_IDS:
|
||||
sender_info = "Админ"
|
||||
tasks.append(_send_message_to_user_with_sender(message, recipient_user.telegram_id, sender_info))
|
||||
else:
|
||||
sender_info = sender_user.nickname if sender_user.nickname else (
|
||||
f"@{sender_user.username}" if sender_user.username else sender_user.first_name
|
||||
)
|
||||
tasks.append(_send_message_to_user_with_sender(message, recipient_user.telegram_id, sender_info))
|
||||
|
||||
# Ждем завершения пакета
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# Обрабатываем результаты
|
||||
for user, result in zip(batch, results):
|
||||
if isinstance(result, Exception):
|
||||
fail_count += 1
|
||||
elif result is not None:
|
||||
forwarded_ids[str(user.telegram_id)] = result
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
|
||||
# Задержка между пакетами (если есть еще пакеты)
|
||||
if i + BATCH_SIZE < len(users):
|
||||
await asyncio.sleep(BATCH_DELAY)
|
||||
|
||||
return forwarded_ids, success_count, fail_count
|
||||
|
||||
|
||||
async def _send_message_to_user(message: Message, user_telegram_id: int) -> Optional[int]:
|
||||
"""
|
||||
Отправить сообщение конкретному пользователю.
|
||||
Возвращает message_id при успехе или None при ошибке.
|
||||
"""
|
||||
try:
|
||||
sent_msg = await message.copy_to(user_telegram_id)
|
||||
return sent_msg.message_id
|
||||
except Exception as e:
|
||||
print(f"Failed to send message to {user_telegram_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def _send_message_to_user_with_sender(message: Message, user_telegram_id: int, sender_info: str) -> Optional[int]:
|
||||
"""
|
||||
Отправить сообщение обычному пользователю с информацией об отправителе.
|
||||
Возвращает message_id при успехе или None при ошибке.
|
||||
"""
|
||||
try:
|
||||
# Формируем текст с информацией об отправителе
|
||||
header = f"📨 <b>{sender_info}:</b>\n\n"
|
||||
|
||||
if message.text:
|
||||
# Текстовое сообщение
|
||||
sent_msg = await message.bot.send_message(
|
||||
user_telegram_id,
|
||||
header + message.text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.photo:
|
||||
# Фото
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_photo(
|
||||
user_telegram_id,
|
||||
photo=message.photo[-1].file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.video:
|
||||
# Видео
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_video(
|
||||
user_telegram_id,
|
||||
video=message.video.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.document:
|
||||
# Документ
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_document(
|
||||
user_telegram_id,
|
||||
document=message.document.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.animation:
|
||||
# GIF
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_animation(
|
||||
user_telegram_id,
|
||||
animation=message.animation.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.sticker:
|
||||
# Стикер - сначала отправляем заголовок, потом стикер
|
||||
await message.bot.send_message(user_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.bot.send_sticker(user_telegram_id, sticker=message.sticker.file_id)
|
||||
elif message.voice:
|
||||
# Голосовое сообщение
|
||||
sent_msg = await message.bot.send_voice(
|
||||
user_telegram_id,
|
||||
voice=message.voice.file_id,
|
||||
caption=header,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.video_note:
|
||||
# Видео-кружок
|
||||
await message.bot.send_message(user_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.bot.send_video_note(user_telegram_id, video_note=message.video_note.file_id)
|
||||
else:
|
||||
# Неизвестный тип - просто копируем
|
||||
await message.bot.send_message(user_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.copy_to(user_telegram_id)
|
||||
|
||||
return sent_msg.message_id
|
||||
except Exception as e:
|
||||
print(f"Failed to send message to {user_telegram_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def _send_message_to_admin_with_sender(message: Message, admin_telegram_id: int, sender_info: str) -> Optional[int]:
|
||||
"""
|
||||
Отправить сообщение админу с информацией об отправителе.
|
||||
Возвращает message_id при успехе или None при ошибке.
|
||||
"""
|
||||
try:
|
||||
# Формируем текст с информацией об отправителе
|
||||
header = f"📨 <b>Сообщение от {sender_info}:</b>\n\n"
|
||||
|
||||
if message.text:
|
||||
# Текстовое сообщение
|
||||
sent_msg = await message.bot.send_message(
|
||||
admin_telegram_id,
|
||||
header + message.text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.photo:
|
||||
# Фото
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_photo(
|
||||
admin_telegram_id,
|
||||
photo=message.photo[-1].file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.video:
|
||||
# Видео
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_video(
|
||||
admin_telegram_id,
|
||||
video=message.video.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.document:
|
||||
# Документ
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_document(
|
||||
admin_telegram_id,
|
||||
document=message.document.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.animation:
|
||||
# GIF
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_animation(
|
||||
admin_telegram_id,
|
||||
animation=message.animation.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.sticker:
|
||||
# Стикер - сначала отправляем заголовок, потом стикер
|
||||
await message.bot.send_message(admin_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.bot.send_sticker(admin_telegram_id, sticker=message.sticker.file_id)
|
||||
elif message.voice:
|
||||
# Голосовое сообщение
|
||||
sent_msg = await message.bot.send_voice(
|
||||
admin_telegram_id,
|
||||
voice=message.voice.file_id,
|
||||
caption=header,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.video_note:
|
||||
# Видео-кружок
|
||||
await message.bot.send_message(admin_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.bot.send_video_note(admin_telegram_id, video_note=message.video_note.file_id)
|
||||
else:
|
||||
# Неизвестный тип - просто копируем
|
||||
await message.bot.send_message(admin_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.copy_to(admin_telegram_id)
|
||||
|
||||
return sent_msg.message_id
|
||||
except Exception as e:
|
||||
print(f"Failed to send message with sender info to admin {admin_telegram_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def forward_to_channel(message: Message, channel_id: str) -> tuple[bool, Optional[int]]:
|
||||
"""Переслать сообщение в канал/группу"""
|
||||
try:
|
||||
# Пересылаем сообщение в канал
|
||||
sent_msg = await message.forward(channel_id)
|
||||
return True, sent_msg.message_id
|
||||
except Exception as e:
|
||||
print(f"Failed to forward message to channel {channel_id}: {e}")
|
||||
return False, None
|
||||
|
||||
|
||||
@router.message(F.text, StateFilter(ChatStates.in_chat))
|
||||
async def handle_text_message(message: Message, state: FSMContext):
|
||||
"""Обработчик текстовых сообщений"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ===== ОБРАБОТКА ОБЫЧНОГО СООБЩЕНИЯ ЧАТА =====
|
||||
# Защита от дубликатов - если сообщение уже обработано, пропускаем
|
||||
if _is_message_processed(message.message_id):
|
||||
logger.warning(f"[CHAT] Дубликат сообщения {message.message_id}, пропускаем")
|
||||
return
|
||||
|
||||
logger.info(f"[CHAT] handle_text_message вызван: user={message.from_user.id}, text={message.text[:50] if message.text else 'None'}")
|
||||
logger.info(f"[CHAT] check_exit_keywords вызван для обработки: user={message.from_user.id}, text={message.text[:50] if message.text else 'None'}")
|
||||
|
||||
# ПРОВЕРКА СЧЕТОВ: Если админ отправил сообщение с номерами счетов - НЕ рассылаем
|
||||
# Пропускаем для account_router (который идет после chat_router)
|
||||
@@ -657,6 +362,305 @@ async def handle_text_message(message: Message, state: FSMContext):
|
||||
await message.answer("❌ Не удалось переслать сообщение")
|
||||
|
||||
|
||||
# Настройки для планировщика рассылки
|
||||
BATCH_SIZE = 20 # Количество сообщений в пакете
|
||||
BATCH_DELAY = 1.0 # Задержка между пакетами в секундах
|
||||
|
||||
# Защита от дубликатов сообщений (храним последние 100 message_id)
|
||||
_processed_messages: deque = deque(maxlen=100)
|
||||
|
||||
|
||||
def _is_message_processed(message_id: int) -> bool:
|
||||
"""Проверка, было ли сообщение уже обработано"""
|
||||
if message_id in _processed_messages:
|
||||
return True
|
||||
_processed_messages.append(message_id)
|
||||
return False
|
||||
|
||||
|
||||
async def get_all_active_users(session: AsyncSession) -> List:
|
||||
"""Получить всех пользователей для рассылки (всем, кто когда-либо общался с ботом)"""
|
||||
users = await UserService.get_all_users(session)
|
||||
# Рассылаем всем пользователям - и зарегистрированным, и незарегистрированным
|
||||
# Они все имеют право общаться в чате (главное - что они вошли в чат)
|
||||
return users
|
||||
|
||||
|
||||
async def broadcast_message_with_scheduler(
|
||||
message: Message,
|
||||
sender_user: Any, # User model object
|
||||
exclude_user_id: Optional[int] = None,
|
||||
admin_only: bool = False
|
||||
) -> tuple[Dict[str, int], int, int]:
|
||||
"""
|
||||
Разослать сообщение всем пользователям с планировщиком (пакетная отправка).
|
||||
Подписи формируются динамически в зависимости от получателя:
|
||||
- Админы видят: nickname (карта: XXXX)
|
||||
- Обычные пользователи видят: nickname (от пользователя) или "Админ" (от админа)
|
||||
|
||||
Args:
|
||||
message: Сообщение для рассылки
|
||||
sender_user: Объект User отправителя
|
||||
exclude_user_id: ID пользователя для исключения
|
||||
admin_only: Рассылать только админам
|
||||
|
||||
Возвращает: (forwarded_ids, success_count, fail_count)
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async with async_session_maker() as session:
|
||||
users = await get_all_active_users(session)
|
||||
|
||||
logger.info(f"[CHAT] broadcast_message_with_scheduler: всего пользователей для рассылки: {len(users)}")
|
||||
|
||||
if exclude_user_id:
|
||||
users = [u for u in users if u.telegram_id != exclude_user_id]
|
||||
logger.info(f"[CHAT] После исключения отправителя: {len(users)} пользователей")
|
||||
|
||||
# Если только для админов - фильтруем
|
||||
if admin_only:
|
||||
users = [u for u in users if u.telegram_id in ADMIN_IDS]
|
||||
logger.info(f"[CHAT] Фильтр админов: {len(users)} пользователей")
|
||||
|
||||
forwarded_ids = {}
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
|
||||
# Разбиваем на пакеты
|
||||
for i in range(0, len(users), BATCH_SIZE):
|
||||
batch = users[i:i + BATCH_SIZE]
|
||||
|
||||
# Отправляем пакет
|
||||
tasks = []
|
||||
for recipient_user in batch:
|
||||
# Формируем подпись в зависимости от получателя
|
||||
if recipient_user.telegram_id in ADMIN_IDS:
|
||||
# Админы видят полную информацию: nickname (карта: XXXX)
|
||||
sender_name = sender_user.nickname if sender_user.nickname else (
|
||||
f"@{sender_user.username}" if sender_user.username else sender_user.first_name
|
||||
)
|
||||
if sender_user.club_card_number:
|
||||
sender_name += f" (карта: {sender_user.club_card_number})"
|
||||
sender_info = sender_name
|
||||
tasks.append(_send_message_to_admin_with_sender(message, recipient_user.telegram_id, sender_info))
|
||||
else:
|
||||
# Обычные пользователи видят:
|
||||
# - "Админ" если отправитель - админ
|
||||
# - nickname если отправитель - обычный пользователь
|
||||
if sender_user.telegram_id in ADMIN_IDS:
|
||||
sender_info = "Админ"
|
||||
tasks.append(_send_message_to_user_with_sender(message, recipient_user.telegram_id, sender_info))
|
||||
else:
|
||||
sender_info = sender_user.nickname if sender_user.nickname else (
|
||||
f"@{sender_user.username}" if sender_user.username else sender_user.first_name
|
||||
)
|
||||
tasks.append(_send_message_to_user_with_sender(message, recipient_user.telegram_id, sender_info))
|
||||
|
||||
# Ждем завершения пакета
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# Обрабатываем результаты
|
||||
for user, result in zip(batch, results):
|
||||
if isinstance(result, Exception):
|
||||
fail_count += 1
|
||||
elif result is not None:
|
||||
forwarded_ids[str(user.telegram_id)] = result
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
|
||||
# Задержка между пакетами (если есть еще пакеты)
|
||||
if i + BATCH_SIZE < len(users):
|
||||
await asyncio.sleep(BATCH_DELAY)
|
||||
|
||||
logger.info(f"[CHAT] broadcast_message_with_scheduler завершена: успешно={success_count}, ошибок={fail_count}")
|
||||
return forwarded_ids, success_count, fail_count
|
||||
|
||||
|
||||
async def _send_message_to_user(message: Message, user_telegram_id: int) -> Optional[int]:
|
||||
"""
|
||||
Отправить сообщение конкретному пользователю.
|
||||
Возвращает message_id при успехе или None при ошибке.
|
||||
"""
|
||||
try:
|
||||
sent_msg = await message.copy_to(user_telegram_id)
|
||||
return sent_msg.message_id
|
||||
except Exception as e:
|
||||
print(f"Failed to send message to {user_telegram_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def _send_message_to_user_with_sender(message: Message, user_telegram_id: int, sender_info: str) -> Optional[int]:
|
||||
"""
|
||||
Отправить сообщение обычному пользователю с информацией об отправителе.
|
||||
Возвращает message_id при успехе или None при ошибке.
|
||||
"""
|
||||
try:
|
||||
# Формируем текст с информацией об отправителе
|
||||
header = f"📨 <b>{sender_info}:</b>\n\n"
|
||||
|
||||
if message.text:
|
||||
# Текстовое сообщение
|
||||
sent_msg = await message.bot.send_message(
|
||||
user_telegram_id,
|
||||
header + message.text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.photo:
|
||||
# Фото
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_photo(
|
||||
user_telegram_id,
|
||||
photo=message.photo[-1].file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.video:
|
||||
# Видео
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_video(
|
||||
user_telegram_id,
|
||||
video=message.video.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.document:
|
||||
# Документ
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_document(
|
||||
user_telegram_id,
|
||||
document=message.document.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.animation:
|
||||
# GIF
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_animation(
|
||||
user_telegram_id,
|
||||
animation=message.animation.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.sticker:
|
||||
# Стикер - сначала отправляем заголовок, потом стикер
|
||||
await message.bot.send_message(user_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.bot.send_sticker(user_telegram_id, sticker=message.sticker.file_id)
|
||||
elif message.voice:
|
||||
# Голосовое сообщение
|
||||
sent_msg = await message.bot.send_voice(
|
||||
user_telegram_id,
|
||||
voice=message.voice.file_id,
|
||||
caption=header,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.video_note:
|
||||
# Видео-кружок
|
||||
await message.bot.send_message(user_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.bot.send_video_note(user_telegram_id, video_note=message.video_note.file_id)
|
||||
else:
|
||||
# Неизвестный тип - просто копируем
|
||||
await message.bot.send_message(user_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.copy_to(user_telegram_id)
|
||||
|
||||
return sent_msg.message_id
|
||||
except Exception as e:
|
||||
print(f"Failed to send message to {user_telegram_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def _send_message_to_admin_with_sender(message: Message, admin_telegram_id: int, sender_info: str) -> Optional[int]:
|
||||
"""
|
||||
Отправить сообщение админу с информацией об отправителе.
|
||||
Возвращает message_id при успехе или None при ошибке.
|
||||
"""
|
||||
try:
|
||||
# Формируем текст с информацией об отправителе
|
||||
header = f"📨 <b>Сообщение от {sender_info}:</b>\n\n"
|
||||
|
||||
if message.text:
|
||||
# Текстовое сообщение
|
||||
sent_msg = await message.bot.send_message(
|
||||
admin_telegram_id,
|
||||
header + message.text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.photo:
|
||||
# Фото
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_photo(
|
||||
admin_telegram_id,
|
||||
photo=message.photo[-1].file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.video:
|
||||
# Видео
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_video(
|
||||
admin_telegram_id,
|
||||
video=message.video.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.document:
|
||||
# Документ
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_document(
|
||||
admin_telegram_id,
|
||||
document=message.document.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.animation:
|
||||
# GIF
|
||||
caption = header + (message.caption or "")
|
||||
sent_msg = await message.bot.send_animation(
|
||||
admin_telegram_id,
|
||||
animation=message.animation.file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.sticker:
|
||||
# Стикер - сначала отправляем заголовок, потом стикер
|
||||
await message.bot.send_message(admin_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.bot.send_sticker(admin_telegram_id, sticker=message.sticker.file_id)
|
||||
elif message.voice:
|
||||
# Голосовое сообщение
|
||||
sent_msg = await message.bot.send_voice(
|
||||
admin_telegram_id,
|
||||
voice=message.voice.file_id,
|
||||
caption=header,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
elif message.video_note:
|
||||
# Видео-кружок
|
||||
await message.bot.send_message(admin_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.bot.send_video_note(admin_telegram_id, video_note=message.video_note.file_id)
|
||||
else:
|
||||
# Неизвестный тип - просто копируем
|
||||
await message.bot.send_message(admin_telegram_id, header, parse_mode="HTML")
|
||||
sent_msg = await message.copy_to(admin_telegram_id)
|
||||
|
||||
return sent_msg.message_id
|
||||
except Exception as e:
|
||||
print(f"Failed to send message with sender info to admin {admin_telegram_id}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def forward_to_channel(message: Message, channel_id: str) -> tuple[bool, Optional[int]]:
|
||||
"""Переслать сообщение в канал/группу"""
|
||||
try:
|
||||
# Пересылаем сообщение в канал
|
||||
sent_msg = await message.forward(channel_id)
|
||||
return True, sent_msg.message_id
|
||||
except Exception as e:
|
||||
print(f"Failed to forward message to channel {channel_id}: {e}")
|
||||
return False, None
|
||||
|
||||
|
||||
|
||||
@router.message(F.photo, StateFilter(ChatStates.in_chat))
|
||||
async def handle_photo_message(message: Message, state: FSMContext):
|
||||
"""Обработчик фото"""
|
||||
|
||||
Reference in New Issue
Block a user