Compare commits

...

18 Commits

Author SHA1 Message Date
Fr1ngg a6734e542d Create lucky_game.py 2025-08-09 07:04:20 +03:00
Fr1ngg 2287a67dfd Create referral_utils.py 2025-08-09 07:03:30 +03:00
Fr1ngg 31dbaa80d7 Update admin_handlers.py 2025-08-09 07:02:43 +03:00
Fr1ngg b1846a3ea9 Update api_error_handlers.py 2025-08-09 07:02:24 +03:00
Fr1ngg e1a7e9e622 Update config.py 2025-08-09 07:02:00 +03:00
Fr1ngg 0ae48faedb Update database.py 2025-08-09 07:01:35 +03:00
Fr1ngg b8122222f8 Update handlers.py 2025-08-09 07:00:51 +03:00
Fr1ngg 5e5a39c22e Update keyboards.py 2025-08-09 07:00:27 +03:00
Fr1ngg 1e7cf214e9 Update main.py 2025-08-09 07:00:10 +03:00
Fr1ngg d7e7a2df9a Update middlewares.py 2025-08-09 06:59:52 +03:00
Fr1ngg 584aa35894 Update remnawave_api.py 2025-08-09 06:59:36 +03:00
Fr1ngg 652ae8057a Update subscription_monitor.py 2025-08-09 06:59:10 +03:00
Fr1ngg 0bc0a1354a Update translations.py 2025-08-09 06:58:45 +03:00
Fr1ngg 7d30838813 Update utils.py 2025-08-09 06:58:22 +03:00
Fr1ngg 3da178ed02 Update handlers.py 2025-08-09 06:58:00 +03:00
Fr1ngg e6ac3e3812 Update README.md 2025-08-09 06:53:32 +03:00
Fr1ngg 62087e20b5 Update README.md 2025-08-09 06:50:45 +03:00
Fr1ngg b7a47c6eb6 Update .env.example 2025-08-09 06:32:47 +03:00
16 changed files with 3887 additions and 1876 deletions
+5 -3
View File
@@ -19,9 +19,6 @@ ADMIN_IDS=123445567,12342123
# Support Configuration
SUPPORT_USERNAME=support
# Database Configuration (optional, defaults to SQLite)
# DATABASE_URL=sqlite+aiosqlite:///bot.db
TRIAL_ENABLED=true
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_GB=2
@@ -32,3 +29,8 @@ TRIAL_PRICE=0.0
MONITOR_CHECK_INTERVAL=3600
MONITOR_DAILY_CHECK_HOUR=10
MONITOR_WARNING_DAYS=2
LUCKY_GAME_ENABLED=true # Включить/выключить игру удачи
LUCKY_GAME_REWARD=50.0 # Размер награды за выигрыш в рублях
LUCKY_GAME_NUMBERS=30 # Общее количество чисел для выбора (1-30)
LUCKY_GAME_WINNING_COUNT=25 # Количество выигрышных чисел из общего количества
+476 -171
View File
@@ -1,244 +1,549 @@
<img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 15 13" src="https://github.com/user-attachments/assets/91098622-1bce-4f27-afef-60a3c5b5061f" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 22" src="https://github.com/user-attachments/assets/46b87e75-b420-4ac6-91b9-8c7e9bcffb2a" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 39" src="https://github.com/user-attachments/assets/ca97811f-ca00-4133-a120-1c11f0efa0fc" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 45" src="https://github.com/user-attachments/assets/258e1adb-2c39-4126-82a7-7791b56d42db" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 53" src="https://github.com/user-attachments/assets/073455fc-f42d-4d70-839d-59042add2d94" /><img width="906" height="316" alt="Снимок экрана 2025-08-05 в 03 16 00" src="https://github.com/user-attachments/assets/2034dde8-a48b-4149-a23f-b788aa40e0b1" /><img width="894" height="317" alt="Снимок экрана 2025-08-05 в 15 32 32" src="https://github.com/user-attachments/assets/a96337cf-f58a-488e-9600-c94a92bdbfc2" /><img width="906" height="366" alt="Снимок экрана 2025-08-05 в 03 16 18" src="https://github.com/user-attachments/assets/3a3d1e0a-92fc-4573-a48c-f36481d6d0de" /><img width="906" height="842" alt="Снимок экрана 2025-08-05 в 03 17 24" src="https://github.com/user-attachments/assets/8b407f69-6861-4810-822e-c3f7b8f63629" /><img width="906" height="274" alt="Снимок экрана 2025-08-05 в 03 17 43" src="https://github.com/user-attachments/assets/923a945a-5ef8-4dcb-9804-fffc37ab8887" /><img width="936" height="364" alt="Снимок экрана 2025-08-05 в 03 20 03" src="https://github.com/user-attachments/assets/1faecdfe-f80c-4ac2-ad38-81a30fc6623d" /><img width="892" height="486" alt="Снимок экрана 2025-08-07 в 07 43 47" src="https://github.com/user-attachments/assets/0dd6cb8e-fd2f-4a98-8920-aadceee09fd0" /><img width="892" height="762" alt="Снимок экрана 2025-08-07 в 07 44 20" src="https://github.com/user-attachments/assets/d7c95e3e-cf04-40bc-9422-d7289447625d" /><img width="892" height="823" alt="Снимок экрана 2025-08-07 в 07 46 45" src="https://github.com/user-attachments/assets/9ab2c378-0abc-447d-9e95-a3ab8dab2f18" />
<img width="892" height="501" alt="Снимок экрана 2025-08-07 в 07 42 07" src="https://github.com/user-attachments/assets/839c02da-4461-4127-894a-772e66175e23" /><img width="892" height="805" alt="Снимок экрана 2025-08-07 в 07 57 01" src="https://github.com/user-attachments/assets/bc35f79d-0b0d-4c81-8623-696b708642ad" /><img width="892" height="834" alt="Снимок экрана 2025-08-07 в 07 41 09" src="https://github.com/user-attachments/assets/d4731a79-0171-4254-aa78-2e4c7305829b" /><img width="631" height="606" alt="Снимок экрана 2025-08-06 в 18 48 31" src="https://github.com/user-attachments/assets/c44548b0-f27b-4f67-b3c0-2c002ae33979" /><img width="866" height="655" alt="Снимок экрана 2025-08-08 в 05 08 56" src="https://github.com/user-attachments/assets/2957b76c-8b2b-4a32-8ca4-44cbe3c8fa7b" /><img width="866" height="256" alt="Снимок экрана 2025-08-08 в 05 10 29" src="https://github.com/user-attachments/assets/39dbece4-6004-475e-815a-a520499c8839" /><img width="866" height="195" alt="Снимок экрана 2025-08-08 в 05 10 54" src="https://github.com/user-attachments/assets/b3196856-9f36-46d9-a1f5-93b71699174f" /><img width="894" height="212" alt="Снимок экрана 2025-08-08 в 05 11 18" src="https://github.com/user-attachments/assets/47b4b625-93d2-48c7-ae3a-a6a96811f935" /><img width="894" height="173" alt="Снимок экрана 2025-08-08 в 05 29 42" src="https://github.com/user-attachments/assets/600eecc7-e255-421f-a355-a410332e4313" />
# 🤖 RemnaWave Telegram Bot
#Описание
[![Docker Image](https://img.shields.io/badge/Docker-fr1ngg/remnawave--bedolaga--telegram--bot-blue?logo=docker)](https://hub.docker.com/r/fr1ngg/remnawave-bedolaga-telegram-bot)
[![Python](https://img.shields.io/badge/Python-3.11+-blue?logo=python)](https://python.org)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-blue?logo=postgresql)](https://postgresql.org)
[![License](https://img.shields.io/badge/License-MIT-green)](LICENSE)
RemnaWave Bedolaga Telegram Bot — это многофункциональный бот для управления подписками(Для каждой подписки возможно назначить свой сквад со своими инбаундами - нововведение Remnawave 2.0.0+), балансом, промокодами, тестовой подпиской и рассылками пользователям через Telegram.
Современный Telegram бот для управления VPN подписками через RemnaWave API с полным функционалом управления пользователями, платежами и администрированием.
Бот интегрирован с системой RemnaWave версии 2.0.8
## 🚀 Особенности
#Основные возможности
### 👤 Для пользователей
- 💰 **Управление балансом** - пополнение через поддержку
- 🛒 **Покупка подписок** - различные тарифные планы c возможностью индивидуальной настройки сквада у плана
- 📱 **Управление подписками** - просмотр, продление, получение ссылок
- 🎁 **Система промокодов** - денежные коды
- 👥 **Реферальная программа** - зарабатывай с друзей
- 🎰 **Игра удачи** - выигрывай бонусы каждые 24 часа
- 🆓 **Тестовая подписка** - бесплатная пробная версия с детальной конфигурацией
- 🌐 **Мультиязычность** - русский и английский
### ⚙️ Для администраторов
- 📊 **Полная статистика** - пользователи, платежи, подписки
- 👥 **Управление пользователями** - поиск, редактирование, баланс
- 💳 **Управление платежами** - одобрение, отклонение, история
- 🎫 **Управление промокодами** - создание, редактирование, статистика
- 🖥 **Мониторинг системы** - состояние нод, пользователей RemnaWave
- 🔄 **Синхронизация данных** - импорт из RemnaWave
- 📨 **Рассылки** - уведомления пользователям
- 🔍 **Мониторинг подписок** - автоматические уведомления об истечении
## 📋 Требования
### Системные требования
- Docker и Docker Compose
- 1+ GB RAM
- 10+ GB свободного места
### Внешние сервисы
- **Telegram Bot Token** - создайте бота через [@BotFather](https://t.me/BotFather)
- **RemnaWave API** - доступ к панели RemnaWave
- **PostgreSQL** - автоматически разворачивается через Docker
## 🛠 Установка и запуск
### 1. Клонирование репозитория
```bash
git clone https://github.com/your-repo/remnawave-telegram-bot.git
cd remnawave-telegram-bot
```
### 2. Настройка переменных окружения
Создайте файл `.env` на основе примера:
```bash
cp .env.example .env
nano .env
```
### 3. Конфигурация
#### 📋 Основные настройки
| Переменная | Описание | Пример |
|------------|----------|---------|
| `BOT_TOKEN` | Токен Telegram бота | `123456:ABC-DEF1234ghIkl-zyx` |
| `BOT_USERNAME` | Username бота (без @) | `your_bot_username` |
| `REMNAWAVE_URL` | URL панели RemnaWave | `https://panel.example.com` |
| `REMNAWAVE_TOKEN` | API токен RemnaWave | `your_api_token` |
| `ADMIN_IDS` | ID администраторов (через запятую) | `123456789,987654321` |
#### 🎁 Реферальная программа
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `REFERRAL_FIRST_REWARD` | Награда за первого реферала | `100.0` |
| `REFERRAL_REFERRED_BONUS` | Бонус приглашенному | `100.0` |
| `REFERRAL_THRESHOLD` | Порог активации (руб.) | `200.0` |
| `REFERRAL_PERCENTAGE` | Процент с платежей | `0.2` (20%) |
#### 🎰 Игра удачи
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `LUCKY_GAME_ENABLED` | Включить игру | `true` |
| `LUCKY_GAME_REWARD` | Награда за выигрыш | `50.0` |
| `LUCKY_GAME_NUMBERS` | Всего чисел | `30` |
| `LUCKY_GAME_WINNING_COUNT` | Выигрышных чисел | `3` |
#### 🆓 Тестовая подписка
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `TRIAL_ENABLED` | Включить тестовую подписку | `true` |
| `TRIAL_DURATION_DAYS` | Длительность (дни) | `3` |
| `TRIAL_TRAFFIC_GB` | Лимит трафика (ГБ) | `2` |
| `TRIAL_SQUAD_UUID` | UUID squad для тестовых | `uuid-here` |
### 4. Docker Compose конфигурация
#### 🚀 Минимальная конфигурация (рекомендуется)
Создайте файл `docker-compose.yml` для базового запуска:
```yaml
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: remnawave_bot_db
restart: unless-stopped
environment:
POSTGRES_DB: remnawave_bot
POSTGRES_USER: remnawave_user
POSTGRES_PASSWORD: secure_password_123
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U remnawave_user -d remnawave_bot"]
interval: 15s
timeout: 10s
retries: 5
start_period: 30s
# RemnaWave Bot
bot:
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
container_name: remnawave_bot
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
env_file:
- .env
environment:
DATABASE_URL: postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
volumes:
- ./logs:/app/logs
- ./data:/app/data
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "python -c 'print(\"Bot is running\")'"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
postgres_data:
driver: local
networks:
bot_network:
driver: bridge
```
#### ⚡ Полная конфигурация (с дополнительными сервисами)
Для расширенной настройки с Redis и Nginx создайте `docker-compose.full.yml`:
```yaml
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: remnawave_bot_db
restart: unless-stopped
environment:
POSTGRES_DB: remnawave_bot
POSTGRES_USER: remnawave_user
POSTGRES_PASSWORD: secure_password_123
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U remnawave_user -d remnawave_bot"]
interval: 15s
timeout: 10s
retries: 5
start_period: 30s
# RemnaWave Bot
bot:
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
container_name: remnawave_bot
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
env_file:
- .env
environment:
DATABASE_URL: postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
volumes:
- ./logs:/app/logs
- ./data:/app/data
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "python -c 'print(\"Bot is running\")'"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
# Redis (для кэширования и улучшения производительности)
redis:
image: redis:7-alpine
container_name: remnawave_bot_redis
restart: unless-stopped
command: redis-server --appendonly yes --requirepass redis_password_123
volumes:
- redis_data:/data
ports:
- "6379:6379"
networks:
- bot_network
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 10s
timeout: 3s
retries: 5
profiles:
- with-redis
# Nginx (для статических файлов или веб-интерфейса)
nginx:
image: nginx:alpine
container_name: remnawave_bot_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
- ./static:/usr/share/nginx/html:ro
networks:
- bot_network
depends_on:
- bot
profiles:
- with-nginx
volumes:
postgres_data:
driver: local
redis_data:
driver: local
networks:
bot_network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
```
### 5. Варианты запуска
#### 🚀 Быстрый старт (минимальная конфигурация)
```bash
# Запуск только бота и базы данных
docker-compose up -d
# Просмотр логов
docker-compose logs -f bot
```
#### ⚡ Расширенный запуск
```bash
# Запуск с Redis для улучшения производительности
docker-compose --profile with-redis up -d
# Запуск с Nginx для веб-интерфейса
docker-compose --profile with-nginx up -d
# Запуск со всеми сервисами
docker-compose --profile with-redis --profile with-nginx up -d
# Или используйте полную конфигурацию
docker-compose -f docker-compose.full.yml up -d
```
#### 🛑 Управление сервисами
```bash
# Остановка всех сервисов
docker-compose down
# Остановка с удалением volumes (ВНИМАНИЕ: удалит все данные!)
docker-compose down -v
# Перезапуск конкретного сервиса
docker-compose restart bot
# Просмотр статуса
docker-compose ps
```
#### 📋 Варианты конфигураций
| Конфигурация | Команда | Описание |
|--------------|---------|----------|
| **Минимальная** | `docker-compose up -d` | Только бот + PostgreSQL |
| **С Redis** | `docker-compose --profile with-redis up -d` | + кэширование |
| **С Nginx** | `docker-compose --profile with-nginx up -d` | + веб-сервер |
| **Полная** | `docker-compose --profile with-redis --profile with-nginx up -d` | Все сервисы |
Мультиязычный интерфейс (на данный момент русский и английский языки)
## 📊 Мониторинг и управление
Создание и покупка подписок
### Просмотр логов
Бесплатная тестовая подписка с заданными ограничениями(срок, лимит трафика, назначение сквада)
```bash
# Логи бота
docker-compose logs -f bot
Пополнение баланса: 1) Через саппорт в ручную 2) Отправка заявки с суммой админу (С возможность подтвердить/отклонить заявку)
# Логи базы данных
docker-compose logs -f postgres
Управление балансом пользователей (пополнение, списание)
# Все логи
docker-compose logs -f
```
Промокоды со скидками и ограничениями по использованию
### Проверка состояния
Реф. программа - С несколькими уровнями: 1) награда первое пополнение приглашенному и владельцу реф ссылки. 2) Награда владельцу за каждое последующее пополнение в процентах от суммы пополнения
```bash
# Статус контейнеров
docker-compose ps
Полноценная админ-панель с контролем пользователей, созданием подписок(Подтягивает UUID сквада из панели), платежей и статистикой
# Использование ресурсов
docker stats
```
Рассылка сообщений отдельным пользователям и всем сразу
### Backup базы данных
Сервис контроля истечения сроков действия подписки(Уведомляет об истечении за указанный в настройках срок), уведомления с предложением продления подписи. (NEW)
```bash
# Создание backup
docker exec remnawave_bot_db pg_dump -U remnawave_user remnawave_bot > backup.sql
Интеграция с RemnaWave API для управления подписками и пользователями RemnaWave
# Восстановление
docker exec -i remnawave_bot_db psql -U remnawave_user remnawave_bot < backup.sql
```
Полная синхранизация Remnawave <--> Bot - Перенос подписок из панели Remnawave в бот по Telegram id
## 🔧 Обновление
Управление системой Remnawave (NEW)
### Обновление Docker образа
Управление платежами (подтверждение, отклонение) + История платежей(Все действия с балансом и подписками в постраничной истории)
```bash
# Остановить сервисы
docker-compose down
# Обновить образ
docker-compose pull bot
#Требования
# Запустить снова
docker-compose up -d
Pip
# Проверить логи
docker-compose logs -f bot
```
Python 3.8+
### Ручная сборка образа
PostgreSQL, SQLite или другая поддерживаемая SQL-база данных
```bash
# Клонировать репозиторий
git clone https://github.com/your-repo/remnawave-telegram-bot.git
cd remnawave-telegram-bot
Токен Telegram-бота
# Собрать образ
docker build -t remnawave-bot .
URL и токен RemnaWave API
# Изменить docker-compose.yml
# bot:
# image: remnawave-bot # вместо fr1ngg/remnawave-bedolaga-telegram-bot
```
Ссылки на подписку из ремны формата SUB_PUBLIC_DOMAIN=sub.example.com/sub
## 🎯 Использование
#Установка
### Для пользователей
1. Клонируйте репозиторий:
1. **Запуск бота** - найдите бота по username и нажмите `/start`
2. **Выбор языка** - выберите русский или английский
3. **Пополнение баланса** - через меню "💰 Баланс" → "💳 Пополнить"
4. **Покупка подписки** - "🛒 Купить подписку" → выбор тарифа → оплата
5. **Управление подписками** - "📋 Мои подписки" → выбор подписки
6. **Реферальная программа** - "👥 Рефералы" → поделиться ссылкой
git clone https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot
cd remnawave-bedolaga-telegram-bot
### Для администраторов
2. Создайте файл .env в корне проекта и заполните его необходимыми переменными окружения. Пример:
Администратор видит дополнительную кнопку "⚙️ Админ панель" с возможностями:
BOT_TOKEN=ваш_telegram_bot_token
REMNAWAVE_URL=https://your-remnawave-url.ru
REMNAWAVE_MODE=remote/local
REMNAWAVE_TOKEN=ваш_remnawave_token
DATABASE_URL=sqlite+aiosqlite:///bot.db
ADMIN_IDS=123456789,987654321
DEFAULT_LANGUAGE=ru
SUPPORT_USERNAME=support
TRIAL_ENABLED=true
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_GB=2
TRIAL_SQUAD_UUID=19bd5bde-5eea-4368-809c-6ba1ffb93897
TRIAL_PRICE=0.0
MONITOR_CHECK_INTERVAL=3600
MONITOR_DAILY_CHECK_HOUR=10
MONITOR_WARNING_DAYS=2
- **📦 Управление подписками** - создание тарифов
- **👥 Управление пользователями** - поиск, редактирование
- **💰 Управление балансом** - пополнение балансов
- **🎁 Управление промокодами** - создание скидок
- **📨 Рассылки** - отправка сообщений
- **🖥 Система RemnaWave** - мониторинг нод и синхронизация
4. Соберите образ (Makefile Dockerfile docker-compose):
## 🔒 Безопасность
make build
### Рекомендации
5. Запуск:
- ✅ Измените пароли PostgreSQL в `docker-compose.yml`
- ✅ Не публикуйте файл `.env` в репозитории
- ✅ Используйте HTTPS для RemnaWave API
- ✅ Регулярно создавайте backup базы данных
- ✅ Ограничьте доступ к серверу через firewall
Запуск минимальной конфигурации (бот + база данных):
make up-min
### Переменные безопасности
Или запуск с Redis:
make up
```bash
# Генерация безопасных паролей
openssl rand -base64 32 # для PostgreSQL
openssl rand -hex 16 # для Redis
```
Или запуск со всеми сервисами включая Nginx:
make up-full
## 🐛 Устранение неполадок
5. Управление
### Частые проблемы
Просмотр логов:
| Проблема | Решение |
|----------|---------|
| Бот не отвечает | Проверьте `BOT_TOKEN` и интернет |
| Ошибка подключения к БД | Проверьте статус PostgreSQL контейнера |
| Ошибки RemnaWave API | Проверьте `REMNAWAVE_URL` и `REMNAWAVE_TOKEN` |
| Игра удачи не работает | Проверьте `LUCKY_GAME_ENABLED=true` |
make logs-bot
### Диагностика
Статус сервисов:
make status
```bash
# Проверка переменных окружения
docker exec remnawave_bot env | grep BOT_
Перезапуск:
make restart
# Подключение к базе данных
docker exec -it remnawave_bot_db psql -U remnawave_user remnawave_bot
Остановка:
make down
#Конфигурация
# Перезапуск бота
docker-compose restart bot
```
BOT_TOKEN — токен Telegram бота от BotFather.
### Логи и отладка
BOT_USERNAME= (Юзернейм бота для работы реф программы)
```bash
# Детальные логи
docker-compose logs -f --tail 100 bot
REFERRAL_FIRST_REWARD=100.0 (Награда за пополнение рефа)
# Ошибки базы данных
docker-compose logs postgres | grep ERROR
REFERRAL_REFERRED_BONUS=100.0 (Награда рефу за первое пополнение)
# Проверка health checks
docker-compose ps
```
REFERRAL_THRESHOLD=200.0 (Сумма на которую должен пополниться реф, чтобы выдало бонус всем)
## 📈 Производительность
REFERRAL_PERCENTAGE=0.2 (Процент за каждое последующее пополнение рефа, работает со второго пополнения)
### Рекомендуемые ресурсы
REMNAWAVE_URL — URL API RemnaWave.
| Пользователей | RAM | CPU | Диск |
|---------------|-----|-----|------|
| До 500 | 1GB | 1 CPU | 10GB |
| До 1,000 | 2GB | 1 CPU | 20GB |
| До 10,000 | 4GB | 2 CPU | 50GB |
| До 50,000 | 8GB | 4 CPU | 100GB |
REMNAWAVE_MODE=remote
### Оптимизация
REMNAWAVE_TOKEN — токен доступа к API RemnaWave.
- Включите Redis для кэширования (профиль `with-redis`)
- Настройте PostgreSQL для production
- Используйте nginx как reverse proxy
- Мониторьте ресурсы через `docker stats`
DATABASE_URL — строка подключения к базе данных.
## 🤝 Поддержка
ADMIN_IDS — через запятую Telegram ID администраторов.
### Получить помощь
SUPPORT_USERNAME — ник поддержки, без @ указывать
- 💬 **Telegram**: @fringg
Параметры тестовой подписки (включение, длительность, трафик, UUID squad, цена).
### Вклад в проект
TRIAL_ENABLED=true/false
1. Fork репозитория
2. Создайте feature branch
3. Внесите изменения
4. Создайте Pull Request
TRIAL_DURATION_DAYS=3 (дни)
## 📄 Лицензия
TRIAL_TRAFFIC_GB=2
Этот проект распространяется под лицензией MIT. См. файл [LICENSE](LICENSE) для подробностей.
TRIAL_SQUAD_UUID=(УКазать UUID сквада из панели!)
## 🗺 Roadmap & TODO
TRIAL_PRICE=0.0(не трогать)
### 📋 Общий план развития
Monitor Service Settings (дополнительные настройки)
| № | Задача | Статус | Версия | Приоритет | Описание |
|---|---------|--------|--------|-----------|----------|
| 1 | Мониторинг подписок | ✅ Done | v1.1.0 | 🔴 High | Служба оповещения об истечении срока и автоконтроль |
| 2 | Платежные шлюзы | 🎯 Planned | v1.4.0+ | 🔴 High | ЮKassa, Сбербанк, Tinkoff, Crypto, Звезды |
| 3 | Синхронизация RemnaWave | ✅ Done | v1.1.0 | 🟡 Medium | Импорт пользователей по Telegram ID |
| 4 | Веб-панель управления | 🎯 Planned | v1.4.0+ | 🟡 Medium | Полнофункциональный веб-интерфейс |
| 5 | Управление промокодами | ✅ Done | v1.3.0 | 🟢 Low | Удаление, редактирование |
| 6 | Безопасное удаление подписок | 🚧 In Progress | v1.4.0 | 🟡 Medium | Архивирование вместо удаления |
| 7 | Уведомления в другие чаты | 🎯 Planned | v1.5.0 | 🟡 Medium | Webhook, Email |
| 8 | Реферальная система | ✅ Done | v1.2.0 | 🟢 Low | Полнофункциональная программа с подробной статистикой |
| 9 | Рулетка | ✅ Done | v1.3.0 | 🟢 Low | Лудочка |
MONITOR_CHECK_INTERVAL=3600 (Запуск службы проверки)
### 🚀 Хотите помочь?
MONITOR_DAILY_CHECK_HOUR=10 (Разовый чек в определенный промежуток дня)
**Разработчикам:**
- 🐛 Найдите и исправьте баги
- ✨ Предложите новые функции
- 📝 Улучшите документацию
- 🧪 Напишите тесты
MONITOR_WARNING_DAYS=2 (За сколько дней слать уведомления)
**Пользователям:**
- 💭 Поделитесь идеями в Issues
- 🐞 Сообщите о найденных проблемах
- ⭐ Поставьте звезду проекту
- 📢 Расскажите друзьям
**Спонсорам:**
- 💰 Поддержите разработку
- 🎯 Закажите приоритетные функции
- 🏢 Корпоративная поддержка
#Использование
---
/start - запуск
**⚠️ Важные замечания от автора:**
#Синхронизация подписок
> 🗑 **Осторожно с удалением подписок!** Сейчас удаление плана подписки скроет эту подписку у всех пользователей, которые ее купили. Используйте деактивацию вместо удаления. Эта проблема будет исправлена в v1.4.0.
Вы можете перенести свои существующие подписки из панели Remnawave прямо в бота всего одним кликом.
Для этого в админ панеле реализован соостветствующий пункт: Админ панель - Система Remnawave - Синхронизация с Remnawave - Импорт всех по Telegram ID. После нажатия подтянет всех пользователей в бота, подпискам из панели будет назначено имя "Старая подписка" - такую подписку невозможно продлить.
---
ДОПОЛНИТЕЛЬНО:
Реализована возможность зачистки импортированных из панели подписок по тг айди Админ панель - Система Remnawave - Синхронизация с Remnawave - Просмотрт планов - Удалалить импортированные
**⭐ Если проект был полезен, поставьте звездочку на GitHub!**
Остальное трогать без понимания кода - не рекомендую.
---
#Структура проекта
main.py — главный файл запуска.
handlers.py — основные обработчики команд и действий пользователя.
admin_handlers.py — обработчики команд и действий администраторов.
database.py — модели и методы работы с базой данных (SQLAlchemy).
remnawave_api.py — интеграция с API RemnaWave.
keyboards.py — генерация клавиатур Telegram.
translations.py — локализация и переводы.
utils.py — вспомогательные функции.
middlewares.py — промежуточные слои для обработки сообщений и запросов.
subscription_monitor.py - сервис мониторинга сроков истечения подписок
.env — файл конфигурации с переменными окружения.
requirements.txt — список зависимостей Python.
run.sh — скрипт установки и управления ботом (опционально).
#Администрирование
Для входа в админ-панель используйте кнопку "⚙️ Админ панель" в главном меню, если вы указаны как администратор в ADMIN_IDS.
В админ-панели доступны:
Управление подписками (создание, редактирование, список, удаление, включение/отключение)
Управление пользователями (просмотр списка, баланс)
Управление балансом (пополнение пользователей через tg id)
Управление промокодами (создание, список)
Одобрение или отклонение платежей (Приходит запрос администраторам)
Отправка сообщений пользователям или массовая рассылка
Мониторинг подписок (Проверка статуса службы, принудитедьный запуск, деактивация истекщих подписок(на случай падения базы), персональный тест(можно отправить уведомления юзеру по tg id)
Просмотр статистики
Управление систеой Remnawave (Ноды, пользователи, синхронизация и импорт подписок из базы Remnawave в бот)
#ToDo
Код колхозный и не без вайбкодинга тут обошлось, но будет допиливаться, текущая реализация работает - уже хорошо
1) Дописать службу для оповещения об истечении срока подписки и контроля - Done v1.1.0
2) Подключить различные шлюзы для пополнения баланса
3) Синхранизацию с Remnawave между пользователями по тг id - Done v1.2.0
4) Полнофункциональную панель упарвления
5) Добавить возможность удаление промокодов - In progress
6) Доработать алгоритм удаления подписок ибо удаление(А НЕ деактивация) сейчас - скроект эту подписку у всех юзеров которые ее купили, так что удаляйте на свой страх и риск я предупредил) - In progress
8) Отправка уведомлений административных в другие чаты-топики
9) Рефка (как по мне беспонтовая штука, сервера нормальные хостите, сервис нормальный делайте и будут клиенты - не ебите мозги, но если будет не лень, то допилю), ладно трех запросов в личку мне хватило для того. чтобы пересмотреть свое мнение и вкорячить эту шляпу приоритетом - Done v 1.3.0
10) Интегрировать панелья управления Remnawave прямо в бота - Done v1.1.0
А так, други, буду рад баг репортам, по настройке тоже не проблема подсказать, стучите в лс или @fringg
*Создано с ❤️ для RemnaWave сообщества*
+1383 -721
View File
File diff suppressed because it is too large Load Diff
-30
View File
@@ -8,18 +8,11 @@ from translations import t
logger = logging.getLogger(__name__)
class APIErrorHandler:
"""Класс для обработки ошибок API и предоставления пользователю понятной информации"""
@staticmethod
async def handle_api_error(callback: CallbackQuery, error: Exception,
operation: str, user_language: str = 'ru',
fallback_keyboard=None) -> bool:
"""
Обработка ошибок API с отправкой понятного сообщения пользователю
Returns:
bool: True если ошибка была обработана, False если нужно перепробросить
"""
error_message = str(error).lower()
if "timeout" in error_message or "connection" in error_message:
@@ -66,12 +59,6 @@ class APIErrorHandler:
@staticmethod
async def safe_api_call(api_method: Callable, *args, **kwargs) -> tuple[bool, Any]:
"""
Безопасный вызов метода API с обработкой ошибок
Returns:
tuple: (success: bool, result: Any)
"""
try:
result = await api_method(*args, **kwargs)
return True, result
@@ -79,15 +66,11 @@ class APIErrorHandler:
logger.error(f"API call failed: {api_method.__name__} - {e}")
return False, str(e)
# Дополнительные обработчики для исправления конкретных проблем
def create_error_recovery_keyboard(error_context: str, language: str = 'ru'):
"""Создание клавиатуры для восстановления после ошибки"""
from keyboards import error_recovery_keyboard
return error_recovery_keyboard(error_context, language)
# Улучшенные функции для работы с RemnaWave API
async def safe_get_nodes(api: RemnaWaveAPI) -> tuple[bool, list]:
"""Безопасное получение списка нод"""
try:
logger.info("Attempting to fetch nodes from API...")
nodes = await api.get_all_nodes()
@@ -108,7 +91,6 @@ async def safe_get_nodes(api: RemnaWaveAPI) -> tuple[bool, list]:
return False, []
async def safe_get_system_users(api: RemnaWaveAPI) -> tuple[bool, list]:
"""Безопасное получение списка пользователей системы"""
try:
logger.info("Attempting to fetch system users from API...")
users = await api.get_all_system_users_full()
@@ -129,7 +111,6 @@ async def safe_get_system_users(api: RemnaWaveAPI) -> tuple[bool, list]:
return False, []
async def safe_restart_nodes(api: RemnaWaveAPI, all_nodes: bool = True, node_id: str = None) -> tuple[bool, str]:
"""Безопасная перезагрузка нод"""
try:
if all_nodes:
logger.info("Attempting to restart all nodes...")
@@ -151,9 +132,7 @@ async def safe_restart_nodes(api: RemnaWaveAPI, all_nodes: bool = True, node_id:
logger.error(f"Error restarting nodes: {e}")
return False, str(e)
# Функции для проверки состояния API
async def check_api_health(api: RemnaWaveAPI) -> Dict[str, Any]:
"""Проверка состояния API"""
health_info = {
'api_available': False,
'nodes_accessible': False,
@@ -166,9 +145,7 @@ async def check_api_health(api: RemnaWaveAPI) -> Dict[str, Any]:
health_info['errors'].append("API instance is None")
return health_info
# Проверяем доступность API
try:
# Простая проверка через получение нод (обычно быстрая операция)
success, nodes = await safe_get_nodes(api)
if success:
health_info['api_available'] = True
@@ -178,7 +155,6 @@ async def check_api_health(api: RemnaWaveAPI) -> Dict[str, Any]:
except Exception as e:
health_info['errors'].append(f"Nodes check failed: {e}")
# Проверяем доступность пользователей
try:
success, users = await safe_get_system_users(api)
if success:
@@ -188,7 +164,6 @@ async def check_api_health(api: RemnaWaveAPI) -> Dict[str, Any]:
except Exception as e:
health_info['errors'].append(f"Users check failed: {e}")
# Проверяем системную статистику
try:
stats = await api.get_system_stats()
if stats:
@@ -200,9 +175,7 @@ async def check_api_health(api: RemnaWaveAPI) -> Dict[str, Any]:
return health_info
# Декоратор для автоматической обработки ошибок API
def handle_api_errors(operation_name: str):
"""Декоратор для автоматической обработки ошибок API в handler'ах"""
def decorator(func):
async def wrapper(callback: CallbackQuery, user, *args, **kwargs):
try:
@@ -210,11 +183,9 @@ def handle_api_errors(operation_name: str):
except Exception as e:
logger.error(f"Error in {func.__name__}: {e}")
# Получаем API из kwargs если есть
api = kwargs.get('api')
fallback_keyboard = None
# Создаем fallback клавиатуру в зависимости от операции
if 'nodes' in operation_name.lower():
from keyboards import admin_system_keyboard
fallback_keyboard = admin_system_keyboard(user.language)
@@ -222,7 +193,6 @@ def handle_api_errors(operation_name: str):
from keyboards import system_users_keyboard
fallback_keyboard = system_users_keyboard(user.language)
# Обрабатываем ошибку
await APIErrorHandler.handle_api_error(
callback, e, operation_name, user.language, fallback_keyboard
)
+109 -152
View File
@@ -1,172 +1,129 @@
import os
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import List
import logging
try:
from dotenv import load_dotenv
load_dotenv()
print("✅ .env файл загружен успешно")
except ImportError:
print("⚠️ python-dotenv не установлен. Используются системные переменные окружения.")
except Exception as e:
print(f"⚠️ Ошибка загрузки .env файла: {e}")
logger = logging.getLogger(__name__)
@dataclass
class Config:
# Bot settings
BOT_TOKEN: str = field(default="")
DATABASE_URL: str = field(default="sqlite+aiosqlite:///bot.db")
# Bot configuration
BOT_TOKEN: str
BOT_USERNAME: str
# RemnaWave API settings
REMNAWAVE_URL: str = field(default="")
REMNAWAVE_TOKEN: str = field(default="")
SUBSCRIPTION_BASE_URL: str = field(default="")
# Database
DATABASE_URL: str
# Admin settings
ADMIN_IDS: List[int] = field(default_factory=list)
SUPPORT_USERNAME: str = field(default="support")
# RemnaWave API
REMNAWAVE_URL: str
REMNAWAVE_TOKEN: str
SUBSCRIPTION_BASE_URL: str
# Bot settings
DEFAULT_LANGUAGE: str = field(default="ru")
BOT_USERNAME: str = field(default="") # ИМЯ БОТА ДЛЯ РЕФЕРАЛЬНЫХ ССЫЛОК
# Admin configuration
ADMIN_IDS: List[int]
SUPPORT_USERNAME: str
DEFAULT_LANGUAGE: str
# Trial subscription settings
TRIAL_ENABLED: bool = field(default=False)
TRIAL_DURATION_DAYS: int = field(default=3)
TRIAL_TRAFFIC_GB: int = field(default=2)
TRIAL_SQUAD_UUID: str = field(default="")
# Trial subscription
TRIAL_ENABLED: bool
TRIAL_DURATION_DAYS: int
TRIAL_TRAFFIC_GB: int
TRIAL_SQUAD_UUID: str
# Referral system
REFERRAL_FIRST_REWARD: float
REFERRAL_REFERRED_BONUS: float
REFERRAL_THRESHOLD: float
REFERRAL_PERCENTAGE: float
# Конфигурация мониторинга подписок
MONITOR_ENABLED: bool = True
MONITOR_CHECK_INTERVAL: int = 3600 # 1 час
MONITOR_DAILY_CHECK_HOUR: int = 10 # 10:00 утра
MONITOR_WARNING_DAYS: int = 3 # предупреждать за 3 дня
MONITOR_WARNING_DAYS: int = field(default=3)
MONITOR_CHECK_INTERVAL: int = field(default=3600) # 1 час
MONITOR_DAILY_CHECK_HOUR: int = field(default=9) # 9 утра
# Referral program settings
REFERRAL_FIRST_REWARD: float = field(default=150.0) # Первая награда
REFERRAL_REFERRED_BONUS: float = field(default=150.0) # Бонус приглашенному
REFERRAL_THRESHOLD: float = field(default=300.0) # Порог для получения бонуса
REFERRAL_PERCENTAGE: float = field(default=0.25) # 25% с платежей
def parse_admin_ids(admin_ids_str: str) -> List[int]:
"""Parse admin IDs from string"""
if not admin_ids_str:
return []
try:
# Support both comma and space separated
ids_str = admin_ids_str.replace(',', ' ').strip()
return [int(id_str.strip()) for id_str in ids_str.split() if id_str.strip().isdigit()]
except ValueError as e:
logger.error(f"Error parsing admin IDs: {e}")
return []
def str_to_bool(value: str) -> bool:
"""Convert string to boolean"""
if isinstance(value, bool):
return value
return value.lower() in ('true', '1', 'yes', 'on', 'enabled')
LUCKY_GAME_ENABLED: bool = True
LUCKY_GAME_REWARD: float = 50.0 # Размер награды за выигрыш
LUCKY_GAME_NUMBERS: int = 30 # Общее количество чисел (1-30)
LUCKY_GAME_WINNING_COUNT: int = 3 # Количество выигрышных чисел
def load_config() -> Config:
"""Load configuration from environment variables"""
# Дебаг: проверяем что переменные загружены
print(f"🔍 BOT_USERNAME из env: '{os.getenv('BOT_USERNAME', 'НЕ НАЙДЕН')}'")
print(f"🔍 REFERRAL_FIRST_REWARD из env: '{os.getenv('REFERRAL_FIRST_REWARD', 'НЕ НАЙДЕН')}'")
config = Config(
# Bot settings
BOT_TOKEN=os.getenv("BOT_TOKEN", ""),
DATABASE_URL=os.getenv("DATABASE_URL", "sqlite+aiosqlite:///bot.db"),
# RemnaWave API settings
REMNAWAVE_URL=os.getenv("REMNAWAVE_URL", ""),
REMNAWAVE_TOKEN=os.getenv("REMNAWAVE_TOKEN", ""),
SUBSCRIPTION_BASE_URL=os.getenv("SUBSCRIPTION_BASE_URL", ""),
# Admin settings
ADMIN_IDS=parse_admin_ids(os.getenv("ADMIN_IDS", "")),
SUPPORT_USERNAME=os.getenv("SUPPORT_USERNAME", "support"),
# Bot settings
DEFAULT_LANGUAGE=os.getenv("DEFAULT_LANGUAGE", "ru"),
BOT_USERNAME=os.getenv("BOT_USERNAME", ""), # ИСПРАВЛЕНО: берется из env
# Trial settings
TRIAL_ENABLED=str_to_bool(os.getenv("TRIAL_ENABLED", "false")),
TRIAL_DURATION_DAYS=int(os.getenv("TRIAL_DURATION_DAYS", "3")),
TRIAL_TRAFFIC_GB=int(os.getenv("TRIAL_TRAFFIC_GB", "2")),
TRIAL_SQUAD_UUID=os.getenv("TRIAL_SQUAD_UUID", ""),
MONITOR_WARNING_DAYS=int(os.getenv("MONITOR_WARNING_DAYS", "3")),
MONITOR_CHECK_INTERVAL=int(os.getenv("MONITOR_CHECK_INTERVAL", "3600")),
MONITOR_DAILY_CHECK_HOUR=int(os.getenv("MONITOR_DAILY_CHECK_HOUR", "9")),
# Referral settings
REFERRAL_FIRST_REWARD=float(os.getenv("REFERRAL_FIRST_REWARD", "150.0")),
REFERRAL_REFERRED_BONUS=float(os.getenv("REFERRAL_REFERRED_BONUS", "150.0")),
REFERRAL_THRESHOLD=float(os.getenv("REFERRAL_THRESHOLD", "300.0")),
REFERRAL_PERCENTAGE=float(os.getenv("REFERRAL_PERCENTAGE", "0.25")),
)
print(f"✅ Config BOT_USERNAME: '{config.BOT_USERNAME}'")
print(f"✅ Config REFERRAL_FIRST_REWARD: {config.REFERRAL_FIRST_REWARD}")
return config
def load_config_manual_dotenv() -> Config:
"""Load config with manual .env parsing"""
# Загружаем .env файл вручную если python-dotenv не доступен
env_path = ".env"
if os.path.exists(env_path):
def parse_admin_ids(admin_ids_str: str) -> List[int]:
if not admin_ids_str:
return []
try:
with open(env_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
# Убираем кавычки если есть
value = value.strip('"\'')
os.environ[key] = value
print(f"✅ Файл {env_path} загружен вручную")
except Exception as e:
print(f"⚠️ Ошибка загрузки {env_path}: {e}")
return [int(id_str.strip()) for id_str in admin_ids_str.split(',') if id_str.strip()]
except (ValueError, AttributeError):
return []
return load_config()
def get_bool(key: str, default: bool = False) -> bool:
value = os.getenv(key, '').lower()
return value in ('true', '1', 'yes', 'on')
def get_int(key: str, default: int = 0) -> int:
try:
return int(os.getenv(key, str(default)))
except (ValueError, TypeError):
return default
def get_float(key: str, default: float = 0.0) -> float:
try:
return float(os.getenv(key, str(default)))
except (ValueError, TypeError):
return default
return Config(
# Bot configuration
BOT_TOKEN=os.getenv('BOT_TOKEN', ''),
BOT_USERNAME=os.getenv('BOT_USERNAME', ''),
# Database
DATABASE_URL=os.getenv('DATABASE_URL', 'sqlite+aiosqlite:///bot.db'),
# RemnaWave API
REMNAWAVE_URL=os.getenv('REMNAWAVE_URL', ''),
REMNAWAVE_TOKEN=os.getenv('REMNAWAVE_TOKEN', ''),
SUBSCRIPTION_BASE_URL=os.getenv('SUBSCRIPTION_BASE_URL', ''),
# Admin configuration
ADMIN_IDS=parse_admin_ids(os.getenv('ADMIN_IDS', '')),
SUPPORT_USERNAME=os.getenv('SUPPORT_USERNAME', 'support'),
DEFAULT_LANGUAGE=os.getenv('DEFAULT_LANGUAGE', 'ru'),
# Trial subscription
TRIAL_ENABLED=get_bool('TRIAL_ENABLED', False),
TRIAL_DURATION_DAYS=get_int('TRIAL_DURATION_DAYS', 3),
TRIAL_TRAFFIC_GB=get_int('TRIAL_TRAFFIC_GB', 2),
TRIAL_SQUAD_UUID=os.getenv('TRIAL_SQUAD_UUID', ''),
# Referral system
REFERRAL_FIRST_REWARD=get_float('REFERRAL_FIRST_REWARD', 150.0),
REFERRAL_REFERRED_BONUS=get_float('REFERRAL_REFERRED_BONUS', 150.0),
REFERRAL_THRESHOLD=get_float('REFERRAL_THRESHOLD', 300.0),
REFERRAL_PERCENTAGE=get_float('REFERRAL_PERCENTAGE', 0.25),
MONITOR_ENABLED=get_bool('MONITOR_ENABLED', True),
MONITOR_CHECK_INTERVAL=get_int('MONITOR_CHECK_INTERVAL', 3600), # 1 час по умолчанию
MONITOR_DAILY_CHECK_HOUR=get_int('MONITOR_DAILY_CHECK_HOUR', 10), # 10 утра
MONITOR_WARNING_DAYS=get_int('MONITOR_WARNING_DAYS', 3), # за 3 дня
# Игра удачи
LUCKY_GAME_ENABLED=get_bool('LUCKY_GAME_ENABLED', True),
LUCKY_GAME_REWARD=get_float('LUCKY_GAME_REWARD', 50.0),
LUCKY_GAME_NUMBERS=get_int('LUCKY_GAME_NUMBERS', 30),
LUCKY_GAME_WINNING_COUNT=get_int('LUCKY_GAME_WINNING_COUNT', 3)
)
def debug_environment():
"""Debug environment variables loading"""
print("\n=== DEBUG ENVIRONMENT VARIABLES ===")
# Ключевые переменные для проверки
key_vars = [
'BOT_TOKEN', 'BOT_USERNAME', 'REMNAWAVE_URL', 'REMNAWAVE_TOKEN',
'ADMIN_IDS', 'REFERRAL_FIRST_REWARD', 'REFERRAL_THRESHOLD'
env_vars = [
'BOT_TOKEN', 'BOT_USERNAME', 'DATABASE_URL',
'REMNAWAVE_URL', 'REMNAWAVE_TOKEN', 'ADMIN_IDS',
'REFERRAL_FIRST_REWARD', 'REFERRAL_THRESHOLD',
'MONITOR_ENABLED', 'MONITOR_CHECK_INTERVAL',
'LUCKY_GAME_ENABLED', 'LUCKY_GAME_REWARD'
]
for var in key_vars:
value = os.getenv(var, 'НЕ УСТАНОВЛЕНА')
print(f"{var}: {value}")
print("=" * 40)
# Проверяем файл .env
if os.path.exists('.env'):
print("📁 Файл .env найден")
try:
with open('.env', 'r') as f:
lines = f.readlines()
print(f"📄 Строк в .env: {len(lines)}")
for i, line in enumerate(lines[:5], 1): # Первые 5 строк
if 'TOKEN' not in line: # Не показываем токены
print(f" {i}: {line.strip()}")
except Exception as e:
print(f"❌ Ошибка чтения .env: {e}")
else:
print("❌ Файл .env не найден в текущей директории")
print(f"📍 Текущая директория: {os.getcwd()}")
print("=" * 40 + "\n")
print("📋 Environment variables:")
for var in env_vars:
value = os.getenv(var, 'NOT SET')
if 'TOKEN' in var and value != 'NOT SET':
value = value[:10] + "..." if len(value) > 10 else value
print(f" {var}: {value}")
+390 -92
View File
@@ -1,8 +1,8 @@
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import BigInteger, String, Float, DateTime, Boolean, Text, Integer, text
from datetime import datetime
from typing import Optional, List, Dict
from sqlalchemy import BigInteger, String, Float, DateTime, Boolean, Text, Integer, text, select, func, and_
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
import logging
logger = logging.getLogger(__name__)
@@ -69,12 +69,12 @@ class UserSubscription(Base):
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, index=True)
subscription_id: Mapped[int] = mapped_column(Integer, index=True)
short_uuid: Mapped[str] = mapped_column(String(255)) # УБРАНО unique=True
short_uuid: Mapped[str] = mapped_column(String(255))
expires_at: Mapped[datetime] = mapped_column(DateTime)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
traffic_limit_gb: Mapped[Optional[int]] = mapped_column(Integer) # Добавлено поле
traffic_limit_gb: Mapped[Optional[int]] = mapped_column(Integer)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, onupdate=datetime.utcnow) # Добавлено поле
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, onupdate=datetime.utcnow)
class Payment(Base):
__tablename__ = 'payments'
@@ -82,9 +82,9 @@ class Payment(Base):
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, index=True)
amount: Mapped[float] = mapped_column(Float)
payment_type: Mapped[str] = mapped_column(String(50)) # 'topup', 'subscription'
payment_type: Mapped[str] = mapped_column(String(50))
description: Mapped[str] = mapped_column(Text)
status: Mapped[str] = mapped_column(String(50), default='pending') # 'pending', 'completed', 'cancelled'
status: Mapped[str] = mapped_column(String(50), default='pending')
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class Promocode(Base):
@@ -108,6 +108,17 @@ class PromocodeUsage(Base):
promocode_id: Mapped[int] = mapped_column(Integer, index=True)
used_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class LuckyGame(Base):
__tablename__ = 'lucky_games'
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, index=True)
chosen_number: Mapped[int] = mapped_column(Integer)
winning_numbers: Mapped[str] = mapped_column(String(255)) # JSON строка с выигрышными номерами
is_winner: Mapped[bool] = mapped_column(Boolean, default=False)
reward_amount: Mapped[float] = mapped_column(Float, default=0.0)
played_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class Database:
def __init__(self, database_url: str):
self.engine = create_async_engine(
@@ -126,16 +137,13 @@ class Database:
async with self.engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# Выполняем миграции
await self.migrate_user_subscriptions()
await self.migrate_subscription_imported_field()
await self.migrate_referral_tables() # НОВАЯ МИГРАЦИЯ
await self.migrate_referral_tables()
async def migrate_referral_tables(self):
"""Create referral system tables if they don't exist"""
try:
async with self.engine.begin() as conn:
# Создаем таблицы реферальной системы
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS referral_programs (
id SERIAL PRIMARY KEY,
@@ -176,7 +184,6 @@ class Database:
async def close(self):
await self.engine.dispose()
# User methods
async def get_user_by_telegram_id(self, telegram_id: int) -> Optional[User]:
async with self.session_factory() as session:
try:
@@ -238,7 +245,6 @@ class Database:
await session.rollback()
return False
# Subscription methods
async def get_all_subscriptions(self, include_inactive: bool = False, exclude_trial: bool = True, exclude_imported: bool = True) -> List[Subscription]:
async with self.session_factory() as session:
try:
@@ -249,7 +255,7 @@ class Database:
if exclude_trial:
query = query.where(Subscription.is_trial == False)
if exclude_imported:
query = query.where(Subscription.is_imported == False) # Исключаем импортированные
query = query.where(Subscription.is_imported == False)
result = await session.execute(query)
return list(result.scalars().all())
except Exception as e:
@@ -257,7 +263,6 @@ class Database:
return []
async def get_all_subscriptions_admin(self) -> List[Subscription]:
"""Get all subscriptions including imported ones (for admin purposes)"""
async with self.session_factory() as session:
try:
from sqlalchemy import select
@@ -268,7 +273,6 @@ class Database:
return []
async def migrate_subscription_imported_field(self):
"""Add is_imported field to subscriptions table"""
try:
async with self.engine.begin() as conn:
try:
@@ -306,7 +310,7 @@ class Database:
duration_days=duration_days,
traffic_limit_gb=traffic_limit_gb,
squad_uuid=squad_uuid,
is_imported=is_imported # Добавляем поддержку is_imported
is_imported=is_imported
)
session.add(subscription)
await session.commit()
@@ -357,10 +361,8 @@ class Database:
async def create_user_subscription(self, user_id: int, subscription_id: int,
short_uuid: str, expires_at: datetime,
is_active: bool = True, traffic_limit_gb: int = None) -> Optional[UserSubscription]:
"""Create user subscription with proper error handling"""
async with self.session_factory() as session:
try:
# Проверяем что подписка не существует
from sqlalchemy import select
existing = await session.execute(
select(UserSubscription).where(
@@ -374,7 +376,6 @@ class Database:
logger.warning(f"Subscription with short_uuid {short_uuid} already exists for user {user_id}")
return existing_sub
# Создаем новую подписку
new_subscription = UserSubscription(
user_id=user_id,
subscription_id=subscription_id,
@@ -394,7 +395,6 @@ class Database:
await session.rollback()
return None
# Payment methods
async def create_payment(self, user_id: int, amount: float, payment_type: str,
description: str, status: str = 'pending') -> Payment:
async with self.session_factory() as session:
@@ -452,7 +452,6 @@ class Database:
logger.error(f"Error getting user payments for {user_id}: {e}")
return []
# Promocode methods
async def get_promocode_by_code(self, code: str) -> Optional[Promocode]:
async with self.session_factory() as session:
try:
@@ -489,7 +488,6 @@ class Database:
async def use_promocode(self, user_id: int, promocode: Promocode) -> bool:
async with self.session_factory() as session:
try:
# Check if already used
from sqlalchemy import select
existing = await session.execute(
select(PromocodeUsage).where(
@@ -500,11 +498,9 @@ class Database:
if existing.scalar_one_or_none():
return False
# Create usage record
usage = PromocodeUsage(user_id=user_id, promocode_id=promocode.id)
session.add(usage)
# Update promocode used count
promocode.used_count += 1
await session.merge(promocode)
@@ -525,7 +521,6 @@ class Database:
logger.error(f"Error getting promocodes: {e}")
return []
# Admin methods
async def get_all_users(self) -> List[User]:
async with self.session_factory() as session:
try:
@@ -541,13 +536,11 @@ class Database:
try:
from sqlalchemy import select, func
# Total users
total_users = await session.execute(
select(func.count(User.id))
)
total_users = total_users.scalar()
# Total subscriptions (excluding trial)
total_subs_non_trial = await session.execute(
select(func.count(UserSubscription.id))
.join(Subscription, UserSubscription.subscription_id == Subscription.id)
@@ -555,11 +548,10 @@ class Database:
)
total_subs_non_trial = total_subs_non_trial.scalar()
# Total payments (excluding trial payments)
total_payments = await session.execute(
select(func.sum(Payment.amount)).where(
Payment.status == 'completed',
Payment.payment_type != 'trial' # Исключаем тестовые платежи
Payment.payment_type != 'trial'
)
)
total_payments = total_payments.scalar() or 0
@@ -578,7 +570,6 @@ class Database:
}
async def get_trial_subscriptions(self) -> List[Subscription]:
"""Get only trial subscriptions"""
async with self.session_factory() as session:
try:
from sqlalchemy import select
@@ -591,7 +582,6 @@ class Database:
return []
async def get_user_subscription_by_short_uuid(self, user_id: int, short_uuid: str) -> Optional[UserSubscription]:
"""Get user subscription by short_uuid"""
async with self.session_factory() as session:
try:
from sqlalchemy import select
@@ -607,7 +597,6 @@ class Database:
return None
async def update_user_subscription(self, user_subscription: UserSubscription) -> bool:
"""Update user subscription"""
async with self.session_factory() as session:
try:
# Устанавливаем время обновления
@@ -623,10 +612,8 @@ class Database:
return False
async def migrate_user_subscriptions(self):
"""Migrate user_subscriptions table to add missing columns"""
try:
async with self.engine.begin() as conn:
# Проверяем существование столбцов и добавляем их если нет
try:
await conn.execute(text("""
ALTER TABLE user_subscriptions
@@ -690,18 +677,15 @@ class Database:
return False
async def get_all_payments_paginated(self, offset: int = 0, limit: int = 10) -> tuple[List[Payment], int]:
"""Get all payments with pagination"""
async with self.session_factory() as session:
try:
from sqlalchemy import select, desc, func
# Получаем общее количество записей
count_result = await session.execute(
select(func.count(Payment.id))
)
total_count = count_result.scalar()
# Получаем платежи с пагинацией
result = await session.execute(
select(Payment)
.order_by(desc(Payment.created_at))
@@ -717,18 +701,15 @@ class Database:
return [], 0
async def get_payments_by_type_paginated(self, payment_type: str, offset: int = 0, limit: int = 10) -> tuple[List[Payment], int]:
"""Get payments by type with pagination"""
async with self.session_factory() as session:
try:
from sqlalchemy import select, desc, func
# Получаем общее количество записей
count_result = await session.execute(
select(func.count(Payment.id)).where(Payment.payment_type == payment_type)
)
total_count = count_result.scalar()
# Получаем платежи с пагинацией
result = await session.execute(
select(Payment)
.where(Payment.payment_type == payment_type)
@@ -745,18 +726,15 @@ class Database:
return [], 0
async def get_payments_by_status_paginated(self, status: str, offset: int = 0, limit: int = 10) -> tuple[List[Payment], int]:
"""Get payments by status with pagination"""
async with self.session_factory() as session:
try:
from sqlalchemy import select, desc, func
# Получаем общее количество записей
count_result = await session.execute(
select(func.count(Payment.id)).where(Payment.status == status)
)
total_count = count_result.scalar()
# Получаем платежи с пагинацией
result = await session.execute(
select(Payment)
.where(Payment.status == status)
@@ -773,7 +751,6 @@ class Database:
return [], 0
async def get_user_subscriptions_by_plan_id(self, plan_id: int) -> List[UserSubscription]:
"""Get all user subscriptions for a specific plan"""
async with self.session_factory() as session:
try:
from sqlalchemy import select
@@ -786,7 +763,6 @@ class Database:
return []
async def delete_user_subscription(self, user_subscription_id: int) -> bool:
"""Delete user subscription by ID"""
async with self.session_factory() as session:
try:
from sqlalchemy import delete
@@ -801,16 +777,13 @@ class Database:
return False
async def create_referral(self, referrer_id: int, referred_id: int, referral_code: str) -> Optional[ReferralProgram]:
"""Create referral relationship - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
async with self.session_factory() as session:
try:
from sqlalchemy import select
if referred_id == 0:
# Генерируем уникальный placeholder ID для хранения кода
placeholder_id = 999999999 - referrer_id # Уникальный ID на основе referrer_id
placeholder_id = 999999999 - referrer_id
# Проверяем что код еще не существует для этого пользователя
existing = await session.execute(
select(ReferralProgram).where(
ReferralProgram.referrer_id == referrer_id,
@@ -823,10 +796,9 @@ class Database:
logger.info(f"Referral code already exists for user {referrer_id}")
return existing_referral
# Создаем запись для хранения кода
referral = ReferralProgram(
referrer_id=referrer_id,
referred_id=placeholder_id, # Уникальный placeholder
referred_id=placeholder_id,
referral_code=referral_code
)
session.add(referral)
@@ -835,24 +807,21 @@ class Database:
logger.info(f"Created referral code storage for user {referrer_id}")
return referral
# Обычная логика для реальных рефералов
existing = await session.execute(
select(ReferralProgram).where(
ReferralProgram.referred_id == referred_id,
ReferralProgram.referred_id < 900000000, # Исключаем placeholder
ReferralProgram.referred_id > 0 # Исключаем нулевые
ReferralProgram.referred_id < 900000000,
ReferralProgram.referred_id > 0
)
)
if existing.scalar_one_or_none():
logger.info(f"User {referred_id} already has a real referrer")
return None
# Проверяем что пользователь не приглашает сам себя
if referrer_id == referred_id:
logger.warning(f"User {referrer_id} tried to refer themselves")
return None
# Создаем реальную реферальную связь
referral = ReferralProgram(
referrer_id=referrer_id,
referred_id=referred_id,
@@ -871,7 +840,6 @@ class Database:
return None
async def get_referral_by_referred_id(self, referred_id: int) -> Optional[ReferralProgram]:
"""Get referral info by referred user ID"""
async with self.session_factory() as session:
try:
from sqlalchemy import select
@@ -884,7 +852,6 @@ class Database:
return None
async def get_user_referrals(self, referrer_id: int) -> List[ReferralProgram]:
"""Get all referrals for a user - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
async with self.session_factory() as session:
try:
from sqlalchemy import select, and_
@@ -892,7 +859,6 @@ class Database:
select(ReferralProgram).where(
and_(
ReferralProgram.referrer_id == referrer_id,
# ИСПРАВЛЕНО: исключаем placeholder записи для хранения кодов
ReferralProgram.referred_id < 900000000,
ReferralProgram.referred_id > 0
)
@@ -906,17 +872,13 @@ class Database:
return []
async def create_referral(self, referrer_id: int, referred_id: int, referral_code: str) -> Optional[ReferralProgram]:
"""Create referral relationship - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
async with self.session_factory() as session:
try:
from sqlalchemy import select
# Специальный случай: создание кода для самого пользователя
if referred_id == 0:
# Генерируем уникальный placeholder ID для хранения кода
placeholder_id = 999999999 - referrer_id # Уникальный ID на основе referrer_id
placeholder_id = 999999999 - referrer_id
# Проверяем что код еще не существует для этого пользователя
existing = await session.execute(
select(ReferralProgram).where(
ReferralProgram.referrer_id == referrer_id,
@@ -929,10 +891,9 @@ class Database:
logger.info(f"Referral code already exists for user {referrer_id}")
return existing_referral
# Создаем запись для хранения кода
referral = ReferralProgram(
referrer_id=referrer_id,
referred_id=placeholder_id, # Уникальный placeholder
referred_id=placeholder_id,
referral_code=referral_code
)
session.add(referral)
@@ -940,7 +901,6 @@ class Database:
await session.refresh(referral)
return referral
# Обычная логика для реальных рефералов
existing = await session.execute(
select(ReferralProgram).where(ReferralProgram.referred_id == referred_id)
)
@@ -948,7 +908,6 @@ class Database:
logger.info(f"User {referred_id} already has a referrer")
return None
# Проверяем что пользователь не приглашает сам себя
if referrer_id == referred_id:
logger.warning(f"User {referrer_id} tried to refer themselves")
return None
@@ -968,39 +927,35 @@ class Database:
return None
async def get_user_referral_stats(self, user_id: int) -> Dict:
"""Get user referral statistics - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
async with self.session_factory() as session:
try:
from sqlalchemy import select, func, and_, or_
placeholder_id = 999999999 - user_id
# Количество приглашенных (исключаем только конкретный placeholder этого пользователя)
referrals_count = await session.execute(
select(func.count(ReferralProgram.id))
.where(
and_(
ReferralProgram.referrer_id == user_id,
ReferralProgram.referred_id != placeholder_id, # Исключаем только наш placeholder
ReferralProgram.referred_id != 0 # Исключаем нулевые записи
ReferralProgram.referred_id != placeholder_id,
ReferralProgram.referred_id != 0
)
)
)
# Количество тех, кто получил первую награду (исключаем placeholder)
active_referrals = await session.execute(
select(func.count(ReferralProgram.id))
.where(
and_(
ReferralProgram.referrer_id == user_id,
ReferralProgram.first_reward_paid == True,
ReferralProgram.referred_id != placeholder_id, # Исключаем только наш placeholder
ReferralProgram.referred_id != 0 # Исключаем нулевые записи
ReferralProgram.referred_id != placeholder_id,
ReferralProgram.referred_id != 0
)
)
)
# Общий заработок
total_earned = await session.execute(
select(func.sum(ReferralEarning.amount))
.where(ReferralEarning.referrer_id == user_id)
@@ -1024,13 +979,11 @@ class Database:
}
async def generate_unique_referral_code(self, user_id: int) -> str:
"""Generate unique referral code for user"""
async with self.session_factory() as session:
try:
import secrets
import string
# Сначала пытаемся создать код на основе user_id
base_code = f"REF{user_id}"
from sqlalchemy import select
@@ -1041,7 +994,6 @@ class Database:
if not existing.scalar_one_or_none():
return base_code
# Если код уже существует, добавляем случайные символы
for _ in range(10):
random_suffix = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(4))
code = f"REF{user_id}{random_suffix}"
@@ -1053,7 +1005,6 @@ class Database:
if not existing.scalar_one_or_none():
return code
# Если все еще не удалось, используем полностью случайный код
return f"REF{''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(8))}"
except Exception as e:
@@ -1061,7 +1012,6 @@ class Database:
return f"REF{user_id}ERR"
async def get_user_referrals(self, referrer_id: int) -> List[ReferralProgram]:
"""Get all referrals for a user - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
async with self.session_factory() as session:
try:
from sqlalchemy import select, and_
@@ -1072,16 +1022,15 @@ class Database:
select(ReferralProgram).where(
and_(
ReferralProgram.referrer_id == referrer_id,
ReferralProgram.referred_id != placeholder_id, # Исключаем только наш placeholder
ReferralProgram.referred_id != 0 # Исключаем нулевые записи
ReferralProgram.referred_id != placeholder_id,
ReferralProgram.referred_id != 0
)
).order_by(ReferralProgram.created_at.desc()) # Сортируем по дате создания
).order_by(ReferralProgram.created_at.desc())
)
referrals = list(result.scalars().all())
logger.info(f"Found {len(referrals)} real referrals for user {referrer_id} (excluding placeholder {placeholder_id})")
# Дополнительно логируем каждого реферала для отладки
for ref in referrals:
logger.debug(f"Referral: referrer={ref.referrer_id}, referred={ref.referred_id}, "
f"first_reward_paid={ref.first_reward_paid}, total_earned={ref.total_earned}")
@@ -1094,7 +1043,6 @@ class Database:
async def create_referral_earning(self, referrer_id: int, referred_id: int,
amount: float, earning_type: str,
related_payment_id: Optional[int] = None) -> bool:
"""Create referral earning record"""
async with self.session_factory() as session:
try:
earning = ReferralEarning(
@@ -1106,10 +1054,8 @@ class Database:
)
session.add(earning)
# Обновляем общий заработок и статус первой награды в реферальной программе
from sqlalchemy import select, update
# Найти запись реферальной программы
referral = await session.execute(
select(ReferralProgram).where(
ReferralProgram.referrer_id == referrer_id,
@@ -1119,10 +1065,8 @@ class Database:
referral_record = referral.scalar_one_or_none()
if referral_record:
# Обновляем total_earned
referral_record.total_earned += amount
# Если это первая награда, помечаем как выплаченную
if earning_type == 'first_reward':
referral_record.first_reward_paid = True
referral_record.first_reward_at = datetime.utcnow()
@@ -1136,3 +1080,357 @@ class Database:
logger.error(f"Error creating referral earning: {e}")
await session.rollback()
return False
async def get_promocode_by_id(self, promocode_id: int) -> Optional[Promocode]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(Promocode).where(Promocode.id == promocode_id)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Error getting promocode by ID {promocode_id}: {e}")
return None
async def update_promocode(self, promocode: Promocode) -> Promocode:
async with self.session_factory() as session:
try:
await session.merge(promocode)
await session.commit()
return promocode
except Exception as e:
logger.error(f"Error updating promocode {promocode.id}: {e}")
await session.rollback()
raise
async def delete_promocode(self, promocode_id: int) -> bool:
async with self.session_factory() as session:
try:
from sqlalchemy import delete
await session.execute(
delete(PromocodeUsage).where(PromocodeUsage.promocode_id == promocode_id)
)
result = await session.execute(
delete(Promocode).where(Promocode.id == promocode_id)
)
await session.commit()
return result.rowcount > 0
except Exception as e:
logger.error(f"Error deleting promocode {promocode_id}: {e}")
await session.rollback()
return False
async def get_regular_promocodes(self, include_inactive: bool = True) -> List[Promocode]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, and_
query = select(Promocode).where(~Promocode.code.startswith('REF'))
if not include_inactive:
query = query.where(Promocode.is_active == True)
result = await session.execute(query.order_by(Promocode.created_at.desc()))
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting regular promocodes: {e}")
return []
async def get_expired_promocodes(self) -> List[Promocode]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, and_
from datetime import datetime
result = await session.execute(
select(Promocode).where(
and_(
Promocode.expires_at < datetime.utcnow(),
~Promocode.code.startswith('REF')
)
)
)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting expired promocodes: {e}")
return []
async def cleanup_expired_promocodes(self) -> int:
async with self.session_factory() as session:
try:
from sqlalchemy import delete, and_
from datetime import datetime
expired_promos = await session.execute(
select(Promocode.id).where(
and_(
Promocode.expires_at < datetime.utcnow(),
~Promocode.code.startswith('REF')
)
)
)
expired_ids = [row[0] for row in expired_promos.fetchall()]
if not expired_ids:
return 0
await session.execute(
delete(PromocodeUsage).where(PromocodeUsage.promocode_id.in_(expired_ids))
)
result = await session.execute(
delete(Promocode).where(Promocode.id.in_(expired_ids))
)
await session.commit()
return result.rowcount
except Exception as e:
logger.error(f"Error cleaning up expired promocodes: {e}")
await session.rollback()
return 0
async def deactivate_all_regular_promocodes(self) -> int:
async with self.session_factory() as session:
try:
from sqlalchemy import update, and_
result = await session.execute(
update(Promocode)
.where(
and_(
~Promocode.code.startswith('REF'), # Исключаем реферальные
Promocode.is_active == True
)
)
.values(is_active=False)
)
await session.commit()
return result.rowcount
except Exception as e:
logger.error(f"Error deactivating all promocodes: {e}")
await session.rollback()
return 0
async def get_promocode_stats(self) -> Dict:
async with self.session_factory() as session:
try:
from sqlalchemy import select, func, and_
from datetime import datetime
total_promos = await session.execute(
select(func.count(Promocode.id)).where(~Promocode.code.startswith('REF'))
)
total_count = total_promos.scalar() or 0
active_promos = await session.execute(
select(func.count(Promocode.id)).where(
and_(
~Promocode.code.startswith('REF'),
Promocode.is_active == True
)
)
)
active_count = active_promos.scalar() or 0
expired_promos = await session.execute(
select(func.count(Promocode.id)).where(
and_(
~Promocode.code.startswith('REF'),
Promocode.expires_at < datetime.utcnow()
)
)
)
expired_count = expired_promos.scalar() or 0
total_usage = await session.execute(
select(func.sum(Promocode.used_count)).where(~Promocode.code.startswith('REF'))
)
usage_count = total_usage.scalar() or 0
total_discount = await session.execute(
select(func.sum(Promocode.discount_amount * Promocode.used_count)).where(
~Promocode.code.startswith('REF')
)
)
discount_amount = total_discount.scalar() or 0.0
top_promos = await session.execute(
select(Promocode.code, Promocode.used_count, Promocode.discount_amount)
.where(~Promocode.code.startswith('REF'))
.order_by(Promocode.used_count.desc())
.limit(5)
)
top_promocodes = list(top_promos.fetchall())
return {
'total_promocodes': total_count,
'active_promocodes': active_count,
'expired_promocodes': expired_count,
'total_usage': usage_count,
'total_discount_amount': discount_amount,
'top_promocodes': top_promocodes
}
except Exception as e:
logger.error(f"Error getting promocode stats: {e}")
return {
'total_promocodes': 0,
'active_promocodes': 0,
'expired_promocodes': 0,
'total_usage': 0,
'total_discount_amount': 0.0,
'top_promocodes': []
}
async def get_promocode_usage_by_id(self, promocode_id: int) -> List[PromocodeUsage]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(PromocodeUsage)
.where(PromocodeUsage.promocode_id == promocode_id)
.order_by(PromocodeUsage.used_at.desc())
)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting promocode usage for {promocode_id}: {e}")
return []
async def create_lucky_game(self, user_id: int, chosen_number: int,
winning_numbers: List[int], is_winner: bool,
reward_amount: float = 0.0) -> Optional[LuckyGame]:
async with self.session_factory() as session:
try:
import json
game = LuckyGame(
user_id=user_id,
chosen_number=chosen_number,
winning_numbers=json.dumps(winning_numbers),
is_winner=is_winner,
reward_amount=reward_amount
)
session.add(game)
await session.commit()
await session.refresh(game)
return game
except Exception as e:
logger.error(f"Error creating lucky game: {e}")
await session.rollback()
return None
async def get_user_last_game_today(self, user_id: int) -> Optional[LuckyGame]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, and_
from datetime import date
today = date.today()
result = await session.execute(
select(LuckyGame)
.where(
and_(
LuckyGame.user_id == user_id,
LuckyGame.played_at >= datetime.combine(today, datetime.min.time()),
LuckyGame.played_at < datetime.combine(today + timedelta(days=1), datetime.min.time())
)
)
.order_by(LuckyGame.played_at.desc())
.limit(1)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Error getting user last game today: {e}")
return None
async def get_user_game_stats(self, user_id: int) -> Dict[str, Any]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, func, and_
# Общее количество игр
games_count = await session.execute(
select(func.count(LuckyGame.id))
.where(LuckyGame.user_id == user_id)
)
total_games = games_count.scalar() or 0
# Количество выигрышей
wins_count = await session.execute(
select(func.count(LuckyGame.id))
.where(
and_( # Вот здесь используется and_
LuckyGame.user_id == user_id,
LuckyGame.is_winner == True
)
)
)
total_wins = wins_count.scalar() or 0
total_reward = await session.execute(
select(func.sum(LuckyGame.reward_amount))
.where(LuckyGame.user_id == user_id)
)
total_won = total_reward.scalar() or 0.0
return {
'total_games': total_games,
'total_wins': total_wins,
'total_won': total_won,
'win_rate': (total_wins / total_games * 100) if total_games > 0 else 0
}
except Exception as e:
logger.error(f"Error getting user game stats: {e}")
return {
'total_games': 0,
'total_wins': 0,
'total_won': 0.0,
'win_rate': 0
}
async def get_user_game_history(self, user_id: int, limit: int = 10) -> List[Dict[str, Any]]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
import json
result = await session.execute(
select(LuckyGame)
.where(LuckyGame.user_id == user_id)
.order_by(LuckyGame.played_at.desc())
.limit(limit)
)
games = result.scalars().all()
history = []
for game in games:
winning_numbers = json.loads(game.winning_numbers) if game.winning_numbers else []
history.append({
'id': game.id,
'chosen_number': game.chosen_number,
'winning_numbers': winning_numbers,
'is_winner': game.is_winner,
'reward_amount': game.reward_amount,
'played_at': game.played_at
})
return history
except Exception as e:
logger.error(f"Error getting user game history: {e}")
return []
async def can_play_lucky_game_today(self, user_id: int) -> bool:
try:
last_game = await self.get_user_last_game_today(user_id)
return last_game is None
except Exception as e:
logger.error(f"Error checking can play today: {e}")
return True
+16 -162
View File
@@ -22,17 +22,15 @@ from referral_utils import (
create_referral_from_promocode,
generate_referral_link
)
from lucky_game import lucky_game_router, LuckyGameStates
logger = logging.getLogger(__name__)
# FSM States
class BotStates(StatesGroup):
waiting_language = State()
waiting_amount = State()
waiting_promocode = State()
waiting_topup_amount = State()
# Admin subscription management
admin_create_sub_name = State()
admin_create_sub_desc = State()
admin_create_sub_price = State()
@@ -41,44 +39,32 @@ class BotStates(StatesGroup):
admin_create_sub_squad = State()
admin_create_sub_squad_select = State()
admin_edit_sub_value = State()
# Admin balance management
admin_add_balance_user = State()
admin_add_balance_amount = State()
admin_payment_history_page = State()
# Admin promocode management
admin_create_promo_code = State()
admin_create_promo_discount = State()
admin_create_promo_limit = State()
# Admin messaging
admin_edit_promo_value = State()
admin_create_promo_expiry = State()
admin_send_message_user = State()
admin_send_message_text = State()
admin_broadcast_text = State()
# Admin user management
admin_search_user_uuid = State()
admin_search_user_any = State()
admin_edit_user_expiry = State()
admin_edit_user_traffic = State()
# Admin monitoring
admin_test_monitor_user = State()
admin_sync_single_user = State()
admin_debug_user_structure = State()
admin_rename_plans_confirm = State()
waiting_number_choice = State()
router = Router()
# Start command
@router.message(Command("start"))
async def start_command(message: Message, state: FSMContext, db: Database, **kwargs):
"""Handle /start command with referral support and language memory - ИСПРАВЛЕНО"""
user = kwargs.get('user')
config = kwargs.get('config')
@@ -87,16 +73,13 @@ async def start_command(message: Message, state: FSMContext, db: Database, **kwa
await message.answer("❌ Ошибка инициализации пользователя. Попробуйте позже.")
return
# Обрабатываем реферальный параметр только если есть параметры
if message.text and len(message.text.split()) > 1:
start_param = message.text.split()[1]
# Проверяем реферальную ссылку
if start_param.startswith("ref_"):
try:
referrer_id = int(start_param.replace("ref_", ""))
# ЗАЩИТА: Проверяем взаимное реферальство
existing_reverse_referral = await db.get_referral_by_referred_id(referrer_id)
if existing_reverse_referral and existing_reverse_referral.referrer_id == user.telegram_id:
await message.answer(
@@ -108,7 +91,6 @@ async def start_command(message: Message, state: FSMContext, db: Database, **kwa
success = await create_referral_from_start_param(user.telegram_id, start_param, db, bot)
if success:
# Уведомляем нового пользователя
import os
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
referred_bonus = float(os.getenv('REFERRAL_REFERRED_BONUS', '150.0'))
@@ -119,21 +101,16 @@ async def start_command(message: Message, state: FSMContext, db: Database, **kwa
f"вы получите бонус {referred_bonus:.0f}₽!"
)
elif not success:
# Проверяем причину неудачи
existing_referral = await db.get_referral_by_referred_id(user.telegram_id)
if existing_referral:
await message.answer("ℹ️ Вы уже использовали реферальную ссылку ранее.")
except (ValueError, TypeError):
# Неверный формат ссылки, просто игнорируем
pass
# Очищаем состояние
await state.clear()
if not user.language or user.language == 'ru' or user.language == '':
# НОВОЕ: Проверяем, это первый запуск или пользователь уже выбирал язык
if user.language == '' or user.language is None:
# Первый запуск - показываем выбор языка
await message.answer(
t('select_language'),
reply_markup=language_keyboard()
@@ -141,30 +118,23 @@ async def start_command(message: Message, state: FSMContext, db: Database, **kwa
await state.set_state(BotStates.waiting_language)
return
else:
# Язык уже выбран (ru) - показываем главное меню
await show_main_menu(message, user.language, user.is_admin, user.telegram_id, db, config)
else:
# У пользователя уже установлен язык - показываем главное меню
await show_main_menu(message, user.language, user.is_admin, user.telegram_id, db, config)
async def process_referral_rewards(user_id: int, amount: float, payment_id: int, db: Database, bot=None):
"""Process referral rewards after successful payment"""
try:
# Проверяем есть ли у пользователя реферер
referral = await db.get_referral_by_referred_id(user_id)
if not referral:
return
# Получаем информацию о пользователе
user = await db.get_user_by_telegram_id(user_id)
if not user:
return
# Проверяем первую награду (если баланс стал >= 300 и награда еще не выплачена)
if not referral.first_reward_paid and user.balance >= 300:
# Выплачиваем первую награду рефереру (150₽)
success = await db.create_referral_earning(
referrer_id=referral.referrer_id,
referred_id=user_id,
@@ -175,7 +145,6 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
if success and bot:
try:
# Уведомляем реферера
await bot.send_message(
referral.referrer_id,
f"🎉 Поздравляем! Ваш реферал пополнил баланс на 300₽+\n\n"
@@ -183,13 +152,11 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
f"Также вы будете получать 25% с каждого его платежа."
)
# Уведомляем самого пользователя
await bot.send_message(
user_id,
f"🎁 Бонус активирован! Вам начислено 150₽ за переход по реферальной ссылке!"
)
# Добавляем бонус рефералу
await db.add_balance(user_id, 150.0)
await db.create_payment(
user_id=user_id,
@@ -202,8 +169,7 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
except Exception as e:
logger.error(f"Failed to send referral notifications: {e}")
# Выплачиваем процент с платежа (25%)
if amount > 0: # Только с положительных платежей
if amount > 0:
percentage_reward = amount * 0.25
success = await db.create_referral_earning(
@@ -214,7 +180,7 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
related_payment_id=payment_id
)
if success and bot and percentage_reward >= 1.0: # Уведомляем только если сумма >= 1₽
if success and bot and percentage_reward >= 1.0:
try:
await bot.send_message(
referral.referrer_id,
@@ -231,7 +197,6 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
# Language selection
@router.callback_query(F.data.startswith("lang_"))
async def language_callback(callback: CallbackQuery, state: FSMContext, db: Database, **kwargs):
"""Handle language selection - ИСПРАВЛЕНО: правильное сохранение языка"""
user = kwargs.get('user')
config = kwargs.get('config')
@@ -241,29 +206,22 @@ async def language_callback(callback: CallbackQuery, state: FSMContext, db: Data
lang = callback.data.split("_")[1]
# Update user language
try:
# ИСПРАВЛЕНО: Сохраняем выбранный язык в базе
user.language = lang
await db.update_user(user)
logger.info(f"Updated language for user {user.telegram_id} to {lang}")
# Check if this is initial language selection or language change
current_state = await state.get_state()
is_initial_setup = current_state == BotStates.waiting_language.state
if is_initial_setup:
# Первоначальная настройка языка
await callback.message.edit_text(
t('language_selected', lang),
reply_markup=None
)
# ВАЖНО: Очищаем состояние и показываем главное меню
await state.clear()
await show_main_menu(callback.message, lang, user.is_admin, user.telegram_id, db, config)
else:
# Смена языка из главного меню
# Проверяем, доступна ли тестовая подписка
show_trial = False
if config and config.TRIAL_ENABLED and db:
try:
@@ -282,27 +240,28 @@ async def language_callback(callback: CallbackQuery, state: FSMContext, db: Data
await callback.answer("❌ Ошибка обновления языка")
async def show_main_menu(message: Message, lang: str, is_admin: bool = False, user_id: int = None, db: Database = None, config: Config = None):
"""Show main menu"""
try:
show_trial = False
show_lucky_game = True # По умолчанию показываем игру
# Проверяем, доступна ли тестовая подписка
if config and config.TRIAL_ENABLED and user_id and db:
has_used = await db.has_used_trial(user_id)
show_trial = not has_used
# Проверяем, включена ли игра удачи в конфиге
if config:
show_lucky_game = getattr(config, 'LUCKY_GAME_ENABLED', True)
await message.answer(
t('main_menu', lang),
reply_markup=main_menu_keyboard(lang, is_admin, show_trial)
reply_markup=main_menu_keyboard(lang, is_admin, show_trial, show_lucky_game)
)
except Exception as e:
logger.error(f"Error showing main menu: {e}")
await message.answer("❌ Ошибка отображения меню")
# Main menu handlers
@router.callback_query(F.data == "main_menu")
async def main_menu_callback(callback: CallbackQuery, **kwargs):
"""Return to main menu"""
user = kwargs.get('user')
db = kwargs.get('db')
config = kwargs.get('config')
@@ -313,7 +272,6 @@ async def main_menu_callback(callback: CallbackQuery, **kwargs):
show_trial = False
# Проверяем, доступна ли тестовая подписка
if config and config.TRIAL_ENABLED and db:
try:
has_used = await db.has_used_trial(user.telegram_id)
@@ -326,10 +284,8 @@ async def main_menu_callback(callback: CallbackQuery, **kwargs):
reply_markup=main_menu_keyboard(user.language, user.is_admin, show_trial)
)
# Trial subscription handlers
@router.callback_query(F.data == "trial_subscription")
async def trial_subscription_callback(callback: CallbackQuery, db: Database, **kwargs):
"""Show trial subscription info"""
user = kwargs.get('user')
config = kwargs.get('config')
@@ -342,7 +298,6 @@ async def trial_subscription_callback(callback: CallbackQuery, db: Database, **k
return
try:
# Проверяем, не использовал ли пользователь уже тестовую подписку
has_used = await db.has_used_trial(user.telegram_id)
if has_used:
await callback.answer(t('trial_already_used', user.language))
@@ -363,7 +318,6 @@ async def trial_subscription_callback(callback: CallbackQuery, db: Database, **k
@router.callback_query(F.data == "confirm_trial")
async def confirm_trial_callback(callback: CallbackQuery, db: Database, **kwargs):
"""Confirm and create trial subscription - ДОБАВЛЕНА ПОДДЕРЖКА URL ИЗ API"""
user = kwargs.get('user')
api = kwargs.get('api')
config = kwargs.get('config')
@@ -377,7 +331,6 @@ async def confirm_trial_callback(callback: CallbackQuery, db: Database, **kwargs
return
try:
# Проверяем еще раз, не использовал ли пользователь тестовую подписку
has_used = await db.has_used_trial(user.telegram_id)
if has_used:
await callback.answer(t('trial_already_used', user.language))
@@ -391,7 +344,6 @@ async def confirm_trial_callback(callback: CallbackQuery, db: Database, **kwargs
)
return
# Создаем пользователя в RemnaWave для тестовой подписки
username = generate_username()
password = generate_password()
@@ -406,7 +358,6 @@ async def confirm_trial_callback(callback: CallbackQuery, db: Database, **kwargs
activeInternalSquads=[config.TRIAL_SQUAD_UUID]
)
# Обрабатываем ответ API
if remna_user:
if 'data' in remna_user and 'uuid' in remna_user['data']:
user_uuid = remna_user['data']['uuid']
@@ -423,7 +374,6 @@ async def confirm_trial_callback(callback: CallbackQuery, db: Database, **kwargs
return
if user_uuid:
# Если shortUuid не получен, запрашиваем его отдельно
if not short_uuid:
user_details = await api.get_user_by_uuid(user_uuid)
if user_details and 'shortUuid' in user_details:
@@ -453,7 +403,6 @@ async def confirm_trial_callback(callback: CallbackQuery, db: Database, **kwargs
)
return
# Создаем временную тестовую подписку
trial_subscription = await db.create_subscription(
name=f"Trial_{user.telegram_id}_{int(datetime.utcnow().timestamp())}",
description="Автоматически созданная тестовая подписка",
@@ -463,12 +412,10 @@ async def confirm_trial_callback(callback: CallbackQuery, db: Database, **kwargs
squad_uuid=config.TRIAL_SQUAD_UUID
)
# Помечаем подписку как тестовую И неактивную для админки
trial_subscription.is_trial = True
trial_subscription.is_active = False # Скрываем от обычных запросов
trial_subscription.is_active = False
await db.update_subscription(trial_subscription)
# Создаем пользовательскую подписку
expires_at = datetime.utcnow() + timedelta(days=config.TRIAL_DURATION_DAYS)
await db.create_user_subscription(
@@ -478,10 +425,8 @@ async def confirm_trial_callback(callback: CallbackQuery, db: Database, **kwargs
expires_at=expires_at
)
# Помечаем, что пользователь использовал тестовую подписку
await db.mark_trial_used(user.telegram_id)
# Создаем запись о платеже (бесплатном)
await db.create_payment(
user_id=user.telegram_id,
amount=0,
@@ -490,7 +435,6 @@ async def confirm_trial_callback(callback: CallbackQuery, db: Database, **kwargs
status='completed'
)
# НОВОЕ: Получаем subscription URL и показываем пользователю
success_text = t('trial_success', user.language)
try:
@@ -517,10 +461,8 @@ async def confirm_trial_callback(callback: CallbackQuery, db: Database, **kwargs
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
# Balance handlers
@router.callback_query(F.data == "change_language")
async def change_language_callback(callback: CallbackQuery, **kwargs):
"""Show language selection for changing language"""
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
@@ -533,7 +475,6 @@ async def change_language_callback(callback: CallbackQuery, **kwargs):
@router.callback_query(F.data == "balance")
async def balance_callback(callback: CallbackQuery, **kwargs):
"""Show balance menu"""
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
@@ -547,7 +488,6 @@ async def balance_callback(callback: CallbackQuery, **kwargs):
@router.callback_query(F.data == "topup_balance")
async def topup_balance_callback(callback: CallbackQuery, **kwargs):
"""Show top up options"""
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
@@ -560,7 +500,6 @@ async def topup_balance_callback(callback: CallbackQuery, **kwargs):
@router.callback_query(F.data == "topup_card")
async def topup_card_callback(callback: CallbackQuery, **kwargs):
"""Handle card payment"""
user = kwargs.get('user')
config = kwargs.get('config')
@@ -577,7 +516,6 @@ async def topup_card_callback(callback: CallbackQuery, **kwargs):
@router.callback_query(F.data == "topup_support")
async def topup_support_callback(callback: CallbackQuery, state: FSMContext, **kwargs):
"""Handle support payment"""
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
@@ -591,7 +529,6 @@ async def topup_support_callback(callback: CallbackQuery, state: FSMContext, **k
@router.message(StateFilter(BotStates.waiting_amount))
async def handle_amount(message: Message, state: FSMContext, db: Database, **kwargs):
"""Handle amount input"""
user = kwargs.get('user')
config = kwargs.get('config')
@@ -606,7 +543,6 @@ async def handle_amount(message: Message, state: FSMContext, db: Database, **kwa
return
try:
# Create payment record
payment = await db.create_payment(
user_id=user.telegram_id,
amount=amount,
@@ -616,7 +552,6 @@ async def handle_amount(message: Message, state: FSMContext, db: Database, **kwa
support_username = config.SUPPORT_USERNAME if config else 'support'
# Уведомляем админов о запросе на пополнение
if config and config.ADMIN_IDS:
admin_text = f"💰 Новый запрос на пополнение!\n\n"
admin_text += f"👤 Пользователь: {user.first_name or 'N/A'} (@{user.username or 'N/A'})\n"
@@ -624,7 +559,6 @@ async def handle_amount(message: Message, state: FSMContext, db: Database, **kwa
admin_text += f"💵 Сумма: {amount} руб.\n"
admin_text += f"📝 ID платежа: {payment.id}"
# Отправляем уведомление всем админам
from aiogram import Bot
bot = kwargs.get('bot')
if bot:
@@ -654,7 +588,6 @@ async def handle_amount(message: Message, state: FSMContext, db: Database, **kwa
@router.callback_query(F.data == "payment_history")
async def payment_history_callback(callback: CallbackQuery, db: Database, **kwargs):
"""Show payment history"""
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
@@ -685,10 +618,8 @@ async def payment_history_callback(callback: CallbackQuery, db: Database, **kwar
logger.error(f"Error getting payment history: {e}")
await callback.answer(t('error_occurred', user.language))
# Subscription handlers
@router.callback_query(F.data == "buy_subscription")
async def buy_subscription_callback(callback: CallbackQuery, db: Database, **kwargs):
"""Show available subscriptions (excluding trial)"""
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
@@ -704,7 +635,6 @@ async def buy_subscription_callback(callback: CallbackQuery, db: Database, **kwa
)
return
# Convert to dict format
sub_list = []
for sub in subscriptions:
sub_list.append({
@@ -723,7 +653,6 @@ async def buy_subscription_callback(callback: CallbackQuery, db: Database, **kwa
@router.callback_query(F.data.startswith("buy_sub_"))
async def buy_subscription_detail(callback: CallbackQuery, db: Database, **kwargs):
"""Show subscription details"""
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
@@ -762,7 +691,6 @@ async def buy_subscription_detail(callback: CallbackQuery, db: Database, **kwarg
@router.callback_query(F.data.startswith("confirm_buy_"))
async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
"""Confirm subscription purchase - ПОЛНОСТЬЮ ИСПРАВЛЕННАЯ ВЕРСИЯ"""
user = kwargs.get('user')
api = kwargs.get('api')
@@ -778,7 +706,6 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
await callback.answer("❌ Подписка не найдена")
return
# Check balance
if user.balance < subscription.price:
await callback.answer(t('insufficient_balance', user.language))
return
@@ -791,10 +718,8 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
)
return
# Показываем индикатор прогресса
await callback.answer("⏳ Создаю подписку...")
# Создаем нового пользователя в RemnaWave для каждой подписки
username = generate_username()
password = generate_password()
@@ -809,7 +734,6 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
activeInternalSquads=[subscription.squad_uuid]
)
# Handle API response
if remna_user:
if 'data' in remna_user and 'uuid' in remna_user['data']:
user_uuid = remna_user['data']['uuid']
@@ -826,7 +750,6 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
return
if user_uuid:
# Если shortUuid не получен, запрашиваем его отдельно
if not short_uuid:
try:
user_details = await api.get_user_by_uuid(user_uuid)
@@ -859,11 +782,9 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
)
return
# Deduct balance ТОЛЬКО после успешного создания пользователя
user.balance -= subscription.price
await db.update_user(user)
# Create user subscription record
expires_at = datetime.utcnow() + timedelta(days=subscription.duration_days)
user_subscription = await db.create_user_subscription(
@@ -873,12 +794,10 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
expires_at=expires_at
)
# Обновляем основного пользователя только если у него еще нет remnawave_uuid
if not user.remnawave_uuid:
user.remnawave_uuid = user_uuid
await db.update_user(user)
# Create payment record
payment = await db.create_payment(
user_id=user.telegram_id,
amount=-subscription.price,
@@ -887,18 +806,15 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
status='completed'
)
# ИСПРАВЛЕНО: убираем config из вызова process_referral_rewards
bot = kwargs.get('bot')
await process_referral_rewards(user.telegram_id, subscription.price, payment.id, db, bot)
# Формируем сообщение с URL из API
success_text = f"✅ Подписка успешно создана!\n\n"
success_text += f"📋 Подписка: {subscription.name}\n"
success_text += f"⏰ Действует до: {format_date(expires_at, user.language)}\n"
success_text += f"💰 Стоимость: {subscription.price} руб.\n"
success_text += f"💳 Остаток: {user.balance} руб.\n\n"
# Получаем subscription URL из API
try:
subscription_url = await api.get_subscription_url(short_uuid)
if subscription_url:
@@ -926,10 +842,8 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
# My subscriptions
@router.callback_query(F.data == "my_subscriptions")
async def my_subscriptions_callback(callback: CallbackQuery, db: Database, **kwargs):
"""Show user's subscriptions with URLs from API - ИСПРАВЛЕНО: пометки для импортированных"""
user = kwargs.get('user')
api = kwargs.get('api')
@@ -954,7 +868,6 @@ async def my_subscriptions_callback(callback: CallbackQuery, db: Database, **kwa
if not subscription:
continue
# Определяем статус
now = datetime.utcnow()
if user_sub.expires_at < now:
status = "❌ Истекла"
@@ -964,7 +877,6 @@ async def my_subscriptions_callback(callback: CallbackQuery, db: Database, **kwa
days_left = (user_sub.expires_at - now).days
status = f"✅ Активна ({days_left} дн.)"
# НОВОЕ: Помечаем импортированные подписки
subscription_name = subscription.name
if subscription.is_imported or subscription.name == "Старая подписка":
subscription_name += " 🔄" # Добавляем иконку импорта
@@ -973,7 +885,6 @@ async def my_subscriptions_callback(callback: CallbackQuery, db: Database, **kwa
text += f" {status}\n"
text += f" До: {format_date(user_sub.expires_at, user.language)}\n"
# НОВОЕ: Получаем URL из API
if user_sub.short_uuid and api:
try:
subscription_url = await api.get_subscription_url(user_sub.short_uuid)
@@ -987,11 +898,7 @@ async def my_subscriptions_callback(callback: CallbackQuery, db: Database, **kwa
text += "\n"
# НОВОЕ: Добавляем пояснение об иконках
text += "ℹ️ 🔄 - импортированная из старой системы\n"
text += " (продление недоступно)\n\n"
# Convert to old format for keyboard
sub_list = []
for user_sub in user_subs:
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
@@ -1018,7 +925,6 @@ async def my_subscriptions_callback(callback: CallbackQuery, db: Database, **kwa
@router.callback_query(F.data.startswith("view_sub_"))
async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwargs):
"""View subscription details with URL from API - ИСПРАВЛЕНО: блокировка продления импортированных подписок"""
user = kwargs.get('user')
api = kwargs.get('api')
@@ -1029,7 +935,6 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
try:
user_sub_id = int(callback.data.split("_")[2])
# Get user subscription
user_subs = await db.get_user_subscriptions(user.telegram_id)
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
@@ -1049,24 +954,20 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
'description': subscription.description or ''
}
# Check if subscription is expiring soon
now = datetime.utcnow()
days_until_expiry = (user_sub.expires_at - now).days
# ИСПРАВЛЕНО: Проверяем является ли подписка импортированной
is_imported = subscription.is_imported or subscription.price == 0
is_trial = subscription.is_trial
# Показываем кнопку продления только для обычных платных подписок
show_extend = (0 <= days_until_expiry <= 3 and
user_sub.is_active and
not is_trial and
not is_imported and # НОВОЕ: блокируем продление импортированных
subscription.price > 0) # НОВОЕ: блокируем продление бесплатных
not is_imported and
subscription.price > 0)
text = format_user_subscription_info(user_sub.__dict__, sub_dict, user_sub.expires_at, user.language)
# НОВОЕ: Добавляем URL из API в детальный просмотр
if user_sub.short_uuid and api:
try:
subscription_url = await api.get_subscription_url(user_sub.short_uuid)
@@ -1075,7 +976,6 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
except Exception as e:
logger.warning(f"Could not get subscription URL: {e}")
# ИСПРАВЛЕНО: Разные сообщения для разных типов подписок
if is_imported and 0 <= days_until_expiry <= 3:
text += f"\n\n⚠️ Это импортированная подписка из старой системы.\n"
text += f"📅 Истекает через {days_until_expiry} дн.\n"
@@ -1120,7 +1020,6 @@ async def extend_subscription_callback(callback: CallbackQuery, db: Database, **
await callback.answer(t('subscription_not_found', user.language))
return
# НОВОЕ: Проверяем тип подписки
if subscription.is_trial:
await callback.answer("❌ Тестовую подписку нельзя продлить")
return
@@ -1153,7 +1052,6 @@ async def extend_subscription_callback(callback: CallbackQuery, db: Database, **
)
return
# Show confirmation
text = f"🔄 Продление подписки\n\n"
text += f"📋 Подписка: {subscription.name}\n"
text += f"💰 Стоимость: {subscription.price} руб.\n"
@@ -1172,7 +1070,6 @@ async def extend_subscription_callback(callback: CallbackQuery, db: Database, **
@router.callback_query(F.data.startswith("confirm_extend_"))
async def confirm_extend_subscription_callback(callback: CallbackQuery, db: Database, **kwargs):
"""Confirm subscription extension - ДОБАВЛЕНА ПОДДЕРЖКА URL ИЗ API"""
user = kwargs.get('user')
api = kwargs.get('api')
@@ -1203,7 +1100,6 @@ async def confirm_extend_subscription_callback(callback: CallbackQuery, db: Data
await callback.answer("❌ Недостаточно средств")
return
# Calculate new expiry date
now = datetime.utcnow()
if user_sub.expires_at > now:
@@ -1211,7 +1107,6 @@ async def confirm_extend_subscription_callback(callback: CallbackQuery, db: Data
else:
new_expiry = now + timedelta(days=subscription.duration_days)
# Update in RemnaWave
if api and user_sub.short_uuid:
try:
logger.info(f"Updating RemnaWave subscription for shortUuid: {user_sub.short_uuid}")
@@ -1232,7 +1127,6 @@ async def confirm_extend_subscription_callback(callback: CallbackQuery, db: Data
result = await api.update_user(user_uuid, update_data)
if not result:
# Try alternative field name
update_data['expiryTime'] = expiry_str
result = await api.update_user(user_uuid, update_data)
@@ -1253,16 +1147,13 @@ async def confirm_extend_subscription_callback(callback: CallbackQuery, db: Data
except Exception as e:
logger.error(f"Failed to update expiry in RemnaWave: {e}")
# Update local database
user_sub.expires_at = new_expiry
user_sub.is_active = True
await db.update_user_subscription(user_sub)
# Deduct balance
user.balance -= subscription.price
await db.update_user(user)
# Create payment record
await db.create_payment(
user_id=user.telegram_id,
amount=-subscription.price,
@@ -1277,7 +1168,6 @@ async def confirm_extend_subscription_callback(callback: CallbackQuery, db: Data
success_text += f"💰 Списано: {subscription.price} руб.\n"
success_text += f"💳 Остаток на балансе: {user.balance} руб."
# НОВОЕ: Получаем обновленный URL из API
if api and user_sub.short_uuid:
try:
subscription_url = await api.get_subscription_url(user_sub.short_uuid)
@@ -1308,7 +1198,6 @@ async def confirm_extend_subscription_callback(callback: CallbackQuery, db: Data
@router.callback_query(F.data.startswith("get_connection_"))
async def get_connection_callback(callback: CallbackQuery, db: Database, **kwargs):
"""Get connection link from API - ПОЛНОСТЬЮ ПЕРЕРАБОТАН"""
user = kwargs.get('user')
api = kwargs.get('api')
@@ -1353,7 +1242,6 @@ async def get_connection_callback(callback: CallbackQuery, db: Database, **kwarg
text += f"3. Добавьте конфигурацию по ссылке\n\n"
text += f"💡 Или нажмите кнопку ниже для автоматического подключения"
# Создаем клавиатуру с кнопкой подключения
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🚀 Подключиться автоматически", url=connection_url)],
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")],
@@ -1370,10 +1258,8 @@ async def get_connection_callback(callback: CallbackQuery, db: Database, **kwarg
logger.error(f"Error getting connection link: {e}")
await callback.answer(t('error_occurred', user.language))
# Support и Promocode handlers
@router.callback_query(F.data == "support")
async def support_callback(callback: CallbackQuery, **kwargs):
"""Show support info"""
user = kwargs.get('user')
config = kwargs.get('config')
@@ -1391,7 +1277,6 @@ async def support_callback(callback: CallbackQuery, **kwargs):
@router.callback_query(F.data == "promocode")
async def promocode_callback(callback: CallbackQuery, state: FSMContext, **kwargs):
"""Handle promocode input"""
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
@@ -1405,7 +1290,6 @@ async def promocode_callback(callback: CallbackQuery, state: FSMContext, **kwarg
@router.message(StateFilter(BotStates.waiting_promocode))
async def handle_promocode(message: Message, state: FSMContext, db: Database, **kwargs):
"""Handle promocode input with referral support and mutual protection"""
user = kwargs.get('user')
if not user:
await message.answer("❌ Ошибка пользователя")
@@ -1418,11 +1302,9 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
return
try:
# Сначала проверяем обычные промокоды
promocode = await db.get_promocode_by_code(code)
if promocode and promocode.is_active:
# Проверяем условия промокода
if promocode.expires_at and promocode.expires_at < datetime.utcnow():
await message.answer(t('promocode_expired', user.language))
return
@@ -1431,17 +1313,14 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
await message.answer(t('promocode_limit', user.language))
return
# Используем промокод
success = await db.use_promocode(user.telegram_id, promocode)
if not success:
await message.answer(t('promocode_used', user.language))
return
# Добавляем на баланс
await db.add_balance(user.telegram_id, promocode.discount_amount)
# Создаем запись о платеже
await db.create_payment(
user_id=user.telegram_id,
amount=promocode.discount_amount,
@@ -1460,11 +1339,9 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
log_user_action(user.telegram_id, "promocode_used", code)
return
# Если обычный промокод не найден, проверяем реферальные коды
if code.startswith("REF"):
bot = kwargs.get('bot')
# Сначала проверяем на взаимное реферальство
async with db.session_factory() as session:
from sqlalchemy import select
result = await session.execute(
@@ -1475,7 +1352,6 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
if referral_record:
referrer_id = referral_record.referrer_id
# Проверяем взаимное реферальство
existing_reverse_referral = await db.get_referral_by_referred_id(referrer_id)
if existing_reverse_referral and existing_reverse_referral.referrer_id == user.telegram_id:
await message.answer(
@@ -1497,7 +1373,6 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
log_user_action(user.telegram_id, "referral_code_used", code)
return
else:
# Проверяем причину неудачи
existing_referral = await db.get_referral_by_referred_id(user.telegram_id)
if existing_referral:
await message.answer("❌ Вы уже использовали реферальный код!")
@@ -1505,7 +1380,6 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
await message.answer("❌ Неверный реферальный код!")
return
# Если ничего не найдено
await message.answer(t('promocode_not_found', user.language))
except Exception as e:
@@ -1519,7 +1393,6 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
@router.callback_query(F.data == "referral_program")
async def referral_program_callback(callback: CallbackQuery, db: Database, **kwargs):
"""Show referral program info - ФИНАЛЬНАЯ ВЕРСИЯ с конфигурацией"""
user = kwargs.get('user')
config = kwargs.get('config')
@@ -1528,27 +1401,22 @@ async def referral_program_callback(callback: CallbackQuery, db: Database, **kwa
return
try:
# Получаем статистику пользователя
stats = await db.get_user_referral_stats(user.telegram_id)
referral_code = await get_or_create_referral_code(user.telegram_id, db)
# Создаем реферальную ссылку
bot_username = config.BOT_USERNAME if config and config.BOT_USERNAME else ""
referral_link = ""
if bot_username:
referral_link = f"https://t.me/{bot_username}?start=ref_{user.telegram_id}"
# Добавляем текущее время для обновления
from datetime import datetime
current_time = datetime.now().strftime("%H:%M")
text = "🎁 **Реферальная программа**\n\n"
# Условия программы - ИСПРАВЛЕНО: используем значения из конфигурации
text += "**📋 Условия программы:**\n"
# Получаем значения из конфигурации с fallback
first_reward = config.REFERRAL_FIRST_REWARD if config else 150.0
referred_bonus = config.REFERRAL_REFERRED_BONUS if config else 150.0
threshold = config.REFERRAL_THRESHOLD if config else 300.0
@@ -1558,13 +1426,11 @@ async def referral_program_callback(callback: CallbackQuery, db: Database, **kwa
text += f"• Твой друг получит **{referred_bonus:.0f}₽** после пополнения на {threshold:.0f}\n"
text += f"• С каждого платежа друга ты получаешь **{percentage*100:.0f}%**\n\n"
# Статистика пользователя
text += "**📊 Твоя статистика:**\n"
text += f"• Приглашено: {stats['total_referrals']} человек\n"
text += f"• Активных рефералов: {stats['active_referrals']}\n"
text += f"• Заработано всего: {stats['total_earned']:.2f}\n\n"
# Ссылка и промокод
if referral_link:
text += "**🔗 Твоя реферальная ссылка:**\n"
text += f"`{referral_link}`\n\n"
@@ -1574,7 +1440,6 @@ async def referral_program_callback(callback: CallbackQuery, db: Database, **kwa
text += f"**🎫 Твой промокод:** `{referral_code}`\n\n"
text += "Отправь ссылку или промокод друзьям!"
# Добавляем время обновления чтобы избежать ошибки "message is not modified"
text += f"\n\n🕐 _Обновлено: {current_time}_"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
@@ -1591,19 +1456,16 @@ async def referral_program_callback(callback: CallbackQuery, db: Database, **kwa
except Exception as e:
logger.error(f"Error showing referral program: {e}")
# Если не можем отредактировать сообщение, просто отвечаем на callback
try:
await callback.answer("✅ Статистика обновлена", show_alert=False)
except:
pass
async def get_or_create_referral_code(user_id: int, db: Database) -> str:
"""Get existing referral code or create new one - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
try:
async with db.session_factory() as session:
from sqlalchemy import select, text
# Ищем код в любой записи где пользователь - реферер
result = await session.execute(
text("SELECT referral_code FROM referral_programs WHERE referrer_id = :user_id LIMIT 1"),
{"user_id": user_id}
@@ -1615,10 +1477,8 @@ async def get_or_create_referral_code(user_id: int, db: Database) -> str:
logger.info(f"Found existing referral code {existing_code} for user {user_id}")
return existing_code
# Если кода нет, генерируем новый
referral_code = await db.generate_unique_referral_code(user_id)
# Создаем запись для сохранения кода
referral = await db.create_referral(user_id, 0, referral_code)
if referral:
@@ -1648,7 +1508,6 @@ async def my_referrals_callback(callback: CallbackQuery, db: Database, **kwargs)
real_referrals = []
for referral in referrals:
# Пропускаем только конкретный placeholder этого пользователя
if referral.referred_id == placeholder_id or referral.referred_id == 0:
continue
@@ -1662,12 +1521,10 @@ async def my_referrals_callback(callback: CallbackQuery, db: Database, **kwargs)
threshold = config.REFERRAL_THRESHOLD if config else 300.0
for i, referral in enumerate(real_referrals[:10], 1): # Показываем первых 10
# Получаем полную информацию о реферале
for i, referral in enumerate(real_referrals[:10], 1):
referred_user = await db.get_user_by_telegram_id(referral.referred_id)
if referred_user:
# Формируем красивое отображение имени
display_name = ""
if referred_user.first_name:
display_name = referred_user.first_name
@@ -1684,14 +1541,11 @@ async def my_referrals_callback(callback: CallbackQuery, db: Database, **kwargs)
display_name = f"Пользователь #{referred_user.telegram_id}"
else:
# Если пользователь не найден в базе бота
display_name = f"Пользователь ID:{referral.referred_id}"
# Статус реферала
status_icon = "" if referral.first_reward_paid else ""
status_text = "Активен" if referral.first_reward_paid else "Ожидает активации"
# Сумма заработка
earned_text = ""
if referral.total_earned > 0:
earned_text = f" (+{referral.total_earned:.0f}₽)"
+146 -69
View File
@@ -4,7 +4,6 @@ from typing import List, Optional, Dict
from translations import t
def language_keyboard() -> InlineKeyboardMarkup:
"""Language selection keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="🇷🇺 Русский", callback_data="lang_ru"),
@@ -13,45 +12,38 @@ def language_keyboard() -> InlineKeyboardMarkup:
])
return keyboard
def main_menu_keyboard(lang: str = 'ru', is_admin: bool = False, show_trial: bool = False) -> InlineKeyboardMarkup:
"""Beautiful main menu keyboard with emojis and better layout"""
def main_menu_keyboard(lang: str = 'ru', is_admin: bool = False, show_trial: bool = False, show_lucky_game: bool = True) -> InlineKeyboardMarkup:
buttons = [
# Первый ряд - основные функции
[
InlineKeyboardButton(text="💰 " + t('balance', lang), callback_data="balance"),
InlineKeyboardButton(text="📋 " + t('my_subscriptions', lang), callback_data="my_subscriptions")
],
# Второй ряд - покупка подписки (выделена отдельно как главная функция)
[InlineKeyboardButton(text="🛒 " + t('buy_subscription', lang), callback_data="buy_subscription")],
]
# Добавляем тестовую подписку если доступна
if show_trial:
buttons.insert(1, [InlineKeyboardButton(text="🆓 Тестовая подписка", callback_data="trial_subscription")])
# Добавляем остальные кнопки
if show_lucky_game:
buttons.append([InlineKeyboardButton(text="🎰 Проверь свою удачу!", callback_data="lucky_game")])
buttons.extend([
# Реферальная программа и дополнительные функции
[
InlineKeyboardButton(text="🎁 " + t('promocode', lang), callback_data="promocode"),
InlineKeyboardButton(text="👥 Рефералы", callback_data="referral_program") # НОВАЯ КНОПКА
InlineKeyboardButton(text="👥 Рефералы", callback_data="referral_program")
],
[
InlineKeyboardButton(text="💬 " + t('support', lang), callback_data="support")
],
# Последний ряд - настройки
[InlineKeyboardButton(text="🌐 " + t('change_language', lang), callback_data="change_language")]
])
# Добавляем админ панель если пользователь админ
if is_admin:
buttons.append([InlineKeyboardButton(text="⚙️ " + t('admin_panel', lang), callback_data="admin_panel")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
# Остальные функции остаются прежними...
def balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful balance menu keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💳 " + t('topup_balance', lang), callback_data="topup_balance")],
[InlineKeyboardButton(text="📊 " + t('payment_history', lang), callback_data="payment_history")],
@@ -60,7 +52,6 @@ def balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return keyboard
def topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful top up balance keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
# [InlineKeyboardButton(text="💳 " + t('topup_card', lang), callback_data="topup_card")],
[InlineKeyboardButton(text="👨‍💼 " + t('topup_support', lang), callback_data="topup_support")],
@@ -69,17 +60,14 @@ def topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return keyboard
def subscriptions_keyboard(subscriptions: List[dict], lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful available subscriptions keyboard"""
buttons = []
# Группируем подписки по две в ряд для компактности
for i in range(0, len(subscriptions), 2):
row = []
for j in range(2):
if i + j < len(subscriptions):
sub = subscriptions[i + j]
price_text = f"{sub['price']:.0f}"
# Используем разные эмодзи для разных ценовых категорий
if sub['price'] <= 100:
emoji = "🥉"
elif sub['price'] <= 300:
@@ -97,7 +85,6 @@ def subscriptions_keyboard(subscriptions: List[dict], lang: str = 'ru') -> Inlin
return InlineKeyboardMarkup(inline_keyboard=buttons)
def subscription_detail_keyboard(subscription_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful subscription detail keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text="💎 " + t('buy_subscription_btn', lang, price=0),
@@ -108,47 +95,37 @@ def subscription_detail_keyboard(subscription_id: int, lang: str = 'ru') -> Inli
return keyboard
def user_subscriptions_keyboard(user_subscriptions: List[dict], lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful user's subscriptions keyboard"""
buttons = []
for sub in user_subscriptions:
# Добавляем эмодзи статуса для каждой подписки
buttons.append([InlineKeyboardButton(
text=f"📱 {sub['name']}",
callback_data=f"view_sub_{sub['id']}"
)])
if not user_subscriptions:
# Если нет подписок, показываем кнопку покупки
buttons.append([InlineKeyboardButton(text="🛒 " + t('buy_subscription', lang), callback_data="buy_subscription")])
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def user_subscription_detail_keyboard(subscription_id: int, lang: str = 'ru', show_extend: bool = False, is_imported: bool = False) -> InlineKeyboardMarkup:
"""Beautiful user's subscription detail keyboard with connection and optional extend button - ИСПРАВЛЕНО"""
buttons = []
# НОВОЕ: Разные кнопки в зависимости от типа подписки
if is_imported:
# Для импортированных подписок - только ссылка подключения и покупка новой
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
buttons.append([InlineKeyboardButton(text="🛒 Купить новую подписку", callback_data="buy_subscription")])
else:
# Для обычных подписок - кнопка продления если нужна
if show_extend:
buttons.append([InlineKeyboardButton(text="" + t('extend_subscription', lang), callback_data=f"extend_sub_{subscription_id}")])
# Connection button (главная кнопка)
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
# Back button
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="my_subscriptions")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def extend_subscription_keyboard(subscription_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful extend subscription confirmation keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да, продлить", callback_data=f"confirm_extend_{subscription_id}"),
@@ -158,35 +135,28 @@ def extend_subscription_keyboard(subscription_id: int, lang: str = 'ru') -> Inli
return keyboard
def back_keyboard(callback_data: str, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful back button keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data=callback_data)]
])
return keyboard
def cancel_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful cancel button keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="" + t('cancel', lang), callback_data="main_menu")]
])
return keyboard
# Admin keyboards
def admin_menu_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin menu keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
# Первый ряд - управление контентом
[
InlineKeyboardButton(text="📦 " + t('manage_subscriptions', lang), callback_data="admin_subscriptions"),
InlineKeyboardButton(text="👥 " + t('manage_users', lang), callback_data="admin_users")
],
# Второй ряд - финансы
[
InlineKeyboardButton(text="💰 " + t('manage_balance', lang), callback_data="admin_balance"),
InlineKeyboardButton(text="🎁 " + t('manage_promocodes', lang), callback_data="admin_promocodes")
],
# Третий ряд - коммуникации и система
[
InlineKeyboardButton(text="📨 " + t('send_message', lang), callback_data="admin_messages"),
InlineKeyboardButton(text="👥 Рефералы", callback_data="admin_referrals") # НОВАЯ КНОПКА
@@ -194,18 +164,15 @@ def admin_menu_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
[
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system")
],
# Четвертый ряд - мониторинг и статистика
[
InlineKeyboardButton(text="🔍 Мониторинг подписок", callback_data="admin_monitor"),
InlineKeyboardButton(text="📊 " + t('statistics', lang), callback_data="admin_stats")
],
# Назад
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")]
])
return keyboard
def admin_subscriptions_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin subscriptions management keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=" " + t('create_subscription', lang), callback_data="create_subscription")],
[InlineKeyboardButton(text="📋 Список подписок", callback_data="list_admin_subscriptions")],
@@ -214,7 +181,6 @@ def admin_subscriptions_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return keyboard
def admin_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin users management keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="👥 Список пользователей", callback_data="list_users")],
[InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="search_user")],
@@ -223,7 +189,6 @@ def admin_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return keyboard
def admin_balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin balance management keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💸 Пополнить баланс пользователю", callback_data="admin_add_balance")],
[InlineKeyboardButton(text="📊 История платежей", callback_data="admin_payment_history")],
@@ -232,16 +197,115 @@ def admin_balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return keyboard
def admin_promocodes_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin promocodes management keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=" " + t('create_promocode', lang), callback_data="create_promocode")],
[InlineKeyboardButton(text="📋 Список промокодов", callback_data="list_promocodes")],
[InlineKeyboardButton(text="📋 Управление промокодами", callback_data="list_promocodes")],
[InlineKeyboardButton(text="📊 Статистика промокодов", callback_data="promocodes_stats")],
[InlineKeyboardButton(text="🧹 Очистить истекшие", callback_data="cleanup_expired_promos")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def promocodes_management_keyboard(promocodes: List, lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
for promo in promocodes[:15]:
status_emoji = "🟢" if promo.is_active else "🔴"
if promo.expires_at and promo.expires_at < datetime.utcnow():
status_emoji = ""
usage_text = f"{promo.used_count}/{promo.usage_limit}"
button_text = f"{status_emoji} {promo.code} ({promo.discount_amount}₽) [{usage_text}]"
buttons.append([
InlineKeyboardButton(
text=button_text,
callback_data=f"promo_info_{promo.id}"
)
])
control_buttons = []
if not promo.code.startswith('REF'):
toggle_text = "🔴" if promo.is_active else "🟢"
control_buttons.append(
InlineKeyboardButton(text=toggle_text, callback_data=f"toggle_promo_{promo.id}")
)
control_buttons.append(
InlineKeyboardButton(text="✏️", callback_data=f"edit_promo_{promo.id}")
)
control_buttons.append(
InlineKeyboardButton(text="🗑", callback_data=f"delete_promo_{promo.id}")
)
else:
control_buttons.append(
InlineKeyboardButton(text="👥 Реферальный", callback_data="noop")
)
if control_buttons:
buttons.append(control_buttons)
buttons.append([
InlineKeyboardButton(text=" Создать промокод", callback_data="create_promocode")
])
buttons.append([
InlineKeyboardButton(text="🧹 Очистить истекшие", callback_data="cleanup_expired_promos"),
InlineKeyboardButton(text="📊 Статистика", callback_data="promocodes_stats")
])
buttons.append([
InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_promocodes")
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def promocode_edit_keyboard(promo_id: int, language: str = 'ru') -> InlineKeyboardMarkup:
buttons = [
[InlineKeyboardButton(text="💰 Изменить скидку", callback_data=f"edit_promo_field_{promo_id}_discount")],
[InlineKeyboardButton(text="📊 Изменить лимит", callback_data=f"edit_promo_field_{promo_id}_limit")],
[InlineKeyboardButton(text="⏰ Изменить срок", callback_data=f"edit_promo_field_{promo_id}_expiry")],
[InlineKeyboardButton(text="🗑 Удалить промокод", callback_data=f"delete_promo_{promo_id}")],
[InlineKeyboardButton(text=t('back', language), callback_data="list_promocodes")]
]
return InlineKeyboardMarkup(inline_keyboard=buttons)
def promocode_info_keyboard(promo_id: int, is_referral: bool = False, lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
if not is_referral:
buttons.extend([
[InlineKeyboardButton(text="✏️ Редактировать", callback_data=f"edit_promo_{promo_id}")],
[
InlineKeyboardButton(text="🟢/🔴 Переключить", callback_data=f"toggle_promo_{promo_id}"),
InlineKeyboardButton(text="🗑 Удалить", callback_data=f"delete_promo_{promo_id}")
]
])
else:
buttons.append([
InlineKeyboardButton(text="👥 Реферальный код", callback_data="noop")
])
buttons.append([
InlineKeyboardButton(text="🔙 К списку", callback_data="list_promocodes")
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def bulk_promocodes_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = [
[InlineKeyboardButton(text="🧹 Удалить все истекшие", callback_data="confirm_cleanup_expired")],
[InlineKeyboardButton(text="🔴 Деактивировать все", callback_data="confirm_deactivate_all")],
[InlineKeyboardButton(text="📊 Экспорт статистики", callback_data="export_promo_stats")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="list_promocodes")]
]
return InlineKeyboardMarkup(inline_keyboard=buttons)
def confirmation_keyboard(confirm_callback: str, cancel_callback: str, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful confirmation keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да", callback_data=confirm_callback),
@@ -251,7 +315,6 @@ def confirmation_keyboard(confirm_callback: str, cancel_callback: str, lang: str
return keyboard
def pagination_keyboard(page: int, total_pages: int, prefix: str, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful pagination keyboard"""
buttons = []
if total_pages > 1:
@@ -270,13 +333,11 @@ def pagination_keyboard(page: int, total_pages: int, prefix: str, lang: str = 'r
return InlineKeyboardMarkup(inline_keyboard=buttons)
def admin_subscriptions_list_keyboard(subs: List[Subscription], lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful keyboard for admin subscriptions list with enhanced controls"""
buttons = []
for sub in subs:
status_emoji = "🟢" if sub.is_active else "🔴"
price = f"{sub.price:.0f}"
# Основная кнопка с информацией о подписке
buttons.append([
InlineKeyboardButton(
text=f"{status_emoji} {sub.name}{price}",
@@ -284,7 +345,6 @@ def admin_subscriptions_list_keyboard(subs: List[Subscription], lang: str = 'ru'
)
])
# Кнопки управления для каждой подписки в одну строку
control_buttons = [
InlineKeyboardButton(text="✏️", callback_data=f"edit_sub_{sub.id}"),
InlineKeyboardButton(
@@ -296,13 +356,11 @@ def admin_subscriptions_list_keyboard(subs: List[Subscription], lang: str = 'ru'
buttons.append(control_buttons)
# Кнопка создания новой подписки
buttons.append([InlineKeyboardButton(text=" Создать подписку", callback_data="create_subscription")])
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_subscriptions")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def admin_payment_keyboard(payment_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful keyboard for admin payment approval"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Одобрить платеж", callback_data=f"approve_payment_{payment_id}"),
@@ -312,7 +370,6 @@ def admin_payment_keyboard(payment_id: int, lang: str = 'ru') -> InlineKeyboardM
return keyboard
def admin_messages_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin messages management keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="👤 " + t('send_to_user', lang), callback_data="admin_send_to_user")],
[InlineKeyboardButton(text="📢 " + t('send_to_all', lang), callback_data="admin_send_to_all")],
@@ -321,7 +378,6 @@ def admin_messages_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return keyboard
def quick_topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Quick topup amounts keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="💳 100₽", callback_data="quick_topup_100"),
@@ -337,7 +393,6 @@ def quick_topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return keyboard
def connection_keyboard(subscription_url: str, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful connection keyboard with web app"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔗 Подключиться", url=subscription_url)],
[InlineKeyboardButton(text="📱 Инструкция", callback_data="connection_guide")],
@@ -346,7 +401,6 @@ def connection_keyboard(subscription_url: str, lang: str = 'ru') -> InlineKeyboa
return keyboard
def trial_subscription_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Trial subscription confirmation keyboard"""
buttons = [
[InlineKeyboardButton(text="✅ Получить тестовую подписку", callback_data="confirm_trial")],
[InlineKeyboardButton(text=t('back', lang), callback_data="main_menu")]
@@ -354,7 +408,6 @@ def trial_subscription_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=buttons)
def admin_monitor_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin monitor management keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📊 Статус сервиса", callback_data="monitor_status")],
[InlineKeyboardButton(text="🔄 Принудительная проверка", callback_data="monitor_force_check")],
@@ -365,7 +418,6 @@ def admin_monitor_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return keyboard
def admin_system_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Beautiful admin system management keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📊 Системная статистика", callback_data="system_stats")],
[InlineKeyboardButton(text="🖥 Управление нодами", callback_data="nodes_management")],
@@ -377,7 +429,6 @@ def admin_system_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return keyboard
def system_stats_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""System statistics keyboard with refresh"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Обновить статистику", callback_data="refresh_system_stats")],
[InlineKeyboardButton(text="🖥 Ноды", callback_data="nodes_management")],
@@ -387,11 +438,9 @@ def system_stats_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return keyboard
def nodes_management_keyboard(nodes: List[Dict], lang: str = 'ru', timestamp: int = None) -> InlineKeyboardMarkup:
"""Improved nodes management keyboard"""
buttons = []
if nodes:
# Statistics row
online_count = len([n for n in nodes if n.get('status') == 'online'])
total_count = len(nodes)
@@ -402,11 +451,9 @@ def nodes_management_keyboard(nodes: List[Dict], lang: str = 'ru', timestamp: in
)
])
# Show first 5 nodes with improved display
for i, node in enumerate(nodes[:5]):
status = node.get('status', 'unknown')
# Status emoji based on actual status
if status == 'online':
status_emoji = "🟢"
elif status == 'disabled':
@@ -421,13 +468,11 @@ def nodes_management_keyboard(nodes: List[Dict], lang: str = 'ru', timestamp: in
node_name = node.get('name', f'Node-{i+1}')
node_id = node.get('id', node.get('uuid'))
# Truncate long names
if len(node_name) > 20:
display_name = node_name[:17] + "..."
else:
display_name = node_name
# CPU/Memory usage if available
usage_info = ""
if node.get('cpuUsage'):
usage_info += f" CPU:{node['cpuUsage']:.0f}%"
@@ -464,19 +509,16 @@ def nodes_management_keyboard(nodes: List[Dict], lang: str = 'ru', timestamp: in
)
])
# Action buttons
buttons.append([
InlineKeyboardButton(text="🔄 Перезагрузить все", callback_data="restart_all_nodes"),
InlineKeyboardButton(text="📊 Статистика", callback_data="nodes_statistics")
])
# Refresh button
refresh_callback = f"refresh_nodes_stats_{timestamp}" if timestamp else "refresh_nodes_stats"
buttons.append([
InlineKeyboardButton(text="🔄 Обновить", callback_data=refresh_callback)
])
# Back button
buttons.append([
InlineKeyboardButton(text="🔙 Назад", callback_data="admin_system")
])
@@ -484,7 +526,6 @@ def nodes_management_keyboard(nodes: List[Dict], lang: str = 'ru', timestamp: in
return InlineKeyboardMarkup(inline_keyboard=buttons)
def system_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""System users management keyboard - ИСПРАВЛЕНО"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📊 Статистика пользователей", callback_data="users_statistics")],
[InlineKeyboardButton(text="👥 Список всех пользователей", callback_data="list_all_system_users")],
@@ -495,7 +536,6 @@ def system_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return keyboard
def bulk_operations_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Bulk operations keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Сбросить трафик", callback_data="bulk_reset_traffic")],
[InlineKeyboardButton(text="❌ Отключить пользователей", callback_data="bulk_disable_users")],
@@ -506,7 +546,6 @@ def bulk_operations_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return keyboard
def confirm_restart_keyboard(node_id: str = None, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Confirmation keyboard for node restart"""
action = f"confirm_restart_node_{node_id}" if node_id else "confirm_restart_all_nodes"
back_action = f"node_details_{node_id}" if node_id else "nodes_management"
@@ -519,7 +558,6 @@ def confirm_restart_keyboard(node_id: str = None, lang: str = 'ru') -> InlineKey
return keyboard
def admin_referrals_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Admin referral management keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📊 Статистика рефералов", callback_data="referral_statistics")],
[InlineKeyboardButton(text="👥 Список рефереров", callback_data="list_referrers")],
@@ -528,3 +566,42 @@ def admin_referrals_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def lucky_game_keyboard(can_play: bool, time_left_str: str = "", lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
if can_play:
buttons.append([InlineKeyboardButton(text="🎲 Играть!", callback_data="start_lucky_game")])
else:
buttons.append([InlineKeyboardButton(text=f"⏰ Приходи через {time_left_str}", callback_data="noop")])
buttons.extend([
[InlineKeyboardButton(text="📈 История игр", callback_data="lucky_game_history")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")]
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def lucky_numbers_keyboard(numbers_count: int) -> InlineKeyboardMarkup:
buttons = []
for i in range(0, numbers_count, 5):
row = []
for j in range(5):
if i + j + 1 <= numbers_count:
number = i + j + 1
row.append(InlineKeyboardButton(
text=str(number),
callback_data=f"choose_number_{number}"
))
buttons.append(row)
buttons.append([InlineKeyboardButton(text="❌ Отмена", callback_data="lucky_game")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def lucky_game_result_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📈 История игр", callback_data="lucky_game_history")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
+349
View File
@@ -0,0 +1,349 @@
import asyncio
from datetime import datetime, timedelta, timezone
import logging
import random
from typing import Optional, List
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.filters import StateFilter
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from database import Database, User
from keyboards import back_keyboard
from translations import t
from utils import log_user_action, format_datetime
logger = logging.getLogger(__name__)
class LuckyGameStates(StatesGroup):
waiting_number_choice = State()
lucky_game_router = Router()
@lucky_game_router.callback_query(F.data == "lucky_game")
async def lucky_game_menu_callback(callback: CallbackQuery, db: Database, **kwargs):
"""Главное меню игры удачи"""
user = kwargs.get('user')
config = kwargs.get('config')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
# Получаем настройки игры из конфига
reward_amount = getattr(config, 'LUCKY_GAME_REWARD', 50.0)
numbers_count = getattr(config, 'LUCKY_GAME_NUMBERS', 30)
winning_numbers = getattr(config, 'LUCKY_GAME_WINNING_COUNT', 3)
# Проверяем, можно ли играть сегодня
can_play, next_game_time = await check_can_play_today(db, user.telegram_id)
# Получаем статистику игр пользователя
games_played, total_won, win_count = await get_user_game_stats(db, user.telegram_id)
text = "🎰 **Проверь свою удачу!**\n\n"
text += "🎯 **Как играть:**\n"
text += f"• Выберите число от 1 до {numbers_count}\n"
text += f"• Из {numbers_count} чисел {winning_numbers} - выигрышные\n"
text += f"• Угадали - получаете {reward_amount:.0f}₽ на баланс!\n"
text += "• Играть можно 1 раз в сутки\n\n"
text += "📊 **Ваша статистика:**\n"
text += f"• Игр сыграно: {games_played}\n"
text += f"• Выигрышей: {win_count}\n"
text += f"• Всего выиграно: {total_won:.0f}\n"
if games_played > 0:
win_rate = (win_count / games_played) * 100
text += f"• Процент побед: {win_rate:.1f}%\n"
# Создаем клавиатуру
buttons = []
if can_play:
buttons.append([InlineKeyboardButton(text="🎲 Играть!", callback_data="start_lucky_game")])
else:
# Вычисляем оставшееся время
now = datetime.utcnow()
time_left = next_game_time - now
hours_left = int(time_left.total_seconds() // 3600)
minutes_left = int((time_left.total_seconds() % 3600) // 60)
if hours_left > 0:
time_text = f"{hours_left}ч {minutes_left}м"
else:
time_text = f"{minutes_left}м"
buttons.append([InlineKeyboardButton(text=f"⏰ Приходи через {time_text}", callback_data="noop")])
buttons.extend([
[InlineKeyboardButton(text="📈 История игр", callback_data="lucky_game_history")],
[InlineKeyboardButton(text="🔙 " + t('back', user.language), callback_data="main_menu")]
])
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
await callback.message.edit_text(
text,
reply_markup=keyboard,
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error in lucky game menu: {e}")
await callback.answer("❌ Ошибка загрузки игры")
@lucky_game_router.callback_query(F.data == "start_lucky_game")
async def start_lucky_game_callback(callback: CallbackQuery, db: Database, state: FSMContext, **kwargs):
"""Начать игру - показать числа для выбора"""
user = kwargs.get('user')
config = kwargs.get('config')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
# Проверяем, можно ли играть
can_play, next_game_time = await check_can_play_today(db, user.telegram_id)
if not can_play:
await callback.answer("⏰ Вы уже играли сегодня! Приходите завтра.", show_alert=True)
return
# Получаем настройки
numbers_count = getattr(config, 'LUCKY_GAME_NUMBERS', 30)
winning_numbers = getattr(config, 'LUCKY_GAME_WINNING_COUNT', 3)
reward_amount = getattr(config, 'LUCKY_GAME_REWARD', 50.0)
# Генерируем выигрышные числа заранее и сохраняем в состоянии
winning_nums = random.sample(range(1, numbers_count + 1), winning_numbers)
await state.update_data(
winning_numbers=winning_nums,
reward_amount=reward_amount,
numbers_count=numbers_count
)
await state.set_state(LuckyGameStates.waiting_number_choice)
text = f"🎯 **Выберите число от 1 до {numbers_count}**\n\n"
text += f"🎁 Награда: {reward_amount:.0f}\n"
text += f"🍀 Удачных чисел: {winning_numbers} из {numbers_count}\n\n"
text += "Нажмите на число, чтобы испытать удачу!"
# Создаем кнопки с числами (5 в ряд)
buttons = []
for i in range(0, numbers_count, 5):
row = []
for j in range(5):
if i + j + 1 <= numbers_count:
number = i + j + 1
row.append(InlineKeyboardButton(
text=str(number),
callback_data=f"choose_number_{number}"
))
buttons.append(row)
buttons.append([InlineKeyboardButton(text="❌ Отмена", callback_data="lucky_game")])
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
await callback.message.edit_text(
text,
reply_markup=keyboard,
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error starting lucky game: {e}")
await callback.answer("❌ Ошибка запуска игры")
@lucky_game_router.callback_query(F.data.startswith("choose_number_"), StateFilter(LuckyGameStates.waiting_number_choice))
async def choose_number_callback(callback: CallbackQuery, db: Database, state: FSMContext, **kwargs):
"""Обработка выбора числа"""
user = kwargs.get('user')
bot = kwargs.get('bot')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
chosen_number = int(callback.data.split("_")[2])
# Получаем данные из состояния
state_data = await state.get_data()
winning_numbers = state_data['winning_numbers']
reward_amount = state_data['reward_amount']
numbers_count = state_data['numbers_count']
# Проверяем, выиграл ли пользователь
is_winner = chosen_number in winning_numbers
# Сохраняем результат игры в БД
await save_game_result(db, user.telegram_id, chosen_number, winning_numbers, is_winner, reward_amount if is_winner else 0.0)
# Формируем сообщение с результатом
if is_winner:
# Начисляем награду
await db.add_balance(user.telegram_id, reward_amount)
# Создаем платеж
await db.create_payment(
user_id=user.telegram_id,
amount=reward_amount,
payment_type='lucky_game',
description=f'Выигрыш в игре удачи (число {chosen_number})',
status='completed'
)
text = "🎉 **ПОЗДРАВЛЯЕМ! ВЫ ВЫИГРАЛИ!** 🎉\n\n"
text += f"🎯 Ваше число: **{chosen_number}**\n"
text += f"💰 Награда: **{reward_amount:.0f}₽** зачислена на баланс!\n"
text += f"🆕 Новый баланс: **{user.balance + reward_amount:.0f}₽**\n\n"
else:
text = "😔 **Не повезло в этот раз...**\n\n"
text += f"🎯 Ваше число: **{chosen_number}**\n"
text += f"🍀 Выигрышные числа: **{', '.join(map(str, sorted(winning_numbers)))}**\n\n"
text += "🔄 Попробуйте завтра!"
# Показываем все выигрышные числа
winning_nums_str = ', '.join(map(str, sorted(winning_numbers)))
text += f"\n🎲 Сегодняшние удачные числа: {winning_nums_str}"
text += f"\n\n⏰ Следующая игра будет доступна завтра!"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📈 История игр", callback_data="lucky_game_history")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
await callback.message.edit_text(
text,
reply_markup=keyboard,
parse_mode='Markdown'
)
# Логируем действие
log_user_action(user.telegram_id, "lucky_game_played",
f"Number: {chosen_number}, Winner: {is_winner}, Reward: {reward_amount if is_winner else 0}")
except Exception as e:
logger.error(f"Error in choose number: {e}")
await callback.answer("❌ Ошибка обработки выбора")
await state.clear()
@lucky_game_router.callback_query(F.data == "lucky_game_history")
async def lucky_game_history_callback(callback: CallbackQuery, db: Database, **kwargs):
"""Показать историю игр пользователя"""
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
# Получаем последние 10 игр
games = await get_user_game_history(db, user.telegram_id, limit=10)
if not games:
text = "📈 **История ваших игр**\n\n"
text += "🎯 Вы еще не играли в игру удачи.\n\n"
text += "Начните играть, чтобы увидеть историю!"
else:
text = "📈 **История ваших игр** (последние 10)\n\n"
for i, game in enumerate(games, 1):
date_str = format_datetime(game['played_at'], user.language)
if game['is_winner']:
emoji = "🎉"
result = f"Выиграли {game['reward_amount']:.0f}"
else:
emoji = "😔"
result = "Не повезло"
text += f"{i}. {emoji} Число: **{game['chosen_number']}** - {result}\n"
text += f" 📅 {date_str}\n\n"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🎰 К игре", callback_data="lucky_game")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
await callback.message.edit_text(
text,
reply_markup=keyboard,
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error showing game history: {e}")
await callback.answer("❌ Ошибка загрузки истории")
@lucky_game_router.callback_query(F.data == "noop")
async def noop_callback(callback: CallbackQuery, **kwargs):
"""Пустая операция для неактивных кнопок"""
await callback.answer()
# Вспомогательные функции
async def check_can_play_today(db: Database, user_id: int) -> tuple[bool, Optional[datetime]]:
"""Проверяет, может ли пользователь играть сегодня"""
try:
can_play = await db.can_play_lucky_game_today(user_id)
if not can_play:
# Вычисляем время следующей игры (завтра в 00:00)
now = datetime.utcnow()
tomorrow = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
return False, tomorrow
return True, None
except Exception as e:
logger.error(f"Error checking can play today: {e}")
return True, None
async def get_user_game_stats(db: Database, user_id: int) -> tuple[int, float, int]:
"""Получает статистику игр пользователя"""
try:
stats = await db.get_user_game_stats(user_id)
return stats['total_games'], stats['total_won'], stats['total_wins']
except Exception as e:
logger.error(f"Error getting user game stats: {e}")
return 0, 0.0, 0
async def save_game_result(db: Database, user_id: int, chosen_number: int,
winning_numbers: List[int], is_winner: bool, reward_amount: float):
"""Сохраняет результат игры в базу данных"""
try:
game = await db.create_lucky_game(
user_id=user_id,
chosen_number=chosen_number,
winning_numbers=winning_numbers,
is_winner=is_winner,
reward_amount=reward_amount
)
if game:
logger.info(f"Game result saved for user {user_id}: number={chosen_number}, winner={is_winner}, reward={reward_amount}")
else:
logger.error(f"Failed to save game result for user {user_id}")
except Exception as e:
logger.error(f"Error saving game result: {e}")
async def get_user_game_history(db: Database, user_id: int, limit: int = 10) -> List[dict]:
"""Получает историю игр пользователя"""
try:
return await db.get_user_game_history(user_id, limit)
except Exception as e:
logger.error(f"Error getting user game history: {e}")
return []
+37 -46
View File
@@ -6,20 +6,18 @@ from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from lucky_game import lucky_game_router
# ДОБАВЛЯЕМ ДЕБАГ ЗАГРУЗКИ .env ПЕРЕД ИМПОРТОМ CONFIG
print("🚀 Запуск бота...")
print(f"📍 Рабочая директория: {os.getcwd()}")
print(f"📁 Файлы в директории: {os.listdir('.')}")
# Проверяем наличие .env файла
if os.path.exists('.env'):
print("✅ Файл .env найден")
else:
print("❌ Файл .env НЕ НАЙДЕН!")
print("💡 Создайте файл .env в корне проекта")
# Import our modules ПОСЛЕ проверки .env
from config import load_config, debug_environment
from database import Database
from remnawave_api import RemnaWaveAPI
@@ -28,7 +26,6 @@ from middlewares import DatabaseMiddleware, UserMiddleware, LoggingMiddleware, T
from handlers import router
from admin_handlers import admin_router
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
@@ -40,7 +37,6 @@ logging.basicConfig(
logger = logging.getLogger(__name__)
class BotApplication:
"""Main bot application class"""
def __init__(self):
self.config = None
@@ -51,21 +47,16 @@ class BotApplication:
self.monitor_service = None
async def initialize(self):
"""Initialize all components"""
# ДЕБАГ: проверяем переменные окружения
debug_environment()
# Load configuration
self.config = load_config()
# ДЕБАГ: проверяем что загрузилось
print(f"🔧 Загруженная конфигурация:")
print(f" BOT_USERNAME: '{self.config.BOT_USERNAME}'")
print(f" REFERRAL_FIRST_REWARD: {self.config.REFERRAL_FIRST_REWARD}")
print(f" ADMIN_IDS: {self.config.ADMIN_IDS}")
# Validate required environment variables
if not self.config.BOT_TOKEN:
logger.error("BOT_TOKEN is required")
raise ValueError("BOT_TOKEN is required")
@@ -74,7 +65,6 @@ class BotApplication:
logger.error("REMNAWAVE_URL and REMNAWAVE_TOKEN are required")
raise ValueError("REMNAWAVE_URL and REMNAWAVE_TOKEN are required")
# ПРЕДУПРЕЖДЕНИЕ если BOT_USERNAME не установлен
if not self.config.BOT_USERNAME:
logger.warning("⚠️ BOT_USERNAME не установлен! Реферальные ссылки работать не будут!")
print("💡 Добавьте BOT_USERNAME=your_bot_username в .env файл")
@@ -84,11 +74,9 @@ class BotApplication:
logger.info(f"Admin IDs: {self.config.ADMIN_IDS}")
logger.info(f"Bot Username: {self.config.BOT_USERNAME}")
# Initialize database
self.db = Database(self.config.DATABASE_URL)
await self._init_database()
# Initialize RemnaWave API
self.api = RemnaWaveAPI(
self.config.REMNAWAVE_URL,
self.config.REMNAWAVE_TOKEN,
@@ -96,26 +84,20 @@ class BotApplication:
)
logger.info("RemnaWave API initialized")
# Test API connection (optional - don't fail if it doesn't work)
await self._test_api_connection()
# Initialize bot and dispatcher
self.bot = Bot(
token=self.config.BOT_TOKEN,
default=DefaultBotProperties(parse_mode=ParseMode.HTML)
)
# Test bot token
await self._test_bot_token()
# Initialize dispatcher
self._setup_dispatcher()
# Initialize subscription monitor service
await self._init_monitor_service()
async def _init_database(self):
"""Initialize database with retry logic"""
max_retries = 3
for attempt in range(max_retries):
try:
@@ -127,10 +109,9 @@ class BotApplication:
if attempt == max_retries - 1:
logger.error("Failed to initialize database after all retries")
raise
await asyncio.sleep(2) # Wait before retry
await asyncio.sleep(2)
async def _test_api_connection(self):
"""Test API connection"""
try:
system_stats = await self.api.get_system_stats()
if system_stats:
@@ -141,12 +122,10 @@ class BotApplication:
logger.warning(f"RemnaWave API connection error: {e} - continuing anyway")
async def _test_bot_token(self):
"""Test bot token before starting"""
try:
bot_info = await self.bot.get_me()
logger.info(f"Bot started: @{bot_info.username} ({bot_info.first_name})")
# АВТОМАТИЧЕСКИ обновляем BOT_USERNAME если он не был установлен
if not self.config.BOT_USERNAME and bot_info.username:
self.config.BOT_USERNAME = bot_info.username
logger.info(f"✅ BOT_USERNAME автоматически установлен: {bot_info.username}")
@@ -157,19 +136,16 @@ class BotApplication:
raise
def _setup_dispatcher(self):
"""Setup dispatcher with middlewares and routers"""
storage = MemoryStorage()
self.dp = Dispatcher(storage=storage)
# Store config, api, db, and monitor_service in dispatcher workflow_data for access in handlers
self.dp.workflow_data.update({
"config": self.config,
"api": self.api,
"db": self.db,
"monitor_service": None # Will be updated after monitor service is created
"monitor_service": None
})
# Setup middlewares in correct order
self.dp.message.middleware(LoggingMiddleware())
self.dp.callback_query.middleware(LoggingMiddleware())
@@ -188,35 +164,56 @@ class BotApplication:
self.dp.message.middleware(UserMiddleware(self.db, self.config))
self.dp.callback_query.middleware(UserMiddleware(self.db, self.config))
# Register routers
self.dp.include_router(router)
self.dp.include_router(admin_router)
self.dp.include_router(lucky_game_router)
async def _init_monitor_service(self):
"""Initialize subscription monitor service"""
try:
logger.info("🔧 Initializing subscription monitor service...")
if not self.bot:
logger.error("❌ Bot instance is None, cannot initialize monitor")
return
if not self.db:
logger.error("❌ Database instance is None, cannot initialize monitor")
return
if not self.config:
logger.error("❌ Config instance is None, cannot initialize monitor")
return
self.monitor_service = await create_subscription_monitor(
self.bot, self.db, self.config, self.api
)
# Update workflow_data with monitor service
if not self.monitor_service:
logger.error("❌ Failed to create monitor service instance")
return
self.dp.workflow_data["monitor_service"] = self.monitor_service
# Start the monitor service
logger.info("✅ Monitor service added to workflow_data")
logger.info("🚀 Starting monitor service...")
await self.monitor_service.start()
logger.info("Subscription monitor service started successfully")
status = await self.monitor_service.get_service_status()
if status['is_running']:
logger.info("✅ Subscription monitor service started successfully")
logger.info(f"📊 Monitor status: interval={status['check_interval']}s, daily_hour={status['daily_check_hour']}, warning_days={status['warning_days']}")
else:
logger.warning("⚠️ Monitor service created but not running")
logger.warning(f"📊 Monitor status: {status}")
except Exception as e:
logger.error(f"Failed to initialize monitor service: {e}")
# Don't fail the entire application if monitor service fails
logger.warning("Continuing without monitor service")
logger.error(f"Failed to initialize monitor service: {e}", exc_info=True)
logger.warning("⚠️ Continuing without monitor service")
self.monitor_service = None
async def start(self):
"""Start bot polling"""
logger.info("Bot polling started successfully")
# ФИНАЛЬНАЯ ПРОВЕРКА конфигурации
if self.config.BOT_USERNAME:
logger.info(f"🎁 Реферальная система активна! Ссылки: https://t.me/{self.config.BOT_USERNAME}?start=ref_USERID")
else:
@@ -231,10 +228,8 @@ class BotApplication:
await self.shutdown()
async def shutdown(self):
"""Shutdown all services"""
logger.info("Shutting down bot...")
# Stop monitor service first
if self.monitor_service:
try:
await self.monitor_service.stop()
@@ -242,7 +237,6 @@ class BotApplication:
except Exception as e:
logger.error(f"Error stopping monitor service: {e}")
# Close API connection
if self.api:
try:
await self.api.close()
@@ -250,7 +244,6 @@ class BotApplication:
except Exception as e:
logger.error(f"Error closing API: {e}")
# Close database connection
if self.db:
try:
await self.db.close()
@@ -258,7 +251,6 @@ class BotApplication:
except Exception as e:
logger.error(f"Error closing database: {e}")
# Close bot session
if self.bot:
try:
await self.bot.session.close()
@@ -269,7 +261,6 @@ class BotApplication:
logger.info("Bot shutdown complete")
async def main():
"""Main function"""
app = None
try:
app = BotApplication()
+2 -17
View File
@@ -8,7 +8,6 @@ from config import Config
logger = logging.getLogger(__name__)
class DatabaseMiddleware(BaseMiddleware):
"""Middleware for database operations"""
def __init__(self, db: Database):
self.db = db
@@ -23,7 +22,6 @@ class DatabaseMiddleware(BaseMiddleware):
return await handler(event, data)
class UserMiddleware(BaseMiddleware):
"""Middleware for user management - ИСПРАВЛЕНО: правильная инициализация языка"""
def __init__(self, db: Database, config: Config):
self.db = db
@@ -39,25 +37,21 @@ class UserMiddleware(BaseMiddleware):
if telegram_user and not telegram_user.is_bot:
try:
# Get or create user
user = await self.db.get_user_by_telegram_id(telegram_user.id)
if not user:
# Check if user is admin
is_admin = telegram_user.id in self.config.ADMIN_IDS
# ИСПРАВЛЕНО: Создаем пользователя с пустым языком для первоначальной настройки
user = await self.db.create_user(
telegram_id=telegram_user.id,
username=telegram_user.username,
first_name=telegram_user.first_name,
last_name=telegram_user.last_name,
language='', # ИЗМЕНЕНО: пустой язык для нового пользователя
language='',
is_admin=is_admin
)
logger.info(f"Created new user: {telegram_user.id} without language")
else:
# Update user info if changed
updated = False
if user.username != telegram_user.username:
user.username = telegram_user.username
@@ -69,7 +63,6 @@ class UserMiddleware(BaseMiddleware):
user.last_name = telegram_user.last_name
updated = True
# Check admin status
should_be_admin = telegram_user.id in self.config.ADMIN_IDS
if user.is_admin != should_be_admin:
user.is_admin = should_be_admin
@@ -83,7 +76,6 @@ class UserMiddleware(BaseMiddleware):
except Exception as e:
logger.error(f"Error in UserMiddleware: {e}")
# Create a minimal user object with defaults if database fails
class FallbackUser:
def __init__(self, telegram_id: int, username: str = None, config: Config = None):
self.telegram_id = telegram_id
@@ -95,7 +87,6 @@ class UserMiddleware(BaseMiddleware):
self.is_admin = telegram_id in (config.ADMIN_IDS if config else [])
self.remnawave_uuid = None
# Try to use fallback, but if database is completely broken, set None
try:
fallback_user = FallbackUser(telegram_user.id, telegram_user.username, self.config)
data['user'] = fallback_user
@@ -110,7 +101,6 @@ class UserMiddleware(BaseMiddleware):
return await handler(event, data)
class LoggingMiddleware(BaseMiddleware):
"""Middleware for logging"""
async def __call__(
self,
@@ -134,7 +124,6 @@ class LoggingMiddleware(BaseMiddleware):
return await handler(event, data)
class ThrottlingMiddleware(BaseMiddleware):
"""Simple throttling middleware"""
def __init__(self, rate_limit: float = 1.0):
self.rate_limit = rate_limit
@@ -163,7 +152,6 @@ class ThrottlingMiddleware(BaseMiddleware):
return await handler(event, data)
class WorkflowDataMiddleware(BaseMiddleware):
"""Middleware to pass workflow data to handlers"""
async def __call__(
self,
@@ -171,20 +159,17 @@ class WorkflowDataMiddleware(BaseMiddleware):
event: TelegramObject,
data: Dict[str, Any]
) -> Any:
# Get dispatcher from event
if hasattr(event, 'bot'):
dp = getattr(event.bot, '_dispatcher', None)
if dp and hasattr(dp, 'workflow_data'):
# Add workflow data to handler data
workflow_data = dp.workflow_data
for key, value in workflow_data.items():
if key not in data: # Don't override existing data
if key not in data:
data[key] = value
return await handler(event, data)
class BotMiddleware(BaseMiddleware):
"""Middleware to add bot instance to handler data"""
def __init__(self, bot):
self.bot = bot
+271
View File
@@ -0,0 +1,271 @@
import logging
from datetime import datetime
from database import Database
from database import ReferralProgram, ReferralEarning
logger = logging.getLogger(__name__)
async def process_referral_rewards(user_id: int, amount: float, payment_id: int, db: Database, bot=None):
try:
import os
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
first_reward = float(os.getenv('REFERRAL_FIRST_REWARD', '150.0'))
referred_bonus = float(os.getenv('REFERRAL_REFERRED_BONUS', '150.0'))
percentage = float(os.getenv('REFERRAL_PERCENTAGE', '0.25'))
referral = await db.get_referral_by_referred_id(user_id)
if not referral:
logger.debug(f"No referral found for user {user_id}")
return
user = await db.get_user_by_telegram_id(user_id)
if not user:
logger.error(f"User {user_id} not found")
return
logger.info(f"Processing referral rewards for user {user_id}, amount {amount}, referrer {referral.referrer_id}")
if not referral.first_reward_paid and user.balance >= threshold:
logger.info(f"Processing first reward for referral {referral.id} (threshold: {threshold}, reward: {first_reward})")
await db.add_balance(referral.referrer_id, first_reward)
await db.create_payment(
user_id=referral.referrer_id,
amount=first_reward,
payment_type='referral',
description=f'Первая награда за реферала ID:{user_id}',
status='completed'
)
success = await db.create_referral_earning(
referrer_id=referral.referrer_id,
referred_id=user_id,
amount=first_reward,
earning_type='first_reward',
related_payment_id=payment_id
)
if success:
logger.info(f"First reward paid: {first_reward}₽ to referrer {referral.referrer_id}")
if bot:
try:
await bot.send_message(
referral.referrer_id,
f"🎉 Поздравляем! Ваш реферал пополнил баланс на {threshold}₽+\n\n"
f"💰 Вам начислено {first_reward}₽ за приведенного друга!\n"
f"Теперь вы будете получать {percentage*100:.0f}% с каждого его следующего платежа."
)
await bot.send_message(
user_id,
f"🎁 Бонус активирован! Вам начислено {referred_bonus}₽ за переход по реферальной ссылке!"
)
await db.add_balance(user_id, referred_bonus)
await db.create_payment(
user_id=user_id,
amount=referred_bonus,
payment_type='referral',
description='Бонус за переход по реферальной ссылке',
status='completed'
)
logger.info(f"Referral bonus notifications sent and balance updated")
except Exception as e:
logger.error(f"Failed to send referral notifications: {e}")
else:
logger.error(f"Failed to create first reward earning")
if amount > 0 and referral.first_reward_paid:
percentage_reward = amount * percentage
if percentage_reward >= 0.01:
await db.add_balance(referral.referrer_id, percentage_reward)
await db.create_payment(
user_id=referral.referrer_id,
amount=percentage_reward,
payment_type='referral',
description=f'{percentage*100:.0f}% дохода от реферала ID:{user_id}',
status='completed'
)
success = await db.create_referral_earning(
referrer_id=referral.referrer_id,
referred_id=user_id,
amount=percentage_reward,
earning_type='percentage',
related_payment_id=payment_id
)
if success:
logger.info(f"Percentage reward paid: {percentage_reward:.2f}₽ ({percentage*100:.0f}%) to referrer {referral.referrer_id}")
if bot and percentage_reward >= 1.0:
try:
await bot.send_message(
referral.referrer_id,
f"💰 Реферальный доход!\n\n"
f"Ваш реферал совершил платеж на {amount:.2f}\n"
f"Вам начислено: {percentage_reward:.2f}₽ ({percentage*100:.0f}%)"
)
except Exception as e:
logger.error(f"Failed to send percentage notification: {e}")
else:
logger.error(f"Failed to create percentage earning")
elif amount > 0 and not referral.first_reward_paid:
logger.info(f"Skipping percentage reward for user {user_id} - first reward not yet paid")
except Exception as e:
logger.error(f"Error processing referral rewards: {e}")
async def create_referral_from_start_param(user_telegram_id: int, start_param: str, db: Database, bot=None):
try:
if not start_param.startswith("ref_"):
return False
referrer_id = int(start_param.replace("ref_", ""))
if referrer_id == user_telegram_id:
logger.warning(f"User {user_telegram_id} tried to refer themselves")
return False
existing_reverse_referral = await db.get_referral_by_referred_id(referrer_id)
if existing_reverse_referral and existing_reverse_referral.referrer_id == user_telegram_id:
logger.warning(f"Mutual referral attempt blocked: {user_telegram_id} is already referrer for {referrer_id}")
return False
existing_referral = await db.get_referral_by_referred_id(user_telegram_id)
if existing_referral:
logger.info(f"User {user_telegram_id} already has referrer")
return False
referral_code = await db.generate_unique_referral_code(referrer_id)
referral = await db.create_referral(referrer_id, user_telegram_id, referral_code)
if referral:
logger.info(f"Created referral: {referrer_id} -> {user_telegram_id} with code {referral_code}")
if bot:
try:
referrer = await db.get_user_by_telegram_id(referrer_id)
if referrer:
import os
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
first_reward = float(os.getenv('REFERRAL_FIRST_REWARD', '150.0'))
percentage = float(os.getenv('REFERRAL_PERCENTAGE', '0.25'))
await bot.send_message(
referrer_id,
f"🎉 Отлично! По вашей ссылке зарегистрировался новый пользователь!\n\n"
f"Вы получите {first_reward:.0f}₽ после того, как он пополнит баланс на {threshold:.0f}₽.\n"
f"И будете получать {percentage*100:.0f}% с каждого его платежа!"
)
except Exception as e:
logger.error(f"Failed to notify referrer: {e}")
return True
return False
except (ValueError, TypeError) as e:
logger.warning(f"Invalid referral parameter: {start_param}")
return False
except Exception as e:
logger.error(f"Error creating referral from start param: {e}")
return False
async def create_referral_from_promocode(user_telegram_id: int, referral_code: str, db: Database, bot=None):
try:
if not referral_code.startswith("REF"):
return False
logger.info(f"Trying to use referral code {referral_code} for user {user_telegram_id}")
async with db.session_factory() as session:
from sqlalchemy import select
result = await session.execute(
select(ReferralProgram).where(ReferralProgram.referral_code == referral_code)
)
referral_record = result.scalar_one_or_none()
if not referral_record:
logger.warning(f"No referrer found for code {referral_code}")
return False
referrer_id = referral_record.referrer_id
if referrer_id == user_telegram_id:
logger.warning(f"User {user_telegram_id} tried to use own referral code")
return False
existing_reverse_referral = await db.get_referral_by_referred_id(referrer_id)
if existing_reverse_referral and existing_reverse_referral.referrer_id == user_telegram_id:
logger.warning(f"Mutual referral attempt blocked: {user_telegram_id} is already referrer for {referrer_id}")
return False
existing_referral = await db.get_referral_by_referred_id(user_telegram_id)
if existing_referral:
logger.info(f"User {user_telegram_id} already has referrer")
return False
referral = await db.create_referral(referrer_id, user_telegram_id, referral_code)
if referral:
logger.info(f"Created referral from promocode: {referrer_id} -> {user_telegram_id}")
if bot:
try:
import os
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
first_reward = float(os.getenv('REFERRAL_FIRST_REWARD', '150.0'))
await bot.send_message(
referrer_id,
f"🎉 По вашему промокоду {referral_code} зарегистрировался новый пользователь!\n\n"
f"Вы получите {first_reward:.0f}₽ после того, как он пополнит баланс на {threshold:.0f}₽."
)
except Exception as e:
logger.error(f"Failed to notify referrer: {e}")
return True
return False
except Exception as e:
logger.error(f"Error creating referral from promocode: {e}")
return False
def generate_referral_link(bot_username: str, user_id: int) -> str:
if not bot_username:
return ""
if bot_username.startswith('@'):
bot_username = bot_username[1:]
return f"https://t.me/{bot_username}?start=ref_{user_id}"
def validate_referral_code(code: str) -> bool:
if not code or not code.startswith("REF"):
return False
if len(code) < 4 or len(code) > 20:
return False
return True
def format_referral_stats(stats: dict, lang: str = 'ru') -> str:
if lang == 'ru':
return (f"👥 Приглашено: {stats['total_referrals']}\n"
f"✅ Активных: {stats['active_referrals']}\n"
f"💰 Заработано: {stats['total_earned']:.2f}")
else:
return (f"👥 Invited: {stats['total_referrals']}\n"
f"✅ Active: {stats['active_referrals']}\n"
f"💰 Earned: ${stats['total_earned']:.2f}")
+169 -155
View File
@@ -11,8 +11,7 @@ class RemnaWaveAPI:
def __init__(self, base_url: str, token: str, subscription_base_url: str = None):
self.base_url = base_url.rstrip('/')
self.token = token
# УДАЛЯЕМ зависимость от subscription_base_url - теперь берем из API
self.subscription_base_url = subscription_base_url # Оставляем для обратной совместимости
self.subscription_base_url = subscription_base_url
self.session = None
async def _get_session(self):
@@ -34,7 +33,6 @@ class RemnaWaveAPI:
await self.session.close()
async def _make_request(self, method: str, endpoint: str, data: Optional[Dict] = None, params: Optional[Dict] = None) -> Optional[Dict]:
# Убедимся, что endpoint начинается с /api/
if not endpoint.startswith('/api/'):
endpoint = '/api' + endpoint
@@ -44,13 +42,11 @@ class RemnaWaveAPI:
try:
logger.debug(f"Making {method} request to {url}")
async with session.request(method, url, json=data, params=params) as response:
# Проверяем Content-Type
content_type = response.headers.get('Content-Type', '')
logger.debug(f"Response Content-Type: {content_type}, Status: {response.status}")
response_text = await response.text()
# Если получили HTML вместо JSON, логируем ошибку
if 'text/html' in content_type:
logger.error(f"Got HTML response instead of JSON from {url}")
logger.debug(f"HTML Response (first 500 chars): {response_text[:500]}")
@@ -79,11 +75,9 @@ class RemnaWaveAPI:
return None
async def get_subscription_info(self, short_uuid: str) -> Optional[Dict]:
"""Get subscription info including URL from RemnaWave API"""
try:
logger.info(f"Getting subscription info for short_uuid: {short_uuid}")
# Пробуем разные эндпоинты для получения информации о подписке
endpoints_to_try = [
f'/api/subscriptions/{short_uuid}',
f'/api/sub/{short_uuid}',
@@ -97,7 +91,6 @@ class RemnaWaveAPI:
if result:
logger.info(f"Successfully got subscription info from {endpoint}")
# Обрабатываем разные структуры ответа
subscription_data = None
if 'response' in result:
@@ -109,7 +102,6 @@ class RemnaWaveAPI:
else:
subscription_data = result
# Проверяем что получили нужные данные
if subscription_data and (
'subscriptionUrl' in subscription_data or
'url' in subscription_data or
@@ -125,15 +117,12 @@ class RemnaWaveAPI:
return None
async def get_subscription_url(self, short_uuid: str) -> str:
"""Get subscription URL from RemnaWave API - ОБНОВЛЕННЫЙ МЕТОД"""
try:
logger.info(f"Getting subscription URL for short_uuid: {short_uuid}")
# Сначала пробуем получить URL из API
subscription_info = await self.get_subscription_info(short_uuid)
if subscription_info:
# Ищем URL в разных возможных полях
subscription_url = (
subscription_info.get('subscriptionUrl') or
subscription_info.get('url') or
@@ -145,31 +134,26 @@ class RemnaWaveAPI:
logger.info(f"Got subscription URL from API: {subscription_url}")
return subscription_url
# Если не получилось через API, пробуем через пользователя
user_data = await self.get_user_by_short_uuid(short_uuid)
if user_data and 'subscriptionUrl' in user_data:
logger.info(f"Got subscription URL from user data: {user_data['subscriptionUrl']}")
return user_data['subscriptionUrl']
# Последний fallback - формируем URL самостоятельно
if self.subscription_base_url:
fallback_url = f"{self.subscription_base_url.rstrip('/')}/sub/{short_uuid}"
logger.warning(f"Using fallback URL: {fallback_url}")
return fallback_url
else:
# Если нет даже fallback URL, пытаемся угадать из base_url
fallback_url = f"{self.base_url.rstrip('/')}/sub/{short_uuid}"
logger.warning(f"Using base_url fallback: {fallback_url}")
return fallback_url
except Exception as e:
logger.error(f"Failed to get subscription URL for {short_uuid}: {e}")
# Последний resort
fallback_url = f"{self.base_url.rstrip('/')}/sub/{short_uuid}"
return fallback_url
async def get_all_subscriptions_with_urls(self) -> Optional[List]:
"""Get all subscriptions with their URLs from RemnaWave API"""
try:
logger.info("Fetching all subscriptions with URLs from API")
result = await self._make_request('GET', '/api/subscriptions')
@@ -180,7 +164,6 @@ class RemnaWaveAPI:
subscriptions_list = []
# Extract subscriptions from response
if 'response' in result and 'subscriptions' in result['response']:
subscriptions_list = result['response']['subscriptions']
elif 'subscriptions' in result:
@@ -190,13 +173,11 @@ class RemnaWaveAPI:
elif isinstance(result, list):
subscriptions_list = result
# Process subscriptions to ensure they have URLs
processed_subscriptions = []
for subscription in subscriptions_list:
if subscription.get('isFound') and 'user' in subscription:
user_data = subscription['user']
# Добавляем URL если его нет
if 'subscriptionUrl' not in subscription and user_data.get('shortUuid'):
subscription['subscriptionUrl'] = await self.get_subscription_url(user_data['shortUuid'])
@@ -209,7 +190,6 @@ class RemnaWaveAPI:
logger.error(f"Exception in get_all_subscriptions_with_urls: {e}", exc_info=True)
return []
# User management - без изменений
async def create_user(self, username: str, password: str = None,
traffic_limit: int = 0, expiry_time: str = None,
telegram_id: int = None, email: str = None,
@@ -237,13 +217,11 @@ class RemnaWaveAPI:
result = await self._make_request('POST', '/api/users', data)
# Обработка ответа от RemnaWave
if result and 'response' in result:
return {'data': result['response']}
return result
async def get_user_by_telegram_id(self, telegram_id: int) -> Optional[Dict]:
"""Get user by Telegram ID with improved response handling"""
logger.debug(f"Getting user by Telegram ID: {telegram_id}")
result = await self._make_request('GET', f'/api/users/by-telegram-id/{telegram_id}')
@@ -255,11 +233,9 @@ class RemnaWaveAPI:
logger.debug(f"Raw API response type: {type(result)}")
logger.debug(f"Raw API response: {result}")
# Обрабатываем различные структуры ответа
user_data = None
if isinstance(result, dict):
# Проверяем стандартные поля ответа
if 'response' in result:
response_data = result['response']
if isinstance(response_data, dict):
@@ -278,16 +254,13 @@ class RemnaWaveAPI:
if 'telegramId' in result or 'username' in result:
user_data = result
elif isinstance(result, list):
# Если API вернул список пользователей
if result:
user_data = result[0]
if user_data and isinstance(user_data, dict):
# Проверяем что это действительно нужный пользователь
if user_data.get('telegramId') == telegram_id:
logger.info(f"Found user: {user_data.get('username')} for Telegram ID {telegram_id}")
# НОВОЕ: Обогащаем данные пользователя URL подписки если есть short_uuid
if user_data.get('shortUuid') and 'subscriptionUrl' not in user_data:
try:
subscription_url = await self.get_subscription_url(user_data['shortUuid'])
@@ -316,7 +289,6 @@ class RemnaWaveAPI:
else:
user_data = result
# Добавляем subscription URL если его нет
if user_data and user_data.get('shortUuid') and 'subscriptionUrl' not in user_data:
try:
subscription_url = await self.get_subscription_url(user_data['shortUuid'])
@@ -328,7 +300,6 @@ class RemnaWaveAPI:
return None
async def get_user_by_short_uuid(self, short_uuid: str) -> Optional[Dict]:
"""Get user data by short UUID"""
logger.debug(f"Getting user by short UUID: {short_uuid}")
result = await self._make_request('GET', f'/api/users/by-short-uuid/{short_uuid}')
@@ -341,7 +312,6 @@ class RemnaWaveAPI:
else:
user_data = result
# Добавляем subscription URL если его нет
if user_data and 'subscriptionUrl' not in user_data:
try:
subscription_url = await self.get_subscription_url(short_uuid)
@@ -353,10 +323,8 @@ class RemnaWaveAPI:
return None
async def update_user(self, uuid: str, data: Dict) -> Optional[Dict]:
"""Update user with proper field names"""
update_data = {'uuid': uuid}
# Map fields correctly based on API documentation
if 'enable' in data:
update_data['status'] = 'ACTIVE' if data['enable'] else 'DISABLED'
if 'expireAt' in data:
@@ -368,7 +336,6 @@ class RemnaWaveAPI:
if 'status' in data:
update_data['status'] = data['status']
# Add other fields directly
for key in ['activeInternalSquads', 'telegramId', 'email']:
if key in data:
update_data[key] = data[key]
@@ -379,7 +346,6 @@ class RemnaWaveAPI:
return result
async def update_user_expiry(self, short_uuid: str, new_expiry: str) -> Optional[Dict]:
"""Update user expiry date by short UUID"""
try:
user_data = await self.get_user_by_short_uuid(short_uuid)
if not user_data:
@@ -391,7 +357,6 @@ class RemnaWaveAPI:
logger.error(f"Could not get UUID from user data")
return None
# Update user with correct field name
update_data = {
'status': 'ACTIVE',
'expireAt': new_expiry
@@ -405,35 +370,30 @@ class RemnaWaveAPI:
return None
async def update_user_traffic_limit(self, uuid: str, traffic_limit_gb: int) -> Optional[Dict]:
"""Update user traffic limit"""
traffic_bytes = traffic_limit_gb * 1024 * 1024 * 1024 if traffic_limit_gb > 0 else 0
update_data = {
'trafficLimitBytes': traffic_bytes
}
return await self.update_user(uuid, update_data)
# Nodes management
async def get_all_nodes(self) -> Optional[List]:
"""Get all nodes with proper field mapping"""
try:
logger.debug("Requesting nodes from /api/nodes")
result = await self._make_request('GET', '/api/nodes')
if not result:
logger.error("Empty response from nodes API")
return []
nodes_list = []
# Extract nodes from response
if 'response' in result:
nodes_list = result['response'] if isinstance(result['response'], list) else []
elif 'data' in result:
nodes_list = result['data'] if isinstance(result['data'], list) else []
elif isinstance(result, list):
nodes_list = result
# Process and normalize node data
processed_nodes = []
for node in nodes_list:
processed_node = {
@@ -448,29 +408,36 @@ class RemnaWaveAPI:
'isXrayRunning': node.get('isXrayRunning', False),
'cpuUsage': node.get('cpuUsage', 0),
'memUsage': node.get('memUsage', 0),
'usersCount': node.get('usersOnline', 0)
'usersCount': node.get('usersOnline', node.get('usersCount', 0)),
'xrayVersion': node.get('xrayVersion'),
'nodeVersion': node.get('nodeVersion'),
'xrayUptime': node.get('xrayUptime'),
'cpuModel': node.get('cpuModel'),
'totalRam': node.get('totalRam'),
'trafficUsedBytes': node.get('trafficUsedBytes', 0),
'viewPosition': node.get('viewPosition'),
'countryCode': node.get('countryCode'),
'region': node.get('region'),
'city': node.get('city'),
'provider': node.get('provider'),
}
processed_nodes.append(processed_node)
logger.info(f"Processed {len(processed_nodes)} nodes")
logger.info(f"Processed {len(processed_nodes)} nodes with enhanced info")
return processed_nodes
except Exception as e:
logger.error(f"Exception in get_all_nodes: {e}", exc_info=True)
return []
def _determine_node_status(self, node: Dict) -> str:
"""Determine node status based on multiple fields"""
# Получаем все поля статуса
is_connected = node.get('isConnected', False)
is_disabled = node.get('isDisabled', False) # False означает включена
is_disabled = node.get('isDisabled', False)
is_node_online = node.get('isNodeOnline', False)
is_xray_running = node.get('isXrayRunning', False)
# Логируем для отладки
logger.debug(f"Node {node.get('name', 'unknown')}: connected={is_connected}, disabled={is_disabled}, online={is_node_online}, xray={is_xray_running}")
# Определяем статус по приоритету
if is_disabled:
return 'disabled'
elif not is_connected:
@@ -483,7 +450,6 @@ class RemnaWaveAPI:
return 'offline'
async def restart_node(self, node_id: str) -> Optional[Dict]:
"""Restart specific node"""
try:
logger.info(f"Attempting to restart node: {node_id}")
result = await self._make_request('POST', f'/api/nodes/{node_id}/actions/restart')
@@ -500,11 +466,9 @@ class RemnaWaveAPI:
return None
async def restart_all_nodes(self) -> Optional[Dict]:
"""Restart all nodes"""
try:
logger.info("Attempting to restart all nodes")
# Get all nodes first
nodes = await self.get_all_nodes()
if not nodes:
return None
@@ -516,7 +480,7 @@ class RemnaWaveAPI:
result = await self.restart_node(node_id)
if result:
success_count += 1
await asyncio.sleep(0.5) # Small delay between restarts
await asyncio.sleep(0.5)
return {
'success': success_count > 0,
@@ -529,65 +493,174 @@ class RemnaWaveAPI:
return None
async def enable_node(self, node_id: str) -> Optional[Dict]:
"""Enable specific node"""
return await self._make_request('POST', f'/api/nodes/{node_id}/actions/enable')
async def disable_node(self, node_id: str) -> Optional[Dict]:
"""Disable specific node"""
return await self._make_request('POST', f'/api/nodes/{node_id}/actions/disable')
# System stats - без изменений
async def get_system_stats(self) -> Optional[Dict]:
"""Get system statistics with proper parsing"""
try:
logger.info("Fetching system stats...")
# Get users count
users_result = await self._make_request('GET', '/api/users', params={'limit': 1})
total_users = 0
active_users = 0
if users_result:
if 'total' in users_result:
total_users = users_result['total']
# Get active users
all_users = await self.get_all_system_users_full()
if all_users:
active_users = len([u for u in all_users if u.get('status') == 'ACTIVE'])
total_users = len(all_users)
# Get nodes info
nodes = await self.get_all_nodes()
online_nodes = 0
if nodes:
online_nodes = len([n for n in nodes if n.get('status') == 'online'])
# Get bandwidth stats if available
logger.info("Fetching enhanced system stats from /api/system/stats...")
system_result = await self._make_request('GET', '/api/system/stats')
if not system_result or 'response' not in system_result:
logger.error("Invalid response from /api/system/stats")
return None
system_data = system_result['response']
logger.info(f"System stats received: {list(system_data.keys())}")
users_stats = system_data.get('users', {})
status_counts = users_stats.get('statusCounts', {})
total_users = users_stats.get('totalUsers', 0)
active_users = status_counts.get('ACTIVE', 0)
disabled_users = status_counts.get('DISABLED', 0)
limited_users = status_counts.get('LIMITED', 0)
expired_users = status_counts.get('EXPIRED', 0)
online_stats = system_data.get('onlineStats', {})
online_now = online_stats.get('onlineNow', 0)
last_day = online_stats.get('lastDay', 0)
last_week = online_stats.get('lastWeek', 0)
never_online = online_stats.get('neverOnline', 0)
cpu_info = system_data.get('cpu', {})
memory_info = system_data.get('memory', {})
def bytes_to_gb(bytes_value):
return round(bytes_value / (1024**3), 2) if bytes_value else 0
memory_total_gb = bytes_to_gb(memory_info.get('total', 0))
memory_active_gb = bytes_to_gb(memory_info.get('active', 0))
memory_available_gb = bytes_to_gb(memory_info.get('available', 0))
memory_free_gb = bytes_to_gb(memory_info.get('free', 0))
all_nodes = await self.get_all_nodes()
total_nodes = len(all_nodes) if all_nodes else 0
nodes_online = 0
if all_nodes:
nodes_online = len([n for n in all_nodes if n.get('status') == 'online'])
if total_nodes == 0:
nodes_info = system_data.get('nodes', {})
nodes_online = nodes_info.get('totalOnline', 0)
total_nodes = nodes_online
bandwidth_result = await self._make_request('GET', '/api/system/stats/bandwidth')
bandwidth = {'downlink': 0, 'uplink': 0}
enhanced_bandwidth = {}
if bandwidth_result and 'response' in bandwidth_result:
bandwidth = bandwidth_result['response']
enhanced_bandwidth = bandwidth_result['response']
logger.info(f"Enhanced bandwidth stats received")
stats = {
'users': active_users,
'active_users': active_users,
'total_users': total_users,
'nodes': {
'total': len(nodes) if nodes else 0,
'online': online_nodes
'disabled_users': disabled_users,
'limited_users': limited_users,
'expired_users': expired_users,
'online_stats': {
'online_now': online_now,
'last_day': last_day,
'last_week': last_week,
'never_online': never_online
},
'bandwidth': bandwidth
'system_resources': {
'cpu': {
'cores': cpu_info.get('cores', 0),
'physical_cores': cpu_info.get('physicalCores', 0)
},
'memory': {
'total_gb': memory_total_gb,
'active_gb': memory_active_gb,
'available_gb': memory_available_gb,
'free_gb': memory_free_gb,
'usage_percent': round((memory_active_gb / memory_total_gb * 100), 1) if memory_total_gb > 0 else 0
},
'uptime': system_data.get('uptime', 0)
},
'nodes': {
'total': total_nodes,
'online': nodes_online,
'offline': total_nodes - nodes_online
},
'bandwidth': enhanced_bandwidth,
'total_traffic_bytes': users_stats.get('totalTrafficBytes', '0')
}
logger.info(f"System stats: {stats}")
logger.info(f"Enhanced system stats compiled successfully")
logger.info(f"Users: {total_users} total, {active_users} active, {online_now} online now")
logger.info(f"Memory: {memory_active_gb}/{memory_total_gb} GB active, {memory_available_gb} GB available")
logger.info(f"Nodes: {nodes_online}/{total_nodes} online")
return stats
except Exception as e:
logger.error(f"Error getting system stats: {e}", exc_info=True)
logger.error(f"Error getting enhanced system stats: {e}", exc_info=True)
return None
async def get_bandwidth_stats(self) -> Optional[Dict]:
try:
logger.info("Fetching detailed bandwidth statistics")
result = await self._make_request('GET', '/api/system/stats/bandwidth')
if result and 'response' in result:
return result['response']
return None
except Exception as e:
logger.error(f"Error getting bandwidth stats: {e}")
return None
async def get_node_detailed_info(self, node_id: str) -> Optional[Dict]:
try:
logger.info(f"Getting detailed info for node: {node_id}")
result = await self._make_request('GET', f'/api/nodes/{node_id}')
if result:
node_data = None
if 'response' in result:
node_data = result['response']
elif 'data' in result:
node_data = result['data']
else:
node_data = result
if node_data:
enhanced_node = {
'id': node_data.get('uuid', node_data.get('id')),
'uuid': node_data.get('uuid', node_data.get('id')),
'name': node_data.get('name', 'Unknown Node'),
'address': node_data.get('address', ''),
'status': self._determine_node_status(node_data),
'isConnected': node_data.get('isConnected', False),
'isDisabled': node_data.get('isDisabled', True),
'isNodeOnline': node_data.get('isNodeOnline', False),
'isXrayRunning': node_data.get('isXrayRunning', False),
'cpuUsage': node_data.get('cpuUsage', 0),
'memUsage': node_data.get('memUsage', 0),
'usersCount': node_data.get('usersOnline', node_data.get('usersCount', 0)),
'xrayVersion': node_data.get('xrayVersion'),
'nodeVersion': node_data.get('nodeVersion'),
'xrayUptime': node_data.get('xrayUptime'),
'cpuModel': node_data.get('cpuModel'),
'totalRam': node_data.get('totalRam'),
'trafficUsedBytes': node_data.get('trafficUsedBytes', 0),
'viewPosition': node_data.get('viewPosition'),
'countryCode': node_data.get('countryCode'),
'region': node_data.get('region'),
'city': node_data.get('city'),
'provider': node_data.get('provider'),
}
return enhanced_node
return None
except Exception as e:
logger.error(f"Error getting detailed node info: {e}")
return None
async def get_all_system_users_full(self) -> Optional[List]:
"""Get all system users with subscription URLs - ОБНОВЛЕННЫЙ МЕТОД"""
try:
all_users = []
offset = 0
@@ -609,14 +682,11 @@ class RemnaWaveAPI:
batch_users = []
total_count = None
# Обработка различных структур ответа API
if isinstance(result, dict):
# Проверяем наличие общего количества
if 'total' in result:
total_count = result['total']
logger.info(f"Total users in system: {total_count}")
# Извлекаем пользователей из различных возможных полей
if 'users' in result:
batch_users = result['users'] if isinstance(result['users'], list) else []
elif 'data' in result:
@@ -640,11 +710,9 @@ class RemnaWaveAPI:
logger.info(f"No users in batch at offset {offset}, stopping")
break
# НОВОЕ: Обогащаем каждого пользователя subscription URL
enriched_users = []
for user in batch_users:
try:
# Добавляем subscription URL если есть shortUuid и его еще нет
if user.get('shortUuid') and 'subscriptionUrl' not in user:
subscription_url = await self.get_subscription_url(user['shortUuid'])
user['subscriptionUrl'] = subscription_url
@@ -653,28 +721,24 @@ class RemnaWaveAPI:
enriched_users.append(user)
except Exception as e:
logger.warning(f"Could not enrich user {user.get('username', 'unknown')} with URL: {e}")
enriched_users.append(user) # Добавляем пользователя даже без URL
enriched_users.append(user)
all_users.extend(enriched_users)
# Проверяем условия остановки
if len(batch_users) < limit:
logger.info(f"Last batch (got {len(batch_users)} < {limit})")
break
# Если знаем общее количество, проверяем достигли ли мы его
if total_count and len(all_users) >= total_count:
logger.info(f"Reached total count: {total_count}")
break
offset += limit
# Защита от бесконечного цикла
if offset > 10000:
logger.warning("Offset limit reached, stopping")
break
# Финальная статистика
if all_users:
active_users = len([u for u in all_users if str(u.get('status', '')).upper() == 'ACTIVE'])
users_with_urls = len([u for u in all_users if u.get('subscriptionUrl')])
@@ -687,13 +751,8 @@ class RemnaWaveAPI:
except Exception as e:
logger.error(f"Error getting all system users: {e}", exc_info=True)
return []
# Остальные методы остаются без изменений...
# (Internal squads, user search, user actions, bulk operations, debug methods, health check, etc.)
# Internal squads
async def get_internal_squads_list(self) -> Optional[List[Dict]]:
"""Get list of internal squads"""
logger.info("Fetching internal squads list")
result = await self._make_request('GET', '/api/internal-squads')
@@ -709,12 +768,10 @@ class RemnaWaveAPI:
return []
async def get_users_count(self) -> Optional[int]:
"""Get total users count"""
try:
result = await self._make_request('GET', '/api/users', params={'limit': 1})
if result:
# Проверяем различные поля для общего количества
if 'total' in result:
return result['total']
elif 'totalCount' in result:
@@ -722,7 +779,6 @@ class RemnaWaveAPI:
elif 'count' in result:
return result['count']
# Если не можем получить count, получаем всех пользователей
all_users = await self.get_all_system_users_full()
return len(all_users) if all_users else 0
@@ -734,7 +790,6 @@ class RemnaWaveAPI:
try:
logger.info("=== DEBUGGING USERS API ===")
# Пробуем получить одного пользователя
result = await self._make_request('GET', '/api/users', params={'limit': 1, 'offset': 0})
debug_info = {
@@ -747,7 +802,6 @@ class RemnaWaveAPI:
}
if isinstance(result, dict):
# Ищем где находятся пользователи
if 'users' in result:
debug_info['users_location'] = 'root.users'
debug_info['has_users'] = True
@@ -776,7 +830,6 @@ class RemnaWaveAPI:
if result['response']:
debug_info['first_user_structure'] = list(result['response'][0].keys())
# Ищем общее количество
for key in ['total', 'totalCount', 'count', 'totalUsers']:
if key in result:
debug_info['total_count'] = result[key]
@@ -795,9 +848,7 @@ class RemnaWaveAPI:
logger.error(f"Error in debug_users_api: {e}")
return {'error': str(e)}
# User search methods
async def get_user_by_username(self, username: str) -> Optional[Dict]:
"""Get user by username"""
result = await self._make_request('GET', f'/api/users/by-username/{username}')
if result:
user_data = None
@@ -808,7 +859,6 @@ class RemnaWaveAPI:
else:
user_data = result
# Добавляем subscription URL если его нет
if user_data and user_data.get('shortUuid') and 'subscriptionUrl' not in user_data:
try:
subscription_url = await self.get_subscription_url(user_data['shortUuid'])
@@ -820,7 +870,6 @@ class RemnaWaveAPI:
return None
async def get_user_by_email(self, email: str) -> Optional[Dict]:
"""Get user by email"""
result = await self._make_request('GET', f'/api/users/by-email/{email}')
if result:
user_data = None
@@ -831,7 +880,6 @@ class RemnaWaveAPI:
else:
user_data = result
# Добавляем subscription URL если его нет
if user_data and user_data.get('shortUuid') and 'subscriptionUrl' not in user_data:
try:
subscription_url = await self.get_subscription_url(user_data['shortUuid'])
@@ -843,7 +891,6 @@ class RemnaWaveAPI:
return None
async def get_user_by_tag(self, tag: str) -> Optional[Dict]:
"""Get user by tag"""
result = await self._make_request('GET', f'/api/users/by-tag/{tag}')
if result:
user_data = None
@@ -854,7 +901,6 @@ class RemnaWaveAPI:
else:
user_data = result
# Добавляем subscription URL если его нет
if user_data and user_data.get('shortUuid') and 'subscriptionUrl' not in user_data:
try:
subscription_url = await self.get_subscription_url(user_data['shortUuid'])
@@ -865,39 +911,29 @@ class RemnaWaveAPI:
return user_data
return None
# User actions
async def disable_user(self, uuid: str) -> Optional[Dict]:
"""Disable user"""
result = await self._make_request('POST', f'/api/users/{uuid}/actions/disable')
if not result:
# Try updating status directly
return await self.update_user(uuid, {'status': 'DISABLED'})
return result
async def enable_user(self, uuid: str) -> Optional[Dict]:
"""Enable user"""
result = await self._make_request('POST', f'/api/users/{uuid}/actions/enable')
if not result:
# Try updating status directly
return await self.update_user(uuid, {'status': 'ACTIVE'})
return result
async def reset_user_traffic(self, uuid: str) -> Optional[Dict]:
"""Reset user traffic"""
return await self._make_request('POST', f'/api/users/{uuid}/actions/reset-traffic')
async def revoke_user_subscription(self, uuid: str) -> Optional[Dict]:
"""Revoke user subscription"""
return await self._make_request('POST', f'/api/users/{uuid}/actions/revoke')
# Bulk operations
async def bulk_reset_traffic(self, user_uuids: List[str]) -> Optional[Dict]:
"""Bulk reset traffic for users"""
data = {'uuids': user_uuids}
return await self._make_request('POST', '/api/users/bulk/reset-traffic', data)
async def bulk_update_users(self, user_uuids: List[str], fields: Dict) -> Optional[Dict]:
"""Bulk update users"""
data = {
'uuids': user_uuids,
'fields': fields
@@ -905,15 +941,11 @@ class RemnaWaveAPI:
return await self._make_request('POST', '/api/users/bulk/update', data)
async def bulk_delete_users(self, user_uuids: List[str]) -> Optional[Dict]:
"""Bulk delete users"""
data = {'uuids': user_uuids}
return await self._make_request('POST', '/api/users/bulk/delete', data)
# Debug method that was missing
async def debug_api_response(self, endpoint: str, method: str = 'GET', data: Optional[Dict] = None) -> Dict:
"""Debug function to check raw API responses"""
try:
# Ensure endpoint starts with /api/
if not endpoint.startswith('/api/'):
endpoint = '/api' + endpoint if not endpoint.startswith('/') else '/api/' + endpoint
@@ -941,14 +973,12 @@ class RemnaWaveAPI:
logger.info(f"DEBUG API Response: Status={response.status}, Content-Type={content_type}, Length={len(response_text)}")
# Try to parse JSON if it's the right content type
if 'application/json' in content_type:
try:
json_data = json.loads(response_text) if response_text else None
debug_info['json'] = json_data
debug_info['parsed_successfully'] = True
# Additional analysis
if isinstance(json_data, dict):
debug_info['response_keys'] = list(json_data.keys())
if 'data' in json_data:
@@ -978,9 +1008,7 @@ class RemnaWaveAPI:
'method': method
}
# Health check
async def get_system_health(self) -> Optional[Dict]:
"""Get system health status"""
try:
nodes = await self.get_all_nodes()
if not nodes:
@@ -1018,7 +1046,6 @@ class RemnaWaveAPI:
}
async def get_nodes_statistics(self) -> Optional[Dict]:
"""Get nodes statistics"""
try:
nodes = await self.get_all_nodes()
return {'data': nodes if nodes else []}
@@ -1026,9 +1053,7 @@ class RemnaWaveAPI:
logger.error(f"Error getting nodes statistics: {e}")
return {'data': []}
# ОБНОВЛЯЕМ метод get_all_subscriptions чтобы правильно обрабатывать URLs
async def get_all_subscriptions(self) -> Optional[List]:
"""Get all subscriptions from RemnaWave - ОБНОВЛЕННАЯ ВЕРСИЯ"""
try:
logger.info("Fetching all subscriptions from API")
result = await self._make_request('GET', '/api/subscriptions')
@@ -1039,7 +1064,6 @@ class RemnaWaveAPI:
subscriptions_list = []
# Extract subscriptions from response based on your API structure
if 'response' in result and 'subscriptions' in result['response']:
subscriptions_list = result['response']['subscriptions']
elif 'subscriptions' in result:
@@ -1049,13 +1073,11 @@ class RemnaWaveAPI:
elif isinstance(result, list):
subscriptions_list = result
# Process subscriptions to extract user data
processed_users = []
for subscription in subscriptions_list:
if subscription.get('isFound') and 'user' in subscription:
user_data = subscription['user']
# Map the subscription data to user format
processed_user = {
'shortUuid': user_data.get('shortUuid'),
'username': user_data.get('username'),
@@ -1065,12 +1087,10 @@ class RemnaWaveAPI:
'trafficUsed': user_data.get('trafficUsed', '0'),
'trafficLimit': user_data.get('trafficLimit', '0'),
'daysLeft': user_data.get('daysLeft', 0),
# ВАЖНО: Используем subscriptionUrl из самой подписки, а не генерируем
'subscriptionUrl': subscription.get('subscriptionUrl') or subscription.get('url'),
'links': subscription.get('links', [])
}
# Если нет URL в подписке, пытаемся получить его
if not processed_user.get('subscriptionUrl') and processed_user.get('shortUuid'):
try:
subscription_url = await self.get_subscription_url(processed_user['shortUuid'])
@@ -1088,17 +1108,14 @@ class RemnaWaveAPI:
return []
async def bulk_reset_all_traffic(self) -> Optional[Dict]:
"""Reset traffic for all users using the correct API endpoint"""
try:
logger.info("Attempting bulk traffic reset for all users")
# Get all users first
all_users = await self.get_all_system_users_full()
if not all_users:
logger.warning("No users found for bulk traffic reset")
return {'success': False, 'message': 'No users found'}
# Extract UUIDs (trying different possible fields)
user_uuids = []
for user in all_users:
user_uuid = user.get('uuid') or user.get('id') or user.get('shortUuid')
@@ -1111,12 +1128,10 @@ class RemnaWaveAPI:
logger.info(f"Resetting traffic for {len(user_uuids)} users")
# Try bulk operation first
bulk_result = await self.bulk_reset_traffic(user_uuids)
if bulk_result:
return {'success': True, 'affected_users': len(user_uuids)}
# Fallback to individual operations
success_count = 0
for user_uuid in user_uuids:
try:
@@ -1126,7 +1141,6 @@ class RemnaWaveAPI:
except Exception as e:
logger.error(f"Failed to reset traffic for user {user_uuid}: {e}")
# Small delay to avoid rate limiting
await asyncio.sleep(0.1)
return {
+432 -134
View File
@@ -1,6 +1,6 @@
import asyncio
import logging
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Optional, List
from database import Database
from remnawave_api import RemnaWaveAPI
@@ -8,7 +8,6 @@ from remnawave_api import RemnaWaveAPI
logger = logging.getLogger(__name__)
class SubscriptionMonitorService:
"""Service for monitoring subscriptions and sending notifications"""
def __init__(self, bot, db: Database, config, api: RemnaWaveAPI = None):
self.bot = bot
@@ -18,37 +17,63 @@ class SubscriptionMonitorService:
self.is_running = False
self._monitor_task = None
self._daily_task = None
self.last_check_time = None
async def start(self):
"""Start the monitoring service"""
if self.is_running:
logger.warning("Monitor service is already running")
return
self.is_running = True
if not getattr(self.config, 'MONITOR_ENABLED', True):
logger.info("Subscription monitoring disabled in config")
return
# Start periodic monitoring task
self._monitor_task = asyncio.create_task(self._monitor_loop())
logger.info("Starting subscription monitor service...")
# Start daily cleanup task
self._daily_task = asyncio.create_task(self._daily_loop())
logger.info("Subscription monitor service started")
try:
self.is_running = True
self._monitor_task = asyncio.create_task(self._monitor_loop())
logger.info(f"Started monitoring loop with interval {self.config.MONITOR_CHECK_INTERVAL}s")
self._daily_task = asyncio.create_task(self._daily_loop())
logger.info(f"Started daily cleanup task for {self.config.MONITOR_DAILY_CHECK_HOUR}:00")
await asyncio.sleep(0.1)
monitor_running = self._monitor_task and not self._monitor_task.done()
daily_running = self._daily_task and not self._daily_task.done()
if monitor_running and daily_running:
logger.info("✅ Subscription monitor service started successfully")
else:
logger.warning(f"⚠️ Monitor service partially started: monitor={monitor_running}, daily={daily_running}")
except Exception as e:
logger.error(f"❌ Failed to start monitor service: {e}", exc_info=True)
self.is_running = False
if self._monitor_task:
self._monitor_task.cancel()
if self._daily_task:
self._daily_task.cancel()
raise
async def stop(self):
"""Stop the monitoring service"""
if not self.is_running:
return
logger.info("Stopping subscription monitor service...")
self.is_running = False
# Cancel tasks
if self._monitor_task:
self._monitor_task.cancel()
try:
await self._monitor_task
except asyncio.CancelledError:
pass
logger.info("Monitor loop stopped")
if self._daily_task:
self._daily_task.cancel()
@@ -56,197 +81,395 @@ class SubscriptionMonitorService:
await self._daily_task
except asyncio.CancelledError:
pass
logger.info("Daily task stopped")
logger.info("Subscription monitor service stopped")
async def _monitor_loop(self):
"""Main monitoring loop"""
logger.info("🔄 Starting monitor loop")
logger.info("⏰ Initial check in 10 seconds...")
await asyncio.sleep(10)
while self.is_running:
try:
await self._check_expiring_subscriptions()
logger.info("🔍 Running periodic subscription check...")
warnings_sent = await self._check_expiring_subscriptions()
if warnings_sent > 0:
logger.info(f"✅ Monitor check completed: {warnings_sent} warnings sent")
else:
logger.info("✅ Monitor check completed: no warnings needed")
self.last_check_time = datetime.now()
logger.debug(f"😴 Sleeping for {self.config.MONITOR_CHECK_INTERVAL} seconds...")
await asyncio.sleep(self.config.MONITOR_CHECK_INTERVAL)
except asyncio.CancelledError:
logger.info("Monitor loop cancelled")
break
except Exception as e:
logger.error(f"Error in monitor loop: {e}")
await asyncio.sleep(60) # Wait 1 minute before retry
logger.error(f"Error in monitor loop: {e}", exc_info=True)
error_sleep = min(300, self.config.MONITOR_CHECK_INTERVAL // 2)
logger.info(f"😴 Error recovery: sleeping for {error_sleep} seconds...")
await asyncio.sleep(error_sleep)
async def _daily_loop(self):
"""Daily cleanup loop"""
logger.info("📅 Starting daily loop")
while self.is_running:
try:
now = datetime.now()
target_hour = self.config.MONITOR_DAILY_CHECK_HOUR
# Wait until the specified hour
if now.hour == self.config.MONITOR_DAILY_CHECK_HOUR:
await self.force_daily_check()
if now.hour == target_hour and now.minute < 30:
logger.info("📅 Starting scheduled daily check...")
deactivated = await self.force_daily_check()
logger.info(f"📅 Daily check completed: {deactivated} subscriptions deactivated")
# Wait until next day
tomorrow = now.replace(hour=self.config.MONITOR_DAILY_CHECK_HOUR, minute=0, second=0, microsecond=0) + timedelta(days=1)
sleep_time = (tomorrow - now).total_seconds()
next_check = now.replace(hour=target_hour, minute=0, second=0, microsecond=0) + timedelta(days=1)
sleep_time = (next_check - now).total_seconds()
logger.info(f"😴 Daily check completed, next check in {sleep_time/3600:.1f} hours")
else:
# Calculate time until next check
target_time = now.replace(hour=self.config.MONITOR_DAILY_CHECK_HOUR, minute=0, second=0, microsecond=0)
if target_time < now:
target_time = now.replace(hour=target_hour, minute=0, second=0, microsecond=0)
if target_time <= now:
target_time += timedelta(days=1)
sleep_time = (target_time - now).total_seconds()
logger.debug(f"😴 Next daily check in {sleep_time/3600:.1f} hours")
await asyncio.sleep(min(sleep_time, 3600)) # Check at least every hour
sleep_duration = min(sleep_time, 3600)
await asyncio.sleep(sleep_duration)
except asyncio.CancelledError:
logger.info("Daily loop cancelled")
break
except Exception as e:
logger.error(f"Error in daily loop: {e}")
logger.error(f"Error in daily loop: {e}", exc_info=True)
await asyncio.sleep(3600) # Wait 1 hour before retry
async def _check_expiring_subscriptions(self):
"""Check for expiring subscriptions and send warnings"""
try:
# Get all users
logger.info("🔍 Checking for expiring subscriptions...")
all_users = await self.db.get_all_users()
logger.info(f"👥 Found {len(all_users)} total users in database")
if not all_users:
logger.warning("⚠️ No users found in database")
return 0
warnings_sent = 0
errors_count = 0
total_subscriptions = 0
now_utc = datetime.utcnow()
logger.info(f"🕐 Current UTC time: {now_utc}")
for user in all_users:
try:
# Get expiring subscriptions for this user
expiring_subs = await self.db.get_expiring_subscriptions(
user.telegram_id,
self.config.MONITOR_WARNING_DAYS
)
for user_sub in expiring_subs:
await self._send_expiry_warning(user, user_sub)
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
total_subscriptions += len(user_subs)
if not user_subs:
logger.debug(f"👤 User {user.telegram_id} has no subscriptions")
continue
logger.debug(f"👤 User {user.telegram_id}: checking {len(user_subs)} subscriptions")
for user_sub in user_subs:
try:
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if not subscription:
logger.warning(f"⚠️ Subscription plan {user_sub.subscription_id} not found")
continue
except Exception as e:
logger.error(f"Error checking subscriptions for user {user.telegram_id}: {e}")
expires_at_utc = user_sub.expires_at
if expires_at_utc.tzinfo is None:
expires_at_utc = expires_at_utc.replace(tzinfo=None)
else:
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
time_diff = expires_at_utc - now_utc
hours_left = time_diff.total_seconds() / 3600
days_left = int(hours_left / 24)
logger.debug(f"📋 Subscription '{subscription.name}': "
f"expires_at={expires_at_utc}, "
f"now_utc={now_utc}, "
f"hours_left={hours_left:.1f}, "
f"days_left={days_left}, "
f"threshold={self.config.MONITOR_WARNING_DAYS}")
if subscription.is_trial:
logger.debug(f"⏭️ Skipping trial subscription '{subscription.name}'")
continue
if getattr(subscription, 'is_imported', False) or subscription.name == "Старая подписка":
logger.debug(f"⏭️ Skipping imported subscription '{subscription.name}'")
continue
should_warn = (
user_sub.is_active and
days_left <= self.config.MONITOR_WARNING_DAYS and
hours_left > 0
)
if should_warn:
logger.info(f"⚠️ Sending warning to user {user.telegram_id}: "
f"subscription '{subscription.name}' expires in {days_left} days "
f"({hours_left:.1f} hours)")
try:
await self._send_expiry_warning(user, user_sub)
warnings_sent += 1
logger.info(f"✅ Warning sent successfully to user {user.telegram_id}")
except Exception as warning_error:
logger.error(f"❌ Failed to send warning to user {user.telegram_id}: {warning_error}")
errors_count += 1
elif hours_left <= 0:
logger.debug(f"❌ Subscription '{subscription.name}' already expired {abs(hours_left):.1f} hours ago")
else:
logger.debug(f"✅ Subscription '{subscription.name}' is OK (expires in {days_left} days)")
except Exception as sub_error:
logger.error(f"❌ Error checking subscription {user_sub.id}: {sub_error}")
errors_count += 1
except Exception as user_error:
logger.error(f"❌ Error checking subscriptions for user {user.telegram_id}: {user_error}")
errors_count += 1
logger.info(f"📊 Check completed: {total_subscriptions} subscriptions checked, {warnings_sent} warnings sent, {errors_count} errors")
return warnings_sent
except Exception as e:
logger.error(f"Error in check_expiring_subscriptions: {e}")
logger.error(f"❌ Critical error in check_expiring_subscriptions: {e}", exc_info=True)
return 0
async def _send_expiry_warning(self, user, user_subscription):
"""Send expiry warning to user"""
try:
days_left = (user_subscription.expires_at - datetime.utcnow()).days
if not self.bot:
logger.error("❌ Bot instance is None, cannot send warning")
return
now_utc = datetime.utcnow()
expires_at_utc = user_subscription.expires_at
if expires_at_utc.tzinfo is None:
expires_at_utc = expires_at_utc.replace(tzinfo=None)
else:
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
time_diff = expires_at_utc - now_utc
hours_left = time_diff.total_seconds() / 3600
days_left = int(hours_left / 24)
subscription = await self.db.get_subscription_by_id(user_subscription.subscription_id)
if not subscription:
logger.warning(f"Subscription {user_subscription.subscription_id} not found")
return
message = self._format_expiry_message_with_action(subscription.name, days_left, user.language)
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from translations import t
keyboard = None
if hours_left > 0:
is_imported = (getattr(subscription, 'is_imported', False) or
subscription.name == "Старая подписка" or
(subscription.description and 'импорт' in subscription.description.lower()))
# Don't send warnings for trial subscriptions
if subscription.is_trial:
return
message = self._format_expiry_message(subscription.name, days_left, user.language)
await self.bot.send_message(user.telegram_id, message)
logger.info(f"Sent expiry warning to user {user.telegram_id} for subscription {subscription.name}")
if not is_imported:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t('extend_subscription_btn', user.language), callback_data=f"extend_sub_{user_subscription.id}")],
[InlineKeyboardButton(text=t('my_subscriptions_btn', user.language), callback_data="my_subscriptions")]
])
else:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t('my_subscriptions_btn', user.language), callback_data="my_subscriptions")],
[InlineKeyboardButton(text=t('buy_new_subscription_btn', user.language), callback_data="buy_subscription")]
])
else:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t('restore_subscription_btn', user.language), callback_data=f"extend_sub_{user_subscription.id}")],
[InlineKeyboardButton(text=t('my_subscriptions_btn', user.language), callback_data="my_subscriptions")]
])
await self.bot.send_message(user.telegram_id, message, reply_markup=keyboard)
if days_left == 0:
logger.info(f"✅ URGENT expiry warning sent to user {user.telegram_id} for subscription '{subscription.name}' (expires today, {hours_left:.1f} hours left)")
elif days_left > 0:
logger.info(f"✅ Expiry warning sent to user {user.telegram_id} for subscription '{subscription.name}' (expires in {days_left} days)")
else:
logger.info(f"✅ Expiry notification sent to user {user.telegram_id} for subscription '{subscription.name}' (already expired)")
except Exception as e:
logger.error(f"Error sending expiry warning: {e}")
logger.error(f"Error sending expiry warning to user {user.telegram_id}: {e}", exc_info=True)
raise
def _format_expiry_message(self, subscription_name: str, days_left: int, language: str = 'ru') -> str:
"""Format expiry warning message"""
if language == 'ru':
if days_left <= 0:
return f"⚠️ Ваша подписка '{subscription_name}' истекла!\n\nДля продления перейдите в раздел 'Мои подписки'."
elif days_left == 1:
return f"⚠️ Ваша подписка '{subscription_name}' истекает завтра!\n\nНе забудьте продлить её в разделе 'Мои подписки'."
else:
return f"⚠️ Ваша подписка '{subscription_name}' истекает через {days_left} дн.!\n\nРекомендуем продлить её заранее в разделе 'Мои подписки'."
def _format_expiry_message_with_action(self, subscription_name: str, days_left: int, language: str = 'ru') -> str:
from translations import t
if days_left < 0:
return t('subscription_expired_message', language, name=subscription_name)
elif days_left == 0:
return t('subscription_expires_today', language, name=subscription_name)
elif days_left == 1:
return t('subscription_expires_tomorrow', language, name=subscription_name)
elif days_left == 2:
return t('subscription_expires_day_after_tomorrow', language, name=subscription_name, days=days_left)
else:
if days_left <= 0:
return f"⚠️ Your subscription '{subscription_name}' has expired!\n\nTo renew, go to 'My Subscriptions'."
elif days_left == 1:
return f"⚠️ Your subscription '{subscription_name}' expires tomorrow!\n\nDon't forget to renew it in 'My Subscriptions'."
else:
return f"⚠️ Your subscription '{subscription_name}' expires in {days_left} days!\n\nWe recommend renewing it in advance in 'My Subscriptions'."
return t('subscription_expires_in_days', language, name=subscription_name, days=days_left)
async def force_daily_check(self):
"""Force daily check and cleanup"""
try:
logger.info("Starting daily check and cleanup")
logger.info("🚀 Starting forced daily check and cleanup")
# Deactivate expired subscriptions
logger.info("📢 Sending expiry warnings...")
warnings_sent = await self._check_expiring_subscriptions()
logger.info(f"📢 Sent {warnings_sent} expiry warnings")
logger.info("🔄 Deactivating expired subscriptions...")
deactivated_count = await self.deactivate_expired_subscriptions()
logger.info(f"🔄 Deactivated {deactivated_count} expired subscriptions")
# Send final expiry notifications
logger.info("📩 Sending final expiry notifications...")
await self._send_final_expiry_notifications()
logger.info("📩 Final notifications sent")
logger.info(f"Daily check completed. Deactivated {deactivated_count} expired subscriptions")
logger.info(f"Daily check completed successfully. Warnings: {warnings_sent}, Deactivated: {deactivated_count}")
return deactivated_count
except Exception as e:
logger.error(f"Error in force_daily_check: {e}")
logger.error(f"Error in force_daily_check: {e}", exc_info=True)
return 0
async def deactivate_expired_subscriptions(self) -> int:
"""Deactivate expired subscriptions"""
try:
count = 0
now_utc = datetime.utcnow()
logger.info(f"🔍 Checking for expired subscriptions (current UTC: {now_utc})")
all_users = await self.db.get_all_users()
logger.info(f"🔍 Checking {len(all_users)} users for expired subscriptions")
for user in all_users:
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
try:
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
for user_sub in user_subs:
if user_sub.is_active and user_sub.expires_at < datetime.utcnow():
user_sub.is_active = False
await self.db.update_user_subscription(user_sub)
count += 1
for user_sub in user_subs:
if user_sub.is_active:
expires_at_utc = user_sub.expires_at
if expires_at_utc.tzinfo is None:
expires_at_utc = expires_at_utc.replace(tzinfo=None)
else:
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
# Also try to deactivate in RemnaWave if possible
if self.api and user_sub.short_uuid:
try:
user_data = await self.api.get_user_by_short_uuid(user_sub.short_uuid)
if user_data and user_data.get('uuid'):
await self.api.update_user(user_data['uuid'], {'status': 'EXPIRED'})
except Exception as e:
logger.warning(f"Could not deactivate user in RemnaWave: {e}")
time_diff = expires_at_utc - now_utc
hours_until_expiry = time_diff.total_seconds() / 3600
if hours_until_expiry < -1.0:
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
subscription_name = subscription.name if subscription else f"ID:{user_sub.subscription_id}"
logger.info(f"❌ Deactivating truly expired subscription '{subscription_name}' "
f"for user {user.telegram_id} (expired {abs(hours_until_expiry):.1f} hours ago)")
user_sub.is_active = False
success = await self.db.update_user_subscription(user_sub)
if success:
count += 1
logger.info(f"✅ Successfully deactivated subscription '{subscription_name}' for user {user.telegram_id}")
if self.api and user_sub.short_uuid:
try:
user_data = await self.api.get_user_by_short_uuid(user_sub.short_uuid)
if user_data and user_data.get('uuid'):
await self.api.update_user(user_data['uuid'], {'status': 'EXPIRED'})
logger.debug(f"🔄 Also deactivated user {user_data['uuid']} in RemnaWave")
except Exception as api_error:
logger.warning(f"⚠️ Could not deactivate user in RemnaWave: {api_error}")
else:
logger.error(f"❌ Failed to deactivate subscription {user_sub.id} in database")
else:
logger.debug(f"✅ Subscription expires in {hours_until_expiry:.1f} hours - not deactivating yet")
except Exception as user_error:
logger.error(f"❌ Error processing user {user.telegram_id}: {user_error}")
logger.info(f"✅ Deactivation completed: {count} subscriptions deactivated")
return count
except Exception as e:
logger.error(f"Error deactivating expired subscriptions: {e}")
logger.error(f"Error deactivating expired subscriptions: {e}", exc_info=True)
return 0
async def _send_final_expiry_notifications(self):
"""Send final notifications for just-expired subscriptions"""
try:
# Get subscriptions that expired today
all_users = await self.db.get_all_users()
notifications_sent = 0
now_utc = datetime.utcnow()
logger.info(f"📩 Checking for subscriptions that expired recently (current UTC: {now_utc})")
for user in all_users:
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
try:
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
for user_sub in user_subs:
# Check if subscription expired today (within last 24 hours)
time_since_expiry = datetime.utcnow() - user_sub.expires_at
for user_sub in user_subs:
expires_at_utc = user_sub.expires_at
if expires_at_utc.tzinfo is None:
expires_at_utc = expires_at_utc.replace(tzinfo=None)
else:
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
if (time_since_expiry.total_seconds() > 0 and
time_since_expiry.total_seconds() <= 86400): # 24 hours
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if subscription and not subscription.is_trial:
message = self._format_expiry_message(subscription.name, 0, user.language)
await self.bot.send_message(user.telegram_id, message)
time_since_expiry = now_utc - expires_at_utc
hours_since_expiry = time_since_expiry.total_seconds() / 3600
if 2 <= hours_since_expiry <= 24:
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if subscription and not subscription.is_trial and not getattr(subscription, 'is_imported', False):
message = self._format_expiry_message_with_action(subscription.name, 0, user.language)
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Восстановить подписку", callback_data=f"extend_sub_{user_sub.id}")],
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")]
])
await self.bot.send_message(user.telegram_id, message, reply_markup=keyboard)
notifications_sent += 1
logger.info(f"📩 Sent final expiry notification for subscription '{subscription.name}' to user {user.telegram_id} (expired {hours_since_expiry:.1f} hours ago)")
except Exception as e:
logger.error(f"❌ Error sending final notification to user {user.telegram_id}: {e}")
if notifications_sent > 0:
logger.info(f"📩 Sent {notifications_sent} final expiry notifications")
except Exception as e:
logger.error(f"Error sending final expiry notifications: {e}")
logger.error(f"Error sending final expiry notifications: {e}", exc_info=True)
async def check_single_user(self, user_id: int):
"""Check subscriptions for a single user (for testing)"""
try:
results = []
logger.info(f"🧪 Testing monitor for user {user_id}")
user = await self.db.get_user_by_telegram_id(user_id)
if not user:
results.append({
'success': False,
'message': f'User {user_id} not found',
'message': f'User {user_id} not found in database',
'error': None
})
return results
# Get user subscriptions
user_subs = await self.db.get_user_subscriptions(user_id)
if not user_subs:
results.append({
'success': True,
@@ -254,56 +477,131 @@ class SubscriptionMonitorService:
'error': None
})
return results
logger.info(f"🧪 Found {len(user_subs)} subscriptions for user {user_id}")
now_utc = datetime.utcnow()
for user_sub in user_subs:
try:
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
days_left = (user_sub.expires_at - datetime.utcnow()).days
if days_left <= self.config.MONITOR_WARNING_DAYS:
# Send test notification
message = self._format_expiry_message(subscription.name, days_left, user.language)
await self.bot.send_message(user_id, f"[ТЕСТ] {message}")
if not subscription:
results.append({
'success': False,
'message': f'Subscription plan {user_sub.subscription_id} not found',
'error': None
})
continue
expires_at_utc = user_sub.expires_at
if expires_at_utc.tzinfo is None:
expires_at_utc = expires_at_utc.replace(tzinfo=None)
else:
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
time_diff = expires_at_utc - now_utc
hours_left = time_diff.total_seconds() / 3600
days_left = int(hours_left / 24)
if subscription.is_trial:
results.append({
'success': True,
'message': f'Sent warning for subscription "{subscription.name}" (expires in {days_left} days)',
'message': f'Trial subscription "{subscription.name}" skipped (no warnings for trials)',
'error': None
})
continue
if getattr(subscription, 'is_imported', False) or subscription.name == "Старая подписка":
results.append({
'success': True,
'message': f'Imported subscription "{subscription.name}" skipped (cannot be renewed)',
'error': None
})
continue
if days_left <= self.config.MONITOR_WARNING_DAYS and hours_left > 0:
message = self._format_expiry_message_with_action(subscription.name, days_left, user.language)
test_message = f"🧪 [ТЕСТОВОЕ УВЕДОМЛЕНИЕ]\n\n{message}"
if self.bot:
try:
await self.bot.send_message(user_id, test_message)
logger.info(f"✅ Test warning sent to user {user_id}")
results.append({
'success': True,
'message': f'✅ Sent test warning for subscription "{subscription.name}" (expires in {days_left} days / {hours_left:.1f} hours)',
'error': None
})
except Exception as send_error:
logger.error(f"❌ Failed to send test message: {send_error}")
results.append({
'success': False,
'message': f'❌ Failed to send test warning for "{subscription.name}"',
'error': str(send_error)
})
else:
logger.error("❌ Bot instance is None")
results.append({
'success': False,
'message': f'❌ Bot instance unavailable for "{subscription.name}"',
'error': 'Bot is None'
})
elif hours_left <= 0:
results.append({
'success': True,
'message': f'❌ Subscription "{subscription.name}" already expired {abs(hours_left):.1f} hours ago',
'error': None
})
else:
results.append({
'success': True,
'message': f'Subscription "{subscription.name}" is OK (expires in {days_left} days)',
'message': f'Subscription "{subscription.name}" is OK (expires in {days_left} days / {hours_left:.1f} hours, warning threshold: {self.config.MONITOR_WARNING_DAYS} days)',
'error': None
})
except Exception as e:
logger.error(f"❌ Error checking subscription {user_sub.id}: {e}")
results.append({
'success': False,
'message': f'Error checking subscription ID {user_sub.id}',
'error': str(e)
})
return results
except Exception as e:
logger.error(f"❌ Error in check_single_user: {e}", exc_info=True)
return [{
'success': False,
'message': f'Error checking user {user_id}',
'message': f'Critical error checking user {user_id}',
'error': str(e)
}]
async def get_service_status(self) -> dict:
"""Get service status information"""
monitor_running = self._monitor_task is not None and not self._monitor_task.done()
daily_running = self._daily_task is not None and not self._daily_task.done()
return {
'is_running': self.is_running,
'is_running': self.is_running and (monitor_running or daily_running),
'monitor_enabled': getattr(self.config, 'MONITOR_ENABLED', True),
'check_interval': self.config.MONITOR_CHECK_INTERVAL,
'daily_check_hour': self.config.MONITOR_DAILY_CHECK_HOUR,
'warning_days': self.config.MONITOR_WARNING_DAYS,
'last_check': datetime.now().strftime("%Y-%m-%d %H:%M:%S") if self.is_running else None
'last_check': self.last_check_time.strftime("%Y-%m-%d %H:%M:%S") if self.last_check_time else None,
'has_tasks': {
'monitor_task': monitor_running,
'daily_task': daily_running
},
'task_status': {
'monitor_task': 'running' if monitor_running else 'stopped',
'daily_task': 'running' if daily_running else 'stopped'
}
}
async def create_subscription_monitor(bot, db: Database, config, api: RemnaWaveAPI = None) -> SubscriptionMonitorService:
"""Create and return subscription monitor service"""
return SubscriptionMonitorService(bot, db, config, api)
logger.info("🏭 Creating subscription monitor service...")
service = SubscriptionMonitorService(bot, db, config, api)
logger.info("✅ Subscription monitor service created")
return service
+66 -61
View File
@@ -1,10 +1,7 @@
TRANSLATIONS = {
'ru': {
# Language selection
'select_language': '🌍 Выберите язык / Choose language',
'language_selected': '🇷🇺 Язык выбран: Русский',
# Main menu
'main_menu': '📱 Главное меню',
'balance': 'Баланс',
'my_subscriptions': 'Мои подписки',
@@ -18,9 +15,6 @@ TRANSLATIONS = {
'trial_error': '❌ Ошибка при создании тестовой подписки',
'trial_info': '🧪 Тестовая подписка выдается на три дня!\n\nНа тарифе действует ограничение в 3 дня\n\nОграничение по трафику - 2гб',
'subscriptions_list': '📋 Список подписок в продаже:',
# Balance menu
'your_balance': '💰 Ваш баланс: {balance:.2f} руб.',
'topup_balance': 'Пополнить баланс',
'payment_history': 'История платежей',
@@ -31,6 +25,11 @@ TRANSLATIONS = {
'nodes_management': 'Управление нодами',
'system_users': 'Системные пользователи',
'system_statistics': 'Системная статистика',
'subscription_expired_message': '❌ Ваша подписка \'{name}\' истекла!\n\n🔄 Для возобновления доступа продлите подписку в разделе \'Мои подписки\'.\n\n💡 Подписку можно продлить в любой момент - срок действия будет увеличен с текущего момента.',
'subscription_expires_today': '⚠️ Срочно! Ваша подписка \'{name}\' истекает сегодня!\n\n🔄 Продлите её прямо сейчас в разделе \'Мои подписки\', чтобы не потерять доступ.\n\n⏰ У вас есть несколько часов до истечения!',
'subscription_expires_tomorrow': '⚠️ Внимание! Ваша подписка \'{name}\' истекает завтра!\n\n🔄 Чтобы не потерять доступ, продлите её сейчас в разделе \'Мои подписки\'.\n\n✨ При продлении срок действия будет увеличен с момента истечения.',
'subscription_expires_day_after_tomorrow': '⏰ Ваша подписка \'{name}\' истекает послезавтра ({days} дня)!\n\n🔄 Рекомендуем продлить её заранее в разделе \'Мои подписки\'.\n\n💰 Проверьте баланс - возможно, стоит пополнить его заранее.',
'subscription_expires_in_days': '⏳ Ваша подписка \'{name}\' истекает через {days} дн.!\n\n🔄 Продлить подписку можно в разделе \'Мои подписки\'.\n\n💡 Продление заранее увеличивает срок с момента истечения текущей подписки.',
'restart_nodes': 'Перезагрузить ноды',
'bulk_operations': 'Массовые операции',
'search_user': 'Поиск пользователя',
@@ -38,7 +37,6 @@ TRANSLATIONS = {
'reset_traffic': 'Сбросить трафик',
'disable_user': 'Отключить пользователя',
'enable_user': 'Включить пользователя',
'send_message': 'Отправить сообщение',
'send_to_user': 'Отправить пользователю',
'send_to_all': 'Отправить всем',
@@ -53,8 +51,6 @@ TRANSLATIONS = {
'subscription_extended': '✅ Подписка продлена',
'extend_confirmation': 'Продлить подписку "{name}" на {days} дней за {price:.2f} руб.?',
'subscription_expires_soon': 'Подписка истекает через {days} дней',
# Subscriptions
'no_subscriptions': '❌ У вас нет активных подписок',
'subscription_info': '📋 Подписка: {name}\n💰 Цена: {price:.2f} руб.\n⏱ Длительность: {days} дней\n📊 Трафик: {traffic}\n\n{description}',
'unlimited_traffic': 'Безлимитный',
@@ -65,16 +61,12 @@ TRANSLATIONS = {
'subscription_expired': 'Истекла',
'subscription_active': 'Активна до: {date}',
'your_subscriptions': '👀 Список ваших подписок:',
# Payments
'payment_created': '✅ Заявка на пополнение создана.\nОбратитесь к @{support} для оплаты.',
'enter_amount': '💰 Введите сумму для пополнения (руб.):',
'invalid_amount': '❌ Неверная сумма',
'payment_card_info': '💳 Для пополнения картой свяжитесь с @{support}',
'no_payments': '❌ История платежей пуста',
'payment_item': '{date}: {amount:.2f} руб. - {description} ({status})',
# Promocodes
'enter_promocode': '🎁 Введите промокод:',
'promocode_success': '✅ Промокод активирован! Скидка: {discount}',
'promocode_not_found': '❌ Промокод не найден',
@@ -95,16 +87,10 @@ TRANSLATIONS = {
'referral_percentage': 'Процент с платежей: 25%',
'no_referrals': 'У вас пока нет рефералов',
'referral_welcome': 'Добро пожаловать по реферальной ссылке!',
# Purchase
'insufficient_balance': '❌ Недостаточно средств на балансе',
'subscription_purchased': '✅ Подписка успешно приобретена!',
'purchase_error': '❌ Ошибка при покупке подписки',
# Support
'support_message': '🎧 Для получения поддержки обратитесь к @{support}',
# Admin panel
'admin_menu': '⚙️ Админ панель',
'manage_subscriptions': 'Управление подписками',
'manage_users': 'Управление пользователями',
@@ -112,8 +98,6 @@ TRANSLATIONS = {
'manage_promocodes': 'Управление промокодами',
'statistics': 'Статистика',
'not_admin': '❌ У вас нет прав администратора',
# Admin - subscriptions
'create_subscription': 'Создать подписку',
'enter_sub_name': 'Введите название подписки:',
'enter_sub_description': 'Введите описание подписки:',
@@ -122,43 +106,58 @@ TRANSLATIONS = {
'enter_sub_traffic': 'Введите лимит трафика (ГБ, 0 для безлимита):',
'enter_squad_uuid': 'Введите UUID squad из панели:',
'subscription_created': '✅ Подписка создана',
# Admin - users
'extend_subscription_btn': '🔄 Продлить подписку',
'my_subscriptions_btn': '📋 Мои подписки',
'buy_new_subscription_btn': '🛒 Купить новую подписку',
'restore_subscription_btn': '🔄 Восстановить подписку',
'user_list': 'Список пользователей:',
'user_item': 'ID: {id}, @{username}, Баланс: {balance:.2f} руб.',
'enter_user_id': 'Введите Telegram ID пользователя:',
'enter_balance_amount': 'Введите сумму для пополнения:',
'balance_added': '✅ Баланс пополнен',
'user_not_found': '❌ Пользователь не найден',
# Admin - promocodes
'create_promocode': 'Создать промокод',
'enter_promo_code': 'Введите код промокода:',
'enter_promo_discount': 'Введите размер скидки (руб.):',
'enter_promo_limit': 'Введите лимит использований:',
'promocode_created': '✅ Промокод создан',
'promocode_exists': '❌ Промокод уже существует',
# Statistics
'stats_info': '📊 Статистика:\n👥 Пользователей: {users}\n📋 Подписок: {subscriptions}\n💰 Выручка: {revenue:.2f} руб.',
# Errors
'error_occurred': '❌ Произошла ошибка',
'try_again': 'Попробуйте еще раз',
'invalid_input': '❌ Неверный ввод',
'cancel': 'Отмена',
# Connection
'connection_link': '🔗 Ссылка для подключения:\n\n`{link}`\n\nНажмите на ссылку чтобы скопировать',
'connection_error': '❌ Ошибка получения ссылки подключения',
'lucky_game': 'Игра удачи',
'lucky_game_title': '🎰 Проверь свою удачу!',
'lucky_game_description': 'Выберите число от 1 до {numbers} и попытайтесь угадать выигрышное!',
'lucky_game_reward': 'Награда за выигрыш: {reward}',
'lucky_game_rules': 'Правила игры',
'lucky_game_play': 'Играть!',
'lucky_game_history': 'История игр',
'lucky_game_stats': 'Ваша статистика',
'lucky_game_choose_number': 'Выберите число от 1 до {numbers}',
'lucky_game_winner': '🎉 ПОЗДРАВЛЯЕМ! ВЫ ВЫИГРАЛИ!',
'lucky_game_loser': '😔 Не повезло в этот раз...',
'lucky_game_your_number': 'Ваше число: {number}',
'lucky_game_winning_numbers': 'Выигрышные числа: {numbers}',
'lucky_game_reward_added': 'Награда {reward}₽ зачислена на баланс!',
'lucky_game_new_balance': 'Новый баланс: {balance}',
'lucky_game_try_tomorrow': 'Попробуйте завтра!',
'lucky_game_already_played': 'Вы уже играли сегодня! Приходите завтра.',
'lucky_game_come_back_in': 'Приходи через {time}',
'lucky_game_no_history': 'Вы еще не играли в игру удачи.',
'lucky_game_start_playing': 'Начните играть, чтобы увидеть историю!',
'lucky_game_games_played': 'Игр сыграно: {count}',
'lucky_game_wins': 'Выигрышей: {count}',
'lucky_game_total_won': 'Всего выиграно: {amount}',
'lucky_game_win_rate': 'Процент побед: {rate}%'
},
'en': {
# Language selection
'select_language': '🌍 Choose language / Выберите язык',
'language_selected': '🇺🇸 Language selected: English',
# Main menu
'main_menu': '📱 Main Menu',
'balance': 'Balance',
'my_subscriptions': 'My Subscriptions',
@@ -184,16 +183,12 @@ TRANSLATIONS = {
'reset_traffic': 'Reset Traffic',
'disable_user': 'Disable User',
'enable_user': 'Enable User',
# Balance menu
'your_balance': '💰 Your balance: ${balance:.2f}',
'topup_balance': 'Top up balance',
'payment_history': 'Payment history',
'topup_card': 'Card payment',
'topup_support': 'Through support',
'back': 'Back',
# Subscriptions
'no_subscriptions': '❌ You have no active subscriptions',
'subscription_info': '📋 Subscription: {name}\n💰 Price: ${price:.2f}\n⏱ Duration: {days} days\n📊 Traffic: {traffic}\n\n{description}',
'unlimited_traffic': 'Unlimited',
@@ -203,8 +198,15 @@ TRANSLATIONS = {
'subscription_expires': 'Expires: {date}',
'subscription_expired': 'Expired',
'subscription_active': 'Active until: {date}',
# Payments
'subscription_expired_message': '❌ Your subscription \'{name}\' has expired!\n\nTo renew, go to \'My Subscriptions\'.\n\n💡 You can renew at any time - the term will be extended from the current moment.',
'subscription_expires_today': '⚠️ Urgent! Your subscription \'{name}\' expires today!\n\nRenew it now in \'My Subscriptions\' to keep access.\n\n⏰ You have a few hours left!',
'subscription_expires_tomorrow': '⚠️ Your subscription \'{name}\' expires tomorrow!\n\nDon\'t forget to renew it in \'My Subscriptions\'.\n\n✨ Renewal will extend the term from expiration date.',
'subscription_expires_day_after_tomorrow': '⏰ Your subscription \'{name}\' expires in {days} days!\n\nWe recommend renewing it in advance in \'My Subscriptions\'.\n\n💰 Check your balance - you might want to top it up.',
'subscription_expires_in_days': '⏳ Your subscription \'{name}\' expires in {days} days!\n\nYou can renew it in \'My Subscriptions\'.\n\n💡 Early renewal extends from current expiration date.',
'extend_subscription_btn': '🔄 Extend Subscription',
'my_subscriptions_btn': '📋 My Subscriptions',
'buy_new_subscription_btn': '🛒 Buy New Subscription',
'restore_subscription_btn': '🔄 Restore Subscription',
'payment_created': '✅ Top-up request created.\nContact @{support} for payment.',
'enter_amount': '💰 Enter amount to top up ($):',
'invalid_amount': '❌ Invalid amount',
@@ -225,24 +227,16 @@ TRANSLATIONS = {
'referral_percentage': 'Percentage from payments: 25%',
'no_referrals': 'You have no referrals yet',
'referral_welcome': 'Welcome via referral link!',
# Promocodes
'enter_promocode': '🎁 Enter promocode:',
'promocode_success': '✅ Promocode activated! Discount: {discount}',
'promocode_not_found': '❌ Promocode not found',
'promocode_expired': '❌ Promocode expired',
'promocode_used': '❌ Promocode already used',
'promocode_limit': '❌ Usage limit reached',
# Purchase
'insufficient_balance': '❌ Insufficient balance',
'subscription_purchased': '✅ Subscription purchased successfully!',
'purchase_error': '❌ Error purchasing subscription',
# Support
'support_message': '🎧 For support contact @{support}',
# Admin panel
'admin_menu': '⚙️ Admin Panel',
'manage_subscriptions': 'Manage Subscriptions',
'manage_users': 'Manage Users',
@@ -250,8 +244,6 @@ TRANSLATIONS = {
'manage_promocodes': 'Manage Promocodes',
'statistics': 'Statistics',
'not_admin': '❌ You don\'t have admin rights',
# Admin - subscriptions
'create_subscription': 'Create Subscription',
'enter_sub_name': 'Enter subscription name:',
'enter_sub_description': 'Enter subscription description:',
@@ -260,40 +252,53 @@ TRANSLATIONS = {
'enter_sub_traffic': 'Enter traffic limit (GB, 0 for unlimited):',
'enter_squad_uuid': 'Enter squad UUID from panel:',
'subscription_created': '✅ Subscription created',
# Admin - users
'user_list': '👥 User list:',
'user_item': 'ID: {id}, @{username}, Balance: ${balance:.2f}',
'enter_user_id': 'Enter user Telegram ID:',
'enter_balance_amount': 'Enter amount to add:',
'balance_added': '✅ Balance added',
'user_not_found': '❌ User not found',
# Admin - promocodes
'create_promocode': 'Create Promocode',
'enter_promo_code': 'Enter promocode:',
'enter_promo_discount': 'Enter discount amount ($):',
'enter_promo_limit': 'Enter usage limit:',
'promocode_created': '✅ Promocode created',
'promocode_exists': '❌ Promocode already exists',
# Statistics
'stats_info': '📊 Statistics:\n👥 Users: {users}\n📋 Subscriptions: {subscriptions}\n💰 Revenue: ${revenue:.2f}',
# Errors
'error_occurred': '❌ An error occurred',
'try_again': 'Try again',
'invalid_input': '❌ Invalid input',
'cancel': 'Cancel',
# Connection
'connection_link': '🔗 Connection link:\n\n`{link}`\n\nClick the link to copy',
'connection_error': '❌ Error getting connection link',
'lucky_game': 'Lucky Game',
'lucky_game_title': '🎰 Test Your Luck!',
'lucky_game_description': 'Choose a number from 1 to {numbers} and try to guess the winning one!',
'lucky_game_reward': 'Winning reward: ${reward}',
'lucky_game_rules': 'Game Rules',
'lucky_game_play': 'Play!',
'lucky_game_history': 'Game History',
'lucky_game_stats': 'Your Statistics',
'lucky_game_choose_number': 'Choose a number from 1 to {numbers}',
'lucky_game_winner': '🎉 CONGRATULATIONS! YOU WON!',
'lucky_game_loser': '😔 Better luck next time...',
'lucky_game_your_number': 'Your number: {number}',
'lucky_game_winning_numbers': 'Winning numbers: {numbers}',
'lucky_game_reward_added': 'Reward ${reward} added to balance!',
'lucky_game_new_balance': 'New balance: ${balance}',
'lucky_game_try_tomorrow': 'Try again tomorrow!',
'lucky_game_already_played': 'You already played today! Come back tomorrow.',
'lucky_game_come_back_in': 'Come back in {time}',
'lucky_game_no_history': 'You haven\'t played the lucky game yet.',
'lucky_game_start_playing': 'Start playing to see history!',
'lucky_game_games_played': 'Games played: {count}',
'lucky_game_wins': 'Wins: {count}',
'lucky_game_total_won': 'Total won: ${amount}',
'lucky_game_win_rate': 'Win rate: {rate}%'
}
}
def t(key: str, lang: str = 'ru', **kwargs) -> str:
"""Get translation for key with optional formatting"""
translation = TRANSLATIONS.get(lang, TRANSLATIONS['ru']).get(key, key)
if kwargs:
try:
+36 -63
View File
@@ -4,26 +4,22 @@ import string
from datetime import datetime, timedelta
from typing import Tuple, Optional, Dict, Any
from database import Database
from database import ReferralProgram, ReferralEarning # ДОБАВЛЕНО: импорт моделей
from database import ReferralProgram, ReferralEarning
logger = logging.getLogger(__name__)
def is_valid_amount(text: str) -> Tuple[bool, float]:
"""Validate amount input"""
try:
# Remove spaces and replace comma with dot
text = text.strip().replace(' ', '').replace(',', '.')
amount = float(text)
# Check if amount is positive and reasonable
if amount <= 0:
return False, 0.0
if amount > 1000000: # Max 1M rubles
if amount > 1000000:
return False, 0.0
# Round to 2 decimal places
amount = round(amount, 2)
return True, amount
@@ -32,35 +28,28 @@ def is_valid_amount(text: str) -> Tuple[bool, float]:
return False, 0.0
def validate_promocode_format(code: str) -> bool:
"""Validate promocode format"""
if not code:
return False
# Remove spaces and convert to uppercase
code = code.strip().upper()
# Check length
if len(code) < 3 or len(code) > 20:
return False
# Check that code contains only letters and numbers
if not code.replace('_', '').isalnum():
return False
return True
def validate_squad_uuid(uuid: str) -> bool:
"""Validate squad UUID format"""
if not uuid or not isinstance(uuid, str):
return False
uuid = uuid.strip()
# Basic UUID format validation
if len(uuid) < 8:
return False
# UUID should contain only hex characters and hyphens
allowed_chars = set('0123456789abcdefABCDEF-')
if not all(c in allowed_chars for c in uuid):
return False
@@ -68,23 +57,18 @@ def validate_squad_uuid(uuid: str) -> bool:
return True
def parse_telegram_id(text: str) -> Optional[int]:
"""Parse Telegram ID from text"""
try:
# Remove spaces and common prefixes
text = text.strip().replace(' ', '')
# Remove common prefixes
if text.startswith('@'):
text = text[1:]
if text.startswith('id'):
text = text[2:]
# Try to convert to int
telegram_id = int(text)
# Validate that it's a reasonable Telegram ID
if telegram_id <= 0 or telegram_id > 9999999999: # Max 10 digits
if telegram_id <= 0 or telegram_id > 9999999999:
return None
return telegram_id
@@ -93,22 +77,18 @@ def parse_telegram_id(text: str) -> Optional[int]:
return None
def generate_username() -> str:
"""Generate random username for RemnaWave"""
prefix = "user_"
random_part = ''.join(secrets.choice(string.ascii_lowercase + string.digits) for _ in range(8))
return prefix + random_part
def generate_password() -> str:
"""Generate random password for RemnaWave"""
return ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(12))
def calculate_expiry_date(days: int) -> str:
"""Calculate expiry date for subscription"""
expiry_date = datetime.now() + timedelta(days=days)
return expiry_date.isoformat() + 'Z'
def format_datetime(dt: datetime, language: str = 'ru') -> str:
"""Format datetime for display"""
if not dt:
return "N/A"
@@ -118,7 +98,6 @@ def format_datetime(dt: datetime, language: str = 'ru') -> str:
return dt.strftime('%Y-%m-%d %H:%M')
def format_date(dt: datetime, language: str = 'ru') -> str:
"""Format date for display"""
if not dt:
return "N/A"
@@ -128,7 +107,6 @@ def format_date(dt: datetime, language: str = 'ru') -> str:
return dt.strftime('%Y-%m-%d')
def format_bytes(bytes_value: int) -> str:
"""Format bytes to human readable format"""
if bytes_value == 0:
return "0 B"
@@ -148,7 +126,6 @@ def format_bytes(bytes_value: int) -> str:
return f"{value:.2f} {units[unit_index]}"
def format_payment_status(status: str, language: str = 'ru') -> str:
"""Format payment status for display"""
status_map = {
'ru': {
'pending': 'Ожидает',
@@ -167,7 +144,6 @@ def format_payment_status(status: str, language: str = 'ru') -> str:
return status_map.get(language, status_map['ru']).get(status, status)
def format_subscription_info(subscription: Dict[str, Any], language: str = 'ru') -> str:
"""Format subscription information for display"""
text = ""
if language == 'ru':
@@ -199,13 +175,11 @@ def format_subscription_info(subscription: Dict[str, Any], language: str = 'ru')
def format_user_subscription_info(user_sub: Dict[str, Any], subscription: Dict[str, Any],
expires_at: datetime, language: str = 'ru') -> str:
"""Format user subscription information for display - ИСПРАВЛЕНО: добавлены пометки для импортированных"""
text = ""
if language == 'ru':
text += f"📋 **{subscription['name']}**\n\n"
# Status
now = datetime.utcnow()
if expires_at < now:
status = "❌ Истекла"
@@ -223,7 +197,6 @@ def format_user_subscription_info(user_sub: Dict[str, Any], subscription: Dict[s
if days_left > 0:
text += f"⏰ Осталось: {days_left} дн.\n"
# Traffic info
if subscription['traffic_limit_gb'] > 0:
text += f"📊 Лимит трафика: {subscription['traffic_limit_gb']} ГБ\n"
else:
@@ -233,13 +206,11 @@ def format_user_subscription_info(user_sub: Dict[str, Any], subscription: Dict[s
text += f"\n🔄 Тип: Импортированная из старой системы\n"
text += f"ℹ️ Продление недоступно"
# Description
if subscription.get('description') and not ('импорт' in subscription.get('description', '').lower()):
text += f"\n📝 {subscription['description']}"
else:
text += f"📋 **{subscription['name']}**\n\n"
# Status
now = datetime.utcnow()
if expires_at < now:
status = "❌ Expired"
@@ -257,7 +228,6 @@ def format_user_subscription_info(user_sub: Dict[str, Any], subscription: Dict[s
if days_left > 0:
text += f"⏰ Days left: {days_left}\n"
# Traffic info
if subscription['traffic_limit_gb'] > 0:
text += f"📊 Traffic limit: {subscription['traffic_limit_gb']} GB\n"
else:
@@ -267,20 +237,16 @@ def format_user_subscription_info(user_sub: Dict[str, Any], subscription: Dict[s
text += f"\n🔄 Type: Imported from old system\n"
text += f"️ Extension not available"
# Description
if subscription.get('description') and not ('import' in subscription.get('description', '').lower()):
text += f"\n📝 {subscription['description']}"
return text
def log_user_action(user_id: int, action: str, details: str = ""):
"""Log user action"""
logger.info(f"USER_ACTION: {user_id} - {action}" + (f" - {details}" if details else ""))
async def process_referral_rewards(user_id: int, amount: float, payment_id: int, db: Database, bot=None):
"""Process referral rewards after successful payment - ИСПРАВЛЕНА СИГНАТУРА"""
try:
# Получаем конфигурацию из переменных окружения напрямую
import os
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
@@ -288,14 +254,12 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
referred_bonus = float(os.getenv('REFERRAL_REFERRED_BONUS', '150.0'))
percentage = float(os.getenv('REFERRAL_PERCENTAGE', '0.25'))
# Проверяем есть ли у пользователя реферер
referral = await db.get_referral_by_referred_id(user_id)
if not referral:
logger.debug(f"No referral found for user {user_id}")
return
# Получаем информацию о пользователе
user = await db.get_user_by_telegram_id(user_id)
if not user:
logger.error(f"User {user_id} not found")
@@ -308,7 +272,6 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
await db.add_balance(referral.referrer_id, first_reward)
# Создаем запись о платеже для реферера
await db.create_payment(
user_id=referral.referrer_id,
amount=first_reward,
@@ -317,7 +280,6 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
status='completed'
)
# Выплачиваем первую награду рефереру (записываем в историю)
success = await db.create_referral_earning(
referrer_id=referral.referrer_id,
referred_id=user_id,
@@ -331,7 +293,6 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
if bot:
try:
# Уведомляем реферера
await bot.send_message(
referral.referrer_id,
f"🎉 Поздравляем! Ваш реферал пополнил баланс на {threshold}₽+\n\n"
@@ -339,13 +300,11 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
f"Теперь вы будете получать {percentage*100:.0f}% с каждого его следующего платежа."
)
# Уведомляем самого пользователя
await bot.send_message(
user_id,
f"🎁 Бонус активирован! Вам начислено {referred_bonus}₽ за переход по реферальной ссылке!"
)
# Добавляем бонус рефералу
await db.add_balance(user_id, referred_bonus)
await db.create_payment(
user_id=user_id,
@@ -362,13 +321,12 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
else:
logger.error(f"Failed to create first reward earning")
if amount > 0 and referral.first_reward_paid: # ИЗМЕНЕНО: добавлено условие first_reward_paid
if amount > 0 and referral.first_reward_paid:
percentage_reward = amount * percentage
if percentage_reward >= 0.01: # Минимум 1 копейка
if percentage_reward >= 0.01:
await db.add_balance(referral.referrer_id, percentage_reward)
# Создаем запись о платеже для реферера
await db.create_payment(
user_id=referral.referrer_id,
amount=percentage_reward,
@@ -388,7 +346,7 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
if success:
logger.info(f"Percentage reward paid: {percentage_reward:.2f}₽ ({percentage*100:.0f}%) to referrer {referral.referrer_id}")
if bot and percentage_reward >= 1.0: # Уведомляем только если сумма >= 1₽
if bot and percentage_reward >= 1.0:
try:
await bot.send_message(
referral.referrer_id,
@@ -407,35 +365,29 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
logger.error(f"Error processing referral rewards: {e}")
async def create_referral_from_start_param(user_telegram_id: int, start_param: str, db: Database, bot=None):
"""Create referral relationship from start parameter"""
try:
if not start_param.startswith("ref_"):
return False
referrer_id = int(start_param.replace("ref_", ""))
# Проверяем что это не тот же пользователь
if referrer_id == user_telegram_id:
logger.warning(f"User {user_telegram_id} tried to refer themselves")
return False
# Проверяем что у пользователя еще нет реферера
existing_referral = await db.get_referral_by_referred_id(user_telegram_id)
if existing_referral:
logger.info(f"User {user_telegram_id} already has referrer")
return False
# Генерируем промокод реферера
referral_code = await db.generate_unique_referral_code(referrer_id)
# Создаем реферальную связь
referral = await db.create_referral(referrer_id, user_telegram_id, referral_code)
if referral:
logger.info(f"Created referral: {referrer_id} -> {user_telegram_id} with code {referral_code}")
# Уведомляем реферера
if bot:
try:
referrer = await db.get_user_by_telegram_id(referrer_id)
@@ -461,14 +413,12 @@ async def create_referral_from_start_param(user_telegram_id: int, start_param: s
return False
async def create_referral_from_promocode(user_telegram_id: int, referral_code: str, db: Database, bot=None):
"""Create referral relationship from promocode - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
try:
if not referral_code.startswith("REF"):
return False
logger.info(f"Trying to use referral code {referral_code} for user {user_telegram_id}")
# Ищем реферера по коду - ИСПРАВЛЕНО: используем правильный импорт
async with db.session_factory() as session:
from sqlalchemy import select
result = await session.execute(
@@ -482,25 +432,21 @@ async def create_referral_from_promocode(user_telegram_id: int, referral_code: s
referrer_id = referral_record.referrer_id
# Проверяем что пользователь не пытается использовать свой код
if referrer_id == user_telegram_id:
logger.warning(f"User {user_telegram_id} tried to use own referral code")
return False
# Проверяем что у пользователя еще нет реферера
existing_referral = await db.get_referral_by_referred_id(user_telegram_id)
if existing_referral:
logger.info(f"User {user_telegram_id} already has referrer")
return False
# Создаем реферальную связь
referral = await db.create_referral(referrer_id, user_telegram_id, referral_code)
if referral:
logger.info(f"Created referral from promocode: {referrer_id} -> {user_telegram_id}")
# Уведомляем реферера
if bot:
try:
await bot.send_message(
@@ -520,7 +466,6 @@ async def create_referral_from_promocode(user_telegram_id: int, referral_code: s
return False
def generate_referral_link(bot_username: str, user_id: int) -> str:
"""Generate referral link for user"""
if not bot_username:
return ""
@@ -530,7 +475,6 @@ def generate_referral_link(bot_username: str, user_id: int) -> str:
return f"https://t.me/{bot_username}?start=ref_{user_id}"
def validate_referral_code(code: str) -> bool:
"""Validate referral code format"""
if not code or not code.startswith("REF"):
return False
if len(code) < 4 or len(code) > 20:
@@ -538,7 +482,6 @@ def validate_referral_code(code: str) -> bool:
return True
def format_referral_stats(stats: dict, lang: str = 'ru') -> str:
"""Format referral statistics for display"""
if lang == 'ru':
return (f"👥 Приглашено: {stats['total_referrals']}\n"
f"✅ Активных: {stats['active_referrals']}\n"
@@ -547,3 +490,33 @@ def format_referral_stats(stats: dict, lang: str = 'ru') -> str:
return (f"👥 Invited: {stats['total_referrals']}\n"
f"✅ Active: {stats['active_referrals']}\n"
f"💰 Earned: ${stats['total_earned']:.2f}")
def bytes_to_gb(bytes_value: int) -> float:
if not bytes_value or bytes_value == 0:
return 0.0
return round(bytes_value / (1024**3), 2)
def format_memory_usage(used_gb: float, total_gb: float) -> str:
if total_gb == 0:
return "N/A"
usage_percent = (used_gb / total_gb) * 100
available_gb = total_gb - used_gb
return f"{used_gb:.1f}/{total_gb:.1f} ГБ ({usage_percent:.1f}%) • Доступно: {available_gb:.1f} ГБ"
def format_uptime(uptime_seconds: float) -> str:
if uptime_seconds <= 0:
return "N/A"
uptime_hours = int(uptime_seconds // 3600)
uptime_days = uptime_hours // 24
uptime_hours = uptime_hours % 24
uptime_minutes = int((uptime_seconds % 3600) // 60)
if uptime_days > 0:
return f"{uptime_days}д {uptime_hours}ч"
elif uptime_hours > 0:
return f"{uptime_hours}ч {uptime_minutes}м"
else:
return f"{uptime_minutes}м"