All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #7
1013 lines
44 KiB
Python
1013 lines
44 KiB
Python
"""Обработчики пользовательских сообщений в чате"""
|
||
from aiogram import Router, F
|
||
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
|
||
from aiogram.fsm.context import FSMContext
|
||
from aiogram.fsm.state import State, StatesGroup
|
||
from aiogram.filters import StateFilter, Command
|
||
|
||
from src.filters.case_insensitive import CaseInsensitiveCommand
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
import asyncio
|
||
from typing import List, Dict, Optional, Set, Any
|
||
from collections import deque
|
||
import time
|
||
|
||
from src.core.chat_services import (
|
||
ChatSettingsService,
|
||
ChatPermissionService,
|
||
ChatMessageService,
|
||
BanService
|
||
)
|
||
from src.core.services import UserService
|
||
from src.core.database import async_session_maker
|
||
from src.core.config import ADMIN_IDS
|
||
from src.utils.account_utils import parse_accounts_from_message
|
||
|
||
|
||
class ChatStates(StatesGroup):
|
||
"""Состояния для работы в чате"""
|
||
in_chat = State() # Пользователь находится в режиме чата
|
||
|
||
|
||
def is_admin(user_id: int) -> bool:
|
||
"""Проверка является ли пользователь админом"""
|
||
return user_id in ADMIN_IDS
|
||
|
||
|
||
def _contains_account_numbers(text: str) -> bool:
|
||
"""Проверка содержит ли текст номера счетов"""
|
||
if not text:
|
||
return False
|
||
accounts = parse_accounts_from_message(text)
|
||
return len(accounts) > 0
|
||
|
||
|
||
router = Router(name='chat_router')
|
||
|
||
|
||
@router.message(CaseInsensitiveCommand("chat"))
|
||
async def enter_chat_command(message: Message, state: FSMContext):
|
||
"""Войти в режим чата через команду /chat (регистронезависимо)"""
|
||
await enter_chat(message, state)
|
||
|
||
|
||
@router.callback_query(F.data == "enter_chat")
|
||
async def enter_chat_callback(callback: CallbackQuery, state: FSMContext):
|
||
"""Войти в режим чата через кнопку"""
|
||
await callback.answer()
|
||
await enter_chat(callback.message, state)
|
||
|
||
|
||
async def enter_chat(message: Message, state: FSMContext):
|
||
"""Общая функция входа в чат"""
|
||
from src.utils.keyboards import get_chat_reply_keyboard
|
||
|
||
await state.set_state(ChatStates.in_chat)
|
||
|
||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||
[InlineKeyboardButton(text="🚪 Выйти из чата", callback_data="exit_chat")],
|
||
[InlineKeyboardButton(text="🏠 Главная", callback_data="back_to_main")]
|
||
])
|
||
|
||
# Обычная клавиатура для чата
|
||
reply_keyboard = get_chat_reply_keyboard()
|
||
|
||
await message.answer(
|
||
"💬 <b>Вы вошли в режим чата</b>\n\n"
|
||
"Теперь все ваши сообщения будут рассылаться участникам.\n"
|
||
"Вы можете отправлять текст, фото, видео, документы и стикеры.\n\n"
|
||
"Для выхода нажмите кнопку ниже или отправьте /exit",
|
||
reply_markup=reply_keyboard, # Обычная клавиатура
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
# Inline клавиатура отдельным сообщением
|
||
await message.answer(
|
||
"Выберите действие:",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
|
||
@router.message(CaseInsensitiveCommand("exit"), StateFilter(ChatStates.in_chat))
|
||
async def exit_chat_command(message: Message, state: FSMContext):
|
||
"""Выйти из режима чата через команду /exit (регистронезависимо)"""
|
||
await exit_chat(message, state)
|
||
|
||
|
||
@router.callback_query(F.data == "exit_chat", StateFilter(ChatStates.in_chat))
|
||
async def exit_chat_callback(callback: CallbackQuery, state: FSMContext):
|
||
"""Выйти из режима чата через кнопку"""
|
||
await callback.answer()
|
||
await exit_chat(callback.message, state)
|
||
|
||
|
||
async def exit_chat(message: Message, state: FSMContext):
|
||
"""Общая функция выхода из чата"""
|
||
from src.utils.keyboards import get_main_reply_keyboard
|
||
from src.core.config import ADMIN_IDS
|
||
from src.core.services import UserService
|
||
from src.core.database import async_session_maker
|
||
|
||
await state.clear()
|
||
|
||
# Получаем информацию о пользователе
|
||
async with async_session_maker() as session:
|
||
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
|
||
is_registered = user.is_registered if user else False
|
||
is_admin_user = message.from_user.id in ADMIN_IDS
|
||
|
||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||
[InlineKeyboardButton(text="💬 Войти в чат", callback_data="enter_chat")],
|
||
[InlineKeyboardButton(text="🏠 Главная", callback_data="back_to_main")]
|
||
])
|
||
|
||
# Обычная клавиатура
|
||
reply_keyboard = get_main_reply_keyboard(is_admin=is_admin_user, is_registered=is_registered)
|
||
|
||
await message.answer(
|
||
"✅ <b>Вы вышли из режима чата</b>\n\n"
|
||
"Ваши сообщения больше не будут рассылаться.",
|
||
reply_markup=reply_keyboard, # Обычная клавиатура
|
||
parse_mode="HTML"
|
||
)
|
||
|
||
# Inline клавиатура отдельным сообщением
|
||
await message.answer(
|
||
"Выберите действие:",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
|
||
@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()
|
||
|
||
# Проверяем ключевые слова для выхода
|
||
exit_keywords = ['/start', 'start', 'старт', '/exit']
|
||
|
||
if text in exit_keywords:
|
||
if text in ['/start', 'start', 'старт']:
|
||
# Выходим из чата и показываем главное меню
|
||
await state.clear()
|
||
|
||
from src.components.ui import UserUI
|
||
keyboard = UserUI.get_main_menu_keyboard(message.from_user.id)
|
||
|
||
await message.answer(
|
||
"🏠 <b>Главное меню</b>\n\n"
|
||
"Вы вышли из режима чата.",
|
||
reply_markup=keyboard,
|
||
parse_mode="HTML"
|
||
)
|
||
return # Не обрабатываем дальше
|
||
else:
|
||
# Для /exit просто выходим
|
||
await exit_chat(message, state)
|
||
return
|
||
|
||
# ===== ОБРАБОТКА ОБЫЧНОГО СООБЩЕНИЯ ЧАТА =====
|
||
# Защита от дубликатов - если сообщение уже обработано, пропускаем
|
||
if _is_message_processed(message.message_id):
|
||
logger.warning(f"[CHAT] Дубликат сообщения {message.message_id}, пропускаем")
|
||
return
|
||
|
||
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)
|
||
if is_admin(message.from_user.id) and message.text and not message.text.startswith('/'):
|
||
if _contains_account_numbers(message.text):
|
||
logger.info(f"[CHAT] Обнаружены счета от админа, пропускаем - account_router обработает")
|
||
# Не делаем return, выбрасываем исключение для пропуска в следующий обработчик
|
||
from aiogram.handlers import SkipHandler
|
||
raise SkipHandler()
|
||
|
||
# БЫСТРОЕ УДАЛЕНИЕ: Если админ отвечает на сообщение словом "удалить"/"del"/"-"
|
||
if message.reply_to_message and is_admin(message.from_user.id):
|
||
if message.text and message.text.lower().strip() in ['удалить', 'del', '-']:
|
||
async with async_session_maker() as session:
|
||
# Ищем сообщение в БД по telegram_message_id
|
||
msg_to_delete = await ChatMessageService.get_message_by_telegram_id(
|
||
session,
|
||
telegram_message_id=message.reply_to_message.message_id
|
||
)
|
||
|
||
if msg_to_delete:
|
||
# Получаем админа
|
||
admin = await UserService.get_or_create_user(
|
||
session,
|
||
message.from_user.id,
|
||
username=message.from_user.username,
|
||
first_name=message.from_user.first_name,
|
||
last_name=message.from_user.last_name
|
||
)
|
||
|
||
# Помечаем как удаленное
|
||
success = await ChatMessageService.mark_as_deleted(
|
||
session,
|
||
msg_to_delete.id,
|
||
admin.id if admin else None
|
||
)
|
||
|
||
if success:
|
||
# Удаляем у всех получателей
|
||
deleted_count = 0
|
||
if msg_to_delete.forwarded_message_ids:
|
||
for user_tg_id, tg_msg_id in msg_to_delete.forwarded_message_ids.items():
|
||
try:
|
||
await message.bot.delete_message(
|
||
chat_id=int(user_tg_id),
|
||
message_id=tg_msg_id
|
||
)
|
||
deleted_count += 1
|
||
except Exception as e:
|
||
logger.warning(f"Не удалось удалить {tg_msg_id} у {user_tg_id}: {e}")
|
||
|
||
# Удаляем оригинал у отправителя
|
||
try:
|
||
await message.bot.delete_message(
|
||
chat_id=msg_to_delete.sender.telegram_id,
|
||
message_id=msg_to_delete.telegram_message_id
|
||
)
|
||
deleted_count += 1
|
||
except Exception as e:
|
||
logger.warning(f"Не удалось удалить оригинал: {e}")
|
||
|
||
# Удаляем команду админа
|
||
try:
|
||
await message.delete()
|
||
except:
|
||
pass
|
||
|
||
# Отправляем уведомление (самоудаляющееся)
|
||
notification = await message.answer(f"✅ Сообщение удалено у {deleted_count} получателей")
|
||
await asyncio.sleep(3)
|
||
try:
|
||
await notification.delete()
|
||
except:
|
||
pass
|
||
|
||
return
|
||
else:
|
||
await message.answer("❌ Сообщение не найдено в БД")
|
||
return
|
||
|
||
# Проверяем является ли это командой
|
||
if message.text and message.text.startswith('/'):
|
||
# Список команд, которые НЕ нужно пересылать
|
||
# (Базовые команды /start, /help уже обработаны раньше в main.py)
|
||
user_commands = ['/my_code', '/my_accounts']
|
||
admin_commands = [
|
||
'/add_account', '/remove_account', '/verify_winner', '/winner_status', '/user_info',
|
||
'/check_unclaimed', '/redraw',
|
||
'/chat_mode', '/set_forward', '/global_ban', '/ban', '/unban', '/banlist', '/delete_msg', '/chat_stats'
|
||
]
|
||
|
||
# Извлекаем команду (первое слово)
|
||
command = message.text.split()[0] if message.text else ''
|
||
|
||
# ИЗМЕНЕНИЕ: Если это команда от АДМИНА - не пересылаем (админ сам её видит)
|
||
if is_admin(message.from_user.id):
|
||
# Если это админская команда - пропускаем, она будет обработана другими обработчиками
|
||
if command in admin_commands:
|
||
return
|
||
# Если это пользовательская команда от админа - тоже пропускаем
|
||
if command in user_commands:
|
||
return
|
||
# Любая другая команда от админа - тоже не пересылаем
|
||
return
|
||
|
||
# ИЗМЕНЕНИЕ: Если команда от обычного пользователя - ПЕРЕСЫЛАЕМ админу
|
||
# Чтобы админ видел, что пользователь отправил /start или другую команду
|
||
# НЕ делаем return, продолжаем выполнение для пересылки
|
||
|
||
async with async_session_maker() as session:
|
||
# Проверяем права на отправку
|
||
can_send, reason = await ChatPermissionService.can_send_message(
|
||
session,
|
||
message.from_user.id,
|
||
is_admin=is_admin(message.from_user.id)
|
||
)
|
||
|
||
if not can_send:
|
||
await message.answer(f"❌ {reason}")
|
||
return
|
||
|
||
# Получаем настройки чата
|
||
settings = await ChatSettingsService.get_or_create_settings(session)
|
||
|
||
# Получаем или создаем пользователя
|
||
user = await UserService.get_or_create_user(
|
||
session,
|
||
message.from_user.id,
|
||
username=message.from_user.username,
|
||
first_name=message.from_user.first_name,
|
||
last_name=message.from_user.last_name
|
||
)
|
||
|
||
# Обрабатываем в зависимости от режима
|
||
if settings.mode == 'broadcast':
|
||
# Режим рассылки с планировщиком
|
||
# Передаем объект user для динамического формирования подписей
|
||
# ВСЕГДА исключаем отправителя - он не должен получать своё же сообщение
|
||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||
message,
|
||
sender_user=user,
|
||
exclude_user_id=message.from_user.id
|
||
)
|
||
|
||
# Сохраняем сообщение в историю
|
||
await ChatMessageService.save_message(
|
||
session,
|
||
user_id=user.id,
|
||
telegram_message_id=message.message_id,
|
||
message_type='text',
|
||
text=message.text,
|
||
forwarded_ids=forwarded_ids
|
||
)
|
||
|
||
# Показываем статистику доставки только админам
|
||
if is_admin(message.from_user.id):
|
||
await message.answer(
|
||
f"✅ Сообщение разослано!\n"
|
||
f"📤 Доставлено: {success}\n"
|
||
f"❌ Не доставлено: {fail}"
|
||
)
|
||
|
||
elif settings.mode == 'forward':
|
||
# Режим пересылки в канал
|
||
if not settings.forward_chat_id:
|
||
await message.answer("❌ Канал для пересылки не настроен")
|
||
return
|
||
|
||
success, channel_msg_id = await forward_to_channel(message, settings.forward_chat_id)
|
||
|
||
if success:
|
||
# Сохраняем сообщение в историю
|
||
await ChatMessageService.save_message(
|
||
session,
|
||
user_id=user.id,
|
||
telegram_message_id=message.message_id,
|
||
message_type='text',
|
||
text=message.text,
|
||
forwarded_ids={'channel': channel_msg_id} if channel_msg_id else None
|
||
)
|
||
|
||
await message.answer("✅ Сообщение переслано в канал")
|
||
else:
|
||
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):
|
||
"""Обработчик фото"""
|
||
# Защита от дубликатов
|
||
if _is_message_processed(message.message_id):
|
||
return
|
||
|
||
async with async_session_maker() as session:
|
||
can_send, reason = await ChatPermissionService.can_send_message(
|
||
session,
|
||
message.from_user.id,
|
||
is_admin=is_admin(message.from_user.id)
|
||
)
|
||
|
||
if not can_send:
|
||
await message.answer(f"❌ {reason}")
|
||
return
|
||
|
||
settings = await ChatSettingsService.get_or_create_settings(session)
|
||
user = await UserService.get_or_create_user(
|
||
session,
|
||
message.from_user.id,
|
||
username=message.from_user.username,
|
||
first_name=message.from_user.first_name,
|
||
last_name=message.from_user.last_name
|
||
)
|
||
|
||
# Получаем file_id самого большого фото
|
||
photo = message.photo[-1]
|
||
|
||
if settings.mode == 'broadcast':
|
||
# Рассылаем фото - ВСЕГДА исключаем отправителя
|
||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||
message,
|
||
sender_user=user,
|
||
exclude_user_id=message.from_user.id
|
||
)
|
||
|
||
await ChatMessageService.save_message(
|
||
session,
|
||
user_id=user.id,
|
||
telegram_message_id=message.message_id,
|
||
message_type='photo',
|
||
text=message.caption,
|
||
file_id=photo.file_id,
|
||
forwarded_ids=forwarded_ids
|
||
)
|
||
|
||
# Показываем статистику только админам
|
||
if is_admin(message.from_user.id):
|
||
await message.answer(f"✅ Фото разослано: {success} получателей")
|
||
|
||
elif settings.mode == 'forward':
|
||
if settings.forward_chat_id:
|
||
success, channel_msg_id = await forward_to_channel(message, settings.forward_chat_id)
|
||
|
||
if success:
|
||
await ChatMessageService.save_message(
|
||
session,
|
||
user_id=user.id,
|
||
telegram_message_id=message.message_id,
|
||
message_type='photo',
|
||
text=message.caption,
|
||
file_id=photo.file_id,
|
||
forwarded_ids={'channel': channel_msg_id} if channel_msg_id else None
|
||
)
|
||
await message.answer("✅ Фото переслано в канал")
|
||
|
||
|
||
@router.message(F.video, StateFilter(ChatStates.in_chat))
|
||
async def handle_video_message(message: Message, state: FSMContext):
|
||
"""Обработчик видео"""
|
||
# Защита от дубликатов
|
||
if _is_message_processed(message.message_id):
|
||
return
|
||
|
||
async with async_session_maker() as session:
|
||
can_send, reason = await ChatPermissionService.can_send_message(
|
||
session,
|
||
message.from_user.id,
|
||
is_admin=is_admin(message.from_user.id)
|
||
)
|
||
|
||
if not can_send:
|
||
await message.answer(f"❌ {reason}")
|
||
return
|
||
|
||
settings = await ChatSettingsService.get_or_create_settings(session)
|
||
user = await UserService.get_or_create_user(
|
||
session,
|
||
message.from_user.id,
|
||
username=message.from_user.username,
|
||
first_name=message.from_user.first_name,
|
||
last_name=message.from_user.last_name
|
||
)
|
||
|
||
if settings.mode == 'broadcast':
|
||
# Рассылаем видео
|
||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||
message,
|
||
sender_user=user,
|
||
exclude_user_id=message.from_user.id
|
||
)
|
||
|
||
await ChatMessageService.save_message(
|
||
session,
|
||
user_id=user.id,
|
||
telegram_message_id=message.message_id,
|
||
message_type='video',
|
||
text=message.caption,
|
||
file_id=message.video.file_id,
|
||
forwarded_ids=forwarded_ids
|
||
)
|
||
|
||
# Показываем статистику только админам
|
||
if is_admin(message.from_user.id):
|
||
await message.answer(f"✅ Видео разослано: {success} получателей")
|
||
|
||
elif settings.mode == 'forward':
|
||
if settings.forward_chat_id:
|
||
success, channel_msg_id = await forward_to_channel(message, settings.forward_chat_id)
|
||
|
||
if success:
|
||
await ChatMessageService.save_message(
|
||
session,
|
||
user_id=user.id,
|
||
telegram_message_id=message.message_id,
|
||
message_type='video',
|
||
text=message.caption,
|
||
file_id=message.video.file_id,
|
||
forwarded_ids={'channel': channel_msg_id} if channel_msg_id else None
|
||
)
|
||
await message.answer("✅ Видео переслано в канал")
|
||
|
||
|
||
@router.message(F.document, StateFilter(ChatStates.in_chat))
|
||
async def handle_document_message(message: Message, state: FSMContext):
|
||
"""Обработчик документов"""
|
||
# Защита от дубликатов
|
||
if _is_message_processed(message.message_id):
|
||
return
|
||
|
||
async with async_session_maker() as session:
|
||
can_send, reason = await ChatPermissionService.can_send_message(
|
||
session,
|
||
message.from_user.id,
|
||
is_admin=is_admin(message.from_user.id)
|
||
)
|
||
|
||
if not can_send:
|
||
await message.answer(f"❌ {reason}")
|
||
return
|
||
|
||
settings = await ChatSettingsService.get_or_create_settings(session)
|
||
user = await UserService.get_or_create_user(
|
||
session,
|
||
message.from_user.id,
|
||
username=message.from_user.username,
|
||
first_name=message.from_user.first_name,
|
||
last_name=message.from_user.last_name
|
||
)
|
||
|
||
if settings.mode == 'broadcast':
|
||
# Рассылаем документ
|
||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||
message,
|
||
sender_user=user,
|
||
exclude_user_id=message.from_user.id
|
||
)
|
||
|
||
await ChatMessageService.save_message(
|
||
session,
|
||
user_id=user.id,
|
||
telegram_message_id=message.message_id,
|
||
message_type='document',
|
||
text=message.caption,
|
||
file_id=message.document.file_id,
|
||
forwarded_ids=forwarded_ids
|
||
)
|
||
|
||
# Показываем статистику только админам
|
||
if is_admin(message.from_user.id):
|
||
await message.answer(f"✅ Документ разослан: {success} получателей")
|
||
|
||
elif settings.mode == 'forward':
|
||
if settings.forward_chat_id:
|
||
success, channel_msg_id = await forward_to_channel(message, settings.forward_chat_id)
|
||
|
||
if success:
|
||
await ChatMessageService.save_message(
|
||
session,
|
||
user_id=user.id,
|
||
telegram_message_id=message.message_id,
|
||
message_type='document',
|
||
text=message.caption,
|
||
file_id=message.document.file_id,
|
||
forwarded_ids={'channel': channel_msg_id} if channel_msg_id else None
|
||
)
|
||
await message.answer("✅ Документ переслан в канал")
|
||
|
||
|
||
@router.message(F.animation, StateFilter(ChatStates.in_chat))
|
||
async def handle_animation_message(message: Message, state: FSMContext):
|
||
"""Обработчик GIF анимаций"""
|
||
# Защита от дубликатов
|
||
if _is_message_processed(message.message_id):
|
||
return
|
||
|
||
async with async_session_maker() as session:
|
||
can_send, reason = await ChatPermissionService.can_send_message(
|
||
session,
|
||
message.from_user.id,
|
||
is_admin=is_admin(message.from_user.id)
|
||
)
|
||
|
||
if not can_send:
|
||
await message.answer(f"❌ {reason}")
|
||
return
|
||
|
||
settings = await ChatSettingsService.get_or_create_settings(session)
|
||
user = await UserService.get_or_create_user(
|
||
session,
|
||
message.from_user.id,
|
||
username=message.from_user.username,
|
||
first_name=message.from_user.first_name,
|
||
last_name=message.from_user.last_name
|
||
)
|
||
|
||
if settings.mode == 'broadcast':
|
||
# Рассылаем анимацию
|
||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||
message,
|
||
sender_user=user,
|
||
exclude_user_id=message.from_user.id
|
||
)
|
||
|
||
await ChatMessageService.save_message(
|
||
session,
|
||
user_id=user.id,
|
||
telegram_message_id=message.message_id,
|
||
message_type='animation',
|
||
text=message.caption,
|
||
file_id=message.animation.file_id,
|
||
forwarded_ids=forwarded_ids
|
||
)
|
||
|
||
# Показываем статистику только админам
|
||
if is_admin(message.from_user.id):
|
||
await message.answer(f"✅ Анимация разослана: {success} получателей")
|
||
|
||
elif settings.mode == 'forward':
|
||
if settings.forward_chat_id:
|
||
success, channel_msg_id = await forward_to_channel(message, settings.forward_chat_id)
|
||
|
||
if success:
|
||
await ChatMessageService.save_message(
|
||
session,
|
||
user_id=user.id,
|
||
telegram_message_id=message.message_id,
|
||
message_type='animation',
|
||
text=message.caption,
|
||
file_id=message.animation.file_id,
|
||
forwarded_ids={'channel': channel_msg_id} if channel_msg_id else None
|
||
)
|
||
await message.answer("✅ Анимация переслана в канал")
|
||
|
||
|
||
@router.message(F.sticker, StateFilter(ChatStates.in_chat))
|
||
async def handle_sticker_message(message: Message, state: FSMContext):
|
||
"""Обработчик стикеров"""
|
||
# Защита от дубликатов
|
||
if _is_message_processed(message.message_id):
|
||
return
|
||
|
||
async with async_session_maker() as session:
|
||
can_send, reason = await ChatPermissionService.can_send_message(
|
||
session,
|
||
message.from_user.id,
|
||
is_admin=is_admin(message.from_user.id)
|
||
)
|
||
|
||
if not can_send:
|
||
await message.answer(f"❌ {reason}")
|
||
return
|
||
|
||
settings = await ChatSettingsService.get_or_create_settings(session)
|
||
user = await UserService.get_or_create_user(
|
||
session,
|
||
message.from_user.id,
|
||
username=message.from_user.username,
|
||
first_name=message.from_user.first_name,
|
||
last_name=message.from_user.last_name
|
||
)
|
||
|
||
if settings.mode == 'broadcast':
|
||
# Рассылаем стикер
|
||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||
message,
|
||
sender_user=user,
|
||
exclude_user_id=message.from_user.id
|
||
)
|
||
|
||
await ChatMessageService.save_message(
|
||
session,
|
||
user_id=user.id,
|
||
telegram_message_id=message.message_id,
|
||
message_type='sticker',
|
||
file_id=message.sticker.file_id,
|
||
forwarded_ids=forwarded_ids
|
||
)
|
||
|
||
# Показываем статистику только админам
|
||
if is_admin(message.from_user.id):
|
||
await message.answer(f"✅ Стикер разослан: {success} получателей")
|
||
|
||
elif settings.mode == 'forward':
|
||
if settings.forward_chat_id:
|
||
success, channel_msg_id = await forward_to_channel(message, settings.forward_chat_id)
|
||
|
||
if success:
|
||
await ChatMessageService.save_message(
|
||
session,
|
||
user_id=user.id,
|
||
telegram_message_id=message.message_id,
|
||
message_type='sticker',
|
||
file_id=message.sticker.file_id,
|
||
forwarded_ids={'channel': channel_msg_id} if channel_msg_id else None
|
||
)
|
||
await message.answer("✅ Стикер переслан в канал")
|
||
|
||
|
||
@router.message(F.voice)
|
||
async def handle_voice_message(message: Message):
|
||
"""Обработчик голосовых сообщений - ЗАБЛОКИРОВАНО"""
|
||
await message.answer(
|
||
"🚫 Голосовые сообщения запрещены.\n\n"
|
||
"Пожалуйста, используйте текстовые сообщения или изображения."
|
||
)
|
||
return
|
||
|
||
|
||
@router.message(F.audio)
|
||
async def handle_audio_message(message: Message):
|
||
"""Обработчик аудиофайлов (музыка, аудиозаписи) - ЗАБЛОКИРОВАНО"""
|
||
await message.answer(
|
||
"🚫 Аудиофайлы запрещены.\n\n"
|
||
"Пожалуйста, используйте текстовые сообщения или изображения."
|
||
)
|
||
return
|