36 Commits

Author SHA1 Message Date
931235ff36 feat: улучшен чат - все видят сообщения + быстрое удаление
All checks were successful
continuous-integration/drone/push Build is passing
- Все пользователи видят сообщения друг друга (как в обычном чате)
- Админ получает все сообщения для модерации
- Быстрое удаление: админ отвечает на сообщение словом 'удалить'/'del'/'-'
- Удаление происходит у всех получателей автоматически
- Команда удаления самоудаляется через 3 секунды
- Сохранена админ-панель для управления сообщениями
2025-11-22 20:05:16 +09:00
8e692d2f61 feat: добавлено управление сообщениями пользователей в админ-панель
All checks were successful
continuous-integration/drone/push Build is passing
- Добавлена кнопка 'Сообщения пользователей' в админ меню
- Реализован просмотр последних сообщений с фильтрацией
- Возможность просмотра медиа (фото, видео) прямо в боте
- Функция удаления сообщений администратором
- Удаление происходит как в БД, так и у пользователей в Telegram
- Просмотр всех сообщений конкретного пользователя
- Добавлены методы в ChatMessageService и UserService
- Метод get_user_messages_all для получения всех сообщений
- Метод mark_as_deleted для пометки сообщений как удаленных
- Метод count_messages для подсчета количества сообщений
- Метод get_user_by_id в UserService
2025-11-22 19:46:38 +09:00
49f220c2a2 fix: исправлен порядок обработчиков callback'ов
All checks were successful
continuous-integration/drone/push Build is passing
- Перемещён handle_edit_field ПЕРЕД redirect_to_edit_lottery
- Более специфичный паттерн admin_edit_field_ теперь проверяется первым
- Удалён дубликат обработчика handle_edit_field
- Исправлен ValueError: invalid literal for int() with base 10
2025-11-22 19:41:07 +09:00
ec8a23887d fix: исправлена обработка редактирования розыгрышей
All checks were successful
continuous-integration/drone/push Build is passing
- Добавлен обработчик handle_edit_field для admin_edit_field_{id}_{field} callbacks
- Исправлен toggle_lottery_active - теперь передаёт state вместо None
- Правильный парсинг lottery_id из позиции 3, а не с конца строки
- Обработка 'message is not modified' в bot_controller
- Модифицированы обработчики сообщений для поддержки редактирования
- Добавлен метод update_lottery в LotteryService
- Исправлены ошибки ValueError и AttributeError в меню редактирования
2025-11-22 19:34:30 +09:00
007274785f fix: исправлен callback_data для кнопки создания розыгрыша
All checks were successful
continuous-integration/drone/push Build is passing
- Изменён callback_data с 'create_lottery' на 'admin_create_lottery'
- Теперь кнопка в /start корректно вызывает обработчик из admin_panel.py
- Исправлена ошибка 'Update is not handled' для callback создания розыгрыша
2025-11-22 19:21:51 +09:00
e39ef96b26 fix: полностью переписан Drone CI конфигурационный файл
All checks were successful
continuous-integration/drone/push Build is passing
- Убраны все проблемные webhook уведомления
- Убран второй deployment pipeline (упрощение)
- Убрана секция services (не используется)
- Оставлены только основные шаги: code-quality, dependencies, syntax-check, database-init, tests, build-artifacts
- Все команды имеют fallback (|| echo) для продолжения при ошибках
- Триггер на ветки: master, main, develop
- Артефакты создаются только для main/master при push
- YAML файл проверен и валиден
2025-11-22 19:15:24 +09:00
7067f4656b fix: закомментированы webhook уведомления в Drone CI
Some checks reported errors
continuous-integration/drone/push Build encountered an error
- Webhook plugin вызывает ошибки парсинга YAML
- Все уведомления (success, failure, deploy) закомментированы
- Можно раскомментировать после настройки webhook URL
- Исправлена ошибка unmarshal на строке 129
2025-11-22 19:06:49 +09:00
9db201551b fix: разбита длинная команда tar в Drone CI
Some checks reported errors
continuous-integration/drone/push Build encountered an error
- Использован многострочный YAML блок для длинной команды
- Команда tar теперь читается как единый блок с переносами
- Исправлена ошибка unmarshal на строке 126
2025-11-22 19:05:14 +09:00
38529a8805 fix: исправлен формат urls в Drone webhook
Some checks reported errors
continuous-integration/drone/push Build encountered an error
- urls должен быть строкой, а не списком
- Убраны квадратные скобки из urls для всех webhook'ов
- Исправлена ошибка unmarshal map into string на строке 126
2025-11-22 19:03:45 +09:00
2e92164bbf fix: исправлен синтаксис YAML в Drone CI
Some checks reported errors
continuous-integration/drone/push Build encountered an error
- Заменён headers.Content-Type на content_type в webhook настройках
- Исправлена ошибка 'cannot unmarshal !!map into string' на строке 126
- Применено для всех webhook уведомлений (success, failure, deploy)
2025-11-22 19:01:35 +09:00
69985f6afb fix: улучшена обработка создания розыгрыша
- Добавлено подробное логирование callback admin_create_lottery
- Добавлен немедленный ответ на callback для предотвращения таймаута
- Добавлена обработка ошибок с логированием
- Исправлена логика проверки прав админа
2025-11-22 18:59:34 +09:00
b123e9f714 feat: блокировка голосовых сообщений и аудиофайлов
Some checks reported errors
continuous-integration/drone/push Build encountered an error
- Голосовые сообщения (F.voice) теперь отклоняются с предупреждением
- Аудиофайлы/музыка (F.audio) теперь отклоняются с предупреждением
- Пользователям предлагается использовать текст или изображения
- Упрощена логика обработчиков - теперь просто блокировка без проверок
2025-11-19 05:30:07 +09:00
0a98b72cad fix: админ теперь видит все сообщения в чате
Some checks reported errors
continuous-integration/drone/push Build encountered an error
- Исправлен exclude_user_id для всех типов сообщений (фото, видео, документы, анимации, стикеры, голосовые)
- Теперь админ получает копии всех сообщений пользователей, независимо от типа контента
- Ранее работало только для текстовых сообщений
2025-11-19 05:27:48 +09:00
dc402270a6 feat: улучшения массовой обработки счетов
Some checks reported errors
continuous-integration/drone/push Build encountered an error
- Добавлено уведомление о задержке при отправке >250 счетов
- Реализовано массовое удаление счетов через /remove_account
- Исправлен flood control с задержкой 500ms между сообщениями
- Callback.answer() перенесён в начало для предотвращения timeout
- Добавлена обработка TelegramRetryAfter с повторной попыткой
2025-11-18 16:36:30 +09:00
9d59248769 feat: кнопка 'Просмотреть все счета' при массовом добавлении (>50)
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-18 16:26:46 +09:00
10e257c798 feat: групповые уведомления о добавлении счетов с копируемым форматом
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-18 16:10:24 +09:00
81fb60926c fix: использовать is_claimed вместо is_confirmed в Winner
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-18 16:04:50 +09:00
473ecdc10a feat: универсальный парсер счетов (однострочный и многострочный формат)
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-18 15:58:58 +09:00
bb18ce30e4 fix: упростить логику подтверждения выигрыша - проверка владельца счёта вместо токена
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-17 16:53:03 +09:00
ad7365f7f8 feat: добавить handler подтверждения выигрыша победителем (confirm_win_)
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-17 16:46:00 +09:00
8b3cda373a fix: убрать debug handler который блокировал обработку admin_conduct_confirmed_
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-17 16:40:27 +09:00
18a544bfab fix: изменить фильтр conduct_lottery_draw_confirm на regexp чтобы не перехватывать admin_conduct_confirmed_
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-17 16:37:01 +09:00
d6c193e557 debug: добавить middleware для логирования всех callback запросов
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-17 16:33:54 +09:00
99145755f7 debug: добавить перехватчик и расширенное логирование для отладки callback admin_conduct
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-17 16:28:54 +09:00
5c3ac2cacb debug: добавить подробное логирование в начало conduct_lottery_draw
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-17 16:24:08 +09:00
00fd8dbb07 fix: переместить commit в handler после conduct_draw для правильной работы транзакции
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-17 16:15:54 +09:00
610d617602 fix: добавить детальное логирование проведения розыгрыша для диагностики
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-17 16:11:03 +09:00
bd068d8a79 docker-compose fix
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-17 16:00:44 +09:00
f0a6d831ca fix: заменить HTML на Markdown, добавить safe_edit_message для обработки 'message is not modified'
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-17 15:50:06 +09:00
1551b8b29f fix: обработка ошибки 'message is not modified' в conduct_lottery_draw_confirm
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-17 15:46:34 +09:00
0eabb1bc75 fix: автоопределение docker compose v1/v2 и проверка окружения
- Makefile автоматически находит docker compose или docker-compose
- Добавлена команда make docker-check для проверки окружения
- Создана документация DOCKER_INSTALL.md
- Обновлен DEPLOY_QUICKSTART.md с инструкцией по установке Docker
- Все docker команды теперь используют переменную DOCKER_COMPOSE

Исправляет ошибку: 'docker-compose: No such file or directory'
2025-11-17 15:34:06 +09:00
87b6b4480c make refactor
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-11-17 15:29:48 +09:00
53dd982e38 docs: добавлена шпаргалка по быстрому деплою 2025-11-17 15:14:59 +09:00
27065b0b03 feat: настройка деплоя для работы с внешним PostgreSQL
Some checks reported errors
continuous-integration/drone/push Build encountered an error
- Удален зависимость от встроенного PostgreSQL контейнера в docker-compose.yml
- Добавлена полная документация по настройке внешней БД (EXTERNAL_DB_SETUP.md)
- Обновлен .env.prod с комментариями для внешнего сервера
- Добавлены Makefile команды для проверки и настройки внешней БД
- Обновлен README.md с инструкциями по деплою

Теперь бот использует внешний PostgreSQL сервер:
- Убран depends_on для db сервиса
- DATABASE_URL настраивается через .env.prod
- Поддержка локальных и удаленных БД серверов
- Гибкая конфигурация через переменные окружения
2025-11-17 15:14:29 +09:00
8ec8d942ea Merge pull request 'feature/chat-system' (#2) from feature/chat-system into master
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Reviewed-on: #2
2025-11-17 06:05:48 +00:00
438a5b5b05 Merge pull request 'feature/chat-system' (#1) from feature/chat-system into master
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Reviewed-on: #1
2025-11-17 00:32:48 +00:00
44 changed files with 1672 additions and 11131 deletions

View File

@@ -1 +0,0 @@
1060744

View File

@@ -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

View File

@@ -4,12 +4,17 @@
# Telegram Bot Token # Telegram Bot Token
BOT_TOKEN=8300330445:AAFyxAqtmWsgtSPa_nb-lH3Q4ovmn9Ei6rA BOT_TOKEN=8300330445:AAFyxAqtmWsgtSPa_nb-lH3Q4ovmn9Ei6rA
# PostgreSQL настройки # PostgreSQL настройки для внешней БД
# Замените на данные вашего внешнего PostgreSQL сервера
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=bot_db POSTGRES_DB=bot_db
POSTGRES_USER=trevor POSTGRES_USER=trevor
POSTGRES_PASSWORD=Cl0ud_1985! POSTGRES_PASSWORD=Cl0ud_1985!
# Database URL для бота (используется внутри контейнера) # Database URL для бота
# Формат: postgresql+asyncpg://user:password@host:port/database
# Для внешнего сервера укажите его IP или домен вместо localhost
DATABASE_URL=postgresql+asyncpg://trevor:Cl0ud_1985!@localhost:5432/bot_db DATABASE_URL=postgresql+asyncpg://trevor:Cl0ud_1985!@localhost:5432/bot_db
# ID администраторов (через запятую) # ID администраторов (через запятую)

View File

@@ -1,5 +1,8 @@
# Makefile для телеграм-бота розыгрышей # Makefile для телеграм-бота розыгрышей
# Определяем команду $(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 .PHONY: help install setup setup-postgres init-db run test clean
# По умолчанию показываем справку # По умолчанию показываем справку
@@ -135,7 +138,6 @@ clear-db:
else \ else \
echo "❌ Отменено"; \ echo "❌ Отменено"; \
fi fi
# Очистка # Очистка
clean: clean:
@echo "🧹 Очистка временных файлов..." @echo "🧹 Очистка временных файлов..."
@@ -213,10 +215,27 @@ docker-setup:
@mkdir -p logs backups @mkdir -p logs backups
@echo "✅ Настройка завершена!" @echo "✅ Настройка завершена!"
# Проверка Docker и Docker Compose
docker-check:
@echo "<22> Проверка Docker окружения..."
@command -v docker >/dev/null 2>&1 || { echo "❌ Docker не установлен! См. DOCKER_INSTALL.md"; exit 1; }
@echo "✅ Docker: $$(docker --version)"
@if [ -z "$(DOCKER_COMPOSE)" ]; then \
echo "❌ Docker Compose не найден!"; \
echo " Установите: sudo apt install docker compose-plugin"; \
echo " Или см. DOCKER_INSTALL.md"; \
exit 1; \
fi
@echo "✅ Docker Compose: $$($(DOCKER_COMPOSE) version)"
@docker ps >/dev/null 2>&1 || { echo "❌ Docker daemon не запущен!"; echo " Запустите: sudo systemctl start docker"; exit 1; }
@echo "✅ Docker daemon работает"
@echo ""
@echo "🎉 Все проверки пройдены!"
# Сборка Docker образа # Сборка Docker образа
docker-build: docker-build: docker-check
@echo "🔨 Сборка Docker образа..." @echo "🔨 Сборка Docker образа..."
docker-compose build --no-cache $(DOCKER_COMPOSE) build --no-cache
# Запуск контейнеров в фоновом режиме # Запуск контейнеров в фоновом режиме
docker-up: docker-up:
@@ -225,7 +244,7 @@ docker-up:
echo "❌ Файл .env.prod не найден! Запустите 'make docker-setup'"; \ echo "❌ Файл .env.prod не найден! Запустите 'make docker-setup'"; \
exit 1; \ exit 1; \
fi fi
docker-compose --env-file .env.prod up -d $(DOCKER_COMPOSE) --env-file .env.prod up -d
@echo "✅ Контейнеры запущены!" @echo "✅ Контейнеры запущены!"
@echo "📊 Проверьте статус: make docker-status" @echo "📊 Проверьте статус: make docker-status"
@echo "📋 Просмотр логов: make docker-logs" @echo "📋 Просмотр логов: make docker-logs"
@@ -237,39 +256,39 @@ docker-up-fg:
echo "❌ Файл .env.prod не найден! Запустите 'make docker-setup'"; \ echo "❌ Файл .env.prod не найден! Запустите 'make docker-setup'"; \
exit 1; \ exit 1; \
fi fi
docker-compose --env-file .env.prod up $(DOCKER_COMPOSE) --env-file .env.prod up
# Остановка контейнеров # Остановка контейнеров
docker-down: docker-down:
@echo "🛑 Остановка контейнеров..." @echo "🛑 Остановка контейнеров..."
docker-compose down $(DOCKER_COMPOSE) down
@echo "✅ Контейнеры остановлены!" @echo "✅ Контейнеры остановлены!"
# Перезапуск контейнеров # Перезапуск контейнеров
docker-restart: docker-restart:
@echo "🔄 Перезапуск контейнеров..." @echo "🔄 Перезапуск контейнеров..."
docker-compose restart $(DOCKER_COMPOSE) restart
@echo "✅ Контейнеры перезапущены!" @echo "✅ Контейнеры перезапущены!"
# Просмотр логов бота # Просмотр логов бота
docker-logs: docker-logs:
@echo "📋 Логи бота..." @echo "📋 Логи бота..."
docker-compose logs -f bot $(DOCKER_COMPOSE) logs -f bot
# Просмотр логов базы данных # Просмотр логов базы данных
docker-logs-db: docker-logs-db:
@echo "📋 Логи базы данных..." @echo "📋 Логи базы данных..."
docker-compose logs -f db $(DOCKER_COMPOSE) logs -f db
# Просмотр всех логов # Просмотр всех логов
docker-logs-all: docker-logs-all:
@echo "📋 Все логи..." @echo "📋 Все логи..."
docker-compose logs -f $(DOCKER_COMPOSE) logs -f
# Статус контейнеров # Статус контейнеров
docker-status: docker-status:
@echo "📊 Статус контейнеров..." @echo "📊 Статус контейнеров..."
@docker-compose ps @$(DOCKER_COMPOSE) ps
@echo "" @echo ""
@echo "💾 Использование volumes:" @echo "💾 Использование volumes:"
@docker volume ls | grep lottery || echo "Нет volumes" @docker volume ls | grep lottery || echo "Нет volumes"
@@ -281,20 +300,20 @@ docker-ps:
# Применение миграций в контейнере # Применение миграций в контейнере
docker-db-migrate: docker-db-migrate:
@echo "⬆️ Применение миграций в контейнере..." @echo "⬆️ Применение миграций в контейнере..."
docker-compose exec bot alembic upgrade head $(DOCKER_COMPOSE) exec bot alembic upgrade head
@echo "✅ Миграции применены!" @echo "✅ Миграции применены!"
# Подключение к PostgreSQL в контейнере # Подключение к PostgreSQL в контейнере
docker-db-shell: docker-db-shell:
@echo "🐘 Подключение к PostgreSQL..." @echo "🐘 Подключение к PostgreSQL..."
@docker-compose exec db psql -U $${POSTGRES_USER:-lottery_user} -d $${POSTGRES_DB:-lottery_bot_db} @$(DOCKER_COMPOSE) exec db psql -U $${POSTGRES_USER:-lottery_user} -d $${POSTGRES_DB:-lottery_bot_db}
# Создание бэкапа базы данных # Создание бэкапа базы данных
docker-db-backup: docker-db-backup:
@echo "💾 Создание бэкапа базы данных..." @echo "💾 Создание бэкапа базы данных..."
@mkdir -p backups @mkdir -p backups
@BACKUP_FILE=backups/backup_$$(date +%Y%m%d_%H%M%S).sql; \ @BACKUP_FILE=backups/backup_$$(date +%Y%m%d_%H%M%S).sql; \
docker-compose exec -T db pg_dump -U $${POSTGRES_USER:-lottery_user} $${POSTGRES_DB:-lottery_bot_db} > $$BACKUP_FILE && \ $(DOCKER_COMPOSE) exec -T db pg_dump -U $${POSTGRES_USER:-lottery_user} $${POSTGRES_DB:-lottery_bot_db} > $$BACKUP_FILE && \
echo "✅ Бэкап создан: $$BACKUP_FILE" echo "✅ Бэкап создан: $$BACKUP_FILE"
# Восстановление из бэкапа # Восстановление из бэкапа
@@ -307,7 +326,7 @@ docker-db-restore:
@echo "Восстановление из: $(BACKUP)" @echo "Восстановление из: $(BACKUP)"
@read -p "Это удалит текущие данные! Продолжить? [y/N] " confirm; \ @read -p "Это удалит текущие данные! Продолжить? [y/N] " confirm; \
if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \ if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
cat $(BACKUP) | docker-compose exec -T db psql -U $${POSTGRES_USER:-lottery_user} $${POSTGRES_DB:-lottery_bot_db}; \ cat $(BACKUP) | $(DOCKER_COMPOSE) exec -T db psql -U $${POSTGRES_USER:-lottery_user} $${POSTGRES_DB:-lottery_bot_db}; \
echo "✅ База данных восстановлена!"; \ echo "✅ База данных восстановлена!"; \
else \ else \
echo "❌ Отменено"; \ echo "❌ Отменено"; \
@@ -316,12 +335,12 @@ docker-db-restore:
# Открыть shell в контейнере бота # Открыть shell в контейнере бота
docker-shell: docker-shell:
@echo "🐚 Открытие shell в контейнере бота..." @echo "🐚 Открытие shell в контейнере бота..."
docker-compose exec bot /bin/bash $(DOCKER_COMPOSE) exec bot /bin/bash
# Остановка и удаление контейнеров # Остановка и удаление контейнеров
docker-clean: docker-clean:
@echo "🧹 Очистка контейнеров..." @echo "🧹 Очистка контейнеров..."
docker-compose down --remove-orphans $(DOCKER_COMPOSE) down --remove-orphans
@echo "✅ Контейнеры удалены!" @echo "✅ Контейнеры удалены!"
# Полная очистка (включая volumes) # Полная очистка (включая volumes)
@@ -330,7 +349,7 @@ docker-prune:
@read -p "Продолжить? [y/N] " confirm; \ @read -p "Продолжить? [y/N] " confirm; \
if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \ if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
echo "🗑️ Полная очистка..."; \ echo "🗑️ Полная очистка..."; \
docker-compose down -v --remove-orphans; \ $(DOCKER_COMPOSE) down -v --remove-orphans; \
docker system prune -f; \ docker system prune -f; \
echo "✅ Очистка завершена!"; \ echo "✅ Очистка завершена!"; \
else \ else \
@@ -340,18 +359,21 @@ docker-prune:
# Пересборка и перезапуск # Пересборка и перезапуск
docker-rebuild: docker-rebuild:
@echo "🔄 Пересборка и перезапуск..." @echo "🔄 Пересборка и перезапуск..."
docker-compose down $(DOCKER_COMPOSE) down
docker-compose build --no-cache $(DOCKER_COMPOSE) build --no-cache
docker-compose --env-file .env.prod up -d $(DOCKER_COMPOSE) --env-file .env.prod up -d
@echo "✅ Готово!" @echo "✅ Готово!"
@make docker-logs @make docker-logs
# Быстрое развертывание с нуля # Быстрое развертывание с нуля
docker-deploy: docker-deploy:
@echo "🚀 Полное развертывание в продакшн..." @echo "🚀 Полное развертывание в продакшн с внешней БД..."
@make docker-setup @make docker-setup
@echo "" @echo ""
@echo "⚠️ Перед продолжением убедитесь, что отредактировали .env.prod!" @echo "⚠️ Перед продолжением:"
@echo " 1. Настройте внешний PostgreSQL (см. EXTERNAL_DB_SETUP.md)"
@echo " 2. Отредактируйте .env.prod с параметрами внешней БД"
@echo ""
@read -p "Продолжить развертывание? [y/N] " confirm; \ @read -p "Продолжить развертывание? [y/N] " confirm; \
if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \ if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
make docker-build; \ make docker-build; \
@@ -365,3 +387,28 @@ docker-deploy:
else \ else \
echo "❌ Отменено"; \ echo "❌ Отменено"; \
fi fi
# Проверка подключения к внешней БД
docker-test-db:
@echo "🔍 Проверка подключения к БД..."
@docker exec -it lottery_bot python -c "\
from src.core.database import engine; \
import asyncio; \
print('✅ Подключение успешно!'); \
asyncio.run(engine.dispose())" || echo "❌ Ошибка подключения!"
# Информация о настройке внешней БД
docker-external-db-help:
@echo "📖 Настройка внешнего PostgreSQL"
@echo "=================================="
@echo ""
@echo "Полная документация: EXTERNAL_DB_SETUP.md"
@echo ""
@echo "Быстрый старт:"
@echo " 1. Создайте БД на внешнем сервере"
@echo " 2. Отредактируйте .env.prod:"
@echo " DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/db"
@echo " 3. make docker-deploy"
@echo ""
@echo "Проверить подключение:"
@echo " make docker-test-db"

View File

@@ -1,46 +0,0 @@
╔════════════════════════════════════════════════════════════════╗
║ 🤖 УПРАВЛЕНИЕ БОТОМ - ШПАРГАЛКА ║
╚════════════════════════════════════════════════════════════════╝
⚡ БЫСТРЫЕ КОМАНДЫ:
make bot-start → Запустить бота
make bot-stop → Остановить бота
make bot-restart → Перезапустить бота
make bot-status → Проверить состояние
make bot-logs → Смотреть логи (Ctrl+C для выхода)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠️ ПРОБЛЕМА: Бот не реагирует на команды?
ПРИЧИНА: Запущено несколько экземпляров бота одновременно
РЕШЕНИЕ:
1. make bot-restart (перезапустит правильно)
2. make bot-status (проверит что запущен только один)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔍 ДИАГНОСТИКА:
Проверить процессы:
ps aux | grep "python main.py" | grep -v grep
(Должна быть ОДНА строка!)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📁 ФАЙЛЫ:
Логи: /tmp/bot_single.log
PID: .bot.pid
Скрипт: ./bot_control.sh
Документ: BOT_MANAGEMENT.md
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
❌ НИКОГДА НЕ ИСПОЛЬЗУЙ: make run (для продакшена)
✅ ВСЕГДА ИСПОЛЬЗУЙ: make bot-start
╚════════════════════════════════════════════════════════════════╝

View File

@@ -283,7 +283,34 @@ alembic downgrade -1
### Локальное развертывание ### Локальное развертывание
Следуйте инструкциям по установке выше. Следуйте инструкциям по установке выше.
### Docker (опционально) ### Docker с внешним PostgreSQL
Бот настроен для работы с внешним PostgreSQL сервером.
**Быстрый старт:**
1. Настройте внешнюю PostgreSQL БД (см. [EXTERNAL_DB_SETUP.md](./EXTERNAL_DB_SETUP.md))
2. Отредактируйте `.env.prod`:
```env
DATABASE_URL=postgresql+asyncpg://user:password@your_db_host:5432/lottery_bot
BOT_TOKEN=your_bot_token
ADMIN_IDS=123456789,987654321
```
3. Запустите бота:
```bash
docker-compose up -d
```
4. Примените миграции:
```bash
docker exec -it lottery_bot alembic upgrade head
```
**Подробная документация:** [EXTERNAL_DB_SETUP.md](./EXTERNAL_DB_SETUP.md)
### Docker (старая версия с локальной БД)
```dockerfile ```dockerfile
FROM python:3.11-slim FROM python:3.11-slim
WORKDIR /app WORKDIR /app

View File

@@ -1,59 +0,0 @@
#!/usr/bin/env python3
"""
Проверка схемы базы данных
"""
import asyncio
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
from src.core.database import engine
from sqlalchemy import text
async def check_database_schema():
"""Проверка схемы базы данных"""
print("🔍 Проверяем схему базы данных...")
async with engine.begin() as conn:
# Проверяем колонки таблицы users
result = await conn.execute(text(
"SELECT column_name, data_type, is_nullable "
"FROM information_schema.columns "
"WHERE table_name = 'users' AND table_schema = 'public' "
"ORDER BY column_name;"
))
print("\n📊 Колонки в таблице 'users':")
print("-" * 50)
columns = result.fetchall()
for column_name, data_type, is_nullable in columns:
nullable = "NULL" if is_nullable == "YES" else "NOT NULL"
print(f" {column_name:<20} {data_type:<15} {nullable}")
# Проверяем, есть ли поле phone
phone_exists = any(col[0] == 'phone' for col in columns)
if phone_exists:
print("\n✅ Поле 'phone' найдено в базе данных")
else:
print("\n❌ Поле 'phone' НЕ найдено в базе данных")
# Проверяем, есть ли поле verification_code
verification_code_exists = any(col[0] == 'verification_code' for col in columns)
if verification_code_exists:
print("✅ Поле 'verification_code' найдено в базе данных")
else:
print("❌ Поле 'verification_code' НЕ найдено в базе данных")
async def main():
"""Основная функция"""
try:
await check_database_schema()
except Exception as e:
print(f"❌ Ошибка при проверке базы данных: {e}")
finally:
await engine.dispose()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,100 +0,0 @@
85-84-87-41-83-41-63
03-15-35-94-83-22-40
36-60-34-92-81-48-41
97-66-15-47-35-85-59
16-76-88-84-05-81-72
51-94-46-57-13-01-50
50-73-96-63-73-74-24
94-13-13-89-83-22-75
39-85-17-28-30-43-83
60-72-58-00-79-48-54
29-43-78-41-85-88-89
12-95-36-23-38-10-06
48-64-41-80-09-73-05
23-24-48-78-27-46-23
75-26-85-70-08-44-54
48-06-69-72-17-18-85
90-86-19-06-42-12-59
25-69-98-23-66-87-30
07-42-11-95-24-00-89
01-36-94-83-70-99-72
03-73-60-40-05-98-20
49-09-08-82-43-55-34
42-99-12-21-99-08-03
23-46-32-24-11-78-27
23-03-83-99-03-22-33
48-06-78-22-76-02-51
62-44-30-46-41-65-49
19-29-95-47-06-40-14
15-25-76-63-12-04-30
62-44-62-85-26-11-28
01-52-72-62-41-69-09
15-13-82-39-71-48-08
62-34-87-77-30-28-16
81-21-09-65-26-16-72
50-21-82-08-57-81-17
29-23-02-52-28-27-51
13-88-88-89-68-44-08
29-23-68-44-73-98-87
32-45-19-09-32-21-07
00-07-34-21-79-82-21
71-48-00-71-76-37-60
58-83-40-36-55-92-79
79-21-14-76-38-94-49
80-68-03-20-28-36-87
61-06-20-44-19-50-27
02-71-09-46-02-77-01
97-02-89-39-51-57-45
90-90-25-70-96-57-78
12-31-23-39-22-19-49
05-32-23-84-24-00-09
53-78-44-05-69-82-19
29-77-88-44-31-29-36
34-73-69-69-53-59-25
71-66-51-35-53-29-95
16-95-52-71-19-23-20
38-16-67-97-47-29-82
87-08-91-20-38-46-32
58-74-83-45-82-59-19
48-41-67-61-01-96-92
76-95-03-63-10-18-39
29-32-93-82-25-29-56
39-32-31-37-91-78-45
00-84-92-88-61-09-66
02-61-52-90-79-96-34
52-97-20-79-38-86-51
76-48-21-82-43-43-80
73-21-43-93-39-36-74
16-87-26-27-94-22-46
64-74-00-76-70-33-26
67-41-92-18-56-05-09
13-55-02-86-61-16-95
68-67-72-43-39-48-71
02-20-42-68-50-30-24
81-59-13-84-17-42-96
93-94-95-35-23-68-02
46-88-55-91-39-85-98
34-41-63-45-30-75-63
73-43-03-86-25-51-40
30-76-97-41-02-58-36
27-37-86-88-71-97-99
07-44-36-19-40-72-04
91-55-25-24-73-65-16
74-54-91-40-64-42-94
36-30-21-26-23-48-68
79-83-86-59-11-18-74
25-99-97-49-02-63-90
56-13-47-96-62-62-16
28-52-83-51-16-13-03
14-80-79-79-62-70-67
54-63-36-53-55-69-20
47-84-33-35-58-35-36
68-35-65-98-15-89-52
01-38-28-66-99-84-39
55-97-59-20-47-69-18
99-88-32-71-12-42-94
33-06-14-42-79-98-95
31-19-17-66-90-50-92
77-00-02-95-76-47-68
88-75-41-20-73-22-22
23-18-39-53-89-39-91

101
deploy.sh
View File

@@ -1,101 +0,0 @@
#!/bin/bash
# Скрипт быстрого развертывания бота в продакшн через Docker
set -e # Остановка при ошибке
echo "🚀 Быстрое развертывание lottery bot в продакшн"
echo "================================================"
echo ""
# Проверка наличия Docker
if ! command -v docker &> /dev/null; then
echo "❌ Docker не установлен!"
echo "Установите Docker: https://docs.docker.com/get-docker/"
exit 1
fi
# Проверка наличия Docker Compose
if ! command -v docker-compose &> /dev/null; then
echo "❌ Docker Compose не установлен!"
echo "Установите Docker Compose: https://docs.docker.com/compose/install/"
exit 1
fi
echo "✅ Docker и Docker Compose установлены"
echo ""
# Проверка .env.prod
if [ ! -f .env.prod ]; then
echo "⚠️ Файл .env.prod не найден"
if [ -f .env.prod.example ]; then
echo "📄 Создаю .env.prod из примера..."
cp .env.prod.example .env.prod
echo ""
echo "⚠️ ВНИМАНИЕ!"
echo "Отредактируйте файл .env.prod и укажите:"
echo " - BOT_TOKEN (токен от @BotFather)"
echo " - POSTGRES_PASSWORD (надежный пароль для БД)"
echo " - DATABASE_URL (обновите пароль в строке подключения)"
echo " - ADMIN_IDS (ваш Telegram ID)"
echo ""
read -p "Нажмите Enter после редактирования .env.prod..."
else
echo "❌ Файл .env.prod.example не найден!"
exit 1
fi
fi
echo "✅ Конфигурация найдена"
echo ""
# Создание необходимых директорий
echo "📁 Создание директорий..."
mkdir -p logs backups data
echo "✅ Директории созданы"
echo ""
# Сборка образа
echo "🔨 Сборка Docker образа..."
docker-compose build --no-cache
echo "✅ Образ собран"
echo ""
# Запуск контейнеров
echo "🚀 Запуск контейнеров..."
docker-compose --env-file .env.prod up -d
echo "✅ Контейнеры запущены"
echo ""
# Ожидание запуска БД
echo "⏳ Ожидание запуска базы данных..."
sleep 10
# Применение миграций
echo "⬆️ Применение миграций..."
docker-compose exec -T bot alembic upgrade head || {
echo "⚠️ Миграции не применены (возможно БД уже актуальна)"
}
echo ""
# Статус
echo "📊 Статус контейнеров:"
docker-compose ps
echo ""
# Проверка логов
echo "📋 Последние логи бота:"
docker-compose logs --tail=20 bot
echo ""
echo "✅ Развертывание завершено!"
echo ""
echo "📝 Полезные команды:"
echo " make docker-logs - Просмотр логов"
echo " make docker-status - Статус контейнеров"
echo " make docker-restart - Перезапуск"
echo " make docker-down - Остановка"
echo " make docker-db-backup - Бэкап БД"
echo ""
echo "🎉 Бот запущен и готов к работе!"

View File

@@ -21,9 +21,6 @@ services:
- bot_data:/app/data - bot_data:/app/data
networks: networks:
- lottery_network - lottery_network
depends_on:
db:
condition: service_healthy
healthcheck: healthcheck:
test: ["CMD", "python", "-c", "import sys; sys.exit(0)"] test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
interval: 30s interval: 30s
@@ -31,32 +28,7 @@ services:
retries: 3 retries: 3
start_period: 10s start_period: 10s
# PostgreSQL Database
db:
image: postgres:15-alpine
container_name: lottery_db
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-lottery_bot_db}
POSTGRES_USER: ${POSTGRES_USER:-lottery_user}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
PGDATA: /var/lib/postgresql/data/pgdata
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- lottery_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-lottery_user} -d ${POSTGRES_DB:-lottery_bot_db}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
volumes: volumes:
postgres_data:
driver: local
bot_data: bot_data:
driver: local driver: local

118
docs/DEPLOY_QUICKSTART.md Normal file
View File

@@ -0,0 +1,118 @@
# 🚀 Быстрый деплой бота с внешним PostgreSQL
## Шаг 0: Установка Docker (если не установлен)
```bash
# Проверка Docker
docker --version
docker compose version
# Если не установлен - см. DOCKER_INSTALL.md
# Или быстрая установка (Ubuntu/Debian):
sudo apt update
sudo apt install -y docker.io docker-compose-plugin
sudo systemctl enable docker
sudo systemctl start docker
# Проверка
make docker-check
```
## Шаг 1: Подготовка PostgreSQL
```bash
# Подключитесь к PostgreSQL
psql -U postgres
# Создайте пользователя и БД
CREATE USER bot_user WITH PASSWORD 'secure_password_here';
CREATE DATABASE lottery_bot OWNER bot_user;
GRANT ALL PRIVILEGES ON DATABASE lottery_bot TO bot_user;
# Выход
\q
```
## Шаг 2: Настройка .env.prod
```bash
# Скопируйте пример
cp .env.prod.example .env.prod
# Отредактируйте .env.prod
nano .env.prod
```
**Заполните:**
```env
# Telegram
BOT_TOKEN=your_bot_token_from_botfather
ADMIN_IDS=123456789,987654321
# PostgreSQL (замените на свои данные)
DATABASE_URL=postgresql+asyncpg://bot_user:secure_password@localhost:5432/lottery_bot
```
## Шаг 3: Деплой
### Вариант A: Docker (рекомендуется)
```bash
# Билд и запуск
make docker-deploy
# Или вручную:
docker-compose build
docker-compose up -d
docker exec -it lottery_bot alembic upgrade head
```
### Вариант B: Локально
```bash
# Установка
make install
# Миграции
source .venv/bin/activate
alembic upgrade head
# Запуск
make bot-start
```
## Шаг 4: Проверка
```bash
# Проверить подключение к БД
make docker-test-db
# Логи
make docker-logs
# Статус
make docker-status
```
## 📋 Полезные команды
```bash
# Остановка
docker-compose down
# Перезапуск
docker-compose restart
# Логи в реальном времени
docker-compose logs -f bot
# Бэкап БД
pg_dump -U bot_user lottery_bot > backup.sql
# Восстановление БД
psql -U bot_user lottery_bot < backup.sql
```
## 🔥 Проблемы?
См. [EXTERNAL_DB_SETUP.md](./EXTERNAL_DB_SETUP.md) раздел "Troubleshooting"

170
docs/DOCKER_INSTALL.md Normal file
View File

@@ -0,0 +1,170 @@
# Установка Docker и Docker Compose
## Для Ubuntu/Debian
### Установка Docker
```bash
# Обновление системы
sudo apt update
sudo apt upgrade -y
# Установка зависимостей
sudo apt install -y ca-certificates curl gnupg lsb-release
# Добавление GPG ключа Docker
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Добавление репозитория Docker
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Установка Docker Engine
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Проверка установки
docker --version
docker compose version
```
### Настройка прав (опционально)
```bash
# Добавить пользователя в группу docker (чтобы не использовать sudo)
sudo usermod -aG docker $USER
# Применить изменения (нужно перелогиниться или выполнить)
newgrp docker
# Проверка
docker ps
```
### Автозапуск Docker
```bash
sudo systemctl enable docker
sudo systemctl start docker
```
## Для других систем
### CentOS/RHEL/Fedora
```bash
# Установка Docker
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Запуск
sudo systemctl start docker
sudo systemctl enable docker
```
### Debian
```bash
# Для Debian используйте те же команды что и для Ubuntu
# Но в добавлении репозитория используйте:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
```
## Проверка установки
```bash
# Версия Docker
docker --version
# Должно вывести: Docker version 24.0.x или новее
# Версия Docker Compose
docker compose version
# Должно вывести: Docker Compose version v2.x.x или новее
# Тест Docker
docker run hello-world
```
## Если Docker Compose v1 (старая версия)
Если у вас установлен `docker-compose` (v1) вместо `docker compose` (v2):
```bash
# Удалите старую версию
sudo apt remove docker-compose
# Установите плагин compose
sudo apt install docker-compose-plugin
# Проверка
docker compose version
```
## Troubleshooting
### Ошибка: "Cannot connect to the Docker daemon"
```bash
# Запустите Docker
sudo systemctl start docker
# Проверьте статус
sudo systemctl status docker
```
### Ошибка: "permission denied"
```bash
# Добавьте пользователя в группу docker
sudo usermod -aG docker $USER
# Перелогиньтесь или выполните
newgrp docker
```
### Ошибка: "docker-compose: command not found" но Docker Compose установлен
Makefile автоматически определит правильную команду:
- `docker compose` (v2, рекомендуется)
- `docker-compose` (v1, устаревшая)
## Полезные команды
```bash
# Информация о Docker
docker info
# Список запущенных контейнеров
docker ps
# Список всех контейнеров
docker ps -a
# Список образов
docker images
# Очистка неиспользуемых ресурсов
docker system prune -a
# Логи контейнера
docker logs container_name
# Остановить все контейнеры
docker stop $(docker ps -aq)
# Удалить все контейнеры
docker rm $(docker ps -aq)
```
## Обновление Docker
```bash
sudo apt update
sudo apt upgrade docker-ce docker-ce-cli containerd.io docker-compose-plugin
```

162
docs/EXTERNAL_DB_SETUP.md Normal file
View File

@@ -0,0 +1,162 @@
# Настройка внешнего PostgreSQL
Этот гайд описывает как настроить бота для работы с внешним PostgreSQL сервером.
## Предварительные требования
1. Запущенный PostgreSQL сервер (версия 13+)
2. Доступ к серверу по сети (если сервер на другой машине)
3. Созданная база данных для бота
## Шаг 1: Подготовка PostgreSQL
### Создание базы данных и пользователя
```sql
-- Подключитесь к PostgreSQL
psql -U postgres
-- Создайте пользователя
CREATE USER bot_user WITH PASSWORD 'your_secure_password';
-- Создайте базу данных
CREATE DATABASE lottery_bot OWNER bot_user;
-- Выдайте права
GRANT ALL PRIVILEGES ON DATABASE lottery_bot TO bot_user;
```
### Настройка доступа (если PostgreSQL на другом сервере)
Отредактируйте `postgresql.conf`:
```conf
listen_addresses = '*' # или конкретный IP
```
Отредактируйте `pg_hba.conf`:
```conf
# Разрешить подключение с определенного IP
host lottery_bot bot_user 192.168.1.0/24 md5
```
Перезапустите PostgreSQL:
```bash
sudo systemctl restart postgresql
```
## Шаг 2: Настройка .env.prod
Отредактируйте `.env.prod`:
```env
# PostgreSQL настройки
POSTGRES_HOST=your_db_server_ip_or_domain
POSTGRES_PORT=5432
POSTGRES_DB=lottery_bot
POSTGRES_USER=bot_user
POSTGRES_PASSWORD=your_secure_password
# Database URL
DATABASE_URL=postgresql+asyncpg://bot_user:your_secure_password@your_db_server_ip_or_domain:5432/lottery_bot
```
### Примеры DATABASE_URL
**Локальная БД:**
```
DATABASE_URL=postgresql+asyncpg://bot_user:password@localhost:5432/lottery_bot
```
**Удаленная БД:**
```
DATABASE_URL=postgresql+asyncpg://bot_user:password@192.168.1.100:5432/lottery_bot
```
**С доменом:**
```
DATABASE_URL=postgresql+asyncpg://bot_user:password@db.example.com:5432/lottery_bot
```
**Через Docker network (если БД в другом контейнере):**
```
DATABASE_URL=postgresql+asyncpg://bot_user:password@postgres_container:5432/lottery_bot
```
## Шаг 3: Применение миграций
После настройки подключения примените миграции:
```bash
# Активируйте виртуальное окружение
source .venv/bin/activate
# Примените миграции
alembic upgrade head
```
## Шаг 4: Запуск бота
### С Docker Compose:
```bash
docker-compose up -d
```
### Локально:
```bash
make bot-start
```
## Проверка подключения
Проверьте подключение к БД:
```bash
# Из контейнера
docker exec -it lottery_bot python -c "from src.core.database import engine; import asyncio; asyncio.run(engine.dispose())"
# Локально
python -c "from src.core.database import engine; import asyncio; asyncio.run(engine.dispose())"
```
## Troubleshooting
### Ошибка: "FATAL: password authentication failed"
- Проверьте правильность пароля в DATABASE_URL
- Убедитесь что пользователь создан в PostgreSQL
- Проверьте настройки pg_hba.conf
### Ошибка: "could not connect to server"
- Проверьте что PostgreSQL запущен
- Убедитесь что порт 5432 открыт (firewall)
- Проверьте listen_addresses в postgresql.conf
### Ошибка: "database does not exist"
- Создайте базу данных (см. Шаг 1)
- Проверьте имя БД в DATABASE_URL
### Ошибка: "SSL connection has been closed unexpectedly"
- Добавьте `?ssl=require` или `?ssl=prefer` в конец DATABASE_URL
- Или отключите SSL: `?ssl=false`
## Рекомендации по безопасности
1. **Используйте сильные пароли** для пользователя БД
2. **Ограничьте доступ** только с нужных IP (pg_hba.conf)
3. **Используйте SSL** для подключения к удаленной БД
4. **Регулярно делайте бэкапы**:
```bash
pg_dump -U bot_user lottery_bot > backup_$(date +%Y%m%d).sql
```
5. **Не коммитьте .env.prod** в git (добавлен в .gitignore)
## Мониторинг
Проверка состояния подключений:
```sql
SELECT * FROM pg_stat_activity WHERE datname = 'lottery_bot';
```
Размер базы данных:
```sql
SELECT pg_size_pretty(pg_database_size('lottery_bot'));
```

View File

@@ -1,118 +0,0 @@
#!/usr/bin/env python3
"""
Исправление схемы базы данных
Добавление недостающих полей в таблицу users
"""
import asyncio
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
from src.core.database import engine
from sqlalchemy import text
async def fix_database_schema():
"""Добавление недостающих полей в базу данных"""
print("🔧 Исправляем схему базы данных...")
async with engine.begin() as conn:
# Проверяем, есть ли поле phone
result = await conn.execute(text(
"SELECT column_name FROM information_schema.columns "
"WHERE table_name = 'users' AND column_name = 'phone'"
))
if not result.fetchone():
print("📞 Добавляем поле 'phone'...")
await conn.execute(text(
"ALTER TABLE users ADD COLUMN phone VARCHAR(20) NULL"
))
print("✅ Поле 'phone' добавлено")
else:
print("✅ Поле 'phone' уже существует")
# Проверяем, есть ли поле club_card_number
result = await conn.execute(text(
"SELECT column_name FROM information_schema.columns "
"WHERE table_name = 'users' AND column_name = 'club_card_number'"
))
if not result.fetchone():
print("💳 Добавляем поле 'club_card_number'...")
await conn.execute(text(
"ALTER TABLE users ADD COLUMN club_card_number VARCHAR(50) NULL"
))
await conn.execute(text(
"CREATE UNIQUE INDEX ix_users_club_card_number ON users (club_card_number)"
))
print("✅ Поле 'club_card_number' добавлено")
else:
print("✅ Поле 'club_card_number' уже существует")
# Проверяем, есть ли поле is_registered
result = await conn.execute(text(
"SELECT column_name FROM information_schema.columns "
"WHERE table_name = 'users' AND column_name = 'is_registered'"
))
if not result.fetchone():
print("📝 Добавляем поле 'is_registered'...")
await conn.execute(text(
"ALTER TABLE users ADD COLUMN is_registered BOOLEAN DEFAULT FALSE NOT NULL"
))
print("✅ Поле 'is_registered' добавлено")
else:
print("✅ Поле 'is_registered' уже существует")
# Проверяем, есть ли поле verification_code
result = await conn.execute(text(
"SELECT column_name FROM information_schema.columns "
"WHERE table_name = 'users' AND column_name = 'verification_code'"
))
if not result.fetchone():
print("🔐 Добавляем поле 'verification_code'...")
await conn.execute(text(
"ALTER TABLE users ADD COLUMN verification_code VARCHAR(10) NULL"
))
await conn.execute(text(
"CREATE UNIQUE INDEX ix_users_verification_code ON users (verification_code)"
))
print("✅ Поле 'verification_code' добавлено")
else:
print("✅ Поле 'verification_code' уже существует")
# Удаляем поле account_number, если оно есть (оно перенесено в отдельную таблицу)
result = await conn.execute(text(
"SELECT column_name FROM information_schema.columns "
"WHERE table_name = 'users' AND column_name = 'account_number'"
))
if result.fetchone():
print("🗑️ Удаляем устаревшее поле 'account_number'...")
# Сначала удаляем индекс
try:
await conn.execute(text("DROP INDEX IF EXISTS ix_users_account_number"))
except:
pass
await conn.execute(text(
"ALTER TABLE users DROP COLUMN account_number"
))
print("✅ Поле 'account_number' удалено")
else:
print("✅ Поле 'account_number' уже удалено")
async def main():
"""Основная функция"""
try:
await fix_database_schema()
print("\n🎉 Схема базы данных успешно исправлена!")
except Exception as e:
print(f"❌ Ошибка при исправлении базы данных: {e}")
finally:
await engine.dispose()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,76 +0,0 @@
#!/usr/bin/env python3
"""
Генератор тестовых счетов для проверки производительности розыгрыша
"""
import random
def generate_account_number():
"""Генерирует случайный номер счета в формате XX-XX-XX-XX-XX-XX-XX"""
parts = []
for _ in range(7):
part = f"{random.randint(0, 99):02d}"
parts.append(part)
return "-".join(parts)
def generate_accounts(count, card_numbers=None):
"""
Генерирует список уникальных счетов
Args:
count: Количество счетов для генерации
card_numbers: Список номеров карт (опционально)
Returns:
List[str]: Список счетов
"""
accounts = set()
while len(accounts) < count:
account = generate_account_number()
# Добавляем с картой или без
if card_numbers and random.random() > 0.3: # 70% с картой
card = random.choice(card_numbers)
full_account = f"{card} {account}"
else:
full_account = account
accounts.add(full_account)
return list(accounts)
def save_to_file(accounts, filename):
"""Сохраняет счета в файл"""
with open(filename, 'w', encoding='utf-8') as f:
for account in accounts:
f.write(account + '\n')
print(f"✅ Сохранено {len(accounts)} счетов в файл {filename}")
def main():
"""Главная функция"""
print("🎲 Генератор тестовых счетов для розыгрыша\n")
# Параметры
counts = [100, 500, 1000, 2000, 5000]
card_numbers = ['2521', '2522', '2523', '2524', '2525']
for count in counts:
print(f"Генерация {count} счетов...")
accounts = generate_accounts(count, card_numbers)
filename = f"test_accounts_{count}.txt"
save_to_file(accounts, filename)
print("\n✅ Генерация завершена!")
print("\nИспользование:")
print("1. Скопируйте содержимое нужного файла")
print("2. В боте: Управление розыгрышами → Выберите розыгрыш → Участники → Добавить массово")
print("3. Вставьте содержимое файла")
print("4. Проведите розыгрыш и проверьте время выполнения")
if __name__ == "__main__":
main()

10
main.py
View File

@@ -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():
"""Контекстный менеджер для получения контроллера с БД сессией""" """Контекстный менеджер для получения контроллера с БД сессией"""

File diff suppressed because it is too large Load Diff

View File

@@ -1,97 +0,0 @@
#!/usr/bin/env python3
"""
Минимальная рабочая версия main.py для лотерейного бота
"""
from aiogram import Bot, Dispatcher
from aiogram.types import BotCommand
from aiogram.fsm.storage.memory import MemoryStorage
import asyncio
import logging
import signal
import sys
from src.core.config import BOT_TOKEN, ADMIN_IDS
from src.core.database import async_session_maker, init_db
# Настройка логирования
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Инициализация бота
bot = Bot(token=BOT_TOKEN)
storage = MemoryStorage()
dp = Dispatcher(storage=storage)
async def set_commands():
"""Установка команд бота"""
commands = [
BotCommand(command="start", description="🚀 Запустить бота"),
BotCommand(command="help", description="❓ Помощь"),
]
await bot.set_my_commands(commands)
async def main():
"""Главная функция"""
try:
logger.info("🔄 Инициализация базы данных...")
await init_db()
logger.info("🔄 Установка команд...")
await set_commands()
# Импортируем и подключаем роутеры
logger.info("🔄 Подключение роутеров...")
try:
from src.handlers.registration_handlers import router as registration_router
dp.include_router(registration_router)
logger.info("✅ Registration router подключен")
except Exception as e:
logger.error(f"❌ Ошибка подключения registration router: {e}")
try:
from src.handlers.admin_panel import admin_router
dp.include_router(admin_router)
logger.info("✅ Admin router подключен")
except Exception as e:
logger.error(f"❌ Ошибка подключения admin router: {e}")
try:
from src.handlers.account_handlers import account_router
dp.include_router(account_router)
logger.info("✅ Account router подключен")
except Exception as e:
logger.error(f"❌ Ошибка подключения account router: {e}")
# Обработка сигналов для graceful shutdown
def signal_handler():
logger.info("Получен сигнал завершения, остановка бота...")
# Настройка обработчиков сигналов
if sys.platform != "win32":
for sig in (signal.SIGTERM, signal.SIGINT):
asyncio.get_event_loop().add_signal_handler(sig, signal_handler)
# Получаем информацию о боте
bot_info = await bot.get_me()
logger.info(f"🚀 Бот запущен: @{bot_info.username} ({bot_info.first_name})")
# Запуск бота
await dp.start_polling(bot)
except Exception as e:
logger.error(f"Критическая ошибка: {e}")
import traceback
traceback.print_exc()
finally:
logger.info("Завершение работы")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
logger.info("Бот остановлен пользователем")
except Exception as e:
logger.error(f"Критическая ошибка: {e}")
finally:
logger.info("Завершение работы")

View File

@@ -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")]

View File

@@ -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"
)

View File

@@ -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:
"""Сервис проверки прав на отправку сообщений""" """Сервис проверки прав на отправку сообщений"""

View File

@@ -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

View File

@@ -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 # Игнорируем если не получилось отправить

View File

@@ -6,6 +6,7 @@ from aiogram import Router, F
from aiogram.types import ( from aiogram.types import (
CallbackQuery, Message, InlineKeyboardButton, InlineKeyboardMarkup CallbackQuery, Message, InlineKeyboardButton, InlineKeyboardMarkup
) )
from aiogram.exceptions import TelegramBadRequest
from aiogram.filters import StateFilter from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.state import State, StatesGroup
@@ -15,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):
# Создание розыгрыша # Создание розыгрыша
@@ -177,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=[
@@ -192,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"
@@ -214,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)
@@ -241,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()
@@ -1678,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):
"""Редирект на редактирование розыгрыша из детального просмотра""" """Редирект на редактирование розыгрыша из детального просмотра"""
@@ -1736,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)
@@ -1758,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")
@@ -2592,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):
@@ -2622,39 +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}")]
] ]
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons)) await safe_edit_message(callback, text, InlineKeyboardMarkup(inline_keyboard=buttons))
@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)
@@ -2674,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:
@@ -3218,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']

View File

@@ -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("✅ Голосовое сообщение переслано в канал")

View File

@@ -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)

View File

@@ -1,21 +0,0 @@
2521 11-22-33-44-55-66-77
2521 12-23-34-45-56-67-78
2521 13-24-35-46-57-68-79
2521 14-25-36-47-58-69-80
2521 15-26-37-48-59-70-81
2521 16-27-38-49-60-71-82
2521 17-28-39-50-61-72-83
2521 18-29-40-51-62-73-84
2521 19-30-41-52-63-74-85
2521 20-31-42-53-64-75-86
2522 21-32-43-54-65-76-87
2522 22-33-44-55-66-77-88
2522 23-34-45-56-67-78-89
2522 24-35-46-57-68-79-90
2522 25-36-47-58-69-80-91
2522 26-37-48-59-70-81-92
2522 27-38-49-60-71-82-93
2522 28-39-50-61-72-83-94
2522 29-40-51-62-73-84-95
2522 30-41-52-63-74-85-96

View File

@@ -1,100 +0,0 @@
2524 13-44-65-38-31-54-67
2523 31-91-70-64-88-67-03
2525 21-87-28-91-13-49-61
2523 35-22-65-25-15-99-32
2525 12-72-37-11-82-58-23
2525 96-39-53-66-81-43-28
2522 31-19-65-97-82-87-06
2521 54-03-08-21-52-27-86
2525 42-85-32-06-39-68-81
2522 94-50-44-81-24-67-25
28-66-94-77-24-23-40
72-64-73-89-62-11-90
2522 12-25-21-03-46-98-22
2524 54-06-23-93-94-44-50
2523 23-61-39-40-29-15-28
2525 13-85-23-66-37-16-95
2525 97-28-72-80-14-30-78
2525 11-69-37-13-79-35-12
89-44-47-63-67-54-12
2525 07-09-98-78-15-23-50
2523 05-03-90-01-62-57-18
65-07-18-74-28-42-66
2525 39-77-17-98-01-23-29
2522 05-50-21-93-79-11-61
2525 61-18-20-81-60-90-05
2521 15-92-74-93-64-78-54
2523 22-21-96-99-90-45-27
2521 30-97-48-67-95-75-79
2524 39-57-99-03-13-46-35
2522 98-54-80-56-33-65-44
20-91-91-30-15-65-25
98-04-80-73-50-11-42
98-34-41-64-88-01-63
2525 29-35-02-04-32-78-51
2523 62-44-20-56-62-78-01
2524 14-36-17-91-34-91-55
2524 17-01-76-83-62-31-93
04-44-22-26-04-55-87
2523 11-43-07-89-40-00-88
2521 84-28-72-28-33-60-44
2525 95-40-78-88-00-43-13
2522 69-21-29-41-81-96-77
2524 37-22-41-64-08-13-92
2524 73-96-94-27-64-09-09
33-27-89-47-46-62-85
2523 75-75-48-01-28-10-88
72-57-79-14-18-91-23
98-32-02-86-87-59-11
97-19-28-45-03-08-64
2523 74-22-18-22-46-58-94
2525 18-13-73-83-02-10-09
2523 41-15-99-26-09-14-97
2525 43-58-60-55-40-73-67
2523 42-97-48-61-70-60-38
80-70-44-15-17-55-49
2522 76-81-33-86-19-53-45
2525 45-94-04-45-89-90-28
2522 20-97-12-37-10-83-76
2524 34-32-51-50-78-80-97
2522 30-97-39-84-02-45-49
83-67-91-16-68-14-66
94-71-04-28-57-75-45
2524 83-82-42-15-67-91-48
2523 97-98-88-10-36-79-53
41-22-09-70-75-40-57
2522 77-94-56-22-88-02-16
2525 43-11-72-35-15-47-04
2525 35-57-25-41-26-07-37
57-06-88-62-15-34-66
2525 98-66-63-02-15-71-13
58-20-77-41-06-52-33
2521 11-98-92-27-38-94-75
2525 09-48-71-70-71-41-26
2525 79-05-30-49-24-22-33
26-70-94-22-64-89-48
2524 34-71-40-14-68-80-57
18-87-93-44-52-37-69
2524 09-39-78-85-80-17-81
2521 32-08-76-43-59-61-14
2523 93-56-87-85-14-53-72
2521 78-51-66-89-56-33-49
2522 20-24-45-32-47-44-53
41-37-43-28-56-43-54
2525 95-88-82-26-44-81-83
95-26-50-93-40-82-27
2521 32-43-09-99-96-51-73
2522 62-54-92-00-89-19-66
2525 28-53-29-95-71-21-66
2523 68-33-54-40-40-99-32
2523 60-51-93-71-70-19-35
2524 01-72-11-22-48-64-15
80-56-98-36-74-46-98
2524 08-02-36-94-18-37-27
2524 33-98-00-04-99-88-91
2523 90-77-79-06-91-29-07
2521 63-16-29-62-15-87-98
2522 61-37-16-90-50-14-83
2521 52-13-01-97-57-81-05
29-11-89-59-59-44-05
96-42-02-79-02-80-82

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,500 +0,0 @@
2524 88-62-46-84-72-08-35
2522 10-22-27-22-58-78-17
51-13-02-75-49-33-24
70-89-01-27-80-15-07
34-92-77-76-25-70-93
38-32-72-86-17-33-56
87-60-70-50-25-91-84
2523 21-14-04-05-19-46-25
2524 89-84-04-85-69-48-11
2524 50-35-99-27-26-02-20
2523 28-62-92-35-74-98-25
2522 93-14-72-96-97-42-96
2525 25-30-32-74-67-29-85
2521 36-86-88-64-61-88-89
2522 44-74-59-58-15-14-89
01-30-55-20-38-31-72
71-18-33-96-66-96-26
2524 94-32-58-56-35-13-97
2523 28-87-80-20-45-21-05
2524 72-50-32-62-44-95-03
2522 25-60-16-18-19-11-70
98-79-01-28-64-95-66
96-26-10-27-17-87-71
23-99-31-56-74-73-76
88-78-77-67-55-73-96
95-27-40-11-78-13-64
81-54-62-27-54-62-69
31-94-80-51-25-36-79
2524 44-10-96-63-84-30-07
2522 53-36-32-70-62-28-43
2523 60-82-65-57-94-68-25
2523 90-62-99-58-03-02-57
84-93-24-28-61-92-83
38-97-88-51-57-47-91
2522 97-18-71-19-17-46-11
74-25-19-72-73-69-05
2523 96-41-78-01-63-40-13
2525 93-75-84-73-30-84-68
2523 29-78-54-03-00-21-31
2524 74-45-78-17-55-77-54
2525 42-11-31-48-56-32-88
2525 69-47-22-59-62-43-20
2523 01-22-13-57-05-25-44
2525 59-22-43-08-53-48-82
2525 32-15-12-73-96-25-50
2525 90-04-74-22-33-88-10
2522 30-32-71-15-43-34-55
30-92-54-05-94-53-54
2525 29-58-08-99-46-04-29
46-27-64-43-09-37-58
2525 77-95-40-98-58-08-54
2525 02-66-43-02-60-18-34
16-37-17-50-65-63-51
28-00-31-28-74-01-13
2521 18-65-37-13-86-46-08
2524 88-84-69-86-18-46-49
25-23-65-85-03-80-42
2523 10-64-29-31-20-89-52
2524 65-21-51-30-91-21-68
33-24-81-00-31-10-06
2522 66-21-20-66-66-77-70
64-36-82-81-22-07-90
2524 59-29-33-33-51-95-17
2523 00-93-53-78-54-23-22
2522 73-77-13-34-10-90-73
2521 80-60-56-32-06-52-22
61-17-66-25-81-17-53
2524 60-47-94-82-73-16-91
2524 42-23-08-47-92-68-73
2523 96-42-17-80-54-21-92
43-41-24-82-73-89-70
58-59-94-04-58-25-95
65-09-40-69-61-49-66
2524 50-80-86-64-00-07-03
2525 49-88-90-85-64-35-76
2524 45-24-80-26-42-84-59
2524 95-24-66-37-33-61-07
2523 49-58-55-29-51-10-61
2525 39-03-45-88-41-32-53
2523 73-96-56-70-51-13-71
65-18-22-20-11-92-26
2525 80-30-71-96-23-95-74
2521 68-19-86-32-40-86-59
2522 07-03-45-99-77-61-66
2522 53-26-95-59-95-36-13
2525 41-02-61-74-69-53-72
2521 40-42-28-13-59-79-73
2522 03-31-84-02-95-87-67
04-54-85-07-18-08-63
2522 51-18-39-20-56-42-88
2525 90-88-19-93-08-36-74
2522 23-14-28-13-65-76-55
2523 24-89-75-22-08-30-07
15-26-21-64-07-12-45
2521 79-89-45-51-27-87-84
2525 01-11-24-63-37-93-77
2524 81-41-39-29-85-72-75
2525 64-96-76-67-37-51-52
21-31-25-17-61-80-92
2524 72-32-21-73-93-88-48
84-27-78-23-47-96-13
2523 52-86-55-42-99-36-96
2524 10-33-99-48-82-51-25
95-69-56-50-65-47-42
2525 99-89-69-98-27-91-33
06-20-51-97-71-00-53
2522 22-05-43-81-46-67-40
2521 37-08-49-25-33-08-77
2524 63-03-27-24-77-20-41
65-59-99-21-28-67-74
51-89-42-53-15-48-48
41-60-33-82-91-19-40
2522 47-26-52-13-21-61-61
32-81-00-16-63-90-66
2524 18-12-12-11-89-20-60
2522 29-93-53-71-59-57-17
2522 17-61-02-56-63-48-90
2522 87-56-66-57-13-34-32
27-43-61-72-26-68-94
2525 15-74-04-57-85-46-89
2525 58-35-93-12-58-24-84
41-09-96-02-81-97-85
04-92-76-03-21-36-38
36-82-09-76-50-91-40
2521 31-48-77-83-23-85-58
91-08-41-12-22-67-92
2525 91-01-95-06-20-56-66
2523 92-09-07-53-90-73-56
2523 24-88-11-05-06-18-63
2525 14-89-03-92-45-65-53
2523 73-98-00-08-94-74-60
11-25-05-77-54-25-38
2525 24-14-14-61-13-96-41
28-33-55-89-06-90-31
2523 92-90-32-07-42-96-04
2525 79-80-48-56-75-29-12
2521 77-97-88-83-04-44-09
2523 82-96-37-98-15-52-75
2522 64-34-21-10-96-85-39
2524 31-52-64-02-96-39-16
03-50-03-64-37-62-21
2521 49-63-37-97-53-63-00
2525 94-49-52-77-74-48-81
55-40-74-74-81-86-50
2524 06-70-54-03-82-67-17
75-19-75-29-43-35-82
2521 42-96-95-66-89-84-01
2521 55-33-17-44-67-26-89
2524 56-64-65-06-52-00-85
2522 93-66-95-15-90-23-90
2523 31-25-99-15-61-01-30
2525 54-54-54-47-69-06-33
2525 17-40-02-42-79-86-21
2522 21-12-01-11-51-55-14
2521 46-20-64-13-21-06-15
2523 92-85-71-89-97-70-84
2523 22-84-47-04-78-47-01
62-49-03-81-98-15-91
2524 79-54-71-16-36-91-63
2522 02-11-79-98-69-92-57
2525 32-76-56-57-96-23-90
2523 06-87-57-07-02-01-85
2521 18-35-94-83-28-73-15
2523 97-04-86-66-40-64-86
2521 55-97-94-59-99-20-57
2525 18-46-50-17-69-33-41
2522 09-48-99-58-34-13-61
2523 28-82-53-71-21-05-09
2523 08-12-90-23-74-10-27
2525 32-08-45-22-72-72-76
60-67-63-50-96-10-27
2525 75-03-19-97-62-80-88
2522 97-86-67-50-27-37-08
49-08-22-06-86-17-86
2524 09-80-21-70-82-91-48
96-06-92-25-94-08-57
2525 21-35-94-03-85-72-61
2521 39-93-53-66-86-81-96
2524 06-18-23-18-88-94-09
2521 52-96-14-51-04-51-36
2522 10-62-26-66-78-03-94
2525 58-22-74-01-66-37-97
2524 22-82-49-98-55-97-36
2523 04-16-77-51-80-89-13
70-51-03-12-10-26-56
2521 80-93-55-85-90-06-27
2525 18-63-31-58-45-52-61
17-10-85-46-30-32-82
73-84-60-73-28-53-48
2521 13-98-24-82-40-06-10
2521 58-59-74-00-18-34-85
2524 92-02-64-75-83-14-50
10-26-44-71-18-12-71
2523 25-09-58-53-10-53-54
2521 34-51-86-52-12-41-76
2522 71-42-30-72-71-45-59
2524 00-71-32-40-12-45-68
2524 74-50-48-06-05-52-06
48-88-23-94-23-40-74
2525 91-22-15-04-72-70-70
2521 76-78-90-23-44-92-83
2525 57-39-63-94-24-69-04
14-88-43-54-27-70-11
2522 18-25-25-91-36-53-23
2524 36-15-88-30-21-64-83
2525 66-11-70-60-37-02-63
43-11-84-99-73-28-48
01-03-64-24-84-70-15
2524 48-76-97-28-23-64-71
2524 77-08-08-23-73-96-22
2521 64-02-43-87-85-72-84
2525 85-46-13-04-03-63-60
2524 56-96-76-02-20-13-95
31-54-15-57-42-74-53
89-00-93-32-62-12-11
45-76-98-25-74-09-04
2521 64-30-44-10-39-95-33
44-71-95-86-12-54-08
63-13-57-14-13-48-16
41-87-71-95-17-22-88
2521 55-23-84-04-27-20-38
2523 80-64-38-39-76-43-04
2523 81-83-82-90-45-95-65
2523 57-84-88-16-25-30-98
2525 78-21-73-66-17-08-23
13-96-69-65-56-65-03
2522 76-37-07-36-14-56-29
2525 25-69-00-04-35-06-73
2525 63-19-14-57-67-48-50
2521 35-43-79-88-05-41-04
2525 24-39-13-22-92-33-38
39-87-05-09-65-00-95
2522 18-68-83-63-94-11-52
59-66-84-42-56-03-62
36-35-03-95-91-45-41
16-11-69-63-84-39-80
04-84-19-52-59-91-38
2523 18-18-33-99-33-21-00
2524 23-70-82-88-62-37-02
2524 84-81-71-58-92-39-45
45-37-02-62-10-07-76
82-02-00-62-68-89-90
2524 86-09-14-71-82-07-96
00-46-39-33-52-92-78
2522 52-39-25-89-07-07-57
2524 84-73-35-01-08-20-67
01-20-59-64-93-70-69
2521 54-32-02-66-48-17-66
2522 27-88-88-20-04-95-37
2522 64-20-24-10-80-29-56
97-57-32-45-22-40-46
96-34-25-40-82-57-74
2522 81-31-85-33-45-63-70
2524 66-71-41-81-31-98-25
49-82-16-11-72-89-45
2521 66-43-39-05-15-18-35
2525 33-11-45-38-33-86-68
2522 98-15-12-20-40-53-38
2523 88-42-37-81-18-01-02
2521 11-65-99-21-43-15-22
53-13-41-07-68-00-08
2524 47-73-46-61-53-08-26
2523 08-19-28-22-45-02-64
2521 44-82-74-93-95-67-71
2523 58-08-17-31-34-08-12
2525 14-35-43-99-32-32-85
16-39-50-48-61-01-68
21-01-79-67-64-02-34
2523 29-90-42-53-74-49-24
43-36-98-42-50-74-58
2521 94-81-74-15-33-82-12
2525 58-11-35-62-67-84-14
51-29-63-65-41-59-61
2521 83-82-27-34-21-39-89
2524 02-33-52-60-73-83-02
98-60-39-67-78-63-16
2523 64-01-33-01-30-29-51
11-75-71-71-03-02-16
2522 26-61-47-07-99-43-61
2525 47-52-94-94-22-86-50
38-06-39-62-20-43-40
2525 35-95-33-15-26-71-68
2525 42-85-13-31-42-01-39
2522 49-75-29-96-44-83-78
77-78-32-83-24-38-75
2523 49-04-42-96-56-31-75
2525 97-48-18-70-00-51-18
44-65-44-13-62-33-58
41-59-53-82-42-97-31
2525 25-11-42-32-67-02-45
71-63-18-02-65-19-04
95-17-37-75-09-90-68
2524 03-54-07-90-12-65-23
80-79-45-70-64-72-68
2523 31-58-15-79-76-04-38
20-15-21-46-53-62-33
2521 36-38-82-78-34-89-65
2524 84-20-61-66-19-69-95
2525 48-16-40-86-41-78-35
2524 03-37-64-84-01-78-94
2524 44-67-25-32-81-53-15
2525 48-52-48-87-90-98-18
30-60-22-87-47-25-15
2525 33-84-89-80-86-70-09
73-93-46-17-69-91-97
2522 84-97-55-42-32-60-92
2525 07-07-64-14-63-51-14
2524 55-03-93-60-14-91-74
2523 32-19-25-22-77-78-15
2521 73-53-49-22-54-23-90
2521 78-87-15-24-92-85-90
2522 34-62-94-56-11-17-51
2522 30-07-45-21-59-94-54
2523 55-92-76-54-95-29-71
76-03-18-42-39-37-30
89-26-94-14-17-99-40
50-10-05-18-34-97-32
2521 04-25-61-71-00-32-50
2523 56-82-78-00-94-99-90
2524 34-99-74-17-91-98-84
75-74-30-25-42-81-71
2524 37-69-87-33-41-40-02
50-19-15-78-99-25-22
18-49-62-94-65-95-87
2523 77-16-41-76-81-66-35
2522 59-70-39-69-97-92-96
2525 81-72-07-51-68-40-23
2525 63-60-68-44-43-62-08
2521 73-20-40-52-98-97-29
2523 38-27-54-83-03-00-26
2522 08-39-39-32-25-45-56
2523 40-34-67-04-37-33-29
2524 11-41-84-92-94-16-33
2521 89-55-98-69-20-03-41
2521 27-09-16-26-04-82-81
2521 38-83-20-21-79-29-81
2525 61-09-59-92-28-67-66
47-19-80-43-43-20-93
2521 87-80-59-51-20-32-74
2524 70-14-85-72-40-80-60
2523 77-57-03-64-45-21-38
2521 88-33-82-62-01-49-55
88-11-93-34-85-87-69
06-02-35-69-77-05-11
2525 84-91-87-54-60-51-46
2525 78-99-73-78-24-94-24
29-50-87-38-87-93-90
2521 84-73-41-32-87-95-52
2521 53-62-20-06-17-74-40
2524 13-47-06-47-93-65-29
2522 38-85-34-37-71-05-30
2523 48-39-49-57-23-78-96
2522 81-22-48-06-91-47-42
15-65-95-20-46-73-48
2521 80-46-01-82-74-75-03
2521 11-40-88-15-16-96-49
2524 43-94-42-84-35-12-17
2524 18-12-45-80-30-07-72
2525 57-99-35-42-43-67-68
63-99-70-67-80-84-31
2521 19-80-66-96-16-61-44
90-66-93-65-04-32-71
52-73-25-85-08-22-10
41-42-86-69-91-89-93
2525 69-06-01-51-03-59-91
2522 25-00-80-31-11-83-55
18-77-42-88-77-67-11
2525 83-90-27-60-78-24-26
2523 94-00-59-37-68-05-50
2521 55-74-61-32-63-51-01
2522 61-90-85-23-11-51-03
2523 94-78-26-87-62-57-55
2524 22-42-80-60-85-42-48
2521 47-06-03-02-78-96-05
2524 78-54-40-11-40-54-75
68-20-77-52-00-10-70
2521 04-82-37-21-22-19-17
2524 62-94-76-61-11-56-75
14-04-11-98-47-23-56
2521 54-41-86-59-91-91-61
14-00-07-96-01-62-04
29-18-98-86-00-88-70
62-78-07-66-28-68-93
23-67-08-74-60-57-55
2521 44-26-69-25-31-41-36
2523 65-82-68-93-69-64-68
25-23-22-44-51-33-19
2521 45-37-36-91-84-70-59
2521 99-23-86-83-01-62-70
85-94-26-28-50-89-75
2521 16-30-23-12-48-81-01
36-43-94-12-58-24-73
2522 22-11-15-28-77-93-46
24-00-68-13-80-33-10
2524 79-10-22-21-74-10-56
2525 50-92-57-27-51-67-57
53-28-93-58-39-45-05
2522 49-13-78-56-46-96-33
2523 65-40-89-45-25-45-78
2523 59-35-54-94-01-68-62
2521 21-26-28-37-80-04-15
31-71-93-03-54-89-84
2524 06-16-02-83-98-00-11
2524 79-24-11-13-14-02-37
2522 08-95-10-92-33-49-44
2521 49-65-96-35-05-04-53
2522 41-32-18-41-45-88-81
2521 53-55-62-25-06-39-43
2521 05-14-32-15-50-24-82
2525 60-47-47-27-56-11-89
2521 44-77-64-51-88-05-75
2523 25-51-51-60-61-81-76
2523 92-38-26-84-23-01-06
28-67-09-28-67-04-31
2525 29-39-37-88-09-23-79
33-48-56-81-66-84-89
23-38-63-69-33-39-02
2522 70-04-29-62-18-94-74
2524 31-07-43-44-22-06-24
2524 58-41-39-65-11-94-61
2525 85-80-40-57-39-02-03
2524 45-80-38-47-70-95-24
82-85-24-60-48-90-50
04-03-01-57-35-97-62
2524 82-00-55-91-97-52-37
2523 97-00-38-05-71-74-38
32-09-89-80-29-48-51
84-75-37-85-77-75-29
2523 51-44-85-74-10-90-74
2523 25-63-16-22-75-48-79
80-59-44-91-58-46-30
2522 31-48-06-26-42-59-84
48-50-24-48-30-74-73
31-26-27-54-59-28-34
2522 87-66-84-15-33-31-95
51-85-47-66-51-64-87
2523 55-09-83-65-81-58-51
2522 99-11-54-41-04-24-54
78-44-82-14-91-00-67
31-38-18-34-44-79-59
2521 75-13-20-65-21-16-15
2523 26-44-92-56-41-70-22
95-71-53-73-55-50-94
10-44-09-45-67-13-75
2525 06-21-87-86-54-94-02
2524 31-85-09-42-29-45-57
2525 42-01-75-05-25-11-40
2524 12-14-10-27-19-30-99
79-97-04-48-87-42-00
2521 90-02-73-89-64-29-10
2523 29-17-32-76-08-65-75
2524 70-31-69-39-33-84-38
2525 71-52-62-55-12-16-57
36-69-53-13-49-70-66
85-12-10-39-29-80-35
2524 26-09-42-08-04-99-55
2523 33-23-74-47-43-33-24
2525 06-91-79-15-79-29-41
60-88-10-40-92-23-52
2523 24-05-58-34-80-77-14
2522 74-71-28-79-29-38-72
2521 80-50-12-20-47-99-78
2521 06-83-17-55-45-79-82
2521 13-52-26-76-99-70-20
2524 84-64-14-58-40-09-62
2524 86-97-11-55-57-83-16
2522 79-38-56-35-52-07-41
91-38-01-67-78-65-73
2523 05-11-50-18-20-12-38
2521 03-88-90-27-37-15-37
2525 83-26-08-00-50-20-68
2521 68-65-73-31-70-44-45
2524 54-66-91-09-07-74-26
2525 72-65-73-73-62-24-96
07-41-74-07-86-07-39
2522 64-48-93-29-40-97-14
2525 79-90-61-88-87-15-59
2524 50-47-16-17-09-15-14
2521 46-06-40-88-48-85-88
91-27-05-71-25-84-20
2522 12-22-39-13-04-78-78
2525 58-11-44-63-05-97-71
2521 70-16-43-07-87-51-85
2521 58-92-61-20-12-28-60
57-80-24-58-22-03-15
2524 12-08-29-52-75-46-34
2524 63-17-74-41-08-29-16
81-05-91-02-20-96-92
96-59-37-84-38-68-85
34-09-34-90-82-90-14
45-66-92-96-14-48-83
2522 01-61-02-21-68-28-60
89-01-37-64-20-77-75
14-00-50-43-04-66-06
2521 06-35-29-40-03-24-19
2524 78-34-98-20-72-56-24
54-05-64-46-00-00-54
87-00-71-87-41-99-40
70-50-43-54-84-95-28
2524 87-53-38-76-20-49-78

File diff suppressed because it is too large Load Diff

View File

@@ -1,68 +0,0 @@
#!/usr/bin/env python3
"""
Упрощенная версия main.py для диагностики
"""
import asyncio
import logging
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
async def test_imports():
"""Тест импортов по порядку"""
try:
logger.info("1. Тест импорта config...")
from src.core.config import BOT_TOKEN, ADMIN_IDS, DATABASE_URL
logger.info(f"✅ Config OK. BOT_TOKEN: {BOT_TOKEN[:10]}..., ADMIN_IDS: {ADMIN_IDS}")
logger.info("2. Тест импорта aiogram...")
from aiogram import Bot, Dispatcher
logger.info("✅ Aiogram OK")
logger.info("3. Тест создания бота...")
bot = Bot(token=BOT_TOKEN)
logger.info("✅ Bot created OK")
logger.info("4. Тест импорта database...")
from src.core.database import async_session_maker, init_db
logger.info("✅ Database imports OK")
logger.info("5. Тест подключения к БД...")
async with async_session_maker() as session:
logger.info("✅ Database connection OK")
logger.info("6. Тест импорта services...")
from src.core.services import UserService, LotteryService
logger.info("✅ Services OK")
logger.info("7. Тест импорта handlers...")
from src.handlers.registration_handlers import router as registration_router
logger.info("✅ Registration handlers OK")
from src.handlers.admin_panel import admin_router
logger.info("✅ Admin panel OK")
logger.info("8. Тест создания диспетчера...")
dp = Dispatcher()
dp.include_router(registration_router)
dp.include_router(admin_router)
logger.info("✅ Dispatcher OK")
logger.info("9. Тест получения информации о боте...")
bot_info = await bot.get_me()
logger.info(f"✅ Bot info: {bot_info.username} ({bot_info.first_name})")
await bot.session.close()
logger.info("Все тесты пройдены успешно!")
except Exception as e:
logger.error(f"❌ Ошибка: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(test_imports())

View File

@@ -1,74 +0,0 @@
#!/usr/bin/env python3
"""
Скрипт для тестирования функциональности бота
"""
import asyncio
import sys
import os
sys.path.insert(0, os.path.dirname(__file__))
from src.core.database import async_session_maker
from src.core.models import User, Lottery
from sqlalchemy import select
async def test_database_connectivity():
"""Тест подключения к базе данных"""
print("🔌 Тестируем подключение к базе данных...")
async with async_session_maker() as session:
# Проверяем подключение
result = await session.execute(select(1))
print("✅ Подключение к PostgreSQL работает")
# Проверяем количество пользователей
users_count = await session.execute(select(User))
users = users_count.scalars().all()
print(f"📊 В базе {len(users)} пользователей")
# Проверяем количество лотерей
lotteries_count = await session.execute(select(Lottery))
lotteries = lotteries_count.scalars().all()
print(f"🎰 В базе {len(lotteries)} лотерей")
async def test_bot_imports():
"""Тест импортов бота"""
print("🔄 Тестируем импорты модулей...")
try:
from src.handlers.registration_handlers import router as registration_router
print("✅ registration_router импортирован")
from src.handlers.admin_panel import admin_router
print("✅ admin_router импортирован")
from src.handlers.account_handlers import account_router
print("✅ account_router импортирован")
from src.core.config import BOT_TOKEN
print("✅ BOT_TOKEN получен из конфигурации")
except Exception as e:
print(f"❌ Ошибка импорта: {e}")
return False
return True
async def main():
"""Основная функция тестирования"""
print("🤖 Тестирование функциональности лотерейного бота")
print("=" * 50)
# Тест импортов
imports_ok = await test_bot_imports()
if imports_ok:
print("\n")
# Тест базы данных
await test_database_connectivity()
print("\n" + "=" * 50)
print("✅ Тестирование завершено")
if __name__ == "__main__":
asyncio.run(main())