chat+lottery refactor
All checks were successful
continuous-integration/drone/pr Build is passing

This commit is contained in:
2026-02-11 18:40:37 +09:00
parent c0407fdb11
commit ca0c63a89c

View File

@@ -6,7 +6,7 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.filters import StateFilter, Command from aiogram.filters import StateFilter, Command
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
import asyncio import asyncio
from typing import List, Dict, Optional, Set from typing import List, Dict, Optional, Set, Any
from collections import deque from collections import deque
import time import time
@@ -130,18 +130,21 @@ async def get_all_active_users(session: AsyncSession) -> List:
async def broadcast_message_with_scheduler( async def broadcast_message_with_scheduler(
message: Message, message: Message,
sender_user: Any, # User model object
exclude_user_id: Optional[int] = None, exclude_user_id: Optional[int] = None,
admin_only: bool = False, admin_only: bool = False
sender_info: Optional[str] = None
) -> tuple[Dict[str, int], int, int]: ) -> tuple[Dict[str, int], int, int]:
""" """
Разослать сообщение всем пользователям с планировщиком (пакетная отправка). Разослать сообщение всем пользователям с планировщиком (пакетная отправка).
Подписи формируются динамически в зависимости от получателя:
- Админы видят: nickname (карта: XXXX)
- Обычные пользователи видят: nickname (от пользователя) или "Админ" (от админа)
Args: Args:
message: Сообщение для рассылки message: Сообщение для рассылки
sender_user: Объект User отправителя
exclude_user_id: ID пользователя для исключения exclude_user_id: ID пользователя для исключения
admin_only: Рассылать только админам admin_only: Рассылать только админам
sender_info: Информация об отправителе (для показа админам)
Возвращает: (forwarded_ids, success_count, fail_count) Возвращает: (forwarded_ids, success_count, fail_count)
""" """
@@ -165,12 +168,29 @@ async def broadcast_message_with_scheduler(
# Отправляем пакет # Отправляем пакет
tasks = [] tasks = []
for user in batch: for recipient_user in batch:
# Для админов добавляем информацию об отправителе, если сообщение от обычного пользователя # Формируем подпись в зависимости от получателя
if sender_info and user.telegram_id in ADMIN_IDS: if recipient_user.telegram_id in ADMIN_IDS:
tasks.append(_send_message_to_admin_with_sender(message, user.telegram_id, sender_info)) # Админы видят полную информацию: 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: else:
tasks.append(_send_message_to_user(message, user.telegram_id)) # Обычные пользователи видят:
# - "Админ" если отправитель - админ
# - 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) results = await asyncio.gather(*tasks, return_exceptions=True)
@@ -205,6 +225,85 @@ async def _send_message_to_user(message: Message, user_telegram_id: int) -> Opti
return None 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]: async def _send_message_to_admin_with_sender(message: Message, admin_telegram_id: int, sender_info: str) -> Optional[int]:
""" """
Отправить сообщение админу с информацией об отправителе. Отправить сообщение админу с информацией об отправителе.
@@ -443,20 +542,12 @@ async def handle_text_message(message: Message, state: FSMContext):
# Обрабатываем в зависимости от режима # Обрабатываем в зависимости от режима
if settings.mode == 'broadcast': if settings.mode == 'broadcast':
# Режим рассылки с планировщиком # Режим рассылки с планировщиком
# Формируем информацию об отправителе для админов (если это не админ) # Передаем объект user для динамического формирования подписей
sender_info = None
if not is_admin(message.from_user.id):
# Используем nickname, если есть, иначе fallback на username или first_name
sender_name = user.nickname if user.nickname else (f"@{user.username}" if user.username else user.first_name)
if user.club_card_number:
sender_name += f" (карта: {user.club_card_number})"
sender_info = sender_name
# ВСЕГДА исключаем отправителя - он не должен получать своё же сообщение # ВСЕГДА исключаем отправителя - он не должен получать своё же сообщение
forwarded_ids, success, fail = await broadcast_message_with_scheduler( forwarded_ids, success, fail = await broadcast_message_with_scheduler(
message, message,
exclude_user_id=message.from_user.id, sender_user=user,
sender_info=sender_info exclude_user_id=message.from_user.id
) )
# Сохраняем сообщение в историю # Сохраняем сообщение в историю
@@ -532,20 +623,11 @@ async def handle_photo_message(message: Message, state: FSMContext):
photo = message.photo[-1] photo = message.photo[-1]
if settings.mode == 'broadcast': if settings.mode == 'broadcast':
# Формируем информацию об отправителе для админов (если это не админ)
sender_info = None
if not is_admin(message.from_user.id):
# Используем nickname, если есть, иначе fallback на username или first_name
sender_name = user.nickname if user.nickname else (f"@{user.username}" if user.username else user.first_name)
if user.club_card_number:
sender_name += f" (карта: {user.club_card_number})"
sender_info = sender_name
# Рассылаем фото - ВСЕГДА исключаем отправителя # Рассылаем фото - ВСЕГДА исключаем отправителя
forwarded_ids, success, fail = await broadcast_message_with_scheduler( forwarded_ids, success, fail = await broadcast_message_with_scheduler(
message, message,
exclude_user_id=message.from_user.id, sender_user=user,
sender_info=sender_info exclude_user_id=message.from_user.id
) )
await ChatMessageService.save_message( await ChatMessageService.save_message(
@@ -607,19 +689,11 @@ async def handle_video_message(message: Message, state: FSMContext):
) )
if settings.mode == 'broadcast': if settings.mode == 'broadcast':
# Формируем информацию об отправителе для админов (если это не админ) # Рассылаем видео
sender_info = None
if not is_admin(message.from_user.id):
# Используем nickname, если есть, иначе fallback на username или first_name
sender_name = user.nickname if user.nickname else (f"@{user.username}" if user.username else user.first_name)
if user.club_card_number:
sender_name += f" (карта: {user.club_card_number})"
sender_info = sender_name
forwarded_ids, success, fail = await broadcast_message_with_scheduler( forwarded_ids, success, fail = await broadcast_message_with_scheduler(
message, message,
exclude_user_id=message.from_user.id, sender_user=user,
sender_info=sender_info exclude_user_id=message.from_user.id
) )
await ChatMessageService.save_message( await ChatMessageService.save_message(
@@ -681,19 +755,11 @@ async def handle_document_message(message: Message, state: FSMContext):
) )
if settings.mode == 'broadcast': if settings.mode == 'broadcast':
# Формируем информацию об отправителе для админов (если это не админ) # Рассылаем документ
sender_info = None
if not is_admin(message.from_user.id):
# Используем nickname, если есть, иначе fallback на username или first_name
sender_name = user.nickname if user.nickname else (f"@{user.username}" if user.username else user.first_name)
if user.club_card_number:
sender_name += f" (карта: {user.club_card_number})"
sender_info = sender_name
forwarded_ids, success, fail = await broadcast_message_with_scheduler( forwarded_ids, success, fail = await broadcast_message_with_scheduler(
message, message,
exclude_user_id=message.from_user.id, sender_user=user,
sender_info=sender_info exclude_user_id=message.from_user.id
) )
await ChatMessageService.save_message( await ChatMessageService.save_message(
@@ -755,19 +821,11 @@ async def handle_animation_message(message: Message, state: FSMContext):
) )
if settings.mode == 'broadcast': if settings.mode == 'broadcast':
# Формируем информацию об отправителе для админов (если это не админ) # Рассылаем анимацию
sender_info = None
if not is_admin(message.from_user.id):
# Используем nickname, если есть, иначе fallback на username или first_name
sender_name = user.nickname if user.nickname else (f"@{user.username}" if user.username else user.first_name)
if user.club_card_number:
sender_name += f" (карта: {user.club_card_number})"
sender_info = sender_name
forwarded_ids, success, fail = await broadcast_message_with_scheduler( forwarded_ids, success, fail = await broadcast_message_with_scheduler(
message, message,
exclude_user_id=message.from_user.id, sender_user=user,
sender_info=sender_info exclude_user_id=message.from_user.id
) )
await ChatMessageService.save_message( await ChatMessageService.save_message(
@@ -829,19 +887,11 @@ async def handle_sticker_message(message: Message, state: FSMContext):
) )
if settings.mode == 'broadcast': if settings.mode == 'broadcast':
# Формируем информацию об отправителе для админов (если это не админ) # Рассылаем стикер
sender_info = None
if not is_admin(message.from_user.id):
# Используем nickname, если есть, иначе fallback на username или first_name
sender_name = user.nickname if user.nickname else (f"@{user.username}" if user.username else user.first_name)
if user.club_card_number:
sender_name += f" (карта: {user.club_card_number})"
sender_info = sender_name
forwarded_ids, success, fail = await broadcast_message_with_scheduler( forwarded_ids, success, fail = await broadcast_message_with_scheduler(
message, message,
exclude_user_id=message.from_user.id, sender_user=user,
sender_info=sender_info exclude_user_id=message.from_user.id
) )
await ChatMessageService.save_message( await ChatMessageService.save_message(