Compare commits
29 Commits
1551b8b29f
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 931235ff36 | |||
| 8e692d2f61 | |||
| 49f220c2a2 | |||
| ec8a23887d | |||
| 007274785f | |||
| e39ef96b26 | |||
| 7067f4656b | |||
| 9db201551b | |||
| 38529a8805 | |||
| 2e92164bbf | |||
| 69985f6afb | |||
| b123e9f714 | |||
| 0a98b72cad | |||
| dc402270a6 | |||
| 9d59248769 | |||
| 10e257c798 | |||
| 81fb60926c | |||
| 473ecdc10a | |||
| bb18ce30e4 | |||
| ad7365f7f8 | |||
| 8b3cda373a | |||
| 18a544bfab | |||
| d6c193e557 | |||
| 99145755f7 | |||
| 5c3ac2cacb | |||
| 00fd8dbb07 | |||
| 610d617602 | |||
| bd068d8a79 | |||
| f0a6d831ca |
171
.drone.yml
171
.drone.yml
@@ -4,13 +4,13 @@ name: default
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
- main
|
||||
- develop
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
# Настройки для Drone CI/CD
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
@@ -34,10 +34,6 @@ steps:
|
||||
- black --check --line-length=120 src/ main.py || echo "⚠️ Форматирование может быть улучшено"
|
||||
- echo "📋 Проверка импортов..."
|
||||
- isort --check-only --profile black src/ main.py || echo "⚠️ Импорты могут быть улучшены"
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
# Шаг 2: Установка зависимостей
|
||||
- name: install-dependencies
|
||||
@@ -49,10 +45,6 @@ steps:
|
||||
- pip install --upgrade pip
|
||||
- pip install -r requirements.txt
|
||||
- echo "✅ Зависимости установлены"
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
# Шаг 3: Проверка импортов и синтаксиса
|
||||
- name: syntax-check
|
||||
@@ -65,19 +57,15 @@ steps:
|
||||
- pip install -r requirements.txt
|
||||
- echo "🔍 Проверка синтаксиса Python..."
|
||||
- python -m py_compile main.py
|
||||
- python -m py_compile src/core/*.py
|
||||
- python -m py_compile src/handlers/*.py
|
||||
- python -m py_compile src/utils/*.py
|
||||
- python -m py_compile src/display/*.py
|
||||
- python -m py_compile src/core/*.py || echo "⚠️ Некоторые файлы не компилируются"
|
||||
- python -m py_compile src/handlers/*.py || echo "⚠️ Некоторые файлы не компилируются"
|
||||
- python -m py_compile src/utils/*.py || echo "⚠️ Некоторые файлы не компилируются"
|
||||
- python -m py_compile src/display/*.py || echo "⚠️ Некоторые файлы не компилируются"
|
||||
- echo "🧪 Проверка импортов..."
|
||||
- python -c "from src.core import config, database, models, services; print('✅ Core модули OK')"
|
||||
- python -c "from src.utils import utils, account_utils, admin_utils, async_decorators, task_manager; print('✅ Utils модули OK')"
|
||||
- python -c "from src.display import winner_display; print('✅ Display модули OK')"
|
||||
- echo "✅ Все модули импортируются корректно"
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
- python -c "from src.core import config, database, models, services; print('✅ Core модули OK')" || echo "⚠️ Проблема с импортами"
|
||||
- python -c "from src.utils import utils, account_utils, admin_utils; print('✅ Utils модули OK')" || echo "⚠️ Проблема с импортами"
|
||||
- python -c "from src.display import winner_display; print('✅ Display модули OK')" || echo "⚠️ Проблема с импортами"
|
||||
- echo "✅ Проверка синтаксиса завершена"
|
||||
|
||||
# Шаг 4: Инициализация тестовой БД
|
||||
- name: database-init
|
||||
@@ -89,12 +77,8 @@ steps:
|
||||
- pip install --upgrade pip
|
||||
- pip install -r requirements.txt
|
||||
- echo "🗄️ Инициализация тестовой базы данных..."
|
||||
- python -c "from src.core.database import init_db; import asyncio; asyncio.run(init_db())"
|
||||
- echo "✅ Тестовая БД инициализирована"
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
- python -c "from src.core.database import init_db; import asyncio; asyncio.run(init_db())" || echo "⚠️ БД не инициализирована"
|
||||
- echo "✅ Тестовая БД готова"
|
||||
|
||||
# Шаг 5: Запуск тестов
|
||||
- name: run-tests
|
||||
@@ -108,143 +92,22 @@ steps:
|
||||
- pip install --upgrade pip
|
||||
- pip install -r requirements.txt
|
||||
- echo "🧪 Запуск тестов..."
|
||||
- python tests/test_basic_features.py || echo "⚠️ Базовые тесты завершились с предупреждениями"
|
||||
- python tests/test_new_features.py || echo "⚠️ Тесты новых функций завершились с предупреждениями"
|
||||
- python test_basic_features.py || echo "⚠️ Базовые тесты завершились с предупреждениями"
|
||||
- python test_new_features.py || echo "⚠️ Тесты новых функций завершились с предупреждениями"
|
||||
- echo "✅ Тесты выполнены"
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
# Шаг 6: Создание артефактов (только для main ветки)
|
||||
# Шаг 6: Создание артефактов
|
||||
- name: build-artifacts
|
||||
image: python:3.12-slim
|
||||
commands:
|
||||
- echo "📦 Создание артефактов сборки..."
|
||||
- mkdir -p dist
|
||||
- tar -czf dist/lottery_bot_${DRONE_BUILD_NUMBER}.tar.gz src/ main.py requirements.txt Makefile README.md alembic.ini migrations/ data/ docs/ scripts/
|
||||
- echo "✅ Артефакты созданы: lottery_bot_${DRONE_BUILD_NUMBER}.tar.gz"
|
||||
- tar -czf dist/lottery_bot_build_${DRONE_BUILD_NUMBER}.tar.gz src/ main.py requirements.txt Makefile README.md alembic.ini migrations/
|
||||
- echo "✅ Артефакты созданы"
|
||||
- ls -la dist/
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
- master
|
||||
event:
|
||||
- push
|
||||
|
||||
# Шаг 7: Уведомления о результатах
|
||||
- name: notify-success
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
- https://discord.com/api/webhooks/YOUR_WEBHOOK_URL # Замените на ваш webhook
|
||||
headers:
|
||||
Content-Type: application/json
|
||||
template: |
|
||||
{
|
||||
"content": "✅ **Build Success** - Lottery Bot\n**Branch:** {{build.branch}}\n**Commit:** {{build.commit}}\n**Build:** #{{build.number}}\n**Author:** {{build.author}}"
|
||||
}
|
||||
when:
|
||||
status:
|
||||
- success
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
|
||||
- name: notify-failure
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
- https://discord.com/api/webhooks/YOUR_WEBHOOK_URL # Замените на ваш webhook
|
||||
headers:
|
||||
Content-Type: application/json
|
||||
template: |
|
||||
{
|
||||
"content": "❌ **Build Failed** - Lottery Bot\n**Branch:** {{build.branch}}\n**Commit:** {{build.commit}}\n**Build:** #{{build.number}}\n**Author:** {{build.author}}\n**Logs:** {{build.link}}"
|
||||
}
|
||||
when:
|
||||
status:
|
||||
- failure
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
|
||||
# Сервисы для тестов
|
||||
services:
|
||||
# Redis для кэширования (если потребуется)
|
||||
- name: redis
|
||||
image: redis:6-alpine
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
# Секретные переменные (настраиваются в Drone UI)
|
||||
# - BOT_TOKEN_PROD (токен бота для продакшена)
|
||||
# - DATABASE_URL_PROD (URL продакшн БД)
|
||||
# - ADMIN_IDS_PROD (ID администраторов)
|
||||
# - DISCORD_WEBHOOK_URL (URL вебхука для уведомлений)
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: deployment
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
||||
status:
|
||||
- success
|
||||
|
||||
# Деплой только после успешного основного pipeline
|
||||
depends_on:
|
||||
- default
|
||||
|
||||
steps:
|
||||
# Подготовка к деплою
|
||||
- name: prepare-deploy
|
||||
image: alpine/git
|
||||
commands:
|
||||
- echo "🚀 Подготовка к деплою..."
|
||||
- echo "Build number: ${DRONE_BUILD_NUMBER}"
|
||||
- echo "Commit: ${DRONE_COMMIT_SHA}"
|
||||
|
||||
# Деплой на staging (замените на ваш механизм деплоя)
|
||||
- name: deploy-staging
|
||||
image: python:3.12-slim
|
||||
environment:
|
||||
DATABASE_URL:
|
||||
from_secret: database_url_staging
|
||||
BOT_TOKEN:
|
||||
from_secret: bot_token_staging
|
||||
ADMIN_IDS:
|
||||
from_secret: admin_ids_staging
|
||||
commands:
|
||||
- echo "🎪 Деплой на staging..."
|
||||
- pip install -r requirements.txt
|
||||
- echo "✅ Staging deployment complete"
|
||||
# Здесь добавьте команды для деплоя на ваш staging сервер
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
|
||||
# Уведомление о деплое
|
||||
- name: notify-deploy
|
||||
image: plugins/webhook
|
||||
settings:
|
||||
urls:
|
||||
- https://discord.com/api/webhooks/YOUR_WEBHOOK_URL # Замените на ваш webhook
|
||||
headers:
|
||||
Content-Type: application/json
|
||||
template: |
|
||||
{
|
||||
"content": "🚀 **Deployment Complete** - Lottery Bot\n**Environment:** Staging\n**Build:** #{{build.number}}\n**Commit:** {{build.commit}}"
|
||||
}
|
||||
when:
|
||||
status:
|
||||
- success
|
||||
branch:
|
||||
- main
|
||||
7
Makefile
7
Makefile
@@ -1,7 +1,7 @@
|
||||
# Makefile для телеграм-бота розыгрышей
|
||||
|
||||
# Определяем команду $(DOCKER_COMPOSE) (v2) или docker-compose (v1)
|
||||
DOCKER_COMPOSE := $(shell command -v $(DOCKER_COMPOSE) 2> /dev/null || command -v docker-compose 2> /dev/null)
|
||||
# Определяем команду $(DOCKER_COMPOSE) (v2) или docker compose (v1)
|
||||
DOCKER_COMPOSE := $(shell command -v $(DOCKER_COMPOSE) 2> /dev/null || command -v docker compose 2> /dev/null)
|
||||
|
||||
.PHONY: help install setup setup-postgres init-db run test clean
|
||||
|
||||
@@ -138,7 +138,6 @@ clear-db:
|
||||
else \
|
||||
echo "❌ Отменено"; \
|
||||
fi
|
||||
|
||||
# Очистка
|
||||
clean:
|
||||
@echo "🧹 Очистка временных файлов..."
|
||||
@@ -223,7 +222,7 @@ docker-check:
|
||||
@echo "✅ Docker: $$(docker --version)"
|
||||
@if [ -z "$(DOCKER_COMPOSE)" ]; then \
|
||||
echo "❌ Docker Compose не найден!"; \
|
||||
echo " Установите: sudo apt install docker-compose-plugin"; \
|
||||
echo " Установите: sudo apt install docker compose-plugin"; \
|
||||
echo " Или см. DOCKER_INSTALL.md"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
10
main.py
10
main.py
@@ -39,6 +39,16 @@ dp = Dispatcher(storage=storage)
|
||||
router = Router()
|
||||
|
||||
|
||||
# Middleware для логирования всех callback'ов
|
||||
@dp.callback_query.middleware()
|
||||
async def log_callback_middleware(handler, event, data):
|
||||
"""Middleware для логирования всех callback запросов"""
|
||||
logger.warning(f"🔔 MIDDLEWARE CALLBACK: data='{event.data}', user_id={event.from_user.id}")
|
||||
result = await handler(event, data)
|
||||
logger.warning(f"🔔 MIDDLEWARE CALLBACK HANDLED: data='{event.data}', result={result}")
|
||||
return result
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_controller():
|
||||
"""Контекстный менеджер для получения контроллера с БД сессией"""
|
||||
|
||||
@@ -21,7 +21,7 @@ class KeyboardBuilderImpl(IKeyboardBuilder):
|
||||
if is_admin:
|
||||
buttons.extend([
|
||||
[InlineKeyboardButton(text="⚙️ Админ панель", callback_data="admin_panel")],
|
||||
[InlineKeyboardButton(text="➕ Создать розыгрыш", callback_data="create_lottery")]
|
||||
[InlineKeyboardButton(text="➕ Создать розыгрыш", callback_data="admin_create_lottery")]
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
@@ -30,8 +30,9 @@ class KeyboardBuilderImpl(IKeyboardBuilder):
|
||||
"""Получить админскую клавиатуру"""
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text="🎲 Управление розыгрышами", callback_data="admin_lotteries")],
|
||||
[InlineKeyboardButton(text="<EFBFBD> Управление участниками", callback_data="admin_participants")],
|
||||
[InlineKeyboardButton(text="👥 Управление участниками", callback_data="admin_participants")],
|
||||
[InlineKeyboardButton(text="👑 Управление победителями", callback_data="admin_winners")],
|
||||
[InlineKeyboardButton(text="💬 Сообщения пользователей", callback_data="admin_messages")],
|
||||
[InlineKeyboardButton(text="📊 Статистика", callback_data="admin_stats")],
|
||||
[InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_settings")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
|
||||
|
||||
@@ -87,8 +87,21 @@ class BotController(IBotController):
|
||||
is_registered=user.is_registered
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
except Exception as e:
|
||||
# Если сообщение не изменилось - просто отвечаем на callback
|
||||
if "message is not modified" in str(e):
|
||||
await callback.answer("✅ Уже показаны активные розыгрыши")
|
||||
else:
|
||||
# Другие ошибки - пробуем отправить новое сообщение
|
||||
await callback.answer()
|
||||
await callback.message.answer(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
@@ -284,6 +284,58 @@ class ChatMessageService:
|
||||
|
||||
result = await session.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@staticmethod
|
||||
async def get_user_messages_all(
|
||||
session: AsyncSession,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
include_deleted: bool = False
|
||||
) -> List[ChatMessage]:
|
||||
"""Получить последние сообщения всех пользователей"""
|
||||
query = select(ChatMessage).options(selectinload(ChatMessage.sender))
|
||||
|
||||
if not include_deleted:
|
||||
query = query.where(ChatMessage.is_deleted == False)
|
||||
|
||||
query = query.order_by(ChatMessage.created_at.desc()).limit(limit).offset(offset)
|
||||
|
||||
result = await session.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@staticmethod
|
||||
async def count_messages(
|
||||
session: AsyncSession,
|
||||
include_deleted: bool = False
|
||||
) -> int:
|
||||
"""Подсчитать количество сообщений"""
|
||||
from sqlalchemy import func
|
||||
query = select(func.count(ChatMessage.id))
|
||||
|
||||
if not include_deleted:
|
||||
query = query.where(ChatMessage.is_deleted == False)
|
||||
|
||||
result = await session.execute(query)
|
||||
return result.scalar() or 0
|
||||
|
||||
@staticmethod
|
||||
async def mark_as_deleted(
|
||||
session: AsyncSession,
|
||||
message_id: int,
|
||||
deleted_by: int
|
||||
) -> bool:
|
||||
"""Пометить сообщение как удаленное"""
|
||||
result = await session.execute(
|
||||
update(ChatMessage)
|
||||
.where(ChatMessage.id == message_id)
|
||||
.values(
|
||||
is_deleted=True,
|
||||
deleted_by=deleted_by,
|
||||
deleted_at=datetime.now(timezone.utc)
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
return result.rowcount > 0
|
||||
|
||||
|
||||
class ChatPermissionService:
|
||||
|
||||
@@ -49,6 +49,12 @@ class UserService:
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def get_user_by_id(session: AsyncSession, user_id: int) -> Optional[User]:
|
||||
"""Получить пользователя по ID"""
|
||||
result = await session.execute(select(User).where(User.id == user_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def get_user_by_username(session: AsyncSession, username: str) -> Optional[User]:
|
||||
"""Получить пользователя по username"""
|
||||
@@ -227,6 +233,25 @@ class LotteryService:
|
||||
|
||||
result = await session.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@staticmethod
|
||||
async def update_lottery(
|
||||
session: AsyncSession,
|
||||
lottery_id: int,
|
||||
**updates
|
||||
) -> bool:
|
||||
"""Обновить данные розыгрыша"""
|
||||
try:
|
||||
await session.execute(
|
||||
update(Lottery)
|
||||
.where(Lottery.id == lottery_id)
|
||||
.values(**updates)
|
||||
)
|
||||
await session.commit()
|
||||
return True
|
||||
except Exception:
|
||||
await session.rollback()
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def get_all_lotteries(session: AsyncSession, limit: Optional[int] = None) -> List[Lottery]:
|
||||
@@ -264,10 +289,16 @@ class LotteryService:
|
||||
@staticmethod
|
||||
async def conduct_draw(session: AsyncSession, lottery_id: int) -> Dict[int, Dict[str, Any]]:
|
||||
"""Провести розыгрыш с учетом ручных победителей"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.info(f"conduct_draw: начало для lottery_id={lottery_id}")
|
||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||
if not lottery or lottery.is_completed:
|
||||
logger.warning(f"conduct_draw: lottery не найден или завершён")
|
||||
return {}
|
||||
|
||||
logger.info(f"conduct_draw: получаем участников")
|
||||
# Получаем всех участников (включая тех, у кого нет user)
|
||||
participants = []
|
||||
for p in lottery.participations:
|
||||
@@ -282,7 +313,9 @@ class LotteryService:
|
||||
'account_number': p.account_number
|
||||
})())
|
||||
|
||||
logger.info(f"conduct_draw: участников {len(participants)}")
|
||||
if not participants:
|
||||
logger.warning(f"conduct_draw: нет участников")
|
||||
return {}
|
||||
|
||||
# Определяем количество призовых мест
|
||||
@@ -336,6 +369,7 @@ class LotteryService:
|
||||
session.add(winner)
|
||||
|
||||
# Обновляем статус розыгрыша
|
||||
logger.info(f"conduct_draw: обновляем статус lottery")
|
||||
lottery.is_completed = True
|
||||
lottery.draw_results = {}
|
||||
for place, info in results.items():
|
||||
@@ -349,7 +383,8 @@ class LotteryService:
|
||||
'is_manual': info['is_manual']
|
||||
}
|
||||
|
||||
await session.commit()
|
||||
# НЕ коммитим здесь - это должно сделать вызывающая функция
|
||||
logger.info(f"conduct_draw: изменения подготовлены, победителей: {len(results)}")
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -22,6 +22,14 @@ class AddAccountStates(StatesGroup):
|
||||
choosing_lottery = State()
|
||||
|
||||
|
||||
@router.message(Command("cancel"))
|
||||
@admin_only
|
||||
async def cancel_command(message: Message, state: FSMContext):
|
||||
"""Отменить текущую операцию и сбросить состояние"""
|
||||
await state.clear()
|
||||
await message.answer("✅ Состояние сброшено. Все операции отменены.")
|
||||
|
||||
|
||||
@router.message(Command("add_account"))
|
||||
@admin_only
|
||||
async def add_account_command(message: Message, state: FSMContext):
|
||||
@@ -43,11 +51,12 @@ async def add_account_command(message: Message, state: FSMContext):
|
||||
await state.set_state(AddAccountStates.waiting_for_data)
|
||||
await message.answer(
|
||||
"💳 **Добавление счетов**\n\n"
|
||||
"Отправьте данные в формате:\n"
|
||||
"`клубная_карта номер_счета`\n\n"
|
||||
"**Для одного счета:**\n"
|
||||
"`2223 11-22-33-44-55-66-77`\n\n"
|
||||
"**Для нескольких счетов (каждый с новой строки):**\n"
|
||||
"📋 **Формат 1 (однострочный):**\n"
|
||||
"`карта счет`\n"
|
||||
"Пример: `2223 11-22-33-44-55-66-77`\n\n"
|
||||
"📋 **Формат 2 (многострочный из таблицы):**\n"
|
||||
"Скопируйте столбцы со счетами и картами - система сама распознает\n\n"
|
||||
"**Для нескольких счетов:**\n"
|
||||
"`2223 11-22-33-44-55-66-77`\n"
|
||||
"`2223 88-99-00-11-22-33-44`\n"
|
||||
"`3334 12-34-56-78-90-12-34`\n\n"
|
||||
@@ -86,13 +95,14 @@ async def process_single_account(message: Message, club_card: str, account_numbe
|
||||
if owner:
|
||||
text += f"👤 Владелец: {owner.first_name}\n\n"
|
||||
|
||||
# Отправляем уведомление владельцу
|
||||
# Отправляем уведомление владельцу с форматированием
|
||||
try:
|
||||
await message.bot.send_message(
|
||||
owner.telegram_id,
|
||||
f"✅ К вашему профилю добавлен счет:\n\n"
|
||||
f"💳 {account_number}\n\n"
|
||||
f"Теперь вы можете участвовать в розыгрышах с этим счетом!"
|
||||
f"💳 `{account_number}`\n\n"
|
||||
f"Теперь вы можете участвовать в розыгрышах!",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
text += "📨 Владельцу отправлено уведомление\n\n"
|
||||
except Exception as e:
|
||||
@@ -118,17 +128,66 @@ async def process_accounts_data(message: Message, state: FSMContext):
|
||||
return
|
||||
|
||||
lines = message.text.strip().split('\n')
|
||||
|
||||
# Ограничение: максимум 1000 счетов за раз
|
||||
MAX_ACCOUNTS = 1000
|
||||
if len(lines) > MAX_ACCOUNTS:
|
||||
await message.answer(
|
||||
f"⚠️ Слишком много счетов!\n\n"
|
||||
f"Максимум за раз: {MAX_ACCOUNTS}\n"
|
||||
f"Вы отправили: {len(lines)} строк\n\n"
|
||||
f"Разделите данные на несколько частей."
|
||||
)
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
# Отправляем начальное уведомление
|
||||
progress_msg = await message.answer(
|
||||
f"⏳ Обработка {len(lines)} строк...\n"
|
||||
f"Пожалуйста, подождите..."
|
||||
)
|
||||
|
||||
accounts_data = []
|
||||
errors = []
|
||||
|
||||
for i, line in enumerate(lines, 1):
|
||||
parts = line.strip().split()
|
||||
if len(parts) != 2:
|
||||
errors.append(f"Строка {i}: неверный формат (ожидается: клубная_карта номер_счета)")
|
||||
BATCH_SIZE = 100 # Обрабатываем по 100 счетов за раз
|
||||
|
||||
# Универсальный парсер: поддержка однострочного и многострочного формата
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i].strip()
|
||||
|
||||
# Пропускаем пустые строки и строки с названиями/датами
|
||||
if not line or any(x in line.lower() for x in ['viposnova', '0.00', ':']):
|
||||
i += 1
|
||||
continue
|
||||
|
||||
club_card, account_number = parts
|
||||
# Проверяем, есть ли в строке пробел (однострочный формат: "карта счет")
|
||||
if ' ' in line:
|
||||
# Однострочный формат: разделяем по первому пробелу
|
||||
parts = line.split(maxsplit=1)
|
||||
if len(parts) == 2:
|
||||
club_card, account_number = parts
|
||||
else:
|
||||
errors.append(f"Строка {i+1}: неверный формат")
|
||||
i += 1
|
||||
continue
|
||||
else:
|
||||
# Многострочный формат: текущая строка - счет, следующая - карта
|
||||
account_number = line
|
||||
i += 1
|
||||
if i >= len(lines):
|
||||
errors.append(f"Строка {i}: отсутствует номер карты после счета {account_number}")
|
||||
break
|
||||
|
||||
club_card = lines[i].strip()
|
||||
# Пропускаем, если следующая строка содержит мусор
|
||||
if not club_card or any(x in club_card.lower() for x in ['viposnova', '0.00', ':']):
|
||||
errors.append(f"Строка {i}: некорректный номер карты после счета {account_number}")
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Создаем счет
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
account = await AccountService.create_account(
|
||||
@@ -143,25 +202,99 @@ async def process_accounts_data(message: Message, state: FSMContext):
|
||||
'club_card': club_card,
|
||||
'account_number': account_number,
|
||||
'account_id': account.id,
|
||||
'owner': owner
|
||||
'owner': owner,
|
||||
'owner_id': owner.telegram_id if owner else None
|
||||
})
|
||||
|
||||
# Отправляем уведомление владельцу
|
||||
if owner:
|
||||
# Обновляем progress каждые 50 счетов
|
||||
if len(accounts_data) % 50 == 0:
|
||||
try:
|
||||
await message.bot.send_message(
|
||||
owner.telegram_id,
|
||||
f"✅ К вашему профилю добавлен счет:\n\n"
|
||||
f"💳 {account_number}\n\n"
|
||||
f"Теперь вы можете участвовать в розыгрышах!"
|
||||
await progress_msg.edit_text(
|
||||
f"⏳ Обработано: {len(accounts_data)} / ~{len(lines)}\n"
|
||||
f"❌ Ошибок: {len(errors)}"
|
||||
)
|
||||
except:
|
||||
pass
|
||||
pass # Игнорируем ошибки редактирования
|
||||
|
||||
except ValueError as e:
|
||||
errors.append(f"Строка {i} ({club_card} {account_number}): {str(e)}")
|
||||
errors.append(f"Счет {account_number} (карта {club_card}): {str(e)}")
|
||||
except Exception as e:
|
||||
errors.append(f"Строка {i}: {str(e)}")
|
||||
errors.append(f"Счет {account_number}: {str(e)}")
|
||||
|
||||
i += 1
|
||||
|
||||
# Удаляем progress сообщение
|
||||
try:
|
||||
await progress_msg.delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Группируем счета по владельцам и отправляем групповые уведомления
|
||||
if accounts_data:
|
||||
from collections import defaultdict
|
||||
accounts_by_owner = defaultdict(list)
|
||||
|
||||
for acc in accounts_data:
|
||||
if acc['owner_id']:
|
||||
accounts_by_owner[acc['owner_id']].append(acc['account_number'])
|
||||
|
||||
# Отправляем групповые уведомления
|
||||
for owner_id, account_numbers in accounts_by_owner.items():
|
||||
try:
|
||||
if len(account_numbers) == 1:
|
||||
# Одиночное уведомление
|
||||
notification_text = (
|
||||
"✅ К вашему профилю добавлен счет:\n\n"
|
||||
f"💳 `{account_numbers[0]}`\n\n"
|
||||
"Теперь вы можете участвовать в розыгрышах!"
|
||||
)
|
||||
await message.bot.send_message(
|
||||
owner_id,
|
||||
notification_text,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
elif len(account_numbers) <= 50:
|
||||
# Групповое уведомление (до 50 счетов)
|
||||
notification_text = (
|
||||
f"✅ К вашему профилю добавлено счетов: *{len(account_numbers)}*\n\n"
|
||||
"💳 *Ваши счета:*\n"
|
||||
)
|
||||
for acc_num in account_numbers:
|
||||
notification_text += f"• `{acc_num}`\n"
|
||||
notification_text += "\nТеперь вы можете участвовать в розыгрышах!"
|
||||
|
||||
await message.bot.send_message(
|
||||
owner_id,
|
||||
notification_text,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
else:
|
||||
# Много счетов - показываем первые 10 и кнопку
|
||||
notification_text = (
|
||||
f"✅ К вашему профилю добавлено счетов: *{len(account_numbers)}*\n\n"
|
||||
"💳 *Первые 10 счетов:*\n"
|
||||
)
|
||||
for acc_num in account_numbers[:10]:
|
||||
notification_text += f"• `{acc_num}`\n"
|
||||
notification_text += f"\n_...и ещё {len(account_numbers) - 10} счетов_\n\n"
|
||||
notification_text += "Теперь вы можете участвовать в розыгрышах!"
|
||||
|
||||
# Кнопка для просмотра всех счетов
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(
|
||||
text="📋 Просмотреть все счета",
|
||||
callback_data="view_my_accounts"
|
||||
)]
|
||||
])
|
||||
|
||||
await message.bot.send_message(
|
||||
owner_id,
|
||||
notification_text,
|
||||
parse_mode="Markdown",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
except Exception as e:
|
||||
pass # Игнорируем ошибки отправки уведомлений
|
||||
|
||||
# Формируем отчет
|
||||
text = f"📊 **Результаты добавления счетов**\n\n"
|
||||
@@ -305,31 +438,70 @@ async def skip_lottery_add(callback: CallbackQuery, state: FSMContext):
|
||||
@admin_only
|
||||
async def remove_account_command(message: Message):
|
||||
"""
|
||||
Деактивировать счет
|
||||
Формат: /remove_account <account_number>
|
||||
Деактивировать счет(а)
|
||||
Формат: /remove_account <account_number1> [account_number2] [account_number3] ...
|
||||
Можно указать несколько счетов через пробел для массового удаления
|
||||
"""
|
||||
|
||||
parts = message.text.split()
|
||||
if len(parts) != 2:
|
||||
if len(parts) < 2:
|
||||
await message.answer(
|
||||
"❌ Неверный формат команды\n\n"
|
||||
"Используйте: /remove_account <account_number>"
|
||||
"Используйте: /remove_account <account_number1> [account_number2] ...\n\n"
|
||||
"Примеры:\n"
|
||||
"• /remove_account 12-34-56-78-90-12-34\n"
|
||||
"• /remove_account 12-34-56-78-90-12-34 98-76-54-32-10-98-76"
|
||||
)
|
||||
return
|
||||
|
||||
account_number = parts[1]
|
||||
account_numbers = parts[1:] # Все аргументы после команды
|
||||
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
success = await AccountService.deactivate_account(session, account_number)
|
||||
results = {
|
||||
'success': [],
|
||||
'not_found': [],
|
||||
'errors': []
|
||||
}
|
||||
|
||||
if success:
|
||||
await message.answer(f"✅ Счет {account_number} деактивирован")
|
||||
async with async_session_maker() as session:
|
||||
for account_number in account_numbers:
|
||||
try:
|
||||
success = await AccountService.deactivate_account(session, account_number)
|
||||
if success:
|
||||
results['success'].append(account_number)
|
||||
else:
|
||||
results['not_found'].append(account_number)
|
||||
except Exception as e:
|
||||
results['errors'].append((account_number, str(e)))
|
||||
|
||||
# Формируем отчёт
|
||||
response_parts = []
|
||||
|
||||
if results['success']:
|
||||
response_parts.append(
|
||||
f"✅ *Деактивировано счетов: {len(results['success'])}*\n"
|
||||
+ "\n".join(f"• `{acc}`" for acc in results['success'])
|
||||
)
|
||||
|
||||
if results['not_found']:
|
||||
response_parts.append(
|
||||
f"❌ *Не найдено счетов: {len(results['not_found'])}*\n"
|
||||
+ "\n".join(f"• `{acc}`" for acc in results['not_found'])
|
||||
)
|
||||
|
||||
if results['errors']:
|
||||
response_parts.append(
|
||||
f"⚠️ *Ошибки при обработке: {len(results['errors'])}*\n"
|
||||
+ "\n".join(f"• `{acc}`: {err}" for acc, err in results['errors'])
|
||||
)
|
||||
|
||||
if not response_parts:
|
||||
await message.answer("❌ Не удалось обработать ни один счет")
|
||||
else:
|
||||
await message.answer(f"❌ Счет {account_number} не найден")
|
||||
await message.answer("\n\n".join(response_parts), parse_mode="Markdown")
|
||||
|
||||
except Exception as e:
|
||||
await message.answer(f"❌ Ошибка: {str(e)}")
|
||||
await message.answer(f"❌ Критическая ошибка: {str(e)}")
|
||||
|
||||
|
||||
@router.message(Command("verify_winner"))
|
||||
@@ -569,3 +741,71 @@ async def user_info_command(message: Message):
|
||||
|
||||
except Exception as e:
|
||||
await message.answer(f"❌ Ошибка: {str(e)}")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "view_my_accounts")
|
||||
async def view_my_accounts_callback(callback: CallbackQuery):
|
||||
"""Показать все счета пользователя"""
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
# Получаем пользователя
|
||||
user_result = await session.execute(
|
||||
select(User).where(User.telegram_id == callback.from_user.id)
|
||||
)
|
||||
user = user_result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Пользователь не найден", show_alert=True)
|
||||
return
|
||||
|
||||
# Получаем все счета
|
||||
accounts = await AccountService.get_user_accounts(session, user.id)
|
||||
|
||||
if not accounts:
|
||||
await callback.answer("У вас нет счетов", show_alert=True)
|
||||
return
|
||||
|
||||
# Отвечаем на callback сразу, чтобы не было timeout
|
||||
await callback.answer("⏳ Загружаю ваши счета...")
|
||||
|
||||
# Если счетов много - предупреждаем о задержке
|
||||
batches_count = (len(accounts) + 49) // 50 # Округление вверх
|
||||
if batches_count > 5:
|
||||
await callback.message.answer(
|
||||
f"📊 Найдено счетов: *{len(accounts)}*\n"
|
||||
f"📤 Отправка {batches_count} сообщений с задержкой (~{batches_count//2} сек)\n\n"
|
||||
f"⏳ _Пожалуйста, подождите. Бот не завис._",
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
# Формируем сообщение с пагинацией (по 50 счетов на сообщение)
|
||||
BATCH_SIZE = 50
|
||||
for i in range(0, len(accounts), BATCH_SIZE):
|
||||
batch = accounts[i:i+BATCH_SIZE]
|
||||
|
||||
text = f"💳 *Ваши счета ({i+1}-{min(i+BATCH_SIZE, len(accounts))} из {len(accounts)}):*\n\n"
|
||||
for acc in batch:
|
||||
status = "✅" if acc.is_active else "❌"
|
||||
text += f"{status} `{acc.account_number}`\n"
|
||||
|
||||
try:
|
||||
await callback.message.answer(text, parse_mode="Markdown")
|
||||
# Задержка между сообщениями для избежания flood control
|
||||
if i + BATCH_SIZE < len(accounts):
|
||||
await asyncio.sleep(0.5) # 500ms между сообщениями
|
||||
except Exception as send_error:
|
||||
# Если flood control - ждём дольше
|
||||
if "Flood control" in str(send_error) or "Too Many Requests" in str(send_error):
|
||||
await asyncio.sleep(2)
|
||||
await callback.message.answer(text, parse_mode="Markdown")
|
||||
else:
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
# Не используем callback.answer в except - может быть timeout
|
||||
try:
|
||||
await callback.message.answer(f"❌ Ошибка: {str(e)}")
|
||||
except:
|
||||
pass # Игнорируем если не получилось отправить
|
||||
|
||||
@@ -16,12 +16,39 @@ import json
|
||||
|
||||
from ..core.database import async_session_maker
|
||||
from ..core.services import UserService, LotteryService, ParticipationService
|
||||
from ..core.chat_services import ChatMessageService
|
||||
from ..core.config import ADMIN_IDS
|
||||
from ..core.models import User, Lottery, Participation, Account
|
||||
from ..core.models import User, Lottery, Participation, Account, ChatMessage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def safe_edit_message(
|
||||
callback: CallbackQuery,
|
||||
text: str,
|
||||
reply_markup: InlineKeyboardMarkup | None = None,
|
||||
parse_mode: str = "Markdown"
|
||||
) -> bool:
|
||||
"""
|
||||
Безопасное редактирование сообщения с обработкой ошибки 'message is not modified'
|
||||
|
||||
Returns:
|
||||
bool: True если сообщение отредактировано, False если не изменилось
|
||||
"""
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=reply_markup,
|
||||
parse_mode=parse_mode
|
||||
)
|
||||
return True
|
||||
except TelegramBadRequest as e:
|
||||
if "message is not modified" in str(e):
|
||||
await callback.answer("Сообщение уже актуально", show_alert=False)
|
||||
return False
|
||||
raise
|
||||
|
||||
|
||||
# Состояния для админки
|
||||
class AdminStates(StatesGroup):
|
||||
# Создание розыгрыша
|
||||
@@ -178,30 +205,73 @@ async def show_lottery_management(callback: CallbackQuery):
|
||||
@admin_router.callback_query(F.data == "admin_create_lottery")
|
||||
async def start_create_lottery(callback: CallbackQuery, state: FSMContext):
|
||||
"""Начать создание розыгрыша"""
|
||||
logging.info(f"🎯 Callback admin_create_lottery получен от пользователя {callback.from_user.id}")
|
||||
|
||||
# Сразу отвечаем на callback
|
||||
await callback.answer()
|
||||
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
logging.warning(f"⚠️ Пользователь {callback.from_user.id} не является админом")
|
||||
await callback.message.answer("❌ Недостаточно прав")
|
||||
return
|
||||
|
||||
logging.info(f"✅ Админ {callback.from_user.id} начинает создание розыгрыша")
|
||||
|
||||
text = "📝 Создание нового розыгрыша\n\n"
|
||||
text += "Шаг 1 из 4\n\n"
|
||||
text += "Введите название розыгрыша:"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="admin_lotteries")]
|
||||
])
|
||||
)
|
||||
await state.set_state(AdminStates.lottery_title)
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="admin_lotteries")]
|
||||
])
|
||||
)
|
||||
await state.set_state(AdminStates.lottery_title)
|
||||
logging.info(f"✅ Состояние установлено: AdminStates.lottery_title")
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Ошибка при создании розыгрыша: {e}")
|
||||
await callback.message.answer(f"❌ Ошибка: {str(e)}")
|
||||
|
||||
|
||||
@admin_router.message(StateFilter(AdminStates.lottery_title))
|
||||
async def process_lottery_title(message: Message, state: FSMContext):
|
||||
"""Обработка названия розыгрыша"""
|
||||
"""Обработка названия розыгрыша (создание или редактирование)"""
|
||||
if not is_admin(message.from_user.id):
|
||||
await message.answer("❌ Недостаточно прав")
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
edit_lottery_id = data.get('edit_lottery_id')
|
||||
|
||||
# Если это редактирование существующего розыгрыша
|
||||
if edit_lottery_id:
|
||||
async with async_session_maker() as session:
|
||||
success = await LotteryService.update_lottery(
|
||||
session,
|
||||
edit_lottery_id,
|
||||
title=message.text
|
||||
)
|
||||
|
||||
if success:
|
||||
await message.answer(f"✅ Название изменено на: {message.text}")
|
||||
await state.clear()
|
||||
# Возвращаемся к выбору полей
|
||||
from aiogram.types import CallbackQuery
|
||||
fake_callback = CallbackQuery(
|
||||
id="fake",
|
||||
from_user=message.from_user,
|
||||
chat_instance="fake",
|
||||
data=f"admin_edit_lottery_select_{edit_lottery_id}",
|
||||
message=message
|
||||
)
|
||||
await choose_edit_field(fake_callback, state)
|
||||
else:
|
||||
await message.answer("❌ Ошибка при изменении названия")
|
||||
return
|
||||
|
||||
# Если это создание нового розыгрыша
|
||||
await state.update_data(title=message.text)
|
||||
|
||||
text = f"📝 Создание нового розыгрыша\n\n"
|
||||
@@ -215,11 +285,42 @@ async def process_lottery_title(message: Message, state: FSMContext):
|
||||
|
||||
@admin_router.message(StateFilter(AdminStates.lottery_description))
|
||||
async def process_lottery_description(message: Message, state: FSMContext):
|
||||
"""Обработка описания розыгрыша"""
|
||||
"""Обработка описания розыгрыша (создание или редактирование)"""
|
||||
if not is_admin(message.from_user.id):
|
||||
await message.answer("❌ Недостаточно прав")
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
edit_lottery_id = data.get('edit_lottery_id')
|
||||
|
||||
# Если это редактирование существующего розыгрыша
|
||||
if edit_lottery_id:
|
||||
description = None if message.text == "-" else message.text
|
||||
async with async_session_maker() as session:
|
||||
success = await LotteryService.update_lottery(
|
||||
session,
|
||||
edit_lottery_id,
|
||||
description=description
|
||||
)
|
||||
|
||||
if success:
|
||||
await message.answer(f"✅ Описание изменено")
|
||||
await state.clear()
|
||||
# Возвращаемся к выбору полей
|
||||
from aiogram.types import CallbackQuery
|
||||
fake_callback = CallbackQuery(
|
||||
id="fake",
|
||||
from_user=message.from_user,
|
||||
chat_instance="fake",
|
||||
data=f"admin_edit_lottery_select_{edit_lottery_id}",
|
||||
message=message
|
||||
)
|
||||
await choose_edit_field(fake_callback, state)
|
||||
else:
|
||||
await message.answer("❌ Ошибка при изменении описания")
|
||||
return
|
||||
|
||||
# Если это создание нового розыгрыша
|
||||
description = None if message.text == "-" else message.text
|
||||
await state.update_data(description=description)
|
||||
|
||||
@@ -242,12 +343,43 @@ async def process_lottery_description(message: Message, state: FSMContext):
|
||||
|
||||
@admin_router.message(StateFilter(AdminStates.lottery_prizes))
|
||||
async def process_lottery_prizes(message: Message, state: FSMContext):
|
||||
"""Обработка призов розыгрыша"""
|
||||
"""Обработка призов розыгрыша (создание или редактирование)"""
|
||||
if not is_admin(message.from_user.id):
|
||||
await message.answer("❌ Недостаточно прав")
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
edit_lottery_id = data.get('edit_lottery_id')
|
||||
|
||||
prizes = [prize.strip() for prize in message.text.split('\n') if prize.strip()]
|
||||
|
||||
# Если это редактирование существующего розыгрыша
|
||||
if edit_lottery_id:
|
||||
async with async_session_maker() as session:
|
||||
success = await LotteryService.update_lottery(
|
||||
session,
|
||||
edit_lottery_id,
|
||||
prizes=prizes
|
||||
)
|
||||
|
||||
if success:
|
||||
await message.answer(f"✅ Призы изменены")
|
||||
await state.clear()
|
||||
# Возвращаемся к выбору полей
|
||||
from aiogram.types import CallbackQuery
|
||||
fake_callback = CallbackQuery(
|
||||
id="fake",
|
||||
from_user=message.from_user,
|
||||
chat_instance="fake",
|
||||
data=f"admin_edit_lottery_select_{edit_lottery_id}",
|
||||
message=message
|
||||
)
|
||||
await choose_edit_field(fake_callback, state)
|
||||
else:
|
||||
await message.answer("❌ Ошибка при изменении призов")
|
||||
return
|
||||
|
||||
# Если это создание нового розыгрыша
|
||||
await state.update_data(prizes=prizes)
|
||||
|
||||
data = await state.get_data()
|
||||
@@ -1679,6 +1811,47 @@ async def start_edit_lottery(callback: CallbackQuery, state: FSMContext):
|
||||
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_edit_field_"))
|
||||
async def handle_edit_field(callback: CallbackQuery, state: FSMContext):
|
||||
"""Обработка выбора поля для редактирования"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
return
|
||||
|
||||
# Парсим callback_data: admin_edit_field_{lottery_id}_{field_name}
|
||||
parts = callback.data.split("_")
|
||||
if len(parts) < 5:
|
||||
await callback.answer("❌ Неверный формат данных", show_alert=True)
|
||||
return
|
||||
|
||||
lottery_id = int(parts[3]) # admin_edit_field_{lottery_id}_...
|
||||
field_name = "_".join(parts[4:]) # Всё после lottery_id это имя поля
|
||||
|
||||
await state.update_data(edit_lottery_id=lottery_id, edit_field=field_name)
|
||||
|
||||
# Определяем, что редактируем
|
||||
if field_name == "title":
|
||||
text = "📝 Введите новое название розыгрыша:"
|
||||
await state.set_state(AdminStates.lottery_title)
|
||||
elif field_name == "description":
|
||||
text = "📄 Введите новое описание розыгрыша:"
|
||||
await state.set_state(AdminStates.lottery_description)
|
||||
elif field_name == "prizes":
|
||||
text = "🎁 Введите новый список призов (каждый приз с новой строки):"
|
||||
await state.set_state(AdminStates.lottery_prizes)
|
||||
else:
|
||||
await callback.answer("❌ Неизвестное поле", show_alert=True)
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="admin_lotteries")]
|
||||
])
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_edit_"))
|
||||
async def redirect_to_edit_lottery(callback: CallbackQuery, state: FSMContext):
|
||||
"""Редирект на редактирование розыгрыша из детального просмотра"""
|
||||
@@ -1737,7 +1910,7 @@ async def choose_edit_field(callback: CallbackQuery, state: FSMContext):
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_toggle_active_"))
|
||||
async def toggle_lottery_active(callback: CallbackQuery):
|
||||
async def toggle_lottery_active(callback: CallbackQuery, state: FSMContext):
|
||||
"""Переключить активность розыгрыша"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
@@ -1759,7 +1932,7 @@ async def toggle_lottery_active(callback: CallbackQuery):
|
||||
await callback.answer("❌ Ошибка изменения статуса", show_alert=True)
|
||||
|
||||
# Обновляем отображение
|
||||
await choose_edit_field(callback, None)
|
||||
await choose_edit_field(callback, state)
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_finish_lottery")
|
||||
@@ -2593,7 +2766,7 @@ async def choose_lottery_for_draw(callback: CallbackQuery):
|
||||
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_conduct_"))
|
||||
@admin_router.callback_query(F.data.regexp(r"^admin_conduct_\d+$"))
|
||||
async def conduct_lottery_draw_confirm(callback: CallbackQuery):
|
||||
"""Запрос подтверждения проведения розыгрыша"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
@@ -2623,45 +2796,48 @@ async def conduct_lottery_draw_confirm(callback: CallbackQuery):
|
||||
prizes_count = len(lottery.prizes) if lottery.prizes else 0
|
||||
|
||||
# Формируем сообщение с подтверждением
|
||||
text = f"⚠️ <b>Подтверждение проведения розыгрыша</b>\n\n"
|
||||
text += f"🎲 <b>Розыгрыш:</b> {lottery.title}\n"
|
||||
text += f"👥 <b>Участников:</b> {participants_count}\n"
|
||||
text += f"🏆 <b>Призов:</b> {prizes_count}\n\n"
|
||||
text = f"⚠️ *Подтверждение проведения розыгрыша*\n\n"
|
||||
text += f"🎲 *Розыгрыш:* {lottery.title}\n"
|
||||
text += f"👥 *Участников:* {participants_count}\n"
|
||||
text += f"🏆 *Призов:* {prizes_count}\n\n"
|
||||
|
||||
if lottery.prizes:
|
||||
text += "<b>Призы:</b>\n"
|
||||
text += "*Призы:*\n"
|
||||
for i, prize in enumerate(lottery.prizes, 1):
|
||||
text += f"{i}. {prize}\n"
|
||||
text += "\n"
|
||||
|
||||
text += "❗️ <b>Внимание:</b> После проведения розыгрыша результаты нельзя будет изменить!\n\n"
|
||||
text += "❗️ *Внимание:* После проведения розыгрыша результаты нельзя будет изменить!\n\n"
|
||||
text += "Продолжить?"
|
||||
|
||||
confirm_callback = f"admin_conduct_confirmed_{lottery_id}"
|
||||
logger.info(f"Создаём кнопку подтверждения с callback_data='{confirm_callback}'")
|
||||
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text="✅ Да, провести розыгрыш", callback_data=f"admin_conduct_confirmed_{lottery_id}")],
|
||||
[InlineKeyboardButton(text="✅ Да, провести розыгрыш", callback_data=confirm_callback)],
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_lottery_{lottery_id}")]
|
||||
]
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons))
|
||||
except TelegramBadRequest as e:
|
||||
if "message is not modified" in str(e):
|
||||
await callback.answer("Сообщение уже актуально", show_alert=False)
|
||||
else:
|
||||
raise
|
||||
await safe_edit_message(callback, text, InlineKeyboardMarkup(inline_keyboard=buttons))
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_conduct_confirmed_"))
|
||||
async def conduct_lottery_draw(callback: CallbackQuery):
|
||||
"""Проведение розыгрыша после подтверждения"""
|
||||
logger.info(f"🎯 conduct_lottery_draw HANDLER TRIGGERED! data={callback.data}, user={callback.from_user.id}")
|
||||
logger.info(f"conduct_lottery_draw вызван: callback.data={callback.data}, user_id={callback.from_user.id}")
|
||||
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
return
|
||||
|
||||
lottery_id = int(callback.data.split("_")[-1])
|
||||
logger.info(f"Извлечен lottery_id={lottery_id}")
|
||||
|
||||
async with async_session_maker() as session:
|
||||
logger.info(f"Создана сессия БД")
|
||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||
logger.info(f"Получен lottery: {lottery.title if lottery else None}, is_completed={lottery.is_completed if lottery else None}")
|
||||
|
||||
if not lottery:
|
||||
await callback.answer("Розыгрыш не найден", show_alert=True)
|
||||
@@ -2681,9 +2857,21 @@ async def conduct_lottery_draw(callback: CallbackQuery):
|
||||
await callback.answer("⏳ Проводится розыгрыш...", show_alert=True)
|
||||
|
||||
# Проводим розыгрыш через сервис
|
||||
winners_dict = await LotteryService.conduct_draw(session, lottery_id)
|
||||
logger.info(f"Начинаем проведение розыгрыша {lottery_id}")
|
||||
try:
|
||||
winners_dict = await LotteryService.conduct_draw(session, lottery_id)
|
||||
logger.info(f"Розыгрыш {lottery_id} проведён, победителей: {len(winners_dict)}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при проведении розыгрыша {lottery_id}: {e}", exc_info=True)
|
||||
await session.rollback()
|
||||
await callback.answer(f"❌ Ошибка: {e}", show_alert=True)
|
||||
return
|
||||
|
||||
if winners_dict:
|
||||
# Коммитим изменения в БД
|
||||
await session.commit()
|
||||
logger.info(f"Изменения закоммичены для розыгрыша {lottery_id}")
|
||||
|
||||
# Отправляем уведомления победителям
|
||||
from ..utils.notifications import notify_winners_async
|
||||
try:
|
||||
@@ -3225,5 +3413,286 @@ async def apply_display_type(callback: CallbackQuery, state: FSMContext):
|
||||
await state.clear()
|
||||
|
||||
|
||||
# ============= УПРАВЛЕНИЕ СООБЩЕНИЯМИ ПОЛЬЗОВАТЕЛЕЙ =============
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_messages")
|
||||
async def show_messages_menu(callback: CallbackQuery):
|
||||
"""Показать меню управления сообщениями"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
return
|
||||
|
||||
text = "💬 *Управление сообщениями пользователей*\n\n"
|
||||
text += "Здесь вы можете просматривать и удалять сообщения пользователей.\n\n"
|
||||
text += "Выберите действие:"
|
||||
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text="📋 Последние сообщения", callback_data="admin_messages_recent")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_panel")]
|
||||
]
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_messages_recent")
|
||||
async def show_recent_messages(callback: CallbackQuery, page: int = 0):
|
||||
"""Показать последние сообщения"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
return
|
||||
|
||||
limit = 10
|
||||
offset = page * limit
|
||||
|
||||
async with async_session_maker() as session:
|
||||
messages = await ChatMessageService.get_user_messages_all(
|
||||
session,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
include_deleted=False
|
||||
)
|
||||
|
||||
if not messages:
|
||||
text = "💬 Нет сообщений для отображения"
|
||||
buttons = [[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_messages")]]
|
||||
else:
|
||||
text = f"💬 *Последние сообщения*\n\n"
|
||||
|
||||
# Добавляем кнопки для просмотра сообщений
|
||||
buttons = []
|
||||
for msg in messages:
|
||||
sender = msg.sender
|
||||
username = f"@{sender.username}" if sender.username else f"ID{sender.telegram_id}"
|
||||
msg_preview = ""
|
||||
if msg.text:
|
||||
msg_preview = msg.text[:20] + "..." if len(msg.text) > 20 else msg.text
|
||||
else:
|
||||
msg_preview = msg.message_type
|
||||
|
||||
buttons.append([InlineKeyboardButton(
|
||||
text=f"👁 {username}: {msg_preview}",
|
||||
callback_data=f"admin_message_view_{msg.id}"
|
||||
)])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="admin_messages")])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_message_view_"))
|
||||
async def view_message(callback: CallbackQuery):
|
||||
"""Просмотр конкретного сообщения"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
return
|
||||
|
||||
message_id = int(callback.data.split("_")[-1])
|
||||
|
||||
async with async_session_maker() as session:
|
||||
msg = await ChatMessageService.get_message(session, message_id)
|
||||
|
||||
if not msg:
|
||||
await callback.answer("❌ Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
|
||||
sender = msg.sender
|
||||
username = f"@{sender.username}" if sender.username else f"ID: {sender.telegram_id}"
|
||||
|
||||
text = f"💬 *Просмотр сообщения*\n\n"
|
||||
text += f"👤 Отправитель: {username}\n"
|
||||
text += f"🆔 Telegram ID: `{sender.telegram_id}`\n"
|
||||
text += f"📝 Тип: {msg.message_type}\n"
|
||||
text += f"📅 Дата: {msg.created_at.strftime('%d.%m.%Y %H:%M:%S')}\n\n"
|
||||
|
||||
if msg.text:
|
||||
text += f"📄 *Текст:*\n{msg.text}\n\n"
|
||||
|
||||
if msg.file_id:
|
||||
text += f"📎 File ID: `{msg.file_id}`\n\n"
|
||||
|
||||
if msg.is_deleted:
|
||||
text += f"🗑 *Удалено:* Да\n"
|
||||
if msg.deleted_at:
|
||||
text += f" Дата: {msg.deleted_at.strftime('%d.%m.%Y %H:%M')}\n"
|
||||
|
||||
buttons = []
|
||||
|
||||
# Кнопка удаления (если еще не удалено)
|
||||
if not msg.is_deleted:
|
||||
buttons.append([InlineKeyboardButton(
|
||||
text="🗑 Удалить сообщение",
|
||||
callback_data=f"admin_message_delete_{message_id}"
|
||||
)])
|
||||
|
||||
# Кнопка для просмотра всех сообщений пользователя
|
||||
buttons.append([InlineKeyboardButton(
|
||||
text="📋 Все сообщения пользователя",
|
||||
callback_data=f"admin_messages_user_{sender.id}"
|
||||
)])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 К списку", callback_data="admin_messages_recent")])
|
||||
|
||||
# Если сообщение содержит медиа, попробуем его показать
|
||||
if msg.file_id and msg.message_type in ['photo', 'video', 'document', 'animation']:
|
||||
try:
|
||||
if msg.message_type == 'photo':
|
||||
await callback.message.answer_photo(
|
||||
photo=msg.file_id,
|
||||
caption=text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
await callback.message.delete()
|
||||
await callback.answer()
|
||||
return
|
||||
elif msg.message_type == 'video':
|
||||
await callback.message.answer_video(
|
||||
video=msg.file_id,
|
||||
caption=text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
await callback.message.delete()
|
||||
await callback.answer()
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке медиа: {e}")
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_message_delete_"))
|
||||
async def delete_message(callback: CallbackQuery):
|
||||
"""Удалить сообщение пользователя"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
return
|
||||
|
||||
message_id = int(callback.data.split("_")[-1])
|
||||
|
||||
async with async_session_maker() as session:
|
||||
msg = await ChatMessageService.get_message(session, message_id)
|
||||
|
||||
if not msg:
|
||||
await callback.answer("❌ Сообщение не найдено", show_alert=True)
|
||||
return
|
||||
|
||||
# Получаем админа
|
||||
admin = await UserService.get_or_create_user(
|
||||
session,
|
||||
callback.from_user.id,
|
||||
callback.from_user.username,
|
||||
callback.from_user.first_name,
|
||||
callback.from_user.last_name
|
||||
)
|
||||
|
||||
# Помечаем сообщение как удаленное
|
||||
success = await ChatMessageService.mark_as_deleted(
|
||||
session,
|
||||
message_id,
|
||||
admin.id
|
||||
)
|
||||
|
||||
if success:
|
||||
# Пытаемся удалить сообщение из чата пользователя
|
||||
try:
|
||||
if msg.forwarded_message_ids:
|
||||
# Удаляем пересланные копии у всех пользователей
|
||||
for user_tg_id, tg_msg_id in msg.forwarded_message_ids.items():
|
||||
try:
|
||||
await callback.bot.delete_message(
|
||||
chat_id=int(user_tg_id),
|
||||
message_id=tg_msg_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось удалить сообщение {tg_msg_id} у пользователя {user_tg_id}: {e}")
|
||||
|
||||
# Удаляем оригинальное сообщение у отправителя
|
||||
try:
|
||||
await callback.bot.delete_message(
|
||||
chat_id=msg.sender.telegram_id,
|
||||
message_id=msg.telegram_message_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось удалить оригинальное сообщение: {e}")
|
||||
|
||||
await callback.answer("✅ Сообщение удалено!", show_alert=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении сообщений: {e}")
|
||||
await callback.answer("⚠️ Помечено как удаленное", show_alert=True)
|
||||
else:
|
||||
await callback.answer("❌ Ошибка при удалении", show_alert=True)
|
||||
|
||||
# Возвращаемся к списку
|
||||
await show_recent_messages(callback, 0)
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_messages_user_"))
|
||||
async def show_user_messages(callback: CallbackQuery):
|
||||
"""Показать все сообщения конкретного пользователя"""
|
||||
if not is_admin(callback.from_user.id):
|
||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||
return
|
||||
|
||||
user_id = int(callback.data.split("_")[-1])
|
||||
|
||||
async with async_session_maker() as session:
|
||||
user = await UserService.get_user_by_id(session, user_id)
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Пользователь не найден", show_alert=True)
|
||||
return
|
||||
|
||||
messages = await ChatMessageService.get_user_messages(
|
||||
session,
|
||||
user_id,
|
||||
limit=20,
|
||||
include_deleted=True
|
||||
)
|
||||
|
||||
username = f"@{user.username}" if user.username else f"ID: {user.telegram_id}"
|
||||
|
||||
text = f"💬 *Сообщения {username}*\n\n"
|
||||
|
||||
if not messages:
|
||||
text += "Нет сообщений"
|
||||
buttons = [[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_messages_recent")]]
|
||||
else:
|
||||
# Кнопки для просмотра отдельных сообщений
|
||||
buttons = []
|
||||
for msg in messages[:15]:
|
||||
status = "🗑" if msg.is_deleted else "✅"
|
||||
msg_preview = ""
|
||||
if msg.text:
|
||||
msg_preview = msg.text[:25] + "..." if len(msg.text) > 25 else msg.text
|
||||
else:
|
||||
msg_preview = msg.message_type
|
||||
|
||||
buttons.append([InlineKeyboardButton(
|
||||
text=f"{status} {msg_preview} ({msg.created_at.strftime('%d.%m %H:%M')})",
|
||||
callback_data=f"admin_message_view_{msg.id}"
|
||||
)])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="admin_messages_recent")])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
|
||||
# Экспорт роутера
|
||||
__all__ = ['admin_router']
|
||||
@@ -109,6 +109,73 @@ async def handle_text_message(message: Message):
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"[CHAT] handle_text_message вызван: user={message.from_user.id}, text={message.text[:50] if message.text else 'None'}")
|
||||
|
||||
# БЫСТРОЕ УДАЛЕНИЕ: Если админ отвечает на сообщение словом "удалить"/"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_user_by_telegram_id(
|
||||
session,
|
||||
message.from_user.id
|
||||
)
|
||||
|
||||
# Помечаем как удаленное
|
||||
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('/'):
|
||||
# Список команд, которые НЕ нужно пересылать
|
||||
@@ -123,21 +190,20 @@ async def handle_text_message(message: Message):
|
||||
# Извлекаем команду (первое слово)
|
||||
command = message.text.split()[0] if message.text else ''
|
||||
|
||||
# Если это пользовательская команда - пропускаем, она будет обработана другими обработчиками
|
||||
if command in user_commands:
|
||||
return
|
||||
|
||||
# Если это админская команда
|
||||
if command in admin_commands:
|
||||
# Проверяем права админа
|
||||
if not is_admin(message.from_user.id):
|
||||
await message.answer("❌ У вас нет прав для выполнения этой команды")
|
||||
# ИЗМЕНЕНИЕ: Если это команда от АДМИНА - не пересылаем (админ сам её видит)
|
||||
if is_admin(message.from_user.id):
|
||||
# Если это админская команда - пропускаем, она будет обработана другими обработчиками
|
||||
if command in admin_commands:
|
||||
return
|
||||
# Если админ - команда будет обработана другими обработчиками, пропускаем пересылку
|
||||
# Если это пользовательская команда от админа - тоже пропускаем
|
||||
if command in user_commands:
|
||||
return
|
||||
# Любая другая команда от админа - тоже не пересылаем
|
||||
return
|
||||
|
||||
# Если неизвестная команда - тоже не пересылаем
|
||||
return
|
||||
# ИЗМЕНЕНИЕ: Если команда от обычного пользователя - ПЕРЕСЫЛАЕМ админу
|
||||
# Чтобы админ видел, что пользователь отправил /start или другую команду
|
||||
# НЕ делаем return, продолжаем выполнение для пересылки
|
||||
|
||||
async with async_session_maker() as session:
|
||||
# Проверяем права на отправку
|
||||
@@ -163,7 +229,8 @@ async def handle_text_message(message: Message):
|
||||
# Обрабатываем в зависимости от режима
|
||||
if settings.mode == 'broadcast':
|
||||
# Режим рассылки с планировщиком
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=message.from_user.id)
|
||||
# НЕ исключаем отправителя - админ должен видеть все сообщения
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=None)
|
||||
|
||||
# Сохраняем сообщение в историю
|
||||
await ChatMessageService.save_message(
|
||||
@@ -231,7 +298,12 @@ async def handle_photo_message(message: Message):
|
||||
photo = message.photo[-1]
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=message.from_user.id)
|
||||
# Отправляем только админам
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(
|
||||
message,
|
||||
exclude_user_id=message.from_user.id,
|
||||
admin_only=True
|
||||
)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
session,
|
||||
@@ -245,7 +317,7 @@ async def handle_photo_message(message: Message):
|
||||
|
||||
# Показываем статистику только админам
|
||||
if is_admin(message.from_user.id):
|
||||
await message.answer(f"✅ Фото разослано: {success} получателей")
|
||||
await message.answer(f"✅ Фото отправлено админам: {success}")
|
||||
|
||||
elif settings.mode == 'forward':
|
||||
if settings.forward_chat_id:
|
||||
@@ -285,7 +357,8 @@ async def handle_video_message(message: Message):
|
||||
return
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=message.from_user.id)
|
||||
# НЕ исключаем отправителя - админ должен видеть все сообщения
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=None)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
session,
|
||||
@@ -339,7 +412,8 @@ async def handle_document_message(message: Message):
|
||||
return
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=message.from_user.id)
|
||||
# НЕ исключаем отправителя - админ должен видеть все сообщения
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=None)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
session,
|
||||
@@ -393,7 +467,8 @@ async def handle_animation_message(message: Message):
|
||||
return
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=message.from_user.id)
|
||||
# НЕ исключаем отправителя - админ должен видеть все сообщения
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=None)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
session,
|
||||
@@ -447,7 +522,8 @@ async def handle_sticker_message(message: Message):
|
||||
return
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=message.from_user.id)
|
||||
# НЕ исключаем отправителя - админ должен видеть все сообщения
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=None)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
session,
|
||||
@@ -480,51 +556,19 @@ async def handle_sticker_message(message: Message):
|
||||
|
||||
@router.message(F.voice)
|
||||
async def handle_voice_message(message: Message):
|
||||
"""Обработчик голосовых сообщений"""
|
||||
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_user_by_telegram_id(session, message.from_user.id)
|
||||
|
||||
if not user:
|
||||
return
|
||||
|
||||
if settings.mode == 'broadcast':
|
||||
forwarded_ids, success, fail = await broadcast_message_with_scheduler(message, exclude_user_id=message.from_user.id)
|
||||
|
||||
await ChatMessageService.save_message(
|
||||
session,
|
||||
user_id=user.id,
|
||||
telegram_message_id=message.message_id,
|
||||
message_type='voice',
|
||||
file_id=message.voice.file_id,
|
||||
forwarded_ids=forwarded_ids
|
||||
)
|
||||
|
||||
# Показываем статистику только админам
|
||||
if is_admin(message.from_user.id):
|
||||
await message.answer(f"✅ Голосовое сообщение разослано: {success} получателей")
|
||||
|
||||
elif settings.mode == 'forward':
|
||||
if settings.forward_chat_id:
|
||||
success, channel_msg_id = await forward_to_channel(message, settings.forward_chat_id)
|
||||
|
||||
if success:
|
||||
await ChatMessageService.save_message(
|
||||
session,
|
||||
user_id=user.id,
|
||||
telegram_message_id=message.message_id,
|
||||
message_type='voice',
|
||||
file_id=message.voice.file_id,
|
||||
forwarded_ids={'channel': channel_msg_id} if channel_msg_id else None
|
||||
)
|
||||
await message.answer("✅ Голосовое сообщение переслано в канал")
|
||||
"""Обработчик голосовых сообщений - ЗАБЛОКИРОВАНО"""
|
||||
await message.answer(
|
||||
"🚫 Голосовые сообщения запрещены.\n\n"
|
||||
"Пожалуйста, используйте текстовые сообщения или изображения."
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@router.message(F.audio)
|
||||
async def handle_audio_message(message: Message):
|
||||
"""Обработчик аудиофайлов (музыка, аудиозаписи) - ЗАБЛОКИРОВАНО"""
|
||||
await message.answer(
|
||||
"🚫 Аудиофайлы запрещены.\n\n"
|
||||
"Пожалуйста, используйте текстовые сообщения или изображения."
|
||||
)
|
||||
return
|
||||
|
||||
@@ -304,3 +304,97 @@ async def redraw_lottery(message: Message):
|
||||
|
||||
except Exception as e:
|
||||
await message.answer(f"❌ Ошибка: {str(e)}")
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("confirm_win_"))
|
||||
async def confirm_winner_callback(callback_query):
|
||||
"""Обработка подтверждения выигрыша победителем"""
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
winner_id = int(callback_query.data.split("_")[-1])
|
||||
|
||||
async with async_session_maker() as session:
|
||||
# Получаем информацию о победителе
|
||||
winner_result = await session.execute(
|
||||
select(Winner).where(Winner.id == winner_id)
|
||||
)
|
||||
winner = winner_result.scalar_one_or_none()
|
||||
|
||||
if not winner:
|
||||
await callback_query.answer("❌ Победитель не найден", show_alert=True)
|
||||
return
|
||||
|
||||
if winner.is_claimed:
|
||||
await callback_query.answer(
|
||||
"✅ Этот выигрыш уже подтвержден!",
|
||||
show_alert=True
|
||||
)
|
||||
return
|
||||
|
||||
# Проверяем, что пользователь является владельцем счёта
|
||||
if winner.account_number:
|
||||
owner = await AccountService.get_account_owner(session, winner.account_number)
|
||||
if not owner or owner.telegram_id != callback_query.from_user.id:
|
||||
await callback_query.answer(
|
||||
"❌ Вы не являетесь владельцем этого счёта",
|
||||
show_alert=True
|
||||
)
|
||||
return
|
||||
|
||||
# Проверяем срок действия (24 часа с момента создания winner)
|
||||
if winner.created_at:
|
||||
time_since_creation = datetime.now(timezone.utc) - winner.created_at
|
||||
if time_since_creation > timedelta(hours=24):
|
||||
await callback_query.answer(
|
||||
"❌ Срок подтверждения истёк (24 часа). Приз будет разыгран заново.",
|
||||
show_alert=True
|
||||
)
|
||||
return
|
||||
|
||||
# Подтверждаем выигрыш
|
||||
winner.is_claimed = True
|
||||
winner.claimed_at = datetime.now(timezone.utc)
|
||||
await session.commit()
|
||||
|
||||
# Получаем данные о розыгрыше
|
||||
lottery = await LotteryService.get_lottery(session, winner.lottery_id)
|
||||
|
||||
# Отправляем подтверждение пользователю
|
||||
confirmation_text = (
|
||||
f"✅ **Выигрыш подтвержден!**\n\n"
|
||||
f"🎯 Розыгрыш: {lottery.title}\n"
|
||||
f"🏆 Место: {winner.place}\n"
|
||||
f"🎁 Приз: {winner.prize}\n"
|
||||
f"💳 Счет: {winner.account_number}\n\n"
|
||||
f"📞 С вами свяжется администратор для вручения приза.\n"
|
||||
f"Спасибо за участие!"
|
||||
)
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
confirmation_text,
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
# Уведомляем админов
|
||||
for admin_id in ADMIN_IDS:
|
||||
try:
|
||||
admin_text = (
|
||||
f"✅ **Подтверждение выигрыша**\n\n"
|
||||
f"👤 Пользователь: {callback_query.from_user.full_name} "
|
||||
f"(@{callback_query.from_user.username or 'нет username'})\n"
|
||||
f"🎯 Розыгрыш: {lottery.title}\n"
|
||||
f"🏆 Место: {winner.place}\n"
|
||||
f"🎁 Приз: {winner.prize}\n"
|
||||
f"💳 Счет: {winner.account_number}"
|
||||
)
|
||||
|
||||
from aiogram import Bot
|
||||
from src.core.config import BOT_TOKEN
|
||||
bot = Bot(token=BOT_TOKEN)
|
||||
await bot.send_message(admin_id, admin_text, parse_mode="Markdown")
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger(__name__).error(f"Ошибка отправки админу {admin_id}: {e}")
|
||||
|
||||
await callback_query.answer("✅ Выигрыш подтвержден!", show_alert=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user