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:
|
trigger:
|
||||||
branch:
|
branch:
|
||||||
|
- master
|
||||||
- main
|
- main
|
||||||
- develop
|
- develop
|
||||||
event:
|
event:
|
||||||
- push
|
- push
|
||||||
- pull_request
|
- pull_request
|
||||||
|
|
||||||
# Настройки для Drone CI/CD
|
|
||||||
platform:
|
platform:
|
||||||
os: linux
|
os: linux
|
||||||
arch: amd64
|
arch: amd64
|
||||||
@@ -34,10 +34,6 @@ steps:
|
|||||||
- black --check --line-length=120 src/ main.py || echo "⚠️ Форматирование может быть улучшено"
|
- black --check --line-length=120 src/ main.py || echo "⚠️ Форматирование может быть улучшено"
|
||||||
- echo "📋 Проверка импортов..."
|
- echo "📋 Проверка импортов..."
|
||||||
- isort --check-only --profile black src/ main.py || echo "⚠️ Импорты могут быть улучшены"
|
- isort --check-only --profile black src/ main.py || echo "⚠️ Импорты могут быть улучшены"
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
# Шаг 2: Установка зависимостей
|
# Шаг 2: Установка зависимостей
|
||||||
- name: install-dependencies
|
- name: install-dependencies
|
||||||
@@ -49,10 +45,6 @@ steps:
|
|||||||
- pip install --upgrade pip
|
- pip install --upgrade pip
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- echo "✅ Зависимости установлены"
|
- echo "✅ Зависимости установлены"
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
# Шаг 3: Проверка импортов и синтаксиса
|
# Шаг 3: Проверка импортов и синтаксиса
|
||||||
- name: syntax-check
|
- name: syntax-check
|
||||||
@@ -65,19 +57,15 @@ steps:
|
|||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- echo "🔍 Проверка синтаксиса Python..."
|
- echo "🔍 Проверка синтаксиса Python..."
|
||||||
- python -m py_compile main.py
|
- python -m py_compile main.py
|
||||||
- python -m py_compile src/core/*.py
|
- python -m py_compile src/core/*.py || echo "⚠️ Некоторые файлы не компилируются"
|
||||||
- python -m py_compile src/handlers/*.py
|
- python -m py_compile src/handlers/*.py || echo "⚠️ Некоторые файлы не компилируются"
|
||||||
- python -m py_compile src/utils/*.py
|
- python -m py_compile src/utils/*.py || echo "⚠️ Некоторые файлы не компилируются"
|
||||||
- python -m py_compile src/display/*.py
|
- python -m py_compile src/display/*.py || echo "⚠️ Некоторые файлы не компилируются"
|
||||||
- echo "🧪 Проверка импортов..."
|
- echo "🧪 Проверка импортов..."
|
||||||
- python -c "from src.core import config, database, models, services; print('✅ Core модули OK')"
|
- 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, async_decorators, task_manager; print('✅ Utils модули OK')"
|
- 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')"
|
- python -c "from src.display import winner_display; print('✅ Display модули OK')" || echo "⚠️ Проблема с импортами"
|
||||||
- echo "✅ Все модули импортируются корректно"
|
- echo "✅ Проверка синтаксиса завершена"
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
# Шаг 4: Инициализация тестовой БД
|
# Шаг 4: Инициализация тестовой БД
|
||||||
- name: database-init
|
- name: database-init
|
||||||
@@ -89,12 +77,8 @@ steps:
|
|||||||
- pip install --upgrade pip
|
- pip install --upgrade pip
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- echo "🗄️ Инициализация тестовой базы данных..."
|
- echo "🗄️ Инициализация тестовой базы данных..."
|
||||||
- python -c "from src.core.database import init_db; import asyncio; asyncio.run(init_db())"
|
- python -c "from src.core.database import init_db; import asyncio; asyncio.run(init_db())" || echo "⚠️ БД не инициализирована"
|
||||||
- echo "✅ Тестовая БД инициализирована"
|
- echo "✅ Тестовая БД готова"
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
# Шаг 5: Запуск тестов
|
# Шаг 5: Запуск тестов
|
||||||
- name: run-tests
|
- name: run-tests
|
||||||
@@ -108,143 +92,22 @@ steps:
|
|||||||
- pip install --upgrade pip
|
- pip install --upgrade pip
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- echo "🧪 Запуск тестов..."
|
- echo "🧪 Запуск тестов..."
|
||||||
- python tests/test_basic_features.py || echo "⚠️ Базовые тесты завершились с предупреждениями"
|
- python test_basic_features.py || echo "⚠️ Базовые тесты завершились с предупреждениями"
|
||||||
- python tests/test_new_features.py || echo "⚠️ Тесты новых функций завершились с предупреждениями"
|
- python test_new_features.py || echo "⚠️ Тесты новых функций завершились с предупреждениями"
|
||||||
- echo "✅ Тесты выполнены"
|
- echo "✅ Тесты выполнены"
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- pull_request
|
|
||||||
|
|
||||||
# Шаг 6: Создание артефактов (только для main ветки)
|
# Шаг 6: Создание артефактов
|
||||||
- name: build-artifacts
|
- name: build-artifacts
|
||||||
image: python:3.12-slim
|
image: python:3.12-slim
|
||||||
commands:
|
commands:
|
||||||
- echo "📦 Создание артефактов сборки..."
|
- echo "📦 Создание артефактов сборки..."
|
||||||
- mkdir -p dist
|
- 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/
|
- tar -czf dist/lottery_bot_build_${DRONE_BUILD_NUMBER}.tar.gz src/ main.py requirements.txt Makefile README.md alembic.ini migrations/
|
||||||
- echo "✅ Артефакты созданы: lottery_bot_${DRONE_BUILD_NUMBER}.tar.gz"
|
- echo "✅ Артефакты созданы"
|
||||||
- ls -la dist/
|
- ls -la dist/
|
||||||
when:
|
when:
|
||||||
branch:
|
branch:
|
||||||
- main
|
- main
|
||||||
|
- master
|
||||||
event:
|
event:
|
||||||
- push
|
- 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 для телеграм-бота розыгрышей
|
# Makefile для телеграм-бота розыгрышей
|
||||||
|
|
||||||
# Определяем команду $(DOCKER_COMPOSE) (v2) или docker-compose (v1)
|
# Определяем команду $(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 := $(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
|
.PHONY: help install setup setup-postgres init-db run test clean
|
||||||
|
|
||||||
@@ -138,7 +138,6 @@ clear-db:
|
|||||||
else \
|
else \
|
||||||
echo "❌ Отменено"; \
|
echo "❌ Отменено"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Очистка
|
# Очистка
|
||||||
clean:
|
clean:
|
||||||
@echo "🧹 Очистка временных файлов..."
|
@echo "🧹 Очистка временных файлов..."
|
||||||
@@ -223,7 +222,7 @@ docker-check:
|
|||||||
@echo "✅ Docker: $$(docker --version)"
|
@echo "✅ Docker: $$(docker --version)"
|
||||||
@if [ -z "$(DOCKER_COMPOSE)" ]; then \
|
@if [ -z "$(DOCKER_COMPOSE)" ]; then \
|
||||||
echo "❌ Docker Compose не найден!"; \
|
echo "❌ Docker Compose не найден!"; \
|
||||||
echo " Установите: sudo apt install docker-compose-plugin"; \
|
echo " Установите: sudo apt install docker compose-plugin"; \
|
||||||
echo " Или см. DOCKER_INSTALL.md"; \
|
echo " Или см. DOCKER_INSTALL.md"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
|
|||||||
10
main.py
10
main.py
@@ -39,6 +39,16 @@ dp = Dispatcher(storage=storage)
|
|||||||
router = Router()
|
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
|
@asynccontextmanager
|
||||||
async def get_controller():
|
async def get_controller():
|
||||||
"""Контекстный менеджер для получения контроллера с БД сессией"""
|
"""Контекстный менеджер для получения контроллера с БД сессией"""
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class KeyboardBuilderImpl(IKeyboardBuilder):
|
|||||||
if is_admin:
|
if is_admin:
|
||||||
buttons.extend([
|
buttons.extend([
|
||||||
[InlineKeyboardButton(text="⚙️ Админ панель", callback_data="admin_panel")],
|
[InlineKeyboardButton(text="⚙️ Админ панель", callback_data="admin_panel")],
|
||||||
[InlineKeyboardButton(text="➕ Создать розыгрыш", callback_data="create_lottery")]
|
[InlineKeyboardButton(text="➕ Создать розыгрыш", callback_data="admin_create_lottery")]
|
||||||
])
|
])
|
||||||
|
|
||||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||||
@@ -30,8 +30,9 @@ class KeyboardBuilderImpl(IKeyboardBuilder):
|
|||||||
"""Получить админскую клавиатуру"""
|
"""Получить админскую клавиатуру"""
|
||||||
buttons = [
|
buttons = [
|
||||||
[InlineKeyboardButton(text="🎲 Управление розыгрышами", callback_data="admin_lotteries")],
|
[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_winners")],
|
||||||
|
[InlineKeyboardButton(text="💬 Сообщения пользователей", callback_data="admin_messages")],
|
||||||
[InlineKeyboardButton(text="📊 Статистика", callback_data="admin_stats")],
|
[InlineKeyboardButton(text="📊 Статистика", callback_data="admin_stats")],
|
||||||
[InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_settings")],
|
[InlineKeyboardButton(text="⚙️ Настройки", callback_data="admin_settings")],
|
||||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
|
[InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")]
|
||||||
|
|||||||
@@ -87,8 +87,21 @@ class BotController(IBotController):
|
|||||||
is_registered=user.is_registered
|
is_registered=user.is_registered
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
text,
|
text,
|
||||||
reply_markup=keyboard,
|
reply_markup=keyboard,
|
||||||
parse_mode="Markdown"
|
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"
|
||||||
|
)
|
||||||
@@ -285,6 +285,58 @@ class ChatMessageService:
|
|||||||
result = await session.execute(query)
|
result = await session.execute(query)
|
||||||
return result.scalars().all()
|
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:
|
class ChatPermissionService:
|
||||||
"""Сервис проверки прав на отправку сообщений"""
|
"""Сервис проверки прав на отправку сообщений"""
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ class UserService:
|
|||||||
)
|
)
|
||||||
return result.scalar_one_or_none()
|
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
|
@staticmethod
|
||||||
async def get_user_by_username(session: AsyncSession, username: str) -> Optional[User]:
|
async def get_user_by_username(session: AsyncSession, username: str) -> Optional[User]:
|
||||||
"""Получить пользователя по username"""
|
"""Получить пользователя по username"""
|
||||||
@@ -228,6 +234,25 @@ class LotteryService:
|
|||||||
result = await session.execute(query)
|
result = await session.execute(query)
|
||||||
return result.scalars().all()
|
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
|
@staticmethod
|
||||||
async def get_all_lotteries(session: AsyncSession, limit: Optional[int] = None) -> List[Lottery]:
|
async def get_all_lotteries(session: AsyncSession, limit: Optional[int] = None) -> List[Lottery]:
|
||||||
"""Получить список всех розыгрышей"""
|
"""Получить список всех розыгрышей"""
|
||||||
@@ -264,10 +289,16 @@ class LotteryService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
async def conduct_draw(session: AsyncSession, lottery_id: int) -> Dict[int, Dict[str, Any]]:
|
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)
|
lottery = await LotteryService.get_lottery(session, lottery_id)
|
||||||
if not lottery or lottery.is_completed:
|
if not lottery or lottery.is_completed:
|
||||||
|
logger.warning(f"conduct_draw: lottery не найден или завершён")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
logger.info(f"conduct_draw: получаем участников")
|
||||||
# Получаем всех участников (включая тех, у кого нет user)
|
# Получаем всех участников (включая тех, у кого нет user)
|
||||||
participants = []
|
participants = []
|
||||||
for p in lottery.participations:
|
for p in lottery.participations:
|
||||||
@@ -282,7 +313,9 @@ class LotteryService:
|
|||||||
'account_number': p.account_number
|
'account_number': p.account_number
|
||||||
})())
|
})())
|
||||||
|
|
||||||
|
logger.info(f"conduct_draw: участников {len(participants)}")
|
||||||
if not participants:
|
if not participants:
|
||||||
|
logger.warning(f"conduct_draw: нет участников")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Определяем количество призовых мест
|
# Определяем количество призовых мест
|
||||||
@@ -336,6 +369,7 @@ class LotteryService:
|
|||||||
session.add(winner)
|
session.add(winner)
|
||||||
|
|
||||||
# Обновляем статус розыгрыша
|
# Обновляем статус розыгрыша
|
||||||
|
logger.info(f"conduct_draw: обновляем статус lottery")
|
||||||
lottery.is_completed = True
|
lottery.is_completed = True
|
||||||
lottery.draw_results = {}
|
lottery.draw_results = {}
|
||||||
for place, info in results.items():
|
for place, info in results.items():
|
||||||
@@ -349,7 +383,8 @@ class LotteryService:
|
|||||||
'is_manual': info['is_manual']
|
'is_manual': info['is_manual']
|
||||||
}
|
}
|
||||||
|
|
||||||
await session.commit()
|
# НЕ коммитим здесь - это должно сделать вызывающая функция
|
||||||
|
logger.info(f"conduct_draw: изменения подготовлены, победителей: {len(results)}")
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ class AddAccountStates(StatesGroup):
|
|||||||
choosing_lottery = State()
|
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"))
|
@router.message(Command("add_account"))
|
||||||
@admin_only
|
@admin_only
|
||||||
async def add_account_command(message: Message, state: FSMContext):
|
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 state.set_state(AddAccountStates.waiting_for_data)
|
||||||
await message.answer(
|
await message.answer(
|
||||||
"💳 **Добавление счетов**\n\n"
|
"💳 **Добавление счетов**\n\n"
|
||||||
"Отправьте данные в формате:\n"
|
"📋 **Формат 1 (однострочный):**\n"
|
||||||
"`клубная_карта номер_счета`\n\n"
|
"`карта счет`\n"
|
||||||
"**Для одного счета:**\n"
|
"Пример: `2223 11-22-33-44-55-66-77`\n\n"
|
||||||
"`2223 11-22-33-44-55-66-77`\n\n"
|
"📋 **Формат 2 (многострочный из таблицы):**\n"
|
||||||
"**Для нескольких счетов (каждый с новой строки):**\n"
|
"Скопируйте столбцы со счетами и картами - система сама распознает\n\n"
|
||||||
|
"**Для нескольких счетов:**\n"
|
||||||
"`2223 11-22-33-44-55-66-77`\n"
|
"`2223 11-22-33-44-55-66-77`\n"
|
||||||
"`2223 88-99-00-11-22-33-44`\n"
|
"`2223 88-99-00-11-22-33-44`\n"
|
||||||
"`3334 12-34-56-78-90-12-34`\n\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:
|
if owner:
|
||||||
text += f"👤 Владелец: {owner.first_name}\n\n"
|
text += f"👤 Владелец: {owner.first_name}\n\n"
|
||||||
|
|
||||||
# Отправляем уведомление владельцу
|
# Отправляем уведомление владельцу с форматированием
|
||||||
try:
|
try:
|
||||||
await message.bot.send_message(
|
await message.bot.send_message(
|
||||||
owner.telegram_id,
|
owner.telegram_id,
|
||||||
f"✅ К вашему профилю добавлен счет:\n\n"
|
f"✅ К вашему профилю добавлен счет:\n\n"
|
||||||
f"💳 {account_number}\n\n"
|
f"💳 `{account_number}`\n\n"
|
||||||
f"Теперь вы можете участвовать в розыгрышах с этим счетом!"
|
f"Теперь вы можете участвовать в розыгрышах!",
|
||||||
|
parse_mode="Markdown"
|
||||||
)
|
)
|
||||||
text += "📨 Владельцу отправлено уведомление\n\n"
|
text += "📨 Владельцу отправлено уведомление\n\n"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -118,17 +128,66 @@ async def process_accounts_data(message: Message, state: FSMContext):
|
|||||||
return
|
return
|
||||||
|
|
||||||
lines = message.text.strip().split('\n')
|
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 = []
|
accounts_data = []
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
for i, line in enumerate(lines, 1):
|
BATCH_SIZE = 100 # Обрабатываем по 100 счетов за раз
|
||||||
parts = line.strip().split()
|
|
||||||
if len(parts) != 2:
|
# Универсальный парсер: поддержка однострочного и многострочного формата
|
||||||
errors.append(f"Строка {i}: неверный формат (ожидается: клубная_карта номер_счета)")
|
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
|
continue
|
||||||
|
|
||||||
|
# Проверяем, есть ли в строке пробел (однострочный формат: "карта счет")
|
||||||
|
if ' ' in line:
|
||||||
|
# Однострочный формат: разделяем по первому пробелу
|
||||||
|
parts = line.split(maxsplit=1)
|
||||||
|
if len(parts) == 2:
|
||||||
club_card, account_number = parts
|
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:
|
try:
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
account = await AccountService.create_account(
|
account = await AccountService.create_account(
|
||||||
@@ -143,25 +202,99 @@ async def process_accounts_data(message: Message, state: FSMContext):
|
|||||||
'club_card': club_card,
|
'club_card': club_card,
|
||||||
'account_number': account_number,
|
'account_number': account_number,
|
||||||
'account_id': account.id,
|
'account_id': account.id,
|
||||||
'owner': owner
|
'owner': owner,
|
||||||
|
'owner_id': owner.telegram_id if owner else None
|
||||||
})
|
})
|
||||||
|
|
||||||
# Отправляем уведомление владельцу
|
# Обновляем progress каждые 50 счетов
|
||||||
if owner:
|
if len(accounts_data) % 50 == 0:
|
||||||
try:
|
try:
|
||||||
await message.bot.send_message(
|
await progress_msg.edit_text(
|
||||||
owner.telegram_id,
|
f"⏳ Обработано: {len(accounts_data)} / ~{len(lines)}\n"
|
||||||
f"✅ К вашему профилю добавлен счет:\n\n"
|
f"❌ Ошибок: {len(errors)}"
|
||||||
f"💳 {account_number}\n\n"
|
|
||||||
f"Теперь вы можете участвовать в розыгрышах!"
|
|
||||||
)
|
)
|
||||||
|
except:
|
||||||
|
pass # Игнорируем ошибки редактирования
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
errors.append(f"Счет {account_number} (карта {club_card}): {str(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"Счет {account_number}: {str(e)}")
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Удаляем progress сообщение
|
||||||
|
try:
|
||||||
|
await progress_msg.delete()
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
except ValueError as e:
|
# Группируем счета по владельцам и отправляем групповые уведомления
|
||||||
errors.append(f"Строка {i} ({club_card} {account_number}): {str(e)}")
|
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:
|
except Exception as e:
|
||||||
errors.append(f"Строка {i}: {str(e)}")
|
pass # Игнорируем ошибки отправки уведомлений
|
||||||
|
|
||||||
# Формируем отчет
|
# Формируем отчет
|
||||||
text = f"📊 **Результаты добавления счетов**\n\n"
|
text = f"📊 **Результаты добавления счетов**\n\n"
|
||||||
@@ -305,31 +438,70 @@ async def skip_lottery_add(callback: CallbackQuery, state: FSMContext):
|
|||||||
@admin_only
|
@admin_only
|
||||||
async def remove_account_command(message: Message):
|
async def remove_account_command(message: Message):
|
||||||
"""
|
"""
|
||||||
Деактивировать счет
|
Деактивировать счет(а)
|
||||||
Формат: /remove_account <account_number>
|
Формат: /remove_account <account_number1> [account_number2] [account_number3] ...
|
||||||
|
Можно указать несколько счетов через пробел для массового удаления
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parts = message.text.split()
|
parts = message.text.split()
|
||||||
if len(parts) != 2:
|
if len(parts) < 2:
|
||||||
await message.answer(
|
await message.answer(
|
||||||
"❌ Неверный формат команды\n\n"
|
"❌ Неверный формат команды\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
|
return
|
||||||
|
|
||||||
account_number = parts[1]
|
account_numbers = parts[1:] # Все аргументы после команды
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with async_session_maker() as session:
|
results = {
|
||||||
success = await AccountService.deactivate_account(session, account_number)
|
'success': [],
|
||||||
|
'not_found': [],
|
||||||
|
'errors': []
|
||||||
|
}
|
||||||
|
|
||||||
|
async with async_session_maker() as session:
|
||||||
|
for account_number in account_numbers:
|
||||||
|
try:
|
||||||
|
success = await AccountService.deactivate_account(session, account_number)
|
||||||
if success:
|
if success:
|
||||||
await message.answer(f"✅ Счет {account_number} деактивирован")
|
results['success'].append(account_number)
|
||||||
else:
|
else:
|
||||||
await message.answer(f"❌ Счет {account_number} не найден")
|
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("\n\n".join(response_parts), parse_mode="Markdown")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await message.answer(f"❌ Ошибка: {str(e)}")
|
await message.answer(f"❌ Критическая ошибка: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.message(Command("verify_winner"))
|
@router.message(Command("verify_winner"))
|
||||||
@@ -569,3 +741,71 @@ async def user_info_command(message: Message):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await message.answer(f"❌ Ошибка: {str(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.database import async_session_maker
|
||||||
from ..core.services import UserService, LotteryService, ParticipationService
|
from ..core.services import UserService, LotteryService, ParticipationService
|
||||||
|
from ..core.chat_services import ChatMessageService
|
||||||
from ..core.config import ADMIN_IDS
|
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__)
|
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):
|
class AdminStates(StatesGroup):
|
||||||
# Создание розыгрыша
|
# Создание розыгрыша
|
||||||
@@ -178,14 +205,23 @@ async def show_lottery_management(callback: CallbackQuery):
|
|||||||
@admin_router.callback_query(F.data == "admin_create_lottery")
|
@admin_router.callback_query(F.data == "admin_create_lottery")
|
||||||
async def start_create_lottery(callback: CallbackQuery, state: FSMContext):
|
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):
|
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
|
return
|
||||||
|
|
||||||
|
logging.info(f"✅ Админ {callback.from_user.id} начинает создание розыгрыша")
|
||||||
|
|
||||||
text = "📝 Создание нового розыгрыша\n\n"
|
text = "📝 Создание нового розыгрыша\n\n"
|
||||||
text += "Шаг 1 из 4\n\n"
|
text += "Шаг 1 из 4\n\n"
|
||||||
text += "Введите название розыгрыша:"
|
text += "Введите название розыгрыша:"
|
||||||
|
|
||||||
|
try:
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
text,
|
text,
|
||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||||
@@ -193,15 +229,49 @@ async def start_create_lottery(callback: CallbackQuery, state: FSMContext):
|
|||||||
])
|
])
|
||||||
)
|
)
|
||||||
await state.set_state(AdminStates.lottery_title)
|
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))
|
@admin_router.message(StateFilter(AdminStates.lottery_title))
|
||||||
async def process_lottery_title(message: Message, state: FSMContext):
|
async def process_lottery_title(message: Message, state: FSMContext):
|
||||||
"""Обработка названия розыгрыша"""
|
"""Обработка названия розыгрыша (создание или редактирование)"""
|
||||||
if not is_admin(message.from_user.id):
|
if not is_admin(message.from_user.id):
|
||||||
await message.answer("❌ Недостаточно прав")
|
await message.answer("❌ Недостаточно прав")
|
||||||
return
|
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)
|
await state.update_data(title=message.text)
|
||||||
|
|
||||||
text = f"📝 Создание нового розыгрыша\n\n"
|
text = f"📝 Создание нового розыгрыша\n\n"
|
||||||
@@ -215,11 +285,42 @@ async def process_lottery_title(message: Message, state: FSMContext):
|
|||||||
|
|
||||||
@admin_router.message(StateFilter(AdminStates.lottery_description))
|
@admin_router.message(StateFilter(AdminStates.lottery_description))
|
||||||
async def process_lottery_description(message: Message, state: FSMContext):
|
async def process_lottery_description(message: Message, state: FSMContext):
|
||||||
"""Обработка описания розыгрыша"""
|
"""Обработка описания розыгрыша (создание или редактирование)"""
|
||||||
if not is_admin(message.from_user.id):
|
if not is_admin(message.from_user.id):
|
||||||
await message.answer("❌ Недостаточно прав")
|
await message.answer("❌ Недостаточно прав")
|
||||||
return
|
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
|
description = None if message.text == "-" else message.text
|
||||||
await state.update_data(description=description)
|
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))
|
@admin_router.message(StateFilter(AdminStates.lottery_prizes))
|
||||||
async def process_lottery_prizes(message: Message, state: FSMContext):
|
async def process_lottery_prizes(message: Message, state: FSMContext):
|
||||||
"""Обработка призов розыгрыша"""
|
"""Обработка призов розыгрыша (создание или редактирование)"""
|
||||||
if not is_admin(message.from_user.id):
|
if not is_admin(message.from_user.id):
|
||||||
await message.answer("❌ Недостаточно прав")
|
await message.answer("❌ Недостаточно прав")
|
||||||
return
|
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()]
|
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)
|
await state.update_data(prizes=prizes)
|
||||||
|
|
||||||
data = await state.get_data()
|
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))
|
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_"))
|
@admin_router.callback_query(F.data.startswith("admin_edit_"))
|
||||||
async def redirect_to_edit_lottery(callback: CallbackQuery, state: FSMContext):
|
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_"))
|
@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):
|
if not is_admin(callback.from_user.id):
|
||||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||||
@@ -1759,7 +1932,7 @@ async def toggle_lottery_active(callback: CallbackQuery):
|
|||||||
await callback.answer("❌ Ошибка изменения статуса", show_alert=True)
|
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")
|
@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))
|
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):
|
async def conduct_lottery_draw_confirm(callback: CallbackQuery):
|
||||||
"""Запрос подтверждения проведения розыгрыша"""
|
"""Запрос подтверждения проведения розыгрыша"""
|
||||||
if not is_admin(callback.from_user.id):
|
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
|
prizes_count = len(lottery.prizes) if lottery.prizes else 0
|
||||||
|
|
||||||
# Формируем сообщение с подтверждением
|
# Формируем сообщение с подтверждением
|
||||||
text = f"⚠️ <b>Подтверждение проведения розыгрыша</b>\n\n"
|
text = f"⚠️ *Подтверждение проведения розыгрыша*\n\n"
|
||||||
text += f"🎲 <b>Розыгрыш:</b> {lottery.title}\n"
|
text += f"🎲 *Розыгрыш:* {lottery.title}\n"
|
||||||
text += f"👥 <b>Участников:</b> {participants_count}\n"
|
text += f"👥 *Участников:* {participants_count}\n"
|
||||||
text += f"🏆 <b>Призов:</b> {prizes_count}\n\n"
|
text += f"🏆 *Призов:* {prizes_count}\n\n"
|
||||||
|
|
||||||
if lottery.prizes:
|
if lottery.prizes:
|
||||||
text += "<b>Призы:</b>\n"
|
text += "*Призы:*\n"
|
||||||
for i, prize in enumerate(lottery.prizes, 1):
|
for i, prize in enumerate(lottery.prizes, 1):
|
||||||
text += f"{i}. {prize}\n"
|
text += f"{i}. {prize}\n"
|
||||||
text += "\n"
|
text += "\n"
|
||||||
|
|
||||||
text += "❗️ <b>Внимание:</b> После проведения розыгрыша результаты нельзя будет изменить!\n\n"
|
text += "❗️ *Внимание:* После проведения розыгрыша результаты нельзя будет изменить!\n\n"
|
||||||
text += "Продолжить?"
|
text += "Продолжить?"
|
||||||
|
|
||||||
|
confirm_callback = f"admin_conduct_confirmed_{lottery_id}"
|
||||||
|
logger.info(f"Создаём кнопку подтверждения с callback_data='{confirm_callback}'")
|
||||||
|
|
||||||
buttons = [
|
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}")]
|
[InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_lottery_{lottery_id}")]
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
await safe_edit_message(callback, text, InlineKeyboardMarkup(inline_keyboard=buttons))
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@admin_router.callback_query(F.data.startswith("admin_conduct_confirmed_"))
|
@admin_router.callback_query(F.data.startswith("admin_conduct_confirmed_"))
|
||||||
async def conduct_lottery_draw(callback: CallbackQuery):
|
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):
|
if not is_admin(callback.from_user.id):
|
||||||
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
await callback.answer("❌ Недостаточно прав", show_alert=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
lottery_id = int(callback.data.split("_")[-1])
|
lottery_id = int(callback.data.split("_")[-1])
|
||||||
|
logger.info(f"Извлечен lottery_id={lottery_id}")
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
|
logger.info(f"Создана сессия БД")
|
||||||
lottery = await LotteryService.get_lottery(session, lottery_id)
|
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:
|
if not lottery:
|
||||||
await callback.answer("Розыгрыш не найден", show_alert=True)
|
await callback.answer("Розыгрыш не найден", show_alert=True)
|
||||||
@@ -2681,9 +2857,21 @@ async def conduct_lottery_draw(callback: CallbackQuery):
|
|||||||
await callback.answer("⏳ Проводится розыгрыш...", show_alert=True)
|
await callback.answer("⏳ Проводится розыгрыш...", show_alert=True)
|
||||||
|
|
||||||
# Проводим розыгрыш через сервис
|
# Проводим розыгрыш через сервис
|
||||||
|
logger.info(f"Начинаем проведение розыгрыша {lottery_id}")
|
||||||
|
try:
|
||||||
winners_dict = await LotteryService.conduct_draw(session, lottery_id)
|
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:
|
if winners_dict:
|
||||||
|
# Коммитим изменения в БД
|
||||||
|
await session.commit()
|
||||||
|
logger.info(f"Изменения закоммичены для розыгрыша {lottery_id}")
|
||||||
|
|
||||||
# Отправляем уведомления победителям
|
# Отправляем уведомления победителям
|
||||||
from ..utils.notifications import notify_winners_async
|
from ..utils.notifications import notify_winners_async
|
||||||
try:
|
try:
|
||||||
@@ -3225,5 +3413,286 @@ async def apply_display_type(callback: CallbackQuery, state: FSMContext):
|
|||||||
await state.clear()
|
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']
|
__all__ = ['admin_router']
|
||||||
@@ -109,6 +109,73 @@ async def handle_text_message(message: Message):
|
|||||||
logger = logging.getLogger(__name__)
|
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'}")
|
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('/'):
|
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 ''
|
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:
|
if command in user_commands:
|
||||||
return
|
return
|
||||||
|
# Любая другая команда от админа - тоже не пересылаем
|
||||||
# Если это админская команда
|
|
||||||
if command in admin_commands:
|
|
||||||
# Проверяем права админа
|
|
||||||
if not is_admin(message.from_user.id):
|
|
||||||
await message.answer("❌ У вас нет прав для выполнения этой команды")
|
|
||||||
return
|
|
||||||
# Если админ - команда будет обработана другими обработчиками, пропускаем пересылку
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Если неизвестная команда - тоже не пересылаем
|
# ИЗМЕНЕНИЕ: Если команда от обычного пользователя - ПЕРЕСЫЛАЕМ админу
|
||||||
return
|
# Чтобы админ видел, что пользователь отправил /start или другую команду
|
||||||
|
# НЕ делаем return, продолжаем выполнение для пересылки
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
# Проверяем права на отправку
|
# Проверяем права на отправку
|
||||||
@@ -163,7 +229,8 @@ async def handle_text_message(message: Message):
|
|||||||
# Обрабатываем в зависимости от режима
|
# Обрабатываем в зависимости от режима
|
||||||
if settings.mode == 'broadcast':
|
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(
|
await ChatMessageService.save_message(
|
||||||
@@ -231,7 +298,12 @@ async def handle_photo_message(message: Message):
|
|||||||
photo = message.photo[-1]
|
photo = message.photo[-1]
|
||||||
|
|
||||||
if settings.mode == 'broadcast':
|
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(
|
await ChatMessageService.save_message(
|
||||||
session,
|
session,
|
||||||
@@ -245,7 +317,7 @@ async def handle_photo_message(message: Message):
|
|||||||
|
|
||||||
# Показываем статистику только админам
|
# Показываем статистику только админам
|
||||||
if is_admin(message.from_user.id):
|
if is_admin(message.from_user.id):
|
||||||
await message.answer(f"✅ Фото разослано: {success} получателей")
|
await message.answer(f"✅ Фото отправлено админам: {success}")
|
||||||
|
|
||||||
elif settings.mode == 'forward':
|
elif settings.mode == 'forward':
|
||||||
if settings.forward_chat_id:
|
if settings.forward_chat_id:
|
||||||
@@ -285,7 +357,8 @@ async def handle_video_message(message: Message):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if settings.mode == 'broadcast':
|
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(
|
await ChatMessageService.save_message(
|
||||||
session,
|
session,
|
||||||
@@ -339,7 +412,8 @@ async def handle_document_message(message: Message):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if settings.mode == 'broadcast':
|
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(
|
await ChatMessageService.save_message(
|
||||||
session,
|
session,
|
||||||
@@ -393,7 +467,8 @@ async def handle_animation_message(message: Message):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if settings.mode == 'broadcast':
|
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(
|
await ChatMessageService.save_message(
|
||||||
session,
|
session,
|
||||||
@@ -447,7 +522,8 @@ async def handle_sticker_message(message: Message):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if settings.mode == 'broadcast':
|
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(
|
await ChatMessageService.save_message(
|
||||||
session,
|
session,
|
||||||
@@ -480,51 +556,19 @@ async def handle_sticker_message(message: Message):
|
|||||||
|
|
||||||
@router.message(F.voice)
|
@router.message(F.voice)
|
||||||
async def handle_voice_message(message: Message):
|
async def handle_voice_message(message: Message):
|
||||||
"""Обработчик голосовых сообщений"""
|
"""Обработчик голосовых сообщений - ЗАБЛОКИРОВАНО"""
|
||||||
async with async_session_maker() as session:
|
await message.answer(
|
||||||
can_send, reason = await ChatPermissionService.can_send_message(
|
"🚫 Голосовые сообщения запрещены.\n\n"
|
||||||
session,
|
"Пожалуйста, используйте текстовые сообщения или изображения."
|
||||||
message.from_user.id,
|
|
||||||
is_admin=is_admin(message.from_user.id)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not can_send:
|
|
||||||
await message.answer(f"❌ {reason}")
|
|
||||||
return
|
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:
|
@router.message(F.audio)
|
||||||
|
async def handle_audio_message(message: Message):
|
||||||
|
"""Обработчик аудиофайлов (музыка, аудиозаписи) - ЗАБЛОКИРОВАНО"""
|
||||||
|
await message.answer(
|
||||||
|
"🚫 Аудиофайлы запрещены.\n\n"
|
||||||
|
"Пожалуйста, используйте текстовые сообщения или изображения."
|
||||||
|
)
|
||||||
return
|
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("✅ Голосовое сообщение переслано в канал")
|
|
||||||
|
|||||||
@@ -304,3 +304,97 @@ async def redraw_lottery(message: Message):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await message.answer(f"❌ Ошибка: {str(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