chat restore
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-02-09 20:07:46 +09:00
parent 062b782fb7
commit 4e2c8981c2
8 changed files with 485 additions and 56 deletions

View File

@@ -1,6 +1,9 @@
"""Обработчики пользовательских сообщений в чате"""
from aiogram import Router, F
from aiogram.types import Message
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 sqlalchemy.ext.asyncio import AsyncSession
import asyncio
from typing import List, Dict, Optional, Set
@@ -19,6 +22,11 @@ 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
@@ -34,6 +42,69 @@ def _contains_account_numbers(text: str) -> bool:
router = Router(name='chat_router')
@router.message(Command("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):
"""Общая функция входа в чат"""
await state.set_state(ChatStates.in_chat)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🚪 Выйти из чата", callback_data="exit_chat")],
[InlineKeyboardButton(text="🏠 В главное меню", callback_data="back_to_main")]
])
await message.answer(
"💬 <b>Вы вошли в режим чата</b>\n\n"
"Теперь все ваши сообщения будут рассылаться участникам.\n"
"Вы можете отправлять текст, фото, видео, документы и стикеры.\n\n"
"Для выхода нажмите кнопку ниже или отправьте /exit",
reply_markup=keyboard,
parse_mode="HTML"
)
@router.message(Command("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):
"""Общая функция выхода из чата"""
await state.clear()
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💬 Войти в чат", callback_data="enter_chat")],
[InlineKeyboardButton(text="🏠 В главное меню", callback_data="back_to_main")]
])
await message.answer(
"✅ <b>Вы вышли из режима чата</b>\n\n"
"Ваши сообщения больше не будут рассылаться.",
reply_markup=keyboard,
parse_mode="HTML"
)
# Настройки для планировщика рассылки
BATCH_SIZE = 20 # Количество сообщений в пакете
BATCH_DELAY = 1.0 # Задержка между пакетами в секундах
@@ -224,8 +295,8 @@ async def forward_to_channel(message: Message, channel_id: str) -> tuple[bool, O
return False, None
@router.message(F.text)
async def handle_text_message(message: Message):
@router.message(F.text, StateFilter(ChatStates.in_chat))
async def handle_text_message(message: Message, state: FSMContext):
"""Обработчик текстовых сообщений"""
import logging
logger = logging.getLogger(__name__)
@@ -238,11 +309,13 @@ async def handle_text_message(message: Message):
logger.info(f"[CHAT] handle_text_message вызван: user={message.from_user.id}, text={message.text[:50] if message.text else 'None'}")
# ПРОВЕРКА СЧЕТОВ: Если админ отправил сообщение с номерами счетов - НЕ рассылаем
# Это сообщение будет обработано account_router для добавления в розыгрыш
# Пропускаем для 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 # Пропускаем - обработает account_router
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):
@@ -256,9 +329,12 @@ async def handle_text_message(message: Message):
if msg_to_delete:
# Получаем админа
admin = await UserService.get_user_by_telegram_id(
admin = await UserService.get_or_create_user(
session,
message.from_user.id
message.from_user.id,
username=message.from_user.username,
first_name=message.from_user.first_name,
last_name=message.from_user.last_name
)
# Помечаем как удаленное
@@ -355,11 +431,14 @@ async def handle_text_message(message: Message):
# Получаем настройки чата
settings = await ChatSettingsService.get_or_create_settings(session)
# Получаем пользователя
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
await message.answer("❌ Пользователь не найден")
return
# Получаем или создаем пользователя
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':
@@ -421,8 +500,8 @@ async def handle_text_message(message: Message):
await message.answer("Не удалось переслать сообщение")
@router.message(F.photo)
async def handle_photo_message(message: Message):
@router.message(F.photo, StateFilter(ChatStates.in_chat))
async def handle_photo_message(message: Message, state: FSMContext):
"""Обработчик фото"""
# Защита от дубликатов
if _is_message_processed(message.message_id):
@@ -440,10 +519,13 @@ async def handle_photo_message(message: Message):
return
settings = await ChatSettingsService.get_or_create_settings(session)
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
return
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]
@@ -495,8 +577,8 @@ async def handle_photo_message(message: Message):
await message.answer("✅ Фото переслано в канал")
@router.message(F.video)
async def handle_video_message(message: Message):
@router.message(F.video, StateFilter(ChatStates.in_chat))
async def handle_video_message(message: Message, state: FSMContext):
"""Обработчик видео"""
# Защита от дубликатов
if _is_message_processed(message.message_id):
@@ -514,10 +596,13 @@ async def handle_video_message(message: Message):
return
settings = await ChatSettingsService.get_or_create_settings(session)
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
return
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':
# Формируем информацию об отправителе для админов (если это не админ)
@@ -565,8 +650,8 @@ async def handle_video_message(message: Message):
await message.answer("✅ Видео переслано в канал")
@router.message(F.document)
async def handle_document_message(message: Message):
@router.message(F.document, StateFilter(ChatStates.in_chat))
async def handle_document_message(message: Message, state: FSMContext):
"""Обработчик документов"""
# Защита от дубликатов
if _is_message_processed(message.message_id):
@@ -584,10 +669,13 @@ async def handle_document_message(message: Message):
return
settings = await ChatSettingsService.get_or_create_settings(session)
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
return
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':
# Формируем информацию об отправителе для админов (если это не админ)
@@ -635,8 +723,8 @@ async def handle_document_message(message: Message):
await message.answer("✅ Документ переслан в канал")
@router.message(F.animation)
async def handle_animation_message(message: Message):
@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):
@@ -654,10 +742,13 @@ async def handle_animation_message(message: Message):
return
settings = await ChatSettingsService.get_or_create_settings(session)
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
return
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':
# Формируем информацию об отправителе для админов (если это не админ)
@@ -705,8 +796,8 @@ async def handle_animation_message(message: Message):
await message.answer("✅ Анимация переслана в канал")
@router.message(F.sticker)
async def handle_sticker_message(message: Message):
@router.message(F.sticker, StateFilter(ChatStates.in_chat))
async def handle_sticker_message(message: Message, state: FSMContext):
"""Обработчик стикеров"""
# Защита от дубликатов
if _is_message_processed(message.message_id):
@@ -724,10 +815,13 @@ async def handle_sticker_message(message: Message):
return
settings = await ChatSettingsService.get_or_create_settings(session)
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
return
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':
# Формируем информацию об отправителе для админов (если это не админ)