Compare commits
1 Commits
dbba2c4b83
...
v2_functio
| Author | SHA1 | Date | |
|---|---|---|---|
| 93f7ccdcf6 |
@@ -2,17 +2,17 @@
|
|||||||
# Скопируйте этот файл в .env.prod и заполните реальными значениями
|
# Скопируйте этот файл в .env.prod и заполните реальными значениями
|
||||||
|
|
||||||
# Telegram Bot Token
|
# Telegram Bot Token
|
||||||
BOT_TOKEN=6804077170:AAGw_t6ktAiwYr2mrby0PUhckt50NZaEs0E
|
BOT_TOKEN=8125171867:AAHA0l2hGGodOUBh0rFlkE4CxK0X6JzZv64
|
||||||
|
|
||||||
# PostgreSQL настройки для Docker контейнера
|
# PostgreSQL настройки для Docker контейнера
|
||||||
POSTGRES_HOST=192.168.0.102
|
POSTGRES_HOST=192.168.0.102
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
POSTGRES_DB=new_lottery_KR
|
POSTGRES_DB=lottery_bot
|
||||||
POSTGRES_USER=trevor
|
POSTGRES_USER=trevor
|
||||||
POSTGRES_PASSWORD=Cl0ud_1985!
|
POSTGRES_PASSWORD=Cl0ud_1985!
|
||||||
|
|
||||||
# Database URL для бота (использует postgres как hostname внутри Docker сети)
|
# Database URL для бота (использует postgres как hostname внутри Docker сети)
|
||||||
DATABASE_URL=postgresql+asyncpg://trevor:Cl0ud_1985!@192.168.0.102:5432/new_lottery_KR
|
DATABASE_URL=postgresql+asyncpg://trevor:Cl0ud_1985!@192.168.0.102:5432/lottery_bot
|
||||||
# Redis URL
|
# Redis URL
|
||||||
REDIS_URL=redis://redis:6379/0
|
REDIS_URL=redis://redis:6379/0
|
||||||
|
|
||||||
@@ -20,4 +20,4 @@ REDIS_URL=redis://redis:6379/0
|
|||||||
ADMIN_IDS=556399210,6639865742
|
ADMIN_IDS=556399210,6639865742
|
||||||
|
|
||||||
# Настройки логирования
|
# Настройки логирования
|
||||||
LOG_LEVEL=DEBUG
|
LOG_LEVEL=INFO
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
# ОТЧЕТ: Исправление проблемы с чатом (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` (документация об исправлении)
|
|
||||||
@@ -8,15 +8,15 @@ services:
|
|||||||
container_name: lottery_postgres
|
container_name: lottery_postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-new_lottery_kr}
|
POSTGRES_DB: ${POSTGRES_DB:-lottery_bot}
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-trevor}
|
POSTGRES_USER: ${POSTGRES_USER:-lottery_user}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-Cl0ud_1985!}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-lottery_password}
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
networks:
|
networks:
|
||||||
- lottery_network
|
- lottery_network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-trevor}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-lottery_user}"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|||||||
@@ -93,10 +93,12 @@ if not owner or owner.telegram_id != callback.from_user.id:
|
|||||||
### Что НЕ может сделать пользователь:
|
### Что НЕ может сделать пользователь:
|
||||||
|
|
||||||
❌ Подтвердить чужой счет
|
❌ Подтвердить чужой счет
|
||||||
|
❌ Подтвердить счет, который ему не принадлежит
|
||||||
❌ Подтвердить один счет дважды
|
❌ Подтвердить один счет дважды
|
||||||
|
|
||||||
### Что может сделать пользователь:
|
### Что может сделать пользователь:
|
||||||
|
|
||||||
|
✅ Подтвердить только свои счета
|
||||||
✅ Подтвердить каждый свой выигрышный счет отдельно
|
✅ Подтвердить каждый свой выигрышный счет отдельно
|
||||||
✅ Видеть номер счета на каждой кнопке
|
✅ Видеть номер счета на каждой кнопке
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
"""
|
|
||||||
|
|
||||||
Revision ID: 41aae82e631b
|
|
||||||
Revises: 64c4f8a81afa
|
|
||||||
Create Date: 2026-02-13 18:12:12.031589
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '41aae82e631b'
|
|
||||||
down_revision = '64c4f8a81afa'
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
pass
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
pass
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -5,7 +5,6 @@ import logging
|
|||||||
from src.interfaces.base import IBotController, ILotteryService, IUserService, IKeyboardBuilder, IMessageFormatter
|
from src.interfaces.base import IBotController, ILotteryService, IUserService, IKeyboardBuilder, IMessageFormatter
|
||||||
from src.interfaces.base import ILotteryRepository, IParticipationRepository
|
from src.interfaces.base import ILotteryRepository, IParticipationRepository
|
||||||
from src.core.config import ADMIN_IDS
|
from src.core.config import ADMIN_IDS
|
||||||
from src.core.telegram_config import get_parse_mode
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -89,7 +88,7 @@ class BotController(IBotController):
|
|||||||
await callback.answer("❌ Нет активных розыгрышей", show_alert=True)
|
await callback.answer("❌ Нет активных розыгрышей", show_alert=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
text = "🎲 <b>Активные розыгрыши:</b>\n\n"
|
text = "🎲 **Активные розыгрыши:**\n\n"
|
||||||
|
|
||||||
for lottery in lotteries:
|
for lottery in lotteries:
|
||||||
participants_count = await self.participation_repo.get_count_by_lottery(lottery.id)
|
participants_count = await self.participation_repo.get_count_by_lottery(lottery.id)
|
||||||
@@ -113,7 +112,7 @@ class BotController(IBotController):
|
|||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
text,
|
text,
|
||||||
reply_markup=keyboard,
|
reply_markup=keyboard,
|
||||||
parse_mode=get_parse_mode("inline_keyboard")
|
parse_mode="Markdown"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Если сообщение не изменилось - просто отвечаем на callback
|
# Если сообщение не изменилось - просто отвечаем на callback
|
||||||
@@ -125,5 +124,5 @@ class BotController(IBotController):
|
|||||||
await callback.message.answer(
|
await callback.message.answer(
|
||||||
text,
|
text,
|
||||||
reply_markup=keyboard,
|
reply_markup=keyboard,
|
||||||
parse_mode=get_parse_mode("inline_keyboard")
|
parse_mode="Markdown"
|
||||||
)
|
)
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
"""
|
|
||||||
Поддержка премиум эмодзи для ботов, созданных с премиум аккаунтов
|
|
||||||
Telegram Bot API поддерживает премиум эмодзи начиная с версии 7.0
|
|
||||||
|
|
||||||
Для использования премиум эмодзи:
|
|
||||||
1. Бот должен быть создан с премиум аккаунта
|
|
||||||
2. Использовать эмодзи напрямую в тексте сообщений
|
|
||||||
3. Использовать parse_mode="HTML" или parse_mode="Markdown"
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
from aiogram.types import MessageEntity, TextQuote
|
|
||||||
from aiogram.enums import MessageEntityType
|
|
||||||
|
|
||||||
|
|
||||||
class PremiumEmojiConfig:
|
|
||||||
"""Конфигурация поддержки премиум эмодзи"""
|
|
||||||
|
|
||||||
# Флаг, что бот может использовать премиум эмодзи
|
|
||||||
SUPPORTS_PREMIUM_EMOJI = True
|
|
||||||
|
|
||||||
# Стандартные parse_mode для автоматической поддержки эмодзи
|
|
||||||
DEFAULT_PARSE_MODE = "HTML" # Поддерживает эмодзи лучше чем Markdown
|
|
||||||
|
|
||||||
# Премиум эмодзи которые используются в приложении
|
|
||||||
PREMIUM_EMOJIS = {
|
|
||||||
# Розыгрыши
|
|
||||||
"🎲_premium": "🎲", # Если есть премиум версия
|
|
||||||
"🏆_premium": "🏆",
|
|
||||||
"🎯_premium": "🎯",
|
|
||||||
|
|
||||||
# Логины
|
|
||||||
"📱_premium": "📱",
|
|
||||||
"🔐_premium": "🔐",
|
|
||||||
|
|
||||||
# Статусы
|
|
||||||
"✅_premium": "✅",
|
|
||||||
"❌_premium": "❌",
|
|
||||||
"⏸_premium": "⏸️",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def supports_premium_emoji() -> bool:
|
|
||||||
"""Проверить поддерживает ли бот премиум эмодзи"""
|
|
||||||
return PremiumEmojiConfig.SUPPORTS_PREMIUM_EMOJI
|
|
||||||
|
|
||||||
|
|
||||||
def get_parse_mode() -> str:
|
|
||||||
"""Получить оптимальный parse_mode для поддержки эмодзи"""
|
|
||||||
return PremiumEmojiConfig.DEFAULT_PARSE_MODE
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_emoji_support(text: str) -> str:
|
|
||||||
"""
|
|
||||||
Убедиться что текст может быть отправлен с эмодзи
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: Текст сообщения
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Обработанный текст с поддержкой эмодзи
|
|
||||||
"""
|
|
||||||
# В Aiogram 3.16+ эмодзи автоматически поддерживаются при правильном parse_mode
|
|
||||||
# Эта функция может быть расширена для дополнительной обработки если нужно
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
async def send_message_with_emoji(
|
|
||||||
send_func,
|
|
||||||
text: str,
|
|
||||||
parse_mode: Optional[str] = None,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Отправить сообщение с поддержкой премиум эмодзи
|
|
||||||
|
|
||||||
Args:
|
|
||||||
send_func: Функция отправки (message.answer, callback.message.edit_text и т.д.)
|
|
||||||
text: Текст сообщения
|
|
||||||
parse_mode: Parse mode (если None, использует default)
|
|
||||||
**kwargs: Дополнительные параметры
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Результат отправки сообщения
|
|
||||||
"""
|
|
||||||
if parse_mode is None:
|
|
||||||
parse_mode = get_parse_mode()
|
|
||||||
|
|
||||||
# Убедиться что текст может содержать эмодзи
|
|
||||||
text = ensure_emoji_support(text)
|
|
||||||
|
|
||||||
# Отправить сообщение
|
|
||||||
return await send_func(text, parse_mode=parse_mode, **kwargs)
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
"""
|
|
||||||
Глобальная конфигурация для Telegram Bot API параметров
|
|
||||||
Включая поддержку премиум эмодзи
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Parse mode для всех сообщений
|
|
||||||
# HTML поддерживает премиум эмодзи лучше чем Markdown
|
|
||||||
GLOBAL_PARSE_MODE = "HTML"
|
|
||||||
|
|
||||||
# Доступные parse modes
|
|
||||||
PARSE_MODES = {
|
|
||||||
"HTML": "HTML",
|
|
||||||
"MARKDOWN": "Markdown",
|
|
||||||
"NONE": None
|
|
||||||
}
|
|
||||||
|
|
||||||
# Какой parse mode использовать для разных типов сообщений
|
|
||||||
MESSAGE_PARSE_MODES = {
|
|
||||||
"text_message": "HTML", # Обычные текстовые сообщения
|
|
||||||
"inline_keyboard": "HTML", # С inline клавиатурой
|
|
||||||
"reply_keyboard": "HTML", # С reply клавиатуре
|
|
||||||
"edit_message": "HTML", # Редактирование сообщения
|
|
||||||
"broadcast": "HTML", # Массовые рассылки
|
|
||||||
"admin_broadcast": "HTML", # Административные рассылки
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_parse_mode(message_type: str = "text_message") -> str:
|
|
||||||
"""
|
|
||||||
Получить parse_mode для типа сообщения
|
|
||||||
|
|
||||||
Args:
|
|
||||||
message_type: Тип сообщения (см. MESSAGE_PARSE_MODES)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Parse mode строка ("HTML", "Markdown", None)
|
|
||||||
"""
|
|
||||||
return MESSAGE_PARSE_MODES.get(message_type, GLOBAL_PARSE_MODE)
|
|
||||||
|
|
||||||
|
|
||||||
def get_global_parse_mode() -> str:
|
|
||||||
"""Получить глобальный parse mode"""
|
|
||||||
return GLOBAL_PARSE_MODE
|
|
||||||
@@ -69,10 +69,6 @@ class AdminStates(StatesGroup):
|
|||||||
remove_participant_bulk_accounts = State()
|
remove_participant_bulk_accounts = State()
|
||||||
participant_search = State()
|
participant_search = State()
|
||||||
|
|
||||||
# Добавление/удаление участников в конкретном розыгрыше
|
|
||||||
add_to_lottery_user = State()
|
|
||||||
remove_from_lottery_user = State()
|
|
||||||
|
|
||||||
# Установка победителей
|
# Установка победителей
|
||||||
set_winner_lottery = State()
|
set_winner_lottery = State()
|
||||||
set_winner_place = State()
|
set_winner_place = State()
|
||||||
@@ -606,7 +602,6 @@ async def show_lottery_detail(callback: CallbackQuery):
|
|||||||
buttons = []
|
buttons = []
|
||||||
|
|
||||||
if not lottery.is_completed:
|
if not lottery.is_completed:
|
||||||
# Розыгрыш ещё не проведён
|
|
||||||
buttons.extend([
|
buttons.extend([
|
||||||
[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}")],
|
||||||
@@ -1483,579 +1478,6 @@ async def process_bulk_remove_participant(message: Message, state: FSMContext):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ======================
|
|
||||||
# ДОБАВЛЕНИЕ/УДАЛЕНИЕ УЧАСТНИКОВ В КОНКРЕТНОМ РОЗЫГРЫШЕ
|
|
||||||
# ======================
|
|
||||||
|
|
||||||
@admin_router.callback_query(F.data.startswith("admin_add_to_"))
|
|
||||||
async def add_participant_to_lottery(callback: CallbackQuery, state: FSMContext):
|
|
||||||
"""Добавление участника в конкретный розыгрыш"""
|
|
||||||
if not is_admin(callback.from_user.id):
|
|
||||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
lottery_id = int(callback.data.split("_")[-1])
|
|
||||||
await state.update_data(add_to_lottery_id=lottery_id)
|
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
|
||||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
|
||||||
|
|
||||||
if not lottery:
|
|
||||||
await callback.answer("❌ Розыгрыш не найден", show_alert=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
text = f"➕ Добавление участника\n\n"
|
|
||||||
text += f"🎯 Розыгрыш: {lottery.title}\n\n"
|
|
||||||
text += "Введите данные участника:\n"
|
|
||||||
text += "• Telegram ID (число)\n"
|
|
||||||
text += "• @username\n"
|
|
||||||
text += "• Номер счета (XX-XX-XX-XX-XX-XX-XX)"
|
|
||||||
|
|
||||||
await callback.message.edit_text(
|
|
||||||
text,
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_participants_{lottery_id}")]
|
|
||||||
])
|
|
||||||
)
|
|
||||||
await state.set_state(AdminStates.add_to_lottery_user)
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.message(StateFilter(AdminStates.add_to_lottery_user))
|
|
||||||
async def process_add_to_lottery(message: Message, state: FSMContext):
|
|
||||||
"""Обработка добавления участника в конкретный розыгрыш"""
|
|
||||||
if not is_admin(message.from_user.id):
|
|
||||||
await message.answer("❌ Недостаточно прав")
|
|
||||||
return
|
|
||||||
|
|
||||||
data = await state.get_data()
|
|
||||||
lottery_id = data.get('add_to_lottery_id')
|
|
||||||
user_input = message.text.strip()
|
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
|
||||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
|
||||||
if not lottery:
|
|
||||||
await message.answer("❌ Розыгрыш не найден")
|
|
||||||
await state.clear()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Определяем тип ввода
|
|
||||||
user = None
|
|
||||||
account_number = None
|
|
||||||
|
|
||||||
if user_input.startswith('@'):
|
|
||||||
# Username
|
|
||||||
username = user_input[1:]
|
|
||||||
user = await UserService.get_user_by_username(session, username)
|
|
||||||
elif user_input.isdigit():
|
|
||||||
# Telegram ID
|
|
||||||
telegram_id = int(user_input)
|
|
||||||
user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
|
||||||
elif '-' in user_input:
|
|
||||||
# Номер счета
|
|
||||||
from src.utils.account_utils import parse_accounts_from_message
|
|
||||||
accounts = parse_accounts_from_message(user_input)
|
|
||||||
if accounts:
|
|
||||||
account_number = accounts[0]
|
|
||||||
|
|
||||||
if not user and not account_number:
|
|
||||||
await message.answer(
|
|
||||||
"❌ Не удалось найти пользователя или распознать счет.\n"
|
|
||||||
"Пользователь должен запустить бота командой /start",
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_participants_{lottery_id}")]
|
|
||||||
])
|
|
||||||
)
|
|
||||||
await state.clear()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Добавляем участника
|
|
||||||
if user:
|
|
||||||
success = await ParticipationService.add_participant(session, lottery_id, user.id)
|
|
||||||
name = f"@{user.username}" if user.username else f"{user.first_name} (ID: {user.telegram_id})"
|
|
||||||
else:
|
|
||||||
# Добавление по номеру счета
|
|
||||||
from sqlalchemy import select
|
|
||||||
from ..core.models import Participation
|
|
||||||
|
|
||||||
# Проверяем, не добавлен ли уже этот счет
|
|
||||||
existing = await session.execute(
|
|
||||||
select(Participation).where(
|
|
||||||
Participation.lottery_id == lottery_id,
|
|
||||||
Participation.account_number == account_number
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if existing.scalar_one_or_none():
|
|
||||||
await message.answer(
|
|
||||||
f"⚠️ Счет {account_number} уже участвует в этом розыгрыше",
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_participants_{lottery_id}")]
|
|
||||||
])
|
|
||||||
)
|
|
||||||
await state.clear()
|
|
||||||
return
|
|
||||||
|
|
||||||
participation = Participation(lottery_id=lottery_id, account_number=account_number)
|
|
||||||
session.add(participation)
|
|
||||||
await session.commit()
|
|
||||||
success = True
|
|
||||||
name = f"Счет: {account_number}"
|
|
||||||
|
|
||||||
await state.clear()
|
|
||||||
|
|
||||||
if success:
|
|
||||||
await message.answer(
|
|
||||||
f"✅ Участник добавлен!\n\n"
|
|
||||||
f"👤 {name}\n"
|
|
||||||
f"🎯 Розыгрыш: {lottery.title}",
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text="➕ Добавить ещё", callback_data=f"admin_add_to_{lottery_id}")],
|
|
||||||
[InlineKeyboardButton(text="👥 К участникам", callback_data=f"admin_participants_{lottery_id}")]
|
|
||||||
])
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await message.answer(
|
|
||||||
f"⚠️ Участник уже добавлен в этот розыгрыш",
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_participants_{lottery_id}")]
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.callback_query(F.data.startswith("admin_remove_from_"))
|
|
||||||
async def remove_participant_from_lottery(callback: CallbackQuery, state: FSMContext):
|
|
||||||
"""Удаление участника из конкретного розыгрыша"""
|
|
||||||
if not is_admin(callback.from_user.id):
|
|
||||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
lottery_id = int(callback.data.split("_")[-1])
|
|
||||||
await state.update_data(remove_from_lottery_id=lottery_id)
|
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
|
||||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
|
||||||
participants_count = await ParticipationService.get_participants_count(session, lottery_id)
|
|
||||||
|
|
||||||
if not lottery:
|
|
||||||
await callback.answer("❌ Розыгрыш не найден", show_alert=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
text = f"➖ Удаление участника\n\n"
|
|
||||||
text += f"🎯 Розыгрыш: {lottery.title}\n"
|
|
||||||
text += f"👥 Участников: {participants_count}\n\n"
|
|
||||||
text += "Введите данные участника для удаления:\n"
|
|
||||||
text += "• Telegram ID (число)\n"
|
|
||||||
text += "• @username\n"
|
|
||||||
text += "• Номер счета (XX-XX-XX-XX-XX-XX-XX)"
|
|
||||||
|
|
||||||
await callback.message.edit_text(
|
|
||||||
text,
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_participants_{lottery_id}")]
|
|
||||||
])
|
|
||||||
)
|
|
||||||
await state.set_state(AdminStates.remove_from_lottery_user)
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.message(StateFilter(AdminStates.remove_from_lottery_user))
|
|
||||||
async def process_remove_from_lottery(message: Message, state: FSMContext):
|
|
||||||
"""Обработка удаления участника из конкретного розыгрыша"""
|
|
||||||
if not is_admin(message.from_user.id):
|
|
||||||
await message.answer("❌ Недостаточно прав")
|
|
||||||
return
|
|
||||||
|
|
||||||
data = await state.get_data()
|
|
||||||
lottery_id = data.get('remove_from_lottery_id')
|
|
||||||
user_input = message.text.strip()
|
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
|
||||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
|
||||||
if not lottery:
|
|
||||||
await message.answer("❌ Розыгрыш не найден")
|
|
||||||
await state.clear()
|
|
||||||
return
|
|
||||||
|
|
||||||
removed = False
|
|
||||||
name = user_input
|
|
||||||
|
|
||||||
if user_input.startswith('@'):
|
|
||||||
# Username
|
|
||||||
username = user_input[1:]
|
|
||||||
user = await UserService.get_user_by_username(session, username)
|
|
||||||
if user:
|
|
||||||
removed = await ParticipationService.remove_participant(session, lottery_id, user.id)
|
|
||||||
name = f"@{user.username}" if user.username else f"{user.first_name}"
|
|
||||||
elif user_input.isdigit():
|
|
||||||
# Telegram ID
|
|
||||||
telegram_id = int(user_input)
|
|
||||||
user = await UserService.get_user_by_telegram_id(session, telegram_id)
|
|
||||||
if user:
|
|
||||||
removed = await ParticipationService.remove_participant(session, lottery_id, user.id)
|
|
||||||
name = f"@{user.username}" if user and user.username else f"ID: {telegram_id}"
|
|
||||||
elif '-' in user_input:
|
|
||||||
# Номер счета
|
|
||||||
from sqlalchemy import select, delete
|
|
||||||
from ..core.models import Participation
|
|
||||||
|
|
||||||
from src.utils.account_utils import parse_accounts_from_message
|
|
||||||
accounts = parse_accounts_from_message(user_input)
|
|
||||||
if accounts:
|
|
||||||
account_number = accounts[0]
|
|
||||||
result = await session.execute(
|
|
||||||
select(Participation).where(
|
|
||||||
Participation.lottery_id == lottery_id,
|
|
||||||
Participation.account_number == account_number
|
|
||||||
)
|
|
||||||
)
|
|
||||||
participation = result.scalar_one_or_none()
|
|
||||||
if participation:
|
|
||||||
await session.delete(participation)
|
|
||||||
await session.commit()
|
|
||||||
removed = True
|
|
||||||
name = f"Счет: {account_number}"
|
|
||||||
|
|
||||||
await state.clear()
|
|
||||||
|
|
||||||
if removed:
|
|
||||||
await message.answer(
|
|
||||||
f"✅ Участник удалён!\n\n"
|
|
||||||
f"👤 {name}\n"
|
|
||||||
f"🎯 Розыгрыш: {lottery.title}",
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text="➖ Удалить ещё", callback_data=f"admin_remove_from_{lottery_id}")],
|
|
||||||
[InlineKeyboardButton(text="👥 К участникам", callback_data=f"admin_participants_{lottery_id}")]
|
|
||||||
])
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await message.answer(
|
|
||||||
f"⚠️ Участник не найден в этом розыгрыше",
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_participants_{lottery_id}")]
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ======================
|
|
||||||
# ПРОВЕРКА ПОБЕДИТЕЛЕЙ И ПОВТОРНЫЙ РОЗЫГРЫШ
|
|
||||||
# ======================
|
|
||||||
|
|
||||||
@admin_router.callback_query(F.data.startswith("admin_check_winners_"))
|
|
||||||
async def check_winners(callback: CallbackQuery):
|
|
||||||
"""Проверка подтверждения победителей"""
|
|
||||||
if not is_admin(callback.from_user.id):
|
|
||||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
lottery_id = int(callback.data.split("_")[-1])
|
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
|
||||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
|
||||||
if not lottery:
|
|
||||||
await callback.answer("❌ Розыгрыш не найден", show_alert=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
winners = await LotteryService.get_winners(session, lottery_id)
|
|
||||||
|
|
||||||
if not winners:
|
|
||||||
await callback.message.edit_text(
|
|
||||||
f"🏆 Проверка победителей\n\n"
|
|
||||||
f"🎯 Розыгрыш: {lottery.title}\n\n"
|
|
||||||
f"❌ Победители не определены. Сначала проведите розыгрыш.",
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_lottery_detail_{lottery_id}")]
|
|
||||||
])
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
text = f"🏆 Проверка победителей\n\n"
|
|
||||||
text += f"🎯 Розыгрыш: {lottery.title}\n\n"
|
|
||||||
|
|
||||||
confirmed_count = 0
|
|
||||||
unconfirmed_count = 0
|
|
||||||
|
|
||||||
for winner in winners:
|
|
||||||
status = "✅" if winner.is_claimed else "⏳"
|
|
||||||
|
|
||||||
if winner.is_claimed:
|
|
||||||
confirmed_count += 1
|
|
||||||
else:
|
|
||||||
unconfirmed_count += 1
|
|
||||||
|
|
||||||
# Определяем имя победителя
|
|
||||||
if winner.account_number:
|
|
||||||
name = f"Счет: {winner.account_number}"
|
|
||||||
elif winner.user:
|
|
||||||
name = f"@{winner.user.username}" if winner.user.username else winner.user.first_name
|
|
||||||
else:
|
|
||||||
name = f"ID: {winner.user_id}"
|
|
||||||
|
|
||||||
# Приз
|
|
||||||
prize = lottery.prizes[winner.place - 1] if lottery.prizes and len(lottery.prizes) >= winner.place else "Не указан"
|
|
||||||
|
|
||||||
text += f"{status} {winner.place} место: {name}\n"
|
|
||||||
text += f" 🎁 Приз: {prize}\n"
|
|
||||||
if winner.is_claimed and winner.claimed_at:
|
|
||||||
text += f" 📅 Подтверждено: {winner.claimed_at.strftime('%d.%m.%Y %H:%M')}\n"
|
|
||||||
text += "\n"
|
|
||||||
|
|
||||||
text += f"📊 Итого: {confirmed_count} подтверждено, {unconfirmed_count} ожидает\n"
|
|
||||||
|
|
||||||
buttons = []
|
|
||||||
if unconfirmed_count > 0:
|
|
||||||
buttons.append([InlineKeyboardButton(text="🔄 Переиграть неподтверждённые", callback_data=f"admin_redraw_{lottery_id}")])
|
|
||||||
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_lottery_detail_{lottery_id}")])
|
|
||||||
|
|
||||||
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.callback_query(F.data.startswith("admin_redraw_"))
|
|
||||||
async def redraw_lottery(callback: CallbackQuery):
|
|
||||||
"""Повторный розыгрыш для неподтверждённых призов"""
|
|
||||||
if not is_admin(callback.from_user.id):
|
|
||||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
lottery_id = int(callback.data.split("_")[-1])
|
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
|
||||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
|
||||||
if not lottery:
|
|
||||||
await callback.answer("❌ Розыгрыш не найден", show_alert=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
winners = await LotteryService.get_winners(session, lottery_id)
|
|
||||||
|
|
||||||
# Находим неподтверждённых победителей
|
|
||||||
unconfirmed = [w for w in winners if not w.is_claimed]
|
|
||||||
|
|
||||||
if not unconfirmed:
|
|
||||||
await callback.answer("✅ Все победители подтверждены!", show_alert=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Показываем подтверждение
|
|
||||||
text = f"⚠️ Повторный розыгрыш\n\n"
|
|
||||||
text += f"🎯 Розыгрыш: {lottery.title}\n\n"
|
|
||||||
text += f"Будут переиграны {len(unconfirmed)} неподтверждённых мест:\n\n"
|
|
||||||
|
|
||||||
for winner in unconfirmed:
|
|
||||||
if winner.account_number:
|
|
||||||
name = f"Счет: {winner.account_number}"
|
|
||||||
elif winner.user:
|
|
||||||
name = f"@{winner.user.username}" if winner.user.username else winner.user.first_name
|
|
||||||
else:
|
|
||||||
name = f"ID: {winner.user_id}"
|
|
||||||
|
|
||||||
prize = lottery.prizes[winner.place - 1] if lottery.prizes and len(lottery.prizes) >= winner.place else "Не указан"
|
|
||||||
text += f"• {winner.place} место: {name} → {prize}\n"
|
|
||||||
|
|
||||||
text += "\n❗️ Эти счета будут исключены из повторного розыгрыша."
|
|
||||||
|
|
||||||
await callback.message.edit_text(
|
|
||||||
text,
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text="✅ Подтвердить переигровку", callback_data=f"admin_redraw_confirm_{lottery_id}")],
|
|
||||||
[InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_check_winners_{lottery_id}")]
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.callback_query(F.data.startswith("admin_redraw_confirm_"))
|
|
||||||
async def confirm_redraw(callback: CallbackQuery):
|
|
||||||
"""Подтверждение и выполнение повторного розыгрыша"""
|
|
||||||
if not is_admin(callback.from_user.id):
|
|
||||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
lottery_id = int(callback.data.split("_")[-1])
|
|
||||||
|
|
||||||
await callback.answer("⏳ Проводится повторный розыгрыш...", show_alert=True)
|
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
|
||||||
from sqlalchemy import select, delete
|
|
||||||
from ..core.models import Winner, Participation
|
|
||||||
import random
|
|
||||||
|
|
||||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
|
||||||
if not lottery:
|
|
||||||
await callback.message.edit_text("❌ Розыгрыш не найден")
|
|
||||||
return
|
|
||||||
|
|
||||||
winners = await LotteryService.get_winners(session, lottery_id)
|
|
||||||
|
|
||||||
# Собираем подтверждённые и неподтверждённые
|
|
||||||
confirmed_winners = [w for w in winners if w.is_claimed]
|
|
||||||
unconfirmed_winners = [w for w in winners if not w.is_claimed]
|
|
||||||
|
|
||||||
if not unconfirmed_winners:
|
|
||||||
await callback.message.edit_text(
|
|
||||||
"✅ Все победители уже подтверждены!",
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_lottery_detail_{lottery_id}")]
|
|
||||||
])
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Собираем исключённые счета (подтверждённые победители + бывшие неподтверждённые)
|
|
||||||
excluded_accounts = set()
|
|
||||||
for w in winners:
|
|
||||||
if w.account_number:
|
|
||||||
excluded_accounts.add(w.account_number)
|
|
||||||
|
|
||||||
# Получаем всех участников, исключая уже выигравших
|
|
||||||
result = await session.execute(
|
|
||||||
select(Participation)
|
|
||||||
.where(Participation.lottery_id == lottery_id)
|
|
||||||
)
|
|
||||||
all_participations = result.scalars().all()
|
|
||||||
|
|
||||||
# Фильтруем участников
|
|
||||||
available_participations = [
|
|
||||||
p for p in all_participations
|
|
||||||
if p.account_number not in excluded_accounts
|
|
||||||
]
|
|
||||||
|
|
||||||
if not available_participations:
|
|
||||||
await callback.message.edit_text(
|
|
||||||
"❌ Нет доступных участников для переигровки.\n"
|
|
||||||
"Все участники уже выиграли или были исключены.",
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"admin_lottery_detail_{lottery_id}")]
|
|
||||||
])
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Удаляем неподтверждённых победителей
|
|
||||||
for winner in unconfirmed_winners:
|
|
||||||
await session.delete(winner)
|
|
||||||
|
|
||||||
# Проводим розыгрыш для неподтверждённых мест
|
|
||||||
new_winners_text = ""
|
|
||||||
random.shuffle(available_participations)
|
|
||||||
|
|
||||||
for i, old_winner in enumerate(unconfirmed_winners):
|
|
||||||
if i >= len(available_participations):
|
|
||||||
break
|
|
||||||
|
|
||||||
new_participation = available_participations[i]
|
|
||||||
|
|
||||||
# Создаём нового победителя
|
|
||||||
new_winner = Winner(
|
|
||||||
lottery_id=lottery_id,
|
|
||||||
user_id=new_participation.user_id,
|
|
||||||
account_number=new_participation.account_number,
|
|
||||||
place=old_winner.place,
|
|
||||||
is_manual=False,
|
|
||||||
is_claimed=False
|
|
||||||
)
|
|
||||||
session.add(new_winner)
|
|
||||||
|
|
||||||
# Исключаем из следующих итераций
|
|
||||||
if new_participation.account_number:
|
|
||||||
excluded_accounts.add(new_participation.account_number)
|
|
||||||
|
|
||||||
prize = lottery.prizes[old_winner.place - 1] if lottery.prizes and len(lottery.prizes) >= old_winner.place else "Приз"
|
|
||||||
name = new_participation.account_number or f"ID: {new_participation.user_id}"
|
|
||||||
new_winners_text += f"🏆 {old_winner.place} место: {name} → {prize}\n"
|
|
||||||
|
|
||||||
await session.commit()
|
|
||||||
|
|
||||||
# Отправляем уведомления новым победителям
|
|
||||||
from ..utils.notifications import notify_winners_async
|
|
||||||
try:
|
|
||||||
await notify_winners_async(callback.bot, session, lottery_id)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка при отправке уведомлений: {e}")
|
|
||||||
|
|
||||||
text = f"🎉 Повторный розыгрыш завершён!\n\n"
|
|
||||||
text += f"🎯 Розыгрыш: {lottery.title}\n\n"
|
|
||||||
text += f"Новые победители:\n{new_winners_text}\n"
|
|
||||||
text += "✅ Уведомления отправлены новым победителям"
|
|
||||||
|
|
||||||
await callback.message.edit_text(
|
|
||||||
text,
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text="✅ Проверить победителей", callback_data=f"admin_check_winners_{lottery_id}")],
|
|
||||||
[InlineKeyboardButton(text="🔙 К розыгрышу", callback_data=f"admin_lottery_detail_{lottery_id}")]
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ======================
|
|
||||||
# УДАЛЕНИЕ РОЗЫГРЫША
|
|
||||||
# ======================
|
|
||||||
|
|
||||||
@admin_router.callback_query(F.data.startswith("admin_del_lottery_"))
|
|
||||||
async def delete_lottery_confirm(callback: CallbackQuery):
|
|
||||||
"""Подтверждение удаления розыгрыша"""
|
|
||||||
if not is_admin(callback.from_user.id):
|
|
||||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
lottery_id = int(callback.data.split("_")[-1])
|
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
|
||||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
|
||||||
if not lottery:
|
|
||||||
await callback.answer("❌ Розыгрыш не найден", show_alert=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
participants_count = await ParticipationService.get_participants_count(session, lottery_id)
|
|
||||||
winners = await LotteryService.get_winners(session, lottery_id)
|
|
||||||
|
|
||||||
text = f"⚠️ Удаление розыгрыша\n\n"
|
|
||||||
text += f"🎯 Название: {lottery.title}\n"
|
|
||||||
text += f"👥 Участников: {participants_count}\n"
|
|
||||||
text += f"🏆 Победителей: {len(winners)}\n\n"
|
|
||||||
text += "❗️ Это действие необратимо!\n"
|
|
||||||
text += "Все данные об участниках и победителях будут удалены."
|
|
||||||
|
|
||||||
await callback.message.edit_text(
|
|
||||||
text,
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text="🗑️ Да, удалить", callback_data=f"admin_del_lottery_yes_{lottery_id}")],
|
|
||||||
[InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_lottery_detail_{lottery_id}")]
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.callback_query(F.data.startswith("admin_del_lottery_yes_"))
|
|
||||||
async def delete_lottery_execute(callback: CallbackQuery):
|
|
||||||
"""Выполнение удаления розыгрыша"""
|
|
||||||
if not is_admin(callback.from_user.id):
|
|
||||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
lottery_id = int(callback.data.split("_")[-1])
|
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
|
||||||
from sqlalchemy import delete as sql_delete
|
|
||||||
from ..core.models import Winner, Participation
|
|
||||||
|
|
||||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
|
||||||
if not lottery:
|
|
||||||
await callback.answer("❌ Розыгрыш не найден", show_alert=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
lottery_title = lottery.title
|
|
||||||
|
|
||||||
# Удаляем победителей
|
|
||||||
await session.execute(sql_delete(Winner).where(Winner.lottery_id == lottery_id))
|
|
||||||
|
|
||||||
# Удаляем участников
|
|
||||||
await session.execute(sql_delete(Participation).where(Participation.lottery_id == lottery_id))
|
|
||||||
|
|
||||||
# Удаляем розыгрыш
|
|
||||||
await session.delete(lottery)
|
|
||||||
await session.commit()
|
|
||||||
|
|
||||||
await callback.message.edit_text(
|
|
||||||
f"✅ Розыгрыш удалён\n\n"
|
|
||||||
f"🗑️ {lottery_title}",
|
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|
||||||
[InlineKeyboardButton(text="📋 К списку розыгрышей", callback_data="admin_list_all_lotteries")]
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ======================
|
# ======================
|
||||||
# МАССОВОЕ УПРАВЛЕНИЕ УЧАСТНИКАМИ ПО СЧЕТАМ
|
# МАССОВОЕ УПРАВЛЕНИЕ УЧАСТНИКАМИ ПО СЧЕТАМ
|
||||||
# ======================
|
# ======================
|
||||||
@@ -3697,6 +3119,7 @@ async def show_admin_settings(callback: CallbackQuery):
|
|||||||
buttons.extend([
|
buttons.extend([
|
||||||
[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,10 +140,7 @@ 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()
|
||||||
|
|
||||||
# Проверяем ключевые слова для выхода
|
# Проверяем ключевые слова для выхода
|
||||||
@@ -169,197 +166,8 @@ async def check_exit_keywords(message: Message, state: FSMContext):
|
|||||||
await exit_chat(message, state)
|
await exit_chat(message, state)
|
||||||
return
|
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("❌ Не удалось переслать сообщение")
|
|
||||||
|
|
||||||
|
|
||||||
# Настройки для планировщика рассылки
|
# Настройки для планировщика рассылки
|
||||||
@@ -379,11 +187,10 @@ def _is_message_processed(message_id: int) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
async def get_all_active_users(session: AsyncSession) -> List:
|
async def get_all_active_users(session: AsyncSession) -> List:
|
||||||
"""Получить всех пользователей для рассылки (всем, кто когда-либо общался с ботом)"""
|
"""Получить всех пользователей для рассылки (зарегистрированные + админы)"""
|
||||||
users = await UserService.get_all_users(session)
|
users = await UserService.get_all_users(session)
|
||||||
# Рассылаем всем пользователям - и зарегистрированным, и незарегистрированным
|
# Рассылаем зарегистрированным пользователям И админам (даже если они не зарегистрированы)
|
||||||
# Они все имеют право общаться в чате (главное - что они вошли в чат)
|
return [u for u in users if u.is_registered or u.telegram_id in ADMIN_IDS]
|
||||||
return users
|
|
||||||
|
|
||||||
|
|
||||||
async def broadcast_message_with_scheduler(
|
async def broadcast_message_with_scheduler(
|
||||||
@@ -406,22 +213,15 @@ async def broadcast_message_with_scheduler(
|
|||||||
|
|
||||||
Возвращает: (forwarded_ids, success_count, fail_count)
|
Возвращает: (forwarded_ids, success_count, fail_count)
|
||||||
"""
|
"""
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
users = await get_all_active_users(session)
|
users = await get_all_active_users(session)
|
||||||
|
|
||||||
logger.info(f"[CHAT] broadcast_message_with_scheduler: всего пользователей для рассылки: {len(users)}")
|
|
||||||
|
|
||||||
if exclude_user_id:
|
if exclude_user_id:
|
||||||
users = [u for u in users if u.telegram_id != 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:
|
if admin_only:
|
||||||
users = [u for u in users if u.telegram_id in ADMIN_IDS]
|
users = [u for u in users if u.telegram_id in ADMIN_IDS]
|
||||||
logger.info(f"[CHAT] Фильтр админов: {len(users)} пользователей")
|
|
||||||
|
|
||||||
forwarded_ids = {}
|
forwarded_ids = {}
|
||||||
success_count = 0
|
success_count = 0
|
||||||
@@ -474,7 +274,6 @@ async def broadcast_message_with_scheduler(
|
|||||||
if i + BATCH_SIZE < len(users):
|
if i + BATCH_SIZE < len(users):
|
||||||
await asyncio.sleep(BATCH_DELAY)
|
await asyncio.sleep(BATCH_DELAY)
|
||||||
|
|
||||||
logger.info(f"[CHAT] broadcast_message_with_scheduler завершена: успешно={success_count}, ошибок={fail_count}")
|
|
||||||
return forwarded_ids, success_count, fail_count
|
return forwarded_ids, success_count, fail_count
|
||||||
|
|
||||||
|
|
||||||
@@ -660,6 +459,203 @@ async def forward_to_channel(message: Message, channel_id: str) -> tuple[bool, O
|
|||||||
return False, None
|
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'}")
|
||||||
|
|
||||||
|
# ПРОВЕРКА СЧЕТОВ: Если админ отправил сообщение с номерами счетов - НЕ рассылаем
|
||||||
|
# Пропускаем для 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("❌ Не удалось переслать сообщение")
|
||||||
|
|
||||||
|
|
||||||
@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):
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ async def handle_p2p_message(message: Message, state: FSMContext):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Получаем информацию о получателе для определения как подписать сообщение
|
# Получаем информацию о получателе для определения как подписать сообщение
|
||||||
recipient = await UserService.get_by_telegram_id(session, recipient_telegram_id)
|
recipient = await UserService.get_user_by_telegram_id(session, recipient_telegram_id)
|
||||||
|
|
||||||
# Формируем подпись сообщения для получателя
|
# Формируем подпись сообщения для получателя
|
||||||
if sender.is_admin:
|
if sender.is_admin:
|
||||||
|
|||||||
101
test_chat_fix.md
101
test_chat_fix.md
@@ -1,101 +0,0 @@
|
|||||||
# Исправление функции чата
|
|
||||||
|
|
||||||
## 🔴 Проблема
|
|
||||||
При переходе в чат, сообщения не отправлялись другим участникам. Пользователи не получали сообщения друг от друга.
|
|
||||||
|
|
||||||
## 🔍 Корневые причины (найдено ДВЕ)
|
|
||||||
|
|
||||||
### Причина 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