Compare commits
118 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2f530a7fe | |||
| 44ea4b6b13 | |||
| ebc8421a70 | |||
| 526f142dea | |||
| ca431d36e6 | |||
| 68a81bdd89 | |||
| 68a3d713ef | |||
| 50d3d08e38 | |||
| 9a2fec4d8f | |||
| e68b1d2092 | |||
| 6dfedef81e | |||
| 317e39c823 | |||
| 0072a3cb71 | |||
| 8d021c1530 | |||
| 23606dc2d4 | |||
| 0838c4196d | |||
| 65605a0de2 | |||
| a20ac80281 | |||
| 02e83fb0d0 | |||
| eddad0db6e | |||
| ffad0afb61 | |||
| 83eed8e1cd | |||
| 0c7c8abade | |||
| a064890ed8 | |||
| f310f4a35e | |||
| 0268e09882 | |||
| 449814d0df | |||
| a3d3577103 | |||
| a3086fdd2c | |||
| daafc6a41e | |||
| c27ee116f7 | |||
| 5167cde443 | |||
| 34542d3a9c | |||
| e73d1df4cc | |||
| f9a95c3d48 | |||
| 2dd2b5fdbf | |||
| 896f1db9d4 | |||
| a1bd2d775f | |||
| 8cbe23f9ed | |||
| edc891c93d | |||
| 2d706a4be0 | |||
| 32cb81ef34 | |||
| 88aa3c4a3d | |||
| e813d3ccb7 | |||
| a24d7a4e24 | |||
| 39987b9717 | |||
| c77a1ea4f8 | |||
| 64b5d90d66 | |||
| 5a1fb32e1d | |||
| c4d8108e8c | |||
| 1350d80083 | |||
| e0cc218007 | |||
| bcaf8f2247 | |||
| d1190ed9dc | |||
| 513d6037d4 | |||
| e86012b2ab | |||
| 214e381dc5 | |||
| aa79cb670b | |||
| f0106e15fa | |||
| 1d194c6be2 | |||
| 6917890007 | |||
| 1c69de7ecd | |||
| 62188793d9 | |||
| 585ee2d42b | |||
| 2a58cfd45c | |||
| 93003c5fe0 | |||
| 91b0787d72 | |||
| 315277d4b0 | |||
| 1821c0d89d | |||
| 5650da0c5f | |||
| 0ca8b73cef | |||
| 6bab26ab27 | |||
| a72243e7fa | |||
| 5bd86d9cf7 | |||
| 00fd3fbadb | |||
| b806f40797 | |||
| c82aa1d253 | |||
| 071919f0db | |||
| ab41e96ba5 | |||
| 95a470ba7b | |||
| 1e0653ed33 | |||
| 3f0df08031 | |||
| 8c3bc8a5ba | |||
| 3a5a8f48e4 | |||
| f4b99ffb09 | |||
| 0c6afdebc3 | |||
| 98b382ba2f | |||
| 54a24385b1 | |||
| 3eeb84cdae | |||
| 204a10ae14 | |||
| ba1dd7547e | |||
| 238585c257 | |||
| d8ebe1ad13 | |||
| 819df3e93a | |||
| 99e46feafa | |||
| a432c8ab54 | |||
| d740b7560c | |||
| 91c4c73d7e | |||
| 4ce2384f27 | |||
| 02a6289d56 | |||
| 52ae6eba81 | |||
| 080b6e74a2 | |||
| ad7d32023c | |||
| 1baf8fbe37 | |||
| 758ca6a373 | |||
| 65504d0a23 | |||
| 0409f4e2e2 | |||
| 8ca63e939a | |||
| 25a621fd51 | |||
| 3f71a8aff7 | |||
| 6ec81c37b9 | |||
| 8a9ff98da8 | |||
| bb7e29eeac | |||
| 27d8783104 | |||
| 9a01e6db43 | |||
| b341ddf9d9 | |||
| 3c8d0ac859 | |||
| 3f529c0dee |
@@ -10,7 +10,6 @@ REFERRAL_PERCENTAGE=0.2
|
||||
|
||||
# RemnaWave API Configuration
|
||||
REMNAWAVE_URL=https://example.com
|
||||
REMNAWAVE_MODE=remote
|
||||
REMNAWAVE_TOKEN=
|
||||
|
||||
# Admin Configuration (comma-separated list of Telegram user IDs)
|
||||
@@ -19,18 +18,43 @@ ADMIN_IDS=123445567,12342123
|
||||
# Support Configuration
|
||||
SUPPORT_USERNAME=support
|
||||
|
||||
TRIAL_ENABLED=true
|
||||
TRIAL_DURATION_DAYS=3
|
||||
TRIAL_TRAFFIC_GB=2
|
||||
TRIAL_SQUAD_UUID=
|
||||
TRIAL_PRICE=0.0
|
||||
TRIAL_ENABLED=true # Включить триал
|
||||
TRIAL_DURATION_DAYS=3 # Дней триала
|
||||
TRIAL_TRAFFIC_GB=2 # Лимит трафика у триал подписки
|
||||
TRIAL_SQUAD_UUID= # UUID сквада из панели remnawave /dashboard/management/internal-squads
|
||||
TRIAL_PRICE=0.0 # Оставить 0!
|
||||
|
||||
# Monitor Service Settings (дополнительные настройки)
|
||||
MONITOR_CHECK_INTERVAL=3600
|
||||
MONITOR_DAILY_CHECK_HOUR=10
|
||||
MONITOR_WARNING_DAYS=2
|
||||
MONITOR_CHECK_INTERVAL=21600 # Промежуток проверики (3600 - будет раз в час проверять и слать уведомления)
|
||||
MONITOR_DAILY_CHECK_HOUR=10 # Ежедневный чек в 10 утра
|
||||
MONITOR_WARNING_DAYS=2 # За сколько начинать до окончания подписки слать уведомления
|
||||
DELETE_EXPIRED_TRIAL_DAYS=1 # Через сколько дней после истечения удалять триал подписки
|
||||
DELETE_EXPIRED_REGULAR_DAYS=7 # Через сколько дней после истечения удалять обычные подписки
|
||||
AUTO_DELETE_ENABLED=true # Включить автоматическое удаление при ежедневной проверке
|
||||
|
||||
LUCKY_GAME_ENABLED=true # Включить/выключить игру удачи
|
||||
LUCKY_GAME_REWARD=50.0 # Размер награды за выигрыш в рублях
|
||||
LUCKY_GAME_NUMBERS=30 # Общее количество чисел для выбора (1-30)
|
||||
LUCKY_GAME_WINNING_COUNT=25 # Количество выигрышных чисел из общего количества
|
||||
|
||||
# ================================
|
||||
# TELEGRAM STARS НАСТРОЙКИ
|
||||
# ================================
|
||||
# Включить/выключить пополнение через Telegram Stars
|
||||
STARS_ENABLED=true
|
||||
# Курсы обмена звезд на рубли
|
||||
# Формат: STARS_{количество_звезд}_RATE={рубли}
|
||||
# 100 звёзд
|
||||
STARS_100_RATE=110
|
||||
# 150 звёзд
|
||||
STARS_150_RATE=165
|
||||
# 250 звёзд
|
||||
STARS_250_RATE=280
|
||||
# 350 звёзд
|
||||
STARS_350_RATE=355
|
||||
# 500 звёзд
|
||||
STARS_500_RATE=550
|
||||
# 750 звёзд
|
||||
STARS_750_RATE=800
|
||||
# 1000 звёзд
|
||||
STARS_1000_RATE=1000
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/screens
|
||||
/assets
|
||||
@@ -1,16 +1,18 @@
|
||||
# 🤖 RemnaWave Telegram Bot
|
||||
# 🤖 Remnawave Telegram Bot
|
||||
|
||||
[](https://hub.docker.com/r/fr1ngg/remnawave-bedolaga-telegram-bot)
|
||||
[](https://python.org)
|
||||
[](https://postgresql.org)
|
||||
[](LICENSE)
|
||||
|
||||
Современный Telegram бот для управления VPN подписками через RemnaWave API с полным функционалом управления пользователями, платежами и администрированием.
|
||||

|
||||
|
||||
Современный Telegram бот для управления VPN подписками через Remnawave API с полным функционалом управления пользователями, платежами и администрированием.
|
||||
|
||||
## 🚀 Особенности
|
||||
|
||||
### 👤 Для пользователей
|
||||
- 💰 **Управление балансом** - пополнение через поддержку
|
||||
- 💰 **Управление балансом** - Пополнение через Telegram Stars + пополнение через поддержку p2p
|
||||
- 🛒 **Покупка подписок** - различные тарифные планы c возможностью индивидуальной настройки сквада у плана
|
||||
- 📱 **Управление подписками** - просмотр, продление, получение ссылок
|
||||
- 🎁 **Система промокодов** - денежные коды
|
||||
@@ -18,16 +20,21 @@
|
||||
- 🎰 **Игра удачи** - выигрывай бонусы каждые 24 часа
|
||||
- 🆓 **Тестовая подписка** - бесплатная пробная версия с детальной конфигурацией
|
||||
- 🌐 **Мультиязычность** - русский и английский
|
||||
- 📋 **Правила сервиса**
|
||||
- ♾️ **Автопродление** - С настройкой вкл/выкл и кол-вом дней до автопродления.
|
||||
|
||||
### ⚙️ Для администраторов
|
||||
- 📊 **Полная статистика** - пользователи, платежи, подписки
|
||||
- 👥 **Управление пользователями** - поиск, редактирование, баланс
|
||||
- 💳 **Управление платежами** - одобрение, отклонение, история
|
||||
- 🎫 **Управление промокодами** - создание, редактирование, статистика
|
||||
- 🖥 **Мониторинг системы** - состояние нод, пользователей RemnaWave
|
||||
- 🔄 **Синхронизация данных** - импорт из RemnaWave
|
||||
- ✍️ **Просмотр подписок юзеров**
|
||||
- 🖥 **Мониторинг системы** - состояние нод, пользователей Remnawave
|
||||
- 🔄 **Синхронизация данных** - импорт из Remnawave
|
||||
- 📨 **Рассылки** - уведомления пользователям
|
||||
- 🔍 **Мониторинг подписок** - автоматические уведомления об истечении
|
||||
- 🔍 **Мониторинг подписок** - автоматические уведомления об истечении и зачистка подписок с истекщим сроком для триал и обычных подписок
|
||||
- 📋 **Правила сервиса** - Настройка страниц из админ панели
|
||||
- ♾️ **Автопродление** - Статус сервиса, статистика автопродления, принудительное продление
|
||||
|
||||
## 📋 Требования
|
||||
|
||||
@@ -38,7 +45,7 @@
|
||||
|
||||
### Внешние сервисы
|
||||
- **Telegram Bot Token** - создайте бота через [@BotFather](https://t.me/BotFather)
|
||||
- **RemnaWave API** - доступ к панели RemnaWave
|
||||
- **Remnawave API** - доступ к панели Remnawave
|
||||
- **PostgreSQL** - автоматически разворачивается через Docker
|
||||
|
||||
## 🛠 Установка и запуск
|
||||
@@ -46,8 +53,8 @@
|
||||
### 1. Клонирование репозитория
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-repo/remnawave-telegram-bot.git
|
||||
cd remnawave-telegram-bot
|
||||
git clone https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot.git
|
||||
cd remnawave-bedolaga-telegram-bot
|
||||
```
|
||||
|
||||
### 2. Настройка переменных окружения
|
||||
@@ -67,8 +74,8 @@ nano .env
|
||||
|------------|----------|---------|
|
||||
| `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` |
|
||||
| `REMNAWAVE_URL` | URL панели Remnawave | `https://panel.example.com` |
|
||||
| `REMNAWAVE_TOKEN` | API токен Remnawave | `your_api_token` |
|
||||
| `ADMIN_IDS` | ID администраторов (через запятую) | `123456789,987654321` |
|
||||
|
||||
#### 🎁 Реферальная программа
|
||||
@@ -78,7 +85,7 @@ nano .env
|
||||
| `REFERRAL_FIRST_REWARD` | Награда за первого реферала | `100.0` |
|
||||
| `REFERRAL_REFERRED_BONUS` | Бонус приглашенному | `100.0` |
|
||||
| `REFERRAL_THRESHOLD` | Порог активации (руб.) | `200.0` |
|
||||
| `REFERRAL_PERCENTAGE` | Процент с платежей | `0.2` (20%) |
|
||||
| `REFERRAL_PERCENTAGE` | Процент со второго и послед. платежей | `0.2` (20%) |
|
||||
|
||||
#### 🎰 Игра удачи
|
||||
|
||||
@@ -93,14 +100,186 @@ nano .env
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|------------|----------|--------------|
|
||||
| `TRIAL_ENABLED` | Включить тестовую подписку | `true` |
|
||||
| `TRIAL_ENABLED` | Включить тестовую подписку | `true/false` |
|
||||
| `TRIAL_DURATION_DAYS` | Длительность (дни) | `3` |
|
||||
| `TRIAL_TRAFFIC_GB` | Лимит трафика (ГБ) | `2` |
|
||||
| `TRIAL_SQUAD_UUID` | UUID squad для тестовых | `uuid-here` |
|
||||
|
||||
### 4. Docker Compose конфигурация
|
||||
#### 🌟 Оплата за звезды
|
||||
|
||||
#### 🚀 Минимальная конфигурация (рекомендуется)
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|------------|----------|--------------|
|
||||
| `STARS_ENABLED=true` | Включить пополнение баланса за звезды | `true/false` |
|
||||
| `STARS_100_RATE` | Курс за 100 звезд | `100` |
|
||||
| `STARS_150_RATE` | Курс за 150 звезд | `150` |
|
||||
| `STARS_250_RATE` | Курс за 250 звезд | `250` |
|
||||
| `И тд` | Курс за XXX звезд | `XXXX` |
|
||||
|
||||
|
||||
#### 💬 Сервис мониторинга сообщений
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|------------|----------|--------------|
|
||||
| `MONITOR_ENABLED` | Включить сервис подписку | `true/false` |
|
||||
| `MONITOR_CHECK_INTERVAL` | Интервал проверки в секундах (1800 = 30 минут) (будет срабатывать через этот промежуток автоматически и слать уведомления) | `1800` |
|
||||
| `MONITOR_DAILY_CHECK_HOUR` | Час ежедневной проверки (0-23, по умолчанию 10:00) | `10` |
|
||||
| `DELETE_EXPIRED_TRIAL_DAYS` | Через сколько дней после истечения удалять триал подписки | `1` |
|
||||
| `DELETE_EXPIRED_REGULAR_DAYS=7` | Через сколько дней после истечения удалять обычняе подписки | `7` |
|
||||
| `MONITOR_WARNING_DAYS` | За сколько дней предупреждать (по умолчанию 3) | `3` |
|
||||
| `AUTO_DELETE_ENABLED` | Включить автоматическое удаление при ежедневной проверке(true/false) | `true` |
|
||||
|
||||
### Максимально быстрый старт через скрипт управления ботом
|
||||
#### 🚀 Быстрая установка Remnawave Bedolaga Bot
|
||||
<img width="419" height="348" alt="Снимок экрана 2025-08-12 в 01 24 57" src="https://github.com/user-attachments/assets/9ab876f5-2758-4c52-93dd-6c9c654a07aa" />
|
||||
|
||||
|
||||
Автоматический установщик для Ubuntu сервера с полным управлением через интерактивное меню.
|
||||
|
||||
##### 📋 Требования
|
||||
|
||||
- **Ubuntu 18.04+** (рекомендуется Ubuntu 20.04 или 22.04)
|
||||
- **Root доступ** или sudo права
|
||||
- **Интернет соединение** для загрузки Docker образов
|
||||
|
||||
##### ⚡ Установка одной командой
|
||||
|
||||
###### Способ 1: Скачать и запустить
|
||||
```bash
|
||||
wget https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/raw/main/bedolaga.sh
|
||||
chmod +x bedolaga.sh
|
||||
sudo ./bedolaga.sh
|
||||
```
|
||||
|
||||
###### Способ 2: Прямой запуск
|
||||
```bash
|
||||
curl -sSL https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/raw/main/bedolaga.sh | sudo bash
|
||||
```
|
||||
|
||||
###### Способ 3: Клонирование репозитория
|
||||
```bash
|
||||
git clone https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot.git
|
||||
cd remnawave-bedolaga-telegram-bot
|
||||
sudo ./bedolaga.sh
|
||||
```
|
||||
|
||||
#### 🔧 Что делает установщик
|
||||
|
||||
1. **Обновляет систему Ubuntu**
|
||||
2. **Устанавливает Docker** и необходимые зависимости
|
||||
3. **Создает структуру проекта** в `/opt/bedolaga-bot`
|
||||
4. **Настраивает Docker Compose** (минимальная или расширенная конфигурация)
|
||||
5. **Интерактивно настраивает .env файл** с параметрами бота
|
||||
6. **Создает systemd службу** (опционально)
|
||||
7. **Запускает интерактивное меню управления**
|
||||
|
||||
#### 📱 Интерактивное меню управления
|
||||
|
||||
После установки вы получите удобное меню для управления ботом:
|
||||
|
||||
##### Когда бот запущен:
|
||||
- 🛑 Выключить бот
|
||||
- 🔄 Перезапустить бот
|
||||
- 📺 Посмотреть логи в реальном времени
|
||||
- ⬆️ Обновить бот
|
||||
- 📋 Посмотреть логи
|
||||
- 💾 Создать резервную копию БД
|
||||
- ♻️ Восстановить базу данных
|
||||
- ✏️ Редактировать .env файл
|
||||
- 🩺 Диагностика базы данных
|
||||
- Восстановление бд после обновления 1.3.3 -> 1.3.4
|
||||
- 🗑️ Удалить базу данных
|
||||
- ❌ Полностью удалить бота
|
||||
|
||||
##### Когда бот остановлен:
|
||||
- ▶️ Запустить бот
|
||||
- ⬆️ Обновить бот
|
||||
- 📋 Посмотреть логи
|
||||
- 💾 Создать резервную копию БД
|
||||
- ♻️ Восстановить базу данных
|
||||
- ✏️ Редактировать .env файл
|
||||
- 🩺 Диагностика базы данных
|
||||
- Восстановление бд после обновления 1.3.3 -> 1.3.4
|
||||
- 🗑️ Удалить базы данных
|
||||
- ❌ Полностью удалить бота
|
||||
|
||||
#### 🔧 Ручное управление через командную строку
|
||||
|
||||
```bash
|
||||
# Перейти в папку проекта
|
||||
cd /opt/bedolaga-bot
|
||||
|
||||
# Управление через Docker Compose
|
||||
docker compose up -d # Запустить
|
||||
docker compose down # Остановить
|
||||
docker compose restart # Перезапустить
|
||||
docker compose logs -f bot # Логи в реальном времени
|
||||
|
||||
# Управление через systemd (если служба создана)
|
||||
sudo systemctl start bedolaga-bot
|
||||
sudo systemctl stop bedolaga-bot
|
||||
sudo systemctl restart bedolaga-bot
|
||||
sudo systemctl status bedolaga-bot
|
||||
```
|
||||
|
||||
#### 📂 Структура установки
|
||||
|
||||
```
|
||||
/opt/bedolaga-bot/
|
||||
├── docker-compose.yml # Конфигурация контейнеров
|
||||
├── .env # Настройки бота
|
||||
├── logs/ # Логи бота
|
||||
├── data/ # Данные бота
|
||||
└── backup_*.sql # Резервные копии БД
|
||||
```
|
||||
|
||||
#### 🔄 Повторный запуск установщика
|
||||
|
||||
Если бот уже установлен, скрипт автоматически перейдет в режим управления без переустановки:
|
||||
|
||||
```bash
|
||||
sudo ./bedolaga.sh
|
||||
```
|
||||
|
||||
#### ❓ Решение проблем
|
||||
|
||||
### Бот не запускается
|
||||
1. Проверьте логи: `docker compose logs bot`
|
||||
2. Используйте диагностику в меню установщика
|
||||
3. Убедитесь что все параметры в `.env` корректны
|
||||
|
||||
##### Проблемы с базой данных
|
||||
1. Запустите диагностику базы данных из меню
|
||||
2. Проверьте логи PostgreSQL: `docker compose logs postgres`
|
||||
3. При необходимости создайте новую базу (удалите старую через меню)
|
||||
|
||||
##### Проблемы с Docker
|
||||
```bash
|
||||
# Перезапуск Docker
|
||||
sudo systemctl restart docker
|
||||
|
||||
# Очистка неиспользуемых ресурсов
|
||||
docker system prune -f
|
||||
```
|
||||
|
||||
#### 📝 Дополнительные команды
|
||||
|
||||
```bash
|
||||
# Обновление только бота (без остановки БД)
|
||||
docker compose pull bot && docker compose up -d bot
|
||||
|
||||
# Резервная копия БД вручную
|
||||
docker compose exec postgres pg_dump -U remnawave_user -d remnawave_bot > backup.sql
|
||||
|
||||
# Просмотр использования ресурсов
|
||||
docker stats
|
||||
|
||||
# Просмотр всех volumes
|
||||
docker volume ls
|
||||
```
|
||||
|
||||
### 4. Docker Compose конфигурация (ЗАПУСК НА СЕРВЕРЕ ВНЕ ПАНЕЛИ REMNAWAVE)
|
||||
|
||||
#### 🚀 Минимальная конфигурация (рекомендуется)
|
||||
|
||||
Создайте файл `docker-compose.yml` для базового запуска:
|
||||
|
||||
@@ -127,7 +306,7 @@ services:
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
# RemnaWave Bot
|
||||
# Remnawave Bot
|
||||
bot:
|
||||
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
|
||||
container_name: remnawave_bot
|
||||
@@ -189,7 +368,7 @@ services:
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
# RemnaWave Bot
|
||||
# Remnawave Bot
|
||||
bot:
|
||||
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
|
||||
container_name: remnawave_bot
|
||||
@@ -266,6 +445,173 @@ networks:
|
||||
- subnet: 172.20.0.0/16
|
||||
```
|
||||
|
||||
### 4.1 Docker Compose конфигурация (ЗАПУСК НА СЕРВЕРЕ C ПАНЕЛЬЮ REMNAWAVE)
|
||||
|
||||
#### 🚀 Минимальная конфигурация (рекомендуется)
|
||||
|
||||
Создайте файл `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:
|
||||
- remnawave-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:
|
||||
- remnawave-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:
|
||||
remnawave-network:
|
||||
name: remnawave-network
|
||||
external: true
|
||||
```
|
||||
|
||||
#### ⚡ Полная конфигурация (с дополнительными сервисами)
|
||||
|
||||
Для расширенной настройки с 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:
|
||||
- remnawave-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:
|
||||
- remnawave-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:
|
||||
- remnawave-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:
|
||||
- remnawave-network
|
||||
depends_on:
|
||||
- bot
|
||||
profiles:
|
||||
- with-nginx
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
remnawave-network:
|
||||
name: remnawave-network
|
||||
external: true
|
||||
```
|
||||
|
||||
### 5. Варианты запуска
|
||||
|
||||
#### 🚀 Быстрый старт (минимальная конфигурация)
|
||||
@@ -407,15 +753,14 @@ docker build -t remnawave-bot .
|
||||
- **💰 Управление балансом** - пополнение балансов
|
||||
- **🎁 Управление промокодами** - создание скидок
|
||||
- **📨 Рассылки** - отправка сообщений
|
||||
- **🖥 Система RemnaWave** - мониторинг нод и синхронизация
|
||||
- **🖥 Система Remnawave** - мониторинг нод и синхронизация
|
||||
|
||||
## 🔒 Безопасность
|
||||
|
||||
### Рекомендации
|
||||
|
||||
- ✅ Измените пароли PostgreSQL в `docker-compose.yml`
|
||||
- ✅ Не публикуйте файл `.env` в репозитории
|
||||
- ✅ Используйте HTTPS для RemnaWave API
|
||||
- ✅ Используйте HTTPS для Remnawave API
|
||||
- ✅ Регулярно создавайте backup базы данных
|
||||
- ✅ Ограничьте доступ к серверу через firewall
|
||||
|
||||
@@ -435,7 +780,7 @@ openssl rand -hex 16 # для Redis
|
||||
|----------|---------|
|
||||
| Бот не отвечает | Проверьте `BOT_TOKEN` и интернет |
|
||||
| Ошибка подключения к БД | Проверьте статус PostgreSQL контейнера |
|
||||
| Ошибки RemnaWave API | Проверьте `REMNAWAVE_URL` и `REMNAWAVE_TOKEN` |
|
||||
| Ошибки Remnawave API | Проверьте `REMNAWAVE_URL` и `REMNAWAVE_TOKEN` |
|
||||
| Игра удачи не работает | Проверьте `LUCKY_GAME_ENABLED=true` |
|
||||
|
||||
### Диагностика
|
||||
@@ -506,14 +851,62 @@ docker-compose ps
|
||||
| № | Задача | Статус | Версия | Приоритет | Описание |
|
||||
|---|---------|--------|--------|-----------|----------|
|
||||
| 1 | Мониторинг подписок | ✅ Done | v1.1.0 | 🔴 High | Служба оповещения об истечении срока и автоконтроль |
|
||||
| 2 | Платежные шлюзы | 🎯 Planned | v1.4.0+ | 🔴 High | ЮKassa, Сбербанк, Tinkoff, Crypto, Звезды |
|
||||
| 3 | Синхронизация RemnaWave | ✅ Done | v1.1.0 | 🟡 Medium | Импорт пользователей по Telegram ID |
|
||||
| 2 | Платежные шлюзы | ✅ Done | v1.3.2 | 🔴 High | Звезды |
|
||||
| 2.1 | Платежные шлюзы | 🎯 Planned | v1.4.0+ | 🔴 High | ЮKassa |
|
||||
| 2.2 | Платежные шлюзы | 🎯 Planned | v1.4.0+ | 🟡 Medium | Сбербанк, Tinkoff, Crypto, Others |
|
||||
| 3 | Синхронизация RemnaWave | ✅ Done | v1.1.0 | 🟡 Medium | Вывод информации о статусе панели remnawave и нод прямо в бота |
|
||||
| 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 | Лудочка |
|
||||
| 10 | Блокировка | 🚧 In Progress | v1.3.0+ | 🟢 Low | Бан/разбан юзеров |
|
||||
| 11 | Удаление истекшик подписок из бота и панели | ✅ Done | v1.3.1 | 🔴 High | Дабы не плодить лишние записи в базе бота и панели ремны, да и в целом это логично)( |
|
||||
| 12 | Правила сервиса | ✅ Done | v1.3.3 | 🟢 Low | Вывод правил сервиса в боте с конфигурацией через админку |
|
||||
| 13 | Автоплатежи | ✅ Done | v1.3.3 | 🟡 Medium | Возможность включать-выключать автоплатежи и настраивать их + админская статистика |
|
||||
| 14 | Все подписки юзеров | ✅ Done | v1.3.4 | 🟡 Medium | Возможность просматривать все подписки купленные юзерами в админ панели |
|
||||
|
||||
|
||||
### 🚨 Возможные проблемы и пути и решения
|
||||
|
||||
#### Переход с версии 1.3.3 на 1.3.4
|
||||
|
||||
Может такое пройзойти, что после обновления образа у вас сломается база с подписками и в консоль полетят sql ошибки, дело в том, что в патче 1.3.4 добавились автоплатежи с доп полями в бд, во время обновления миграция может пройти некорректно(Я еще не до конца разобрался в чем проблема, но имейте в виду).
|
||||
|
||||
Для фикса в репозитории скачайте файлик emergency_fix.py и положите его рядом с докер файлом
|
||||
|
||||
В докер добавьте сервис:
|
||||
|
||||
emergency-fix:
|
||||
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest # Используем тот же образ что и бот
|
||||
volumes:
|
||||
- ./emergency_fix.py:/app/emergency_fix.py
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot #путь до DATABASE_URL
|
||||
networks:
|
||||
- bot_network
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
profiles:
|
||||
- emergency # Профиль, чтобы не запускался по умолчанию
|
||||
command: python emergency_fix.py
|
||||
|
||||
После выполните:
|
||||
|
||||
docker compose run --rm emergency-fix
|
||||
|
||||
Скрипт добавит необходимые поля и починит вашу базу с подписками. После можете удалить секцию с emergency-fix и скрипт из докер файла.
|
||||
|
||||
**Для тех кто запускает бота через скрипт управления ботом добавлены кнопки для восстановления бд, 10) Экстренное исправление БД (Python) 11) Экстренное исправление БД (SQL)**
|
||||
|
||||
**⚠️ Важные замечания от автора:**
|
||||
|
||||
> 🗑 **Осторожно с удалением подписок!** Сейчас удаление плана подписки скроет эту подписку у всех пользователей, которые ее купили. Используйте деактивацию вместо удаления. Эта проблема будет исправлена в v1.4.0.
|
||||
|
||||
---
|
||||
|
||||
### 🚀 Хотите помочь?
|
||||
|
||||
@@ -536,14 +929,12 @@ docker-compose ps
|
||||
|
||||
---
|
||||
|
||||
**⚠️ Важные замечания от автора:**
|
||||
|
||||
> 🗑 **Осторожно с удалением подписок!** Сейчас удаление плана подписки скроет эту подписку у всех пользователей, которые ее купили. Используйте деактивацию вместо удаления. Эта проблема будет исправлена в v1.4.0.
|
||||
|
||||
---
|
||||
|
||||
**⭐ Если проект был полезен, поставьте звездочку на GitHub!**
|
||||
|
||||
---
|
||||
|
||||
*Создано с ❤️ для RemnaWave сообщества*
|
||||
### PS
|
||||
|
||||
Автор не является проф. разработчиком, да и без вайбкодинга не обошлось, но я прикладываю все усилия, чтобы сделать удобного публичного бота для ваших сервисов)(
|
||||
|
||||
*Создано с ❤️ для Remnawave сообщества*
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
<svg viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<!-- Градиенты -->
|
||||
<linearGradient id="mainGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#764ba2;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#f093fb;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="skinGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#d4a574;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#b8935f;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="redSkinGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#e8b598;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#d4a574;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="clothesGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#3a3a3a;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#595959;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="bottleGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2d5016;stop-opacity:0.8" />
|
||||
<stop offset="100%" style="stop-color:#1a2f0d;stop-opacity:0.9" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- Фильтры -->
|
||||
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<filter id="dropshadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="4"/>
|
||||
<feOffset dx="2" dy="4" result="offset" />
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.3"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Фон -->
|
||||
<rect width="400" height="400" fill="#1a1a2e" rx="50"/>
|
||||
|
||||
<!-- Декоративные элементы фона -->
|
||||
<circle cx="50" cy="80" r="30" fill="url(#mainGradient)" opacity="0.1"/>
|
||||
<circle cx="350" cy="120" r="40" fill="#8B4513" opacity="0.1"/>
|
||||
|
||||
<!-- Волны RemnaWave в фоне -->
|
||||
<g transform="translate(200,200)" opacity="0.2">
|
||||
<path d="M-120,-100 Q-60,-130 0,-100 Q60,-70 120,-100"
|
||||
stroke="url(#mainGradient)"
|
||||
stroke-width="4"
|
||||
fill="none">
|
||||
<animate attributeName="d"
|
||||
values="M-120,-100 Q-60,-130 0,-100 Q60,-70 120,-100;M-120,-100 Q-60,-70 0,-100 Q60,-130 120,-100;M-120,-100 Q-60,-130 0,-100 Q60,-70 120,-100"
|
||||
dur="4s"
|
||||
repeatCount="indefinite"/>
|
||||
</path>
|
||||
</g>
|
||||
|
||||
<!-- Главный персонаж - Бомж-алкаш -->
|
||||
<g transform="translate(200,180)" filter="url(#dropshadow)">
|
||||
|
||||
<!-- Тело в рваной куртке -->
|
||||
<ellipse cx="0" cy="30" rx="45" ry="60" fill="url(#clothesGradient)"/>
|
||||
|
||||
<!-- Рваные края одежды -->
|
||||
<path d="M-45,70 L-40,75 L-35,70 L-30,75 L-25,70 L-20,75 L-15,70 L-10,75 L-5,70 L0,75 L5,70 L10,75 L15,70 L20,75 L25,70 L30,75 L35,70 L40,75 L45,70"
|
||||
stroke="none" fill="#2a2a2a"/>
|
||||
|
||||
<!-- Заплатки -->
|
||||
<rect x="-20" y="10" width="12" height="10" fill="#5a4a3a" opacity="0.7" rx="2"/>
|
||||
<rect x="15" y="35" width="10" height="8" fill="#4a4a4a" opacity="0.8" rx="1"/>
|
||||
<rect x="-10" y="45" width="8" height="12" fill="#6a5a4a" opacity="0.6" rx="1"/>
|
||||
|
||||
<!-- Шея -->
|
||||
<rect x="-12" y="-25" width="24" height="20" fill="url(#redSkinGradient)" rx="8"/>
|
||||
|
||||
<!-- Голова (более человечная форма) -->
|
||||
<ellipse cx="0" cy="-45" rx="28" ry="32" fill="url(#redSkinGradient)"/>
|
||||
|
||||
<!-- Сальные волосы -->
|
||||
<path d="M-25,-70 Q-20,-75 -15,-72 Q-10,-78 -5,-73 Q0,-80 5,-73 Q10,-78 15,-72 Q20,-75 25,-70"
|
||||
stroke="#3a3a3a" stroke-width="4" fill="#3a3a3a" stroke-linecap="round"/>
|
||||
<path d="M-28,-65 Q-25,-70 -20,-67" stroke="#3a3a3a" stroke-width="3" fill="none" stroke-linecap="round"/>
|
||||
<path d="M28,-65 Q25,-70 20,-67" stroke="#3a3a3a" stroke-width="3" fill="none" stroke-linecap="round"/>
|
||||
|
||||
<!-- Левое ухо -->
|
||||
<ellipse cx="-28" cy="-45" rx="6" ry="10" fill="url(#skinGradient)"/>
|
||||
|
||||
<!-- Правое ухо -->
|
||||
<ellipse cx="28" cy="-45" rx="6" ry="10" fill="url(#skinGradient)"/>
|
||||
|
||||
<!-- Опухшие глаза -->
|
||||
<ellipse cx="-10" cy="-50" rx="8" ry="5" fill="white"/>
|
||||
<ellipse cx="10" cy="-50" rx="8" ry="5" fill="white"/>
|
||||
|
||||
<!-- Зрачки (косящие) -->
|
||||
<g id="eyes">
|
||||
<circle cx="-8" cy="-49" r="3" fill="#333">
|
||||
<animate attributeName="cx" values="-8;-10;-8" dur="3s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="12" cy="-49" r="3" fill="#333">
|
||||
<animate attributeName="cx" values="12;10;12" dur="3s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</g>
|
||||
|
||||
<!-- Красные глаза -->
|
||||
<path d="M-18,-50 L-15,-51" stroke="red" stroke-width="1" opacity="0.7"/>
|
||||
<path d="M-17,-48 L-14,-49" stroke="red" stroke-width="0.5" opacity="0.7"/>
|
||||
<path d="M18,-50 L15,-51" stroke="red" stroke-width="1" opacity="0.7"/>
|
||||
<path d="M17,-48 L14,-49" stroke="red" stroke-width="0.5" opacity="0.7"/>
|
||||
|
||||
<!-- Мешки под глазами -->
|
||||
<ellipse cx="-10" cy="-44" rx="8" ry="3" fill="#8a6a4a" opacity="0.5"/>
|
||||
<ellipse cx="10" cy="-44" rx="8" ry="3" fill="#8a6a4a" opacity="0.5"/>
|
||||
|
||||
<!-- Красный нос алкаша -->
|
||||
<ellipse cx="0" cy="-38" rx="6" ry="5" fill="#d67070"/>
|
||||
<circle cx="-2" cy="-39" r="1" fill="#e08080" opacity="0.8"/>
|
||||
|
||||
<!-- Небритость -->
|
||||
<g opacity="0.6">
|
||||
<circle cx="-15" cy="-30" r="1" fill="#555"/>
|
||||
<circle cx="-12" cy="-28" r="0.8" fill="#555"/>
|
||||
<circle cx="-8" cy="-30" r="0.8" fill="#555"/>
|
||||
<circle cx="15" cy="-30" r="1" fill="#555"/>
|
||||
<circle cx="12" cy="-28" r="0.8" fill="#555"/>
|
||||
<circle cx="8" cy="-30" r="0.8" fill="#555"/>
|
||||
<circle cx="0" cy="-28" r="0.8" fill="#555"/>
|
||||
<circle cx="-5" cy="-26" r="0.8" fill="#555"/>
|
||||
<circle cx="5" cy="-26" r="0.8" fill="#555"/>
|
||||
</g>
|
||||
|
||||
<!-- Рот с сигаретой/бутылкой (анимированный) -->
|
||||
<g id="mouth-group">
|
||||
<!-- Основной рот -->
|
||||
<path d="M-12,-20 Q0,-18 12,-20" stroke="#444" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
|
||||
<!-- Сигарета (появляется и исчезает) -->
|
||||
<g id="cigarette">
|
||||
<rect x="10" y="-22" width="25" height="3" fill="white" rx="1">
|
||||
<animate attributeName="opacity" values="0;0;0;1;1;1;0;0;0" dur="8s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
<rect x="30" y="-22" width="5" height="3" fill="#ff6b35">
|
||||
<animate attributeName="opacity" values="0;0;0;1;1;1;0;0;0" dur="8s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
<!-- Дым от сигареты -->
|
||||
<g opacity="0.4">
|
||||
<circle cx="35" cy="-22" r="2" fill="#999">
|
||||
<animate attributeName="opacity" values="0;0;0;0.6;0.3;0;0;0;0" dur="8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="cy" values="-22;-22;-22;-28;-35;-40;-22;-22;-22" dur="8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="r" values="2;2;2;4;6;8;2;2;2" dur="8s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Левая рука с бутылкой -->
|
||||
<g id="left-arm">
|
||||
<!-- Рука -->
|
||||
<line x1="-45" y1="10" x2="-55" y2="-5" stroke="url(#skinGradient)" stroke-width="8" stroke-linecap="round">
|
||||
<animate attributeName="x2" values="-55;-50;-55" dur="8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="y2" values="-5;-20;-5" dur="8s" repeatCount="indefinite"/>
|
||||
</line>
|
||||
|
||||
<!-- Кисть -->
|
||||
<circle cx="-55" cy="-5" r="6" fill="url(#skinGradient)">
|
||||
<animate attributeName="cx" values="-55;-50;-55" dur="8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="cy" values="-5;-20;-5" dur="8s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
|
||||
<!-- Бутылка -->
|
||||
<g id="bottle">
|
||||
<rect x="-62" y="-25" width="14" height="30" fill="url(#bottleGradient)" rx="2">
|
||||
<animate attributeName="x" values="-62;-57;-62" dur="8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="y" values="-25;-40;-25" dur="8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="transform" values="rotate(0 -55 -10);rotate(-30 -50 -25);rotate(0 -55 -10)" dur="8s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
<rect x="-58" y="-30" width="6" height="8" fill="#2d5016">
|
||||
<animate attributeName="x" values="-58;-53;-58" dur="8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="y" values="-30;-45;-30" dur="8s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
<!-- Этикетка -->
|
||||
<rect x="-60" y="-15" width="10" height="8" fill="white" opacity="0.3">
|
||||
<animate attributeName="x" values="-60;-55;-60" dur="8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="y" values="-15;-30;-15" dur="8s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Правая рука (свободная, дрожащая) -->
|
||||
<g id="right-arm">
|
||||
<line x1="45" y1="10" x2="55" y2="20" stroke="url(#skinGradient)" stroke-width="8" stroke-linecap="round">
|
||||
<animate attributeName="x2" values="55;57;55;53;55" dur="2s" repeatCount="indefinite"/>
|
||||
</line>
|
||||
<circle cx="55" cy="20" r="6" fill="url(#skinGradient)">
|
||||
<animate attributeName="cx" values="55;57;55;53;55" dur="2s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</g>
|
||||
|
||||
<!-- Ноги в рваных штанах -->
|
||||
<rect x="-15" y="85" width="12" height="35" fill="#4a4a5a" rx="2"/>
|
||||
<rect x="3" y="85" width="12" height="35" fill="#4a4a5a" rx="2"/>
|
||||
|
||||
<!-- Дырки в штанах -->
|
||||
<ellipse cx="-9" cy="95" rx="3" ry="4" fill="#1a1a2e"/>
|
||||
<ellipse cx="9" cy="105" rx="2" ry="3" fill="#1a1a2e"/>
|
||||
|
||||
<!-- Старые ботинки -->
|
||||
<ellipse cx="-9" cy="125" rx="10" ry="6" fill="#2a2a2a"/>
|
||||
<ellipse cx="9" cy="125" rx="10" ry="6" fill="#2a2a2a"/>
|
||||
<!-- Дырки в ботинках -->
|
||||
<circle cx="-6" cy="123" r="2" fill="#1a1a2e"/>
|
||||
<circle cx="12" cy="123" r="1.5" fill="#1a1a2e"/>
|
||||
|
||||
<!-- Значок "B" на груди -->
|
||||
<circle cx="0" cy="25" r="20" fill="url(#mainGradient)" opacity="0.8" stroke="white" stroke-width="1"/>
|
||||
<text x="0" y="35"
|
||||
font-family="Arial Black, sans-serif"
|
||||
font-size="28"
|
||||
font-weight="bold"
|
||||
text-anchor="middle"
|
||||
fill="white">
|
||||
B
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<!-- Упавшие бутылки вокруг -->
|
||||
<g opacity="0.6">
|
||||
<rect x="80" y="340" width="8" height="20" fill="#2d5016" rx="1" transform="rotate(75 84 350)"/>
|
||||
<rect x="310" y="330" width="7" height="18" fill="#3a5a1a" rx="1" transform="rotate(-60 313 339)"/>
|
||||
<rect x="150" y="350" width="6" height="16" fill="#2d5016" rx="1" transform="rotate(45 153 358)"/>
|
||||
</g>
|
||||
|
||||
<!-- Окурки -->
|
||||
<g opacity="0.7">
|
||||
<rect x="120" y="360" width="12" height="2" fill="white" rx="1"/>
|
||||
<rect x="130" y="360" width="2" height="2" fill="#ff6b35"/>
|
||||
|
||||
<rect x="250" y="355" width="10" height="2" fill="white" rx="1" transform="rotate(30 255 356)"/>
|
||||
<rect x="258" y="355" width="2" height="2" fill="#ff6b35" transform="rotate(30 259 356)"/>
|
||||
</g>
|
||||
|
||||
<!-- Текст логотипа -->
|
||||
<text x="200" y="320"
|
||||
font-family="Arial Black, sans-serif"
|
||||
font-size="20"
|
||||
font-weight="bold"
|
||||
text-anchor="middle"
|
||||
fill="url(#mainGradient)"
|
||||
filter="url(#glow)">
|
||||
Remnawave
|
||||
</text>
|
||||
|
||||
<text x="200" y="345"
|
||||
font-family="Arial Black, sans-serif"
|
||||
font-size="18"
|
||||
font-weight="bold"
|
||||
text-anchor="middle"
|
||||
fill="#d67070">
|
||||
Bedolaga Bot
|
||||
</text>
|
||||
|
||||
<!-- Подпись -->
|
||||
<text x="200" y="365"
|
||||
font-family="Arial, sans-serif"
|
||||
font-size="11"
|
||||
text-anchor="middle"
|
||||
fill="#8892b0"
|
||||
opacity="0.8">
|
||||
Telegram VPN Manager
|
||||
</text>
|
||||
|
||||
<!-- Мухи летающие вокруг (признак антисанитарии) -->
|
||||
<g opacity="0.5">
|
||||
<circle cx="150" cy="150" r="1" fill="#333">
|
||||
<animateTransform attributeName="transform" type="rotate" values="0 200 180;360 200 180" dur="6s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="250" cy="150" r="1" fill="#333">
|
||||
<animateTransform attributeName="transform" type="rotate" values="360 200 180;0 200 180" dur="5s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,284 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional
|
||||
from database import Database, UserSubscription, User, Subscription
|
||||
from remnawave_api import RemnaWaveAPI
|
||||
from referral_utils import process_referral_rewards
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AutoPayService:
|
||||
|
||||
def __init__(self, db: Database, api: Optional[RemnaWaveAPI] = None, bot=None):
|
||||
self.db = db
|
||||
self.api = api
|
||||
self.bot = bot
|
||||
self.is_running = False
|
||||
self.check_task = None
|
||||
|
||||
async def start(self):
|
||||
if self.is_running:
|
||||
logger.warning("AutoPay service is already running")
|
||||
return
|
||||
|
||||
self.is_running = True
|
||||
logger.info("🔄 Starting AutoPay service...")
|
||||
|
||||
self.check_task = asyncio.create_task(self._periodic_check())
|
||||
|
||||
async def stop(self):
|
||||
if not self.is_running:
|
||||
return
|
||||
|
||||
self.is_running = False
|
||||
logger.info("⏹ Stopping AutoPay service...")
|
||||
|
||||
if self.check_task:
|
||||
self.check_task.cancel()
|
||||
try:
|
||||
await self.check_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
async def _periodic_check(self):
|
||||
while self.is_running:
|
||||
try:
|
||||
await self.process_autopayments()
|
||||
# Проверяем каждые 30 минут
|
||||
await asyncio.sleep(1800)
|
||||
except asyncio.CancelledError:
|
||||
logger.info("AutoPay periodic check cancelled")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error in AutoPay periodic check: {e}")
|
||||
await asyncio.sleep(300)
|
||||
|
||||
async def process_autopayments(self) -> dict:
|
||||
logger.info("🔄 Processing autopayments...")
|
||||
|
||||
try:
|
||||
subscriptions_to_pay = await self.db.get_subscriptions_for_autopay()
|
||||
|
||||
if not subscriptions_to_pay:
|
||||
logger.info("No subscriptions ready for autopay")
|
||||
return {
|
||||
'processed': 0,
|
||||
'successful': 0,
|
||||
'failed': 0,
|
||||
'insufficient_balance': 0,
|
||||
'errors': []
|
||||
}
|
||||
|
||||
logger.info(f"Found {len(subscriptions_to_pay)} subscriptions for autopay")
|
||||
|
||||
stats = {
|
||||
'processed': 0,
|
||||
'successful': 0,
|
||||
'failed': 0,
|
||||
'insufficient_balance': 0,
|
||||
'errors': []
|
||||
}
|
||||
|
||||
for user_sub in subscriptions_to_pay:
|
||||
try:
|
||||
result = await self._process_single_autopayment(user_sub)
|
||||
stats['processed'] += 1
|
||||
|
||||
if result['success']:
|
||||
stats['successful'] += 1
|
||||
logger.info(f"✅ Autopay successful for user {user_sub.user_id}, subscription {user_sub.id}")
|
||||
elif result['reason'] == 'insufficient_balance':
|
||||
stats['insufficient_balance'] += 1
|
||||
logger.info(f"💳 Insufficient balance for user {user_sub.user_id}, subscription {user_sub.id}")
|
||||
else:
|
||||
stats['failed'] += 1
|
||||
stats['errors'].append(f"User {user_sub.user_id}: {result['reason']}")
|
||||
logger.warning(f"❌ Autopay failed for user {user_sub.user_id}: {result['reason']}")
|
||||
|
||||
except Exception as e:
|
||||
stats['processed'] += 1
|
||||
stats['failed'] += 1
|
||||
error_msg = f"User {user_sub.user_id}: {str(e)}"
|
||||
stats['errors'].append(error_msg)
|
||||
logger.error(f"Error processing autopay for user {user_sub.user_id}: {e}")
|
||||
|
||||
logger.info(f"📊 Autopay processing complete: {stats['successful']} successful, "
|
||||
f"{stats['failed']} failed, {stats['insufficient_balance']} insufficient balance")
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in process_autopayments: {e}")
|
||||
return {
|
||||
'processed': 0,
|
||||
'successful': 0,
|
||||
'failed': 0,
|
||||
'insufficient_balance': 0,
|
||||
'errors': [str(e)]
|
||||
}
|
||||
|
||||
async def _process_single_autopayment(self, user_sub: UserSubscription) -> dict:
|
||||
try:
|
||||
user = await self.db.get_user_by_telegram_id(user_sub.user_id)
|
||||
if not user:
|
||||
return {'success': False, 'reason': 'User not found'}
|
||||
|
||||
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription:
|
||||
return {'success': False, 'reason': 'Subscription plan not found'}
|
||||
|
||||
if subscription.is_trial:
|
||||
logger.info(f"Skipping autopay for trial subscription: user {user_sub.user_id}")
|
||||
return {'success': False, 'reason': 'Trial subscriptions are not eligible for autopay'}
|
||||
|
||||
if not user_sub.is_active or not user_sub.auto_pay_enabled:
|
||||
return {'success': False, 'reason': 'Subscription inactive or autopay disabled'}
|
||||
|
||||
if user.balance < subscription.price:
|
||||
await self._notify_insufficient_balance(user, subscription, user_sub)
|
||||
return {'success': False, 'reason': 'insufficient_balance'}
|
||||
|
||||
return await self._execute_autopayment(user, subscription, user_sub)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in _process_single_autopayment: {e}")
|
||||
return {'success': False, 'reason': str(e)}
|
||||
|
||||
async def _execute_autopayment(self, user: User, subscription: Subscription, user_sub: UserSubscription) -> dict:
|
||||
try:
|
||||
user.balance -= subscription.price
|
||||
await self.db.update_user(user)
|
||||
|
||||
now = datetime.utcnow()
|
||||
if user_sub.expires_at > now:
|
||||
new_expiry = user_sub.expires_at + timedelta(days=subscription.duration_days)
|
||||
else:
|
||||
new_expiry = now + timedelta(days=subscription.duration_days)
|
||||
|
||||
user_sub.expires_at = new_expiry
|
||||
user_sub.is_active = True
|
||||
await self.db.update_user_subscription(user_sub)
|
||||
|
||||
if self.api and user_sub.short_uuid:
|
||||
try:
|
||||
remna_user_details = await self.api.get_user_by_short_uuid(user_sub.short_uuid)
|
||||
if remna_user_details:
|
||||
user_uuid = remna_user_details.get('uuid')
|
||||
if user_uuid:
|
||||
expiry_str = new_expiry.isoformat() + 'Z'
|
||||
update_data = {
|
||||
'enable': True,
|
||||
'expireAt': expiry_str
|
||||
}
|
||||
await self.api.update_user(user_uuid, update_data)
|
||||
logger.info(f"Updated RemnaWave expiry for user {user_sub.user_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to update RemnaWave expiry: {e}")
|
||||
|
||||
payment = await self.db.create_payment(
|
||||
user_id=user_sub.user_id,
|
||||
amount=-subscription.price,
|
||||
payment_type='autopay',
|
||||
description=f'Автоплатеж: {subscription.name}',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
if self.bot:
|
||||
try:
|
||||
await process_referral_rewards(
|
||||
user_sub.user_id,
|
||||
subscription.price,
|
||||
payment.id,
|
||||
self.db,
|
||||
self.bot,
|
||||
payment_type='autopay'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to process referral rewards for autopay: {e}")
|
||||
|
||||
await self._notify_successful_autopay(user, subscription, user_sub, new_expiry)
|
||||
|
||||
return {'success': True, 'reason': 'Payment processed successfully'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing autopayment: {e}")
|
||||
try:
|
||||
user.balance += subscription.price
|
||||
await self.db.update_user(user)
|
||||
except:
|
||||
pass
|
||||
return {'success': False, 'reason': str(e)}
|
||||
|
||||
async def _notify_successful_autopay(self, user: User, subscription: Subscription,
|
||||
user_sub: UserSubscription, new_expiry: datetime):
|
||||
if not self.bot:
|
||||
return
|
||||
|
||||
try:
|
||||
from utils import format_datetime
|
||||
|
||||
text = f"✅ Автоматическое продление подписки\n\n"
|
||||
text += f"📋 Подписка: {subscription.name}\n"
|
||||
text += f"💰 Списано: {subscription.price} руб.\n"
|
||||
text += f"📅 Продлено до: {format_datetime(new_expiry, user.language)}\n"
|
||||
text += f"💳 Остаток на балансе: {user.balance} руб.\n\n"
|
||||
text += f"🔄 Следующее продление произойдет автоматически за {user_sub.auto_pay_days_before} дн. до истечения.\n\n"
|
||||
text += f"ℹ️ Для отключения автоплатежа перейдите в 'Мои подписки' → выберите подписку"
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")],
|
||||
[InlineKeyboardButton(text="💰 Баланс", callback_data="balance")]
|
||||
])
|
||||
|
||||
await self.bot.send_message(
|
||||
user.telegram_id,
|
||||
text,
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send autopay success notification: {e}")
|
||||
|
||||
async def _notify_insufficient_balance(self, user: User, subscription: Subscription, user_sub: UserSubscription):
|
||||
if not self.bot:
|
||||
return
|
||||
|
||||
try:
|
||||
from utils import format_datetime
|
||||
|
||||
needed = subscription.price - user.balance
|
||||
days_left = (user_sub.expires_at - datetime.utcnow()).days
|
||||
|
||||
text = f"⚠️ Не удалось автоматически продлить подписку\n\n"
|
||||
text += f"📋 Подписка: {subscription.name}\n"
|
||||
text += f"💰 Нужно для продления: {subscription.price} руб.\n"
|
||||
text += f"💳 Ваш баланс: {user.balance} руб.\n"
|
||||
text += f"💸 Недостает: {needed} руб.\n\n"
|
||||
text += f"📅 Подписка истекает: {format_datetime(user_sub.expires_at, user.language)}\n"
|
||||
text += f"⏰ Осталось дней: {days_left}\n\n"
|
||||
text += f"💡 Пополните баланс для автоматического продления"
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="topup_balance")],
|
||||
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")]
|
||||
])
|
||||
|
||||
await self.bot.send_message(
|
||||
user.telegram_id,
|
||||
text,
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send insufficient balance notification: {e}")
|
||||
|
||||
async def get_service_status(self) -> dict:
|
||||
return {
|
||||
'is_running': self.is_running,
|
||||
'check_interval': 1800, # 30 минут
|
||||
'has_api': self.api is not None,
|
||||
'has_bot': self.bot is not None
|
||||
}
|
||||
@@ -1,48 +1,41 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
from typing import List, Dict
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
# Bot configuration
|
||||
BOT_TOKEN: str
|
||||
BOT_USERNAME: str
|
||||
|
||||
# Database
|
||||
DATABASE_URL: str
|
||||
|
||||
# RemnaWave API
|
||||
REMNAWAVE_URL: str
|
||||
REMNAWAVE_TOKEN: str
|
||||
SUBSCRIPTION_BASE_URL: str
|
||||
|
||||
# Admin configuration
|
||||
ADMIN_IDS: List[int]
|
||||
SUPPORT_USERNAME: str
|
||||
DEFAULT_LANGUAGE: str
|
||||
|
||||
# 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_CHECK_INTERVAL: int = 3600
|
||||
MONITOR_DAILY_CHECK_HOUR: int = 10
|
||||
MONITOR_WARNING_DAYS: int = 3
|
||||
DELETE_EXPIRED_TRIAL_DAYS: int = 1
|
||||
DELETE_EXPIRED_REGULAR_DAYS: int = 7
|
||||
AUTO_DELETE_ENABLED: bool = False
|
||||
|
||||
LUCKY_GAME_ENABLED: bool = True
|
||||
LUCKY_GAME_REWARD: float = 50.0 # Размер награды за выигрыш
|
||||
LUCKY_GAME_NUMBERS: int = 30 # Общее количество чисел (1-30)
|
||||
LUCKY_GAME_WINNING_COUNT: int = 3 # Количество выигрышных чисел
|
||||
LUCKY_GAME_REWARD: float = 50.0
|
||||
LUCKY_GAME_NUMBERS: int = 30
|
||||
LUCKY_GAME_WINNING_COUNT: int = 3
|
||||
|
||||
STARS_ENABLED: bool = True
|
||||
STARS_RATES: Dict[int, float] = None
|
||||
|
||||
def load_config() -> Config:
|
||||
|
||||
@@ -70,46 +63,61 @@ def load_config() -> Config:
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
def parse_stars_rates() -> Dict[int, float]:
|
||||
default_rates = {
|
||||
100: 150.0,
|
||||
150: 220.0,
|
||||
250: 400.0,
|
||||
350: 500.0,
|
||||
500: 800.0,
|
||||
750: 1150.0,
|
||||
1000: 1500.0
|
||||
}
|
||||
|
||||
custom_rates = {}
|
||||
|
||||
for stars in [100, 150, 250, 350, 500, 750, 1000]:
|
||||
env_key = f"STARS_{stars}_RATE"
|
||||
rate = get_float(env_key, 0.0)
|
||||
if rate > 0:
|
||||
custom_rates[stars] = rate
|
||||
|
||||
if custom_rates:
|
||||
return custom_rates
|
||||
|
||||
return default_rates
|
||||
|
||||
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 дня
|
||||
|
||||
# Игра удачи
|
||||
MONITOR_CHECK_INTERVAL=get_int('MONITOR_CHECK_INTERVAL', 3600),
|
||||
MONITOR_DAILY_CHECK_HOUR=get_int('MONITOR_DAILY_CHECK_HOUR', 10),
|
||||
MONITOR_WARNING_DAYS=get_int('MONITOR_WARNING_DAYS', 3),
|
||||
DELETE_EXPIRED_TRIAL_DAYS=get_int('DELETE_EXPIRED_TRIAL_DAYS', 1),
|
||||
DELETE_EXPIRED_REGULAR_DAYS=get_int('DELETE_EXPIRED_REGULAR_DAYS', 7),
|
||||
AUTO_DELETE_ENABLED=get_bool('AUTO_DELETE_ENABLED', False),
|
||||
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)
|
||||
LUCKY_GAME_WINNING_COUNT=get_int('LUCKY_GAME_WINNING_COUNT', 3),
|
||||
STARS_ENABLED=get_bool('STARS_ENABLED', True),
|
||||
STARS_RATES=parse_stars_rates()
|
||||
)
|
||||
|
||||
def debug_environment():
|
||||
@@ -118,7 +126,9 @@ def debug_environment():
|
||||
'REMNAWAVE_URL', 'REMNAWAVE_TOKEN', 'ADMIN_IDS',
|
||||
'REFERRAL_FIRST_REWARD', 'REFERRAL_THRESHOLD',
|
||||
'MONITOR_ENABLED', 'MONITOR_CHECK_INTERVAL',
|
||||
'LUCKY_GAME_ENABLED', 'LUCKY_GAME_REWARD'
|
||||
'DELETE_EXPIRED_TRIAL_DAYS', 'DELETE_EXPIRED_REGULAR_DAYS', 'AUTO_DELETE_ENABLED',
|
||||
'LUCKY_GAME_ENABLED', 'LUCKY_GAME_REWARD',
|
||||
'STARS_ENABLED', 'STARS_100_RATE', 'STARS_500_RATE', 'STARS_1000_RATE'
|
||||
]
|
||||
|
||||
print("📋 Environment variables:")
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Экстренное исправление проблемы с отображением подписок
|
||||
Этот патч добавляет недостающие поля в таблицу user_subscriptions
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Добавляем корневую директорию в путь
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from config import load_config
|
||||
from database import Database
|
||||
from sqlalchemy import text
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def check_and_add_column(db, column_name, column_definition):
|
||||
"""Проверяет и добавляет колонку в таблицу"""
|
||||
try:
|
||||
# Отдельная транзакция для проверки
|
||||
async with db.engine.begin() as conn:
|
||||
await conn.execute(text(f"SELECT {column_name} FROM user_subscriptions LIMIT 1"))
|
||||
logger.info(f"✅ Поле {column_name} уже существует")
|
||||
return True
|
||||
except Exception:
|
||||
# Отдельная транзакция для добавления колонки
|
||||
try:
|
||||
async with db.engine.begin() as conn:
|
||||
logger.info(f"➕ Добавляю поле {column_name}...")
|
||||
await conn.execute(text(f"""
|
||||
ALTER TABLE user_subscriptions
|
||||
ADD COLUMN {column_name} {column_definition}
|
||||
"""))
|
||||
logger.info(f"✅ Поле {column_name} добавлено")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при добавлении {column_name}: {e}")
|
||||
return False
|
||||
|
||||
async def emergency_fix():
|
||||
"""Экстренное исправление базы данных"""
|
||||
|
||||
try:
|
||||
# Загружаем конфигурацию
|
||||
config = load_config()
|
||||
|
||||
# Подключаемся к базе данных
|
||||
db = Database(config.DATABASE_URL)
|
||||
|
||||
logger.info("🔧 Выполняю экстренное исправление базы данных...")
|
||||
|
||||
# Проверяем существование таблицы user_subscriptions
|
||||
try:
|
||||
async with db.engine.begin() as conn:
|
||||
result = await conn.execute(text("SELECT COUNT(*) FROM user_subscriptions"))
|
||||
count = result.scalar()
|
||||
logger.info(f"📊 Найдено {count} подписок в таблице user_subscriptions")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Таблица user_subscriptions не найдена: {e}")
|
||||
await db.close()
|
||||
return
|
||||
|
||||
# Добавляем поля по одному в отдельных транзакциях
|
||||
success1 = await check_and_add_column(db, "auto_pay_enabled", "BOOLEAN DEFAULT FALSE")
|
||||
success2 = await check_and_add_column(db, "auto_pay_days_before", "INTEGER DEFAULT 3")
|
||||
|
||||
# Финальная проверка в отдельной транзакции
|
||||
if success1 and success2:
|
||||
try:
|
||||
async with db.engine.begin() as conn:
|
||||
result = await conn.execute(text("""
|
||||
SELECT id, auto_pay_enabled, auto_pay_days_before
|
||||
FROM user_subscriptions LIMIT 1
|
||||
"""))
|
||||
row = result.fetchone()
|
||||
if row:
|
||||
logger.info("✅ Все поля доступны для чтения")
|
||||
logger.info(f"🔍 Пример записи: id={row[0]}, auto_pay_enabled={row[1]}, auto_pay_days_before={row[2]}")
|
||||
else:
|
||||
logger.info("✅ Все поля доступны, но таблица пуста")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Поля все еще недоступны: {e}")
|
||||
else:
|
||||
logger.error("❌ Не удалось добавить все необходимые поля")
|
||||
|
||||
await db.close()
|
||||
logger.info("🎉 Экстренное исправление завершено!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"💥 Критическая ошибка: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(emergency_fix())
|
||||
@@ -8,12 +8,13 @@ import logging
|
||||
import secrets
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from database import Database, User
|
||||
from database import Database, User, ReferralProgram, ServiceRule
|
||||
from remnawave_api import RemnaWaveAPI
|
||||
from keyboards import *
|
||||
from translations import t
|
||||
from utils import *
|
||||
from config import *
|
||||
from stars_handlers import stars_router
|
||||
import base64
|
||||
import json
|
||||
from referral_utils import (
|
||||
@@ -59,6 +60,12 @@ class BotStates(StatesGroup):
|
||||
admin_debug_user_structure = State()
|
||||
admin_rename_plans_confirm = State()
|
||||
waiting_number_choice = State()
|
||||
waiting_rule_title = State()
|
||||
waiting_rule_content = State()
|
||||
waiting_rule_order = State()
|
||||
waiting_rule_edit_title = State()
|
||||
waiting_rule_edit_content = State()
|
||||
waiting_rule_edit_order = State()
|
||||
|
||||
|
||||
router = Router()
|
||||
@@ -91,10 +98,9 @@ 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'))
|
||||
|
||||
threshold = config.REFERRAL_THRESHOLD if config else 300.0
|
||||
referred_bonus = config.REFERRAL_REFERRED_BONUS if config else 150.0
|
||||
|
||||
await message.answer(
|
||||
"🎁 Добро пожаловать!\n\n"
|
||||
f"Вы перешли по реферальной ссылке! После пополнения баланса на {threshold:.0f}₽ "
|
||||
@@ -122,79 +128,6 @@ async def start_command(message: Message, state: FSMContext, db: Database, **kwa
|
||||
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):
|
||||
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
|
||||
|
||||
if not referral.first_reward_paid and user.balance >= 300:
|
||||
success = await db.create_referral_earning(
|
||||
referrer_id=referral.referrer_id,
|
||||
referred_id=user_id,
|
||||
amount=150.0,
|
||||
earning_type='first_reward',
|
||||
related_payment_id=payment_id
|
||||
)
|
||||
|
||||
if success and bot:
|
||||
try:
|
||||
await bot.send_message(
|
||||
referral.referrer_id,
|
||||
f"🎉 Поздравляем! Ваш реферал пополнил баланс на 300₽+\n\n"
|
||||
f"💰 Вам начислено 150₽ за приведенного друга!\n"
|
||||
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,
|
||||
amount=150.0,
|
||||
payment_type='referral',
|
||||
description='Бонус за переход по реферальной ссылке',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send referral notifications: {e}")
|
||||
|
||||
if amount > 0:
|
||||
percentage_reward = amount * 0.25
|
||||
|
||||
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 and 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}₽ (25%)"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send percentage notification: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing referral rewards: {e}")
|
||||
|
||||
# Language selection
|
||||
@router.callback_query(F.data.startswith("lang_"))
|
||||
async def language_callback(callback: CallbackQuery, state: FSMContext, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
@@ -242,13 +175,12 @@ async def language_callback(callback: CallbackQuery, state: FSMContext, db: Data
|
||||
async def show_main_menu(message: Message, lang: str, is_admin: bool = False, user_id: int = None, db: Database = None, config: Config = None):
|
||||
try:
|
||||
show_trial = False
|
||||
show_lucky_game = True # По умолчанию показываем игру
|
||||
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)
|
||||
|
||||
@@ -489,13 +421,24 @@ async def balance_callback(callback: CallbackQuery, **kwargs):
|
||||
@router.callback_query(F.data == "topup_balance")
|
||||
async def topup_balance_callback(callback: CallbackQuery, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
stars_enabled = config and config.STARS_ENABLED and config.STARS_RATES
|
||||
|
||||
text = t('topup_balance', user.language)
|
||||
|
||||
if stars_enabled:
|
||||
text += "\n\n⭐ **Новинка!** Теперь можно пополнять баланс через Telegram Stars!"
|
||||
text += "\n💎 Быстро, безопасно, без комиссий!"
|
||||
|
||||
await callback.message.edit_text(
|
||||
t('topup_balance', user.language),
|
||||
reply_markup=topup_keyboard(user.language)
|
||||
text,
|
||||
reply_markup=topup_keyboard(user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
@router.callback_query(F.data == "topup_card")
|
||||
@@ -546,7 +489,7 @@ async def handle_amount(message: Message, state: FSMContext, db: Database, **kwa
|
||||
payment = await db.create_payment(
|
||||
user_id=user.telegram_id,
|
||||
amount=amount,
|
||||
payment_type='topup',
|
||||
payment_type='topup',
|
||||
description=f'Пополнение баланса на {amount} руб.'
|
||||
)
|
||||
|
||||
@@ -596,19 +539,49 @@ async def payment_history_callback(callback: CallbackQuery, db: Database, **kwar
|
||||
try:
|
||||
payments = await db.get_user_payments(user.telegram_id)
|
||||
|
||||
if not payments:
|
||||
star_payments = await db.get_user_star_payments(user.telegram_id, 5)
|
||||
|
||||
if not payments and not star_payments:
|
||||
text = t('no_payments', user.language)
|
||||
else:
|
||||
text = "📊 " + t('payment_history', user.language) + ":\n\n"
|
||||
for payment in payments[:10]: # Show last 10 payments
|
||||
date_str = format_datetime(payment.created_at, user.language)
|
||||
status = format_payment_status(payment.status, user.language)
|
||||
text += t('payment_item', user.language,
|
||||
date=date_str,
|
||||
amount=payment.amount,
|
||||
description=payment.description,
|
||||
status=status
|
||||
) + "\n"
|
||||
|
||||
all_payments = []
|
||||
|
||||
for payment in payments[:10]:
|
||||
all_payments.append({
|
||||
'type': 'regular',
|
||||
'date': payment.created_at,
|
||||
'amount': payment.amount,
|
||||
'description': payment.description,
|
||||
'status': payment.status
|
||||
})
|
||||
|
||||
for star_payment in star_payments:
|
||||
if star_payment.status == 'completed':
|
||||
all_payments.append({
|
||||
'type': 'stars',
|
||||
'date': star_payment.completed_at or star_payment.created_at,
|
||||
'amount': star_payment.rub_amount,
|
||||
'description': f'Пополнение через Stars ({star_payment.stars_amount} ⭐)',
|
||||
'status': star_payment.status,
|
||||
'stars': star_payment.stars_amount
|
||||
})
|
||||
|
||||
all_payments.sort(key=lambda x: x['date'], reverse=True)
|
||||
|
||||
for payment in all_payments[:10]:
|
||||
date_str = format_datetime(payment['date'], user.language)
|
||||
status = format_payment_status(payment['status'], user.language)
|
||||
|
||||
if payment['type'] == 'stars':
|
||||
text += f"⭐ +{payment['amount']:.0f}₽ ({payment['stars']} ⭐)\n"
|
||||
else:
|
||||
amount_str = f"+{payment['amount']}" if payment['amount'] > 0 else str(payment['amount'])
|
||||
text += f"💳 {amount_str}₽\n"
|
||||
|
||||
text += f" 📅 {date_str} | {status}\n"
|
||||
text += f" 📝 {payment['description']}\n\n"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
@@ -800,14 +773,12 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
|
||||
|
||||
payment = await db.create_payment(
|
||||
user_id=user.telegram_id,
|
||||
amount=-subscription.price,
|
||||
payment_type='subscription',
|
||||
amount=-subscription.price,
|
||||
payment_type='subscription',
|
||||
description=f'Покупка подписки: {subscription.name}',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
bot = kwargs.get('bot')
|
||||
await process_referral_rewards(user.telegram_id, subscription.price, payment.id, db, bot)
|
||||
|
||||
success_text = f"✅ Подписка успешно создана!\n\n"
|
||||
success_text += f"📋 Подписка: {subscription.name}\n"
|
||||
@@ -879,7 +850,7 @@ async def my_subscriptions_callback(callback: CallbackQuery, db: Database, **kwa
|
||||
|
||||
subscription_name = subscription.name
|
||||
if subscription.is_imported or subscription.name == "Старая подписка":
|
||||
subscription_name += " 🔄" # Добавляем иконку импорта
|
||||
subscription_name += " 🔄"
|
||||
|
||||
text += f"{i}. {subscription_name}\n"
|
||||
text += f" {status}\n"
|
||||
@@ -968,6 +939,20 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
|
||||
|
||||
text = format_user_subscription_info(user_sub.__dict__, sub_dict, user_sub.expires_at, user.language)
|
||||
|
||||
if not is_trial and not is_imported and subscription.price > 0:
|
||||
text += f"\n\n🔄 **Автоплатеж:**\n"
|
||||
if user_sub.auto_pay_enabled:
|
||||
text += f"✅ Включен (за {user_sub.auto_pay_days_before} дн. до истечения)\n"
|
||||
text += f"💰 Стоимость продления: {subscription.price} руб.\n"
|
||||
text += f"💳 Ваш баланс: {user.balance} руб."
|
||||
|
||||
if user.balance < subscription.price:
|
||||
needed = subscription.price - user.balance
|
||||
text += f"\n⚠️ Недостаточно средств! Нужно еще {needed} руб."
|
||||
else:
|
||||
text += f"❌ Отключен\n"
|
||||
text += f"💡 Включите для автоматического продления"
|
||||
|
||||
if user_sub.short_uuid and api:
|
||||
try:
|
||||
subscription_url = await api.get_subscription_url(user_sub.short_uuid)
|
||||
@@ -977,8 +962,7 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
|
||||
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"
|
||||
text += f"\n\n📅 Истекает через {days_until_expiry} дн.\n"
|
||||
text += f"🛒 Для продолжения работы приобретите новый тарифный план."
|
||||
elif is_trial and 0 <= days_until_expiry <= 3:
|
||||
text += f"\n\nℹ️ Тестовая подписка истекает через {days_until_expiry} дн.\n"
|
||||
@@ -988,7 +972,14 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=user_subscription_detail_keyboard(user_sub_id, user.language, show_extend, is_imported),
|
||||
reply_markup=user_subscription_detail_keyboard(
|
||||
user_sub_id,
|
||||
user.language,
|
||||
show_extend,
|
||||
is_imported,
|
||||
is_trial,
|
||||
user_sub.auto_pay_enabled
|
||||
),
|
||||
parse_mode='HTML',
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
@@ -1154,14 +1145,15 @@ async def confirm_extend_subscription_callback(callback: CallbackQuery, db: Data
|
||||
user.balance -= subscription.price
|
||||
await db.update_user(user)
|
||||
|
||||
await db.create_payment(
|
||||
payment = await db.create_payment(
|
||||
user_id=user.telegram_id,
|
||||
amount=-subscription.price,
|
||||
payment_type='subscription_extend',
|
||||
amount=-subscription.price,
|
||||
payment_type='subscription_extend',
|
||||
description=f'Продление подписки: {subscription.name}',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
|
||||
success_text = f"✅ Подписка успешно продлена!\n\n"
|
||||
success_text += f"📋 Подписка: {subscription.name}\n"
|
||||
success_text += f"📅 Новая дата истечения: {format_datetime(new_expiry, user.language)}\n"
|
||||
@@ -1282,23 +1274,48 @@ async def promocode_callback(callback: CallbackQuery, state: FSMContext, **kwarg
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
t('enter_promocode', user.language),
|
||||
await state.clear()
|
||||
|
||||
edited_message = await callback.message.edit_text(
|
||||
"🎁 Введите промокод:\n\n"
|
||||
"• Обычные промокоды (скидки)\n"
|
||||
"• Реферальные коды (REF...)\n\n"
|
||||
"ℹ️ После ввода вы вернетесь в главное меню",
|
||||
reply_markup=cancel_keyboard(user.language)
|
||||
)
|
||||
|
||||
await state.update_data(promocode_message_id=edited_message.message_id)
|
||||
await state.set_state(BotStates.waiting_promocode)
|
||||
|
||||
@router.message(StateFilter(BotStates.waiting_promocode))
|
||||
async def handle_promocode(message: Message, state: FSMContext, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
bot = kwargs.get('bot')
|
||||
|
||||
if not user:
|
||||
await message.answer("❌ Ошибка пользователя")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
code = message.text.strip().upper()
|
||||
|
||||
state_data = await state.get_data()
|
||||
promocode_message_id = state_data.get('promocode_message_id')
|
||||
|
||||
if not validate_promocode_format(code):
|
||||
await message.answer(t('invalid_input', user.language))
|
||||
response_msg = await message.answer(
|
||||
"❌ Неверный формат промокода",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not delete promocode request message: {e}")
|
||||
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -1306,17 +1323,50 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
|
||||
|
||||
if promocode and promocode.is_active:
|
||||
if promocode.expires_at and promocode.expires_at < datetime.utcnow():
|
||||
await message.answer(t('promocode_expired', user.language))
|
||||
response_msg = await message.answer(
|
||||
"❌ Промокод истек",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
except:
|
||||
pass
|
||||
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
if promocode.used_count >= promocode.usage_limit:
|
||||
await message.answer(t('promocode_limit', user.language))
|
||||
response_msg = await message.answer(
|
||||
"❌ Лимит использования промокода исчерпан",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
except:
|
||||
pass
|
||||
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
success = await db.use_promocode(user.telegram_id, promocode)
|
||||
|
||||
if not success:
|
||||
await message.answer(t('promocode_used', user.language))
|
||||
response_msg = await message.answer(
|
||||
"❌ Вы уже использовали этот промокод",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
except:
|
||||
pass
|
||||
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
await db.add_balance(user.telegram_id, promocode.discount_amount)
|
||||
@@ -1330,18 +1380,23 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
|
||||
)
|
||||
|
||||
discount_text = f"{promocode.discount_amount} руб."
|
||||
await message.answer(
|
||||
success_msg = await message.answer(
|
||||
t('promocode_success', user.language, discount=discount_text),
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
logger.info(f"Deleted promocode request message {promocode_message_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not delete promocode request message: {e}")
|
||||
|
||||
await state.clear()
|
||||
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(
|
||||
@@ -1354,42 +1409,93 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
|
||||
|
||||
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(
|
||||
response_msg = await message.answer(
|
||||
"❌ Нельзя использовать код человека, которого вы пригласили!\n\n"
|
||||
"Взаимные рефералы не допускаются."
|
||||
"Взаимные рефералы не допускаются.",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
except:
|
||||
pass
|
||||
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
success = await create_referral_from_promocode(user.telegram_id, code, db, bot)
|
||||
|
||||
if success:
|
||||
await message.answer(
|
||||
"🎉 Реферальный код активирован!\n\n"
|
||||
"После пополнения баланса на 200₽ вы получите бонус 150₽!",
|
||||
threshold = config.REFERRAL_THRESHOLD if config else 300.0
|
||||
referred_bonus = config.REFERRAL_REFERRED_BONUS if config else 150.0
|
||||
|
||||
success_msg = await message.answer(
|
||||
f"🎉 Реферальный код активирован!\n\n"
|
||||
f"После пополнения баланса на {threshold:.0f}₽ вы получите бонус {referred_bonus:.0f}₽!",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
logger.info(f"Deleted promocode request message {promocode_message_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not delete promocode request message: {e}")
|
||||
|
||||
await state.clear()
|
||||
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("❌ Вы уже использовали реферальный код!")
|
||||
error_text = "❌ Вы уже использовали реферальный код!"
|
||||
else:
|
||||
await message.answer("❌ Неверный реферальный код!")
|
||||
error_text = "❌ Неверный реферальный код!"
|
||||
|
||||
response_msg = await message.answer(
|
||||
error_text,
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
except:
|
||||
pass
|
||||
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
await message.answer(t('promocode_not_found', user.language))
|
||||
response_msg = await message.answer(
|
||||
"❌ Промокод не найден\n\n"
|
||||
"Проверьте правильность ввода и попробуйте снова через главное меню.",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
logger.info(f"Deleted promocode request message {promocode_message_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not delete promocode request message: {e}")
|
||||
|
||||
await state.clear()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling promocode: {e}")
|
||||
await message.answer(
|
||||
t('error_occurred', user.language),
|
||||
response_msg = await message.answer(
|
||||
"❌ Произошла ошибка при обработке промокода",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
if bot and promocode_message_id:
|
||||
try:
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
|
||||
except:
|
||||
pass
|
||||
|
||||
await state.clear()
|
||||
|
||||
@router.callback_query(F.data == "referral_program")
|
||||
async def referral_program_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
@@ -1424,7 +1530,7 @@ async def referral_program_callback(callback: CallbackQuery, db: Database, **kwa
|
||||
|
||||
text += f"• Приведи друга и получи **{first_reward:.0f}₽** на баланс\n"
|
||||
text += f"• Твой друг получит **{referred_bonus:.0f}₽** после пополнения на {threshold:.0f}₽\n"
|
||||
text += f"• С каждого платежа друга ты получаешь **{percentage*100:.0f}%**\n\n"
|
||||
text += f"• С каждого **пополнения баланса** друга ты получаешь **{percentage*100:.0f}%**\n\n"
|
||||
|
||||
text += "**📊 Твоя статистика:**\n"
|
||||
text += f"• Приглашено: {stats['total_referrals']} человек\n"
|
||||
@@ -1573,3 +1679,342 @@ async def my_referrals_callback(callback: CallbackQuery, db: Database, **kwargs)
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing referrals: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки")
|
||||
|
||||
@router.callback_query(F.data == "service_rules")
|
||||
async def service_rules_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
rules = await db.get_all_service_rules(active_only=True)
|
||||
|
||||
if not rules:
|
||||
await callback.message.edit_text(
|
||||
"📜 Правила сервиса пока не добавлены администратором.",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
|
||||
])
|
||||
)
|
||||
return
|
||||
|
||||
await show_rules_page(callback, rules, 0, user.language)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing service rules: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки правил")
|
||||
|
||||
async def show_rules_page(callback: CallbackQuery, rules: List[ServiceRule], page_index: int, lang: str = 'ru'):
|
||||
if page_index < 0 or page_index >= len(rules):
|
||||
page_index = 0
|
||||
|
||||
rule = rules[page_index]
|
||||
total_pages = len(rules)
|
||||
|
||||
safe_title = rule.title.replace('*', '\\*').replace('_', '\\_').replace('[', '\\[').replace(']', '\\]').replace('`', '\\`')
|
||||
text = f"📜 **{safe_title}**\n\n"
|
||||
text += rule.content
|
||||
|
||||
if total_pages > 1:
|
||||
text += f"\n\n📄 Страница {page_index + 1} из {total_pages}"
|
||||
|
||||
if len(text) > 4000:
|
||||
text = text[:3950] + "\n\n... (текст обрезан)"
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=service_rules_keyboard(page_index, total_pages, lang),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
except Exception as markdown_error:
|
||||
logger.warning(f"Markdown parsing failed in user rules, retrying without markdown: {markdown_error}")
|
||||
try:
|
||||
clean_text = text.replace('**', '').replace('*', '').replace('_', '').replace('`', '')
|
||||
await callback.message.edit_text(
|
||||
clean_text,
|
||||
reply_markup=service_rules_keyboard(page_index, total_pages, lang)
|
||||
)
|
||||
except Exception as edit_error:
|
||||
await callback.message.answer(
|
||||
clean_text,
|
||||
reply_markup=service_rules_keyboard(page_index, total_pages, lang)
|
||||
)
|
||||
|
||||
@router.callback_query(F.data.startswith("rules_page_"))
|
||||
async def rules_page_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
page_index = int(callback.data.split("_")[-1])
|
||||
rules = await db.get_all_service_rules(active_only=True)
|
||||
|
||||
if not rules:
|
||||
await callback.answer("❌ Правила не найдены")
|
||||
return
|
||||
|
||||
await show_rules_page(callback, rules, page_index, user.language)
|
||||
|
||||
except (ValueError, IndexError) as e:
|
||||
logger.error(f"Error parsing rules page number: {e}")
|
||||
await callback.answer("❌ Ошибка навигации")
|
||||
except Exception as e:
|
||||
logger.error(f"Error switching rules page: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки страницы")
|
||||
|
||||
@router.callback_query(F.data == "cancel", StateFilter(BotStates.waiting_promocode))
|
||||
async def cancel_promocode_callback(callback: CallbackQuery, state: FSMContext, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
db = kwargs.get('db')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
|
||||
show_trial = False
|
||||
show_lucky_game = True
|
||||
|
||||
if config and config.TRIAL_ENABLED and db:
|
||||
try:
|
||||
has_used = await db.has_used_trial(user.telegram_id)
|
||||
show_trial = not has_used
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking trial availability: {e}")
|
||||
|
||||
if config:
|
||||
show_lucky_game = getattr(config, 'LUCKY_GAME_ENABLED', True)
|
||||
|
||||
await callback.message.edit_text(
|
||||
t('main_menu', user.language),
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin, show_trial, show_lucky_game)
|
||||
)
|
||||
|
||||
@router.callback_query(F.data.startswith("toggle_autopay_"))
|
||||
async def toggle_autopay_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
user_sub_id = int(callback.data.split("_")[2])
|
||||
|
||||
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)
|
||||
|
||||
if not user_sub:
|
||||
await callback.answer("❌ Подписка не найдена")
|
||||
return
|
||||
|
||||
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription:
|
||||
await callback.answer("❌ План подписки не найден")
|
||||
return
|
||||
|
||||
if subscription.is_trial:
|
||||
await callback.answer("❌ Автоплатеж недоступен для тестовых подписок")
|
||||
return
|
||||
|
||||
if subscription.is_imported or subscription.price == 0:
|
||||
await callback.answer("❌ Автоплатеж недоступен для импортированных подписок")
|
||||
return
|
||||
|
||||
new_state = not user_sub.auto_pay_enabled
|
||||
success = await db.toggle_autopay(user_sub_id, new_state)
|
||||
|
||||
if success:
|
||||
status_text = "включен" if new_state else "отключен"
|
||||
await callback.answer(f"✅ Автоплатеж {status_text}")
|
||||
|
||||
updated_subs = await db.get_user_subscriptions(user.telegram_id)
|
||||
updated_sub = next((sub for sub in updated_subs if sub.id == user_sub_id), None)
|
||||
|
||||
if updated_sub:
|
||||
await autopay_settings_callback(callback, db, user=user)
|
||||
else:
|
||||
await callback.answer("❌ Ошибка изменения настроек")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error toggling autopay: {e}")
|
||||
await callback.answer("❌ Ошибка операции")
|
||||
|
||||
@router.callback_query(F.data == "autopay_help")
|
||||
async def autopay_help_callback(callback: CallbackQuery, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
text = "🔄 **Автоплатеж - как это работает?**\n\n"
|
||||
|
||||
text += "**🎯 Что такое автоплатеж:**\n"
|
||||
text += "Автоматическое продление подписки за несколько дней до истечения срока действия.\n\n"
|
||||
|
||||
text += "**⚙️ Как настроить:**\n"
|
||||
text += "1. Перейдите в 'Мои подписки'\n"
|
||||
text += "2. Выберите нужную подписку\n"
|
||||
text += "3. Нажмите 'Настроить автоплатеж'\n"
|
||||
text += "4. Включите автоплатеж и выберите за сколько дней до истечения продлевать\n\n"
|
||||
|
||||
text += "**💰 Требования:**\n"
|
||||
text += "• На балансе должно быть достаточно средств\n"
|
||||
text += "• Подписка должна быть активной\n"
|
||||
text += "• Автоплатеж работает только с обычными подписками\n\n"
|
||||
|
||||
text += "**📅 Варианты продления:**\n"
|
||||
text += "• За 1 день - для тех, кто следит за балансом\n"
|
||||
text += "• За 3 дня - рекомендуемый вариант\n"
|
||||
text += "• За 5 дней - для подстраховки\n"
|
||||
text += "• За 7 дней - максимальный запас времени\n\n"
|
||||
|
||||
text += "**🔔 Уведомления:**\n"
|
||||
text += "Вы получите уведомление при успешном продлении или если недостаточно средств.\n\n"
|
||||
|
||||
text += "**❓ Проблемы:**\n"
|
||||
text += "Если автоплатеж не сработал, проверьте баланс и обратитесь в поддержку."
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=autopay_help_keyboard(user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
@router.callback_query(F.data == "autopay_insufficient_balance_help")
|
||||
async def autopay_insufficient_balance_help_callback(callback: CallbackQuery, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
text = "⚠️ **Недостаточно средств для автоплатежа**\n\n"
|
||||
|
||||
text += f"💳 **Ваш текущий баланс:** {user.balance}₽\n\n"
|
||||
|
||||
text += "**🔄 Что происходит:**\n"
|
||||
text += "Система пыталась автоматически продлить вашу подписку, но на балансе недостаточно средств.\n\n"
|
||||
|
||||
text += "**✅ Что нужно сделать:**\n"
|
||||
text += "1. Пополните баланс на нужную сумму\n"
|
||||
text += "2. Автоплатеж попробует снова при следующей проверке\n"
|
||||
text += "3. Или продлите подписку вручную\n\n"
|
||||
|
||||
text += "**⏰ Важно:**\n"
|
||||
text += "Пополните баланс до истечения срока подписки, иначе доступ будет приостановлен."
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=autopay_help_keyboard(user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
@router.callback_query(F.data.startswith("autopay_settings_"))
|
||||
async def autopay_settings_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
user_sub_id = int(callback.data.split("_")[2])
|
||||
|
||||
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)
|
||||
|
||||
if not user_sub:
|
||||
await callback.answer("❌ Подписка не найдена")
|
||||
return
|
||||
|
||||
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription:
|
||||
await callback.answer("❌ План подписки не найден")
|
||||
return
|
||||
|
||||
if subscription.is_trial:
|
||||
await callback.answer("❌ Автоплатеж недоступен для тестовых подписок")
|
||||
return
|
||||
|
||||
if subscription.is_imported or subscription.price == 0:
|
||||
await callback.answer("❌ Автоплатеж недоступен для импортированных подписок")
|
||||
return
|
||||
|
||||
from datetime import datetime
|
||||
now = datetime.utcnow()
|
||||
days_left = (user_sub.expires_at - now).days
|
||||
|
||||
text = f"⚙️ Настройки автоплатежа\n\n"
|
||||
text += f"📋 Подписка: {subscription.name}\n"
|
||||
text += f"💰 Цена продления: {subscription.price} руб.\n"
|
||||
text += f"⏱ Длительность: {subscription.duration_days} дн.\n"
|
||||
text += f"📅 Истекает через: {days_left} дн.\n\n"
|
||||
|
||||
status_emoji = "✅" if user_sub.auto_pay_enabled else "❌"
|
||||
status_text = "Включен" if user_sub.auto_pay_enabled else "Отключен"
|
||||
text += f"🔄 Автоплатеж: {status_emoji} {status_text}\n"
|
||||
|
||||
if user_sub.auto_pay_enabled:
|
||||
text += f"📅 Продлять за: {user_sub.auto_pay_days_before} дн. до истечения\n\n"
|
||||
|
||||
if user.balance >= subscription.price:
|
||||
text += f"✅ На балансе достаточно средств ({user.balance}₽)\n"
|
||||
text += f"💡 Подписка будет автоматически продлена"
|
||||
else:
|
||||
needed = subscription.price - user.balance
|
||||
text += f"⚠️ Недостаточно средств на балансе!\n"
|
||||
text += f"💳 Ваш баланс: {user.balance}₽\n"
|
||||
text += f"💸 Нужно еще: {needed}₽\n"
|
||||
text += f"💡 Пополните баланс для автоматического продления"
|
||||
else:
|
||||
text += f"\n💡 Включите автоплатеж для автоматического продления подписки\n"
|
||||
text += f"🎯 Рекомендуем включить за 3 дня до истечения"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=autopay_settings_keyboard(user_sub_id, user_sub, user.language)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing autopay settings: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки настроек")
|
||||
|
||||
@router.callback_query(F.data.startswith("autopay_days_"))
|
||||
async def autopay_days_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
parts = callback.data.split("_")
|
||||
user_sub_id = int(parts[2])
|
||||
days = int(parts[3])
|
||||
|
||||
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)
|
||||
|
||||
if not user_sub:
|
||||
await callback.answer("❌ Подписка не найдена")
|
||||
return
|
||||
|
||||
success = await db.set_autopay_days(user_sub_id, days)
|
||||
|
||||
if success:
|
||||
await callback.answer(f"✅ Установлено: продлять за {days} дн.")
|
||||
|
||||
updated_subs = await db.get_user_subscriptions(user.telegram_id)
|
||||
updated_sub = next((sub for sub in updated_subs if sub.id == user_sub_id), None)
|
||||
|
||||
if updated_sub:
|
||||
await autopay_settings_callback(callback, db, user=user)
|
||||
else:
|
||||
await callback.answer("❌ Ошибка обновления настроек")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting autopay days: {e}")
|
||||
await callback.answer("❌ Ошибка операции")
|
||||
|
||||
@@ -2,6 +2,7 @@ from database import Subscription
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from typing import List, Optional, Dict
|
||||
from translations import t
|
||||
from datetime import datetime
|
||||
|
||||
def language_keyboard() -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
@@ -35,7 +36,8 @@ def main_menu_keyboard(lang: str = 'ru', is_admin: bool = False, show_trial: boo
|
||||
[
|
||||
InlineKeyboardButton(text="💬 " + t('support', lang), callback_data="support")
|
||||
],
|
||||
[InlineKeyboardButton(text="🌐 " + t('change_language', lang), callback_data="change_language")]
|
||||
[InlineKeyboardButton(text="🌐 " + t('change_language', lang), callback_data="change_language")],
|
||||
[InlineKeyboardButton(text="📜 Правила сервиса", callback_data="service_rules")]
|
||||
])
|
||||
|
||||
if is_admin:
|
||||
@@ -109,22 +111,52 @@ def user_subscriptions_keyboard(user_subscriptions: List[dict], lang: str = 'ru'
|
||||
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:
|
||||
def user_subscription_detail_keyboard(subscription_id: int, lang: str = 'ru',
|
||||
show_extend: bool = False, is_imported: bool = False,
|
||||
is_trial: bool = False, autopay_enabled: bool = False) -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
if is_imported:
|
||||
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
|
||||
buttons.append([InlineKeyboardButton(text="🛒 Купить новую подписку", callback_data="buy_subscription")])
|
||||
elif is_trial:
|
||||
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}")])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
|
||||
|
||||
if autopay_enabled:
|
||||
autopay_text = "🔄✅ Настроить автоплатеж"
|
||||
else:
|
||||
autopay_text = "🔄❌ Настроить автоплатеж"
|
||||
|
||||
buttons.append([InlineKeyboardButton(text=autopay_text, callback_data=f"autopay_settings_{subscription_id}")])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="my_subscriptions")])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def autopay_confirmation_keyboard(subscription_id: int, action: str, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_autopay_{action}_{subscription_id}"),
|
||||
InlineKeyboardButton(text="❌ Нет", callback_data=f"autopay_settings_{subscription_id}")
|
||||
]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def autopay_help_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Клавиатура помощи по автоплатежам"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="topup_balance")],
|
||||
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")],
|
||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def extend_subscription_keyboard(subscription_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
@@ -159,13 +191,17 @@ def admin_menu_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📨 " + t('send_message', lang), callback_data="admin_messages"),
|
||||
InlineKeyboardButton(text="👥 Рефералы", callback_data="admin_referrals") # НОВАЯ КНОПКА
|
||||
InlineKeyboardButton(text="👥 Рефералы", callback_data="admin_referrals")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system")
|
||||
InlineKeyboardButton(text="📜 Правила сервиса", callback_data="admin_rules"),
|
||||
InlineKeyboardButton(text="🔄 Автоплатежи", callback_data="admin_autopay")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system"),
|
||||
InlineKeyboardButton(text="🔍 Мониторинг подписок", callback_data="admin_monitor")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🔍 Мониторинг подписок", callback_data="admin_monitor"),
|
||||
InlineKeyboardButton(text="📊 " + t('statistics', lang), callback_data="admin_stats")
|
||||
],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")]
|
||||
@@ -183,15 +219,89 @@ def admin_subscriptions_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
def admin_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="👥 Список пользователей", callback_data="list_users")],
|
||||
[InlineKeyboardButton(text="📋 Все подписки пользователей", callback_data="admin_user_subscriptions_all")],
|
||||
[InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="search_user")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def admin_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="👥 Список пользователей", callback_data="list_users")],
|
||||
[InlineKeyboardButton(text="📋 Все подписки пользователей", callback_data="admin_user_subscriptions_all")],
|
||||
[InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="search_user")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def admin_user_subscriptions_filters_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Клавиатура фильтров для подписок пользователей"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="🟢 Активные", callback_data="filter_subs_active"),
|
||||
InlineKeyboardButton(text="🔴 Истекшие", callback_data="filter_subs_expired")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🔄✅ С автоплатежом", callback_data="filter_subs_autopay"),
|
||||
InlineKeyboardButton(text="⏰ Истекают скоро", callback_data="filter_subs_expiring")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🆓 Триальные", callback_data="filter_subs_trial"),
|
||||
InlineKeyboardButton(text="📦 Импортированные", callback_data="filter_subs_imported")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📋 Все подписки", callback_data="admin_user_subscriptions_all"),
|
||||
InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")
|
||||
]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def admin_user_subscription_detail_keyboard(subscription_id: int, user_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Клавиатура для детального просмотра подписки пользователя"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="✏️ Редактировать", callback_data=f"edit_user_sub_{subscription_id}"),
|
||||
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_user_sub_{subscription_id}")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="👤 К пользователю", callback_data=f"admin_user_detail_{user_id}"),
|
||||
InlineKeyboardButton(text="📋 К списку подписок", callback_data="admin_user_subscriptions_all")
|
||||
],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def user_subscriptions_pagination_keyboard(current_page: int, total_pages: int,
|
||||
filter_type: str = "all", lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
if total_pages > 1:
|
||||
nav_row = []
|
||||
|
||||
if current_page > 0:
|
||||
nav_row.append(InlineKeyboardButton(text="⬅️", callback_data=f"user_subs_page_{current_page - 1}_{filter_type}"))
|
||||
|
||||
nav_row.append(InlineKeyboardButton(text=f"{current_page + 1}/{total_pages}", callback_data="noop"))
|
||||
|
||||
if current_page < total_pages - 1:
|
||||
nav_row.append(InlineKeyboardButton(text="➡️", callback_data=f"user_subs_page_{current_page + 1}_{filter_type}"))
|
||||
|
||||
buttons.append(nav_row)
|
||||
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text="🔍 Фильтры", callback_data="admin_user_subscriptions_filters"),
|
||||
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_user_subs_{filter_type}")
|
||||
])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def admin_balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💸 Пополнить баланс пользователю", callback_data="admin_add_balance")],
|
||||
[InlineKeyboardButton(text="📊 История платежей", callback_data="admin_payment_history")],
|
||||
[InlineKeyboardButton(text="⭐ Telegram Stars", callback_data="admin_stars_payments")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
|
||||
])
|
||||
return keyboard
|
||||
@@ -410,9 +520,6 @@ def trial_subscription_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
def admin_monitor_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📊 Статус сервиса", callback_data="monitor_status")],
|
||||
[InlineKeyboardButton(text="🔄 Принудительная проверка", callback_data="monitor_force_check")],
|
||||
[InlineKeyboardButton(text="⚰️ Деактивировать истекшие", callback_data="monitor_deactivate_expired")],
|
||||
[InlineKeyboardButton(text="👤 Тест для пользователя", callback_data="monitor_test_user")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
|
||||
])
|
||||
return keyboard
|
||||
@@ -605,3 +712,219 @@ def lucky_game_result_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
[InlineKeyboardButton(text="📈 История игр", callback_data="lucky_game_history")],
|
||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
|
||||
])
|
||||
|
||||
def topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="⭐ Telegram Stars", callback_data="topup_stars")],
|
||||
[InlineKeyboardButton(text="👨💼 " + t('topup_support', lang), callback_data="topup_support")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="balance")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def stars_topup_keyboard(stars_rates: Dict[int, float], lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
sorted_rates = sorted(stars_rates.items())
|
||||
|
||||
for i in range(0, len(sorted_rates), 2):
|
||||
row = []
|
||||
for j in range(2):
|
||||
if i + j < len(sorted_rates):
|
||||
stars, rubles = sorted_rates[i + j]
|
||||
if stars >= 500:
|
||||
emoji = "🔥" # Выгодное предложение
|
||||
elif stars >= 250:
|
||||
emoji = "💎" # Хорошее предложение
|
||||
else:
|
||||
emoji = "⭐" # Базовое предложение
|
||||
|
||||
button_text = f"{emoji} {stars} ⭐ → {rubles:.0f}₽"
|
||||
row.append(InlineKeyboardButton(
|
||||
text=button_text,
|
||||
callback_data=f"buy_stars_{stars}"
|
||||
))
|
||||
buttons.append(row)
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="topup_balance")])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def stars_payment_keyboard(stars_amount: int, rub_amount: float, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="topup_stars")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def service_rules_keyboard(current_page: int, total_pages: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
if total_pages > 1:
|
||||
nav_row = []
|
||||
|
||||
if current_page > 0:
|
||||
nav_row.append(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"rules_page_{current_page - 1}"))
|
||||
|
||||
nav_row.append(InlineKeyboardButton(text=f"{current_page + 1}/{total_pages}", callback_data="noop"))
|
||||
|
||||
if current_page < total_pages - 1:
|
||||
nav_row.append(InlineKeyboardButton(text="Вперед ➡️", callback_data=f"rules_page_{current_page + 1}"))
|
||||
|
||||
buttons.append(nav_row)
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def admin_rules_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📋 Список правил", callback_data="admin_rules_list")],
|
||||
[InlineKeyboardButton(text="➕ Добавить страницу", callback_data="admin_rules_create")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def admin_rules_list_keyboard(rules, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
for rule in rules:
|
||||
status_emoji = "🟢" if rule.is_active else "🔴"
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=f"{status_emoji} {rule.page_order}. {rule.title}",
|
||||
callback_data=f"admin_rule_view_{rule.id}"
|
||||
)
|
||||
])
|
||||
|
||||
buttons.extend([
|
||||
[InlineKeyboardButton(text="➕ Добавить страницу", callback_data="admin_rules_create")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_rules")]
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def admin_rule_edit_keyboard(rule_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="✏️ Редактировать заголовок", callback_data=f"admin_rule_edit_title_{rule_id}"),
|
||||
InlineKeyboardButton(text="📝 Редактировать содержимое", callback_data=f"admin_rule_edit_content_{rule_id}")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🔄 Изменить порядок", callback_data=f"admin_rule_edit_order_{rule_id}"),
|
||||
InlineKeyboardButton(text="🟢/🔴 Вкл/Выкл", callback_data=f"admin_rule_toggle_{rule_id}")
|
||||
],
|
||||
[InlineKeyboardButton(text="🗑 Удалить", callback_data=f"admin_rule_delete_{rule_id}")],
|
||||
[InlineKeyboardButton(text="🔙 К списку", callback_data="admin_rules_list")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def admin_rule_delete_confirm_keyboard(rule_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="✅ Да, удалить", callback_data=f"admin_rule_confirm_delete_{rule_id}"),
|
||||
InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_rule_view_{rule_id}")
|
||||
]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def autopay_settings_keyboard(user_sub_id: int, user_sub, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
if user_sub.auto_pay_enabled:
|
||||
toggle_text = "❌ Отключить автоплатеж"
|
||||
toggle_callback = f"toggle_autopay_{user_sub_id}"
|
||||
else:
|
||||
toggle_text = "✅ Включить автоплатеж"
|
||||
toggle_callback = f"toggle_autopay_{user_sub_id}"
|
||||
|
||||
buttons.append([InlineKeyboardButton(text=toggle_text, callback_data=toggle_callback)])
|
||||
|
||||
if user_sub.auto_pay_enabled:
|
||||
buttons.append([InlineKeyboardButton(text="📅 Настроить дни до продления", callback_data="noop")])
|
||||
|
||||
days_row = []
|
||||
for days in [1, 3, 5, 7]:
|
||||
emoji = "🔹" if user_sub.auto_pay_days_before == days else "⚪"
|
||||
days_row.append(InlineKeyboardButton(
|
||||
text=f"{emoji} {days}д",
|
||||
callback_data=f"autopay_days_{user_sub_id}_{days}"
|
||||
))
|
||||
buttons.append(days_row)
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 К подписке", callback_data=f"view_sub_{user_sub_id}")])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def autopay_status_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_status")],
|
||||
[InlineKeyboardButton(text="🚀 Принудительная проверка", callback_data="autopay_force_check")],
|
||||
[InlineKeyboardButton(text="📋 Список подписок", callback_data="autopay_subscriptions_list")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def autopay_subscriptions_keyboard(subscriptions_data: List[Dict], lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
expired = [s for s in subscriptions_data if s['expires_in_days'] <= 0]
|
||||
due_soon = [s for s in subscriptions_data if 0 < s['expires_in_days'] <= s['auto_pay_days_before']]
|
||||
|
||||
critical_subs = expired + due_soon
|
||||
|
||||
for sub_data in critical_subs[:8]:
|
||||
username = sub_data['username'] if sub_data['username'] != 'N/A' else f"ID:{sub_data['user_id']}"
|
||||
days = sub_data['expires_in_days']
|
||||
|
||||
if days <= 0:
|
||||
status_emoji = "❌"
|
||||
status_text = f"Истекла"
|
||||
elif days <= sub_data['auto_pay_days_before']:
|
||||
status_emoji = "⚠️"
|
||||
status_text = f"Через {days}д"
|
||||
else:
|
||||
status_emoji = "✅"
|
||||
status_text = f"Через {days}д"
|
||||
|
||||
button_text = f"{status_emoji} @{username} ({status_text})"
|
||||
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=button_text,
|
||||
callback_data=f"autopay_user_detail_{sub_data['user_id']}"
|
||||
)
|
||||
])
|
||||
|
||||
if len(subscriptions_data) > 8:
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=f"... и еще {len(subscriptions_data) - 8}",
|
||||
callback_data="noop"
|
||||
)
|
||||
])
|
||||
|
||||
buttons.extend([
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_subscriptions_list")],
|
||||
[InlineKeyboardButton(text="📊 Статистика", callback_data="autopay_statistics")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def autopay_user_detail_keyboard(user_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💳 Добавить баланс", callback_data=f"admin_add_balance_to_{user_id}")],
|
||||
[InlineKeyboardButton(text="📋 Управление подписками", callback_data=f"admin_user_subscriptions_{user_id}")],
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data=f"autopay_user_detail_{user_id}")],
|
||||
[InlineKeyboardButton(text="🔙 К списку", callback_data="autopay_subscriptions_list")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
|
||||
def autopay_statistics_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="⚠️ Недостаточно средств", callback_data="autopay_insufficient_balance_users")],
|
||||
[InlineKeyboardButton(text="📋 Список подписок", callback_data="autopay_subscriptions_list")],
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_statistics")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
@@ -25,7 +25,6 @@ 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')
|
||||
|
||||
@@ -34,15 +33,12 @@ async def lucky_game_menu_callback(callback: CallbackQuery, db: Database, **kwar
|
||||
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"
|
||||
@@ -61,13 +57,11 @@ async def lucky_game_menu_callback(callback: CallbackQuery, db: Database, **kwar
|
||||
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)
|
||||
@@ -108,19 +102,16 @@ async def start_lucky_game_callback(callback: CallbackQuery, db: Database, state
|
||||
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(
|
||||
@@ -135,7 +126,6 @@ async def start_lucky_game_callback(callback: CallbackQuery, db: Database, state
|
||||
text += f"🍀 Удачных чисел: {winning_numbers} из {numbers_count}\n\n"
|
||||
text += "Нажмите на число, чтобы испытать удачу!"
|
||||
|
||||
# Создаем кнопки с числами (5 в ряд)
|
||||
buttons = []
|
||||
for i in range(0, numbers_count, 5):
|
||||
row = []
|
||||
@@ -175,24 +165,18 @@ async def choose_number_callback(callback: CallbackQuery, db: Database, state: F
|
||||
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,
|
||||
@@ -211,7 +195,6 @@ async def choose_number_callback(callback: CallbackQuery, db: Database, state: F
|
||||
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⏰ Следующая игра будет доступна завтра!"
|
||||
@@ -227,7 +210,6 @@ async def choose_number_callback(callback: CallbackQuery, db: Database, state: F
|
||||
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}")
|
||||
|
||||
@@ -239,7 +221,6 @@ async def choose_number_callback(callback: CallbackQuery, db: Database, state: F
|
||||
|
||||
@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:
|
||||
@@ -247,7 +228,6 @@ async def lucky_game_history_callback(callback: CallbackQuery, db: Database, **k
|
||||
return
|
||||
|
||||
try:
|
||||
# Получаем последние 10 игр
|
||||
games = await get_user_game_history(db, user.telegram_id, limit=10)
|
||||
|
||||
if not games:
|
||||
@@ -289,8 +269,7 @@ async def lucky_game_history_callback(callback: CallbackQuery, db: Database, **k
|
||||
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]]:
|
||||
"""Проверяет, может ли пользователь играть сегодня"""
|
||||
@@ -298,7 +277,6 @@ async def check_can_play_today(db: Database, user_id: int) -> tuple[bool, Option
|
||||
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
|
||||
|
||||
@@ -7,6 +7,8 @@ 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
|
||||
from stars_handlers import stars_router
|
||||
from autopay_service import AutoPayService
|
||||
|
||||
print("🚀 Запуск бота...")
|
||||
print(f"📍 Рабочая директория: {os.getcwd()}")
|
||||
@@ -45,6 +47,40 @@ class BotApplication:
|
||||
self.bot = None
|
||||
self.dp = None
|
||||
self.monitor_service = None
|
||||
self.autopay_service = None
|
||||
|
||||
async def _init_autopay_service(self):
|
||||
"""Инициализирует сервис автоплатежей"""
|
||||
try:
|
||||
logger.info("🔧 Initializing autopay service...")
|
||||
|
||||
if not self.bot:
|
||||
logger.error("❌ Bot instance is None, cannot initialize autopay")
|
||||
return
|
||||
|
||||
if not self.db:
|
||||
logger.error("❌ Database instance is None, cannot initialize autopay")
|
||||
return
|
||||
|
||||
self.autopay_service = AutoPayService(self.db, self.api, self.bot)
|
||||
|
||||
self.dp.workflow_data["autopay_service"] = self.autopay_service
|
||||
logger.info("✅ Autopay service added to workflow_data")
|
||||
|
||||
logger.info("🚀 Starting autopay service...")
|
||||
await self.autopay_service.start()
|
||||
|
||||
status = await self.autopay_service.get_service_status()
|
||||
if status['is_running']:
|
||||
logger.info("✅ Autopay service started successfully")
|
||||
logger.info(f"📊 Autopay status: interval=30min")
|
||||
else:
|
||||
logger.warning("⚠️ Autopay service created but not running")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Failed to initialize autopay service: {e}", exc_info=True)
|
||||
logger.warning("⚠️ Continuing without autopay service")
|
||||
self.autopay_service = None
|
||||
|
||||
async def initialize(self):
|
||||
|
||||
@@ -96,6 +132,25 @@ class BotApplication:
|
||||
self._setup_dispatcher()
|
||||
|
||||
await self._init_monitor_service()
|
||||
await self._init_autopay_service()
|
||||
|
||||
if self.config.STARS_ENABLED:
|
||||
logger.info("✅ Telegram Stars пополнение включено")
|
||||
logger.info(f"📊 Настроенные курсы: {self.config.STARS_RATES}")
|
||||
|
||||
if not self.config.STARS_RATES or len(self.config.STARS_RATES) == 0:
|
||||
logger.warning("⚠️ STARS_RATES пустые! Telegram Stars будут недоступны")
|
||||
self.config.STARS_ENABLED = False
|
||||
else:
|
||||
valid_rates = all(
|
||||
isinstance(stars, int) and isinstance(rubles, (int, float)) and stars > 0 and rubles > 0
|
||||
for stars, rubles in self.config.STARS_RATES.items()
|
||||
)
|
||||
if not valid_rates:
|
||||
logger.warning("⚠️ Неверные STARS_RATES! Telegram Stars будут недоступны")
|
||||
self.config.STARS_ENABLED = False
|
||||
else:
|
||||
logger.info("❌ Telegram Stars пополнение отключено")
|
||||
|
||||
async def _init_database(self):
|
||||
max_retries = 3
|
||||
@@ -167,6 +222,7 @@ class BotApplication:
|
||||
self.dp.include_router(router)
|
||||
self.dp.include_router(admin_router)
|
||||
self.dp.include_router(lucky_game_router)
|
||||
self.dp.include_router(stars_router)
|
||||
|
||||
async def _init_monitor_service(self):
|
||||
try:
|
||||
@@ -229,6 +285,13 @@ class BotApplication:
|
||||
|
||||
async def shutdown(self):
|
||||
logger.info("Shutting down bot...")
|
||||
|
||||
if self.autopay_service:
|
||||
try:
|
||||
await self.autopay_service.stop()
|
||||
logger.info("Autopay service stopped")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping autopay service: {e}")
|
||||
|
||||
if self.monitor_service:
|
||||
try:
|
||||
|
||||
@@ -5,10 +5,18 @@ 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):
|
||||
async def process_referral_rewards(user_id: int, amount: float, payment_id: int, db: Database, bot=None, payment_type: str = None):
|
||||
try:
|
||||
import os
|
||||
|
||||
if payment_type not in ['topup', 'admin_topup']:
|
||||
logger.debug(f"Skipping referral rewards for payment type: {payment_type}")
|
||||
return
|
||||
|
||||
if amount <= 0:
|
||||
logger.debug(f"Skipping referral rewards for non-positive amount: {amount}")
|
||||
return
|
||||
|
||||
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'))
|
||||
@@ -25,7 +33,7 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
|
||||
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}")
|
||||
logger.info(f"Processing referral rewards for topup: 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})")
|
||||
@@ -57,7 +65,7 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
|
||||
referral.referrer_id,
|
||||
f"🎉 Поздравляем! Ваш реферал пополнил баланс на {threshold}₽+\n\n"
|
||||
f"💰 Вам начислено {first_reward}₽ за приведенного друга!\n"
|
||||
f"Теперь вы будете получать {percentage*100:.0f}% с каждого его следующего платежа."
|
||||
f"Теперь вы будете получать {percentage*100:.0f}% с каждого его пополнения баланса."
|
||||
)
|
||||
|
||||
await bot.send_message(
|
||||
@@ -81,7 +89,7 @@ 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:
|
||||
if referral.first_reward_paid:
|
||||
percentage_reward = amount * percentage
|
||||
|
||||
if percentage_reward >= 0.01:
|
||||
@@ -91,7 +99,7 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
|
||||
user_id=referral.referrer_id,
|
||||
amount=percentage_reward,
|
||||
payment_type='referral',
|
||||
description=f'{percentage*100:.0f}% дохода от реферала ID:{user_id}',
|
||||
description=f'{percentage*100:.0f}% от пополнения реферала ID:{user_id}',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
@@ -106,19 +114,19 @@ 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:
|
||||
if bot and percentage_reward >= 1.0:
|
||||
try:
|
||||
await bot.send_message(
|
||||
referral.referrer_id,
|
||||
f"💰 Реферальный доход!\n\n"
|
||||
f"Ваш реферал совершил платеж на {amount:.2f}₽\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:
|
||||
else:
|
||||
logger.info(f"Skipping percentage reward for user {user_id} - first reward not yet paid")
|
||||
|
||||
except Exception as e:
|
||||
@@ -166,7 +174,7 @@ async def create_referral_from_start_param(user_telegram_id: int, start_param: s
|
||||
referrer_id,
|
||||
f"🎉 Отлично! По вашей ссылке зарегистрировался новый пользователь!\n\n"
|
||||
f"Вы получите {first_reward:.0f}₽ после того, как он пополнит баланс на {threshold:.0f}₽.\n"
|
||||
f"И будете получать {percentage*100:.0f}% с каждого его платежа!"
|
||||
f"И будете получать {percentage*100:.0f}% с каждого его пополнения баланса!"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to notify referrer: {e}")
|
||||
|
||||
@@ -19,7 +19,9 @@ class RemnaWaveAPI:
|
||||
headers = {
|
||||
'Authorization': f'Bearer {self.token}',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'X-Forwarded-Proto': 'https',
|
||||
'X-Forwarded-For': '127.0.0.1'
|
||||
}
|
||||
timeout = aiohttp.ClientTimeout(total=30)
|
||||
self.session = aiohttp.ClientSession(
|
||||
@@ -52,7 +54,7 @@ class RemnaWaveAPI:
|
||||
logger.debug(f"HTML Response (first 500 chars): {response_text[:500]}")
|
||||
return None
|
||||
|
||||
if response.status in [200, 201]:
|
||||
if response.status in [200, 201, 204]:
|
||||
if response_text:
|
||||
try:
|
||||
return await response.json()
|
||||
@@ -60,7 +62,7 @@ class RemnaWaveAPI:
|
||||
logger.error(f"Failed to decode JSON from {url}: {e}")
|
||||
logger.debug(f"Raw response: {response_text[:500]}")
|
||||
return None
|
||||
return None
|
||||
return {'success': True} # Для статуса 204 (No Content)
|
||||
elif response.status == 404:
|
||||
logger.warning(f"API 404 for {endpoint}")
|
||||
return None
|
||||
@@ -74,121 +76,115 @@ class RemnaWaveAPI:
|
||||
logger.error(f"Request error for {endpoint}: {e}")
|
||||
return None
|
||||
|
||||
async def get_subscription_info(self, short_uuid: str) -> Optional[Dict]:
|
||||
async def delete_user(self, uuid: str) -> Optional[Dict]:
|
||||
"""
|
||||
Удаляет пользователя по полному UUID
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Getting subscription info for short_uuid: {short_uuid}")
|
||||
logger.info(f"Deleting user with UUID: {uuid}")
|
||||
result = await self._make_request('DELETE', f'/api/users/{uuid}')
|
||||
|
||||
endpoints_to_try = [
|
||||
f'/api/subscriptions/{short_uuid}',
|
||||
f'/api/sub/{short_uuid}',
|
||||
f'/api/subscription/{short_uuid}'
|
||||
]
|
||||
|
||||
for endpoint in endpoints_to_try:
|
||||
logger.debug(f"Trying endpoint: {endpoint}")
|
||||
result = await self._make_request('GET', endpoint)
|
||||
|
||||
if result:
|
||||
logger.info(f"Successfully got subscription info from {endpoint}")
|
||||
|
||||
subscription_data = None
|
||||
|
||||
if 'response' in result:
|
||||
subscription_data = result['response']
|
||||
elif 'data' in result:
|
||||
subscription_data = result['data']
|
||||
elif 'subscription' in result:
|
||||
subscription_data = result['subscription']
|
||||
else:
|
||||
subscription_data = result
|
||||
|
||||
if subscription_data and (
|
||||
'subscriptionUrl' in subscription_data or
|
||||
'url' in subscription_data or
|
||||
'link' in subscription_data
|
||||
):
|
||||
return subscription_data
|
||||
|
||||
logger.warning(f"Could not get subscription info for {short_uuid} from any endpoint")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting subscription info for {short_uuid}: {e}")
|
||||
return None
|
||||
|
||||
async def get_subscription_url(self, short_uuid: str) -> str:
|
||||
try:
|
||||
logger.info(f"Getting subscription URL for short_uuid: {short_uuid}")
|
||||
|
||||
subscription_info = await self.get_subscription_info(short_uuid)
|
||||
|
||||
if subscription_info:
|
||||
subscription_url = (
|
||||
subscription_info.get('subscriptionUrl') or
|
||||
subscription_info.get('url') or
|
||||
subscription_info.get('link') or
|
||||
subscription_info.get('subscription_url')
|
||||
)
|
||||
|
||||
if subscription_url:
|
||||
logger.info(f"Got subscription URL from API: {subscription_url}")
|
||||
return subscription_url
|
||||
|
||||
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']
|
||||
|
||||
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
|
||||
if result is not None:
|
||||
logger.info(f"Successfully deleted user {uuid}")
|
||||
return result
|
||||
else:
|
||||
fallback_url = f"{self.base_url.rstrip('/')}/sub/{short_uuid}"
|
||||
logger.warning(f"Using base_url fallback: {fallback_url}")
|
||||
return fallback_url
|
||||
logger.error(f"Failed to delete user {uuid}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get subscription URL for {short_uuid}: {e}")
|
||||
fallback_url = f"{self.base_url.rstrip('/')}/sub/{short_uuid}"
|
||||
return fallback_url
|
||||
logger.error(f"Error deleting user {uuid}: {e}")
|
||||
return None
|
||||
|
||||
async def get_all_subscriptions_with_urls(self) -> Optional[List]:
|
||||
async def delete_user_by_short_uuid(self, short_uuid: str) -> Optional[Dict]:
|
||||
"""
|
||||
Удаляет пользователя по short_uuid (сначала получает полный UUID, затем удаляет)
|
||||
"""
|
||||
try:
|
||||
logger.info("Fetching all subscriptions with URLs from API")
|
||||
result = await self._make_request('GET', '/api/subscriptions')
|
||||
logger.info(f"Deleting user by short UUID: {short_uuid}")
|
||||
|
||||
if not result:
|
||||
logger.error("Empty response from subscriptions API")
|
||||
return []
|
||||
# Сначала получаем полную информацию о пользователе
|
||||
user_data = await self.get_user_by_short_uuid(short_uuid)
|
||||
|
||||
subscriptions_list = []
|
||||
if not user_data:
|
||||
logger.warning(f"User with short_uuid {short_uuid} not found")
|
||||
return None
|
||||
|
||||
if 'response' in result and 'subscriptions' in result['response']:
|
||||
subscriptions_list = result['response']['subscriptions']
|
||||
elif 'subscriptions' in result:
|
||||
subscriptions_list = result['subscriptions']
|
||||
elif 'data' in result:
|
||||
subscriptions_list = result['data']
|
||||
elif isinstance(result, list):
|
||||
subscriptions_list = result
|
||||
full_uuid = user_data.get('uuid')
|
||||
if not full_uuid:
|
||||
logger.error(f"Could not get full UUID for short_uuid {short_uuid}")
|
||||
return None
|
||||
|
||||
processed_subscriptions = []
|
||||
for subscription in subscriptions_list:
|
||||
if subscription.get('isFound') and 'user' in subscription:
|
||||
user_data = subscription['user']
|
||||
|
||||
if 'subscriptionUrl' not in subscription and user_data.get('shortUuid'):
|
||||
subscription['subscriptionUrl'] = await self.get_subscription_url(user_data['shortUuid'])
|
||||
|
||||
processed_subscriptions.append(subscription)
|
||||
# Удаляем пользователя по полному UUID
|
||||
result = await self.delete_user(full_uuid)
|
||||
|
||||
logger.info(f"Processed {len(processed_subscriptions)} subscriptions with URLs")
|
||||
return processed_subscriptions
|
||||
if result:
|
||||
logger.info(f"Successfully deleted user {short_uuid} (UUID: {full_uuid})")
|
||||
return {
|
||||
'success': True,
|
||||
'short_uuid': short_uuid,
|
||||
'full_uuid': full_uuid,
|
||||
'username': user_data.get('username', 'unknown')
|
||||
}
|
||||
else:
|
||||
logger.error(f"Failed to delete user {short_uuid}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Exception in get_all_subscriptions_with_urls: {e}", exc_info=True)
|
||||
return []
|
||||
logger.error(f"Error deleting user by short UUID {short_uuid}: {e}")
|
||||
return None
|
||||
|
||||
async def bulk_delete_users_by_short_uuids(self, short_uuids: List[str]) -> Dict[str, Any]:
|
||||
"""
|
||||
Массовое удаление пользователей по списку short_uuid
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Bulk deleting {len(short_uuids)} users by short UUIDs")
|
||||
|
||||
results = {
|
||||
'total': len(short_uuids),
|
||||
'success': 0,
|
||||
'failed': 0,
|
||||
'errors': [],
|
||||
'deleted_users': []
|
||||
}
|
||||
|
||||
for short_uuid in short_uuids:
|
||||
try:
|
||||
result = await self.delete_user_by_short_uuid(short_uuid)
|
||||
|
||||
if result and result.get('success'):
|
||||
results['success'] += 1
|
||||
results['deleted_users'].append({
|
||||
'short_uuid': short_uuid,
|
||||
'full_uuid': result.get('full_uuid'),
|
||||
'username': result.get('username')
|
||||
})
|
||||
logger.debug(f"Successfully deleted user {short_uuid}")
|
||||
else:
|
||||
results['failed'] += 1
|
||||
results['errors'].append(f"Failed to delete {short_uuid}")
|
||||
logger.warning(f"Failed to delete user {short_uuid}")
|
||||
|
||||
# Небольшая пауза между запросами, чтобы не перегружать API
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
results['failed'] += 1
|
||||
results['errors'].append(f"Error deleting {short_uuid}: {str(e)}")
|
||||
logger.error(f"Error deleting user {short_uuid}: {e}")
|
||||
|
||||
logger.info(f"Bulk deletion completed: {results['success']} success, {results['failed']} failed")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in bulk_delete_users_by_short_uuids: {e}")
|
||||
return {
|
||||
'total': len(short_uuids),
|
||||
'success': 0,
|
||||
'failed': len(short_uuids),
|
||||
'errors': [f"Critical error: {str(e)}"],
|
||||
'deleted_users': []
|
||||
}
|
||||
|
||||
async def create_user(self, username: str, password: str = None,
|
||||
traffic_limit: int = 0, expiry_time: str = None,
|
||||
@@ -298,7 +294,86 @@ class RemnaWaveAPI:
|
||||
|
||||
return user_data
|
||||
return None
|
||||
|
||||
|
||||
async def get_subscription_info(self, short_uuid: str) -> Optional[Dict]:
|
||||
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}',
|
||||
f'/api/subscription/{short_uuid}'
|
||||
]
|
||||
|
||||
for endpoint in endpoints_to_try:
|
||||
logger.debug(f"Trying endpoint: {endpoint}")
|
||||
result = await self._make_request('GET', endpoint)
|
||||
|
||||
if result:
|
||||
logger.info(f"Successfully got subscription info from {endpoint}")
|
||||
|
||||
subscription_data = None
|
||||
|
||||
if 'response' in result:
|
||||
subscription_data = result['response']
|
||||
elif 'data' in result:
|
||||
subscription_data = result['data']
|
||||
elif 'subscription' in result:
|
||||
subscription_data = result['subscription']
|
||||
else:
|
||||
subscription_data = result
|
||||
|
||||
if subscription_data and (
|
||||
'subscriptionUrl' in subscription_data or
|
||||
'url' in subscription_data or
|
||||
'link' in subscription_data
|
||||
):
|
||||
return subscription_data
|
||||
|
||||
logger.warning(f"Could not get subscription info for {short_uuid} from any endpoint")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting subscription info for {short_uuid}: {e}")
|
||||
return None
|
||||
|
||||
async def get_subscription_url(self, short_uuid: str) -> str:
|
||||
try:
|
||||
logger.info(f"Getting subscription URL for short_uuid: {short_uuid}")
|
||||
|
||||
subscription_info = await self.get_subscription_info(short_uuid)
|
||||
|
||||
if subscription_info:
|
||||
subscription_url = (
|
||||
subscription_info.get('subscriptionUrl') or
|
||||
subscription_info.get('url') or
|
||||
subscription_info.get('link') or
|
||||
subscription_info.get('subscription_url')
|
||||
)
|
||||
|
||||
if subscription_url:
|
||||
logger.info(f"Got subscription URL from API: {subscription_url}")
|
||||
return subscription_url
|
||||
|
||||
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']
|
||||
|
||||
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 = 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}")
|
||||
fallback_url = f"{self.base_url.rstrip('/')}/sub/{short_uuid}"
|
||||
return fallback_url
|
||||
|
||||
async def get_user_by_short_uuid(self, short_uuid: str) -> Optional[Dict]:
|
||||
logger.debug(f"Getting user by short UUID: {short_uuid}")
|
||||
result = await self._make_request('GET', f'/api/users/by-short-uuid/{short_uuid}')
|
||||
@@ -321,6 +396,43 @@ class RemnaWaveAPI:
|
||||
|
||||
return user_data
|
||||
return None
|
||||
|
||||
async def get_all_subscriptions_with_urls(self) -> Optional[List]:
|
||||
try:
|
||||
logger.info("Fetching all subscriptions with URLs from API")
|
||||
result = await self._make_request('GET', '/api/subscriptions')
|
||||
|
||||
if not result:
|
||||
logger.error("Empty response from subscriptions API")
|
||||
return []
|
||||
|
||||
subscriptions_list = []
|
||||
|
||||
if 'response' in result and 'subscriptions' in result['response']:
|
||||
subscriptions_list = result['response']['subscriptions']
|
||||
elif 'subscriptions' in result:
|
||||
subscriptions_list = result['subscriptions']
|
||||
elif 'data' in result:
|
||||
subscriptions_list = result['data']
|
||||
elif isinstance(result, list):
|
||||
subscriptions_list = result
|
||||
|
||||
processed_subscriptions = []
|
||||
for subscription in subscriptions_list:
|
||||
if subscription.get('isFound') and 'user' in subscription:
|
||||
user_data = subscription['user']
|
||||
|
||||
if 'subscriptionUrl' not in subscription and user_data.get('shortUuid'):
|
||||
subscription['subscriptionUrl'] = await self.get_subscription_url(user_data['shortUuid'])
|
||||
|
||||
processed_subscriptions.append(subscription)
|
||||
|
||||
logger.info(f"Processed {len(processed_subscriptions)} subscriptions with URLs")
|
||||
return processed_subscriptions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Exception in get_all_subscriptions_with_urls: {e}", exc_info=True)
|
||||
return []
|
||||
|
||||
async def update_user(self, uuid: str, data: Dict) -> Optional[Dict]:
|
||||
update_data = {'uuid': uuid}
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# === RemnaWave Bot Setup & Management Script ===
|
||||
|
||||
SERVICE_NAME="remnawave-bot"
|
||||
BOT_FILE="main.py"
|
||||
ENV_FILE=".env"
|
||||
VENV_DIR="venv"
|
||||
|
||||
# Select language
|
||||
echo "🌐 Select language / Выберите язык:"
|
||||
echo "1) English"
|
||||
echo "2) Русский"
|
||||
read -p ">>> " LANG_CHOICE
|
||||
|
||||
case $LANG_CHOICE in
|
||||
2) LANG_CODE="ru" ;;
|
||||
*) LANG_CODE="en" ;;
|
||||
esac
|
||||
|
||||
# === Internationalized messages ===
|
||||
msg() {
|
||||
case $1 in
|
||||
check_python) [[ $LANG_CODE == "ru" ]] && echo "🔍 Проверка Python..." || echo "🔍 Checking Python..." ;;
|
||||
python_missing) [[ $LANG_CODE == "ru" ]] && echo "❌ Python 3 не установлен. Установите Python 3.8+." || echo "❌ Python 3 is not installed. Please install Python 3.8+." ;;
|
||||
version) echo "📋 Python version: $PYTHON_VERSION" ;;
|
||||
creating_venv) [[ $LANG_CODE == "ru" ]] && echo "🔧 Создание виртуального окружения..." || echo "🔧 Creating virtual environment..." ;;
|
||||
activating_venv) [[ $LANG_CODE == "ru" ]] && echo "🔧 Активация виртуального окружения..." || echo "🔧 Activating virtual environment..." ;;
|
||||
upgrading_pip) [[ $LANG_CODE == "ru" ]] && echo "📦 Обновление pip..." || echo "📦 Upgrading pip..." ;;
|
||||
installing_requirements) [[ $LANG_CODE == "ru" ]] && echo "📦 Установка зависимостей..." || echo "📦 Installing requirements..." ;;
|
||||
env_missing) [[ $LANG_CODE == "ru" ]] && echo "⚠️ Файл .env не найден!" || echo "⚠️ .env file not found!" ;;
|
||||
env_created) [[ $LANG_CODE == "ru" ]] && echo "📋 Создан .env файл. Пожалуйста, отредактируйте его." || echo "📋 .env created. Please edit it." ;;
|
||||
validating) [[ $LANG_CODE == "ru" ]] && echo "🔍 Проверка конфигурации..." || echo "🔍 Validating configuration..." ;;
|
||||
config_ok) [[ $LANG_CODE == "ru" ]] && echo "✅ Конфигурация прошла проверку." || echo "✅ Configuration validated successfully." ;;
|
||||
creating_service) [[ $LANG_CODE == "ru" ]] && echo "🔧 Создание systemd-сервиса..." || echo "🔧 Creating systemd service..." ;;
|
||||
service_created) [[ $LANG_CODE == "ru" ]] && echo "✅ Сервис создан и включён." || echo "✅ Service created and enabled." ;;
|
||||
start_prompt) [[ $LANG_CODE == "ru" ]] && echo "🚀 Запуск бота..." || echo "🚀 Starting the bot..." ;;
|
||||
already_running) [[ $LANG_CODE == "ru" ]] && echo "🟢 Бот уже запущен!" || echo "🟢 Bot is already running!" ;;
|
||||
last_logs) [[ $LANG_CODE == "ru" ]] && echo "📄 Последние 20 строк лога:" || echo "📄 Last 20 log lines:" ;;
|
||||
action_menu)
|
||||
[[ $LANG_CODE == "ru" ]] && {
|
||||
echo -e "\nЧто вы хотите сделать?"
|
||||
echo "1) Остановить бота"
|
||||
echo "2) Перезапустить бота"
|
||||
echo "3) Выйти"
|
||||
} || {
|
||||
echo -e "\nWhat would you like to do?"
|
||||
echo "1) Stop the bot"
|
||||
echo "2) Restart the bot"
|
||||
echo "3) Exit"
|
||||
}
|
||||
;;
|
||||
restarting) [[ $LANG_CODE == "ru" ]] && echo "🔄 Перезапуск бота..." || echo "🔄 Restarting bot..." ;;
|
||||
completed)
|
||||
echo ""
|
||||
[[ $LANG_CODE == "ru" ]] && {
|
||||
echo "🎉 Установка завершена!"
|
||||
echo "📋 Возможности бота:"
|
||||
echo " ✅ Мультиязычный интерфейс"
|
||||
echo " ✅ Управление балансом и подписками"
|
||||
echo " ✅ Промокоды и поддержка"
|
||||
echo ""
|
||||
} || {
|
||||
echo "🎉 Installation complete!"
|
||||
echo "📋 Bot Features:"
|
||||
echo " ✅ Multi-language support"
|
||||
echo " ✅ Subscription and balance management"
|
||||
echo " ✅ Promocode system and support"
|
||||
echo ""
|
||||
}
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# === Check if bot is running ===
|
||||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
msg already_running
|
||||
msg last_logs
|
||||
sudo journalctl -u "$SERVICE_NAME" -n 20 --no-pager
|
||||
msg action_menu
|
||||
read -p ">>> " choice
|
||||
case $choice in
|
||||
1)
|
||||
[[ $LANG_CODE == "ru" ]] && echo "🛑 Остановка бота..." || echo "🛑 Stopping the bot..."
|
||||
sudo systemctl stop "$SERVICE_NAME"
|
||||
[[ $LANG_CODE == "ru" ]] && echo "✅ Бот остановлен." || echo "✅ Bot stopped."
|
||||
exit 0
|
||||
;;
|
||||
2)
|
||||
msg restarting
|
||||
sudo systemctl restart "$SERVICE_NAME"
|
||||
[[ $LANG_CODE == "ru" ]] && echo "✅ Бот перезапущен." || echo "✅ Bot restarted."
|
||||
exit 0
|
||||
;;
|
||||
3)
|
||||
[[ $LANG_CODE == "ru" ]] && echo "👋 Выход." || echo "👋 Exiting."
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
[[ $LANG_CODE == "ru" ]] && echo "⚠️ Неверный выбор." || echo "⚠️ Invalid choice."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# === Install/Setup ===
|
||||
msg check_python
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
msg python_missing
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
|
||||
msg version
|
||||
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
msg creating_venv
|
||||
python3 -m venv "$VENV_DIR"
|
||||
fi
|
||||
|
||||
msg activating_venv
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
msg upgrading_pip
|
||||
pip install --upgrade pip
|
||||
|
||||
msg installing_requirements
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Create .env if needed
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
msg env_missing
|
||||
cat > "$ENV_FILE" <<EOF
|
||||
# Bot Configuration
|
||||
BOT_TOKEN=
|
||||
|
||||
# RemnaWave API Configuration
|
||||
REMNAWAVE_URL=
|
||||
REMNAWAVE_MODE=
|
||||
REMNAWAVE_TOKEN=
|
||||
|
||||
# Admin Configuration (comma-separated list of Telegram user IDs)
|
||||
ADMIN_IDS=
|
||||
|
||||
# Support Configuration
|
||||
SUPPORT_USERNAME=support
|
||||
|
||||
# Database Configuration (optional, defaults to SQLite)
|
||||
# DATABASE_URL=sqlite+aiosqlite:///bot.db
|
||||
|
||||
# Trial Configuration
|
||||
TRIAL_ENABLED=true
|
||||
TRIAL_DURATION_DAYS=3
|
||||
TRIAL_TRAFFIC_GB=2
|
||||
TRIAL_SQUAD_UUID=
|
||||
TRIAL_PRICE=0.0
|
||||
EOF
|
||||
msg env_created
|
||||
echo ""
|
||||
read -p "🔧 Press Enter after configuring .env / Нажмите Enter после настройки .env..."
|
||||
fi
|
||||
|
||||
# Load env
|
||||
export $(grep -v '^#' "$ENV_FILE" | xargs)
|
||||
|
||||
msg validating
|
||||
|
||||
if [[ -z "$BOT_TOKEN" || "$BOT_TOKEN" == "your_telegram_bot_token_here" ]]; then
|
||||
echo "❌ BOT_TOKEN not set properly."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$REMNAWAVE_URL" || "$REMNAWAVE_URL" == "https://your-panel.com" ]]; then
|
||||
echo "❌ REMNAWAVE_URL not set properly."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$REMNAWAVE_TOKEN" || "$REMNAWAVE_TOKEN" == "your_jwt_token_here" ]]; then
|
||||
echo "❌ REMNAWAVE_TOKEN not set properly."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg config_ok
|
||||
|
||||
# Create systemd service
|
||||
read -p "🔧 Create systemd service? (y/n): " CREATE_SERVICE
|
||||
if [[ "$CREATE_SERVICE" == "y" || "$CREATE_SERVICE" == "Y" ]]; then
|
||||
msg creating_service
|
||||
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
|
||||
CURRENT_DIR=$(pwd)
|
||||
CURRENT_USER=$(whoami)
|
||||
sudo tee "$SERVICE_FILE" > /dev/null <<EOF
|
||||
[Unit]
|
||||
Description=RemnaWave Bot
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$CURRENT_USER
|
||||
WorkingDirectory=$CURRENT_DIR
|
||||
Environment=PATH=$CURRENT_DIR/$VENV_DIR/bin
|
||||
EnvironmentFile=$CURRENT_DIR/.env
|
||||
ExecStart=$CURRENT_DIR/$VENV_DIR/bin/python3 $BOT_FILE
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable "$SERVICE_NAME"
|
||||
msg service_created
|
||||
fi
|
||||
|
||||
msg completed
|
||||
msg start_prompt
|
||||
python3 "$BOT_FILE"
|
||||
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 176 KiB |
@@ -0,0 +1 @@
|
||||
|
||||
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 327 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 111 KiB |
@@ -0,0 +1 @@
|
||||
|
||||
|
After Width: | Height: | Size: 211 KiB |
@@ -0,0 +1,415 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import Message, CallbackQuery, LabeledPrice, PreCheckoutQuery, InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from database import Database, User
|
||||
from keyboards import stars_topup_keyboard, stars_payment_keyboard, balance_keyboard
|
||||
from translations import t
|
||||
from utils import log_user_action
|
||||
from referral_utils import process_referral_rewards
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
stars_router = Router()
|
||||
|
||||
@stars_router.callback_query(F.data == "topup_stars")
|
||||
async def topup_stars_callback(callback: CallbackQuery, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
if not config or not config.STARS_ENABLED:
|
||||
await callback.message.edit_text(
|
||||
"❌ Пополнение через Telegram Stars временно недоступно",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
return
|
||||
|
||||
if not config.STARS_RATES:
|
||||
await callback.message.edit_text(
|
||||
"❌ Курсы звезд не настроены",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
return
|
||||
|
||||
text = "⭐ **Пополнение через Telegram Stars**\n\n"
|
||||
text += "🚀 **Преимущества:**\n"
|
||||
text += "• Мгновенное зачисление\n"
|
||||
text += "• Безопасные платежи через Telegram\n"
|
||||
text += "• Без комиссий и скрытых платежей\n\n"
|
||||
|
||||
text += "💎 **Доступные варианты:**\n"
|
||||
|
||||
sorted_rates = sorted(config.STARS_RATES.items())
|
||||
for stars, rubles in sorted_rates:
|
||||
rate_per_star = rubles / stars
|
||||
|
||||
if stars >= 500:
|
||||
bonus_text = " 🔥 Выгодно!"
|
||||
elif stars >= 250:
|
||||
bonus_text = " 💎 Хорошо!"
|
||||
else:
|
||||
bonus_text = ""
|
||||
|
||||
text += f"• {stars} ⭐ → {rubles:.0f}₽{bonus_text}\n"
|
||||
|
||||
text += f"\n💡 Выберите подходящий вариант:"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
@stars_router.callback_query(F.data.startswith("buy_stars_"))
|
||||
async def buy_stars_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
bot = kwargs.get('bot')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
if not config or not config.STARS_ENABLED or not config.STARS_RATES:
|
||||
await callback.answer("❌ Пополнение недоступно")
|
||||
return
|
||||
|
||||
try:
|
||||
stars_amount = int(callback.data.split("_")[2])
|
||||
|
||||
if stars_amount not in config.STARS_RATES:
|
||||
await callback.answer("❌ Неверное количество звезд")
|
||||
return
|
||||
|
||||
rub_amount = config.STARS_RATES[stars_amount]
|
||||
|
||||
star_payment = await db.create_star_payment(
|
||||
user_id=user.telegram_id,
|
||||
stars_amount=stars_amount,
|
||||
rub_amount=rub_amount
|
||||
)
|
||||
|
||||
prices = [LabeledPrice(label=f"Пополнение на {rub_amount:.0f}₽", amount=stars_amount)]
|
||||
|
||||
try:
|
||||
await callback.answer("💳 Создаю платеж...")
|
||||
|
||||
await bot.send_invoice(
|
||||
chat_id=callback.message.chat.id,
|
||||
title=f"Пополнение баланса",
|
||||
description=f"Пополнение баланса на {rub_amount:.0f}₽ за {stars_amount} ⭐",
|
||||
payload=f"star_payment_{star_payment.id}",
|
||||
currency="XTR",
|
||||
prices=prices
|
||||
)
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
f"💳 **Оплата через Telegram Stars**\n\n"
|
||||
f"⭐ Количество звезд: {stars_amount}\n"
|
||||
f"💰 Сумма пополнения: {rub_amount:.0f}₽\n\n"
|
||||
f"👆 Нажмите кнопку \"Оплатить\" в инвойсе ниже⤵️\n\n"
|
||||
f"Если передумали - нажмите кнопку ниже \n\n❌ Отменить платеж",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отменить платеж", callback_data=f"cancel_star_payment_{star_payment.id}")],
|
||||
[InlineKeyboardButton(text="🔙 Назад к выбору", callback_data="topup_stars")]
|
||||
]),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
await callback.message.answer(
|
||||
f"💳 **Оплата через Telegram Stars**\n\n"
|
||||
f"⭐ Количество звезд: {stars_amount}\n"
|
||||
f"💰 Сумма пополнения: {rub_amount:.0f}₽\n\n"
|
||||
f"👆 Нажмите кнопку \"Оплатить\" в инвойсе ниже⤵️",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отменить платеж", callback_data=f"cancel_star_payment_{star_payment.id}")],
|
||||
[InlineKeyboardButton(text="🔙 Назад к выбору", callback_data="topup_stars")]
|
||||
]),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except TelegramBadRequest as e:
|
||||
logger.error(f"Failed to send invoice: {e}")
|
||||
await callback.answer("❌ Ошибка создания платежа", show_alert=True)
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка создания платежа. Попробуйте позже.",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language)
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
await callback.message.answer(
|
||||
"❌ Ошибка создания платежа. Попробуйте позже.",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language)
|
||||
)
|
||||
|
||||
await db.cancel_star_payment(star_payment.id)
|
||||
|
||||
except (ValueError, IndexError) as e:
|
||||
logger.error(f"Error processing stars amount: {e}")
|
||||
await callback.answer("❌ Ошибка обработки")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in buy_stars_callback: {e}")
|
||||
await callback.answer("❌ Произошла ошибка")
|
||||
|
||||
@stars_router.pre_checkout_query()
|
||||
async def pre_checkout_query_handler(pre_checkout_query: PreCheckoutQuery, db: Database, **kwargs):
|
||||
try:
|
||||
if not pre_checkout_query.invoice_payload.startswith("star_payment_"):
|
||||
await pre_checkout_query.answer(ok=False, error_message="Неверный формат платежа")
|
||||
return
|
||||
|
||||
payment_id = int(pre_checkout_query.invoice_payload.split("_")[2])
|
||||
|
||||
star_payment = await db.get_star_payment_by_id(payment_id)
|
||||
|
||||
if not star_payment:
|
||||
await pre_checkout_query.answer(ok=False, error_message="Платеж не найден")
|
||||
return
|
||||
|
||||
if star_payment.status != 'pending':
|
||||
await pre_checkout_query.answer(ok=False, error_message="Платеж уже обработан")
|
||||
return
|
||||
|
||||
if pre_checkout_query.total_amount != star_payment.stars_amount:
|
||||
await pre_checkout_query.answer(ok=False, error_message="Неверная сумма платежа")
|
||||
return
|
||||
|
||||
await pre_checkout_query.answer(ok=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in pre_checkout_query_handler: {e}")
|
||||
await pre_checkout_query.answer(ok=False, error_message="Внутренняя ошибка")
|
||||
|
||||
@stars_router.message(F.successful_payment)
|
||||
async def successful_payment_handler(message: Message, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
bot = kwargs.get('bot')
|
||||
|
||||
if not user:
|
||||
logger.error("User not found in successful payment handler")
|
||||
return
|
||||
|
||||
try:
|
||||
payment_info = message.successful_payment
|
||||
|
||||
if not payment_info.invoice_payload.startswith("star_payment_"):
|
||||
logger.error(f"Invalid payment payload: {payment_info.invoice_payload}")
|
||||
return
|
||||
|
||||
payment_id = int(payment_info.invoice_payload.split("_")[2])
|
||||
|
||||
success = await db.complete_star_payment(
|
||||
payment_id=payment_id,
|
||||
telegram_payment_charge_id=payment_info.telegram_payment_charge_id
|
||||
)
|
||||
|
||||
if success:
|
||||
star_payment = await db.get_star_payment_by_id(payment_id)
|
||||
|
||||
if star_payment:
|
||||
updated_user = await db.get_user_by_telegram_id(user.telegram_id)
|
||||
|
||||
success_text = "✅ **Платеж успешно обработан!**\n\n"
|
||||
success_text += f"⭐ Оплачено звезд: {star_payment.stars_amount}\n"
|
||||
success_text += f"💰 Зачислено: {star_payment.rub_amount:.0f}₽\n"
|
||||
success_text += f"💳 Текущий баланс: {updated_user.balance:.0f}₽\n\n"
|
||||
success_text += "🎉 Средства уже доступны на вашем балансе!"
|
||||
|
||||
await message.answer(
|
||||
success_text,
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
if bot:
|
||||
try:
|
||||
regular_payment = await db.create_payment(
|
||||
user_id=user.telegram_id,
|
||||
amount=star_payment.rub_amount,
|
||||
payment_type='stars',
|
||||
description=f'Пополнение через Telegram Stars ({star_payment.stars_amount} ⭐)',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
await process_referral_rewards(
|
||||
user.telegram_id,
|
||||
star_payment.rub_amount,
|
||||
regular_payment.id,
|
||||
db,
|
||||
bot,
|
||||
payment_type='stars'
|
||||
)
|
||||
except Exception as ref_error:
|
||||
logger.error(f"Error processing referral rewards for stars payment: {ref_error}")
|
||||
|
||||
log_user_action(user.telegram_id, "stars_payment_completed",
|
||||
f"Stars: {star_payment.stars_amount}, Amount: {star_payment.rub_amount}")
|
||||
else:
|
||||
await message.answer("❌ Ошибка получения данных платежа")
|
||||
else:
|
||||
await message.answer("❌ Ошибка обработки платежа. Обратитесь в поддержку.")
|
||||
logger.error(f"Failed to complete star payment {payment_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in successful_payment_handler: {e}")
|
||||
await message.answer("❌ Произошла ошибка при обработке платежа. Обратитесь в поддержку.")
|
||||
|
||||
def get_stars_rate_info(stars_rates: Dict[int, float], lang: str = 'ru') -> str:
|
||||
if not stars_rates:
|
||||
return "Курсы не настроены"
|
||||
|
||||
text = "⭐ **Курсы Telegram Stars:**\n\n"
|
||||
|
||||
sorted_rates = sorted(stars_rates.items())
|
||||
for stars, rubles in sorted_rates:
|
||||
rate_per_star = rubles / stars
|
||||
text += f"• {stars} ⭐ = {rubles:.0f}₽ (курс: {rate_per_star:.2f}₽ за ⭐)\n"
|
||||
|
||||
return text
|
||||
|
||||
@stars_router.callback_query(F.data.startswith("cancel_star_payment_"))
|
||||
async def cancel_star_payment_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
bot = kwargs.get('bot')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
payment_id = int(callback.data.split("_")[-1])
|
||||
|
||||
star_payment = await db.get_star_payment_by_id(payment_id)
|
||||
|
||||
if not star_payment:
|
||||
await callback.answer("❌ Платеж не найден")
|
||||
return
|
||||
|
||||
if star_payment.user_id != user.telegram_id:
|
||||
await callback.answer("❌ Нет доступа к этому платежу")
|
||||
return
|
||||
|
||||
if star_payment.status != 'pending':
|
||||
await callback.answer("❌ Платеж уже обработан")
|
||||
return
|
||||
|
||||
success = await db.cancel_star_payment(payment_id)
|
||||
|
||||
if success:
|
||||
await callback.answer("✅ Платеж отменен", show_alert=True)
|
||||
|
||||
if bot:
|
||||
invoice_deleted = False
|
||||
try:
|
||||
await bot.delete_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
message_id=callback.message.message_id
|
||||
)
|
||||
invoice_deleted = True
|
||||
logger.info(f"Deleted invoice message {callback.message.message_id} for cancelled payment {payment_id}")
|
||||
except TelegramBadRequest as e:
|
||||
logger.warning(f"Failed to delete invoice message: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error deleting invoice message: {e}")
|
||||
|
||||
if invoice_deleted:
|
||||
try:
|
||||
if config and config.STARS_RATES:
|
||||
text = "❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\n"
|
||||
text += "🚀 **Преимущества:**\n"
|
||||
text += "• Мгновенное зачисление\n"
|
||||
text += "• Безопасные платежи через Telegram\n"
|
||||
text += "• Без комиссий и скрытых платежей\n\n"
|
||||
text += "💎 **Доступные варианты:**\n"
|
||||
|
||||
sorted_rates = sorted(config.STARS_RATES.items())
|
||||
for stars, rubles in sorted_rates:
|
||||
if stars >= 500:
|
||||
bonus_text = " 🔥 Выгодно!"
|
||||
elif stars >= 250:
|
||||
bonus_text = " 💎 Хорошо!"
|
||||
else:
|
||||
bonus_text = ""
|
||||
text += f"• {stars} ⭐ → {rubles:.0f}₽{bonus_text}\n"
|
||||
|
||||
text += f"\n💡 Выберите другой вариант:"
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
text=text,
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
else:
|
||||
await bot.send_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
text="❌ Платеж отменен\n\n💰 Управление балансом:",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send menu after invoice deletion: {e}")
|
||||
else:
|
||||
try:
|
||||
if config and config.STARS_RATES:
|
||||
await callback.message.edit_text(
|
||||
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Платеж отменен",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
try:
|
||||
if config and config.STARS_RATES:
|
||||
await bot.send_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
text="❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
else:
|
||||
await bot.send_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
text="❌ Платеж отменен",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send fallback message: {e}")
|
||||
else:
|
||||
try:
|
||||
if config and config.STARS_RATES:
|
||||
await callback.message.edit_text(
|
||||
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Платеж отменен",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
logger.warning("Failed to edit message when bot instance not available")
|
||||
|
||||
log_user_action(user.telegram_id, "stars_payment_cancelled", f"Payment ID: {payment_id}")
|
||||
else:
|
||||
await callback.answer("❌ Ошибка отмены платежа")
|
||||
|
||||
except (ValueError, IndexError) as e:
|
||||
logger.error(f"Error parsing payment ID from callback: {e}")
|
||||
await callback.answer("❌ Ошибка обработки")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in cancel_star_payment_callback: {e}")
|
||||
await callback.answer("❌ Произошла ошибка")
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Dict, Any
|
||||
from database import Database
|
||||
from remnawave_api import RemnaWaveAPI
|
||||
|
||||
@@ -147,7 +147,192 @@ class SubscriptionMonitorService:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error in daily loop: {e}", exc_info=True)
|
||||
await asyncio.sleep(3600) # Wait 1 hour before retry
|
||||
await asyncio.sleep(3600)
|
||||
|
||||
async def delete_expired_trial_subscriptions(self, force: bool = False) -> Dict[str, Any]:
|
||||
try:
|
||||
logger.info("🗑️ Starting deletion of expired trial subscriptions...")
|
||||
|
||||
now_utc = datetime.utcnow()
|
||||
delete_threshold_days = getattr(self.config, 'DELETE_EXPIRED_TRIAL_DAYS', 1)
|
||||
cutoff_date = now_utc - timedelta(days=delete_threshold_days)
|
||||
|
||||
logger.info(f"🗑️ Deleting trial subscriptions expired before: {cutoff_date} (older than {delete_threshold_days} days)")
|
||||
|
||||
results = {
|
||||
'total_checked': 0,
|
||||
'deleted_from_db': 0,
|
||||
'deleted_from_api': 0,
|
||||
'errors': [],
|
||||
'deleted_subscriptions': []
|
||||
}
|
||||
|
||||
all_users = await self.db.get_all_users()
|
||||
|
||||
for user in all_users:
|
||||
try:
|
||||
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
|
||||
|
||||
for user_sub in user_subs:
|
||||
results['total_checked'] += 1
|
||||
|
||||
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription or not subscription.is_trial:
|
||||
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)
|
||||
|
||||
if expires_at_utc > cutoff_date and not force:
|
||||
continue
|
||||
|
||||
logger.info(f"🗑️ Deleting expired trial subscription '{subscription.name}' for user {user.telegram_id} "
|
||||
f"(expired: {expires_at_utc}, cutoff: {cutoff_date})")
|
||||
|
||||
api_deleted = False
|
||||
if self.api and user_sub.short_uuid:
|
||||
try:
|
||||
api_result = await self.api.delete_user_by_short_uuid(user_sub.short_uuid)
|
||||
if api_result and api_result.get('success'):
|
||||
api_deleted = True
|
||||
results['deleted_from_api'] += 1
|
||||
logger.info(f"✅ Deleted from RemnaWave API: {user_sub.short_uuid}")
|
||||
else:
|
||||
results['errors'].append(f"Failed to delete {user_sub.short_uuid} from API")
|
||||
logger.warning(f"⚠️ Failed to delete {user_sub.short_uuid} from API")
|
||||
except Exception as api_error:
|
||||
results['errors'].append(f"API error for {user_sub.short_uuid}: {str(api_error)}")
|
||||
logger.error(f"❌ API error deleting {user_sub.short_uuid}: {api_error}")
|
||||
|
||||
db_deleted = await self.db.delete_user_subscription(user_sub.id)
|
||||
if db_deleted:
|
||||
results['deleted_from_db'] += 1
|
||||
results['deleted_subscriptions'].append({
|
||||
'user_id': user.telegram_id,
|
||||
'subscription_name': subscription.name,
|
||||
'short_uuid': user_sub.short_uuid,
|
||||
'expired_at': expires_at_utc.isoformat(),
|
||||
'deleted_from_api': api_deleted,
|
||||
'deleted_from_db': True
|
||||
})
|
||||
logger.info(f"✅ Deleted from database: subscription ID {user_sub.id}")
|
||||
else:
|
||||
results['errors'].append(f"Failed to delete subscription ID {user_sub.id} from database")
|
||||
logger.error(f"❌ Failed to delete subscription ID {user_sub.id} from database")
|
||||
|
||||
except Exception as user_error:
|
||||
results['errors'].append(f"Error processing user {user.telegram_id}: {str(user_error)}")
|
||||
logger.error(f"❌ Error processing user {user.telegram_id}: {user_error}")
|
||||
|
||||
logger.info(f"🗑️ Trial deletion completed: {results['deleted_from_db']} from DB, {results['deleted_from_api']} from API")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error in delete_expired_trial_subscriptions: {e}", exc_info=True)
|
||||
return {
|
||||
'total_checked': 0,
|
||||
'deleted_from_db': 0,
|
||||
'deleted_from_api': 0,
|
||||
'errors': [f"Critical error: {str(e)}"],
|
||||
'deleted_subscriptions': []
|
||||
}
|
||||
|
||||
async def delete_expired_regular_subscriptions(self, force: bool = False) -> Dict[str, Any]:
|
||||
try:
|
||||
logger.info("🗑️ Starting deletion of expired regular subscriptions...")
|
||||
|
||||
now_utc = datetime.utcnow()
|
||||
delete_threshold_days = getattr(self.config, 'DELETE_EXPIRED_REGULAR_DAYS', 7)
|
||||
cutoff_date = now_utc - timedelta(days=delete_threshold_days)
|
||||
|
||||
logger.info(f"🗑️ Deleting regular subscriptions expired before: {cutoff_date} (older than {delete_threshold_days} days)")
|
||||
|
||||
results = {
|
||||
'total_checked': 0,
|
||||
'deleted_from_db': 0,
|
||||
'deleted_from_api': 0,
|
||||
'errors': [],
|
||||
'deleted_subscriptions': []
|
||||
}
|
||||
|
||||
all_users = await self.db.get_all_users()
|
||||
|
||||
for user in all_users:
|
||||
try:
|
||||
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
|
||||
|
||||
for user_sub in user_subs:
|
||||
results['total_checked'] += 1
|
||||
|
||||
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription or subscription.is_trial:
|
||||
continue
|
||||
|
||||
if getattr(subscription, 'is_imported', False) or subscription.name == "Старая подписка":
|
||||
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)
|
||||
|
||||
if expires_at_utc > cutoff_date and not force:
|
||||
continue
|
||||
|
||||
logger.info(f"🗑️ Deleting expired regular subscription '{subscription.name}' for user {user.telegram_id} "
|
||||
f"(expired: {expires_at_utc}, cutoff: {cutoff_date})")
|
||||
|
||||
api_deleted = False
|
||||
if self.api and user_sub.short_uuid:
|
||||
try:
|
||||
api_result = await self.api.delete_user_by_short_uuid(user_sub.short_uuid)
|
||||
if api_result and api_result.get('success'):
|
||||
api_deleted = True
|
||||
results['deleted_from_api'] += 1
|
||||
logger.info(f"✅ Deleted from RemnaWave API: {user_sub.short_uuid}")
|
||||
else:
|
||||
results['errors'].append(f"Failed to delete {user_sub.short_uuid} from API")
|
||||
logger.warning(f"⚠️ Failed to delete {user_sub.short_uuid} from API")
|
||||
except Exception as api_error:
|
||||
results['errors'].append(f"API error for {user_sub.short_uuid}: {str(api_error)}")
|
||||
logger.error(f"❌ API error deleting {user_sub.short_uuid}: {api_error}")
|
||||
|
||||
db_deleted = await self.db.delete_user_subscription(user_sub.id)
|
||||
if db_deleted:
|
||||
results['deleted_from_db'] += 1
|
||||
results['deleted_subscriptions'].append({
|
||||
'user_id': user.telegram_id,
|
||||
'subscription_name': subscription.name,
|
||||
'short_uuid': user_sub.short_uuid,
|
||||
'expired_at': expires_at_utc.isoformat(),
|
||||
'deleted_from_api': api_deleted,
|
||||
'deleted_from_db': True
|
||||
})
|
||||
logger.info(f"✅ Deleted from database: subscription ID {user_sub.id}")
|
||||
else:
|
||||
results['errors'].append(f"Failed to delete subscription ID {user_sub.id} from database")
|
||||
logger.error(f"❌ Failed to delete subscription ID {user_sub.id} from database")
|
||||
|
||||
except Exception as user_error:
|
||||
results['errors'].append(f"Error processing user {user.telegram_id}: {str(user_error)}")
|
||||
logger.error(f"❌ Error processing user {user.telegram_id}: {user_error}")
|
||||
|
||||
logger.info(f"🗑️ Regular deletion completed: {results['deleted_from_db']} from DB, {results['deleted_from_api']} from API")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error in delete_expired_regular_subscriptions: {e}", exc_info=True)
|
||||
return {
|
||||
'total_checked': 0,
|
||||
'deleted_from_db': 0,
|
||||
'deleted_from_api': 0,
|
||||
'errors': [f"Critical error: {str(e)}"],
|
||||
'deleted_subscriptions': []
|
||||
}
|
||||
|
||||
async def _check_expiring_subscriptions(self):
|
||||
try:
|
||||
@@ -336,11 +521,25 @@ class SubscriptionMonitorService:
|
||||
deactivated_count = await self.deactivate_expired_subscriptions()
|
||||
logger.info(f"🔄 Deactivated {deactivated_count} expired subscriptions")
|
||||
|
||||
deleted_trials = 0
|
||||
deleted_regular = 0
|
||||
if getattr(self.config, 'AUTO_DELETE_ENABLED', False):
|
||||
logger.info("🗑️ Auto-deletion enabled, deleting expired subscriptions...")
|
||||
|
||||
trial_result = await self.delete_expired_trial_subscriptions()
|
||||
deleted_trials = trial_result.get('deleted_from_db', 0)
|
||||
|
||||
regular_result = await self.delete_expired_regular_subscriptions()
|
||||
deleted_regular = regular_result.get('deleted_from_db', 0)
|
||||
|
||||
logger.info(f"🗑️ Auto-deleted {deleted_trials} trial and {deleted_regular} regular subscriptions")
|
||||
|
||||
logger.info("📩 Sending final expiry notifications...")
|
||||
await self._send_final_expiry_notifications()
|
||||
logger.info("📩 Final notifications sent")
|
||||
|
||||
logger.info(f"✅ Daily check completed successfully. Warnings: {warnings_sent}, Deactivated: {deactivated_count}")
|
||||
logger.info(f"✅ Daily check completed successfully. Warnings: {warnings_sent}, Deactivated: {deactivated_count}, "
|
||||
f"Deleted trials: {deleted_trials}, Deleted regular: {deleted_regular}")
|
||||
return deactivated_count
|
||||
|
||||
except Exception as e:
|
||||
@@ -589,6 +788,9 @@ class SubscriptionMonitorService:
|
||||
'check_interval': self.config.MONITOR_CHECK_INTERVAL,
|
||||
'daily_check_hour': self.config.MONITOR_DAILY_CHECK_HOUR,
|
||||
'warning_days': self.config.MONITOR_WARNING_DAYS,
|
||||
'delete_trial_days': getattr(self.config, 'DELETE_EXPIRED_TRIAL_DAYS', 1),
|
||||
'delete_regular_days': getattr(self.config, 'DELETE_EXPIRED_REGULAR_DAYS', 7),
|
||||
'auto_delete_enabled': getattr(self.config, 'AUTO_DELETE_ENABLED', False),
|
||||
'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,
|
||||
|
||||
@@ -11,9 +11,9 @@ TRANSLATIONS = {
|
||||
'admin_panel': 'Админ панель',
|
||||
'trial_subscription': '🆓 Тестовая подписка',
|
||||
'trial_not_available': '❌ Тестовая подписка недоступна',
|
||||
'trial_success': '🎉 Тестовая подписка успешно активирована!\n\nТеперь вы можете найти её в разделе "Мои подписки".',
|
||||
'trial_success': '🎉 Тестовая подписка успешно активирована!\n\nТеперь вы можете найти её в разделе: \n\n"Мои подписки"',
|
||||
'trial_error': '❌ Ошибка при создании тестовой подписки',
|
||||
'trial_info': '🧪 Тестовая подписка выдается на три дня!\n\nНа тарифе действует ограничение в 3 дня\n\nОграничение по трафику - 2гб',
|
||||
'trial_info': 'Попробуйте наш сервис бесплатно! \n\n🧪 Тестовая подписка выдается на {days} дня!\n\nОграничение по трафику - {traffic} гб',
|
||||
'subscriptions_list': '📋 Список подписок в продаже:',
|
||||
'your_balance': '💰 Ваш баланс: {balance:.2f} руб.',
|
||||
'topup_balance': 'Пополнить баланс',
|
||||
@@ -97,6 +97,48 @@ TRANSLATIONS = {
|
||||
'manage_balance': 'Управление балансом',
|
||||
'manage_promocodes': 'Управление промокодами',
|
||||
'statistics': 'Статистика',
|
||||
'admin_user_subscriptions': 'Подписки пользователей',
|
||||
'all_user_subscriptions': 'Все подписки пользователей',
|
||||
'user_subscription_filters': 'Фильтры подписок',
|
||||
'active_subscriptions': 'Активные подписки',
|
||||
'expired_subscriptions': 'Истекшие подписки',
|
||||
'expiring_subscriptions': 'Истекающие подписки',
|
||||
'autopay_subscriptions': 'Подписки с автоплатежом',
|
||||
'trial_subscriptions': 'Триальные подписки',
|
||||
'imported_subscriptions': 'Импортированные подписки',
|
||||
'subscription_details': 'Детали подписки',
|
||||
'user_info': 'Информация о пользователе',
|
||||
'subscription_info': 'Информация о подписке',
|
||||
'subscription_status': 'Статус подписки',
|
||||
'subscription_dates': 'Временные рамки',
|
||||
'autopay_settings': 'Настройки автоплатежа',
|
||||
'subscription_type': 'Тип подписки',
|
||||
'edit_subscription': 'Редактировать подписку',
|
||||
'toggle_status': 'Изменить статус',
|
||||
'edit_expiry': 'Изменить срок',
|
||||
'edit_autopay': 'Настроить автоплатеж',
|
||||
'subscription_activated': 'Подписка активирована',
|
||||
'subscription_deactivated': 'Подписка деактивирована',
|
||||
'insufficient_funds': 'Недостаточно средств',
|
||||
'funds_needed': 'Нужно еще',
|
||||
'created_date': 'Дата создания',
|
||||
'expires_date': 'Дата истечения',
|
||||
'updated_date': 'Дата обновления',
|
||||
'days_left': 'Дней осталось',
|
||||
'autopay_enabled': 'Автоплатеж включен',
|
||||
'autopay_disabled': 'Автоплатеж отключен',
|
||||
'renew_before_days': 'Продлять за дней',
|
||||
'view_details': 'Просмотр деталей',
|
||||
'back_to_list': 'К списку',
|
||||
'refresh_data': 'Обновить данные',
|
||||
'no_subscriptions_found': 'Подписки не найдены',
|
||||
'subscription_not_found': 'Подписка не найдена',
|
||||
'database_unavailable': 'База данных недоступна',
|
||||
'error_loading_subscriptions': 'Ошибка загрузки подписок',
|
||||
'error_loading_details': 'Ошибка загрузки деталей',
|
||||
'pagination_error': 'Ошибка навигации',
|
||||
'edit_error': 'Ошибка редактирования',
|
||||
'status_toggle_error': 'Ошибка изменения статуса',
|
||||
'not_admin': '❌ У вас нет прав администратора',
|
||||
'create_subscription': 'Создать подписку',
|
||||
'enter_sub_name': 'Введите название подписки:',
|
||||
@@ -294,7 +336,49 @@ TRANSLATIONS = {
|
||||
'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}%'
|
||||
'lucky_game_win_rate': 'Win rate: {rate}%',
|
||||
'admin_user_subscriptions': 'User Subscriptions',
|
||||
'all_user_subscriptions': 'All User Subscriptions',
|
||||
'user_subscription_filters': 'Subscription Filters',
|
||||
'active_subscriptions': 'Active Subscriptions',
|
||||
'expired_subscriptions': 'Expired Subscriptions',
|
||||
'expiring_subscriptions': 'Expiring Subscriptions',
|
||||
'autopay_subscriptions': 'Autopay Subscriptions',
|
||||
'trial_subscriptions': 'Trial Subscriptions',
|
||||
'imported_subscriptions': 'Imported Subscriptions',
|
||||
'subscription_details': 'Subscription Details',
|
||||
'user_info': 'User Information',
|
||||
'subscription_info': 'Subscription Information',
|
||||
'subscription_status': 'Subscription Status',
|
||||
'subscription_dates': 'Time Frames',
|
||||
'autopay_settings': 'Autopay Settings',
|
||||
'subscription_type': 'Subscription Type',
|
||||
'edit_subscription': 'Edit Subscription',
|
||||
'toggle_status': 'Toggle Status',
|
||||
'edit_expiry': 'Edit Expiry',
|
||||
'edit_autopay': 'Configure Autopay',
|
||||
'subscription_activated': 'Subscription activated',
|
||||
'subscription_deactivated': 'Subscription deactivated',
|
||||
'insufficient_funds': 'Insufficient funds',
|
||||
'funds_needed': 'Need more',
|
||||
'created_date': 'Created Date',
|
||||
'expires_date': 'Expiry Date',
|
||||
'updated_date': 'Updated Date',
|
||||
'days_left': 'Days Left',
|
||||
'autopay_enabled': 'Autopay Enabled',
|
||||
'autopay_disabled': 'Autopay Disabled',
|
||||
'renew_before_days': 'Renew Before Days',
|
||||
'view_details': 'View Details',
|
||||
'back_to_list': 'Back to List',
|
||||
'refresh_data': 'Refresh Data',
|
||||
'no_subscriptions_found': 'No subscriptions found',
|
||||
'subscription_not_found': 'Subscription not found',
|
||||
'database_unavailable': 'Database unavailable',
|
||||
'error_loading_subscriptions': 'Error loading subscriptions',
|
||||
'error_loading_details': 'Error loading details',
|
||||
'pagination_error': 'Navigation error',
|
||||
'edit_error': 'Edit error',
|
||||
'status_toggle_error': 'Status toggle error'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ import secrets
|
||||
import string
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Tuple, Optional, Dict, Any
|
||||
from database import Database
|
||||
from database import ReferralProgram, ReferralEarning
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -245,252 +243,6 @@ def format_user_subscription_info(user_sub: Dict[str, Any], subscription: Dict[s
|
||||
def log_user_action(user_id: int, action: str, details: str = ""):
|
||||
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):
|
||||
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_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:
|
||||
await bot.send_message(
|
||||
referrer_id,
|
||||
f"🎉 Отлично! По вашей ссылке зарегистрировался новый пользователь!\n\n"
|
||||
f"Вы получите 150₽ после того, как он пополнит баланс на 300₽.\n"
|
||||
f"И будете получать 25% с каждого его платежа!"
|
||||
)
|
||||
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_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(
|
||||
referrer_id,
|
||||
f"🎉 По вашему промокоду {referral_code} зарегистрировался новый пользователь!\n\n"
|
||||
f"Вы получите 150₽ после того, как он пополнит баланс на 300₽."
|
||||
)
|
||||
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}")
|
||||
|
||||
def bytes_to_gb(bytes_value: int) -> float:
|
||||
if not bytes_value or bytes_value == 0:
|
||||
return 0.0
|
||||
|
||||