Compare commits

...

79 Commits

Author SHA1 Message Date
Fr1ngg edc891c93d Update README.md 2025-08-11 22:06:49 +03:00
Fr1ngg 2d706a4be0 Update keyboards.py 2025-08-11 22:01:38 +03:00
Fr1ngg 32cb81ef34 Update handlers.py 2025-08-11 22:01:09 +03:00
Fr1ngg 88aa3c4a3d Update database.py
autopay mod
2025-08-11 22:00:43 +03:00
Fr1ngg e813d3ccb7 Create autopay_service.py 2025-08-11 22:00:19 +03:00
Fr1ngg a24d7a4e24 Update main.py 2025-08-11 21:59:51 +03:00
Fr1ngg 39987b9717 Update admin_handlers.py
autopay mod
2025-08-11 21:59:31 +03:00
Fr1ngg c77a1ea4f8 Update README.md 2025-08-11 17:57:35 +03:00
Fr1ngg 64b5d90d66 Update README.md 2025-08-11 03:00:54 +03:00
Fr1ngg 5a1fb32e1d Update admin_handlers.py 2025-08-11 02:58:19 +03:00
Fr1ngg c4d8108e8c Update config.py 2025-08-11 02:53:37 +03:00
Fr1ngg 1350d80083 Update database.py 2025-08-11 02:51:28 +03:00
Fr1ngg e0cc218007 Update handlers.py 2025-08-11 02:49:06 +03:00
Fr1ngg bcaf8f2247 Update keyboards.py 2025-08-11 02:48:36 +03:00
Fr1ngg d1190ed9dc Update stars_handlers.py 2025-08-11 02:47:05 +03:00
Fr1ngg 513d6037d4 Update subscription_monitor.py 2025-08-11 02:46:40 +03:00
Fr1ngg e86012b2ab Update translations.py 2025-08-11 02:46:11 +03:00
Fr1ngg 214e381dc5 Update README.md 2025-08-10 10:41:22 +03:00
Fr1ngg aa79cb670b Update .env.example 2025-08-10 10:30:33 +03:00
Fr1ngg f0106e15fa Create stars_handlers.py 2025-08-10 10:26:28 +03:00
Fr1ngg 1d194c6be2 Update .env.example 2025-08-10 10:18:03 +03:00
Fr1ngg 6917890007 Update admin_handlers.py 2025-08-10 10:15:34 +03:00
Fr1ngg 1c69de7ecd Update config.py 2025-08-10 10:13:14 +03:00
Fr1ngg 62188793d9 Update database.py 2025-08-10 10:12:21 +03:00
Fr1ngg 585ee2d42b Update handlers.py 2025-08-10 10:11:12 +03:00
Fr1ngg 2a58cfd45c Update keyboards.py 2025-08-10 10:10:54 +03:00
Fr1ngg 93003c5fe0 Update main.py 2025-08-10 10:10:27 +03:00
Fr1ngg 91b0787d72 Update README.md 2025-08-10 07:04:17 +03:00
Fr1ngg 315277d4b0 Create bedolaga.sh 2025-08-10 06:56:04 +03:00
Fr1ngg 1821c0d89d Delete run.sh 2025-08-10 06:54:40 +03:00
Fr1ngg 5650da0c5f Update README.md 2025-08-10 03:56:42 +03:00
Fr1ngg 0ca8b73cef Update .env.example 2025-08-10 03:51:10 +03:00
Fr1ngg 6bab26ab27 Update lucky_game.py 2025-08-10 03:44:57 +03:00
Fr1ngg a72243e7fa Update keyboards.py 2025-08-10 03:42:56 +03:00
Fr1ngg 5bd86d9cf7 Update config.py 2025-08-10 03:40:36 +03:00
Fr1ngg 00fd3fbadb Update database.py 2025-08-10 03:39:51 +03:00
Fr1ngg b806f40797 Update middlewares.py 2025-08-10 03:38:20 +03:00
Fr1ngg c82aa1d253 Update main.py 2025-08-10 03:37:51 +03:00
Fr1ngg 071919f0db Update remnawave_api.py 2025-08-10 03:36:51 +03:00
Fr1ngg ab41e96ba5 Update subscription_monitor.py 2025-08-10 03:34:52 +03:00
Fr1ngg 95a470ba7b Update admin_handlers.py 2025-08-10 03:32:41 +03:00
Fr1ngg 1e0653ed33 Update referral_utils.py 2025-08-10 03:27:15 +03:00
Fr1ngg 3f0df08031 Update handlers.py 2025-08-10 03:25:53 +03:00
Fr1ngg 8c3bc8a5ba Update utils.py 2025-08-10 03:22:42 +03:00
Fr1ngg 3a5a8f48e4 Update README.md 2025-08-09 11:31:02 +03:00
Fr1ngg f4b99ffb09 Rename logo.svg to logo2.svg 2025-08-09 11:30:57 +03:00
Fr1ngg 0c6afdebc3 Update README.md 2025-08-09 11:30:17 +03:00
Fr1ngg 98b382ba2f Update logo.svg 2025-08-09 11:29:57 +03:00
Fr1ngg 54a24385b1 Update README.md 2025-08-09 08:54:57 +03:00
Fr1ngg 3eeb84cdae Update README.md 2025-08-09 08:29:02 +03:00
Fr1ngg 204a10ae14 Update README.md 2025-08-09 08:26:02 +03:00
Fr1ngg ba1dd7547e Create logo.svg 2025-08-09 08:24:25 +03:00
Fr1ngg 238585c257 Update .gitignore 2025-08-09 08:24:02 +03:00
Fr1ngg d8ebe1ad13 Add files via upload 2025-08-09 08:18:01 +03:00
Fr1ngg 819df3e93a Create 1 2025-08-09 08:13:36 +03:00
Fr1ngg 99e46feafa Delete screens/sub/1 2025-08-09 08:07:05 +03:00
Fr1ngg a432c8ab54 Add files via upload 2025-08-09 08:06:52 +03:00
Fr1ngg d740b7560c Create 1 2025-08-09 08:06:17 +03:00
Fr1ngg 91c4c73d7e Delete screens/Balance/1 2025-08-09 08:05:33 +03:00
Fr1ngg 4ce2384f27 Update handlers.py 2025-08-09 08:04:43 +03:00
Fr1ngg 02a6289d56 Add files via upload 2025-08-09 07:57:18 +03:00
Fr1ngg 52ae6eba81 Create 1 2025-08-09 07:55:29 +03:00
Fr1ngg 080b6e74a2 Add files via upload 2025-08-09 07:54:08 +03:00
Fr1ngg ad7d32023c Delete screens/Luckgame/1 2025-08-09 07:53:17 +03:00
Fr1ngg 1baf8fbe37 Add files via upload 2025-08-09 07:53:03 +03:00
Fr1ngg 758ca6a373 Create 1 2025-08-09 07:52:38 +03:00
Fr1ngg 65504d0a23 Delete screens/1 2025-08-09 07:50:15 +03:00
Fr1ngg 0409f4e2e2 Delete screens/Рефералы/1 2025-08-09 07:49:57 +03:00
Fr1ngg 8ca63e939a Add files via upload 2025-08-09 07:49:38 +03:00
Fr1ngg 25a621fd51 Delete screens/Главное меню/1 2025-08-09 07:43:58 +03:00
Fr1ngg 3f71a8aff7 Rename Снимок экрана 2025-08-09 в 07.42.12.png to Mainmenu.png 2025-08-09 07:43:45 +03:00
Fr1ngg 6ec81c37b9 Add files via upload 2025-08-09 07:42:41 +03:00
Fr1ngg 8a9ff98da8 Create 1 2025-08-09 07:40:31 +03:00
Fr1ngg bb7e29eeac Create 1 2025-08-09 07:40:05 +03:00
Fr1ngg 27d8783104 Create 1 2025-08-09 07:39:34 +03:00
Fr1ngg 9a01e6db43 Create .gitignore 2025-08-09 07:37:50 +03:00
Fr1ngg b341ddf9d9 Update README.md 2025-08-09 07:33:26 +03:00
Fr1ngg 3c8d0ac859 Update README.md 2025-08-09 07:33:03 +03:00
Fr1ngg 3f529c0dee Update README.md 2025-08-09 07:19:13 +03:00
50 changed files with 5743 additions and 905 deletions
+33 -8
View File
@@ -19,18 +19,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
+2
View File
@@ -0,0 +1,2 @@
/screens
/assets
+189 -8
View File
@@ -5,12 +5,14 @@
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-blue?logo=postgresql)](https://postgresql.org)
[![License](https://img.shields.io/badge/License-MIT-green)](LICENSE)
![RemnaWave Bedolaga Bot Logo](./assets/logo2.svg)
Современный Telegram бот для управления VPN подписками через RemnaWave API с полным функционалом управления пользователями, платежами и администрированием.
## 🚀 Особенности
### 👤 Для пользователей
- 💰 **Управление балансом** - пополнение через поддержку
- 💰 **Управление балансом** - Пополнение через Telegram Stars + пополнение через поддержку p2p
- 🛒 **Покупка подписок** - различные тарифные планы c возможностью индивидуальной настройки сквада у плана
- 📱 **Управление подписками** - просмотр, продление, получение ссылок
- 🎁 **Система промокодов** - денежные коды
@@ -18,6 +20,8 @@
- 🎰 **Игра удачи** - выигрывай бонусы каждые 24 часа
- 🆓 **Тестовая подписка** - бесплатная пробная версия с детальной конфигурацией
- 🌐 **Мультиязычность** - русский и английский
- 📋 **Правила сервиса**
- ♾️ **Автопродление** - С настройкой вкл/выкл и кол-вом дней до автопродления.
### ⚙️ Для администраторов
- 📊 **Полная статистика** - пользователи, платежи, подписки
@@ -27,7 +31,9 @@
- 🖥 **Мониторинг системы** - состояние нод, пользователей RemnaWave
- 🔄 **Синхронизация данных** - импорт из RemnaWave
- 📨 **Рассылки** - уведомления пользователям
- 🔍 **Мониторинг подписок** - автоматические уведомления об истечении
- 🔍 **Мониторинг подписок** - автоматические уведомления об истечении и зачистка подписок с истекщим сроком для триал и обычных подписок
- 📋 **Правила сервиса** - Настройка страниц из админ панели
- ♾️ **Автопродление** - Статус сервиса, статистика автопродления, принудительное продление
## 📋 Требования
@@ -46,8 +52,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. Настройка переменных окружения
@@ -68,6 +74,7 @@ nano .env
| `BOT_TOKEN` | Токен Telegram бота | `123456:ABC-DEF1234ghIkl-zyx` |
| `BOT_USERNAME` | Username бота (без @) | `your_bot_username` |
| `REMNAWAVE_URL` | URL панели RemnaWave | `https://panel.example.com` |
| `REMNAWAVE_MODE` | Тип подключения | `remote/local` |
| `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,11 +100,179 @@ nano .env
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `TRIAL_ENABLED` | Включить тестовую подписку | `true` |
| `TRIAL_ENABLED` | Включить тестовую подписку | `true/false` |
| `TRIAL_DURATION_DAYS` | Длительность (дни) | `3` |
| `TRIAL_TRAFFIC_GB` | Лимит трафика (ГБ) | `2` |
| `TRIAL_SQUAD_UUID` | UUID squad для тестовых | `uuid-here` |
#### 🌟 Оплата за звезды
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `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
Автоматический установщик для 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 файл
- 🩺 Диагностика базы данных
- 🗑️ Удалить базу данных
- ❌ Полностью удалить бота
##### Когда бот остановлен:
- ▶️ Запустить бот
- ⬆️ Обновить бот
- 📋 Посмотреть логи
- 💾 Создать резервную копию БД
- ♻️ Восстановить базу данных
- ✏️ Редактировать .env файл
- 🩺 Диагностика базы данных
- 🗑️ Удалить базы данных
- ❌ Полностью удалить бота
#### 🔧 Ручное управление через командную строку
```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 конфигурация
#### 🚀 Минимальная конфигурация (рекомендуется)
@@ -506,14 +681,20 @@ 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.4 | Синхронизация 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 | Вывод правил сервиса в боте с конфигурацией через админку |
### 🚀 Хотите помочь?
+1402 -40
View File
File diff suppressed because it is too large Load Diff
+295
View File
@@ -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

+284
View File
@@ -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
}
+965
View File
@@ -0,0 +1,965 @@
#!/bin/bash
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
BOT_DIR="/opt/bedolaga-bot"
COMPOSE_FILE="$BOT_DIR/docker-compose.yml"
ENV_FILE="$BOT_DIR/.env"
SERVICE_FILE="/etc/systemd/system/bedolaga-bot.service"
# Функция для логирования
log() {
echo -e "${GREEN}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Проверка является ли пользователь root
check_root() {
if [ "$EUID" -ne 0 ]; then
error "Этот скрипт должен быть запущен от имени root (используйте sudo)"
exit 1
fi
}
# Проверка установки бота
check_installation() {
if [ -d "$BOT_DIR" ] && [ -f "$COMPOSE_FILE" ] && [ -f "$ENV_FILE" ]; then
return 0
else
return 1
fi
}
# Обновление системы
update_system() {
log "Обновление системы Ubuntu..."
apt update && apt upgrade -y
log "Система обновлена успешно"
}
# Установка Docker
install_docker() {
log "Установка Docker..."
# Удаление старых версий
apt remove -y docker docker-engine docker.io containerd runc
# Установка зависимостей
apt install -y apt-transport-https ca-certificates curl gnupg lsb-release nano
# Добавление GPG ключа Docker
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Добавление репозитория
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
# Установка Docker
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Запуск Docker
systemctl enable docker
systemctl start docker
log "Docker установлен успешно"
}
# Проверка и установка nano
ensure_nano() {
if ! command -v nano &> /dev/null; then
log "Установка nano..."
apt update
apt install -y nano
log "Nano установлен успешно"
fi
}
# Создание папки и структуры проекта
create_project_structure() {
log "Создание структуры проекта..."
mkdir -p "$BOT_DIR"
mkdir -p "$BOT_DIR/logs"
mkdir -p "$BOT_DIR/data"
log "Структура проекта создана в $BOT_DIR"
}
# Создание docker-compose.yml
create_docker_compose() {
log "Создание docker-compose.yml..."
echo "Выберите конфигурацию Docker Compose:"
echo "1) Минимальная (рекомендуется) - только PostgreSQL и бот"
echo "2) Расширенная - с Redis и Nginx"
while true; do
read -p "Ваш выбор (1-2): " choice
case $choice in
1)
create_minimal_compose
break
;;
2)
create_full_compose
break
;;
*)
error "Неверный выбор. Попробуйте снова."
;;
esac
done
}
create_minimal_compose() {
cat > "$COMPOSE_FILE" << 'EOF'
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: remnawave_bot_db
restart: unless-stopped
environment:
POSTGRES_DB: remnawave_bot
POSTGRES_USER: remnawave_user
POSTGRES_PASSWORD: secure_password_123
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U remnawave_user -d remnawave_bot"]
interval: 15s
timeout: 10s
retries: 5
start_period: 30s
# RemnaWave Bot
bot:
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
container_name: remnawave_bot
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
env_file:
- .env
environment:
DATABASE_URL: postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
volumes:
- ./logs:/app/logs
- ./data:/app/data
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "python -c 'print(\"Bot is running\")'"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
postgres_data:
driver: local
networks:
bot_network:
driver: bridge
EOF
log "Минимальная конфигурация Docker Compose создана"
}
create_full_compose() {
cat > "$COMPOSE_FILE" << 'EOF'
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: remnawave_bot_db
restart: unless-stopped
environment:
POSTGRES_DB: remnawave_bot
POSTGRES_USER: remnawave_user
POSTGRES_PASSWORD: secure_password_123
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U remnawave_user -d remnawave_bot"]
interval: 15s
timeout: 10s
retries: 5
start_period: 30s
# RemnaWave Bot
bot:
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
container_name: remnawave_bot
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
env_file:
- .env
environment:
DATABASE_URL: postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
volumes:
- ./logs:/app/logs
- ./data:/app/data
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "python -c 'print(\"Bot is running\")'"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
# Redis (для кэширования и улучшения производительности)
redis:
image: redis:7-alpine
container_name: remnawave_bot_redis
restart: unless-stopped
command: redis-server --appendonly yes --requirepass redis_password_123
volumes:
- redis_data:/data
ports:
- "6379:6379"
networks:
- bot_network
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 10s
timeout: 3s
retries: 5
profiles:
- with-redis
# Nginx (для статических файлов или веб-интерфейса)
nginx:
image: nginx:alpine
container_name: remnawave_bot_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
- ./static:/usr/share/nginx/html:ro
networks:
- bot_network
depends_on:
- bot
profiles:
- with-nginx
volumes:
postgres_data:
driver: local
redis_data:
driver: local
networks:
bot_network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
EOF
log "Расширенная конфигурация Docker Compose создана"
}
# Создание .env файла
create_env_file() {
log "Настройка .env файла..."
# Основные настройки бота
read -p "Введите BOT_TOKEN: " BOT_TOKEN
read -p "Введите BOT_USERNAME (без @): " BOT_USERNAME
read -p "Введите ADMIN_IDS (через запятую): " ADMIN_IDS
# Настройки RemnaWave
echo "Выберите режим RemnaWave:"
echo "1) remote"
echo "2) local"
while true; do
read -p "Ваш выбор (1-2): " remna_choice
case $remna_choice in
1)
REMNAWAVE_MODE="remote"
break
;;
2)
REMNAWAVE_MODE="local"
break
;;
*)
error "Неверный выбор. Попробуйте снова."
;;
esac
done
read -p "Введите REMNAWAVE_URL: " REMNAWAVE_URL
read -p "Введите REMNAWAVE_TOKEN: " REMNAWAVE_TOKEN
# Настройки триала
while true; do
read -p "Включить триал? (y/n): " trial_enabled
case $trial_enabled in
[Yy]*)
TRIAL_ENABLED="true"
read -p "Введите TRIAL_DURATION_DAYS: " TRIAL_DURATION_DAYS
read -p "Введите TRIAL_TRAFFIC_GB: " TRIAL_TRAFFIC_GB
read -p "Введите TRIAL_SQUAD_UUID: " TRIAL_SQUAD_UUID
break
;;
[Nn]*)
TRIAL_ENABLED="false"
TRIAL_DURATION_DAYS=""
TRIAL_TRAFFIC_GB=""
TRIAL_SQUAD_UUID=""
break
;;
*)
error "Пожалуйста, ответьте y или n."
;;
esac
done
# Настройки реферальной системы
read -p "Введите REFERRAL_FIRST_REWARD (сумму с .0 на конце): " REFERRAL_FIRST_REWARD
read -p "Введите REFERRAL_REFERRED_BONUS (сумму с .0 на конце): " REFERRAL_REFERRED_BONUS
read -p "Введите REFERRAL_THRESHOLD (сумму с .0 на конце): " REFERRAL_THRESHOLD
read -p "Введите REFERRAL_PERCENTAGE (с 0. в начале): " REFERRAL_PERCENTAGE
# Настройки мониторинга
read -p "Введите DELETE_EXPIRED_TRIAL_DAYS: " DELETE_EXPIRED_TRIAL_DAYS
read -p "Введите DELETE_EXPIRED_REGULAR_DAYS: " DELETE_EXPIRED_REGULAR_DAYS
# Создание .env файла
cat > "$ENV_FILE" << EOF
BOT_TOKEN=$BOT_TOKEN
BOT_USERNAME=$BOT_USERNAME
ADMIN_IDS=$ADMIN_IDS
REMNAWAVE_MODE=$REMNAWAVE_MODE
REMNAWAVE_URL=$REMNAWAVE_URL
REMNAWAVE_TOKEN=$REMNAWAVE_TOKEN
TRIAL_ENABLED=$TRIAL_ENABLED
EOF
if [ "$TRIAL_ENABLED" = "true" ]; then
cat >> "$ENV_FILE" << EOF
TRIAL_DURATION_DAYS=$TRIAL_DURATION_DAYS
TRIAL_TRAFFIC_GB=$TRIAL_TRAFFIC_GB
TRIAL_SQUAD_UUID=$TRIAL_SQUAD_UUID
EOF
fi
cat >> "$ENV_FILE" << EOF
TRIAL_PRICE=0.0
REFERRAL_FIRST_REWARD=$REFERRAL_FIRST_REWARD
REFERRAL_REFERRED_BONUS=$REFERRAL_REFERRED_BONUS
REFERRAL_THRESHOLD=$REFERRAL_THRESHOLD
REFERRAL_PERCENTAGE=$REFERRAL_PERCENTAGE
# Monitor Service Settings
MONITOR_ENABLED=true
MONITOR_CHECK_INTERVAL=21600
MONITOR_DAILY_CHECK_HOUR=12
MONITOR_WARNING_DAYS=2
DELETE_EXPIRED_TRIAL_DAYS=$DELETE_EXPIRED_TRIAL_DAYS
DELETE_EXPIRED_REGULAR_DAYS=$DELETE_EXPIRED_REGULAR_DAYS
AUTO_DELETE_ENABLED=true
LUCKY_GAME_ENABLED=true
LUCKY_GAME_REWARD=50.0
LUCKY_GAME_NUMBERS=30
LUCKY_GAME_WINNING_COUNT=5
EOF
log ".env файл создан успешно"
}
# Создание службы systemd
create_service() {
while true; do
read -p "Создать службу для запуска бота? (y/n): " create_service_choice
case $create_service_choice in
[Yy]*)
log "Создание службы systemd..."
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=RemnaWave Bedolaga Bot
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=$BOT_DIR
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable bedolaga-bot.service
log "Служба bedolaga-bot создана и включена"
break
;;
[Nn]*)
log "Служба не будет создана"
break
;;
*)
error "Пожалуйста, ответьте y или n."
;;
esac
done
}
# Проверка статуса бота
check_bot_status() {
if docker compose -f "$COMPOSE_FILE" ps --services --filter "status=running" | grep -q "bot"; then
return 0 # Запущен
else
return 1 # Не запущен
fi
}
# Проверка подключения к RemnaWave API
check_remnawave_connection() {
if [ -f "$ENV_FILE" ]; then
source "$ENV_FILE"
if [ ! -z "$REMNAWAVE_URL" ]; then
if curl -s --connect-timeout 5 "$REMNAWAVE_URL/api/auth/status" > /dev/null; then
return 0 # Подключен
else
return 1 # Не подключен
fi
else
return 1
fi
else
return 1
fi
}
# Запуск бота
start_bot() {
log "Запуск бота..."
cd "$BOT_DIR"
docker compose up -d
log "Бот запущен"
}
# Остановка бота
stop_bot() {
log "Остановка бота..."
cd "$BOT_DIR"
docker compose down
log "Бот остановлен"
}
# Перезапуск бота
restart_bot() {
log "Перезапуск бота..."
cd "$BOT_DIR"
docker compose restart
log "Бот перезапущен"
}
# Обновление бота
update_bot() {
log "Обновление бота..."
cd "$BOT_DIR"
docker compose down
docker compose pull bot
docker compose up -d
log "Бот обновлен"
}
# Просмотр логов
view_logs() {
cd "$BOT_DIR"
docker compose logs bot
}
# Просмотр логов в реальном времени
view_live_logs() {
cd "$BOT_DIR"
docker compose logs -f bot
}
# Создание резервной копии базы данных
backup_database() {
log "Создание резервной копии базы данных..."
cd "$BOT_DIR"
# Проверяем, запущен ли контейнер postgres
if ! docker compose ps postgres | grep -q "Up"; then
log "Контейнер PostgreSQL не запущен. Запускаем PostgreSQL..."
docker compose up -d postgres
# Ждем готовности базы данных
log "Ожидание готовности базы данных..."
for i in {1..30}; do
if docker compose exec postgres pg_isready -U remnawave_user -d remnawave_bot &>/dev/null; then
log "PostgreSQL готов к работе"
break
fi
if [ $i -eq 30 ]; then
error "PostgreSQL не запустился в течение 60 секунд"
return 1
fi
sleep 2
echo -n "."
done
echo ""
fi
# Проверяем доступность базы данных
log "Проверка подключения к базе данных..."
if ! docker compose exec postgres pg_isready -U remnawave_user -d remnawave_bot &>/dev/null; then
error "База данных недоступна. Проверьте логи: docker compose logs postgres"
return 1
fi
BACKUP_FILE="$BOT_DIR/backup_$(date +%Y%m%d_%H%M%S).sql"
log "Создание дампа базы данных..."
# Используем docker compose exec без -T и с правильным выводом
if docker compose exec postgres pg_dump -U remnawave_user -d remnawave_bot --verbose --no-owner --no-privileges > "$BACKUP_FILE" 2>/dev/null; then
# Проверяем размер созданного файла
if [ -s "$BACKUP_FILE" ]; then
FILE_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
log "Резервная копия создана успешно: $(basename "$BACKUP_FILE")"
echo -e "${GREEN}Размер файла: $FILE_SIZE${NC}"
echo -e "${GREEN}Путь: $BACKUP_FILE${NC}"
# Показываем краткую информацию о содержимом
LINES_COUNT=$(wc -l < "$BACKUP_FILE")
echo -e "${BLUE}Количество строк в дампе: $LINES_COUNT${NC}"
else
error "Резервная копия создана, но файл пустой. Возможные причины:"
echo " - База данных пуста (бот еще не создал таблицы)"
echo " - Нет прав доступа к базе данных"
echo " - Неправильные параметры подключения"
rm -f "$BACKUP_FILE"
return 1
fi
else
error "Ошибка при создании резервной копии"
# Показываем подробную информацию об ошибке
echo "Попытка диагностики проблемы..."
docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "\dt" 2>&1 || {
echo "Не удается подключиться к базе данных."
echo "Проверьте логи контейнера: docker compose logs postgres"
}
rm -f "$BACKUP_FILE" 2>/dev/null
return 1
fi
}
# Восстановление базы данных
restore_database() {
log "Восстановление базы данных из резервной копии"
# Поиск файлов резервных копий
BACKUP_FILES=($(find "$BOT_DIR" -name "backup_*.sql" -type f 2>/dev/null))
if [ ${#BACKUP_FILES[@]} -eq 0 ]; then
error "Файлы резервных копий не найдены в $BOT_DIR"
echo "Поместите файл резервной копии (.sql) в папку $BOT_DIR"
return 1
fi
echo -e "${YELLOW}Найденные резервные копии:${NC}"
for i in "${!BACKUP_FILES[@]}"; do
BACKUP_FILE="${BACKUP_FILES[$i]}"
FILE_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
FILE_DATE=$(basename "$BACKUP_FILE" | sed 's/backup_\([0-9]\{8\}_[0-9]\{6\}\)\.sql/\1/' | sed 's/_/ /')
echo "$((i+1))) $(basename "$BACKUP_FILE") - Размер: $FILE_SIZE - Дата: $FILE_DATE"
done
echo "$((${#BACKUP_FILES[@]}+1))) Указать путь к другому файлу"
echo "0) Отмена"
while true; do
read -p "Выберите файл для восстановления: " choice
if [ "$choice" = "0" ]; then
log "Операция отменена"
return 0
elif [ "$choice" = "$((${#BACKUP_FILES[@]}+1))" ]; then
read -p "Введите полный путь к файлу резервной копии: " SELECTED_BACKUP
if [ ! -f "$SELECTED_BACKUP" ]; then
error "Файл не найден: $SELECTED_BACKUP"
continue
fi
break
elif [ "$choice" -ge 1 ] && [ "$choice" -le "${#BACKUP_FILES[@]}" ]; then
SELECTED_BACKUP="${BACKUP_FILES[$((choice-1))]}"
break
else
error "Неверный выбор. Попробуйте снова."
fi
done
warn "ВНИМАНИЕ! Это действие перезапишет текущую базу данных!"
warn "Убедитесь, что у вас есть резервная копия текущих данных!"
read -p "Продолжить восстановление? Введите 'YES' для подтверждения: " confirm
if [ "$confirm" != "YES" ]; then
log "Операция отменена"
return 0
fi
cd "$BOT_DIR"
# Проверяем, запущен ли контейнер postgres
if ! docker compose ps postgres | grep -q "Up"; then
log "Запуск контейнера PostgreSQL..."
docker compose up -d postgres
# Ждем готовности базы данных
log "Ожидание готовности базы данных..."
for i in {1..30}; do
if docker compose exec postgres pg_isready -U remnawave_user -d remnawave_bot &>/dev/null; then
break
fi
sleep 2
echo -n "."
done
echo ""
fi
log "Очистка текущей базы данных..."
if docker compose exec -T postgres psql -U remnawave_user -d remnawave_bot -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" &>/dev/null; then
log "База данных очищена"
else
error "Ошибка при очистке базы данных"
return 1
fi
log "Восстановление данных из файла: $(basename "$SELECTED_BACKUP")"
if docker compose exec -T postgres psql -U remnawave_user -d remnawave_bot < "$SELECTED_BACKUP"; then
log "База данных успешно восстановлена!"
log "Перезапуск бота для применения изменений..."
docker compose restart bot
log "Восстановление завершено успешно"
else
error "Ошибка при восстановлении базы данных"
return 1
fi
}
# Диагностика базы данных
diagnose_database() {
log "Диагностика состояния базы данных..."
cd "$BOT_DIR"
# Проверяем состояние контейнера
echo -e "${YELLOW}Состояние контейнеров:${NC}"
docker compose ps
echo ""
# Проверяем, запущен ли PostgreSQL, если нет - запускаем
if ! docker compose ps postgres | grep -q "Up"; then
log "PostgreSQL не запущен. Запускаем..."
docker compose up -d postgres
# Ждем готовности базы данных
log "Ожидание готовности базы данных..."
for i in {1..30}; do
if docker compose exec postgres pg_isready -U remnawave_user -d remnawave_bot &>/dev/null; then
log "PostgreSQL готов к работе"
break
fi
if [ $i -eq 30 ]; then
error "PostgreSQL не запустился в течение 60 секунд"
return 1
fi
sleep 2
echo -n "."
done
echo ""
fi
# Проверяем доступность PostgreSQL
echo -e "${YELLOW}Проверка доступности PostgreSQL:${NC}"
if docker compose exec postgres pg_isready -U remnawave_user -d remnawave_bot; then
echo -e "${GREEN}✓ PostgreSQL доступен${NC}"
else
echo -e "${RED}✗ PostgreSQL недоступен${NC}"
echo "Проверьте логи: docker compose logs postgres"
return 1
fi
echo ""
# Проверяем подключение к базе данных
echo -e "${YELLOW}Информация о базе данных:${NC}"
docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "
SELECT
current_database() as database_name,
current_user as current_user,
version() as postgresql_version;
" 2>/dev/null || {
echo -e "${RED}Ошибка подключения к базе данных${NC}"
return 1
}
echo ""
# Показываем список таблиц
echo -e "${YELLOW}Таблицы в базе данных:${NC}"
TABLES_OUTPUT=$(docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "\dt" 2>/dev/null)
if echo "$TABLES_OUTPUT" | grep -q "No relations found"; then
echo -e "${YELLOW}База данных пуста - таблицы еще не созданы${NC}"
echo -e "${BLUE}Это нормально если бот еще ни разу не запускался${NC}"
elif [ -z "$TABLES_OUTPUT" ]; then
echo -e "${YELLOW}Не удается получить список таблиц${NC}"
else
echo "$TABLES_OUTPUT"
fi
echo ""
# Показываем размер базы данных
echo -e "${YELLOW}Размер базы данных:${NC}"
docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "
SELECT
pg_database.datname,
pg_size_pretty(pg_database_size(pg_database.datname)) AS size
FROM pg_database
WHERE datname = 'remnawave_bot';
" 2>/dev/null || {
echo -e "${RED}Не удается получить размер базы данных${NC}"
}
}
edit_env_file() {
ensure_nano
if [ ! -f "$ENV_FILE" ]; then
error "Файл .env не найден: $ENV_FILE"
return 1
fi
log "Открытие .env файла для редактирования..."
log "После изменений перезапустите бота для применения настроек"
# Создаем резервную копию .env файла
cp "$ENV_FILE" "$ENV_FILE.backup.$(date +%Y%m%d_%H%M%S)"
nano "$ENV_FILE"
log "Редактирование завершено"
echo -e "${YELLOW}Не забудьте перезапустить бота для применения изменений!${NC}"
}
# Удаление базы данных
remove_database() {
warn "ВНИМАНИЕ! Это действие удалит всю базу данных!"
warn "Все данные бота (пользователи, подписки, настройки) будут потеряны!"
read -p "Вы уверены? Введите 'YES' для подтверждения: " confirm
if [ "$confirm" = "YES" ]; then
log "Удаление базы данных..."
cd "$BOT_DIR"
# Останавливаем все сервисы если они запущены
if docker compose ps --services --filter "status=running" | grep -q "."; then
log "Остановка всех контейнеров..."
docker compose down
fi
# Удаляем volume с данными PostgreSQL
log "Удаление volume с данными PostgreSQL..."
VOLUME_NAME=$(docker compose config --volumes 2>/dev/null | grep postgres || echo "bedolaga-bot_postgres_data")
# Пробуем удалить volume разными способами
if docker volume ls | grep -q "$VOLUME_NAME"; then
if docker volume rm "$VOLUME_NAME" 2>/dev/null; then
log "Volume $VOLUME_NAME успешно удален"
else
log "Принудительное удаление volume..."
docker volume rm "$VOLUME_NAME" --force 2>/dev/null || true
fi
fi
# Также пробуем удалить стандартное имя volume
docker volume rm "$(basename $BOT_DIR)_postgres_data" 2>/dev/null || true
# Удаляем все неиспользуемые volumes связанные с проектом
log "Очистка неиспользуемых volumes..."
docker volume prune -f 2>/dev/null || true
log "База данных удалена"
log "При следующем запуске бота будет создана новая пустая база данных"
else
log "Операция отменена"
fi
}
# Полное удаление бота
remove_bot() {
warn "ВНИМАНИЕ! Это действие полностью удалит бота и все данные!"
read -p "Вы уверены? Введите 'YES' для подтверждения: " confirm
if [ "$confirm" = "YES" ]; then
log "Удаление бота..."
cd "$BOT_DIR"
docker compose down -v
systemctl disable bedolaga-bot.service 2>/dev/null || true
rm -f "$SERVICE_FILE"
rm -rf "$BOT_DIR"
log "Бот полностью удален"
exit 0
else
log "Операция отменена"
fi
}
# Главное меню
show_menu() {
clear
echo -e "${BLUE}=== RemnaWave Bedolaga Bot Management ===${NC}"
echo ""
# Показать статус бота
echo -e "${YELLOW}Статус бота:${NC}"
if check_bot_status; then
echo -e "🟢 Бот: ${GREEN}ЗАПУЩЕН${NC}"
else
echo -e "🔴 Бот: ${RED}ОСТАНОВЛЕН${NC}"
fi
# Показать статус подключения к API
echo -e "${YELLOW}Подключение к RemnaWave API:${NC}"
if check_remnawave_connection; then
echo -e "🟢 API: ${GREEN}ПОДКЛЮЧЕН${NC}"
else
echo -e "🔴 API: ${RED}НЕ ПОДКЛЮЧЕН${NC}"
fi
echo ""
echo -e "${YELLOW}Доступные действия:${NC}"
if check_bot_status; then
# Бот запущен
echo "1) Выключить бот"
echo "2) Перезапустить бот"
echo "3) Посмотреть логи в реальном времени"
echo "4) Обновить бот"
echo "5) Посмотреть логи"
echo "6) Создать резервную копию БД"
echo "7) Восстановить базу данных"
echo "8) Редактировать .env файл"
echo "9) Диагностика базы данных"
echo "10) Удалить базу данных"
echo "11) Полностью удалить бота"
echo "0) Выход"
read -p "Выберите действие: " choice
case $choice in
1) stop_bot; read -p "Нажмите Enter для продолжения..."; ;;
2) restart_bot; read -p "Нажмите Enter для продолжения..."; ;;
3) view_live_logs; ;;
4) update_bot; read -p "Нажмите Enter для продолжения..."; ;;
5) view_logs; read -p "Нажмите Enter для продолжения..."; ;;
6) backup_database; read -p "Нажмите Enter для продолжения..."; ;;
7) restore_database; read -p "Нажмите Enter для продолжения..."; ;;
8) edit_env_file; read -p "Нажмите Enter для продолжения..."; ;;
9) diagnose_database; read -p "Нажмите Enter для продолжения..."; ;;
10) remove_database; read -p "Нажмите Enter для продолжения..."; ;;
11) remove_bot; ;;
0) exit 0; ;;
*) error "Неверный выбор"; read -p "Нажмите Enter для продолжения..."; ;;
esac
else
# Бот остановлен
echo "1) Запустить бот"
echo "2) Обновить бот"
echo "3) Посмотреть логи"
echo "4) Создать резервную копию БД"
echo "5) Восстановить базу данных"
echo "6) Редактировать .env файл"
echo "7) Диагностика базы данных"
echo "8) Удалить базу данных"
echo "9) Полностью удалить бота"
echo "0) Выход"
read -p "Выберите действие: " choice
case $choice in
1) start_bot; read -p "Нажмите Enter для продолжения..."; ;;
2) update_bot; read -p "Нажмите Enter для продолжения..."; ;;
3) view_logs; read -p "Нажмите Enter для продолжения..."; ;;
4) backup_database; read -p "Нажмите Enter для продолжения..."; ;;
5) restore_database; read -p "Нажмите Enter для продолжения..."; ;;
6) edit_env_file; read -p "Нажмите Enter для продолжения..."; ;;
7) diagnose_database; read -p "Нажмите Enter для продолжения..."; ;;
8) remove_database; read -p "Нажмите Enter для продолжения..."; ;;
9) remove_bot; ;;
0) exit 0; ;;
*) error "Неверный выбор"; read -p "Нажмите Enter для продолжения..."; ;;
esac
fi
}
# Основная функция установки
install_bot() {
log "Начало установки RemnaWave Bedolaga Bot"
update_system
install_docker
create_project_structure
create_docker_compose
create_env_file
create_service
log "Установка завершена!"
log "Бот установлен в: $BOT_DIR"
log "Для управления ботом используйте это меню или systemctl"
read -p "Нажмите Enter для перехода в меню управления..."
}
# Главная функция
main() {
check_root
if check_installation; then
log "Бот уже установлен. Переход в меню управления..."
ensure_nano # Убеждаемся что nano установлен
else
install_bot
fi
# Основной цикл меню
while true; do
show_menu
done
}
# Запуск скрипта
main "$@"
+49 -39
View File
@@ -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:")
+778 -69
View File
@@ -1,6 +1,6 @@
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import BigInteger, String, Float, DateTime, Boolean, Text, Integer, text, select, func, and_
from sqlalchemy import BigInteger, String, Float, DateTime, Boolean, Text, Integer, text, select, func, and_, Column
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
import logging
@@ -14,23 +14,23 @@ class ReferralProgram(Base):
__tablename__ = 'referral_programs'
id: Mapped[int] = mapped_column(primary_key=True)
referrer_id: Mapped[int] = mapped_column(BigInteger, index=True) # Кто пригласил
referred_id: Mapped[int] = mapped_column(BigInteger, unique=True, index=True) # Кто был приглашен
referral_code: Mapped[str] = mapped_column(String(20), index=True) # Промокод реферера
first_reward_paid: Mapped[bool] = mapped_column(Boolean, default=False) # Выплачена ли разовая награда
total_earned: Mapped[float] = mapped_column(Float, default=0.0) # Всего заработано
referrer_id: Mapped[int] = mapped_column(BigInteger, index=True)
referred_id: Mapped[int] = mapped_column(BigInteger, unique=True, index=True)
referral_code: Mapped[str] = mapped_column(String(20), index=True)
first_reward_paid: Mapped[bool] = mapped_column(Boolean, default=False)
total_earned: Mapped[float] = mapped_column(Float, default=0.0)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
first_reward_at: Mapped[Optional[datetime]] = mapped_column(DateTime) # Когда выплатили первую награду
first_reward_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
class ReferralEarning(Base):
__tablename__ = 'referral_earnings'
id: Mapped[int] = mapped_column(primary_key=True)
referrer_id: Mapped[int] = mapped_column(BigInteger, index=True) # Кто получил награду
referred_id: Mapped[int] = mapped_column(BigInteger, index=True) # От кого получена награда
amount: Mapped[float] = mapped_column(Float) # Размер награды
earning_type: Mapped[str] = mapped_column(String(20)) # 'first_reward', 'percentage'
related_payment_id: Mapped[Optional[int]] = mapped_column(Integer) # Связанный платеж
referrer_id: Mapped[int] = mapped_column(BigInteger, index=True)
referred_id: Mapped[int] = mapped_column(BigInteger, index=True)
amount: Mapped[float] = mapped_column(Float)
earning_type: Mapped[str] = mapped_column(String(20))
related_payment_id: Mapped[Optional[int]] = mapped_column(Integer)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class User(Base):
@@ -56,7 +56,7 @@ class Subscription(Base):
description: Mapped[Optional[str]] = mapped_column(Text)
price: Mapped[float] = mapped_column(Float)
duration_days: Mapped[int] = mapped_column(Integer)
traffic_limit_gb: Mapped[int] = mapped_column(Integer, default=0) # 0 = unlimited
traffic_limit_gb: Mapped[int] = mapped_column(Integer, default=0)
squad_uuid: Mapped[str] = mapped_column(String(255))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
@@ -74,7 +74,9 @@ class UserSubscription(Base):
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
traffic_limit_gb: Mapped[Optional[int]] = mapped_column(Integer)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, onupdate=datetime.utcnow)
updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, onupdate=datetime.utcnow)
auto_pay_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
auto_pay_days_before: Mapped[int] = mapped_column(Integer, default=3)
class Payment(Base):
__tablename__ = 'payments'
@@ -114,11 +116,34 @@ class LuckyGame(Base):
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, index=True)
chosen_number: Mapped[int] = mapped_column(Integer)
winning_numbers: Mapped[str] = mapped_column(String(255)) # JSON строка с выигрышными номерами
winning_numbers: Mapped[str] = mapped_column(String(255))
is_winner: Mapped[bool] = mapped_column(Boolean, default=False)
reward_amount: Mapped[float] = mapped_column(Float, default=0.0)
played_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class StarPayment(Base):
__tablename__ = 'star_payments'
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, index=True)
stars_amount: Mapped[int] = mapped_column(Integer)
rub_amount: Mapped[float] = mapped_column(Float)
status: Mapped[str] = mapped_column(String(50), default='pending')
telegram_payment_charge_id: Mapped[Optional[str]] = mapped_column(String(255))
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
class ServiceRule(Base):
__tablename__ = 'service_rules'
id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
content = Column(Text, nullable=False)
page_order = Column(Integer, nullable=False, default=1)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
class Database:
def __init__(self, database_url: str):
self.engine = create_async_engine(
@@ -140,42 +165,141 @@ class Database:
await self.migrate_user_subscriptions()
await self.migrate_subscription_imported_field()
await self.migrate_referral_tables()
await self.migrate_star_payments_table()
await self.migrate_autopay_fields()
async def toggle_autopay(self, user_subscription_id: int, enabled: bool) -> bool:
"""Включает/выключает автоплатеж для подписки"""
async with self.session_factory() as session:
try:
from sqlalchemy import update
result = await session.execute(
update(UserSubscription)
.where(UserSubscription.id == user_subscription_id)
.values(auto_pay_enabled=enabled)
)
await session.commit()
return result.rowcount > 0
except Exception as e:
logger.error(f"Error toggling autopay: {e}")
await session.rollback()
return False
async def set_autopay_days(self, user_subscription_id: int, days_before: int) -> bool:
"""Устанавливает количество дней до истечения для автоплатежа"""
async with self.session_factory() as session:
try:
from sqlalchemy import update
result = await session.execute(
update(UserSubscription)
.where(UserSubscription.id == user_subscription_id)
.values(auto_pay_days_before=days_before)
)
await session.commit()
return result.rowcount > 0
except Exception as e:
logger.error(f"Error setting autopay days: {e}")
await session.rollback()
return False
async def get_subscriptions_for_autopay(self, days_threshold: int = None) -> List[UserSubscription]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, and_
from datetime import datetime, timedelta
if days_threshold is None:
current_time = datetime.utcnow()
conditions = []
for days in [1, 2, 3, 5, 7]:
threshold_date = current_time + timedelta(days=days)
conditions.append(
and_(
UserSubscription.auto_pay_days_before == days,
UserSubscription.expires_at <= threshold_date,
UserSubscription.expires_at > current_time
)
)
from sqlalchemy import or_
query = select(UserSubscription).where(
and_(
UserSubscription.auto_pay_enabled == True,
UserSubscription.is_active == True,
or_(*conditions)
)
)
else:
threshold_date = datetime.utcnow() + timedelta(days=days_threshold)
query = select(UserSubscription).where(
and_(
UserSubscription.auto_pay_enabled == True,
UserSubscription.is_active == True,
UserSubscription.expires_at <= threshold_date,
UserSubscription.expires_at > datetime.utcnow()
)
)
result = await session.execute(query)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting subscriptions for autopay: {e}")
return []
async def migrate_referral_tables(self):
try:
async with self.engine.begin() as conn:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS referral_programs (
id SERIAL PRIMARY KEY,
referrer_id BIGINT NOT NULL,
referred_id BIGINT UNIQUE NOT NULL,
referral_code VARCHAR(20) NOT NULL,
first_reward_paid BOOLEAN DEFAULT FALSE,
total_earned DOUBLE PRECISION DEFAULT 0.0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
first_reward_at TIMESTAMP,
INDEX idx_referrer (referrer_id),
INDEX idx_referred (referred_id),
INDEX idx_referral_code (referral_code)
)
"""))
try:
await conn.execute(text("SELECT 1 FROM referral_programs LIMIT 1"))
logger.info("referral_programs table already exists")
except Exception:
await conn.execute(text("""
CREATE TABLE referral_programs (
id SERIAL PRIMARY KEY,
referrer_id BIGINT NOT NULL,
referred_id BIGINT UNIQUE NOT NULL,
referral_code VARCHAR(20) NOT NULL,
first_reward_paid BOOLEAN DEFAULT FALSE,
total_earned DOUBLE PRECISION DEFAULT 0.0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
first_reward_at TIMESTAMP
)
"""))
try:
await conn.execute(text("CREATE INDEX idx_referrer ON referral_programs(referrer_id)"))
await conn.execute(text("CREATE INDEX idx_referred ON referral_programs(referred_id)"))
await conn.execute(text("CREATE INDEX idx_referral_code ON referral_programs(referral_code)"))
except Exception as e:
logger.warning(f"Some referral_programs indexes may already exist: {e}")
logger.info("Created referral_programs table")
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS referral_earnings (
id SERIAL PRIMARY KEY,
referrer_id BIGINT NOT NULL,
referred_id BIGINT NOT NULL,
amount DOUBLE PRECISION NOT NULL,
earning_type VARCHAR(20) NOT NULL,
related_payment_id INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_referrer_earnings (referrer_id),
INDEX idx_referred_earnings (referred_id),
INDEX idx_earning_type (earning_type)
)
"""))
try:
await conn.execute(text("SELECT 1 FROM referral_earnings LIMIT 1"))
logger.info("referral_earnings table already exists")
except Exception:
await conn.execute(text("""
CREATE TABLE referral_earnings (
id SERIAL PRIMARY KEY,
referrer_id BIGINT NOT NULL,
referred_id BIGINT NOT NULL,
amount DOUBLE PRECISION NOT NULL,
earning_type VARCHAR(20) NOT NULL,
related_payment_id INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""))
try:
await conn.execute(text("CREATE INDEX idx_referrer_earnings ON referral_earnings(referrer_id)"))
await conn.execute(text("CREATE INDEX idx_referred_earnings ON referral_earnings(referred_id)"))
await conn.execute(text("CREATE INDEX idx_earning_type ON referral_earnings(earning_type)"))
except Exception as e:
logger.warning(f"Some referral_earnings indexes may already exist: {e}")
logger.info("Created referral_earnings table")
logger.info("Successfully created referral system tables")
except Exception as e:
@@ -276,13 +400,17 @@ class Database:
try:
async with self.engine.begin() as conn:
try:
await conn.execute(text("""
ALTER TABLE subscriptions
ADD COLUMN IF NOT EXISTS is_imported BOOLEAN DEFAULT FALSE
"""))
logger.info("Successfully added is_imported field to subscriptions table")
except Exception as e:
logger.info(f"Migration may have already been applied: {e}")
await conn.execute(text("SELECT is_imported FROM subscriptions LIMIT 1"))
logger.info("is_imported field already exists")
except Exception:
try:
await conn.execute(text("""
ALTER TABLE subscriptions
ADD COLUMN is_imported BOOLEAN DEFAULT FALSE
"""))
logger.info("Successfully added is_imported field to subscriptions table")
except Exception as e:
logger.warning(f"Error adding is_imported field: {e}")
except Exception as e:
logger.error(f"Error during subscription migration: {e}")
@@ -599,10 +727,8 @@ class Database:
async def update_user_subscription(self, user_subscription: UserSubscription) -> bool:
async with self.session_factory() as session:
try:
# Устанавливаем время обновления
user_subscription.updated_at = datetime.utcnow()
# Обновляем подписку
await session.merge(user_subscription)
await session.commit()
return True
@@ -614,17 +740,27 @@ class Database:
async def migrate_user_subscriptions(self):
try:
async with self.engine.begin() as conn:
try:
await conn.execute(text("""
ALTER TABLE user_subscriptions
ADD COLUMN IF NOT EXISTS traffic_limit_gb INTEGER,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP
"""))
logger.info("Successfully migrated user_subscriptions table")
except Exception as e:
logger.info(f"Migration may have already been applied or error occurred: {e}")
fields_to_add = [
("traffic_limit_gb", "INTEGER"),
("updated_at", "TIMESTAMP")
]
for field_name, field_type in fields_to_add:
try:
await conn.execute(text(f"SELECT {field_name} FROM user_subscriptions LIMIT 1"))
except Exception:
try:
await conn.execute(text(f"""
ALTER TABLE user_subscriptions
ADD COLUMN {field_name} {field_type}
"""))
logger.info(f"Added {field_name} column to user_subscriptions")
except Exception as e:
logger.warning(f"Error adding {field_name} column: {e}")
logger.info("Successfully migrated user_subscriptions table")
except Exception as e:
logger.error(f"Error during migration: {e}")
logger.error(f"Error during user_subscriptions migration: {e}")
async def get_expiring_subscriptions(self, user_id: int, days_threshold: int = 3) -> List[UserSubscription]:
async with self.session_factory() as session:
@@ -1203,7 +1339,7 @@ class Database:
update(Promocode)
.where(
and_(
~Promocode.code.startswith('REF'), # Исключаем реферальные
~Promocode.code.startswith('REF'),
Promocode.is_active == True
)
)
@@ -1356,18 +1492,16 @@ class Database:
try:
from sqlalchemy import select, func, and_
# Общее количество игр
games_count = await session.execute(
select(func.count(LuckyGame.id))
.where(LuckyGame.user_id == user_id)
)
total_games = games_count.scalar() or 0
# Количество выигрышей
wins_count = await session.execute(
select(func.count(LuckyGame.id))
.where(
and_( # Вот здесь используется and_
and_(
LuckyGame.user_id == user_id,
LuckyGame.is_winner == True
)
@@ -1434,3 +1568,578 @@ class Database:
except Exception as e:
logger.error(f"Error checking can play today: {e}")
return True
async def create_star_payment(self, user_id: int, stars_amount: int, rub_amount: float) -> StarPayment:
"""Создать платеж через звезды"""
async with self.session_factory() as session:
try:
payment = StarPayment(
user_id=user_id,
stars_amount=stars_amount,
rub_amount=rub_amount,
status='pending'
)
session.add(payment)
await session.commit()
await session.refresh(payment)
return payment
except Exception as e:
logger.error(f"Error creating star payment: {e}")
await session.rollback()
raise
async def get_star_payment_by_id(self, payment_id: int) -> Optional[StarPayment]:
"""Получить платеж через звезды по ID"""
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(StarPayment).where(StarPayment.id == payment_id)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Error getting star payment {payment_id}: {e}")
return None
async def complete_star_payment(self, payment_id: int, telegram_payment_charge_id: str) -> bool:
"""Завершить платеж через звезды"""
async with self.session_factory() as session:
try:
from sqlalchemy import update
result = await session.execute(
update(StarPayment)
.where(StarPayment.id == payment_id)
.values(
status='completed',
telegram_payment_charge_id=telegram_payment_charge_id,
completed_at=datetime.utcnow()
)
)
if result.rowcount == 0:
return False
payment_result = await session.execute(
select(StarPayment).where(StarPayment.id == payment_id)
)
payment = payment_result.scalar_one_or_none()
if not payment:
return False
balance_result = await session.execute(
update(User)
.where(User.telegram_id == payment.user_id)
.values(balance=User.balance + payment.rub_amount)
)
regular_payment = Payment(
user_id=payment.user_id,
amount=payment.rub_amount,
payment_type='stars',
description=f'Пополнение через Telegram Stars ({payment.stars_amount} ⭐)',
status='completed'
)
session.add(regular_payment)
await session.commit()
return True
except Exception as e:
logger.error(f"Error completing star payment {payment_id}: {e}")
await session.rollback()
return False
async def cancel_star_payment(self, payment_id: int) -> bool:
"""Отменить платеж через звезды"""
async with self.session_factory() as session:
try:
from sqlalchemy import update
result = await session.execute(
update(StarPayment)
.where(StarPayment.id == payment_id)
.values(status='cancelled')
)
await session.commit()
return result.rowcount > 0
except Exception as e:
logger.error(f"Error cancelling star payment {payment_id}: {e}")
await session.rollback()
return False
async def get_user_star_payments(self, user_id: int, limit: int = 10) -> List[StarPayment]:
"""Получить платежи пользователя через звезды"""
async with self.session_factory() as session:
try:
from sqlalchemy import select, desc
result = await session.execute(
select(StarPayment)
.where(StarPayment.user_id == user_id)
.order_by(desc(StarPayment.created_at))
.limit(limit)
)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting user star payments for {user_id}: {e}")
return []
async def migrate_star_payments_table(self):
try:
async with self.engine.begin() as conn:
try:
await conn.execute(text("SELECT 1 FROM star_payments LIMIT 1"))
logger.info("star_payments table already exists")
return
except Exception:
pass
await conn.execute(text("""
CREATE TABLE star_payments (
id SERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
stars_amount INTEGER NOT NULL,
rub_amount DOUBLE PRECISION NOT NULL,
status VARCHAR(50) DEFAULT 'pending',
telegram_payment_charge_id VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP
)
"""))
try:
await conn.execute(text("CREATE INDEX idx_star_payments_user ON star_payments(user_id)"))
await conn.execute(text("CREATE INDEX idx_star_payments_status ON star_payments(status)"))
except Exception as e:
logger.warning(f"Some indexes may already exist: {e}")
logger.info("Successfully created star_payments table")
except Exception as e:
logger.error(f"Error creating star_payments table: {e}")
async def create_service_rule(self, title: str, content: str, page_order: int = None) -> ServiceRule:
async with self.session_factory() as session:
if page_order is None:
result = await session.execute(
select(func.max(ServiceRule.page_order)).where(ServiceRule.is_active == True)
)
max_order = result.scalar() or 0
page_order = max_order + 1
rule = ServiceRule(
title=title,
content=content,
page_order=page_order
)
session.add(rule)
await session.commit()
await session.refresh(rule)
return rule
async def get_all_service_rules(self, active_only: bool = True) -> List[ServiceRule]:
async with self.session_factory() as session:
query = select(ServiceRule).order_by(ServiceRule.page_order)
if active_only:
query = query.where(ServiceRule.is_active == True)
result = await session.execute(query)
return result.scalars().all()
async def get_service_rule_by_id(self, rule_id: int) -> Optional[ServiceRule]:
async with self.session_factory() as session:
result = await session.execute(
select(ServiceRule).where(ServiceRule.id == rule_id)
)
return result.scalar_one_or_none()
async def update_service_rule(self, rule: ServiceRule) -> bool:
try:
async with self.session_factory() as session:
await session.merge(rule)
await session.commit()
return True
except Exception as e:
logger.error(f"Error updating service rule: {e}")
return False
async def delete_service_rule(self, rule_id: int) -> bool:
try:
async with self.session_factory() as session:
result = await session.execute(
select(ServiceRule).where(ServiceRule.id == rule_id)
)
rule = result.scalar_one_or_none()
if rule:
await session.delete(rule)
await session.commit()
return True
return False
except Exception as e:
logger.error(f"Error deleting service rule: {e}")
return False
async def reorder_service_rules(self, rule_orders: List[tuple]) -> bool:
try:
async with self.session_factory() as session:
for rule_id, new_order in rule_orders:
await session.execute(
update(ServiceRule).where(ServiceRule.id == rule_id).values(page_order=new_order)
)
await session.commit()
return True
except Exception as e:
logger.error(f"Error reordering service rules: {e}")
return False
async def migrate_service_rules_table(self):
try:
async with self.engine.begin() as conn:
try:
await conn.execute(text("SELECT 1 FROM service_rules LIMIT 1"))
logger.info("service_rules table already exists")
return
except Exception:
pass
await conn.execute(text("""
CREATE TABLE service_rules (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
page_order INTEGER NOT NULL DEFAULT 1,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""))
try:
await conn.execute(text("CREATE INDEX idx_service_rules_order ON service_rules(page_order)"))
await conn.execute(text("CREATE INDEX idx_service_rules_active ON service_rules(is_active)"))
except Exception as e:
logger.warning(f"Some indexes may already exist: {e}")
check_result = await conn.execute(text("SELECT COUNT(*) FROM service_rules"))
count = check_result.scalar()
if count == 0:
await conn.execute(text("""
INSERT INTO service_rules (title, content, page_order) VALUES
('Общие положения',
'**1. Общие положения**
Настоящие Правила определяют условия использования VPN-сервиса.
**1.1** Используя наш сервис, вы соглашаетесь с данными правилами.
**1.2** Мы оставляем за собой право изменять правила в любое время.
**1.3** Продолжение использования сервиса после изменений означает ваше согласие с новыми условиями.',
1),
('Права и обязанности',
'**2. Права и обязанности пользователей**
**2.1 Права пользователя:**
• Использовать VPN-сервис в соответствии с тарифным планом
• Получать техническую поддержку
• Защиту персональных данных
**2.2 Обязанности пользователя:**
• Не использовать сервис для незаконной деятельности
• Не передавать данные доступа третьим лицам
• Своевременно оплачивать услуги
**2.3 Запрещается:**
• Попытки взлома или нарушения работы сервиса
• Спам и рассылка нежелательных сообщений
• Нарушение авторских прав',
2),
('Оплата и возврат средств',
'**3. Условия оплаты и возврата**
**3.1 Оплата:**
• Все платежи производятся в российских рублях
• Доступны различные способы оплаты
• Средства зачисляются автоматически или в течение 24 часов
**3.2 Возврат средств:**
• Возврат возможен в течение 7 дней с момента оплаты
• При технических проблемах возврат производится полностью
• Обращайтесь в поддержку для возврата
**3.3 Скидки и промокоды:**
• Действуют ограничения по времени и количеству использований
• Нельзя комбинировать несколько скидок',
3)
"""))
logger.info("Inserted default service rules")
logger.info("Successfully created service_rules table")
except Exception as e:
logger.error(f"Error creating service_rules table: {e}")
async def migrate_autopay_fields(self):
try:
async with self.engine.begin() as conn:
try:
await conn.execute(text("SELECT auto_pay_enabled FROM user_subscriptions LIMIT 1"))
logger.info("Autopay fields already exist")
return
except Exception:
pass
try:
await conn.execute(text("""
ALTER TABLE user_subscriptions
ADD COLUMN auto_pay_enabled BOOLEAN DEFAULT FALSE
"""))
logger.info("Added auto_pay_enabled column")
except Exception as e:
logger.warning(f"Column auto_pay_enabled may already exist: {e}")
try:
await conn.execute(text("""
ALTER TABLE user_subscriptions
ADD COLUMN auto_pay_days_before INTEGER DEFAULT 3
"""))
logger.info("Added auto_pay_days_before column")
except Exception as e:
logger.warning(f"Column auto_pay_days_before may already exist: {e}")
logger.info("Successfully added autopay fields to user_subscriptions table")
except Exception as e:
logger.error(f"Error adding autopay fields: {e}")
async def get_autopay_history(self, limit: int = 50) -> List[Dict[str, Any]]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, desc
result = await session.execute(
select(Payment)
.where(Payment.payment_type == 'autopay')
.order_by(desc(Payment.created_at))
.limit(limit)
)
payments = result.scalars().all()
autopay_history = []
for payment in payments:
try:
user_result = await session.execute(
select(User).where(User.telegram_id == payment.user_id)
)
user_obj = user_result.scalar_one_or_none()
autopay_history.append({
'payment_id': payment.id,
'user_id': payment.user_id,
'username': user_obj.username if user_obj else 'N/A',
'first_name': user_obj.first_name if user_obj else 'N/A',
'amount': payment.amount,
'description': payment.description,
'status': payment.status,
'created_at': payment.created_at
})
except Exception as e:
logger.warning(f"Error processing autopay history for payment {payment.id}: {e}")
continue
return autopay_history
except Exception as e:
logger.error(f"Error getting autopay history: {e}")
return []
async def disable_autopay_for_user(self, user_id: int) -> int:
async with self.session_factory() as session:
try:
from sqlalchemy import update
result = await session.execute(
update(UserSubscription)
.where(
and_(
UserSubscription.user_id == user_id,
UserSubscription.auto_pay_enabled == True
)
)
.values(auto_pay_enabled=False)
)
await session.commit()
return result.rowcount
except Exception as e:
logger.error(f"Error disabling autopay for user {user_id}: {e}")
await session.rollback()
return 0
async def get_autopay_subscription_by_id(self, subscription_id: int) -> Optional[UserSubscription]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(UserSubscription).where(
and_(
UserSubscription.id == subscription_id,
UserSubscription.auto_pay_enabled == True
)
)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Error getting autopay subscription {subscription_id}: {e}")
return None
async def get_autopay_statistics(self) -> Dict[str, Any]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, func, and_, case
from datetime import datetime, timedelta
total_autopay = await session.execute(
select(func.count(UserSubscription.id))
.where(UserSubscription.auto_pay_enabled == True)
)
total_autopay_subscriptions = total_autopay.scalar() or 0
active_autopay = await session.execute(
select(func.count(UserSubscription.id))
.where(
and_(
UserSubscription.auto_pay_enabled == True,
UserSubscription.is_active == True,
UserSubscription.expires_at > datetime.utcnow()
)
)
)
active_autopay_subscriptions = active_autopay.scalar() or 0
expired_autopay = await session.execute(
select(func.count(UserSubscription.id))
.where(
and_(
UserSubscription.auto_pay_enabled == True,
UserSubscription.expires_at <= datetime.utcnow()
)
)
)
expired_autopay_subscriptions = expired_autopay.scalar() or 0
ready_for_autopay = []
current_time = datetime.utcnow()
for days in [1, 2, 3, 5, 7]:
threshold_date = current_time + timedelta(days=days)
ready_count = await session.execute(
select(func.count(UserSubscription.id))
.where(
and_(
UserSubscription.auto_pay_enabled == True,
UserSubscription.is_active == True,
UserSubscription.auto_pay_days_before == days,
UserSubscription.expires_at <= threshold_date,
UserSubscription.expires_at > current_time
)
)
)
count = ready_count.scalar() or 0
ready_for_autopay.append({
'days': days,
'count': count
})
return {
'total_autopay_subscriptions': total_autopay_subscriptions,
'active_autopay_subscriptions': active_autopay_subscriptions,
'expired_autopay_subscriptions': expired_autopay_subscriptions,
'ready_for_autopay': ready_for_autopay
}
except Exception as e:
logger.error(f"Error getting autopay statistics: {e}")
return {
'total_autopay_subscriptions': 0,
'active_autopay_subscriptions': 0,
'expired_autopay_subscriptions': 0,
'ready_for_autopay': []
}
async def get_users_with_insufficient_autopay_balance(self) -> List[Dict[str, Any]]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, and_
from datetime import datetime, timedelta
current_time = datetime.utcnow()
insufficient_users = []
autopay_subs = await session.execute(
select(UserSubscription)
.where(
and_(
UserSubscription.auto_pay_enabled == True,
UserSubscription.is_active == True,
UserSubscription.expires_at > current_time
)
)
)
for user_sub in autopay_subs.scalars().all():
try:
threshold_date = current_time + timedelta(days=user_sub.auto_pay_days_before)
if user_sub.expires_at <= threshold_date:
user_result = await session.execute(
select(User).where(User.telegram_id == user_sub.user_id)
)
user_obj = user_result.scalar_one_or_none()
if not user_obj:
continue
sub_result = await session.execute(
select(Subscription).where(Subscription.id == user_sub.subscription_id)
)
subscription = sub_result.scalar_one_or_none()
if not subscription:
continue
if user_obj.balance < subscription.price:
days_left = (user_sub.expires_at - current_time).days
needed_amount = subscription.price - user_obj.balance
insufficient_users.append({
'user_id': user_obj.telegram_id,
'username': user_obj.username or 'N/A',
'first_name': user_obj.first_name or 'N/A',
'current_balance': user_obj.balance,
'subscription_price': subscription.price,
'needed_amount': needed_amount,
'subscription_name': subscription.name,
'expires_in_days': days_left,
'auto_pay_days_before': user_sub.auto_pay_days_before
})
except Exception as e:
logger.warning(f"Error processing user subscription {user_sub.id}: {e}")
continue
return insufficient_users
except Exception as e:
logger.error(f"Error getting users with insufficient autopay balance: {e}")
return []
+571 -126
View File
@@ -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("❌ Ошибка операции")
+257 -8
View File
@@ -35,7 +35,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 +110,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 +190,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")]
@@ -192,6 +227,7 @@ 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 +446,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 +638,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
+1 -23
View File
@@ -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
+63
View File
@@ -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:
+17 -9
View File
@@ -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}")
+214 -104
View File
@@ -52,7 +52,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 +60,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 +74,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 +292,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 +394,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}
-218
View File
@@ -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"
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

+1
View File
@@ -0,0 +1 @@
Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

+415
View File
@@ -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("❌ Произошла ошибка")
+205 -3
View File
@@ -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,
+2 -2
View File
@@ -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': 'Пополнить баланс',
-248
View File
@@ -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