Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64b5d90d66 | |||
| 5a1fb32e1d | |||
| c4d8108e8c | |||
| 1350d80083 | |||
| e0cc218007 | |||
| bcaf8f2247 | |||
| d1190ed9dc | |||
| 513d6037d4 | |||
| e86012b2ab | |||
| 214e381dc5 | |||
| aa79cb670b | |||
| f0106e15fa | |||
| 1d194c6be2 | |||
| 6917890007 | |||
| 1c69de7ecd | |||
| 62188793d9 | |||
| 585ee2d42b | |||
| 2a58cfd45c | |||
| 93003c5fe0 | |||
| 91b0787d72 | |||
| 315277d4b0 | |||
| 1821c0d89d | |||
| 5650da0c5f | |||
| 0ca8b73cef | |||
| 6bab26ab27 | |||
| a72243e7fa | |||
| 5bd86d9cf7 | |||
| 00fd3fbadb | |||
| b806f40797 | |||
| c82aa1d253 | |||
| 071919f0db | |||
| ab41e96ba5 | |||
| 95a470ba7b | |||
| 1e0653ed33 | |||
| 3f0df08031 | |||
| 8c3bc8a5ba | |||
| 3a5a8f48e4 | |||
| f4b99ffb09 | |||
| 0c6afdebc3 | |||
| 98b382ba2f | |||
| 54a24385b1 | |||
| 3eeb84cdae | |||
| 204a10ae14 | |||
| ba1dd7547e | |||
| 238585c257 | |||
| d8ebe1ad13 | |||
| 819df3e93a | |||
| 99e46feafa | |||
| a432c8ab54 | |||
| d740b7560c | |||
| 91c4c73d7e | |||
| 4ce2384f27 | |||
| 02a6289d56 | |||
| 52ae6eba81 | |||
| 080b6e74a2 | |||
| ad7d32023c | |||
| 1baf8fbe37 | |||
| 758ca6a373 | |||
| 65504d0a23 | |||
| 0409f4e2e2 | |||
| 8ca63e939a | |||
| 25a621fd51 | |||
| 3f71a8aff7 | |||
| 6ec81c37b9 | |||
| 8a9ff98da8 | |||
| bb7e29eeac | |||
| 27d8783104 | |||
| 9a01e6db43 | |||
| b341ddf9d9 | |||
| 3c8d0ac859 | |||
| 3f529c0dee |
@@ -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
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/screens
|
||||
/assets
|
||||
@@ -5,12 +5,14 @@
|
||||
[](https://postgresql.org)
|
||||
[](LICENSE)
|
||||
|
||||

|
||||
|
||||
Современный Telegram бот для управления VPN подписками через RemnaWave API с полным функционалом управления пользователями, платежами и администрированием.
|
||||
|
||||
## 🚀 Особенности
|
||||
|
||||
### 👤 Для пользователей
|
||||
- 💰 **Управление балансом** - пополнение через поддержку
|
||||
- 💰 **Управление балансом** - Пополнение через Telegram Stars + пополнение через поддержку p2p
|
||||
- 🛒 **Покупка подписок** - различные тарифные планы c возможностью индивидуальной настройки сквада у плана
|
||||
- 📱 **Управление подписками** - просмотр, продление, получение ссылок
|
||||
- 🎁 **Система промокодов** - денежные коды
|
||||
@@ -18,6 +20,7 @@
|
||||
- 🎰 **Игра удачи** - выигрывай бонусы каждые 24 часа
|
||||
- 🆓 **Тестовая подписка** - бесплатная пробная версия с детальной конфигурацией
|
||||
- 🌐 **Мультиязычность** - русский и английский
|
||||
- 📋 **Правила сервиса**
|
||||
|
||||
### ⚙️ Для администраторов
|
||||
- 📊 **Полная статистика** - пользователи, платежи, подписки
|
||||
@@ -27,7 +30,8 @@
|
||||
- 🖥 **Мониторинг системы** - состояние нод, пользователей RemnaWave
|
||||
- 🔄 **Синхронизация данных** - импорт из RemnaWave
|
||||
- 📨 **Рассылки** - уведомления пользователям
|
||||
- 🔍 **Мониторинг подписок** - автоматические уведомления об истечении
|
||||
- 🔍 **Мониторинг подписок** - автоматические уведомления об истечении и зачистка подписок с истекщим сроком для триал и обычных подписок
|
||||
- 📋 **Правила сервиса** - Настройка страниц из админ панели
|
||||
|
||||
## 📋 Требования
|
||||
|
||||
@@ -46,8 +50,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 +72,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 +83,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 +98,168 @@ nano .env
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|------------|----------|--------------|
|
||||
| `TRIAL_ENABLED` | Включить тестовую подписку | `true` |
|
||||
| `TRIAL_ENABLED` | Включить тестовую подписку | `true/false` |
|
||||
| `TRIAL_DURATION_DAYS` | Длительность (дни) | `3` |
|
||||
| `TRIAL_TRAFFIC_GB` | Лимит трафика (ГБ) | `2` |
|
||||
| `TRIAL_SQUAD_UUID` | UUID squad для тестовых | `uuid-here` |
|
||||
|
||||
#### 💬 Сервис мониторинга сообщений
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|------------|----------|--------------|
|
||||
| `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 +668,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 | Вывод правил сервиса в боте с конфигурацией через админку |
|
||||
|
||||
|
||||
### 🚀 Хотите помочь?
|
||||
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
<svg viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<!-- Градиенты -->
|
||||
<linearGradient id="mainGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#764ba2;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#f093fb;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="skinGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#d4a574;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#b8935f;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="redSkinGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#e8b598;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#d4a574;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="clothesGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#3a3a3a;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#595959;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="bottleGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2d5016;stop-opacity:0.8" />
|
||||
<stop offset="100%" style="stop-color:#1a2f0d;stop-opacity:0.9" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- Фильтры -->
|
||||
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<filter id="dropshadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="4"/>
|
||||
<feOffset dx="2" dy="4" result="offset" />
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.3"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Фон -->
|
||||
<rect width="400" height="400" fill="#1a1a2e" rx="50"/>
|
||||
|
||||
<!-- Декоративные элементы фона -->
|
||||
<circle cx="50" cy="80" r="30" fill="url(#mainGradient)" opacity="0.1"/>
|
||||
<circle cx="350" cy="120" r="40" fill="#8B4513" opacity="0.1"/>
|
||||
|
||||
<!-- Волны RemnaWave в фоне -->
|
||||
<g transform="translate(200,200)" opacity="0.2">
|
||||
<path d="M-120,-100 Q-60,-130 0,-100 Q60,-70 120,-100"
|
||||
stroke="url(#mainGradient)"
|
||||
stroke-width="4"
|
||||
fill="none">
|
||||
<animate attributeName="d"
|
||||
values="M-120,-100 Q-60,-130 0,-100 Q60,-70 120,-100;M-120,-100 Q-60,-70 0,-100 Q60,-130 120,-100;M-120,-100 Q-60,-130 0,-100 Q60,-70 120,-100"
|
||||
dur="4s"
|
||||
repeatCount="indefinite"/>
|
||||
</path>
|
||||
</g>
|
||||
|
||||
<!-- Главный персонаж - Бомж-алкаш -->
|
||||
<g transform="translate(200,180)" filter="url(#dropshadow)">
|
||||
|
||||
<!-- Тело в рваной куртке -->
|
||||
<ellipse cx="0" cy="30" rx="45" ry="60" fill="url(#clothesGradient)"/>
|
||||
|
||||
<!-- Рваные края одежды -->
|
||||
<path d="M-45,70 L-40,75 L-35,70 L-30,75 L-25,70 L-20,75 L-15,70 L-10,75 L-5,70 L0,75 L5,70 L10,75 L15,70 L20,75 L25,70 L30,75 L35,70 L40,75 L45,70"
|
||||
stroke="none" fill="#2a2a2a"/>
|
||||
|
||||
<!-- Заплатки -->
|
||||
<rect x="-20" y="10" width="12" height="10" fill="#5a4a3a" opacity="0.7" rx="2"/>
|
||||
<rect x="15" y="35" width="10" height="8" fill="#4a4a4a" opacity="0.8" rx="1"/>
|
||||
<rect x="-10" y="45" width="8" height="12" fill="#6a5a4a" opacity="0.6" rx="1"/>
|
||||
|
||||
<!-- Шея -->
|
||||
<rect x="-12" y="-25" width="24" height="20" fill="url(#redSkinGradient)" rx="8"/>
|
||||
|
||||
<!-- Голова (более человечная форма) -->
|
||||
<ellipse cx="0" cy="-45" rx="28" ry="32" fill="url(#redSkinGradient)"/>
|
||||
|
||||
<!-- Сальные волосы -->
|
||||
<path d="M-25,-70 Q-20,-75 -15,-72 Q-10,-78 -5,-73 Q0,-80 5,-73 Q10,-78 15,-72 Q20,-75 25,-70"
|
||||
stroke="#3a3a3a" stroke-width="4" fill="#3a3a3a" stroke-linecap="round"/>
|
||||
<path d="M-28,-65 Q-25,-70 -20,-67" stroke="#3a3a3a" stroke-width="3" fill="none" stroke-linecap="round"/>
|
||||
<path d="M28,-65 Q25,-70 20,-67" stroke="#3a3a3a" stroke-width="3" fill="none" stroke-linecap="round"/>
|
||||
|
||||
<!-- Левое ухо -->
|
||||
<ellipse cx="-28" cy="-45" rx="6" ry="10" fill="url(#skinGradient)"/>
|
||||
|
||||
<!-- Правое ухо -->
|
||||
<ellipse cx="28" cy="-45" rx="6" ry="10" fill="url(#skinGradient)"/>
|
||||
|
||||
<!-- Опухшие глаза -->
|
||||
<ellipse cx="-10" cy="-50" rx="8" ry="5" fill="white"/>
|
||||
<ellipse cx="10" cy="-50" rx="8" ry="5" fill="white"/>
|
||||
|
||||
<!-- Зрачки (косящие) -->
|
||||
<g id="eyes">
|
||||
<circle cx="-8" cy="-49" r="3" fill="#333">
|
||||
<animate attributeName="cx" values="-8;-10;-8" dur="3s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="12" cy="-49" r="3" fill="#333">
|
||||
<animate attributeName="cx" values="12;10;12" dur="3s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</g>
|
||||
|
||||
<!-- Красные глаза -->
|
||||
<path d="M-18,-50 L-15,-51" stroke="red" stroke-width="1" opacity="0.7"/>
|
||||
<path d="M-17,-48 L-14,-49" stroke="red" stroke-width="0.5" opacity="0.7"/>
|
||||
<path d="M18,-50 L15,-51" stroke="red" stroke-width="1" opacity="0.7"/>
|
||||
<path d="M17,-48 L14,-49" stroke="red" stroke-width="0.5" opacity="0.7"/>
|
||||
|
||||
<!-- Мешки под глазами -->
|
||||
<ellipse cx="-10" cy="-44" rx="8" ry="3" fill="#8a6a4a" opacity="0.5"/>
|
||||
<ellipse cx="10" cy="-44" rx="8" ry="3" fill="#8a6a4a" opacity="0.5"/>
|
||||
|
||||
<!-- Красный нос алкаша -->
|
||||
<ellipse cx="0" cy="-38" rx="6" ry="5" fill="#d67070"/>
|
||||
<circle cx="-2" cy="-39" r="1" fill="#e08080" opacity="0.8"/>
|
||||
|
||||
<!-- Небритость -->
|
||||
<g opacity="0.6">
|
||||
<circle cx="-15" cy="-30" r="1" fill="#555"/>
|
||||
<circle cx="-12" cy="-28" r="0.8" fill="#555"/>
|
||||
<circle cx="-8" cy="-30" r="0.8" fill="#555"/>
|
||||
<circle cx="15" cy="-30" r="1" fill="#555"/>
|
||||
<circle cx="12" cy="-28" r="0.8" fill="#555"/>
|
||||
<circle cx="8" cy="-30" r="0.8" fill="#555"/>
|
||||
<circle cx="0" cy="-28" r="0.8" fill="#555"/>
|
||||
<circle cx="-5" cy="-26" r="0.8" fill="#555"/>
|
||||
<circle cx="5" cy="-26" r="0.8" fill="#555"/>
|
||||
</g>
|
||||
|
||||
<!-- Рот с сигаретой/бутылкой (анимированный) -->
|
||||
<g id="mouth-group">
|
||||
<!-- Основной рот -->
|
||||
<path d="M-12,-20 Q0,-18 12,-20" stroke="#444" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||||
|
||||
<!-- Сигарета (появляется и исчезает) -->
|
||||
<g id="cigarette">
|
||||
<rect x="10" y="-22" width="25" height="3" fill="white" rx="1">
|
||||
<animate attributeName="opacity" values="0;0;0;1;1;1;0;0;0" dur="8s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
<rect x="30" y="-22" width="5" height="3" fill="#ff6b35">
|
||||
<animate attributeName="opacity" values="0;0;0;1;1;1;0;0;0" dur="8s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
<!-- Дым от сигареты -->
|
||||
<g opacity="0.4">
|
||||
<circle cx="35" cy="-22" r="2" fill="#999">
|
||||
<animate attributeName="opacity" values="0;0;0;0.6;0.3;0;0;0;0" dur="8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="cy" values="-22;-22;-22;-28;-35;-40;-22;-22;-22" dur="8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="r" values="2;2;2;4;6;8;2;2;2" dur="8s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Левая рука с бутылкой -->
|
||||
<g id="left-arm">
|
||||
<!-- Рука -->
|
||||
<line x1="-45" y1="10" x2="-55" y2="-5" stroke="url(#skinGradient)" stroke-width="8" stroke-linecap="round">
|
||||
<animate attributeName="x2" values="-55;-50;-55" dur="8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="y2" values="-5;-20;-5" dur="8s" repeatCount="indefinite"/>
|
||||
</line>
|
||||
|
||||
<!-- Кисть -->
|
||||
<circle cx="-55" cy="-5" r="6" fill="url(#skinGradient)">
|
||||
<animate attributeName="cx" values="-55;-50;-55" dur="8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="cy" values="-5;-20;-5" dur="8s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
|
||||
<!-- Бутылка -->
|
||||
<g id="bottle">
|
||||
<rect x="-62" y="-25" width="14" height="30" fill="url(#bottleGradient)" rx="2">
|
||||
<animate attributeName="x" values="-62;-57;-62" dur="8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="y" values="-25;-40;-25" dur="8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="transform" values="rotate(0 -55 -10);rotate(-30 -50 -25);rotate(0 -55 -10)" dur="8s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
<rect x="-58" y="-30" width="6" height="8" fill="#2d5016">
|
||||
<animate attributeName="x" values="-58;-53;-58" dur="8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="y" values="-30;-45;-30" dur="8s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
<!-- Этикетка -->
|
||||
<rect x="-60" y="-15" width="10" height="8" fill="white" opacity="0.3">
|
||||
<animate attributeName="x" values="-60;-55;-60" dur="8s" repeatCount="indefinite"/>
|
||||
<animate attributeName="y" values="-15;-30;-15" dur="8s" repeatCount="indefinite"/>
|
||||
</rect>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Правая рука (свободная, дрожащая) -->
|
||||
<g id="right-arm">
|
||||
<line x1="45" y1="10" x2="55" y2="20" stroke="url(#skinGradient)" stroke-width="8" stroke-linecap="round">
|
||||
<animate attributeName="x2" values="55;57;55;53;55" dur="2s" repeatCount="indefinite"/>
|
||||
</line>
|
||||
<circle cx="55" cy="20" r="6" fill="url(#skinGradient)">
|
||||
<animate attributeName="cx" values="55;57;55;53;55" dur="2s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</g>
|
||||
|
||||
<!-- Ноги в рваных штанах -->
|
||||
<rect x="-15" y="85" width="12" height="35" fill="#4a4a5a" rx="2"/>
|
||||
<rect x="3" y="85" width="12" height="35" fill="#4a4a5a" rx="2"/>
|
||||
|
||||
<!-- Дырки в штанах -->
|
||||
<ellipse cx="-9" cy="95" rx="3" ry="4" fill="#1a1a2e"/>
|
||||
<ellipse cx="9" cy="105" rx="2" ry="3" fill="#1a1a2e"/>
|
||||
|
||||
<!-- Старые ботинки -->
|
||||
<ellipse cx="-9" cy="125" rx="10" ry="6" fill="#2a2a2a"/>
|
||||
<ellipse cx="9" cy="125" rx="10" ry="6" fill="#2a2a2a"/>
|
||||
<!-- Дырки в ботинках -->
|
||||
<circle cx="-6" cy="123" r="2" fill="#1a1a2e"/>
|
||||
<circle cx="12" cy="123" r="1.5" fill="#1a1a2e"/>
|
||||
|
||||
<!-- Значок "B" на груди -->
|
||||
<circle cx="0" cy="25" r="20" fill="url(#mainGradient)" opacity="0.8" stroke="white" stroke-width="1"/>
|
||||
<text x="0" y="35"
|
||||
font-family="Arial Black, sans-serif"
|
||||
font-size="28"
|
||||
font-weight="bold"
|
||||
text-anchor="middle"
|
||||
fill="white">
|
||||
B
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<!-- Упавшие бутылки вокруг -->
|
||||
<g opacity="0.6">
|
||||
<rect x="80" y="340" width="8" height="20" fill="#2d5016" rx="1" transform="rotate(75 84 350)"/>
|
||||
<rect x="310" y="330" width="7" height="18" fill="#3a5a1a" rx="1" transform="rotate(-60 313 339)"/>
|
||||
<rect x="150" y="350" width="6" height="16" fill="#2d5016" rx="1" transform="rotate(45 153 358)"/>
|
||||
</g>
|
||||
|
||||
<!-- Окурки -->
|
||||
<g opacity="0.7">
|
||||
<rect x="120" y="360" width="12" height="2" fill="white" rx="1"/>
|
||||
<rect x="130" y="360" width="2" height="2" fill="#ff6b35"/>
|
||||
|
||||
<rect x="250" y="355" width="10" height="2" fill="white" rx="1" transform="rotate(30 255 356)"/>
|
||||
<rect x="258" y="355" width="2" height="2" fill="#ff6b35" transform="rotate(30 259 356)"/>
|
||||
</g>
|
||||
|
||||
<!-- Текст логотипа -->
|
||||
<text x="200" y="320"
|
||||
font-family="Arial Black, sans-serif"
|
||||
font-size="20"
|
||||
font-weight="bold"
|
||||
text-anchor="middle"
|
||||
fill="url(#mainGradient)"
|
||||
filter="url(#glow)">
|
||||
RemnaWave
|
||||
</text>
|
||||
|
||||
<text x="200" y="345"
|
||||
font-family="Arial Black, sans-serif"
|
||||
font-size="18"
|
||||
font-weight="bold"
|
||||
text-anchor="middle"
|
||||
fill="#d67070">
|
||||
Bedolaga Bot
|
||||
</text>
|
||||
|
||||
<!-- Подпись -->
|
||||
<text x="200" y="365"
|
||||
font-family="Arial, sans-serif"
|
||||
font-size="11"
|
||||
text-anchor="middle"
|
||||
fill="#8892b0"
|
||||
opacity="0.8">
|
||||
Telegram VPN Manager
|
||||
</text>
|
||||
|
||||
<!-- Мухи летающие вокруг (признак антисанитарии) -->
|
||||
<g opacity="0.5">
|
||||
<circle cx="150" cy="150" r="1" fill="#333">
|
||||
<animateTransform attributeName="transform" type="rotate" values="0 200 180;360 200 180" dur="6s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="250" cy="150" r="1" fill="#333">
|
||||
<animateTransform attributeName="transform" type="rotate" values="360 200 180;0 200 180" dur="5s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,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 "$@"
|
||||
@@ -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:")
|
||||
|
||||
@@ -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)
|
||||
@@ -114,11 +114,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,6 +163,7 @@ class Database:
|
||||
await self.migrate_user_subscriptions()
|
||||
await self.migrate_subscription_imported_field()
|
||||
await self.migrate_referral_tables()
|
||||
await self.migrate_star_payments_table()
|
||||
|
||||
async def migrate_referral_tables(self):
|
||||
try:
|
||||
@@ -599,10 +623,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
|
||||
@@ -1203,7 +1225,7 @@ class Database:
|
||||
update(Promocode)
|
||||
.where(
|
||||
and_(
|
||||
~Promocode.code.startswith('REF'), # Исключаем реферальные
|
||||
~Promocode.code.startswith('REF'),
|
||||
Promocode.is_active == True
|
||||
)
|
||||
)
|
||||
@@ -1356,18 +1378,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 +1454,364 @@ 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:
|
||||
db_type = str(conn.get_dialect().name).lower()
|
||||
|
||||
if db_type == 'postgresql':
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS 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
|
||||
)
|
||||
"""))
|
||||
|
||||
await conn.execute(text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_star_payments_user ON star_payments(user_id)
|
||||
"""))
|
||||
await conn.execute(text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_star_payments_status ON star_payments(status)
|
||||
"""))
|
||||
else:
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS 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,
|
||||
|
||||
INDEX idx_star_payments_user (user_id),
|
||||
INDEX idx_star_payments_status (status)
|
||||
)
|
||||
"""))
|
||||
|
||||
logger.info("Successfully created star_payments table")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating star_payments table: {e}")
|
||||
pass
|
||||
|
||||
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:
|
||||
db_type = str(conn.get_dialect().name).lower()
|
||||
|
||||
if db_type == 'postgresql':
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS 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
|
||||
)
|
||||
"""))
|
||||
|
||||
await conn.execute(text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_service_rules_order ON service_rules(page_order)
|
||||
"""))
|
||||
await conn.execute(text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_service_rules_active ON service_rules(is_active)
|
||||
"""))
|
||||
|
||||
await conn.execute(text("""
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$ language 'plpgsql'
|
||||
"""))
|
||||
|
||||
await conn.execute(text("""
|
||||
DROP TRIGGER IF EXISTS update_service_rules_updated_at ON service_rules
|
||||
"""))
|
||||
|
||||
await conn.execute(text("""
|
||||
CREATE TRIGGER update_service_rules_updated_at
|
||||
BEFORE UPDATE ON service_rules
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column()
|
||||
"""))
|
||||
|
||||
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")
|
||||
|
||||
else:
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS 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 ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_service_rules_order (page_order),
|
||||
INDEX idx_service_rules_active (is_active)
|
||||
)
|
||||
"""))
|
||||
|
||||
logger.info("Successfully created service_rules table")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating service_rules table: {e}")
|
||||
pass
|
||||
|
||||
@@ -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"
|
||||
@@ -977,8 +948,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"
|
||||
@@ -1154,14 +1124,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 +1253,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 +1302,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 +1359,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 +1388,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 +1509,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 +1658,119 @@ 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)
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
@@ -162,6 +163,7 @@ def admin_menu_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
InlineKeyboardButton(text="👥 Рефералы", callback_data="admin_referrals") # НОВАЯ КНОПКА
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📜 Правила сервиса", callback_data="admin_rules"), # НОВАЯ КНОПКА
|
||||
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system")
|
||||
],
|
||||
[
|
||||
@@ -192,6 +194,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 +413,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 +605,117 @@ 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
|
||||
|
||||
@@ -25,7 +25,6 @@ lucky_game_router = Router()
|
||||
|
||||
@lucky_game_router.callback_query(F.data == "lucky_game")
|
||||
async def lucky_game_menu_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
"""Главное меню игры удачи"""
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
|
||||
@@ -34,15 +33,12 @@ async def lucky_game_menu_callback(callback: CallbackQuery, db: Database, **kwar
|
||||
return
|
||||
|
||||
try:
|
||||
# Получаем настройки игры из конфига
|
||||
reward_amount = getattr(config, 'LUCKY_GAME_REWARD', 50.0)
|
||||
numbers_count = getattr(config, 'LUCKY_GAME_NUMBERS', 30)
|
||||
winning_numbers = getattr(config, 'LUCKY_GAME_WINNING_COUNT', 3)
|
||||
|
||||
# Проверяем, можно ли играть сегодня
|
||||
can_play, next_game_time = await check_can_play_today(db, user.telegram_id)
|
||||
|
||||
# Получаем статистику игр пользователя
|
||||
games_played, total_won, win_count = await get_user_game_stats(db, user.telegram_id)
|
||||
|
||||
text = "🎰 **Проверь свою удачу!**\n\n"
|
||||
@@ -61,13 +57,11 @@ async def lucky_game_menu_callback(callback: CallbackQuery, db: Database, **kwar
|
||||
win_rate = (win_count / games_played) * 100
|
||||
text += f"• Процент побед: {win_rate:.1f}%\n"
|
||||
|
||||
# Создаем клавиатуру
|
||||
buttons = []
|
||||
|
||||
if can_play:
|
||||
buttons.append([InlineKeyboardButton(text="🎲 Играть!", callback_data="start_lucky_game")])
|
||||
else:
|
||||
# Вычисляем оставшееся время
|
||||
now = datetime.utcnow()
|
||||
time_left = next_game_time - now
|
||||
hours_left = int(time_left.total_seconds() // 3600)
|
||||
@@ -108,19 +102,16 @@ async def start_lucky_game_callback(callback: CallbackQuery, db: Database, state
|
||||
return
|
||||
|
||||
try:
|
||||
# Проверяем, можно ли играть
|
||||
can_play, next_game_time = await check_can_play_today(db, user.telegram_id)
|
||||
|
||||
if not can_play:
|
||||
await callback.answer("⏰ Вы уже играли сегодня! Приходите завтра.", show_alert=True)
|
||||
return
|
||||
|
||||
# Получаем настройки
|
||||
numbers_count = getattr(config, 'LUCKY_GAME_NUMBERS', 30)
|
||||
winning_numbers = getattr(config, 'LUCKY_GAME_WINNING_COUNT', 3)
|
||||
reward_amount = getattr(config, 'LUCKY_GAME_REWARD', 50.0)
|
||||
|
||||
# Генерируем выигрышные числа заранее и сохраняем в состоянии
|
||||
winning_nums = random.sample(range(1, numbers_count + 1), winning_numbers)
|
||||
|
||||
await state.update_data(
|
||||
@@ -135,7 +126,6 @@ async def start_lucky_game_callback(callback: CallbackQuery, db: Database, state
|
||||
text += f"🍀 Удачных чисел: {winning_numbers} из {numbers_count}\n\n"
|
||||
text += "Нажмите на число, чтобы испытать удачу!"
|
||||
|
||||
# Создаем кнопки с числами (5 в ряд)
|
||||
buttons = []
|
||||
for i in range(0, numbers_count, 5):
|
||||
row = []
|
||||
@@ -175,24 +165,18 @@ async def choose_number_callback(callback: CallbackQuery, db: Database, state: F
|
||||
try:
|
||||
chosen_number = int(callback.data.split("_")[2])
|
||||
|
||||
# Получаем данные из состояния
|
||||
state_data = await state.get_data()
|
||||
winning_numbers = state_data['winning_numbers']
|
||||
reward_amount = state_data['reward_amount']
|
||||
numbers_count = state_data['numbers_count']
|
||||
|
||||
# Проверяем, выиграл ли пользователь
|
||||
is_winner = chosen_number in winning_numbers
|
||||
|
||||
# Сохраняем результат игры в БД
|
||||
await save_game_result(db, user.telegram_id, chosen_number, winning_numbers, is_winner, reward_amount if is_winner else 0.0)
|
||||
|
||||
# Формируем сообщение с результатом
|
||||
if is_winner:
|
||||
# Начисляем награду
|
||||
await db.add_balance(user.telegram_id, reward_amount)
|
||||
|
||||
# Создаем платеж
|
||||
await db.create_payment(
|
||||
user_id=user.telegram_id,
|
||||
amount=reward_amount,
|
||||
@@ -211,7 +195,6 @@ async def choose_number_callback(callback: CallbackQuery, db: Database, state: F
|
||||
text += f"🍀 Выигрышные числа: **{', '.join(map(str, sorted(winning_numbers)))}**\n\n"
|
||||
text += "🔄 Попробуйте завтра!"
|
||||
|
||||
# Показываем все выигрышные числа
|
||||
winning_nums_str = ', '.join(map(str, sorted(winning_numbers)))
|
||||
text += f"\n🎲 Сегодняшние удачные числа: {winning_nums_str}"
|
||||
text += f"\n\n⏰ Следующая игра будет доступна завтра!"
|
||||
@@ -227,7 +210,6 @@ async def choose_number_callback(callback: CallbackQuery, db: Database, state: F
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
# Логируем действие
|
||||
log_user_action(user.telegram_id, "lucky_game_played",
|
||||
f"Number: {chosen_number}, Winner: {is_winner}, Reward: {reward_amount if is_winner else 0}")
|
||||
|
||||
@@ -239,7 +221,6 @@ async def choose_number_callback(callback: CallbackQuery, db: Database, state: F
|
||||
|
||||
@lucky_game_router.callback_query(F.data == "lucky_game_history")
|
||||
async def lucky_game_history_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
"""Показать историю игр пользователя"""
|
||||
user = kwargs.get('user')
|
||||
|
||||
if not user:
|
||||
@@ -247,7 +228,6 @@ async def lucky_game_history_callback(callback: CallbackQuery, db: Database, **k
|
||||
return
|
||||
|
||||
try:
|
||||
# Получаем последние 10 игр
|
||||
games = await get_user_game_history(db, user.telegram_id, limit=10)
|
||||
|
||||
if not games:
|
||||
@@ -289,8 +269,7 @@ async def lucky_game_history_callback(callback: CallbackQuery, db: Database, **k
|
||||
async def noop_callback(callback: CallbackQuery, **kwargs):
|
||||
"""Пустая операция для неактивных кнопок"""
|
||||
await callback.answer()
|
||||
|
||||
# Вспомогательные функции
|
||||
|
||||
|
||||
async def check_can_play_today(db: Database, user_id: int) -> tuple[bool, Optional[datetime]]:
|
||||
"""Проверяет, может ли пользователь играть сегодня"""
|
||||
@@ -298,7 +277,6 @@ async def check_can_play_today(db: Database, user_id: int) -> tuple[bool, Option
|
||||
can_play = await db.can_play_lucky_game_today(user_id)
|
||||
|
||||
if not can_play:
|
||||
# Вычисляем время следующей игры (завтра в 00:00)
|
||||
now = datetime.utcnow()
|
||||
tomorrow = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
|
||||
return False, tomorrow
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
|
||||
print("🚀 Запуск бота...")
|
||||
print(f"📍 Рабочая директория: {os.getcwd()}")
|
||||
@@ -96,6 +97,24 @@ class BotApplication:
|
||||
self._setup_dispatcher()
|
||||
|
||||
await self._init_monitor_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 +186,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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# === RemnaWave Bot Setup & Management Script ===
|
||||
|
||||
SERVICE_NAME="remnawave-bot"
|
||||
BOT_FILE="main.py"
|
||||
ENV_FILE=".env"
|
||||
VENV_DIR="venv"
|
||||
|
||||
# Select language
|
||||
echo "🌐 Select language / Выберите язык:"
|
||||
echo "1) English"
|
||||
echo "2) Русский"
|
||||
read -p ">>> " LANG_CHOICE
|
||||
|
||||
case $LANG_CHOICE in
|
||||
2) LANG_CODE="ru" ;;
|
||||
*) LANG_CODE="en" ;;
|
||||
esac
|
||||
|
||||
# === Internationalized messages ===
|
||||
msg() {
|
||||
case $1 in
|
||||
check_python) [[ $LANG_CODE == "ru" ]] && echo "🔍 Проверка Python..." || echo "🔍 Checking Python..." ;;
|
||||
python_missing) [[ $LANG_CODE == "ru" ]] && echo "❌ Python 3 не установлен. Установите Python 3.8+." || echo "❌ Python 3 is not installed. Please install Python 3.8+." ;;
|
||||
version) echo "📋 Python version: $PYTHON_VERSION" ;;
|
||||
creating_venv) [[ $LANG_CODE == "ru" ]] && echo "🔧 Создание виртуального окружения..." || echo "🔧 Creating virtual environment..." ;;
|
||||
activating_venv) [[ $LANG_CODE == "ru" ]] && echo "🔧 Активация виртуального окружения..." || echo "🔧 Activating virtual environment..." ;;
|
||||
upgrading_pip) [[ $LANG_CODE == "ru" ]] && echo "📦 Обновление pip..." || echo "📦 Upgrading pip..." ;;
|
||||
installing_requirements) [[ $LANG_CODE == "ru" ]] && echo "📦 Установка зависимостей..." || echo "📦 Installing requirements..." ;;
|
||||
env_missing) [[ $LANG_CODE == "ru" ]] && echo "⚠️ Файл .env не найден!" || echo "⚠️ .env file not found!" ;;
|
||||
env_created) [[ $LANG_CODE == "ru" ]] && echo "📋 Создан .env файл. Пожалуйста, отредактируйте его." || echo "📋 .env created. Please edit it." ;;
|
||||
validating) [[ $LANG_CODE == "ru" ]] && echo "🔍 Проверка конфигурации..." || echo "🔍 Validating configuration..." ;;
|
||||
config_ok) [[ $LANG_CODE == "ru" ]] && echo "✅ Конфигурация прошла проверку." || echo "✅ Configuration validated successfully." ;;
|
||||
creating_service) [[ $LANG_CODE == "ru" ]] && echo "🔧 Создание systemd-сервиса..." || echo "🔧 Creating systemd service..." ;;
|
||||
service_created) [[ $LANG_CODE == "ru" ]] && echo "✅ Сервис создан и включён." || echo "✅ Service created and enabled." ;;
|
||||
start_prompt) [[ $LANG_CODE == "ru" ]] && echo "🚀 Запуск бота..." || echo "🚀 Starting the bot..." ;;
|
||||
already_running) [[ $LANG_CODE == "ru" ]] && echo "🟢 Бот уже запущен!" || echo "🟢 Bot is already running!" ;;
|
||||
last_logs) [[ $LANG_CODE == "ru" ]] && echo "📄 Последние 20 строк лога:" || echo "📄 Last 20 log lines:" ;;
|
||||
action_menu)
|
||||
[[ $LANG_CODE == "ru" ]] && {
|
||||
echo -e "\nЧто вы хотите сделать?"
|
||||
echo "1) Остановить бота"
|
||||
echo "2) Перезапустить бота"
|
||||
echo "3) Выйти"
|
||||
} || {
|
||||
echo -e "\nWhat would you like to do?"
|
||||
echo "1) Stop the bot"
|
||||
echo "2) Restart the bot"
|
||||
echo "3) Exit"
|
||||
}
|
||||
;;
|
||||
restarting) [[ $LANG_CODE == "ru" ]] && echo "🔄 Перезапуск бота..." || echo "🔄 Restarting bot..." ;;
|
||||
completed)
|
||||
echo ""
|
||||
[[ $LANG_CODE == "ru" ]] && {
|
||||
echo "🎉 Установка завершена!"
|
||||
echo "📋 Возможности бота:"
|
||||
echo " ✅ Мультиязычный интерфейс"
|
||||
echo " ✅ Управление балансом и подписками"
|
||||
echo " ✅ Промокоды и поддержка"
|
||||
echo ""
|
||||
} || {
|
||||
echo "🎉 Installation complete!"
|
||||
echo "📋 Bot Features:"
|
||||
echo " ✅ Multi-language support"
|
||||
echo " ✅ Subscription and balance management"
|
||||
echo " ✅ Promocode system and support"
|
||||
echo ""
|
||||
}
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# === Check if bot is running ===
|
||||
if systemctl is-active --quiet "$SERVICE_NAME"; then
|
||||
msg already_running
|
||||
msg last_logs
|
||||
sudo journalctl -u "$SERVICE_NAME" -n 20 --no-pager
|
||||
msg action_menu
|
||||
read -p ">>> " choice
|
||||
case $choice in
|
||||
1)
|
||||
[[ $LANG_CODE == "ru" ]] && echo "🛑 Остановка бота..." || echo "🛑 Stopping the bot..."
|
||||
sudo systemctl stop "$SERVICE_NAME"
|
||||
[[ $LANG_CODE == "ru" ]] && echo "✅ Бот остановлен." || echo "✅ Bot stopped."
|
||||
exit 0
|
||||
;;
|
||||
2)
|
||||
msg restarting
|
||||
sudo systemctl restart "$SERVICE_NAME"
|
||||
[[ $LANG_CODE == "ru" ]] && echo "✅ Бот перезапущен." || echo "✅ Bot restarted."
|
||||
exit 0
|
||||
;;
|
||||
3)
|
||||
[[ $LANG_CODE == "ru" ]] && echo "👋 Выход." || echo "👋 Exiting."
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
[[ $LANG_CODE == "ru" ]] && echo "⚠️ Неверный выбор." || echo "⚠️ Invalid choice."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# === Install/Setup ===
|
||||
msg check_python
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
msg python_missing
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
|
||||
msg version
|
||||
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
msg creating_venv
|
||||
python3 -m venv "$VENV_DIR"
|
||||
fi
|
||||
|
||||
msg activating_venv
|
||||
source "$VENV_DIR/bin/activate"
|
||||
|
||||
msg upgrading_pip
|
||||
pip install --upgrade pip
|
||||
|
||||
msg installing_requirements
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Create .env if needed
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
msg env_missing
|
||||
cat > "$ENV_FILE" <<EOF
|
||||
# Bot Configuration
|
||||
BOT_TOKEN=
|
||||
|
||||
# RemnaWave API Configuration
|
||||
REMNAWAVE_URL=
|
||||
REMNAWAVE_MODE=
|
||||
REMNAWAVE_TOKEN=
|
||||
|
||||
# Admin Configuration (comma-separated list of Telegram user IDs)
|
||||
ADMIN_IDS=
|
||||
|
||||
# Support Configuration
|
||||
SUPPORT_USERNAME=support
|
||||
|
||||
# Database Configuration (optional, defaults to SQLite)
|
||||
# DATABASE_URL=sqlite+aiosqlite:///bot.db
|
||||
|
||||
# Trial Configuration
|
||||
TRIAL_ENABLED=true
|
||||
TRIAL_DURATION_DAYS=3
|
||||
TRIAL_TRAFFIC_GB=2
|
||||
TRIAL_SQUAD_UUID=
|
||||
TRIAL_PRICE=0.0
|
||||
EOF
|
||||
msg env_created
|
||||
echo ""
|
||||
read -p "🔧 Press Enter after configuring .env / Нажмите Enter после настройки .env..."
|
||||
fi
|
||||
|
||||
# Load env
|
||||
export $(grep -v '^#' "$ENV_FILE" | xargs)
|
||||
|
||||
msg validating
|
||||
|
||||
if [[ -z "$BOT_TOKEN" || "$BOT_TOKEN" == "your_telegram_bot_token_here" ]]; then
|
||||
echo "❌ BOT_TOKEN not set properly."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$REMNAWAVE_URL" || "$REMNAWAVE_URL" == "https://your-panel.com" ]]; then
|
||||
echo "❌ REMNAWAVE_URL not set properly."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$REMNAWAVE_TOKEN" || "$REMNAWAVE_TOKEN" == "your_jwt_token_here" ]]; then
|
||||
echo "❌ REMNAWAVE_TOKEN not set properly."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg config_ok
|
||||
|
||||
# Create systemd service
|
||||
read -p "🔧 Create systemd service? (y/n): " CREATE_SERVICE
|
||||
if [[ "$CREATE_SERVICE" == "y" || "$CREATE_SERVICE" == "Y" ]]; then
|
||||
msg creating_service
|
||||
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
|
||||
CURRENT_DIR=$(pwd)
|
||||
CURRENT_USER=$(whoami)
|
||||
sudo tee "$SERVICE_FILE" > /dev/null <<EOF
|
||||
[Unit]
|
||||
Description=RemnaWave Bot
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$CURRENT_USER
|
||||
WorkingDirectory=$CURRENT_DIR
|
||||
Environment=PATH=$CURRENT_DIR/$VENV_DIR/bin
|
||||
EnvironmentFile=$CURRENT_DIR/.env
|
||||
ExecStart=$CURRENT_DIR/$VENV_DIR/bin/python3 $BOT_FILE
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable "$SERVICE_NAME"
|
||||
msg service_created
|
||||
fi
|
||||
|
||||
msg completed
|
||||
msg start_prompt
|
||||
python3 "$BOT_FILE"
|
||||
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 176 KiB |
@@ -0,0 +1 @@
|
||||
|
||||
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 327 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 111 KiB |
@@ -0,0 +1,415 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import Message, CallbackQuery, LabeledPrice, PreCheckoutQuery, InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from database import Database, User
|
||||
from keyboards import stars_topup_keyboard, stars_payment_keyboard, balance_keyboard
|
||||
from translations import t
|
||||
from utils import log_user_action
|
||||
from referral_utils import process_referral_rewards
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
stars_router = Router()
|
||||
|
||||
@stars_router.callback_query(F.data == "topup_stars")
|
||||
async def topup_stars_callback(callback: CallbackQuery, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
if not config or not config.STARS_ENABLED:
|
||||
await callback.message.edit_text(
|
||||
"❌ Пополнение через Telegram Stars временно недоступно",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
return
|
||||
|
||||
if not config.STARS_RATES:
|
||||
await callback.message.edit_text(
|
||||
"❌ Курсы звезд не настроены",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
return
|
||||
|
||||
text = "⭐ **Пополнение через Telegram Stars**\n\n"
|
||||
text += "🚀 **Преимущества:**\n"
|
||||
text += "• Мгновенное зачисление\n"
|
||||
text += "• Безопасные платежи через Telegram\n"
|
||||
text += "• Без комиссий и скрытых платежей\n\n"
|
||||
|
||||
text += "💎 **Доступные варианты:**\n"
|
||||
|
||||
sorted_rates = sorted(config.STARS_RATES.items())
|
||||
for stars, rubles in sorted_rates:
|
||||
rate_per_star = rubles / stars
|
||||
|
||||
if stars >= 500:
|
||||
bonus_text = " 🔥 Выгодно!"
|
||||
elif stars >= 250:
|
||||
bonus_text = " 💎 Хорошо!"
|
||||
else:
|
||||
bonus_text = ""
|
||||
|
||||
text += f"• {stars} ⭐ → {rubles:.0f}₽{bonus_text}\n"
|
||||
|
||||
text += f"\n💡 Выберите подходящий вариант:"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
@stars_router.callback_query(F.data.startswith("buy_stars_"))
|
||||
async def buy_stars_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
bot = kwargs.get('bot')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
if not config or not config.STARS_ENABLED or not config.STARS_RATES:
|
||||
await callback.answer("❌ Пополнение недоступно")
|
||||
return
|
||||
|
||||
try:
|
||||
stars_amount = int(callback.data.split("_")[2])
|
||||
|
||||
if stars_amount not in config.STARS_RATES:
|
||||
await callback.answer("❌ Неверное количество звезд")
|
||||
return
|
||||
|
||||
rub_amount = config.STARS_RATES[stars_amount]
|
||||
|
||||
star_payment = await db.create_star_payment(
|
||||
user_id=user.telegram_id,
|
||||
stars_amount=stars_amount,
|
||||
rub_amount=rub_amount
|
||||
)
|
||||
|
||||
prices = [LabeledPrice(label=f"Пополнение на {rub_amount:.0f}₽", amount=stars_amount)]
|
||||
|
||||
try:
|
||||
await callback.answer("💳 Создаю платеж...")
|
||||
|
||||
await bot.send_invoice(
|
||||
chat_id=callback.message.chat.id,
|
||||
title=f"Пополнение баланса",
|
||||
description=f"Пополнение баланса на {rub_amount:.0f}₽ за {stars_amount} ⭐",
|
||||
payload=f"star_payment_{star_payment.id}",
|
||||
currency="XTR",
|
||||
prices=prices
|
||||
)
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
f"💳 **Оплата через Telegram Stars**\n\n"
|
||||
f"⭐ Количество звезд: {stars_amount}\n"
|
||||
f"💰 Сумма пополнения: {rub_amount:.0f}₽\n\n"
|
||||
f"👆 Нажмите кнопку \"Оплатить\" в инвойсе ниже⤵️\n\n"
|
||||
f"Если передумали - нажмите кнопку ниже \n\n❌ Отменить платеж",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отменить платеж", callback_data=f"cancel_star_payment_{star_payment.id}")],
|
||||
[InlineKeyboardButton(text="🔙 Назад к выбору", callback_data="topup_stars")]
|
||||
]),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
await callback.message.answer(
|
||||
f"💳 **Оплата через Telegram Stars**\n\n"
|
||||
f"⭐ Количество звезд: {stars_amount}\n"
|
||||
f"💰 Сумма пополнения: {rub_amount:.0f}₽\n\n"
|
||||
f"👆 Нажмите кнопку \"Оплатить\" в инвойсе ниже⤵️",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отменить платеж", callback_data=f"cancel_star_payment_{star_payment.id}")],
|
||||
[InlineKeyboardButton(text="🔙 Назад к выбору", callback_data="topup_stars")]
|
||||
]),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except TelegramBadRequest as e:
|
||||
logger.error(f"Failed to send invoice: {e}")
|
||||
await callback.answer("❌ Ошибка создания платежа", show_alert=True)
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка создания платежа. Попробуйте позже.",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language)
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
await callback.message.answer(
|
||||
"❌ Ошибка создания платежа. Попробуйте позже.",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language)
|
||||
)
|
||||
|
||||
await db.cancel_star_payment(star_payment.id)
|
||||
|
||||
except (ValueError, IndexError) as e:
|
||||
logger.error(f"Error processing stars amount: {e}")
|
||||
await callback.answer("❌ Ошибка обработки")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in buy_stars_callback: {e}")
|
||||
await callback.answer("❌ Произошла ошибка")
|
||||
|
||||
@stars_router.pre_checkout_query()
|
||||
async def pre_checkout_query_handler(pre_checkout_query: PreCheckoutQuery, db: Database, **kwargs):
|
||||
try:
|
||||
if not pre_checkout_query.invoice_payload.startswith("star_payment_"):
|
||||
await pre_checkout_query.answer(ok=False, error_message="Неверный формат платежа")
|
||||
return
|
||||
|
||||
payment_id = int(pre_checkout_query.invoice_payload.split("_")[2])
|
||||
|
||||
star_payment = await db.get_star_payment_by_id(payment_id)
|
||||
|
||||
if not star_payment:
|
||||
await pre_checkout_query.answer(ok=False, error_message="Платеж не найден")
|
||||
return
|
||||
|
||||
if star_payment.status != 'pending':
|
||||
await pre_checkout_query.answer(ok=False, error_message="Платеж уже обработан")
|
||||
return
|
||||
|
||||
if pre_checkout_query.total_amount != star_payment.stars_amount:
|
||||
await pre_checkout_query.answer(ok=False, error_message="Неверная сумма платежа")
|
||||
return
|
||||
|
||||
await pre_checkout_query.answer(ok=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in pre_checkout_query_handler: {e}")
|
||||
await pre_checkout_query.answer(ok=False, error_message="Внутренняя ошибка")
|
||||
|
||||
@stars_router.message(F.successful_payment)
|
||||
async def successful_payment_handler(message: Message, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
bot = kwargs.get('bot')
|
||||
|
||||
if not user:
|
||||
logger.error("User not found in successful payment handler")
|
||||
return
|
||||
|
||||
try:
|
||||
payment_info = message.successful_payment
|
||||
|
||||
if not payment_info.invoice_payload.startswith("star_payment_"):
|
||||
logger.error(f"Invalid payment payload: {payment_info.invoice_payload}")
|
||||
return
|
||||
|
||||
payment_id = int(payment_info.invoice_payload.split("_")[2])
|
||||
|
||||
success = await db.complete_star_payment(
|
||||
payment_id=payment_id,
|
||||
telegram_payment_charge_id=payment_info.telegram_payment_charge_id
|
||||
)
|
||||
|
||||
if success:
|
||||
star_payment = await db.get_star_payment_by_id(payment_id)
|
||||
|
||||
if star_payment:
|
||||
updated_user = await db.get_user_by_telegram_id(user.telegram_id)
|
||||
|
||||
success_text = "✅ **Платеж успешно обработан!**\n\n"
|
||||
success_text += f"⭐ Оплачено звезд: {star_payment.stars_amount}\n"
|
||||
success_text += f"💰 Зачислено: {star_payment.rub_amount:.0f}₽\n"
|
||||
success_text += f"💳 Текущий баланс: {updated_user.balance:.0f}₽\n\n"
|
||||
success_text += "🎉 Средства уже доступны на вашем балансе!"
|
||||
|
||||
await message.answer(
|
||||
success_text,
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
if bot:
|
||||
try:
|
||||
regular_payment = await db.create_payment(
|
||||
user_id=user.telegram_id,
|
||||
amount=star_payment.rub_amount,
|
||||
payment_type='stars',
|
||||
description=f'Пополнение через Telegram Stars ({star_payment.stars_amount} ⭐)',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
await process_referral_rewards(
|
||||
user.telegram_id,
|
||||
star_payment.rub_amount,
|
||||
regular_payment.id,
|
||||
db,
|
||||
bot,
|
||||
payment_type='stars'
|
||||
)
|
||||
except Exception as ref_error:
|
||||
logger.error(f"Error processing referral rewards for stars payment: {ref_error}")
|
||||
|
||||
log_user_action(user.telegram_id, "stars_payment_completed",
|
||||
f"Stars: {star_payment.stars_amount}, Amount: {star_payment.rub_amount}")
|
||||
else:
|
||||
await message.answer("❌ Ошибка получения данных платежа")
|
||||
else:
|
||||
await message.answer("❌ Ошибка обработки платежа. Обратитесь в поддержку.")
|
||||
logger.error(f"Failed to complete star payment {payment_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in successful_payment_handler: {e}")
|
||||
await message.answer("❌ Произошла ошибка при обработке платежа. Обратитесь в поддержку.")
|
||||
|
||||
def get_stars_rate_info(stars_rates: Dict[int, float], lang: str = 'ru') -> str:
|
||||
if not stars_rates:
|
||||
return "Курсы не настроены"
|
||||
|
||||
text = "⭐ **Курсы Telegram Stars:**\n\n"
|
||||
|
||||
sorted_rates = sorted(stars_rates.items())
|
||||
for stars, rubles in sorted_rates:
|
||||
rate_per_star = rubles / stars
|
||||
text += f"• {stars} ⭐ = {rubles:.0f}₽ (курс: {rate_per_star:.2f}₽ за ⭐)\n"
|
||||
|
||||
return text
|
||||
|
||||
@stars_router.callback_query(F.data.startswith("cancel_star_payment_"))
|
||||
async def cancel_star_payment_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
bot = kwargs.get('bot')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
payment_id = int(callback.data.split("_")[-1])
|
||||
|
||||
star_payment = await db.get_star_payment_by_id(payment_id)
|
||||
|
||||
if not star_payment:
|
||||
await callback.answer("❌ Платеж не найден")
|
||||
return
|
||||
|
||||
if star_payment.user_id != user.telegram_id:
|
||||
await callback.answer("❌ Нет доступа к этому платежу")
|
||||
return
|
||||
|
||||
if star_payment.status != 'pending':
|
||||
await callback.answer("❌ Платеж уже обработан")
|
||||
return
|
||||
|
||||
success = await db.cancel_star_payment(payment_id)
|
||||
|
||||
if success:
|
||||
await callback.answer("✅ Платеж отменен", show_alert=True)
|
||||
|
||||
if bot:
|
||||
invoice_deleted = False
|
||||
try:
|
||||
await bot.delete_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
message_id=callback.message.message_id
|
||||
)
|
||||
invoice_deleted = True
|
||||
logger.info(f"Deleted invoice message {callback.message.message_id} for cancelled payment {payment_id}")
|
||||
except TelegramBadRequest as e:
|
||||
logger.warning(f"Failed to delete invoice message: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error deleting invoice message: {e}")
|
||||
|
||||
if invoice_deleted:
|
||||
try:
|
||||
if config and config.STARS_RATES:
|
||||
text = "❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\n"
|
||||
text += "🚀 **Преимущества:**\n"
|
||||
text += "• Мгновенное зачисление\n"
|
||||
text += "• Безопасные платежи через Telegram\n"
|
||||
text += "• Без комиссий и скрытых платежей\n\n"
|
||||
text += "💎 **Доступные варианты:**\n"
|
||||
|
||||
sorted_rates = sorted(config.STARS_RATES.items())
|
||||
for stars, rubles in sorted_rates:
|
||||
if stars >= 500:
|
||||
bonus_text = " 🔥 Выгодно!"
|
||||
elif stars >= 250:
|
||||
bonus_text = " 💎 Хорошо!"
|
||||
else:
|
||||
bonus_text = ""
|
||||
text += f"• {stars} ⭐ → {rubles:.0f}₽{bonus_text}\n"
|
||||
|
||||
text += f"\n💡 Выберите другой вариант:"
|
||||
|
||||
await bot.send_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
text=text,
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
else:
|
||||
await bot.send_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
text="❌ Платеж отменен\n\n💰 Управление балансом:",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send menu after invoice deletion: {e}")
|
||||
else:
|
||||
try:
|
||||
if config and config.STARS_RATES:
|
||||
await callback.message.edit_text(
|
||||
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Платеж отменен",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
try:
|
||||
if config and config.STARS_RATES:
|
||||
await bot.send_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
text="❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
else:
|
||||
await bot.send_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
text="❌ Платеж отменен",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send fallback message: {e}")
|
||||
else:
|
||||
try:
|
||||
if config and config.STARS_RATES:
|
||||
await callback.message.edit_text(
|
||||
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
else:
|
||||
await callback.message.edit_text(
|
||||
"❌ Платеж отменен",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
logger.warning("Failed to edit message when bot instance not available")
|
||||
|
||||
log_user_action(user.telegram_id, "stars_payment_cancelled", f"Payment ID: {payment_id}")
|
||||
else:
|
||||
await callback.answer("❌ Ошибка отмены платежа")
|
||||
|
||||
except (ValueError, IndexError) as e:
|
||||
logger.error(f"Error parsing payment ID from callback: {e}")
|
||||
await callback.answer("❌ Ошибка обработки")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in cancel_star_payment_callback: {e}")
|
||||
await callback.answer("❌ Произошла ошибка")
|
||||
@@ -1,7 +1,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Dict, Any
|
||||
from database import Database
|
||||
from remnawave_api import RemnaWaveAPI
|
||||
|
||||
@@ -147,7 +147,192 @@ class SubscriptionMonitorService:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error in daily loop: {e}", exc_info=True)
|
||||
await asyncio.sleep(3600) # Wait 1 hour before retry
|
||||
await asyncio.sleep(3600)
|
||||
|
||||
async def delete_expired_trial_subscriptions(self, force: bool = False) -> Dict[str, Any]:
|
||||
try:
|
||||
logger.info("🗑️ Starting deletion of expired trial subscriptions...")
|
||||
|
||||
now_utc = datetime.utcnow()
|
||||
delete_threshold_days = getattr(self.config, 'DELETE_EXPIRED_TRIAL_DAYS', 1)
|
||||
cutoff_date = now_utc - timedelta(days=delete_threshold_days)
|
||||
|
||||
logger.info(f"🗑️ Deleting trial subscriptions expired before: {cutoff_date} (older than {delete_threshold_days} days)")
|
||||
|
||||
results = {
|
||||
'total_checked': 0,
|
||||
'deleted_from_db': 0,
|
||||
'deleted_from_api': 0,
|
||||
'errors': [],
|
||||
'deleted_subscriptions': []
|
||||
}
|
||||
|
||||
all_users = await self.db.get_all_users()
|
||||
|
||||
for user in all_users:
|
||||
try:
|
||||
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
|
||||
|
||||
for user_sub in user_subs:
|
||||
results['total_checked'] += 1
|
||||
|
||||
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription or not subscription.is_trial:
|
||||
continue
|
||||
|
||||
expires_at_utc = user_sub.expires_at
|
||||
if expires_at_utc.tzinfo is None:
|
||||
expires_at_utc = expires_at_utc.replace(tzinfo=None)
|
||||
else:
|
||||
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
if expires_at_utc > cutoff_date and not force:
|
||||
continue
|
||||
|
||||
logger.info(f"🗑️ Deleting expired trial subscription '{subscription.name}' for user {user.telegram_id} "
|
||||
f"(expired: {expires_at_utc}, cutoff: {cutoff_date})")
|
||||
|
||||
api_deleted = False
|
||||
if self.api and user_sub.short_uuid:
|
||||
try:
|
||||
api_result = await self.api.delete_user_by_short_uuid(user_sub.short_uuid)
|
||||
if api_result and api_result.get('success'):
|
||||
api_deleted = True
|
||||
results['deleted_from_api'] += 1
|
||||
logger.info(f"✅ Deleted from RemnaWave API: {user_sub.short_uuid}")
|
||||
else:
|
||||
results['errors'].append(f"Failed to delete {user_sub.short_uuid} from API")
|
||||
logger.warning(f"⚠️ Failed to delete {user_sub.short_uuid} from API")
|
||||
except Exception as api_error:
|
||||
results['errors'].append(f"API error for {user_sub.short_uuid}: {str(api_error)}")
|
||||
logger.error(f"❌ API error deleting {user_sub.short_uuid}: {api_error}")
|
||||
|
||||
db_deleted = await self.db.delete_user_subscription(user_sub.id)
|
||||
if db_deleted:
|
||||
results['deleted_from_db'] += 1
|
||||
results['deleted_subscriptions'].append({
|
||||
'user_id': user.telegram_id,
|
||||
'subscription_name': subscription.name,
|
||||
'short_uuid': user_sub.short_uuid,
|
||||
'expired_at': expires_at_utc.isoformat(),
|
||||
'deleted_from_api': api_deleted,
|
||||
'deleted_from_db': True
|
||||
})
|
||||
logger.info(f"✅ Deleted from database: subscription ID {user_sub.id}")
|
||||
else:
|
||||
results['errors'].append(f"Failed to delete subscription ID {user_sub.id} from database")
|
||||
logger.error(f"❌ Failed to delete subscription ID {user_sub.id} from database")
|
||||
|
||||
except Exception as user_error:
|
||||
results['errors'].append(f"Error processing user {user.telegram_id}: {str(user_error)}")
|
||||
logger.error(f"❌ Error processing user {user.telegram_id}: {user_error}")
|
||||
|
||||
logger.info(f"🗑️ Trial deletion completed: {results['deleted_from_db']} from DB, {results['deleted_from_api']} from API")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error in delete_expired_trial_subscriptions: {e}", exc_info=True)
|
||||
return {
|
||||
'total_checked': 0,
|
||||
'deleted_from_db': 0,
|
||||
'deleted_from_api': 0,
|
||||
'errors': [f"Critical error: {str(e)}"],
|
||||
'deleted_subscriptions': []
|
||||
}
|
||||
|
||||
async def delete_expired_regular_subscriptions(self, force: bool = False) -> Dict[str, Any]:
|
||||
try:
|
||||
logger.info("🗑️ Starting deletion of expired regular subscriptions...")
|
||||
|
||||
now_utc = datetime.utcnow()
|
||||
delete_threshold_days = getattr(self.config, 'DELETE_EXPIRED_REGULAR_DAYS', 7)
|
||||
cutoff_date = now_utc - timedelta(days=delete_threshold_days)
|
||||
|
||||
logger.info(f"🗑️ Deleting regular subscriptions expired before: {cutoff_date} (older than {delete_threshold_days} days)")
|
||||
|
||||
results = {
|
||||
'total_checked': 0,
|
||||
'deleted_from_db': 0,
|
||||
'deleted_from_api': 0,
|
||||
'errors': [],
|
||||
'deleted_subscriptions': []
|
||||
}
|
||||
|
||||
all_users = await self.db.get_all_users()
|
||||
|
||||
for user in all_users:
|
||||
try:
|
||||
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
|
||||
|
||||
for user_sub in user_subs:
|
||||
results['total_checked'] += 1
|
||||
|
||||
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription or subscription.is_trial:
|
||||
continue
|
||||
|
||||
if getattr(subscription, 'is_imported', False) or subscription.name == "Старая подписка":
|
||||
continue
|
||||
|
||||
expires_at_utc = user_sub.expires_at
|
||||
if expires_at_utc.tzinfo is None:
|
||||
expires_at_utc = expires_at_utc.replace(tzinfo=None)
|
||||
else:
|
||||
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
if expires_at_utc > cutoff_date and not force:
|
||||
continue
|
||||
|
||||
logger.info(f"🗑️ Deleting expired regular subscription '{subscription.name}' for user {user.telegram_id} "
|
||||
f"(expired: {expires_at_utc}, cutoff: {cutoff_date})")
|
||||
|
||||
api_deleted = False
|
||||
if self.api and user_sub.short_uuid:
|
||||
try:
|
||||
api_result = await self.api.delete_user_by_short_uuid(user_sub.short_uuid)
|
||||
if api_result and api_result.get('success'):
|
||||
api_deleted = True
|
||||
results['deleted_from_api'] += 1
|
||||
logger.info(f"✅ Deleted from RemnaWave API: {user_sub.short_uuid}")
|
||||
else:
|
||||
results['errors'].append(f"Failed to delete {user_sub.short_uuid} from API")
|
||||
logger.warning(f"⚠️ Failed to delete {user_sub.short_uuid} from API")
|
||||
except Exception as api_error:
|
||||
results['errors'].append(f"API error for {user_sub.short_uuid}: {str(api_error)}")
|
||||
logger.error(f"❌ API error deleting {user_sub.short_uuid}: {api_error}")
|
||||
|
||||
db_deleted = await self.db.delete_user_subscription(user_sub.id)
|
||||
if db_deleted:
|
||||
results['deleted_from_db'] += 1
|
||||
results['deleted_subscriptions'].append({
|
||||
'user_id': user.telegram_id,
|
||||
'subscription_name': subscription.name,
|
||||
'short_uuid': user_sub.short_uuid,
|
||||
'expired_at': expires_at_utc.isoformat(),
|
||||
'deleted_from_api': api_deleted,
|
||||
'deleted_from_db': True
|
||||
})
|
||||
logger.info(f"✅ Deleted from database: subscription ID {user_sub.id}")
|
||||
else:
|
||||
results['errors'].append(f"Failed to delete subscription ID {user_sub.id} from database")
|
||||
logger.error(f"❌ Failed to delete subscription ID {user_sub.id} from database")
|
||||
|
||||
except Exception as user_error:
|
||||
results['errors'].append(f"Error processing user {user.telegram_id}: {str(user_error)}")
|
||||
logger.error(f"❌ Error processing user {user.telegram_id}: {user_error}")
|
||||
|
||||
logger.info(f"🗑️ Regular deletion completed: {results['deleted_from_db']} from DB, {results['deleted_from_api']} from API")
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error in delete_expired_regular_subscriptions: {e}", exc_info=True)
|
||||
return {
|
||||
'total_checked': 0,
|
||||
'deleted_from_db': 0,
|
||||
'deleted_from_api': 0,
|
||||
'errors': [f"Critical error: {str(e)}"],
|
||||
'deleted_subscriptions': []
|
||||
}
|
||||
|
||||
async def _check_expiring_subscriptions(self):
|
||||
try:
|
||||
@@ -336,11 +521,25 @@ class SubscriptionMonitorService:
|
||||
deactivated_count = await self.deactivate_expired_subscriptions()
|
||||
logger.info(f"🔄 Deactivated {deactivated_count} expired subscriptions")
|
||||
|
||||
deleted_trials = 0
|
||||
deleted_regular = 0
|
||||
if getattr(self.config, 'AUTO_DELETE_ENABLED', False):
|
||||
logger.info("🗑️ Auto-deletion enabled, deleting expired subscriptions...")
|
||||
|
||||
trial_result = await self.delete_expired_trial_subscriptions()
|
||||
deleted_trials = trial_result.get('deleted_from_db', 0)
|
||||
|
||||
regular_result = await self.delete_expired_regular_subscriptions()
|
||||
deleted_regular = regular_result.get('deleted_from_db', 0)
|
||||
|
||||
logger.info(f"🗑️ Auto-deleted {deleted_trials} trial and {deleted_regular} regular subscriptions")
|
||||
|
||||
logger.info("📩 Sending final expiry notifications...")
|
||||
await self._send_final_expiry_notifications()
|
||||
logger.info("📩 Final notifications sent")
|
||||
|
||||
logger.info(f"✅ Daily check completed successfully. Warnings: {warnings_sent}, Deactivated: {deactivated_count}")
|
||||
logger.info(f"✅ Daily check completed successfully. Warnings: {warnings_sent}, Deactivated: {deactivated_count}, "
|
||||
f"Deleted trials: {deleted_trials}, Deleted regular: {deleted_regular}")
|
||||
return deactivated_count
|
||||
|
||||
except Exception as e:
|
||||
@@ -589,6 +788,9 @@ class SubscriptionMonitorService:
|
||||
'check_interval': self.config.MONITOR_CHECK_INTERVAL,
|
||||
'daily_check_hour': self.config.MONITOR_DAILY_CHECK_HOUR,
|
||||
'warning_days': self.config.MONITOR_WARNING_DAYS,
|
||||
'delete_trial_days': getattr(self.config, 'DELETE_EXPIRED_TRIAL_DAYS', 1),
|
||||
'delete_regular_days': getattr(self.config, 'DELETE_EXPIRED_REGULAR_DAYS', 7),
|
||||
'auto_delete_enabled': getattr(self.config, 'AUTO_DELETE_ENABLED', False),
|
||||
'last_check': self.last_check_time.strftime("%Y-%m-%d %H:%M:%S") if self.last_check_time else None,
|
||||
'has_tasks': {
|
||||
'monitor_task': monitor_running,
|
||||
|
||||
@@ -11,9 +11,9 @@ TRANSLATIONS = {
|
||||
'admin_panel': 'Админ панель',
|
||||
'trial_subscription': '🆓 Тестовая подписка',
|
||||
'trial_not_available': '❌ Тестовая подписка недоступна',
|
||||
'trial_success': '🎉 Тестовая подписка успешно активирована!\n\nТеперь вы можете найти её в разделе "Мои подписки".',
|
||||
'trial_success': '🎉 Тестовая подписка успешно активирована!\n\nТеперь вы можете найти её в разделе: \n\n"Мои подписки"',
|
||||
'trial_error': '❌ Ошибка при создании тестовой подписки',
|
||||
'trial_info': '🧪 Тестовая подписка выдается на три дня!\n\nНа тарифе действует ограничение в 3 дня\n\nОграничение по трафику - 2гб',
|
||||
'trial_info': 'Попробуйте наш сервис бесплатно! \n\n🧪 Тестовая подписка выдается на {days} дня!\n\nОграничение по трафику - {traffic} гб',
|
||||
'subscriptions_list': '📋 Список подписок в продаже:',
|
||||
'your_balance': '💰 Ваш баланс: {balance:.2f} руб.',
|
||||
'topup_balance': 'Пополнить баланс',
|
||||
|
||||
@@ -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
|
||||
|
||||