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