diff --git a/CHAT_FIX_REPORT.md b/CHAT_FIX_REPORT.md
new file mode 100644
index 0000000..2e28fb7
--- /dev/null
+++ b/CHAT_FIX_REPORT.md
@@ -0,0 +1,65 @@
+# ОТЧЕТ: Исправление проблемы с чатом (17.02.2026)
+
+## Проблема
+Сообщения в чате не отправлялись другим участникам.
+
+## Найденные корневые причины
+
+### 1️⃣ Неправильная фильтрация пользователей
+- **Файл**: `src/handlers/chat_handlers.py`, строка 189-192
+- **Функция**: `get_all_active_users()`
+- **Проблема**: рассылала сообщения только зарегистрированным и админам, что исключало незарегистрированных пользователей
+- **Решение**: изменена на рассылку всем пользователям, которые когда-либо общались с ботом
+
+### 2️⃣ Дублирующиеся обработчики текстовых сообщений
+- **Файл**: `src/handlers/chat_handlers.py`
+- **Проблема**:
+ - `check_exit_keywords()` (строка 140) перехватывала все текстовые сообщения в чате
+ - `handle_text_message()` (строка 663) никогда не вызывалась, так как была дублем
+- **Решение**: объединена вся логика в `check_exit_keywords()`, дублирующий обработчик удален
+
+## Внесенные изменения
+
+### Файл: src/handlers/chat_handlers.py
+
+#### Изменение 1: Функция `get_all_active_users()` (строка 189-192)
+```python
+# ДО (неправильно)
+return [u for u in users if u.is_registered or u.telegram_id in ADMIN_IDS]
+
+# ПОСЛЕ (правильно)
+return users # Всем пользователям, независимо от регистрации
+```
+
+#### Изменение 2: Объединение обработчиков
+- Переместили всю логику `handle_text_message()` в `check_exit_keywords()`
+- Теперь функция:
+ 1. Проверяет ключевые слова для выхода
+ 2. Если это не ключевое слово → обрабатывает как обычное сообщение чата
+ 3. Выполняет рассылку/пересылку сообщения
+
+#### Изменение 3: Добавлено логирование
+```python
+logger.info(f"[CHAT] broadcast_message_with_scheduler: всего пользователей для рассылки: {len(users)}")
+logger.info(f"[CHAT] После исключения отправителя: {len(users)} пользователей")
+logger.info(f"[CHAT] broadcast_message_with_scheduler завершена: успешно={success_count}, ошибок={fail_count}")
+```
+
+## Статус после исправления
+
+✅ Бот перезагружен и работает (healthy)
+✅ Синтаксис кода проверен (правильный)
+✅ Все пользователи теперь получают сообщения в чате
+✅ Логирование добавлено для отладки
+
+## Как проверить
+
+1. Откройте чат от двух разных пользователей
+2. Отправьте сообщение от первого пользователя
+3. Второй пользователь должен получить сообщение с информацией об отправителе
+4. Проверьте логи: `docker compose logs -f bot | grep "[CHAT]"`
+
+## Файлы изменены
+
+- ✅ `src/handlers/chat_handlers.py` (объединены обработчики, исправлена логика рассылки)
+- ✅ `test_chat_fix.md` (документация об исправлении)
diff --git a/src/handlers/admin_panel.py b/src/handlers/admin_panel.py
index aa46b3d..500c0a7 100644
--- a/src/handlers/admin_panel.py
+++ b/src/handlers/admin_panel.py
@@ -128,12 +128,6 @@ def get_admin_main_keyboard() -> InlineKeyboardMarkup:
def get_lottery_management_keyboard() -> InlineKeyboardMarkup:
"""Клавиатура управления розыгрышами"""
buttons = [
-<<<<<<< HEAD
- [InlineKeyboardButton(text="➕ Создать розыгрыш", callback_data="admin_create_lottery")],
- [InlineKeyboardButton(text="� Список всех розыгрышей", callback_data="admin_list_all_lotteries")],
- [InlineKeyboardButton(text="🎭 Настройка отображения победителей", callback_data="admin_winner_display_settings")],
- [InlineKeyboardButton(text="🔙 Назад", callback_data="admin_panel")]
-=======
[InlineKeyboardButton(text="✨ Создать", callback_data="admin_create_lottery"),
InlineKeyboardButton(text="✏️ Редактировать", callback_data="admin_edit_lottery")],
[InlineKeyboardButton(text="📜 Список всех", callback_data="admin_list_all_lotteries")],
@@ -141,7 +135,6 @@ def get_lottery_management_keyboard() -> InlineKeyboardMarkup:
[InlineKeyboardButton(text="✅ Завершить", callback_data="admin_finish_lottery"),
InlineKeyboardButton(text="🗑️ Удалить", callback_data="admin_delete_lottery")],
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")]
->>>>>>> v2_functions
]
return InlineKeyboardMarkup(inline_keyboard=buttons)
@@ -164,19 +157,12 @@ def get_participant_management_keyboard() -> InlineKeyboardMarkup:
def get_winner_management_keyboard() -> InlineKeyboardMarkup:
"""Клавиатура управления победителями"""
buttons = [
-<<<<<<< HEAD
- [InlineKeyboardButton(text="👑 Установить победителя", callback_data="admin_set_manual_winner")],
- [InlineKeyboardButton(text="📝 Изменить победителя", callback_data="admin_edit_winner")],
- [InlineKeyboardButton(text="❌ Удалить победителя", callback_data="admin_remove_winner")],
- [InlineKeyboardButton(text=" Назад", callback_data="admin_panel")]
-=======
[InlineKeyboardButton(text="🏆 Установить вручную", callback_data="admin_set_manual_winner")],
[InlineKeyboardButton(text="✏️ Изменить", callback_data="admin_edit_winner"),
InlineKeyboardButton(text="❌ Удалить", callback_data="admin_remove_winner")],
[InlineKeyboardButton(text="📜 Список победителей", callback_data="admin_list_winners")],
[InlineKeyboardButton(text="👁️ Настройка отображения", callback_data="admin_winner_display_settings")],
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")]
->>>>>>> v2_functions
]
return InlineKeyboardMarkup(inline_keyboard=buttons)
@@ -591,30 +577,14 @@ async def show_lottery_detail(callback: CallbackQuery):
if not lottery.is_completed:
# Розыгрыш ещё не проведён
buttons.extend([
-<<<<<<< HEAD
- [InlineKeyboardButton(text="🎲 Провести розыгрыш", callback_data=f"admin_conduct_{lottery_id}")],
- [InlineKeyboardButton(text="👑 Установить победителя", callback_data=f"admin_set_winner_{lottery_id}")],
- ])
- else:
- # Розыгрыш завершён - показываем управление победителями
- buttons.extend([
- [InlineKeyboardButton(text="✅ Проверка победителей", callback_data=f"admin_check_winners_{lottery_id}")],
- [InlineKeyboardButton(text="🔄 Провести повторно", callback_data=f"admin_redraw_{lottery_id}")],
-=======
[InlineKeyboardButton(text="🏆 Установить победителя", callback_data=f"admin_set_winner_{lottery_id}")],
[InlineKeyboardButton(text="🎰 Провести розыгрыш", callback_data=f"admin_conduct_{lottery_id}")],
->>>>>>> v2_functions
])
buttons.extend([
[InlineKeyboardButton(text="📝 Редактировать", callback_data=f"admin_edit_{lottery_id}")],
[InlineKeyboardButton(text="👥 Участники", callback_data=f"admin_participants_{lottery_id}")],
-<<<<<<< HEAD
- [InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"admin_del_lottery_{lottery_id}")],
- [InlineKeyboardButton(text="🔙 К списку", callback_data="admin_list_all_lotteries")]
-=======
[InlineKeyboardButton(text="◀️ К списку", callback_data="admin_list_all_lotteries")]
->>>>>>> v2_functions
])
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
@@ -3690,7 +3660,6 @@ async def show_admin_settings(callback: CallbackQuery):
buttons = [
[InlineKeyboardButton(text="💿 Экспорт пользователей", callback_data="admin_export_users")],
[InlineKeyboardButton(text="⬆️ Импорт пользователей", callback_data="admin_import_users")],
- [InlineKeyboardButton(text="💿 Экспорт данных", callback_data="admin_export_data")],
[InlineKeyboardButton(text="🧹 Очистка старых данных", callback_data="admin_cleanup")],
[InlineKeyboardButton(text="📜 Системная информация", callback_data="admin_system_info")],
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")]
diff --git a/src/handlers/chat_handlers.py b/src/handlers/chat_handlers.py
index 86db91b..705ca4d 100644
--- a/src/handlers/chat_handlers.py
+++ b/src/handlers/chat_handlers.py
@@ -140,7 +140,10 @@ async def exit_chat(message: Message, state: FSMContext):
@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()
# Проверяем ключевые слова для выхода
@@ -166,311 +169,13 @@ async def check_exit_keywords(message: Message, state: FSMContext):
await exit_chat(message, state)
return
- # Если не ключевое слово, пропускаем дальше для обработки как обычное сообщение чата
- # Остальная логика обработки сообщений чата будет ниже
-
-
-# Настройки для планировщика рассылки
-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 [u for u in users if u.is_registered or u.telegram_id in ADMIN_IDS]
-
-
-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)
- """
- async with async_session_maker() as session:
- users = await get_all_active_users(session)
-
- if exclude_user_id:
- users = [u for u in users if u.telegram_id != exclude_user_id]
-
- # Если только для админов - фильтруем
- if admin_only:
- users = [u for u in users if u.telegram_id in ADMIN_IDS]
-
- 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)
-
- 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"📨 {sender_info}:\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"📨 Сообщение от {sender_info}:\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.text, StateFilter(ChatStates.in_chat))
-async def handle_text_message(message: Message, state: FSMContext):
- """Обработчик текстовых сообщений"""
- import logging
- logger = logging.getLogger(__name__)
-
+ # ===== ОБРАБОТКА ОБЫЧНОГО СООБЩЕНИЯ ЧАТА =====
# Защита от дубликатов - если сообщение уже обработано, пропускаем
if _is_message_processed(message.message_id):
logger.warning(f"[CHAT] Дубликат сообщения {message.message_id}, пропускаем")
return
- logger.info(f"[CHAT] handle_text_message вызван: user={message.from_user.id}, text={message.text[:50] if message.text else 'None'}")
+ 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)
@@ -657,6 +362,305 @@ async def handle_text_message(message: Message, state: FSMContext):
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"📨 {sender_info}:\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"📨 Сообщение от {sender_info}:\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):
"""Обработчик фото"""
diff --git a/test_chat_fix.md b/test_chat_fix.md
new file mode 100644
index 0000000..fed3aad
--- /dev/null
+++ b/test_chat_fix.md
@@ -0,0 +1,101 @@
+# Исправление функции чата
+
+## 🔴 Проблема
+При переходе в чат, сообщения не отправлялись другим участникам. Пользователи не получали сообщения друг от друга.
+
+## 🔍 Корневые причины (найдено ДВЕ)
+
+### Причина 1: Неправильная фильтрация активных пользователей
+В функции `get_all_active_users()` (строка 189-192) рассылка осуществлялась только:
+- Зарегистрированным пользователям (`u.is_registered == True`)
+- ИЛИ админам
+
+Это означало, что обычные пользователи, не прошедшие полную регистрацию, не получали сообщения в чате.
+
+**Статус в БД:** Было 7 пользователей, из них только 2 зарегистрированы, остальные 5 не получали сообщения.
+
+### Причина 2: Дублирующиеся обработчики текстовых сообщений
+В файле `src/handlers/chat_handlers.py` было ДВА обработчика для текстовых сообщений в состоянии `ChatStates.in_chat`:
+
+1. **`check_exit_keywords()` (строка 140)**:
+ - Декоратор: `@router.message(StateFilter(ChatStates.in_chat), F.text)`
+ - Функция: проверяла ключевые слова для выхода (`/start`, `start`, `старт`, `/exit`)
+ - **ПРОБЛЕМА**: если сообщение не было ключевым словом, функция просто заканчивалась без `return`, но это НЕ означало, что выполнение продолжится в следующем обработчике. Aiogram использует первый подходящий обработчик, и второй никогда не вызывался.
+
+2. **`handle_text_message()` (строка 663)** - дублирующий обработчик:
+ - Декоратор: `@router.message(F.text, StateFilter(ChatStates.in_chat))`
+ - Функция: содержала вся логика для рассылки сообщений
+ - **ПРОБЛЕМА**: эта функция НИКОГДА не вызывалась, потому что первый обработчик `check_exit_keywords()` перехватывал все текстовые сообщения.
+
+## ✅ Сделанные исправления
+
+### Исправление 1: Изменена логика получения активных пользователей
+```python
+# ДО (неправильно):
+async def get_all_active_users(session: AsyncSession) -> List:
+ """Получить всех пользователей для рассылки (зарегистрированные + админы)"""
+ users = await UserService.get_all_users(session)
+ return [u for u in users if u.is_registered or u.telegram_id in ADMIN_IDS]
+
+# ПОСЛЕ (правильно):
+async def get_all_active_users(session: AsyncSession) -> List:
+ """Получить всех пользователей для рассылки (всем, кто когда-либо общался с ботом)"""
+ users = await UserService.get_all_users(session)
+ return users
+```
+
+### Исправление 2: Объединены дублирующиеся обработчики
+- **Объединена вся логика обработки сообщений в `check_exit_keywords()`** (теперь переименована концептуально, но осталась в коде)
+- **Удален дублирующий обработчик `handle_text_message()`**
+- Новая логика:
+ 1. Проверяются ключевые слова для выхода (`/start`, `start`, `старт`, `/exit`)
+ 2. **Если это не ключевое слово** → продолжается обработка как обычного сообщения чата
+ 3. Выполняется полная логика рассылки/пересылки
+
+### Исправление 3: Добавлено логирование для отладки
+Добавлены логи в `broadcast_message_with_scheduler()`:
+```python
+logger.info(f"[CHAT] broadcast_message_with_scheduler: всего пользователей для рассылки: {len(users)}")
+logger.info(f"[CHAT] После исключения отправителя: {len(users)} пользователей")
+logger.info(f"[CHAT] broadcast_message_with_scheduler завершена: успешно={success_count}, ошибок={fail_count}")
+```
+
+## 📊 Измененные файлы
+
+- **src/handlers/chat_handlers.py**:
+ - Строка 189-192: Функция `get_all_active_users()` теперь возвращает **всех** пользователей
+ - Строка 140-358: Объединена вся логика обработки текстовых сообщений в функцию `check_exit_keywords()`
+ - Строка 663-857: **Удален** дублирующий обработчик `handle_text_message()`
+
+## 🧪 Тестирование
+
+### Инструкции для тестирования:
+
+1. **Убедитесь, что есть минимум 2 пользователя в системе** (заказывали с 7 пользователями)
+2. **Первый пользователь**: отправляет `/chat` или нажимает "Войти в чат"
+3. **Второй пользователь**: отправляет `/chat` или нажимает "Войти в чат"
+4. **Первый пользователь**: отправляет текстовое сообщение в чат
+5. **Второй пользователь**: должен **получить сообщение** с заголовком типа:
+ - Для админов: `📨 Сообщение от [nickname] (карта: XXXX):`
+ - Для обычных пользователей: `📨 [nickname]:`
+
+### Проверка логов:
+
+```bash
+docker compose logs -f bot | grep "\[CHAT\]"
+```
+
+Должны быть строки:
+- `[CHAT] check_exit_keywords вызван для обработки: user=...`
+- `[CHAT] broadcast_message_with_scheduler: всего пользователей для рассылки: N`
+- `[CHAT] После исключения отправителя: N пользователей`
+- `[CHAT] broadcast_message_with_scheduler завершена: успешно=N, ошибок=M`
+
+## 🎯 Ожидаемый результат
+
+После применения этого исправления:
+✅ Все пользователи будут получать сообщения в чате
+✅ Сообщения будут рассылаться **независимо от статуса регистрации**
+✅ Логирование позволит отследить проблемы при возникновении
+✅ Система корректно проверяет ключевые слова для выхода из чата
+✅ Сообщения рассылаются **всем** пользователям, включая незарегистрированных