This commit is contained in:
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user