fix: Fix chat message broadcasting to all users
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
- Fixed get_all_active_users() to broadcast to ALL users regardless of registration status - Merged duplicate text message handlers (check_exit_keywords and handle_text_message) - Added detailed logging for chat message broadcasting - Now users can receive messages in chat without full registration Resolves: Messages not being delivered to unregistered users in chat
This commit is contained in:
65
CHAT_FIX_REPORT.md
Normal file
65
CHAT_FIX_REPORT.md
Normal file
@@ -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` (документация об исправлении)
|
||||||
@@ -128,12 +128,6 @@ def get_admin_main_keyboard() -> InlineKeyboardMarkup:
|
|||||||
def get_lottery_management_keyboard() -> InlineKeyboardMarkup:
|
def get_lottery_management_keyboard() -> InlineKeyboardMarkup:
|
||||||
"""Клавиатура управления розыгрышами"""
|
"""Клавиатура управления розыгрышами"""
|
||||||
buttons = [
|
buttons = [
|
||||||
<<<<<<< HEAD
|
|
||||||
[InlineKeyboardButton(text="➕ Создать розыгрыш", callback_data="admin_create_lottery")],
|
|
||||||
[InlineKeyboardButton(text="<EFBFBD> Список всех розыгрышей", 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_create_lottery"),
|
||||||
InlineKeyboardButton(text="✏️ Редактировать", callback_data="admin_edit_lottery")],
|
InlineKeyboardButton(text="✏️ Редактировать", callback_data="admin_edit_lottery")],
|
||||||
[InlineKeyboardButton(text="📜 Список всех", callback_data="admin_list_all_lotteries")],
|
[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_finish_lottery"),
|
||||||
InlineKeyboardButton(text="🗑️ Удалить", callback_data="admin_delete_lottery")],
|
InlineKeyboardButton(text="🗑️ Удалить", callback_data="admin_delete_lottery")],
|
||||||
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")]
|
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")]
|
||||||
>>>>>>> v2_functions
|
|
||||||
]
|
]
|
||||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||||
|
|
||||||
@@ -164,19 +157,12 @@ def get_participant_management_keyboard() -> InlineKeyboardMarkup:
|
|||||||
def get_winner_management_keyboard() -> InlineKeyboardMarkup:
|
def get_winner_management_keyboard() -> InlineKeyboardMarkup:
|
||||||
"""Клавиатура управления победителями"""
|
"""Клавиатура управления победителями"""
|
||||||
buttons = [
|
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_set_manual_winner")],
|
||||||
[InlineKeyboardButton(text="✏️ Изменить", callback_data="admin_edit_winner"),
|
[InlineKeyboardButton(text="✏️ Изменить", callback_data="admin_edit_winner"),
|
||||||
InlineKeyboardButton(text="❌ Удалить", callback_data="admin_remove_winner")],
|
InlineKeyboardButton(text="❌ Удалить", callback_data="admin_remove_winner")],
|
||||||
[InlineKeyboardButton(text="📜 Список победителей", callback_data="admin_list_winners")],
|
[InlineKeyboardButton(text="📜 Список победителей", callback_data="admin_list_winners")],
|
||||||
[InlineKeyboardButton(text="👁️ Настройка отображения", callback_data="admin_winner_display_settings")],
|
[InlineKeyboardButton(text="👁️ Настройка отображения", callback_data="admin_winner_display_settings")],
|
||||||
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")]
|
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")]
|
||||||
>>>>>>> v2_functions
|
|
||||||
]
|
]
|
||||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||||
|
|
||||||
@@ -591,30 +577,14 @@ async def show_lottery_detail(callback: CallbackQuery):
|
|||||||
if not lottery.is_completed:
|
if not lottery.is_completed:
|
||||||
# Розыгрыш ещё не проведён
|
# Розыгрыш ещё не проведён
|
||||||
buttons.extend([
|
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_set_winner_{lottery_id}")],
|
||||||
[InlineKeyboardButton(text="🎰 Провести розыгрыш", callback_data=f"admin_conduct_{lottery_id}")],
|
[InlineKeyboardButton(text="🎰 Провести розыгрыш", callback_data=f"admin_conduct_{lottery_id}")],
|
||||||
>>>>>>> v2_functions
|
|
||||||
])
|
])
|
||||||
|
|
||||||
buttons.extend([
|
buttons.extend([
|
||||||
[InlineKeyboardButton(text="📝 Редактировать", callback_data=f"admin_edit_{lottery_id}")],
|
[InlineKeyboardButton(text="📝 Редактировать", callback_data=f"admin_edit_{lottery_id}")],
|
||||||
[InlineKeyboardButton(text="👥 Участники", callback_data=f"admin_participants_{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")]
|
[InlineKeyboardButton(text="◀️ К списку", callback_data="admin_list_all_lotteries")]
|
||||||
>>>>>>> v2_functions
|
|
||||||
])
|
])
|
||||||
|
|
||||||
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
|
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
|
||||||
@@ -3690,7 +3660,6 @@ async def show_admin_settings(callback: CallbackQuery):
|
|||||||
buttons = [
|
buttons = [
|
||||||
[InlineKeyboardButton(text="💿 Экспорт пользователей", callback_data="admin_export_users")],
|
[InlineKeyboardButton(text="💿 Экспорт пользователей", callback_data="admin_export_users")],
|
||||||
[InlineKeyboardButton(text="⬆️ Импорт пользователей", callback_data="admin_import_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_cleanup")],
|
||||||
[InlineKeyboardButton(text="📜 Системная информация", callback_data="admin_system_info")],
|
[InlineKeyboardButton(text="📜 Системная информация", callback_data="admin_system_info")],
|
||||||
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")]
|
[InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")]
|
||||||
|
|||||||
@@ -140,7 +140,10 @@ async def exit_chat(message: Message, state: FSMContext):
|
|||||||
|
|
||||||
@router.message(StateFilter(ChatStates.in_chat), F.text)
|
@router.message(StateFilter(ChatStates.in_chat), F.text)
|
||||||
async def check_exit_keywords(message: Message, state: FSMContext):
|
async def check_exit_keywords(message: Message, state: FSMContext):
|
||||||
"""Проверка на ключевые слова для выхода из чата"""
|
"""Проверка на ключевые слова для выхода из чата + обработка сообщений"""
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
text = message.text.strip().lower()
|
text = message.text.strip().lower()
|
||||||
|
|
||||||
# Проверяем ключевые слова для выхода
|
# Проверяем ключевые слова для выхода
|
||||||
@@ -166,311 +169,13 @@ async def check_exit_keywords(message: Message, state: FSMContext):
|
|||||||
await exit_chat(message, state)
|
await exit_chat(message, state)
|
||||||
return
|
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"📨 <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.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):
|
if _is_message_processed(message.message_id):
|
||||||
logger.warning(f"[CHAT] Дубликат сообщения {message.message_id}, пропускаем")
|
logger.warning(f"[CHAT] Дубликат сообщения {message.message_id}, пропускаем")
|
||||||
return
|
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)
|
# Пропускаем для account_router (который идет после chat_router)
|
||||||
@@ -657,6 +362,305 @@ async def handle_text_message(message: Message, state: FSMContext):
|
|||||||
await message.answer("❌ Не удалось переслать сообщение")
|
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))
|
@router.message(F.photo, StateFilter(ChatStates.in_chat))
|
||||||
async def handle_photo_message(message: Message, state: FSMContext):
|
async def handle_photo_message(message: Message, state: FSMContext):
|
||||||
"""Обработчик фото"""
|
"""Обработчик фото"""
|
||||||
|
|||||||
101
test_chat_fix.md
Normal file
101
test_chat_fix.md
Normal file
@@ -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`
|
||||||
|
|
||||||
|
## 🎯 Ожидаемый результат
|
||||||
|
|
||||||
|
После применения этого исправления:
|
||||||
|
✅ Все пользователи будут получать сообщения в чате
|
||||||
|
✅ Сообщения будут рассылаться **независимо от статуса регистрации**
|
||||||
|
✅ Логирование позволит отследить проблемы при возникновении
|
||||||
|
✅ Система корректно проверяет ключевые слова для выхода из чата
|
||||||
|
✅ Сообщения рассылаются **всем** пользователям, включая незарегистрированных
|
||||||
Reference in New Issue
Block a user