Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| a6734e542d | |||
| 2287a67dfd | |||
| 31dbaa80d7 | |||
| b1846a3ea9 | |||
| e1a7e9e622 | |||
| 0ae48faedb | |||
| b8122222f8 | |||
| 5e5a39c22e | |||
| 1e7cf214e9 | |||
| d7e7a2df9a | |||
| 584aa35894 | |||
| 652ae8057a | |||
| 0bc0a1354a | |||
| 7d30838813 | |||
| 3da178ed02 | |||
| e6ac3e3812 | |||
| 62087e20b5 | |||
| b7a47c6eb6 | |||
| 9b14baca51 | |||
| eb1a75c9f5 | |||
| 5e79106134 | |||
| ee45ec356e | |||
| 9275bff828 | |||
| 9150c70618 | |||
| b792e749d6 | |||
| 9871572740 | |||
| 52c1cb3cbc | |||
| d5b2b371c8 | |||
| d5cb022b02 | |||
| 021f62fd30 | |||
| 684e398798 | |||
| 12b64e9978 | |||
| 273df23f20 | |||
| a9b0dbf3eb | |||
| 5f56e068d3 | |||
| 92b82132d8 | |||
| 79ae9d3a1c | |||
| 86714a93f9 | |||
| 553f6f8cf6 | |||
| b561f67c12 | |||
| d03f9b80c7 |
@@ -1,29 +1,61 @@
|
||||
# Bot Configuration
|
||||
BOT_TOKEN=
|
||||
|
||||
# настройки реф программы юзернейм без @!
|
||||
BOT_USERNAME=
|
||||
REFERRAL_FIRST_REWARD=100.0
|
||||
REFERRAL_REFERRED_BONUS=100.0
|
||||
REFERRAL_THRESHOLD=200.0
|
||||
REFERRAL_PERCENTAGE=0.2
|
||||
|
||||
# RemnaWave API Configuration
|
||||
REMNAWAVE_URL=https://example.com
|
||||
REMNAWAVE_MODE=remote
|
||||
REMNAWAVE_TOKEN=
|
||||
SUBSCRIPTION_BASE_URL=https://example.com
|
||||
|
||||
# Admin Configuration (comma-separated list of Telegram user IDs)
|
||||
ADMIN_IDS=123445567,12342123
|
||||
|
||||
# Support Configuration
|
||||
DEFAULT_LANGUAGE=ru
|
||||
SUPPORT_USERNAME=support
|
||||
|
||||
# Database Configuration (optional, defaults to SQLite)
|
||||
# DATABASE_URL=sqlite+aiosqlite:///bot.db
|
||||
|
||||
TRIAL_ENABLED=true
|
||||
TRIAL_DURATION_DAYS=3
|
||||
TRIAL_TRAFFIC_GB=2
|
||||
TRIAL_SQUAD_UUID=
|
||||
TRIAL_PRICE=0.0
|
||||
TRIAL_ENABLED=true # Включить триал
|
||||
TRIAL_DURATION_DAYS=3 # Дней триала
|
||||
TRIAL_TRAFFIC_GB=2 # Лимит трафика у триал подписки
|
||||
TRIAL_SQUAD_UUID= # UUID сквада из панели remnawave /dashboard/management/internal-squads
|
||||
TRIAL_PRICE=0.0 # Оставить 0!
|
||||
|
||||
# Monitor Service Settings (дополнительные настройки)
|
||||
MONITOR_CHECK_INTERVAL=3600
|
||||
MONITOR_DAILY_CHECK_HOUR=10
|
||||
MONITOR_WARNING_DAYS=2
|
||||
MONITOR_CHECK_INTERVAL=21600 # Промежуток проверики (3600 - будет раз в час проверять и слать уведомления)
|
||||
MONITOR_DAILY_CHECK_HOUR=10 # Ежедневный чек в 10 утра
|
||||
MONITOR_WARNING_DAYS=2 # За сколько начинать до окончания подписки слать уведомления
|
||||
DELETE_EXPIRED_TRIAL_DAYS=1 # Через сколько дней после истечения удалять триал подписки
|
||||
DELETE_EXPIRED_REGULAR_DAYS=7 # Через сколько дней после истечения удалять обычные подписки
|
||||
AUTO_DELETE_ENABLED=true # Включить автоматическое удаление при ежедневной проверке
|
||||
|
||||
LUCKY_GAME_ENABLED=true # Включить/выключить игру удачи
|
||||
LUCKY_GAME_REWARD=50.0 # Размер награды за выигрыш в рублях
|
||||
LUCKY_GAME_NUMBERS=30 # Общее количество чисел для выбора (1-30)
|
||||
LUCKY_GAME_WINNING_COUNT=25 # Количество выигрышных чисел из общего количества
|
||||
|
||||
# ================================
|
||||
# TELEGRAM STARS НАСТРОЙКИ
|
||||
# ================================
|
||||
# Включить/выключить пополнение через Telegram Stars
|
||||
STARS_ENABLED=true
|
||||
# Курсы обмена звезд на рубли
|
||||
# Формат: STARS_{количество_звезд}_RATE={рубли}
|
||||
# 100 звёзд
|
||||
STARS_100_RATE=110
|
||||
# 150 звёзд
|
||||
STARS_150_RATE=165
|
||||
# 250 звёзд
|
||||
STARS_250_RATE=280
|
||||
# 350 звёзд
|
||||
STARS_350_RATE=355
|
||||
# 500 звёзд
|
||||
STARS_500_RATE=550
|
||||
# 750 звёзд
|
||||
STARS_750_RATE=800
|
||||
# 1000 звёзд
|
||||
STARS_1000_RATE=1000
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/screens
|
||||
/assets
|
||||
@@ -1,234 +1,712 @@
|
||||
<img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 15 13" src="https://github.com/user-attachments/assets/91098622-1bce-4f27-afef-60a3c5b5061f" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 22" src="https://github.com/user-attachments/assets/46b87e75-b420-4ac6-91b9-8c7e9bcffb2a" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 39" src="https://github.com/user-attachments/assets/ca97811f-ca00-4133-a120-1c11f0efa0fc" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 45" src="https://github.com/user-attachments/assets/258e1adb-2c39-4126-82a7-7791b56d42db" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 53" src="https://github.com/user-attachments/assets/073455fc-f42d-4d70-839d-59042add2d94" /><img width="906" height="316" alt="Снимок экрана 2025-08-05 в 03 16 00" src="https://github.com/user-attachments/assets/2034dde8-a48b-4149-a23f-b788aa40e0b1" /><img width="894" height="317" alt="Снимок экрана 2025-08-05 в 15 32 32" src="https://github.com/user-attachments/assets/a96337cf-f58a-488e-9600-c94a92bdbfc2" /><img width="906" height="366" alt="Снимок экрана 2025-08-05 в 03 16 18" src="https://github.com/user-attachments/assets/3a3d1e0a-92fc-4573-a48c-f36481d6d0de" /><img width="906" height="842" alt="Снимок экрана 2025-08-05 в 03 17 24" src="https://github.com/user-attachments/assets/8b407f69-6861-4810-822e-c3f7b8f63629" /><img width="906" height="274" alt="Снимок экрана 2025-08-05 в 03 17 43" src="https://github.com/user-attachments/assets/923a945a-5ef8-4dcb-9804-fffc37ab8887" /><img width="936" height="364" alt="Снимок экрана 2025-08-05 в 03 20 03" src="https://github.com/user-attachments/assets/1faecdfe-f80c-4ac2-ad38-81a30fc6623d" /><img width="892" height="486" alt="Снимок экрана 2025-08-07 в 07 43 47" src="https://github.com/user-attachments/assets/0dd6cb8e-fd2f-4a98-8920-aadceee09fd0" /><img width="892" height="762" alt="Снимок экрана 2025-08-07 в 07 44 20" src="https://github.com/user-attachments/assets/d7c95e3e-cf04-40bc-9422-d7289447625d" /><img width="892" height="823" alt="Снимок экрана 2025-08-07 в 07 46 45" src="https://github.com/user-attachments/assets/9ab2c378-0abc-447d-9e95-a3ab8dab2f18" />
|
||||
<img width="892" height="501" alt="Снимок экрана 2025-08-07 в 07 42 07" src="https://github.com/user-attachments/assets/839c02da-4461-4127-894a-772e66175e23" /><img width="892" height="805" alt="Снимок экрана 2025-08-07 в 07 57 01" src="https://github.com/user-attachments/assets/bc35f79d-0b0d-4c81-8623-696b708642ad" /><img width="892" height="834" alt="Снимок экрана 2025-08-07 в 07 41 09" src="https://github.com/user-attachments/assets/d4731a79-0171-4254-aa78-2e4c7305829b" /><img width="631" height="606" alt="Снимок экрана 2025-08-06 в 18 48 31" src="https://github.com/user-attachments/assets/c44548b0-f27b-4f67-b3c0-2c002ae33979" />
|
||||
# 🤖 RemnaWave Telegram Bot
|
||||
|
||||
[](https://hub.docker.com/r/fr1ngg/remnawave-bedolaga-telegram-bot)
|
||||
[](https://python.org)
|
||||
[](https://postgresql.org)
|
||||
[](LICENSE)
|
||||
|
||||

|
||||
|
||||
Современный Telegram бот для управления VPN подписками через RemnaWave API с полным функционалом управления пользователями, платежами и администрированием.
|
||||
|
||||
## 🚀 Особенности
|
||||
|
||||
### 👤 Для пользователей
|
||||
- 💰 **Управление балансом** - пополнение через поддержку
|
||||
- 🛒 **Покупка подписок** - различные тарифные планы c возможностью индивидуальной настройки сквада у плана
|
||||
- 📱 **Управление подписками** - просмотр, продление, получение ссылок
|
||||
- 🎁 **Система промокодов** - денежные коды
|
||||
- 👥 **Реферальная программа** - зарабатывай с друзей
|
||||
- 🎰 **Игра удачи** - выигрывай бонусы каждые 24 часа
|
||||
- 🆓 **Тестовая подписка** - бесплатная пробная версия с детальной конфигурацией
|
||||
- 🌐 **Мультиязычность** - русский и английский
|
||||
|
||||
### ⚙️ Для администраторов
|
||||
- 📊 **Полная статистика** - пользователи, платежи, подписки
|
||||
- 👥 **Управление пользователями** - поиск, редактирование, баланс
|
||||
- 💳 **Управление платежами** - одобрение, отклонение, история
|
||||
- 🎫 **Управление промокодами** - создание, редактирование, статистика
|
||||
- 🖥 **Мониторинг системы** - состояние нод, пользователей RemnaWave
|
||||
- 🔄 **Синхронизация данных** - импорт из RemnaWave
|
||||
- 📨 **Рассылки** - уведомления пользователям
|
||||
- 🔍 **Мониторинг подписок** - автоматические уведомления об истечении и зачистка подписок с истекщим сроком для триал и обычных подписок
|
||||
|
||||
## 📋 Требования
|
||||
|
||||
#Описание
|
||||
### Системные требования
|
||||
- Docker и Docker Compose
|
||||
- 1+ GB RAM
|
||||
- 10+ GB свободного места
|
||||
|
||||
### Внешние сервисы
|
||||
- **Telegram Bot Token** - создайте бота через [@BotFather](https://t.me/BotFather)
|
||||
- **RemnaWave API** - доступ к панели RemnaWave
|
||||
- **PostgreSQL** - автоматически разворачивается через Docker
|
||||
|
||||
## 🛠 Установка и запуск
|
||||
|
||||
### 1. Клонирование репозитория
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot.git
|
||||
cd remnawave-bedolaga-telegram-bot
|
||||
```
|
||||
|
||||
### 2. Настройка переменных окружения
|
||||
|
||||
Создайте файл `.env` на основе примера:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
```
|
||||
|
||||
### 3. Конфигурация
|
||||
|
||||
#### 📋 Основные настройки
|
||||
|
||||
| Переменная | Описание | Пример |
|
||||
|------------|----------|---------|
|
||||
| `BOT_TOKEN` | Токен Telegram бота | `123456:ABC-DEF1234ghIkl-zyx` |
|
||||
| `BOT_USERNAME` | Username бота (без @) | `your_bot_username` |
|
||||
| `REMNAWAVE_URL` | URL панели RemnaWave | `https://panel.example.com` |
|
||||
| `REMNAWAVE_MODE` | Тип подключения | `remote/local` |
|
||||
| `REMNAWAVE_TOKEN` | API токен RemnaWave | `your_api_token` |
|
||||
| `ADMIN_IDS` | ID администраторов (через запятую) | `123456789,987654321` |
|
||||
|
||||
#### 🎁 Реферальная программа
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|------------|----------|--------------|
|
||||
| `REFERRAL_FIRST_REWARD` | Награда за первого реферала | `100.0` |
|
||||
| `REFERRAL_REFERRED_BONUS` | Бонус приглашенному | `100.0` |
|
||||
| `REFERRAL_THRESHOLD` | Порог активации (руб.) | `200.0` |
|
||||
| `REFERRAL_PERCENTAGE` | Процент со второго и послед. платежей | `0.2` (20%) |
|
||||
|
||||
#### 🎰 Игра удачи
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|------------|----------|--------------|
|
||||
| `LUCKY_GAME_ENABLED` | Включить игру | `true` |
|
||||
| `LUCKY_GAME_REWARD` | Награда за выигрыш | `50.0` |
|
||||
| `LUCKY_GAME_NUMBERS` | Всего чисел | `30` |
|
||||
| `LUCKY_GAME_WINNING_COUNT` | Выигрышных чисел | `3` |
|
||||
|
||||
#### 🆓 Тестовая подписка
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|------------|----------|--------------|
|
||||
| `TRIAL_ENABLED` | Включить тестовую подписку | `true/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 конфигурация
|
||||
|
||||
#### 🚀 Минимальная конфигурация (рекомендуется)
|
||||
|
||||
Создайте файл `docker-compose.yml` для базового запуска:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: remnawave_bot_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: remnawave_bot
|
||||
POSTGRES_USER: remnawave_user
|
||||
POSTGRES_PASSWORD: secure_password_123
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
networks:
|
||||
- bot_network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U remnawave_user -d remnawave_bot"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
# RemnaWave Bot
|
||||
bot:
|
||||
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
|
||||
container_name: remnawave_bot
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
DATABASE_URL: postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./data:/app/data
|
||||
networks:
|
||||
- bot_network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "python -c 'print(\"Bot is running\")'"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
bot_network:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
#### ⚡ Полная конфигурация (с дополнительными сервисами)
|
||||
|
||||
Для расширенной настройки с Redis и Nginx создайте `docker-compose.full.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
image: postgres:15-alpine
|
||||
container_name: remnawave_bot_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: remnawave_bot
|
||||
POSTGRES_USER: remnawave_user
|
||||
POSTGRES_PASSWORD: secure_password_123
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
networks:
|
||||
- bot_network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U remnawave_user -d remnawave_bot"]
|
||||
interval: 15s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
# RemnaWave Bot
|
||||
bot:
|
||||
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
|
||||
container_name: remnawave_bot
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
DATABASE_URL: postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./data:/app/data
|
||||
networks:
|
||||
- bot_network
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "python -c 'print(\"Bot is running\")'"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
# Redis (для кэширования и улучшения производительности)
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: remnawave_bot_redis
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes --requirepass redis_password_123
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
ports:
|
||||
- "6379:6379"
|
||||
networks:
|
||||
- bot_network
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
profiles:
|
||||
- with-redis
|
||||
|
||||
# Nginx (для статических файлов или веб-интерфейса)
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: remnawave_bot_nginx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./ssl:/etc/nginx/ssl:ro
|
||||
- ./static:/usr/share/nginx/html:ro
|
||||
networks:
|
||||
- bot_network
|
||||
depends_on:
|
||||
- bot
|
||||
profiles:
|
||||
- with-nginx
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
bot_network:
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
```
|
||||
|
||||
### 5. Варианты запуска
|
||||
|
||||
#### 🚀 Быстрый старт (минимальная конфигурация)
|
||||
|
||||
```bash
|
||||
# Запуск только бота и базы данных
|
||||
docker-compose up -d
|
||||
|
||||
# Просмотр логов
|
||||
docker-compose logs -f bot
|
||||
```
|
||||
|
||||
#### ⚡ Расширенный запуск
|
||||
|
||||
```bash
|
||||
# Запуск с Redis для улучшения производительности
|
||||
docker-compose --profile with-redis up -d
|
||||
|
||||
# Запуск с Nginx для веб-интерфейса
|
||||
docker-compose --profile with-nginx up -d
|
||||
|
||||
# Запуск со всеми сервисами
|
||||
docker-compose --profile with-redis --profile with-nginx up -d
|
||||
|
||||
# Или используйте полную конфигурацию
|
||||
docker-compose -f docker-compose.full.yml up -d
|
||||
```
|
||||
|
||||
#### 🛑 Управление сервисами
|
||||
|
||||
```bash
|
||||
# Остановка всех сервисов
|
||||
docker-compose down
|
||||
|
||||
# Остановка с удалением volumes (ВНИМАНИЕ: удалит все данные!)
|
||||
docker-compose down -v
|
||||
|
||||
# Перезапуск конкретного сервиса
|
||||
docker-compose restart bot
|
||||
|
||||
# Просмотр статуса
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
#### 📋 Варианты конфигураций
|
||||
|
||||
| Конфигурация | Команда | Описание |
|
||||
|--------------|---------|----------|
|
||||
| **Минимальная** | `docker-compose up -d` | Только бот + PostgreSQL |
|
||||
| **С Redis** | `docker-compose --profile with-redis up -d` | + кэширование |
|
||||
| **С Nginx** | `docker-compose --profile with-nginx up -d` | + веб-сервер |
|
||||
| **Полная** | `docker-compose --profile with-redis --profile with-nginx up -d` | Все сервисы |
|
||||
|
||||
RemnaWave Bedolaga Telegram Bot — это многофункциональный бот для управления подписками(Для каждой подписки возможно назначить свой сквад со своими инбаундами - нововведение Remnawave 2.0.0+), балансом, промокодами, тестовой подпиской и рассылками пользователям через Telegram.
|
||||
## 📊 Мониторинг и управление
|
||||
|
||||
Бот интегрирован с системой RemnaWave версии 2.0.8
|
||||
### Просмотр логов
|
||||
|
||||
#Основные возможности
|
||||
```bash
|
||||
# Логи бота
|
||||
docker-compose logs -f bot
|
||||
|
||||
Мультиязычный интерфейс (на данный момент русский и английский языки)
|
||||
# Логи базы данных
|
||||
docker-compose logs -f postgres
|
||||
|
||||
Создание и покупка подписок с управлением трафиком, длительностью и ценой
|
||||
# Все логи
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
Бесплатная тестовая подписка с заданными ограничениями(срок, лимит трафика, назначение сквада)
|
||||
### Проверка состояния
|
||||
|
||||
Пополнение баланса: 1) Через саппорт в ручную 2) Отправка заявки с суммой админу (С возможность подтвердить/отклонить заявку)
|
||||
```bash
|
||||
# Статус контейнеров
|
||||
docker-compose ps
|
||||
|
||||
Управление балансом пользователей (пополнение, списание)
|
||||
# Использование ресурсов
|
||||
docker stats
|
||||
```
|
||||
|
||||
Промокоды со скидками и ограничениями по использованию
|
||||
### Backup базы данных
|
||||
|
||||
Полноценная админ-панель с контролем пользователей, созданием подписок(Подтягивает UUID сквада из панели), платежей и статистикой
|
||||
```bash
|
||||
# Создание backup
|
||||
docker exec remnawave_bot_db pg_dump -U remnawave_user remnawave_bot > backup.sql
|
||||
|
||||
Рассылка сообщений отдельным пользователям и всем сразу
|
||||
# Восстановление
|
||||
docker exec -i remnawave_bot_db psql -U remnawave_user remnawave_bot < backup.sql
|
||||
```
|
||||
|
||||
Сервис контроля истечения сроков действия подписки(Уведомляет об истечении за указанный в настройках срок), уведомления с предложением продления подписи. (NEW)
|
||||
## 🔧 Обновление
|
||||
|
||||
Интеграция с RemnaWave API для управления подписками и пользователями RemnaWave
|
||||
### Обновление Docker образа
|
||||
|
||||
Полная синхранизация Remnawave <--> Bot - Перенос подписок из панели Remnawave в бот по Telegram id
|
||||
```bash
|
||||
# Остановить сервисы
|
||||
docker-compose down
|
||||
|
||||
Управление системой Remnawave (NEW)
|
||||
# Обновить образ
|
||||
docker-compose pull bot
|
||||
|
||||
Управление платежами (подтверждение, отклонение) + История платежей(Все действия с балансом и подписками в постраничной истории)
|
||||
# Запустить снова
|
||||
docker-compose up -d
|
||||
|
||||
# Проверить логи
|
||||
docker-compose logs -f bot
|
||||
```
|
||||
|
||||
#Требования
|
||||
### Ручная сборка образа
|
||||
|
||||
Pip
|
||||
```bash
|
||||
# Клонировать репозиторий
|
||||
git clone https://github.com/your-repo/remnawave-telegram-bot.git
|
||||
cd remnawave-telegram-bot
|
||||
|
||||
Python 3.8+
|
||||
# Собрать образ
|
||||
docker build -t remnawave-bot .
|
||||
|
||||
PostgreSQL, SQLite или другая поддерживаемая SQL-база данных
|
||||
# Изменить docker-compose.yml
|
||||
# bot:
|
||||
# image: remnawave-bot # вместо fr1ngg/remnawave-bedolaga-telegram-bot
|
||||
```
|
||||
|
||||
Токен Telegram-бота
|
||||
## 🎯 Использование
|
||||
|
||||
URL и токен RemnaWave API
|
||||
### Для пользователей
|
||||
|
||||
Ссылки на подписку из ремны формата SUB_PUBLIC_DOMAIN=sub.example.com/sub
|
||||
1. **Запуск бота** - найдите бота по username и нажмите `/start`
|
||||
2. **Выбор языка** - выберите русский или английский
|
||||
3. **Пополнение баланса** - через меню "💰 Баланс" → "💳 Пополнить"
|
||||
4. **Покупка подписки** - "🛒 Купить подписку" → выбор тарифа → оплата
|
||||
5. **Управление подписками** - "📋 Мои подписки" → выбор подписки
|
||||
6. **Реферальная программа** - "👥 Рефералы" → поделиться ссылкой
|
||||
|
||||
#Установка
|
||||
### Для администраторов
|
||||
|
||||
1. Клонируйте репозиторий:
|
||||
Администратор видит дополнительную кнопку "⚙️ Админ панель" с возможностями:
|
||||
|
||||
git clone https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot
|
||||
cd remnawave-bedolaga-telegram-bot
|
||||
- **📦 Управление подписками** - создание тарифов
|
||||
- **👥 Управление пользователями** - поиск, редактирование
|
||||
- **💰 Управление балансом** - пополнение балансов
|
||||
- **🎁 Управление промокодами** - создание скидок
|
||||
- **📨 Рассылки** - отправка сообщений
|
||||
- **🖥 Система RemnaWave** - мониторинг нод и синхронизация
|
||||
|
||||
2. Создайте файл .env в корне проекта и заполните его необходимыми переменными окружения. Пример:
|
||||
## 🔒 Безопасность
|
||||
|
||||
BOT_TOKEN=ваш_telegram_bot_token
|
||||
REMNAWAVE_URL=https://your-remnawave-url.ru
|
||||
REMNAWAVE_MODE=remote/local
|
||||
REMNAWAVE_TOKEN=ваш_remnawave_token
|
||||
SUBSCRIPTION_BASE_URL=https://
|
||||
DATABASE_URL=sqlite+aiosqlite:///bot.db
|
||||
ADMIN_IDS=123456789,987654321
|
||||
DEFAULT_LANGUAGE=ru
|
||||
SUPPORT_USERNAME=support
|
||||
TRIAL_ENABLED=true
|
||||
TRIAL_DURATION_DAYS=3
|
||||
TRIAL_TRAFFIC_GB=2
|
||||
TRIAL_SQUAD_UUID=19bd5bde-5eea-4368-809c-6ba1ffb93897
|
||||
TRIAL_PRICE=0.0
|
||||
MONITOR_CHECK_INTERVAL=3600
|
||||
MONITOR_DAILY_CHECK_HOUR=10
|
||||
MONITOR_WARNING_DAYS=2
|
||||
### Рекомендации
|
||||
|
||||
|
||||
4. Соберите образ (Makefile Dockerfile docker-compose):
|
||||
- ✅ Измените пароли PostgreSQL в `docker-compose.yml`
|
||||
- ✅ Не публикуйте файл `.env` в репозитории
|
||||
- ✅ Используйте HTTPS для RemnaWave API
|
||||
- ✅ Регулярно создавайте backup базы данных
|
||||
- ✅ Ограничьте доступ к серверу через firewall
|
||||
|
||||
make build
|
||||
### Переменные безопасности
|
||||
|
||||
5. Запуск:
|
||||
```bash
|
||||
# Генерация безопасных паролей
|
||||
openssl rand -base64 32 # для PostgreSQL
|
||||
openssl rand -hex 16 # для Redis
|
||||
```
|
||||
|
||||
Запуск минимальной конфигурации (бот + база данных):
|
||||
|
||||
make up-min
|
||||
## 🐛 Устранение неполадок
|
||||
|
||||
Или запуск с Redis:
|
||||
|
||||
make up
|
||||
### Частые проблемы
|
||||
|
||||
Или запуск со всеми сервисами включая Nginx:
|
||||
|
||||
make up-full
|
||||
| Проблема | Решение |
|
||||
|----------|---------|
|
||||
| Бот не отвечает | Проверьте `BOT_TOKEN` и интернет |
|
||||
| Ошибка подключения к БД | Проверьте статус PostgreSQL контейнера |
|
||||
| Ошибки RemnaWave API | Проверьте `REMNAWAVE_URL` и `REMNAWAVE_TOKEN` |
|
||||
| Игра удачи не работает | Проверьте `LUCKY_GAME_ENABLED=true` |
|
||||
|
||||
5. Управление
|
||||
### Диагностика
|
||||
|
||||
Просмотр логов:
|
||||
```bash
|
||||
# Проверка переменных окружения
|
||||
docker exec remnawave_bot env | grep BOT_
|
||||
|
||||
make logs-bot
|
||||
# Подключение к базе данных
|
||||
docker exec -it remnawave_bot_db psql -U remnawave_user remnawave_bot
|
||||
|
||||
Статус сервисов:
|
||||
|
||||
make status
|
||||
# Перезапуск бота
|
||||
docker-compose restart bot
|
||||
```
|
||||
|
||||
Перезапуск:
|
||||
|
||||
make restart
|
||||
### Логи и отладка
|
||||
|
||||
Остановка:
|
||||
|
||||
make down
|
||||
|
||||
#Конфигурация
|
||||
```bash
|
||||
# Детальные логи
|
||||
docker-compose logs -f --tail 100 bot
|
||||
|
||||
BOT_TOKEN — токен Telegram бота от BotFather.
|
||||
# Ошибки базы данных
|
||||
docker-compose logs postgres | grep ERROR
|
||||
|
||||
REMNAWAVE_URL — URL API RemnaWave.
|
||||
# Проверка health checks
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
REMNAWAVE_MODE=remote
|
||||
## 📈 Производительность
|
||||
|
||||
REMNAWAVE_TOKEN — токен доступа к API RemnaWave.
|
||||
### Рекомендуемые ресурсы
|
||||
|
||||
DATABASE_URL — строка подключения к базе данных.
|
||||
| Пользователей | RAM | CPU | Диск |
|
||||
|---------------|-----|-----|------|
|
||||
| До 500 | 1GB | 1 CPU | 10GB |
|
||||
| До 1,000 | 2GB | 1 CPU | 20GB |
|
||||
| До 10,000 | 4GB | 2 CPU | 50GB |
|
||||
| До 50,000 | 8GB | 4 CPU | 100GB |
|
||||
|
||||
SUBSCRIPTION_BASE_URL=https://sub.example.com (без / на конце)
|
||||
### Оптимизация
|
||||
|
||||
ADMIN_IDS — через запятую Telegram ID администраторов.
|
||||
- Включите Redis для кэширования (профиль `with-redis`)
|
||||
- Настройте PostgreSQL для production
|
||||
- Используйте nginx как reverse proxy
|
||||
- Мониторьте ресурсы через `docker stats`
|
||||
|
||||
SUPPORT_USERNAME — ник поддержки, без @ указывать
|
||||
## 🤝 Поддержка
|
||||
|
||||
Параметры тестовой подписки (включение, длительность, трафик, UUID squad, цена).
|
||||
### Получить помощь
|
||||
|
||||
TRIAL_ENABLED=true/false
|
||||
- 💬 **Telegram**: @fringg
|
||||
|
||||
TRIAL_DURATION_DAYS=3 (дни)
|
||||
### Вклад в проект
|
||||
|
||||
TRIAL_TRAFFIC_GB=2
|
||||
1. Fork репозитория
|
||||
2. Создайте feature branch
|
||||
3. Внесите изменения
|
||||
4. Создайте Pull Request
|
||||
|
||||
TRIAL_SQUAD_UUID=(УКазать UUID сквада из панели!)
|
||||
## 📄 Лицензия
|
||||
|
||||
TRIAL_PRICE=0.0(не трогать)
|
||||
Этот проект распространяется под лицензией MIT. См. файл [LICENSE](LICENSE) для подробностей.
|
||||
|
||||
Monitor Service Settings (дополнительные настройки)
|
||||
## 🗺 Roadmap & TODO
|
||||
|
||||
MONITOR_CHECK_INTERVAL=3600 (Запуск службы проверки)
|
||||
### 📋 Общий план развития
|
||||
|
||||
MONITOR_DAILY_CHECK_HOUR=10 (Разовый чек в определенный промежуток дня)
|
||||
| № | Задача | Статус | Версия | Приоритет | Описание |
|
||||
|---|---------|--------|--------|-----------|----------|
|
||||
| 1 | Мониторинг подписок | ✅ Done | v1.1.0 | 🔴 High | Служба оповещения об истечении срока и автоконтроль |
|
||||
| 2 | Платежные шлюзы | 🎯 Planned | v1.4.0+ | 🔴 High | ЮKassa, Сбербанк, Tinkoff, Crypto, Звезды |
|
||||
| 3 | Синхронизация RemnaWave | ✅ Done | v1.1.0 | 🟡 Medium | Импорт пользователей по Telegram ID |
|
||||
| 4 | Веб-панель управления | 🎯 Planned | v1.4.0+ | 🟡 Medium | Полнофункциональный веб-интерфейс |
|
||||
| 5 | Управление промокодами | ✅ Done | v1.3.0 | 🟢 Low | Удаление, редактирование |
|
||||
| 6 | Безопасное удаление подписок | 🚧 In Progress | v1.4.0 | 🟡 Medium | Архивирование вместо удаления |
|
||||
| 7 | Уведомления в другие чаты | 🎯 Planned | v1.5.0 | 🟡 Medium | Webhook, Email |
|
||||
| 8 | Реферальная система | ✅ Done | v1.2.0 | 🟢 Low | Полнофункциональная программа с подробной статистикой |
|
||||
| 9 | Рулетка | ✅ Done | v1.3.0 | 🟢 Low | Лудочка |
|
||||
| 10 | Блокировка | 🚧 In Progress | v1.3.0+ | 🟢 Low | Бан/разбан юзеров |
|
||||
| 11 | Удаление истекшик подписок из бота и панели | ✅ Done | v1.3.1 | 🔴 High | Дабы не плодить лишние записи в базе бота и панели ремны, да и в целом это логично)( |
|
||||
|
||||
MONITOR_WARNING_DAYS=2 (За сколько дней слать уведомления)
|
||||
|
||||
### 🚀 Хотите помочь?
|
||||
|
||||
#Использование
|
||||
**Разработчикам:**
|
||||
- 🐛 Найдите и исправьте баги
|
||||
- ✨ Предложите новые функции
|
||||
- 📝 Улучшите документацию
|
||||
- 🧪 Напишите тесты
|
||||
|
||||
/start - запуск
|
||||
**Пользователям:**
|
||||
- 💭 Поделитесь идеями в Issues
|
||||
- 🐞 Сообщите о найденных проблемах
|
||||
- ⭐ Поставьте звезду проекту
|
||||
- 📢 Расскажите друзьям
|
||||
|
||||
#Синхронизация подписок
|
||||
**Спонсорам:**
|
||||
- 💰 Поддержите разработку
|
||||
- 🎯 Закажите приоритетные функции
|
||||
- 🏢 Корпоративная поддержка
|
||||
|
||||
Вы можете перенести свои существующие подписки из панели Remnawave прямо в бота всего одним кликом.
|
||||
Для этого в админ панеле реализован соостветствующий пункт: Админ панель - Система Remnawave - Синхронизация с Remnawave - Импорт всех по Telegram ID. После нажатия подтянет всех пользователей в бота, подпискам из панели будет назначено имя "Старая подписка" - такую подписку невозможно продлить.
|
||||
---
|
||||
|
||||
ДОПОЛНИТЕЛЬНО:
|
||||
Реализована возможность зачистки импортированных из панели подписок по тг айди Админ панель - Система Remnawave - Синхронизация с Remnawave - Просмотрт планов - Удалалить импортированные
|
||||
**⚠️ Важные замечания от автора:**
|
||||
|
||||
Остальное трогать без понимания кода - не рекомендую.
|
||||
> 🗑 **Осторожно с удалением подписок!** Сейчас удаление плана подписки скроет эту подписку у всех пользователей, которые ее купили. Используйте деактивацию вместо удаления. Эта проблема будет исправлена в v1.4.0.
|
||||
|
||||
#Структура проекта
|
||||
---
|
||||
|
||||
main.py — главный файл запуска.
|
||||
**⭐ Если проект был полезен, поставьте звездочку на GitHub!**
|
||||
|
||||
handlers.py — основные обработчики команд и действий пользователя.
|
||||
---
|
||||
|
||||
admin_handlers.py — обработчики команд и действий администраторов.
|
||||
|
||||
database.py — модели и методы работы с базой данных (SQLAlchemy).
|
||||
|
||||
remnawave_api.py — интеграция с API RemnaWave.
|
||||
|
||||
keyboards.py — генерация клавиатур Telegram.
|
||||
|
||||
translations.py — локализация и переводы.
|
||||
|
||||
utils.py — вспомогательные функции.
|
||||
|
||||
middlewares.py — промежуточные слои для обработки сообщений и запросов.
|
||||
|
||||
subscription_monitor.py - сервис мониторинга сроков истечения подписок
|
||||
|
||||
.env — файл конфигурации с переменными окружения.
|
||||
|
||||
requirements.txt — список зависимостей Python.
|
||||
|
||||
run.sh — скрипт установки и управления ботом (опционально).
|
||||
|
||||
#Администрирование
|
||||
|
||||
Для входа в админ-панель используйте кнопку "⚙️ Админ панель" в главном меню, если вы указаны как администратор в ADMIN_IDS.
|
||||
|
||||
В админ-панели доступны:
|
||||
|
||||
Управление подписками (создание, редактирование, список, удаление, включение/отключение)
|
||||
|
||||
Управление пользователями (просмотр списка, баланс)
|
||||
|
||||
Управление балансом (пополнение пользователей через tg id)
|
||||
|
||||
Управление промокодами (создание, список)
|
||||
|
||||
Одобрение или отклонение платежей (Приходит запрос администраторам)
|
||||
|
||||
Отправка сообщений пользователям или массовая рассылка
|
||||
|
||||
Мониторинг подписок (Проверка статуса службы, принудитедьный запуск, деактивация истекщих подписок(на случай падения базы), персональный тест(можно отправить уведомления юзеру по tg id)
|
||||
|
||||
Просмотр статистики
|
||||
|
||||
Управление систеой Remnawave (Ноды, пользователи, синхронизация и импорт подписок из базы Remnawave в бот)
|
||||
|
||||
#ToDo
|
||||
|
||||
Код колхозный и не без вайбкодинга тут обошлось, но будет допиливаться, текущая реализация работает - уже хорошо
|
||||
1) Дописать службу для оповещения об истечении срока подписки и контроля - Done v1.1.0
|
||||
2) Подключить различные шлюзы для пополнения баланса
|
||||
3) Синхранизацию с Remnawave между пользователями по тг id
|
||||
4) Полнофункциональную панель упарвления
|
||||
5) Добавить возможность удаление промокодов - In progress
|
||||
6) Доработать алгоритм удаления подписок ибо удаление(А НЕ деактивация) сейчас - скроект эту подписку у всех юзеров которые ее купили, так что удаляйте на свой страх и риск я предупредил) - In progress
|
||||
8) Отправка уведомлений административных в другие чаты-топики
|
||||
9) Рефка (как по мне беспонтовая штука, сервера нормальные хостите, сервис нормальный делайте и будут клиенты - не ебите мозги, но если будет не лень, то допилю)
|
||||
*Создано с ❤️ для RemnaWave сообщества*
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# api_error_handlers.py - Дополнительные утилиты для обработки ошибок API
|
||||
|
||||
import logging
|
||||
from typing import Optional, Dict, Any, Callable
|
||||
from aiogram.types import CallbackQuery
|
||||
@@ -10,18 +8,11 @@ from translations import t
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class APIErrorHandler:
|
||||
"""Класс для обработки ошибок API и предоставления пользователю понятной информации"""
|
||||
|
||||
@staticmethod
|
||||
async def handle_api_error(callback: CallbackQuery, error: Exception,
|
||||
operation: str, user_language: str = 'ru',
|
||||
fallback_keyboard=None) -> bool:
|
||||
"""
|
||||
Обработка ошибок API с отправкой понятного сообщения пользователю
|
||||
|
||||
Returns:
|
||||
bool: True если ошибка была обработана, False если нужно перепробросить
|
||||
"""
|
||||
error_message = str(error).lower()
|
||||
|
||||
if "timeout" in error_message or "connection" in error_message:
|
||||
@@ -68,12 +59,6 @@ class APIErrorHandler:
|
||||
|
||||
@staticmethod
|
||||
async def safe_api_call(api_method: Callable, *args, **kwargs) -> tuple[bool, Any]:
|
||||
"""
|
||||
Безопасный вызов метода API с обработкой ошибок
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, result: Any)
|
||||
"""
|
||||
try:
|
||||
result = await api_method(*args, **kwargs)
|
||||
return True, result
|
||||
@@ -81,15 +66,11 @@ class APIErrorHandler:
|
||||
logger.error(f"API call failed: {api_method.__name__} - {e}")
|
||||
return False, str(e)
|
||||
|
||||
# Дополнительные обработчики для исправления конкретных проблем
|
||||
def create_error_recovery_keyboard(error_context: str, language: str = 'ru'):
|
||||
"""Создание клавиатуры для восстановления после ошибки"""
|
||||
from keyboards import error_recovery_keyboard
|
||||
return error_recovery_keyboard(error_context, language)
|
||||
|
||||
# Улучшенные функции для работы с RemnaWave API
|
||||
async def safe_get_nodes(api: RemnaWaveAPI) -> tuple[bool, list]:
|
||||
"""Безопасное получение списка нод"""
|
||||
try:
|
||||
logger.info("Attempting to fetch nodes from API...")
|
||||
nodes = await api.get_all_nodes()
|
||||
@@ -110,7 +91,6 @@ async def safe_get_nodes(api: RemnaWaveAPI) -> tuple[bool, list]:
|
||||
return False, []
|
||||
|
||||
async def safe_get_system_users(api: RemnaWaveAPI) -> tuple[bool, list]:
|
||||
"""Безопасное получение списка пользователей системы"""
|
||||
try:
|
||||
logger.info("Attempting to fetch system users from API...")
|
||||
users = await api.get_all_system_users_full()
|
||||
@@ -131,7 +111,6 @@ async def safe_get_system_users(api: RemnaWaveAPI) -> tuple[bool, list]:
|
||||
return False, []
|
||||
|
||||
async def safe_restart_nodes(api: RemnaWaveAPI, all_nodes: bool = True, node_id: str = None) -> tuple[bool, str]:
|
||||
"""Безопасная перезагрузка нод"""
|
||||
try:
|
||||
if all_nodes:
|
||||
logger.info("Attempting to restart all nodes...")
|
||||
@@ -153,9 +132,7 @@ async def safe_restart_nodes(api: RemnaWaveAPI, all_nodes: bool = True, node_id:
|
||||
logger.error(f"Error restarting nodes: {e}")
|
||||
return False, str(e)
|
||||
|
||||
# Функции для проверки состояния API
|
||||
async def check_api_health(api: RemnaWaveAPI) -> Dict[str, Any]:
|
||||
"""Проверка состояния API"""
|
||||
health_info = {
|
||||
'api_available': False,
|
||||
'nodes_accessible': False,
|
||||
@@ -168,9 +145,7 @@ async def check_api_health(api: RemnaWaveAPI) -> Dict[str, Any]:
|
||||
health_info['errors'].append("API instance is None")
|
||||
return health_info
|
||||
|
||||
# Проверяем доступность API
|
||||
try:
|
||||
# Простая проверка через получение нод (обычно быстрая операция)
|
||||
success, nodes = await safe_get_nodes(api)
|
||||
if success:
|
||||
health_info['api_available'] = True
|
||||
@@ -180,7 +155,6 @@ async def check_api_health(api: RemnaWaveAPI) -> Dict[str, Any]:
|
||||
except Exception as e:
|
||||
health_info['errors'].append(f"Nodes check failed: {e}")
|
||||
|
||||
# Проверяем доступность пользователей
|
||||
try:
|
||||
success, users = await safe_get_system_users(api)
|
||||
if success:
|
||||
@@ -190,7 +164,6 @@ async def check_api_health(api: RemnaWaveAPI) -> Dict[str, Any]:
|
||||
except Exception as e:
|
||||
health_info['errors'].append(f"Users check failed: {e}")
|
||||
|
||||
# Проверяем системную статистику
|
||||
try:
|
||||
stats = await api.get_system_stats()
|
||||
if stats:
|
||||
@@ -202,9 +175,7 @@ async def check_api_health(api: RemnaWaveAPI) -> Dict[str, Any]:
|
||||
|
||||
return health_info
|
||||
|
||||
# Декоратор для автоматической обработки ошибок API
|
||||
def handle_api_errors(operation_name: str):
|
||||
"""Декоратор для автоматической обработки ошибок API в handler'ах"""
|
||||
def decorator(func):
|
||||
async def wrapper(callback: CallbackQuery, user, *args, **kwargs):
|
||||
try:
|
||||
@@ -212,11 +183,9 @@ def handle_api_errors(operation_name: str):
|
||||
except Exception as e:
|
||||
logger.error(f"Error in {func.__name__}: {e}")
|
||||
|
||||
# Получаем API из kwargs если есть
|
||||
api = kwargs.get('api')
|
||||
fallback_keyboard = None
|
||||
|
||||
# Создаем fallback клавиатуру в зависимости от операции
|
||||
if 'nodes' in operation_name.lower():
|
||||
from keyboards import admin_system_keyboard
|
||||
fallback_keyboard = admin_system_keyboard(user.language)
|
||||
@@ -224,7 +193,6 @@ def handle_api_errors(operation_name: str):
|
||||
from keyboards import system_users_keyboard
|
||||
fallback_keyboard = system_users_keyboard(user.language)
|
||||
|
||||
# Обрабатываем ошибку
|
||||
await APIErrorHandler.handle_api_error(
|
||||
callback, e, operation_name, user.language, fallback_keyboard
|
||||
)
|
||||
|
||||
@@ -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,73 +1,172 @@
|
||||
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
|
||||
REMNAWAVE_MODE: str
|
||||
DATABASE_URL: str
|
||||
ADMIN_IDS: List[int]
|
||||
DEFAULT_LANGUAGE: str
|
||||
SUPPORT_USERNAME: str
|
||||
|
||||
# Subscription URL settings
|
||||
SUBSCRIPTION_BASE_URL: str
|
||||
|
||||
# Trial subscription settings
|
||||
# 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
|
||||
TRIAL_PRICE: float
|
||||
|
||||
# Monitor service settings
|
||||
MONITOR_CHECK_INTERVAL: int
|
||||
MONITOR_DAILY_CHECK_HOUR: int
|
||||
MONITOR_WARNING_DAYS: int
|
||||
# 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 дня
|
||||
|
||||
# Новые настройки для удаления истекших подписок
|
||||
DELETE_EXPIRED_TRIAL_DAYS: int = 1 # Удалять триальные через 1 день после истечения
|
||||
DELETE_EXPIRED_REGULAR_DAYS: int = 7 # Удалять обычные через 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 # Количество выигрышных чисел
|
||||
|
||||
STARS_ENABLED: bool = True
|
||||
STARS_RATES: Dict[int, float] = None
|
||||
|
||||
def load_config() -> Config:
|
||||
"""Load configuration from environment variables"""
|
||||
# Parse admin IDs
|
||||
admin_ids_str = os.getenv('ADMIN_IDS', '')
|
||||
admin_ids = []
|
||||
if admin_ids_str:
|
||||
|
||||
def parse_admin_ids(admin_ids_str: str) -> List[int]:
|
||||
if not admin_ids_str:
|
||||
return []
|
||||
try:
|
||||
admin_ids = [int(x.strip()) for x in admin_ids_str.split(',') if x.strip()]
|
||||
except ValueError:
|
||||
admin_ids = []
|
||||
return [int(id_str.strip()) for id_str in admin_ids_str.split(',') if id_str.strip()]
|
||||
except (ValueError, AttributeError):
|
||||
return []
|
||||
|
||||
# Get subscription base URL -
|
||||
subscription_base_url = os.getenv('SUBSCRIPTION_BASE_URL', '')
|
||||
def get_bool(key: str, default: bool = False) -> bool:
|
||||
value = os.getenv(key, '').lower()
|
||||
return value in ('true', '1', 'yes', 'on')
|
||||
|
||||
# Если SUBSCRIPTION_BASE_URL не установлен, используем значение по умолчанию
|
||||
if not subscription_base_url:
|
||||
subscription_base_url = 'https://sub.fring.tech'
|
||||
def get_int(key: str, default: int = 0) -> int:
|
||||
try:
|
||||
return int(os.getenv(key, str(default)))
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
def get_float(key: str, default: float = 0.0) -> float:
|
||||
try:
|
||||
return float(os.getenv(key, str(default)))
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
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_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', ''),
|
||||
REMNAWAVE_MODE=os.getenv('REMNAWAVE_MODE', 'local'),
|
||||
DATABASE_URL=os.getenv('DATABASE_URL', 'sqlite+aiosqlite:///bot.db'),
|
||||
ADMIN_IDS=admin_ids,
|
||||
DEFAULT_LANGUAGE=os.getenv('DEFAULT_LANGUAGE', 'ru'),
|
||||
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'),
|
||||
|
||||
# Subscription URL
|
||||
SUBSCRIPTION_BASE_URL=subscription_base_url,
|
||||
# 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', ''),
|
||||
|
||||
# Trial subscription settings
|
||||
TRIAL_ENABLED=os.getenv('TRIAL_ENABLED', 'true').lower() == 'true',
|
||||
TRIAL_DURATION_DAYS=int(os.getenv('TRIAL_DURATION_DAYS', '3')),
|
||||
TRIAL_TRAFFIC_GB=int(os.getenv('TRIAL_TRAFFIC_GB', '2')),
|
||||
TRIAL_SQUAD_UUID=os.getenv('TRIAL_SQUAD_UUID', '19bd5bde-5eea-4368-809c-6ba1ffb93897'),
|
||||
TRIAL_PRICE=float(os.getenv('TRIAL_PRICE', '0.0')),
|
||||
# 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 service settings
|
||||
MONITOR_CHECK_INTERVAL=int(os.getenv('MONITOR_CHECK_INTERVAL', '3600')),
|
||||
MONITOR_DAILY_CHECK_HOUR=int(os.getenv('MONITOR_DAILY_CHECK_HOUR', '10')),
|
||||
MONITOR_WARNING_DAYS=int(os.getenv('MONITOR_WARNING_DAYS', '2'))
|
||||
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 дня
|
||||
|
||||
# Новые настройки для удаления истекших подписок
|
||||
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),
|
||||
|
||||
# Telegram Stars
|
||||
STARS_ENABLED=get_bool('STARS_ENABLED', True),
|
||||
STARS_RATES=parse_stars_rates()
|
||||
)
|
||||
|
||||
def debug_environment():
|
||||
env_vars = [
|
||||
'BOT_TOKEN', 'BOT_USERNAME', 'DATABASE_URL',
|
||||
'REMNAWAVE_URL', 'REMNAWAVE_TOKEN', 'ADMIN_IDS',
|
||||
'REFERRAL_FIRST_REWARD', 'REFERRAL_THRESHOLD',
|
||||
'MONITOR_ENABLED', 'MONITOR_CHECK_INTERVAL',
|
||||
'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:")
|
||||
for var in env_vars:
|
||||
value = os.getenv(var, 'NOT SET')
|
||||
if 'TOKEN' in var and value != 'NOT SET':
|
||||
value = value[:10] + "..." if len(value) > 10 else value
|
||||
print(f" {var}: {value}")
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import List, Optional, Dict
|
||||
from translations import t
|
||||
|
||||
def language_keyboard() -> InlineKeyboardMarkup:
|
||||
"""Language selection keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="🇷🇺 Русский", callback_data="lang_ru"),
|
||||
@@ -13,42 +12,38 @@ def language_keyboard() -> InlineKeyboardMarkup:
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def main_menu_keyboard(lang: str = 'ru', is_admin: bool = False, show_trial: bool = False) -> InlineKeyboardMarkup:
|
||||
"""Beautiful main menu keyboard with emojis and better layout"""
|
||||
def main_menu_keyboard(lang: str = 'ru', is_admin: bool = False, show_trial: bool = False, show_lucky_game: bool = True) -> InlineKeyboardMarkup:
|
||||
buttons = [
|
||||
# Первый ряд - основные функции
|
||||
[
|
||||
InlineKeyboardButton(text="💰 " + t('balance', lang), callback_data="balance"),
|
||||
InlineKeyboardButton(text="📋 " + t('my_subscriptions', lang), callback_data="my_subscriptions")
|
||||
],
|
||||
# Второй ряд - покупка подписки (выделена отдельно как главная функция)
|
||||
[InlineKeyboardButton(text="🛒 " + t('buy_subscription', lang), callback_data="buy_subscription")],
|
||||
]
|
||||
|
||||
# Добавляем тестовую подписку если доступна
|
||||
if show_trial:
|
||||
buttons.insert(1, [InlineKeyboardButton(text="🆓 Тестовая подписка", callback_data="trial_subscription")])
|
||||
|
||||
# Добавляем остальные кнопки
|
||||
if show_lucky_game:
|
||||
buttons.append([InlineKeyboardButton(text="🎰 Проверь свою удачу!", callback_data="lucky_game")])
|
||||
|
||||
buttons.extend([
|
||||
# Дополнительные функции
|
||||
[
|
||||
InlineKeyboardButton(text="🎁 " + t('promocode', lang), callback_data="promocode"),
|
||||
InlineKeyboardButton(text="👥 Рефералы", callback_data="referral_program")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="💬 " + t('support', lang), callback_data="support")
|
||||
],
|
||||
# Последний ряд - настройки
|
||||
[InlineKeyboardButton(text="🌐 " + t('change_language', lang), callback_data="change_language")]
|
||||
])
|
||||
|
||||
# Добавляем админ панель если пользователь админ
|
||||
if is_admin:
|
||||
buttons.append([InlineKeyboardButton(text="⚙️ " + t('admin_panel', lang), callback_data="admin_panel")])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
# Остальные функции остаются прежними...
|
||||
def balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful balance menu keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💳 " + t('topup_balance', lang), callback_data="topup_balance")],
|
||||
[InlineKeyboardButton(text="📊 " + t('payment_history', lang), callback_data="payment_history")],
|
||||
@@ -57,26 +52,22 @@ def balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
return keyboard
|
||||
|
||||
def topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful top up balance keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💳 " + t('topup_card', lang), callback_data="topup_card")],
|
||||
# [InlineKeyboardButton(text="💳 " + t('topup_card', lang), callback_data="topup_card")],
|
||||
[InlineKeyboardButton(text="👨💼 " + t('topup_support', lang), callback_data="topup_support")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="balance")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def subscriptions_keyboard(subscriptions: List[dict], lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful available subscriptions keyboard"""
|
||||
buttons = []
|
||||
|
||||
# Группируем подписки по две в ряд для компактности
|
||||
for i in range(0, len(subscriptions), 2):
|
||||
row = []
|
||||
for j in range(2):
|
||||
if i + j < len(subscriptions):
|
||||
sub = subscriptions[i + j]
|
||||
price_text = f"{sub['price']:.0f}₽"
|
||||
# Используем разные эмодзи для разных ценовых категорий
|
||||
if sub['price'] <= 100:
|
||||
emoji = "🥉"
|
||||
elif sub['price'] <= 300:
|
||||
@@ -94,7 +85,6 @@ def subscriptions_keyboard(subscriptions: List[dict], lang: str = 'ru') -> Inlin
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def subscription_detail_keyboard(subscription_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful subscription detail keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(
|
||||
text="💎 " + t('buy_subscription_btn', lang, price=0),
|
||||
@@ -105,41 +95,37 @@ def subscription_detail_keyboard(subscription_id: int, lang: str = 'ru') -> Inli
|
||||
return keyboard
|
||||
|
||||
def user_subscriptions_keyboard(user_subscriptions: List[dict], lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful user's subscriptions keyboard"""
|
||||
buttons = []
|
||||
|
||||
for sub in user_subscriptions:
|
||||
# Добавляем эмодзи статуса для каждой подписки
|
||||
buttons.append([InlineKeyboardButton(
|
||||
text=f"📱 {sub['name']}",
|
||||
callback_data=f"view_sub_{sub['id']}"
|
||||
)])
|
||||
|
||||
if not user_subscriptions:
|
||||
# Если нет подписок, показываем кнопку покупки
|
||||
buttons.append([InlineKeyboardButton(text="🛒 " + t('buy_subscription', lang), callback_data="buy_subscription")])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")])
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def user_subscription_detail_keyboard(subscription_id: int, lang: str = 'ru', show_extend: bool = False) -> InlineKeyboardMarkup:
|
||||
"""Beautiful user's subscription detail keyboard with connection and optional extend button"""
|
||||
def user_subscription_detail_keyboard(subscription_id: int, lang: str = 'ru', show_extend: bool = False, is_imported: bool = False) -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
# Add extend button if subscription is expiring soon
|
||||
if show_extend:
|
||||
buttons.append([InlineKeyboardButton(text="⏰ " + t('extend_subscription', lang), callback_data=f"extend_sub_{subscription_id}")])
|
||||
if is_imported:
|
||||
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
|
||||
buttons.append([InlineKeyboardButton(text="🛒 Купить новую подписку", callback_data="buy_subscription")])
|
||||
else:
|
||||
if show_extend:
|
||||
buttons.append([InlineKeyboardButton(text="⏰ " + t('extend_subscription', lang), callback_data=f"extend_sub_{subscription_id}")])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
|
||||
|
||||
# Connection button (главная кнопка)
|
||||
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
|
||||
|
||||
# Back button
|
||||
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="my_subscriptions")])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def extend_subscription_keyboard(subscription_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful extend subscription confirmation keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="✅ Да, продлить", callback_data=f"confirm_extend_{subscription_id}"),
|
||||
@@ -149,51 +135,44 @@ def extend_subscription_keyboard(subscription_id: int, lang: str = 'ru') -> Inli
|
||||
return keyboard
|
||||
|
||||
def back_keyboard(callback_data: str, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful back button keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data=callback_data)]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def cancel_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful cancel button keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ " + t('cancel', lang), callback_data="main_menu")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
# Admin keyboards
|
||||
|
||||
def admin_menu_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful admin menu keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
# Первый ряд - управление контентом
|
||||
[
|
||||
InlineKeyboardButton(text="📦 " + t('manage_subscriptions', lang), callback_data="admin_subscriptions"),
|
||||
InlineKeyboardButton(text="👥 " + t('manage_users', lang), callback_data="admin_users")
|
||||
],
|
||||
# Второй ряд - финансы
|
||||
[
|
||||
InlineKeyboardButton(text="💰 " + t('manage_balance', lang), callback_data="admin_balance"),
|
||||
InlineKeyboardButton(text="🎁 " + t('manage_promocodes', lang), callback_data="admin_promocodes")
|
||||
],
|
||||
# Третий ряд - коммуникации и система
|
||||
[
|
||||
InlineKeyboardButton(text="📨 " + t('send_message', lang), callback_data="admin_messages"),
|
||||
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system") # НОВОЕ!
|
||||
InlineKeyboardButton(text="👥 Рефералы", callback_data="admin_referrals") # НОВАЯ КНОПКА
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system")
|
||||
],
|
||||
# Четвертый ряд - мониторинг и статистика
|
||||
[
|
||||
InlineKeyboardButton(text="🔍 Мониторинг подписок", callback_data="admin_monitor"),
|
||||
InlineKeyboardButton(text="📊 " + t('statistics', lang), callback_data="admin_stats")
|
||||
],
|
||||
# Назад
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def admin_subscriptions_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful admin subscriptions management keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="➕ " + t('create_subscription', lang), callback_data="create_subscription")],
|
||||
[InlineKeyboardButton(text="📋 Список подписок", callback_data="list_admin_subscriptions")],
|
||||
@@ -202,7 +181,6 @@ def admin_subscriptions_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
return keyboard
|
||||
|
||||
def admin_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful admin users management keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="👥 Список пользователей", callback_data="list_users")],
|
||||
[InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="search_user")],
|
||||
@@ -211,25 +189,124 @@ def admin_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
return keyboard
|
||||
|
||||
def admin_balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful admin balance management keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💸 Пополнить баланс пользователю", callback_data="admin_add_balance")],
|
||||
[InlineKeyboardButton(text="📊 История платежей", callback_data="admin_payment_history")],
|
||||
[InlineKeyboardButton(text="⭐ Telegram Stars", callback_data="admin_stars_payments")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def admin_promocodes_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful admin promocodes management keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="➕ " + t('create_promocode', lang), callback_data="create_promocode")],
|
||||
[InlineKeyboardButton(text="📋 Список промокодов", callback_data="list_promocodes")],
|
||||
[InlineKeyboardButton(text="📋 Управление промокодами", callback_data="list_promocodes")],
|
||||
[InlineKeyboardButton(text="📊 Статистика промокодов", callback_data="promocodes_stats")],
|
||||
[InlineKeyboardButton(text="🧹 Очистить истекшие", callback_data="cleanup_expired_promos")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def promocodes_management_keyboard(promocodes: List, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
for promo in promocodes[:15]:
|
||||
status_emoji = "🟢" if promo.is_active else "🔴"
|
||||
|
||||
if promo.expires_at and promo.expires_at < datetime.utcnow():
|
||||
status_emoji = "⏰"
|
||||
|
||||
usage_text = f"{promo.used_count}/{promo.usage_limit}"
|
||||
button_text = f"{status_emoji} {promo.code} ({promo.discount_amount}₽) [{usage_text}]"
|
||||
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=button_text,
|
||||
callback_data=f"promo_info_{promo.id}"
|
||||
)
|
||||
])
|
||||
|
||||
control_buttons = []
|
||||
|
||||
if not promo.code.startswith('REF'):
|
||||
toggle_text = "🔴" if promo.is_active else "🟢"
|
||||
control_buttons.append(
|
||||
InlineKeyboardButton(text=toggle_text, callback_data=f"toggle_promo_{promo.id}")
|
||||
)
|
||||
|
||||
control_buttons.append(
|
||||
InlineKeyboardButton(text="✏️", callback_data=f"edit_promo_{promo.id}")
|
||||
)
|
||||
|
||||
control_buttons.append(
|
||||
InlineKeyboardButton(text="🗑", callback_data=f"delete_promo_{promo.id}")
|
||||
)
|
||||
else:
|
||||
control_buttons.append(
|
||||
InlineKeyboardButton(text="👥 Реферальный", callback_data="noop")
|
||||
)
|
||||
|
||||
if control_buttons:
|
||||
buttons.append(control_buttons)
|
||||
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text="➕ Создать промокод", callback_data="create_promocode")
|
||||
])
|
||||
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text="🧹 Очистить истекшие", callback_data="cleanup_expired_promos"),
|
||||
InlineKeyboardButton(text="📊 Статистика", callback_data="promocodes_stats")
|
||||
])
|
||||
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_promocodes")
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
|
||||
def promocode_edit_keyboard(promo_id: int, language: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text="💰 Изменить скидку", callback_data=f"edit_promo_field_{promo_id}_discount")],
|
||||
[InlineKeyboardButton(text="📊 Изменить лимит", callback_data=f"edit_promo_field_{promo_id}_limit")],
|
||||
[InlineKeyboardButton(text="⏰ Изменить срок", callback_data=f"edit_promo_field_{promo_id}_expiry")],
|
||||
[InlineKeyboardButton(text="🗑 Удалить промокод", callback_data=f"delete_promo_{promo_id}")],
|
||||
[InlineKeyboardButton(text=t('back', language), callback_data="list_promocodes")]
|
||||
]
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def promocode_info_keyboard(promo_id: int, is_referral: bool = False, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
if not is_referral:
|
||||
buttons.extend([
|
||||
[InlineKeyboardButton(text="✏️ Редактировать", callback_data=f"edit_promo_{promo_id}")],
|
||||
[
|
||||
InlineKeyboardButton(text="🟢/🔴 Переключить", callback_data=f"toggle_promo_{promo_id}"),
|
||||
InlineKeyboardButton(text="🗑 Удалить", callback_data=f"delete_promo_{promo_id}")
|
||||
]
|
||||
])
|
||||
else:
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text="👥 Реферальный код", callback_data="noop")
|
||||
])
|
||||
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text="🔙 К списку", callback_data="list_promocodes")
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def bulk_promocodes_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text="🧹 Удалить все истекшие", callback_data="confirm_cleanup_expired")],
|
||||
[InlineKeyboardButton(text="🔴 Деактивировать все", callback_data="confirm_deactivate_all")],
|
||||
[InlineKeyboardButton(text="📊 Экспорт статистики", callback_data="export_promo_stats")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="list_promocodes")]
|
||||
]
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def confirmation_keyboard(confirm_callback: str, cancel_callback: str, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful confirmation keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="✅ Да", callback_data=confirm_callback),
|
||||
@@ -239,7 +316,6 @@ def confirmation_keyboard(confirm_callback: str, cancel_callback: str, lang: str
|
||||
return keyboard
|
||||
|
||||
def pagination_keyboard(page: int, total_pages: int, prefix: str, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful pagination keyboard"""
|
||||
buttons = []
|
||||
|
||||
if total_pages > 1:
|
||||
@@ -258,13 +334,11 @@ def pagination_keyboard(page: int, total_pages: int, prefix: str, lang: str = 'r
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def admin_subscriptions_list_keyboard(subs: List[Subscription], lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful keyboard for admin subscriptions list with enhanced controls"""
|
||||
buttons = []
|
||||
for sub in subs:
|
||||
status_emoji = "🟢" if sub.is_active else "🔴"
|
||||
price = f"{sub.price:.0f}₽"
|
||||
|
||||
# Основная кнопка с информацией о подписке
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=f"{status_emoji} {sub.name} — {price}",
|
||||
@@ -272,7 +346,6 @@ def admin_subscriptions_list_keyboard(subs: List[Subscription], lang: str = 'ru'
|
||||
)
|
||||
])
|
||||
|
||||
# Кнопки управления для каждой подписки в одну строку
|
||||
control_buttons = [
|
||||
InlineKeyboardButton(text="✏️", callback_data=f"edit_sub_{sub.id}"),
|
||||
InlineKeyboardButton(
|
||||
@@ -284,13 +357,11 @@ def admin_subscriptions_list_keyboard(subs: List[Subscription], lang: str = 'ru'
|
||||
|
||||
buttons.append(control_buttons)
|
||||
|
||||
# Кнопка создания новой подписки
|
||||
buttons.append([InlineKeyboardButton(text="➕ Создать подписку", callback_data="create_subscription")])
|
||||
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_subscriptions")])
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def admin_payment_keyboard(payment_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful keyboard for admin payment approval"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="✅ Одобрить платеж", callback_data=f"approve_payment_{payment_id}"),
|
||||
@@ -300,7 +371,6 @@ def admin_payment_keyboard(payment_id: int, lang: str = 'ru') -> InlineKeyboardM
|
||||
return keyboard
|
||||
|
||||
def admin_messages_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful admin messages management keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="👤 " + t('send_to_user', lang), callback_data="admin_send_to_user")],
|
||||
[InlineKeyboardButton(text="📢 " + t('send_to_all', lang), callback_data="admin_send_to_all")],
|
||||
@@ -309,7 +379,6 @@ def admin_messages_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
return keyboard
|
||||
|
||||
def quick_topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Quick topup amounts keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="💳 100₽", callback_data="quick_topup_100"),
|
||||
@@ -325,7 +394,6 @@ def quick_topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
return keyboard
|
||||
|
||||
def connection_keyboard(subscription_url: str, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful connection keyboard with web app"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔗 Подключиться", url=subscription_url)],
|
||||
[InlineKeyboardButton(text="📱 Инструкция", callback_data="connection_guide")],
|
||||
@@ -334,7 +402,6 @@ def connection_keyboard(subscription_url: str, lang: str = 'ru') -> InlineKeyboa
|
||||
return keyboard
|
||||
|
||||
def trial_subscription_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Trial subscription confirmation keyboard"""
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text="✅ Получить тестовую подписку", callback_data="confirm_trial")],
|
||||
[InlineKeyboardButton(text=t('back', lang), callback_data="main_menu")]
|
||||
@@ -342,18 +409,13 @@ def trial_subscription_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def admin_monitor_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful admin monitor management keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📊 Статус сервиса", callback_data="monitor_status")],
|
||||
[InlineKeyboardButton(text="🔄 Принудительная проверка", callback_data="monitor_force_check")],
|
||||
[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
|
||||
|
||||
def admin_system_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful admin system management keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📊 Системная статистика", callback_data="system_stats")],
|
||||
[InlineKeyboardButton(text="🖥 Управление нодами", callback_data="nodes_management")],
|
||||
@@ -365,7 +427,6 @@ def admin_system_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
return keyboard
|
||||
|
||||
def system_stats_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""System statistics keyboard with refresh"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔄 Обновить статистику", callback_data="refresh_system_stats")],
|
||||
[InlineKeyboardButton(text="🖥 Ноды", callback_data="nodes_management")],
|
||||
@@ -375,11 +436,9 @@ def system_stats_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
return keyboard
|
||||
|
||||
def nodes_management_keyboard(nodes: List[Dict], lang: str = 'ru', timestamp: int = None) -> InlineKeyboardMarkup:
|
||||
"""Improved nodes management keyboard"""
|
||||
buttons = []
|
||||
|
||||
if nodes:
|
||||
# Statistics row
|
||||
online_count = len([n for n in nodes if n.get('status') == 'online'])
|
||||
total_count = len(nodes)
|
||||
|
||||
@@ -390,11 +449,9 @@ def nodes_management_keyboard(nodes: List[Dict], lang: str = 'ru', timestamp: in
|
||||
)
|
||||
])
|
||||
|
||||
# Show first 5 nodes with improved display
|
||||
for i, node in enumerate(nodes[:5]):
|
||||
status = node.get('status', 'unknown')
|
||||
|
||||
# Status emoji based on actual status
|
||||
if status == 'online':
|
||||
status_emoji = "🟢"
|
||||
elif status == 'disabled':
|
||||
@@ -409,13 +466,11 @@ def nodes_management_keyboard(nodes: List[Dict], lang: str = 'ru', timestamp: in
|
||||
node_name = node.get('name', f'Node-{i+1}')
|
||||
node_id = node.get('id', node.get('uuid'))
|
||||
|
||||
# Truncate long names
|
||||
if len(node_name) > 20:
|
||||
display_name = node_name[:17] + "..."
|
||||
else:
|
||||
display_name = node_name
|
||||
|
||||
# CPU/Memory usage if available
|
||||
usage_info = ""
|
||||
if node.get('cpuUsage'):
|
||||
usage_info += f" CPU:{node['cpuUsage']:.0f}%"
|
||||
@@ -452,19 +507,16 @@ def nodes_management_keyboard(nodes: List[Dict], lang: str = 'ru', timestamp: in
|
||||
)
|
||||
])
|
||||
|
||||
# Action buttons
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text="🔄 Перезагрузить все", callback_data="restart_all_nodes"),
|
||||
InlineKeyboardButton(text="📊 Статистика", callback_data="nodes_statistics")
|
||||
])
|
||||
|
||||
# Refresh button
|
||||
refresh_callback = f"refresh_nodes_stats_{timestamp}" if timestamp else "refresh_nodes_stats"
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text="🔄 Обновить", callback_data=refresh_callback)
|
||||
])
|
||||
|
||||
# Back button
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text="🔙 Назад", callback_data="admin_system")
|
||||
])
|
||||
@@ -472,7 +524,6 @@ def nodes_management_keyboard(nodes: List[Dict], lang: str = 'ru', timestamp: in
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def system_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""System users management keyboard - ИСПРАВЛЕНО"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📊 Статистика пользователей", callback_data="users_statistics")],
|
||||
[InlineKeyboardButton(text="👥 Список всех пользователей", callback_data="list_all_system_users")],
|
||||
@@ -483,7 +534,6 @@ def system_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
return keyboard
|
||||
|
||||
def bulk_operations_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Bulk operations keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔄 Сбросить трафик", callback_data="bulk_reset_traffic")],
|
||||
[InlineKeyboardButton(text="❌ Отключить пользователей", callback_data="bulk_disable_users")],
|
||||
@@ -494,7 +544,6 @@ def bulk_operations_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
return keyboard
|
||||
|
||||
def confirm_restart_keyboard(node_id: str = None, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Confirmation keyboard for node restart"""
|
||||
action = f"confirm_restart_node_{node_id}" if node_id else "confirm_restart_all_nodes"
|
||||
back_action = f"node_details_{node_id}" if node_id else "nodes_management"
|
||||
|
||||
@@ -505,3 +554,100 @@ def confirm_restart_keyboard(node_id: str = None, lang: str = 'ru') -> InlineKey
|
||||
]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def admin_referrals_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📊 Статистика рефералов", callback_data="referral_statistics")],
|
||||
[InlineKeyboardButton(text="👥 Список рефереров", callback_data="list_referrers")],
|
||||
[InlineKeyboardButton(text="💰 История выплат", callback_data="referral_payments")],
|
||||
[InlineKeyboardButton(text="⚙️ Настройки программы", callback_data="referral_settings")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def lucky_game_keyboard(can_play: bool, time_left_str: str = "", lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
if can_play:
|
||||
buttons.append([InlineKeyboardButton(text="🎲 Играть!", callback_data="start_lucky_game")])
|
||||
else:
|
||||
buttons.append([InlineKeyboardButton(text=f"⏰ Приходи через {time_left_str}", callback_data="noop")])
|
||||
|
||||
buttons.extend([
|
||||
[InlineKeyboardButton(text="📈 История игр", callback_data="lucky_game_history")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")]
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def lucky_numbers_keyboard(numbers_count: int) -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
for i in range(0, numbers_count, 5):
|
||||
row = []
|
||||
for j in range(5):
|
||||
if i + j + 1 <= numbers_count:
|
||||
number = i + j + 1
|
||||
row.append(InlineKeyboardButton(
|
||||
text=str(number),
|
||||
callback_data=f"choose_number_{number}"
|
||||
))
|
||||
buttons.append(row)
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="❌ Отмена", callback_data="lucky_game")])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def lucky_game_result_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📈 История игр", callback_data="lucky_game_history")],
|
||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
|
||||
])
|
||||
|
||||
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())
|
||||
|
||||
# Создаем кнопки по 2 в ряд
|
||||
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:
|
||||
"""Клавиатура подтверждения платежа через звезды (не используется в send_invoice)"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="topup_stars")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import logging
|
||||
import random
|
||||
from typing import Optional, List
|
||||
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
from database import Database, User
|
||||
from keyboards import back_keyboard
|
||||
from translations import t
|
||||
from utils import log_user_action, format_datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class LuckyGameStates(StatesGroup):
|
||||
waiting_number_choice = State()
|
||||
|
||||
lucky_game_router = Router()
|
||||
|
||||
|
||||
@lucky_game_router.callback_query(F.data == "lucky_game")
|
||||
async def lucky_game_menu_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
reward_amount = getattr(config, 'LUCKY_GAME_REWARD', 50.0)
|
||||
numbers_count = getattr(config, 'LUCKY_GAME_NUMBERS', 30)
|
||||
winning_numbers = getattr(config, 'LUCKY_GAME_WINNING_COUNT', 3)
|
||||
|
||||
can_play, next_game_time = await check_can_play_today(db, user.telegram_id)
|
||||
|
||||
games_played, total_won, win_count = await get_user_game_stats(db, user.telegram_id)
|
||||
|
||||
text = "🎰 **Проверь свою удачу!**\n\n"
|
||||
text += "🎯 **Как играть:**\n"
|
||||
text += f"• Выберите число от 1 до {numbers_count}\n"
|
||||
text += f"• Из {numbers_count} чисел {winning_numbers} - выигрышные\n"
|
||||
text += f"• Угадали - получаете {reward_amount:.0f}₽ на баланс!\n"
|
||||
text += "• Играть можно 1 раз в сутки\n\n"
|
||||
|
||||
text += "📊 **Ваша статистика:**\n"
|
||||
text += f"• Игр сыграно: {games_played}\n"
|
||||
text += f"• Выигрышей: {win_count}\n"
|
||||
text += f"• Всего выиграно: {total_won:.0f}₽\n"
|
||||
|
||||
if games_played > 0:
|
||||
win_rate = (win_count / games_played) * 100
|
||||
text += f"• Процент побед: {win_rate:.1f}%\n"
|
||||
|
||||
buttons = []
|
||||
|
||||
if can_play:
|
||||
buttons.append([InlineKeyboardButton(text="🎲 Играть!", callback_data="start_lucky_game")])
|
||||
else:
|
||||
now = datetime.utcnow()
|
||||
time_left = next_game_time - now
|
||||
hours_left = int(time_left.total_seconds() // 3600)
|
||||
minutes_left = int((time_left.total_seconds() % 3600) // 60)
|
||||
|
||||
if hours_left > 0:
|
||||
time_text = f"{hours_left}ч {minutes_left}м"
|
||||
else:
|
||||
time_text = f"{minutes_left}м"
|
||||
|
||||
buttons.append([InlineKeyboardButton(text=f"⏰ Приходи через {time_text}", callback_data="noop")])
|
||||
|
||||
buttons.extend([
|
||||
[InlineKeyboardButton(text="📈 История игр", callback_data="lucky_game_history")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', user.language), callback_data="main_menu")]
|
||||
])
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in lucky game menu: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки игры")
|
||||
|
||||
@lucky_game_router.callback_query(F.data == "start_lucky_game")
|
||||
async def start_lucky_game_callback(callback: CallbackQuery, db: Database, state: FSMContext, **kwargs):
|
||||
"""Начать игру - показать числа для выбора"""
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
can_play, next_game_time = await check_can_play_today(db, user.telegram_id)
|
||||
|
||||
if not can_play:
|
||||
await callback.answer("⏰ Вы уже играли сегодня! Приходите завтра.", show_alert=True)
|
||||
return
|
||||
|
||||
numbers_count = getattr(config, 'LUCKY_GAME_NUMBERS', 30)
|
||||
winning_numbers = getattr(config, 'LUCKY_GAME_WINNING_COUNT', 3)
|
||||
reward_amount = getattr(config, 'LUCKY_GAME_REWARD', 50.0)
|
||||
|
||||
winning_nums = random.sample(range(1, numbers_count + 1), winning_numbers)
|
||||
|
||||
await state.update_data(
|
||||
winning_numbers=winning_nums,
|
||||
reward_amount=reward_amount,
|
||||
numbers_count=numbers_count
|
||||
)
|
||||
await state.set_state(LuckyGameStates.waiting_number_choice)
|
||||
|
||||
text = f"🎯 **Выберите число от 1 до {numbers_count}**\n\n"
|
||||
text += f"🎁 Награда: {reward_amount:.0f}₽\n"
|
||||
text += f"🍀 Удачных чисел: {winning_numbers} из {numbers_count}\n\n"
|
||||
text += "Нажмите на число, чтобы испытать удачу!"
|
||||
|
||||
buttons = []
|
||||
for i in range(0, numbers_count, 5):
|
||||
row = []
|
||||
for j in range(5):
|
||||
if i + j + 1 <= numbers_count:
|
||||
number = i + j + 1
|
||||
row.append(InlineKeyboardButton(
|
||||
text=str(number),
|
||||
callback_data=f"choose_number_{number}"
|
||||
))
|
||||
buttons.append(row)
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="❌ Отмена", callback_data="lucky_game")])
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting lucky game: {e}")
|
||||
await callback.answer("❌ Ошибка запуска игры")
|
||||
|
||||
@lucky_game_router.callback_query(F.data.startswith("choose_number_"), StateFilter(LuckyGameStates.waiting_number_choice))
|
||||
async def choose_number_callback(callback: CallbackQuery, db: Database, state: FSMContext, **kwargs):
|
||||
"""Обработка выбора числа"""
|
||||
user = kwargs.get('user')
|
||||
bot = kwargs.get('bot')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
chosen_number = int(callback.data.split("_")[2])
|
||||
|
||||
state_data = await state.get_data()
|
||||
winning_numbers = state_data['winning_numbers']
|
||||
reward_amount = state_data['reward_amount']
|
||||
numbers_count = state_data['numbers_count']
|
||||
|
||||
is_winner = chosen_number in winning_numbers
|
||||
|
||||
await save_game_result(db, user.telegram_id, chosen_number, winning_numbers, is_winner, reward_amount if is_winner else 0.0)
|
||||
|
||||
if is_winner:
|
||||
await db.add_balance(user.telegram_id, reward_amount)
|
||||
|
||||
await db.create_payment(
|
||||
user_id=user.telegram_id,
|
||||
amount=reward_amount,
|
||||
payment_type='lucky_game',
|
||||
description=f'Выигрыш в игре удачи (число {chosen_number})',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
text = "🎉 **ПОЗДРАВЛЯЕМ! ВЫ ВЫИГРАЛИ!** 🎉\n\n"
|
||||
text += f"🎯 Ваше число: **{chosen_number}**\n"
|
||||
text += f"💰 Награда: **{reward_amount:.0f}₽** зачислена на баланс!\n"
|
||||
text += f"🆕 Новый баланс: **{user.balance + reward_amount:.0f}₽**\n\n"
|
||||
else:
|
||||
text = "😔 **Не повезло в этот раз...**\n\n"
|
||||
text += f"🎯 Ваше число: **{chosen_number}**\n"
|
||||
text += f"🍀 Выигрышные числа: **{', '.join(map(str, sorted(winning_numbers)))}**\n\n"
|
||||
text += "🔄 Попробуйте завтра!"
|
||||
|
||||
winning_nums_str = ', '.join(map(str, sorted(winning_numbers)))
|
||||
text += f"\n🎲 Сегодняшние удачные числа: {winning_nums_str}"
|
||||
text += f"\n\n⏰ Следующая игра будет доступна завтра!"
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📈 История игр", callback_data="lucky_game_history")],
|
||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
log_user_action(user.telegram_id, "lucky_game_played",
|
||||
f"Number: {chosen_number}, Winner: {is_winner}, Reward: {reward_amount if is_winner else 0}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in choose number: {e}")
|
||||
await callback.answer("❌ Ошибка обработки выбора")
|
||||
|
||||
await state.clear()
|
||||
|
||||
@lucky_game_router.callback_query(F.data == "lucky_game_history")
|
||||
async def lucky_game_history_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
games = await get_user_game_history(db, user.telegram_id, limit=10)
|
||||
|
||||
if not games:
|
||||
text = "📈 **История ваших игр**\n\n"
|
||||
text += "🎯 Вы еще не играли в игру удачи.\n\n"
|
||||
text += "Начните играть, чтобы увидеть историю!"
|
||||
else:
|
||||
text = "📈 **История ваших игр** (последние 10)\n\n"
|
||||
|
||||
for i, game in enumerate(games, 1):
|
||||
date_str = format_datetime(game['played_at'], user.language)
|
||||
|
||||
if game['is_winner']:
|
||||
emoji = "🎉"
|
||||
result = f"Выиграли {game['reward_amount']:.0f}₽"
|
||||
else:
|
||||
emoji = "😔"
|
||||
result = "Не повезло"
|
||||
|
||||
text += f"{i}. {emoji} Число: **{game['chosen_number']}** - {result}\n"
|
||||
text += f" 📅 {date_str}\n\n"
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🎰 К игре", callback_data="lucky_game")],
|
||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing game history: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки истории")
|
||||
|
||||
@lucky_game_router.callback_query(F.data == "noop")
|
||||
async def noop_callback(callback: CallbackQuery, **kwargs):
|
||||
"""Пустая операция для неактивных кнопок"""
|
||||
await callback.answer()
|
||||
|
||||
|
||||
async def check_can_play_today(db: Database, user_id: int) -> tuple[bool, Optional[datetime]]:
|
||||
"""Проверяет, может ли пользователь играть сегодня"""
|
||||
try:
|
||||
can_play = await db.can_play_lucky_game_today(user_id)
|
||||
|
||||
if not can_play:
|
||||
now = datetime.utcnow()
|
||||
tomorrow = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
|
||||
return False, tomorrow
|
||||
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking can play today: {e}")
|
||||
return True, None
|
||||
|
||||
async def get_user_game_stats(db: Database, user_id: int) -> tuple[int, float, int]:
|
||||
"""Получает статистику игр пользователя"""
|
||||
try:
|
||||
stats = await db.get_user_game_stats(user_id)
|
||||
return stats['total_games'], stats['total_won'], stats['total_wins']
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user game stats: {e}")
|
||||
return 0, 0.0, 0
|
||||
|
||||
async def save_game_result(db: Database, user_id: int, chosen_number: int,
|
||||
winning_numbers: List[int], is_winner: bool, reward_amount: float):
|
||||
"""Сохраняет результат игры в базу данных"""
|
||||
try:
|
||||
game = await db.create_lucky_game(
|
||||
user_id=user_id,
|
||||
chosen_number=chosen_number,
|
||||
winning_numbers=winning_numbers,
|
||||
is_winner=is_winner,
|
||||
reward_amount=reward_amount
|
||||
)
|
||||
|
||||
if game:
|
||||
logger.info(f"Game result saved for user {user_id}: number={chosen_number}, winner={is_winner}, reward={reward_amount}")
|
||||
else:
|
||||
logger.error(f"Failed to save game result for user {user_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving game result: {e}")
|
||||
|
||||
async def get_user_game_history(db: Database, user_id: int, limit: int = 10) -> List[dict]:
|
||||
"""Получает историю игр пользователя"""
|
||||
try:
|
||||
return await db.get_user_game_history(user_id, limit)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user game history: {e}")
|
||||
return []
|
||||
@@ -6,9 +6,20 @@ from aiogram import Bot, Dispatcher
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.enums import ParseMode
|
||||
from lucky_game import lucky_game_router
|
||||
from stars_handlers import stars_router
|
||||
|
||||
# Import our modules
|
||||
from config import load_config
|
||||
print("🚀 Запуск бота...")
|
||||
print(f"📍 Рабочая директория: {os.getcwd()}")
|
||||
print(f"📁 Файлы в директории: {os.listdir('.')}")
|
||||
|
||||
if os.path.exists('.env'):
|
||||
print("✅ Файл .env найден")
|
||||
else:
|
||||
print("❌ Файл .env НЕ НАЙДЕН!")
|
||||
print("💡 Создайте файл .env в корне проекта")
|
||||
|
||||
from config import load_config, debug_environment
|
||||
from database import Database
|
||||
from remnawave_api import RemnaWaveAPI
|
||||
from subscription_monitor import create_subscription_monitor
|
||||
@@ -16,7 +27,6 @@ from middlewares import DatabaseMiddleware, UserMiddleware, LoggingMiddleware, T
|
||||
from handlers import router
|
||||
from admin_handlers import admin_router
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
@@ -28,7 +38,6 @@ logging.basicConfig(
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BotApplication:
|
||||
"""Main bot application class"""
|
||||
|
||||
def __init__(self):
|
||||
self.config = None
|
||||
@@ -39,11 +48,16 @@ class BotApplication:
|
||||
self.monitor_service = None
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize all components"""
|
||||
# Load configuration
|
||||
|
||||
debug_environment()
|
||||
|
||||
self.config = load_config()
|
||||
|
||||
# Validate required environment variables
|
||||
print(f"🔧 Загруженная конфигурация:")
|
||||
print(f" BOT_USERNAME: '{self.config.BOT_USERNAME}'")
|
||||
print(f" REFERRAL_FIRST_REWARD: {self.config.REFERRAL_FIRST_REWARD}")
|
||||
print(f" ADMIN_IDS: {self.config.ADMIN_IDS}")
|
||||
|
||||
if not self.config.BOT_TOKEN:
|
||||
logger.error("BOT_TOKEN is required")
|
||||
raise ValueError("BOT_TOKEN is required")
|
||||
@@ -52,15 +66,18 @@ class BotApplication:
|
||||
logger.error("REMNAWAVE_URL and REMNAWAVE_TOKEN are required")
|
||||
raise ValueError("REMNAWAVE_URL and REMNAWAVE_TOKEN are required")
|
||||
|
||||
if not self.config.BOT_USERNAME:
|
||||
logger.warning("⚠️ BOT_USERNAME не установлен! Реферальные ссылки работать не будут!")
|
||||
print("💡 Добавьте BOT_USERNAME=your_bot_username в .env файл")
|
||||
|
||||
logger.info("Starting RemnaWave Bot...")
|
||||
logger.info(f"RemnaWave URL: {self.config.REMNAWAVE_URL}")
|
||||
logger.info(f"Admin IDs: {self.config.ADMIN_IDS}")
|
||||
logger.info(f"Bot Username: {self.config.BOT_USERNAME}")
|
||||
|
||||
# Initialize database
|
||||
self.db = Database(self.config.DATABASE_URL)
|
||||
await self._init_database()
|
||||
|
||||
# Initialize RemnaWave API
|
||||
self.api = RemnaWaveAPI(
|
||||
self.config.REMNAWAVE_URL,
|
||||
self.config.REMNAWAVE_TOKEN,
|
||||
@@ -68,26 +85,38 @@ class BotApplication:
|
||||
)
|
||||
logger.info("RemnaWave API initialized")
|
||||
|
||||
# Test API connection (optional - don't fail if it doesn't work)
|
||||
await self._test_api_connection()
|
||||
|
||||
# Initialize bot and dispatcher
|
||||
self.bot = Bot(
|
||||
token=self.config.BOT_TOKEN,
|
||||
default=DefaultBotProperties(parse_mode=ParseMode.HTML)
|
||||
)
|
||||
|
||||
# Test bot token
|
||||
await self._test_bot_token()
|
||||
|
||||
# Initialize dispatcher
|
||||
self._setup_dispatcher()
|
||||
|
||||
# Initialize subscription monitor service
|
||||
await self._init_monitor_service()
|
||||
|
||||
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):
|
||||
"""Initialize database with retry logic"""
|
||||
max_retries = 3
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
@@ -99,10 +128,9 @@ class BotApplication:
|
||||
if attempt == max_retries - 1:
|
||||
logger.error("Failed to initialize database after all retries")
|
||||
raise
|
||||
await asyncio.sleep(2) # Wait before retry
|
||||
await asyncio.sleep(2)
|
||||
|
||||
async def _test_api_connection(self):
|
||||
"""Test API connection"""
|
||||
try:
|
||||
system_stats = await self.api.get_system_stats()
|
||||
if system_stats:
|
||||
@@ -113,28 +141,30 @@ class BotApplication:
|
||||
logger.warning(f"RemnaWave API connection error: {e} - continuing anyway")
|
||||
|
||||
async def _test_bot_token(self):
|
||||
"""Test bot token before starting"""
|
||||
try:
|
||||
bot_info = await self.bot.get_me()
|
||||
logger.info(f"Bot started: @{bot_info.username} ({bot_info.first_name})")
|
||||
|
||||
if not self.config.BOT_USERNAME and bot_info.username:
|
||||
self.config.BOT_USERNAME = bot_info.username
|
||||
logger.info(f"✅ BOT_USERNAME автоматически установлен: {bot_info.username}")
|
||||
print("💡 Добавьте BOT_USERNAME в .env файл для постоянного сохранения")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Invalid bot token or network error: {e}")
|
||||
raise
|
||||
|
||||
def _setup_dispatcher(self):
|
||||
"""Setup dispatcher with middlewares and routers"""
|
||||
storage = MemoryStorage()
|
||||
self.dp = Dispatcher(storage=storage)
|
||||
|
||||
# Store config, api, db, and monitor_service in dispatcher workflow_data for access in handlers
|
||||
self.dp.workflow_data.update({
|
||||
"config": self.config,
|
||||
"api": self.api,
|
||||
"db": self.db,
|
||||
"monitor_service": None # Will be updated after monitor service is created
|
||||
"monitor_service": None
|
||||
})
|
||||
|
||||
# Setup middlewares in correct order
|
||||
self.dp.message.middleware(LoggingMiddleware())
|
||||
self.dp.callback_query.middleware(LoggingMiddleware())
|
||||
|
||||
@@ -153,33 +183,62 @@ class BotApplication:
|
||||
self.dp.message.middleware(UserMiddleware(self.db, self.config))
|
||||
self.dp.callback_query.middleware(UserMiddleware(self.db, self.config))
|
||||
|
||||
# Register routers
|
||||
self.dp.include_router(router)
|
||||
self.dp.include_router(admin_router)
|
||||
self.dp.include_router(lucky_game_router)
|
||||
self.dp.include_router(stars_router)
|
||||
|
||||
async def _init_monitor_service(self):
|
||||
"""Initialize subscription monitor service"""
|
||||
try:
|
||||
logger.info("🔧 Initializing subscription monitor service...")
|
||||
|
||||
if not self.bot:
|
||||
logger.error("❌ Bot instance is None, cannot initialize monitor")
|
||||
return
|
||||
|
||||
if not self.db:
|
||||
logger.error("❌ Database instance is None, cannot initialize monitor")
|
||||
return
|
||||
|
||||
if not self.config:
|
||||
logger.error("❌ Config instance is None, cannot initialize monitor")
|
||||
return
|
||||
|
||||
self.monitor_service = await create_subscription_monitor(
|
||||
self.bot, self.db, self.config, self.api
|
||||
)
|
||||
|
||||
# Update workflow_data with monitor service
|
||||
|
||||
if not self.monitor_service:
|
||||
logger.error("❌ Failed to create monitor service instance")
|
||||
return
|
||||
|
||||
self.dp.workflow_data["monitor_service"] = self.monitor_service
|
||||
|
||||
# Start the monitor service
|
||||
logger.info("✅ Monitor service added to workflow_data")
|
||||
|
||||
logger.info("🚀 Starting monitor service...")
|
||||
await self.monitor_service.start()
|
||||
logger.info("Subscription monitor service started successfully")
|
||||
|
||||
|
||||
status = await self.monitor_service.get_service_status()
|
||||
if status['is_running']:
|
||||
logger.info("✅ Subscription monitor service started successfully")
|
||||
logger.info(f"📊 Monitor status: interval={status['check_interval']}s, daily_hour={status['daily_check_hour']}, warning_days={status['warning_days']}")
|
||||
else:
|
||||
logger.warning("⚠️ Monitor service created but not running")
|
||||
logger.warning(f"📊 Monitor status: {status}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize monitor service: {e}")
|
||||
# Don't fail the entire application if monitor service fails
|
||||
logger.warning("Continuing without monitor service")
|
||||
logger.error(f"❌ Failed to initialize monitor service: {e}", exc_info=True)
|
||||
logger.warning("⚠️ Continuing without monitor service")
|
||||
self.monitor_service = None
|
||||
|
||||
async def start(self):
|
||||
"""Start bot polling"""
|
||||
logger.info("Bot polling started successfully")
|
||||
|
||||
if self.config.BOT_USERNAME:
|
||||
logger.info(f"🎁 Реферальная система активна! Ссылки: https://t.me/{self.config.BOT_USERNAME}?start=ref_USERID")
|
||||
else:
|
||||
logger.warning("⚠️ Реферальная система неактивна! Установите BOT_USERNAME")
|
||||
|
||||
try:
|
||||
await self.dp.start_polling(self.bot)
|
||||
except Exception as e:
|
||||
@@ -189,10 +248,8 @@ class BotApplication:
|
||||
await self.shutdown()
|
||||
|
||||
async def shutdown(self):
|
||||
"""Shutdown all services"""
|
||||
logger.info("Shutting down bot...")
|
||||
|
||||
# Stop monitor service first
|
||||
if self.monitor_service:
|
||||
try:
|
||||
await self.monitor_service.stop()
|
||||
@@ -200,7 +257,6 @@ class BotApplication:
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping monitor service: {e}")
|
||||
|
||||
# Close API connection
|
||||
if self.api:
|
||||
try:
|
||||
await self.api.close()
|
||||
@@ -208,7 +264,6 @@ class BotApplication:
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing API: {e}")
|
||||
|
||||
# Close database connection
|
||||
if self.db:
|
||||
try:
|
||||
await self.db.close()
|
||||
@@ -216,7 +271,6 @@ class BotApplication:
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing database: {e}")
|
||||
|
||||
# Close bot session
|
||||
if self.bot:
|
||||
try:
|
||||
await self.bot.session.close()
|
||||
@@ -227,7 +281,6 @@ class BotApplication:
|
||||
logger.info("Bot shutdown complete")
|
||||
|
||||
async def main():
|
||||
"""Main function"""
|
||||
app = None
|
||||
try:
|
||||
app = BotApplication()
|
||||
|
||||
@@ -8,7 +8,6 @@ from config import Config
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DatabaseMiddleware(BaseMiddleware):
|
||||
"""Middleware for database operations"""
|
||||
|
||||
def __init__(self, db: Database):
|
||||
self.db = db
|
||||
@@ -23,7 +22,6 @@ class DatabaseMiddleware(BaseMiddleware):
|
||||
return await handler(event, data)
|
||||
|
||||
class UserMiddleware(BaseMiddleware):
|
||||
"""Middleware for user management"""
|
||||
|
||||
def __init__(self, db: Database, config: Config):
|
||||
self.db = db
|
||||
@@ -39,25 +37,21 @@ class UserMiddleware(BaseMiddleware):
|
||||
|
||||
if telegram_user and not telegram_user.is_bot:
|
||||
try:
|
||||
# Get or create user
|
||||
user = await self.db.get_user_by_telegram_id(telegram_user.id)
|
||||
|
||||
if not user:
|
||||
# Check if user is admin
|
||||
is_admin = telegram_user.id in self.config.ADMIN_IDS
|
||||
|
||||
# Create new user
|
||||
user = await self.db.create_user(
|
||||
telegram_id=telegram_user.id,
|
||||
username=telegram_user.username,
|
||||
first_name=telegram_user.first_name,
|
||||
last_name=telegram_user.last_name,
|
||||
language=self.config.DEFAULT_LANGUAGE,
|
||||
language='',
|
||||
is_admin=is_admin
|
||||
)
|
||||
logger.info(f"Created new user: {telegram_user.id}")
|
||||
logger.info(f"Created new user: {telegram_user.id} without language")
|
||||
else:
|
||||
# Update user info if changed
|
||||
updated = False
|
||||
if user.username != telegram_user.username:
|
||||
user.username = telegram_user.username
|
||||
@@ -69,7 +63,6 @@ class UserMiddleware(BaseMiddleware):
|
||||
user.last_name = telegram_user.last_name
|
||||
updated = True
|
||||
|
||||
# Check admin status
|
||||
should_be_admin = telegram_user.id in self.config.ADMIN_IDS
|
||||
if user.is_admin != should_be_admin:
|
||||
user.is_admin = should_be_admin
|
||||
@@ -79,11 +72,10 @@ class UserMiddleware(BaseMiddleware):
|
||||
await self.db.update_user(user)
|
||||
|
||||
data['user'] = user
|
||||
data['lang'] = user.language if user else self.config.DEFAULT_LANGUAGE
|
||||
data['lang'] = user.language if user.language and user.language != '' else self.config.DEFAULT_LANGUAGE
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in UserMiddleware: {e}")
|
||||
# Create a minimal user object with defaults if database fails
|
||||
class FallbackUser:
|
||||
def __init__(self, telegram_id: int, username: str = None, config: Config = None):
|
||||
self.telegram_id = telegram_id
|
||||
@@ -95,7 +87,6 @@ class UserMiddleware(BaseMiddleware):
|
||||
self.is_admin = telegram_id in (config.ADMIN_IDS if config else [])
|
||||
self.remnawave_uuid = None
|
||||
|
||||
# Try to use fallback, but if database is completely broken, set None
|
||||
try:
|
||||
fallback_user = FallbackUser(telegram_user.id, telegram_user.username, self.config)
|
||||
data['user'] = fallback_user
|
||||
@@ -110,7 +101,6 @@ class UserMiddleware(BaseMiddleware):
|
||||
return await handler(event, data)
|
||||
|
||||
class LoggingMiddleware(BaseMiddleware):
|
||||
"""Middleware for logging"""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
@@ -134,7 +124,6 @@ class LoggingMiddleware(BaseMiddleware):
|
||||
return await handler(event, data)
|
||||
|
||||
class ThrottlingMiddleware(BaseMiddleware):
|
||||
"""Simple throttling middleware"""
|
||||
|
||||
def __init__(self, rate_limit: float = 1.0):
|
||||
self.rate_limit = rate_limit
|
||||
@@ -163,7 +152,6 @@ class ThrottlingMiddleware(BaseMiddleware):
|
||||
return await handler(event, data)
|
||||
|
||||
class WorkflowDataMiddleware(BaseMiddleware):
|
||||
"""Middleware to pass workflow data to handlers"""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
@@ -171,20 +159,17 @@ class WorkflowDataMiddleware(BaseMiddleware):
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any]
|
||||
) -> Any:
|
||||
# Get dispatcher from event
|
||||
if hasattr(event, 'bot'):
|
||||
dp = getattr(event.bot, '_dispatcher', None)
|
||||
if dp and hasattr(dp, 'workflow_data'):
|
||||
# Add workflow data to handler data
|
||||
workflow_data = dp.workflow_data
|
||||
for key, value in workflow_data.items():
|
||||
if key not in data: # Don't override existing data
|
||||
if key not in data:
|
||||
data[key] = value
|
||||
|
||||
return await handler(event, data)
|
||||
|
||||
class BotMiddleware(BaseMiddleware):
|
||||
"""Middleware to add bot instance to handler data"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from database import Database
|
||||
from database import ReferralProgram, ReferralEarning
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def process_referral_rewards(user_id: int, amount: float, payment_id: int, db: Database, bot=None, 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'))
|
||||
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 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})")
|
||||
|
||||
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 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")
|
||||
else:
|
||||
logger.info(f"Skipping percentage reward for user {user_id} - first reward not yet paid")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing referral rewards: {e}")
|
||||
|
||||
async def create_referral_from_start_param(user_telegram_id: int, start_param: str, db: Database, bot=None):
|
||||
try:
|
||||
if not start_param.startswith("ref_"):
|
||||
return False
|
||||
|
||||
referrer_id = int(start_param.replace("ref_", ""))
|
||||
|
||||
if referrer_id == user_telegram_id:
|
||||
logger.warning(f"User {user_telegram_id} tried to refer themselves")
|
||||
return False
|
||||
|
||||
existing_reverse_referral = await db.get_referral_by_referred_id(referrer_id)
|
||||
if existing_reverse_referral and existing_reverse_referral.referrer_id == user_telegram_id:
|
||||
logger.warning(f"Mutual referral attempt blocked: {user_telegram_id} is already referrer for {referrer_id}")
|
||||
return False
|
||||
|
||||
existing_referral = await db.get_referral_by_referred_id(user_telegram_id)
|
||||
|
||||
if existing_referral:
|
||||
logger.info(f"User {user_telegram_id} already has referrer")
|
||||
return False
|
||||
|
||||
referral_code = await db.generate_unique_referral_code(referrer_id)
|
||||
|
||||
referral = await db.create_referral(referrer_id, user_telegram_id, referral_code)
|
||||
|
||||
if referral:
|
||||
logger.info(f"Created referral: {referrer_id} -> {user_telegram_id} with code {referral_code}")
|
||||
|
||||
if bot:
|
||||
try:
|
||||
referrer = await db.get_user_by_telegram_id(referrer_id)
|
||||
if referrer:
|
||||
import os
|
||||
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
|
||||
first_reward = float(os.getenv('REFERRAL_FIRST_REWARD', '150.0'))
|
||||
percentage = float(os.getenv('REFERRAL_PERCENTAGE', '0.25'))
|
||||
|
||||
await bot.send_message(
|
||||
referrer_id,
|
||||
f"🎉 Отлично! По вашей ссылке зарегистрировался новый пользователь!\n\n"
|
||||
f"Вы получите {first_reward:.0f}₽ после того, как он пополнит баланс на {threshold:.0f}₽.\n"
|
||||
f"И будете получать {percentage*100:.0f}% с каждого его пополнения баланса!"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to notify referrer: {e}")
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.warning(f"Invalid referral parameter: {start_param}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating referral from start param: {e}")
|
||||
return False
|
||||
|
||||
async def create_referral_from_promocode(user_telegram_id: int, referral_code: str, db: Database, bot=None):
|
||||
try:
|
||||
if not referral_code.startswith("REF"):
|
||||
return False
|
||||
|
||||
logger.info(f"Trying to use referral code {referral_code} for user {user_telegram_id}")
|
||||
|
||||
async with db.session_factory() as session:
|
||||
from sqlalchemy import select
|
||||
result = await session.execute(
|
||||
select(ReferralProgram).where(ReferralProgram.referral_code == referral_code)
|
||||
)
|
||||
referral_record = result.scalar_one_or_none()
|
||||
|
||||
if not referral_record:
|
||||
logger.warning(f"No referrer found for code {referral_code}")
|
||||
return False
|
||||
|
||||
referrer_id = referral_record.referrer_id
|
||||
|
||||
if referrer_id == user_telegram_id:
|
||||
logger.warning(f"User {user_telegram_id} tried to use own referral code")
|
||||
return False
|
||||
|
||||
existing_reverse_referral = await db.get_referral_by_referred_id(referrer_id)
|
||||
if existing_reverse_referral and existing_reverse_referral.referrer_id == user_telegram_id:
|
||||
logger.warning(f"Mutual referral attempt blocked: {user_telegram_id} is already referrer for {referrer_id}")
|
||||
return False
|
||||
|
||||
existing_referral = await db.get_referral_by_referred_id(user_telegram_id)
|
||||
|
||||
if existing_referral:
|
||||
logger.info(f"User {user_telegram_id} already has referrer")
|
||||
return False
|
||||
|
||||
referral = await db.create_referral(referrer_id, user_telegram_id, referral_code)
|
||||
|
||||
if referral:
|
||||
logger.info(f"Created referral from promocode: {referrer_id} -> {user_telegram_id}")
|
||||
|
||||
if bot:
|
||||
try:
|
||||
import os
|
||||
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
|
||||
first_reward = float(os.getenv('REFERRAL_FIRST_REWARD', '150.0'))
|
||||
|
||||
await bot.send_message(
|
||||
referrer_id,
|
||||
f"🎉 По вашему промокоду {referral_code} зарегистрировался новый пользователь!\n\n"
|
||||
f"Вы получите {first_reward:.0f}₽ после того, как он пополнит баланс на {threshold:.0f}₽."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to notify referrer: {e}")
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating referral from promocode: {e}")
|
||||
return False
|
||||
|
||||
def generate_referral_link(bot_username: str, user_id: int) -> str:
|
||||
if not bot_username:
|
||||
return ""
|
||||
|
||||
if bot_username.startswith('@'):
|
||||
bot_username = bot_username[1:]
|
||||
|
||||
return f"https://t.me/{bot_username}?start=ref_{user_id}"
|
||||
|
||||
def validate_referral_code(code: str) -> bool:
|
||||
if not code or not code.startswith("REF"):
|
||||
return False
|
||||
if len(code) < 4 or len(code) > 20:
|
||||
return False
|
||||
return True
|
||||
|
||||
def format_referral_stats(stats: dict, lang: str = 'ru') -> str:
|
||||
if lang == 'ru':
|
||||
return (f"👥 Приглашено: {stats['total_referrals']}\n"
|
||||
f"✅ Активных: {stats['active_referrals']}\n"
|
||||
f"💰 Заработано: {stats['total_earned']:.2f}₽")
|
||||
else:
|
||||
return (f"👥 Invited: {stats['total_referrals']}\n"
|
||||
f"✅ Active: {stats['active_referrals']}\n"
|
||||
f"💰 Earned: ${stats['total_earned']:.2f}")
|
||||
@@ -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,348 @@
|
||||
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"❌ Если передумали - нажмите кнопку ниже",
|
||||
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):
|
||||
"""Обработка pre-checkout запроса для звезд"""
|
||||
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')
|
||||
|
||||
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 config and config.STARS_RATES:
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
await callback.message.answer(
|
||||
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
|
||||
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
else:
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
"❌ Платеж отменен",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
await callback.message.answer(
|
||||
"❌ Платеж отменен",
|
||||
reply_markup=balance_keyboard(user.language)
|
||||
)
|
||||
|
||||
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,10 +1,7 @@
|
||||
TRANSLATIONS = {
|
||||
'ru': {
|
||||
# Language selection
|
||||
'select_language': '🌍 Выберите язык / Choose language',
|
||||
'language_selected': '🇷🇺 Язык выбран: Русский',
|
||||
|
||||
# Main menu
|
||||
'main_menu': '📱 Главное меню',
|
||||
'balance': 'Баланс',
|
||||
'my_subscriptions': 'Мои подписки',
|
||||
@@ -18,9 +15,6 @@ TRANSLATIONS = {
|
||||
'trial_error': '❌ Ошибка при создании тестовой подписки',
|
||||
'trial_info': '🧪 Тестовая подписка выдается на три дня!\n\nНа тарифе действует ограничение в 3 дня\n\nОграничение по трафику - 2гб',
|
||||
'subscriptions_list': '📋 Список подписок в продаже:',
|
||||
|
||||
|
||||
# Balance menu
|
||||
'your_balance': '💰 Ваш баланс: {balance:.2f} руб.',
|
||||
'topup_balance': 'Пополнить баланс',
|
||||
'payment_history': 'История платежей',
|
||||
@@ -31,6 +25,11 @@ TRANSLATIONS = {
|
||||
'nodes_management': 'Управление нодами',
|
||||
'system_users': 'Системные пользователи',
|
||||
'system_statistics': 'Системная статистика',
|
||||
'subscription_expired_message': '❌ Ваша подписка \'{name}\' истекла!\n\n🔄 Для возобновления доступа продлите подписку в разделе \'Мои подписки\'.\n\n💡 Подписку можно продлить в любой момент - срок действия будет увеличен с текущего момента.',
|
||||
'subscription_expires_today': '⚠️ Срочно! Ваша подписка \'{name}\' истекает сегодня!\n\n🔄 Продлите её прямо сейчас в разделе \'Мои подписки\', чтобы не потерять доступ.\n\n⏰ У вас есть несколько часов до истечения!',
|
||||
'subscription_expires_tomorrow': '⚠️ Внимание! Ваша подписка \'{name}\' истекает завтра!\n\n🔄 Чтобы не потерять доступ, продлите её сейчас в разделе \'Мои подписки\'.\n\n✨ При продлении срок действия будет увеличен с момента истечения.',
|
||||
'subscription_expires_day_after_tomorrow': '⏰ Ваша подписка \'{name}\' истекает послезавтра ({days} дня)!\n\n🔄 Рекомендуем продлить её заранее в разделе \'Мои подписки\'.\n\n💰 Проверьте баланс - возможно, стоит пополнить его заранее.',
|
||||
'subscription_expires_in_days': '⏳ Ваша подписка \'{name}\' истекает через {days} дн.!\n\n🔄 Продлить подписку можно в разделе \'Мои подписки\'.\n\n💡 Продление заранее увеличивает срок с момента истечения текущей подписки.',
|
||||
'restart_nodes': 'Перезагрузить ноды',
|
||||
'bulk_operations': 'Массовые операции',
|
||||
'search_user': 'Поиск пользователя',
|
||||
@@ -38,7 +37,6 @@ TRANSLATIONS = {
|
||||
'reset_traffic': 'Сбросить трафик',
|
||||
'disable_user': 'Отключить пользователя',
|
||||
'enable_user': 'Включить пользователя',
|
||||
|
||||
'send_message': 'Отправить сообщение',
|
||||
'send_to_user': 'Отправить пользователю',
|
||||
'send_to_all': 'Отправить всем',
|
||||
@@ -53,8 +51,6 @@ TRANSLATIONS = {
|
||||
'subscription_extended': '✅ Подписка продлена',
|
||||
'extend_confirmation': 'Продлить подписку "{name}" на {days} дней за {price:.2f} руб.?',
|
||||
'subscription_expires_soon': 'Подписка истекает через {days} дней',
|
||||
|
||||
# Subscriptions
|
||||
'no_subscriptions': '❌ У вас нет активных подписок',
|
||||
'subscription_info': '📋 Подписка: {name}\n💰 Цена: {price:.2f} руб.\n⏱ Длительность: {days} дней\n📊 Трафик: {traffic}\n\n{description}',
|
||||
'unlimited_traffic': 'Безлимитный',
|
||||
@@ -64,32 +60,37 @@ TRANSLATIONS = {
|
||||
'subscription_expires': 'Истекает: {date}',
|
||||
'subscription_expired': 'Истекла',
|
||||
'subscription_active': 'Активна до: {date}',
|
||||
|
||||
# Payments
|
||||
'your_subscriptions': '👀 Список ваших подписок:',
|
||||
'payment_created': '✅ Заявка на пополнение создана.\nОбратитесь к @{support} для оплаты.',
|
||||
'enter_amount': '💰 Введите сумму для пополнения (руб.):',
|
||||
'invalid_amount': '❌ Неверная сумма',
|
||||
'payment_card_info': '💳 Для пополнения картой свяжитесь с @{support}',
|
||||
'no_payments': '❌ История платежей пуста',
|
||||
'payment_item': '{date}: {amount:.2f} руб. - {description} ({status})',
|
||||
|
||||
# Promocodes
|
||||
'enter_promocode': '🎁 Введите промокод:',
|
||||
'promocode_success': '✅ Промокод активирован! Скидка: {discount}',
|
||||
'promocode_not_found': '❌ Промокод не найден',
|
||||
'promocode_expired': '❌ Промокод истек',
|
||||
'promocode_used': '❌ Промокод уже использован',
|
||||
'promocode_limit': '❌ Лимит использований исчерпан',
|
||||
|
||||
# Purchase
|
||||
'referral_program': 'Реферальная программа',
|
||||
'referral_conditions': 'Условия программы',
|
||||
'referral_link': 'Ваша реферальная ссылка',
|
||||
'referral_code': 'Ваш промокод',
|
||||
'referral_stats': 'Статистика',
|
||||
'total_referrals': 'Всего приглашено',
|
||||
'active_referrals': 'Активных рефералов',
|
||||
'total_earned': 'Заработано',
|
||||
'my_referrals': 'Мои рефералы',
|
||||
'referral_reward_referrer': 'За каждого приглашенного: 150₽',
|
||||
'referral_reward_referred': 'Бонус новичку: 150₽ после пополнения на 300₽',
|
||||
'referral_percentage': 'Процент с платежей: 25%',
|
||||
'no_referrals': 'У вас пока нет рефералов',
|
||||
'referral_welcome': 'Добро пожаловать по реферальной ссылке!',
|
||||
'insufficient_balance': '❌ Недостаточно средств на балансе',
|
||||
'subscription_purchased': '✅ Подписка успешно приобретена!',
|
||||
'purchase_error': '❌ Ошибка при покупке подписки',
|
||||
|
||||
# Support
|
||||
'support_message': '🎧 Для получения поддержки обратитесь к @{support}',
|
||||
|
||||
# Admin panel
|
||||
'admin_menu': '⚙️ Админ панель',
|
||||
'manage_subscriptions': 'Управление подписками',
|
||||
'manage_users': 'Управление пользователями',
|
||||
@@ -97,8 +98,6 @@ TRANSLATIONS = {
|
||||
'manage_promocodes': 'Управление промокодами',
|
||||
'statistics': 'Статистика',
|
||||
'not_admin': '❌ У вас нет прав администратора',
|
||||
|
||||
# Admin - subscriptions
|
||||
'create_subscription': 'Создать подписку',
|
||||
'enter_sub_name': 'Введите название подписки:',
|
||||
'enter_sub_description': 'Введите описание подписки:',
|
||||
@@ -107,43 +106,58 @@ TRANSLATIONS = {
|
||||
'enter_sub_traffic': 'Введите лимит трафика (ГБ, 0 для безлимита):',
|
||||
'enter_squad_uuid': 'Введите UUID squad из панели:',
|
||||
'subscription_created': '✅ Подписка создана',
|
||||
|
||||
# Admin - users
|
||||
'extend_subscription_btn': '🔄 Продлить подписку',
|
||||
'my_subscriptions_btn': '📋 Мои подписки',
|
||||
'buy_new_subscription_btn': '🛒 Купить новую подписку',
|
||||
'restore_subscription_btn': '🔄 Восстановить подписку',
|
||||
'user_list': 'Список пользователей:',
|
||||
'user_item': 'ID: {id}, @{username}, Баланс: {balance:.2f} руб.',
|
||||
'enter_user_id': 'Введите Telegram ID пользователя:',
|
||||
'enter_balance_amount': 'Введите сумму для пополнения:',
|
||||
'balance_added': '✅ Баланс пополнен',
|
||||
'user_not_found': '❌ Пользователь не найден',
|
||||
|
||||
# Admin - promocodes
|
||||
'create_promocode': 'Создать промокод',
|
||||
'enter_promo_code': 'Введите код промокода:',
|
||||
'enter_promo_discount': 'Введите размер скидки (руб.):',
|
||||
'enter_promo_limit': 'Введите лимит использований:',
|
||||
'promocode_created': '✅ Промокод создан',
|
||||
'promocode_exists': '❌ Промокод уже существует',
|
||||
|
||||
# Statistics
|
||||
'stats_info': '📊 Статистика:\n👥 Пользователей: {users}\n📋 Подписок: {subscriptions}\n💰 Выручка: {revenue:.2f} руб.',
|
||||
|
||||
# Errors
|
||||
'error_occurred': '❌ Произошла ошибка',
|
||||
'try_again': 'Попробуйте еще раз',
|
||||
'invalid_input': '❌ Неверный ввод',
|
||||
'cancel': 'Отмена',
|
||||
|
||||
# Connection
|
||||
'connection_link': '🔗 Ссылка для подключения:\n\n`{link}`\n\nНажмите на ссылку чтобы скопировать',
|
||||
'connection_error': '❌ Ошибка получения ссылки подключения',
|
||||
'lucky_game': 'Игра удачи',
|
||||
'lucky_game_title': '🎰 Проверь свою удачу!',
|
||||
'lucky_game_description': 'Выберите число от 1 до {numbers} и попытайтесь угадать выигрышное!',
|
||||
'lucky_game_reward': 'Награда за выигрыш: {reward}₽',
|
||||
'lucky_game_rules': 'Правила игры',
|
||||
'lucky_game_play': 'Играть!',
|
||||
'lucky_game_history': 'История игр',
|
||||
'lucky_game_stats': 'Ваша статистика',
|
||||
'lucky_game_choose_number': 'Выберите число от 1 до {numbers}',
|
||||
'lucky_game_winner': '🎉 ПОЗДРАВЛЯЕМ! ВЫ ВЫИГРАЛИ!',
|
||||
'lucky_game_loser': '😔 Не повезло в этот раз...',
|
||||
'lucky_game_your_number': 'Ваше число: {number}',
|
||||
'lucky_game_winning_numbers': 'Выигрышные числа: {numbers}',
|
||||
'lucky_game_reward_added': 'Награда {reward}₽ зачислена на баланс!',
|
||||
'lucky_game_new_balance': 'Новый баланс: {balance}₽',
|
||||
'lucky_game_try_tomorrow': 'Попробуйте завтра!',
|
||||
'lucky_game_already_played': 'Вы уже играли сегодня! Приходите завтра.',
|
||||
'lucky_game_come_back_in': 'Приходи через {time}',
|
||||
'lucky_game_no_history': 'Вы еще не играли в игру удачи.',
|
||||
'lucky_game_start_playing': 'Начните играть, чтобы увидеть историю!',
|
||||
'lucky_game_games_played': 'Игр сыграно: {count}',
|
||||
'lucky_game_wins': 'Выигрышей: {count}',
|
||||
'lucky_game_total_won': 'Всего выиграно: {amount}₽',
|
||||
'lucky_game_win_rate': 'Процент побед: {rate}%'
|
||||
},
|
||||
|
||||
'en': {
|
||||
# Language selection
|
||||
'select_language': '🌍 Choose language / Выберите язык',
|
||||
'language_selected': '🇺🇸 Language selected: English',
|
||||
|
||||
# Main menu
|
||||
'main_menu': '📱 Main Menu',
|
||||
'balance': 'Balance',
|
||||
'my_subscriptions': 'My Subscriptions',
|
||||
@@ -169,16 +183,12 @@ TRANSLATIONS = {
|
||||
'reset_traffic': 'Reset Traffic',
|
||||
'disable_user': 'Disable User',
|
||||
'enable_user': 'Enable User',
|
||||
|
||||
# Balance menu
|
||||
'your_balance': '💰 Your balance: ${balance:.2f}',
|
||||
'topup_balance': 'Top up balance',
|
||||
'payment_history': 'Payment history',
|
||||
'topup_card': 'Card payment',
|
||||
'topup_support': 'Through support',
|
||||
'back': 'Back',
|
||||
|
||||
# Subscriptions
|
||||
'no_subscriptions': '❌ You have no active subscriptions',
|
||||
'subscription_info': '📋 Subscription: {name}\n💰 Price: ${price:.2f}\n⏱ Duration: {days} days\n📊 Traffic: {traffic}\n\n{description}',
|
||||
'unlimited_traffic': 'Unlimited',
|
||||
@@ -188,32 +198,45 @@ TRANSLATIONS = {
|
||||
'subscription_expires': 'Expires: {date}',
|
||||
'subscription_expired': 'Expired',
|
||||
'subscription_active': 'Active until: {date}',
|
||||
|
||||
# Payments
|
||||
'subscription_expired_message': '❌ Your subscription \'{name}\' has expired!\n\nTo renew, go to \'My Subscriptions\'.\n\n💡 You can renew at any time - the term will be extended from the current moment.',
|
||||
'subscription_expires_today': '⚠️ Urgent! Your subscription \'{name}\' expires today!\n\nRenew it now in \'My Subscriptions\' to keep access.\n\n⏰ You have a few hours left!',
|
||||
'subscription_expires_tomorrow': '⚠️ Your subscription \'{name}\' expires tomorrow!\n\nDon\'t forget to renew it in \'My Subscriptions\'.\n\n✨ Renewal will extend the term from expiration date.',
|
||||
'subscription_expires_day_after_tomorrow': '⏰ Your subscription \'{name}\' expires in {days} days!\n\nWe recommend renewing it in advance in \'My Subscriptions\'.\n\n💰 Check your balance - you might want to top it up.',
|
||||
'subscription_expires_in_days': '⏳ Your subscription \'{name}\' expires in {days} days!\n\nYou can renew it in \'My Subscriptions\'.\n\n💡 Early renewal extends from current expiration date.',
|
||||
'extend_subscription_btn': '🔄 Extend Subscription',
|
||||
'my_subscriptions_btn': '📋 My Subscriptions',
|
||||
'buy_new_subscription_btn': '🛒 Buy New Subscription',
|
||||
'restore_subscription_btn': '🔄 Restore Subscription',
|
||||
'payment_created': '✅ Top-up request created.\nContact @{support} for payment.',
|
||||
'enter_amount': '💰 Enter amount to top up ($):',
|
||||
'invalid_amount': '❌ Invalid amount',
|
||||
'payment_card_info': '💳 For card payment contact @{support}',
|
||||
'no_payments': '❌ Payment history is empty',
|
||||
'payment_item': '{date}: ${amount:.2f} - {description} ({status})',
|
||||
|
||||
# Promocodes
|
||||
'referral_program': 'Referral Program',
|
||||
'referral_conditions': 'Program Conditions',
|
||||
'referral_link': 'Your referral link',
|
||||
'referral_code': 'Your promo code',
|
||||
'referral_stats': 'Statistics',
|
||||
'total_referrals': 'Total invited',
|
||||
'active_referrals': 'Active referrals',
|
||||
'total_earned': 'Total earned',
|
||||
'my_referrals': 'My referrals',
|
||||
'referral_reward_referrer': 'For each invited: $150',
|
||||
'referral_reward_referred': 'Newbie bonus: $150 after topping up $300',
|
||||
'referral_percentage': 'Percentage from payments: 25%',
|
||||
'no_referrals': 'You have no referrals yet',
|
||||
'referral_welcome': 'Welcome via referral link!',
|
||||
'enter_promocode': '🎁 Enter promocode:',
|
||||
'promocode_success': '✅ Promocode activated! Discount: {discount}',
|
||||
'promocode_not_found': '❌ Promocode not found',
|
||||
'promocode_expired': '❌ Promocode expired',
|
||||
'promocode_used': '❌ Promocode already used',
|
||||
'promocode_limit': '❌ Usage limit reached',
|
||||
|
||||
# Purchase
|
||||
'insufficient_balance': '❌ Insufficient balance',
|
||||
'subscription_purchased': '✅ Subscription purchased successfully!',
|
||||
'purchase_error': '❌ Error purchasing subscription',
|
||||
|
||||
# Support
|
||||
'support_message': '🎧 For support contact @{support}',
|
||||
|
||||
# Admin panel
|
||||
'admin_menu': '⚙️ Admin Panel',
|
||||
'manage_subscriptions': 'Manage Subscriptions',
|
||||
'manage_users': 'Manage Users',
|
||||
@@ -221,8 +244,6 @@ TRANSLATIONS = {
|
||||
'manage_promocodes': 'Manage Promocodes',
|
||||
'statistics': 'Statistics',
|
||||
'not_admin': '❌ You don\'t have admin rights',
|
||||
|
||||
# Admin - subscriptions
|
||||
'create_subscription': 'Create Subscription',
|
||||
'enter_sub_name': 'Enter subscription name:',
|
||||
'enter_sub_description': 'Enter subscription description:',
|
||||
@@ -231,40 +252,53 @@ TRANSLATIONS = {
|
||||
'enter_sub_traffic': 'Enter traffic limit (GB, 0 for unlimited):',
|
||||
'enter_squad_uuid': 'Enter squad UUID from panel:',
|
||||
'subscription_created': '✅ Subscription created',
|
||||
|
||||
# Admin - users
|
||||
'user_list': '👥 User list:',
|
||||
'user_item': 'ID: {id}, @{username}, Balance: ${balance:.2f}',
|
||||
'enter_user_id': 'Enter user Telegram ID:',
|
||||
'enter_balance_amount': 'Enter amount to add:',
|
||||
'balance_added': '✅ Balance added',
|
||||
'user_not_found': '❌ User not found',
|
||||
|
||||
# Admin - promocodes
|
||||
'create_promocode': 'Create Promocode',
|
||||
'enter_promo_code': 'Enter promocode:',
|
||||
'enter_promo_discount': 'Enter discount amount ($):',
|
||||
'enter_promo_limit': 'Enter usage limit:',
|
||||
'promocode_created': '✅ Promocode created',
|
||||
'promocode_exists': '❌ Promocode already exists',
|
||||
|
||||
# Statistics
|
||||
'stats_info': '📊 Statistics:\n👥 Users: {users}\n📋 Subscriptions: {subscriptions}\n💰 Revenue: ${revenue:.2f}',
|
||||
|
||||
# Errors
|
||||
'error_occurred': '❌ An error occurred',
|
||||
'try_again': 'Try again',
|
||||
'invalid_input': '❌ Invalid input',
|
||||
'cancel': 'Cancel',
|
||||
|
||||
# Connection
|
||||
'connection_link': '🔗 Connection link:\n\n`{link}`\n\nClick the link to copy',
|
||||
'connection_error': '❌ Error getting connection link',
|
||||
'lucky_game': 'Lucky Game',
|
||||
'lucky_game_title': '🎰 Test Your Luck!',
|
||||
'lucky_game_description': 'Choose a number from 1 to {numbers} and try to guess the winning one!',
|
||||
'lucky_game_reward': 'Winning reward: ${reward}',
|
||||
'lucky_game_rules': 'Game Rules',
|
||||
'lucky_game_play': 'Play!',
|
||||
'lucky_game_history': 'Game History',
|
||||
'lucky_game_stats': 'Your Statistics',
|
||||
'lucky_game_choose_number': 'Choose a number from 1 to {numbers}',
|
||||
'lucky_game_winner': '🎉 CONGRATULATIONS! YOU WON!',
|
||||
'lucky_game_loser': '😔 Better luck next time...',
|
||||
'lucky_game_your_number': 'Your number: {number}',
|
||||
'lucky_game_winning_numbers': 'Winning numbers: {numbers}',
|
||||
'lucky_game_reward_added': 'Reward ${reward} added to balance!',
|
||||
'lucky_game_new_balance': 'New balance: ${balance}',
|
||||
'lucky_game_try_tomorrow': 'Try again tomorrow!',
|
||||
'lucky_game_already_played': 'You already played today! Come back tomorrow.',
|
||||
'lucky_game_come_back_in': 'Come back in {time}',
|
||||
'lucky_game_no_history': 'You haven\'t played the lucky game yet.',
|
||||
'lucky_game_start_playing': 'Start playing to see history!',
|
||||
'lucky_game_games_played': 'Games played: {count}',
|
||||
'lucky_game_wins': 'Wins: {count}',
|
||||
'lucky_game_total_won': 'Total won: ${amount}',
|
||||
'lucky_game_win_rate': 'Win rate: {rate}%'
|
||||
}
|
||||
}
|
||||
|
||||
def t(key: str, lang: str = 'ru', **kwargs) -> str:
|
||||
"""Get translation for key with optional formatting"""
|
||||
translation = TRANSLATIONS.get(lang, TRANSLATIONS['ru']).get(key, key)
|
||||
if kwargs:
|
||||
try:
|
||||
|
||||
@@ -1,194 +1,110 @@
|
||||
import re
|
||||
import uuid
|
||||
import logging
|
||||
import secrets
|
||||
import string
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List, Dict, Any, Tuple
|
||||
import logging
|
||||
from typing import Tuple, Optional, Dict, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def generate_username() -> str:
|
||||
"""Generate random username for RemnaWave"""
|
||||
return f"user_{secrets.token_hex(8)}"
|
||||
|
||||
def generate_password() -> str:
|
||||
"""Generate random password"""
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(12))
|
||||
|
||||
def generate_promocode() -> str:
|
||||
"""Generate random promocode"""
|
||||
alphabet = string.ascii_uppercase + string.digits
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(8))
|
||||
|
||||
def is_valid_email(email: str) -> bool:
|
||||
"""Validate email format"""
|
||||
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
return re.match(pattern, email) is not None
|
||||
|
||||
def is_valid_amount(amount_str: str) -> Tuple[bool, float]:
|
||||
"""Validate and parse amount"""
|
||||
def is_valid_amount(text: str) -> Tuple[bool, float]:
|
||||
try:
|
||||
amount = float(amount_str.replace(',', '.'))
|
||||
text = text.strip().replace(' ', '').replace(',', '.')
|
||||
|
||||
amount = float(text)
|
||||
|
||||
if amount <= 0:
|
||||
return False, 0
|
||||
if amount > 100000: # Max amount limit
|
||||
return False, 0
|
||||
return False, 0.0
|
||||
|
||||
if amount > 1000000:
|
||||
return False, 0.0
|
||||
|
||||
amount = round(amount, 2)
|
||||
|
||||
return True, amount
|
||||
except ValueError:
|
||||
return False, 0
|
||||
|
||||
def format_date(date: datetime, lang: str = 'ru') -> str:
|
||||
"""Format date for display"""
|
||||
if lang == 'ru':
|
||||
months = [
|
||||
'января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
|
||||
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'
|
||||
]
|
||||
return f"{date.day} {months[date.month-1]} {date.year}"
|
||||
else:
|
||||
return date.strftime("%B %d, %Y")
|
||||
|
||||
def format_datetime(date: datetime, lang: str = 'ru') -> str:
|
||||
"""Format datetime for display"""
|
||||
if lang == 'ru':
|
||||
return date.strftime("%d.%m.%Y %H:%M")
|
||||
else:
|
||||
return date.strftime("%m/%d/%Y %H:%M")
|
||||
|
||||
def calculate_expiry_date(days: int) -> str:
|
||||
"""Calculate expiry date in ISO format"""
|
||||
expiry = datetime.utcnow() + timedelta(days=days)
|
||||
return expiry.isoformat() + 'Z'
|
||||
|
||||
def parse_telegram_id(text: str) -> Optional[int]:
|
||||
"""Parse Telegram ID from text"""
|
||||
try:
|
||||
telegram_id = int(text.strip())
|
||||
if telegram_id > 0:
|
||||
return telegram_id
|
||||
except ValueError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def format_traffic(gb: int, lang: str = 'ru') -> str:
|
||||
"""Format traffic limit for display"""
|
||||
if gb == 0:
|
||||
return "Безлимитный" if lang == 'ru' else "Unlimited"
|
||||
else:
|
||||
return f"{gb} ГБ" if lang == 'ru' else f"{gb} GB"
|
||||
|
||||
def paginate_list(items: List[Any], page: int, per_page: int = 10) -> Tuple[List[Any], int]:
|
||||
"""Paginate list of items"""
|
||||
total_pages = (len(items) + per_page - 1) // per_page
|
||||
start_idx = (page - 1) * per_page
|
||||
end_idx = start_idx + per_page
|
||||
return items[start_idx:end_idx], total_pages
|
||||
|
||||
def escape_markdown(text: str) -> str:
|
||||
"""Escape markdown special characters"""
|
||||
special_chars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']
|
||||
for char in special_chars:
|
||||
text = text.replace(char, f'\\{char}')
|
||||
return text
|
||||
|
||||
def truncate_text(text: str, max_length: int = 4000) -> str:
|
||||
"""Truncate text to fit Telegram message limits"""
|
||||
if len(text) <= max_length:
|
||||
return text
|
||||
return text[:max_length-3] + "..."
|
||||
|
||||
def validate_squad_uuid(uuid_str: str) -> bool:
|
||||
"""Validate UUID format"""
|
||||
try:
|
||||
uuid.UUID(uuid_str)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def format_subscription_info(subscription: Dict[str, Any], lang: str = 'ru') -> str:
|
||||
"""Format subscription information for display"""
|
||||
from translations import t
|
||||
|
||||
traffic = format_traffic(subscription['traffic_limit_gb'], lang)
|
||||
|
||||
info = t('subscription_info', lang,
|
||||
name=subscription['name'],
|
||||
price=subscription['price'],
|
||||
days=subscription['duration_days'],
|
||||
traffic=traffic,
|
||||
description=subscription.get('description', '')
|
||||
)
|
||||
|
||||
return info
|
||||
|
||||
def format_user_subscription_info(user_sub: Dict[str, Any], subscription: Dict[str, Any],
|
||||
expires_at: datetime, lang: str = 'ru') -> str:
|
||||
"""Format user subscription information"""
|
||||
from translations import t
|
||||
|
||||
traffic = format_traffic(subscription['traffic_limit_gb'], lang)
|
||||
|
||||
# Check if expired
|
||||
now = datetime.utcnow()
|
||||
if expires_at < now:
|
||||
status = t('subscription_expired', lang)
|
||||
else:
|
||||
status = t('subscription_active', lang, date=format_date(expires_at, lang))
|
||||
|
||||
info = f"📋 {subscription['name']}\n"
|
||||
info += f"⏱ {subscription['duration_days']} дней\n" if lang == 'ru' else f"⏱ {subscription['duration_days']} days\n"
|
||||
info += f"📊 {traffic}\n"
|
||||
info += f"🕒 {status}\n"
|
||||
|
||||
if subscription.get('description'):
|
||||
info += f"\n{subscription['description']}"
|
||||
|
||||
return info
|
||||
|
||||
except (ValueError, TypeError):
|
||||
return False, 0.0
|
||||
|
||||
def validate_promocode_format(code: str) -> bool:
|
||||
"""Validate promocode format"""
|
||||
if not code:
|
||||
return False
|
||||
|
||||
code = code.strip().upper()
|
||||
|
||||
if len(code) < 3 or len(code) > 20:
|
||||
return False
|
||||
if not re.match(r'^[A-Z0-9]+$', code.upper()):
|
||||
|
||||
if not code.replace('_', '').isalnum():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def calculate_discount(original_price: float, promocode: Dict[str, Any]) -> float:
|
||||
"""Calculate discount amount"""
|
||||
if promocode.get('discount_percent'):
|
||||
return original_price * (promocode['discount_percent'] / 100)
|
||||
def validate_squad_uuid(uuid: str) -> bool:
|
||||
if not uuid or not isinstance(uuid, str):
|
||||
return False
|
||||
|
||||
uuid = uuid.strip()
|
||||
|
||||
if len(uuid) < 8:
|
||||
return False
|
||||
|
||||
allowed_chars = set('0123456789abcdefABCDEF-')
|
||||
if not all(c in allowed_chars for c in uuid):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def parse_telegram_id(text: str) -> Optional[int]:
|
||||
try:
|
||||
text = text.strip().replace(' ', '')
|
||||
|
||||
if text.startswith('@'):
|
||||
text = text[1:]
|
||||
|
||||
if text.startswith('id'):
|
||||
text = text[2:]
|
||||
|
||||
telegram_id = int(text)
|
||||
|
||||
if telegram_id <= 0 or telegram_id > 9999999999:
|
||||
return None
|
||||
|
||||
return telegram_id
|
||||
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
def generate_username() -> str:
|
||||
prefix = "user_"
|
||||
random_part = ''.join(secrets.choice(string.ascii_lowercase + string.digits) for _ in range(8))
|
||||
return prefix + random_part
|
||||
|
||||
def generate_password() -> str:
|
||||
return ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(12))
|
||||
|
||||
def calculate_expiry_date(days: int) -> str:
|
||||
expiry_date = datetime.now() + timedelta(days=days)
|
||||
return expiry_date.isoformat() + 'Z'
|
||||
|
||||
def format_datetime(dt: datetime, language: str = 'ru') -> str:
|
||||
if not dt:
|
||||
return "N/A"
|
||||
|
||||
if language == 'ru':
|
||||
return dt.strftime('%d.%m.%Y %H:%M')
|
||||
else:
|
||||
return min(promocode.get('discount_amount', 0), original_price)
|
||||
return dt.strftime('%Y-%m-%d %H:%M')
|
||||
|
||||
def format_payment_status(status: str, lang: str = 'ru') -> str:
|
||||
"""Format payment status for display"""
|
||||
status_map = {
|
||||
'pending': 'В ожидании' if lang == 'ru' else 'Pending',
|
||||
'completed': 'Завершен' if lang == 'ru' else 'Completed',
|
||||
'cancelled': 'Отменен' if lang == 'ru' else 'Cancelled',
|
||||
'failed': 'Ошибка' if lang == 'ru' else 'Failed'
|
||||
}
|
||||
return status_map.get(status, status)
|
||||
|
||||
def clean_phone_number(phone: str) -> str:
|
||||
"""Clean and format phone number"""
|
||||
# Remove all non-digit characters
|
||||
digits = re.sub(r'\D', '', phone)
|
||||
def format_date(dt: datetime, language: str = 'ru') -> str:
|
||||
if not dt:
|
||||
return "N/A"
|
||||
|
||||
# Handle Russian phone numbers
|
||||
if digits.startswith('8') and len(digits) == 11:
|
||||
digits = '7' + digits[1:]
|
||||
elif digits.startswith('9') and len(digits) == 10:
|
||||
digits = '7' + digits
|
||||
|
||||
return digits
|
||||
if language == 'ru':
|
||||
return dt.strftime('%d.%m.%Y')
|
||||
else:
|
||||
return dt.strftime('%Y-%m-%d')
|
||||
|
||||
def format_bytes(bytes_value: int) -> str:
|
||||
"""Format bytes to human readable format"""
|
||||
if bytes_value == 0:
|
||||
return "0 B"
|
||||
|
||||
@@ -200,83 +116,159 @@ def format_bytes(bytes_value: int) -> str:
|
||||
value /= 1024
|
||||
unit_index += 1
|
||||
|
||||
if unit_index == 0:
|
||||
return f"{int(value)} {units[unit_index]}"
|
||||
else:
|
||||
if value >= 100:
|
||||
return f"{value:.0f} {units[unit_index]}"
|
||||
elif value >= 10:
|
||||
return f"{value:.1f} {units[unit_index]}"
|
||||
|
||||
def get_subscription_connection_url(base_url: str, short_uuid: str) -> str:
|
||||
"""Generate subscription connection URL"""
|
||||
return f"{base_url.rstrip('/')}/api/sub/{short_uuid}"
|
||||
|
||||
def log_user_action(telegram_id: int, action: str, details: str = None):
|
||||
"""Log user action for audit"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
log_message = f"Admin action by {telegram_id}: {action}"
|
||||
if details:
|
||||
log_message += f" - {details}"
|
||||
|
||||
logger.info(log_message)
|
||||
|
||||
def format_subscription_status(expires_at: datetime, lang: str = 'ru') -> str:
|
||||
"""Format subscription status with emoji"""
|
||||
now = datetime.utcnow()
|
||||
|
||||
if expires_at < now:
|
||||
return "❌ Истекла" if lang == 'ru' else "❌ Expired"
|
||||
|
||||
days_left = (expires_at - now).days
|
||||
|
||||
if days_left == 0:
|
||||
return "⚠️ Истекает сегодня" if lang == 'ru' else "⚠️ Expires today"
|
||||
elif days_left == 1:
|
||||
return "⚠️ Истекает завтра" if lang == 'ru' else "⚠️ Expires tomorrow"
|
||||
elif days_left <= 3:
|
||||
return f"🔶 Осталось {days_left} дней" if lang == 'ru' else f"🔶 {days_left} days left"
|
||||
else:
|
||||
return f"✅ Активна ({days_left} дней)" if lang == 'ru' else f"✅ Active ({days_left} days)"
|
||||
return f"{value:.2f} {units[unit_index]}"
|
||||
|
||||
def format_monitor_notification_type(notification_type: str, lang: str = 'ru') -> str:
|
||||
"""Format notification type for display"""
|
||||
type_map = {
|
||||
'expired': 'Истекла' if lang == 'ru' else 'Expired',
|
||||
'expires_today': 'Истекает сегодня' if lang == 'ru' else 'Expires today',
|
||||
'expires_tomorrow': 'Истекает завтра' if lang == 'ru' else 'Expires tomorrow',
|
||||
'warning': 'Предупреждение' if lang == 'ru' else 'Warning',
|
||||
'urgent': 'Срочно' if lang == 'ru' else 'Urgent'
|
||||
def format_payment_status(status: str, language: str = 'ru') -> str:
|
||||
status_map = {
|
||||
'ru': {
|
||||
'pending': 'Ожидает',
|
||||
'completed': 'Завершен',
|
||||
'cancelled': 'Отменен',
|
||||
'failed': 'Ошибка'
|
||||
},
|
||||
'en': {
|
||||
'pending': 'Pending',
|
||||
'completed': 'Completed',
|
||||
'cancelled': 'Cancelled',
|
||||
'failed': 'Failed'
|
||||
}
|
||||
}
|
||||
return type_map.get(notification_type, notification_type)
|
||||
|
||||
def calculate_days_until_expiry(expires_at: datetime) -> int:
|
||||
"""Calculate days until expiry"""
|
||||
now = datetime.utcnow()
|
||||
delta = expires_at - now
|
||||
return max(0, delta.days)
|
||||
|
||||
def is_subscription_expiring_soon(expires_at: datetime, warning_days: int = 2) -> bool:
|
||||
"""Check if subscription is expiring soon"""
|
||||
days_left = calculate_days_until_expiry(expires_at)
|
||||
return days_left <= warning_days
|
||||
|
||||
class States:
|
||||
"""State constants for FSM"""
|
||||
WAITING_LANGUAGE = "waiting_language"
|
||||
WAITING_AMOUNT = "waiting_amount"
|
||||
WAITING_PROMOCODE = "waiting_promocode"
|
||||
|
||||
# Admin states
|
||||
ADMIN_CREATE_SUB_NAME = "admin_create_sub_name"
|
||||
ADMIN_CREATE_SUB_DESC = "admin_create_sub_desc"
|
||||
ADMIN_CREATE_SUB_PRICE = "admin_create_sub_price"
|
||||
ADMIN_CREATE_SUB_DAYS = "admin_create_sub_days"
|
||||
ADMIN_CREATE_SUB_TRAFFIC = "admin_create_sub_traffic"
|
||||
ADMIN_CREATE_SUB_SQUAD = "admin_create_sub_squad"
|
||||
return status_map.get(language, status_map['ru']).get(status, status)
|
||||
|
||||
def format_subscription_info(subscription: Dict[str, Any], language: str = 'ru') -> str:
|
||||
text = ""
|
||||
|
||||
ADMIN_ADD_BALANCE_USER = "admin_add_balance_user"
|
||||
ADMIN_ADD_BALANCE_AMOUNT = "admin_add_balance_amount"
|
||||
if language == 'ru':
|
||||
text += f"📋 **Подписка: {subscription['name']}**\n\n"
|
||||
text += f"💰 Цена: {subscription['price']} руб.\n"
|
||||
text += f"⏱ Длительность: {subscription['duration_days']} дн.\n"
|
||||
|
||||
if subscription['traffic_limit_gb'] > 0:
|
||||
text += f"📊 Лимит трафика: {subscription['traffic_limit_gb']} ГБ\n"
|
||||
else:
|
||||
text += f"📊 Лимит трафика: Безлимит\n"
|
||||
|
||||
if subscription.get('description'):
|
||||
text += f"\n📝 Описание:\n{subscription['description']}"
|
||||
else:
|
||||
text += f"📋 **Subscription: {subscription['name']}**\n\n"
|
||||
text += f"💰 Price: ${subscription['price']}\n"
|
||||
text += f"⏱ Duration: {subscription['duration_days']} days\n"
|
||||
|
||||
if subscription['traffic_limit_gb'] > 0:
|
||||
text += f"📊 Traffic limit: {subscription['traffic_limit_gb']} GB\n"
|
||||
else:
|
||||
text += f"📊 Traffic limit: Unlimited\n"
|
||||
|
||||
if subscription.get('description'):
|
||||
text += f"\n📝 Description:\n{subscription['description']}"
|
||||
|
||||
ADMIN_CREATE_PROMO_CODE = "admin_create_promo_code"
|
||||
ADMIN_CREATE_PROMO_DISCOUNT = "admin_create_promo_discount"
|
||||
ADMIN_CREATE_PROMO_LIMIT = "admin_create_promo_limit"
|
||||
return text
|
||||
|
||||
def format_user_subscription_info(user_sub: Dict[str, Any], subscription: Dict[str, Any],
|
||||
expires_at: datetime, language: str = 'ru') -> str:
|
||||
text = ""
|
||||
|
||||
if language == 'ru':
|
||||
text += f"📋 **{subscription['name']}**\n\n"
|
||||
|
||||
now = datetime.utcnow()
|
||||
if expires_at < now:
|
||||
status = "❌ Истекла"
|
||||
days_left = 0
|
||||
elif not user_sub.get('is_active', True):
|
||||
status = "⏸ Приостановлена"
|
||||
days_left = (expires_at - now).days
|
||||
else:
|
||||
days_left = (expires_at - now).days
|
||||
status = f"✅ Активна"
|
||||
|
||||
text += f"🔘 Статус: {status}\n"
|
||||
text += f"📅 Истекает: {format_datetime(expires_at, language)}\n"
|
||||
|
||||
if days_left > 0:
|
||||
text += f"⏰ Осталось: {days_left} дн.\n"
|
||||
|
||||
if subscription['traffic_limit_gb'] > 0:
|
||||
text += f"📊 Лимит трафика: {subscription['traffic_limit_gb']} ГБ\n"
|
||||
else:
|
||||
text += f"📊 Лимит трафика: Безлимит\n"
|
||||
|
||||
if subscription.get('name') == "Старая подписка" or (subscription.get('description') and 'импорт' in subscription.get('description', '').lower()):
|
||||
text += f"\n🔄 Тип: Импортированная из старой системы\n"
|
||||
text += f"ℹ️ Продление недоступно"
|
||||
|
||||
if subscription.get('description') and not ('импорт' in subscription.get('description', '').lower()):
|
||||
text += f"\n📝 {subscription['description']}"
|
||||
else:
|
||||
text += f"📋 **{subscription['name']}**\n\n"
|
||||
|
||||
now = datetime.utcnow()
|
||||
if expires_at < now:
|
||||
status = "❌ Expired"
|
||||
days_left = 0
|
||||
elif not user_sub.get('is_active', True):
|
||||
status = "⏸ Suspended"
|
||||
days_left = (expires_at - now).days
|
||||
else:
|
||||
days_left = (expires_at - now).days
|
||||
status = f"✅ Active"
|
||||
|
||||
text += f"🔘 Status: {status}\n"
|
||||
text += f"📅 Expires: {format_datetime(expires_at, language)}\n"
|
||||
|
||||
if days_left > 0:
|
||||
text += f"⏰ Days left: {days_left}\n"
|
||||
|
||||
if subscription['traffic_limit_gb'] > 0:
|
||||
text += f"📊 Traffic limit: {subscription['traffic_limit_gb']} GB\n"
|
||||
else:
|
||||
text += f"📊 Traffic limit: Unlimited\n"
|
||||
|
||||
if subscription.get('name') == "Старая подписка" or (subscription.get('description') and 'import' in subscription.get('description', '').lower()):
|
||||
text += f"\n🔄 Type: Imported from old system\n"
|
||||
text += f"ℹ️ Extension not available"
|
||||
|
||||
if subscription.get('description') and not ('import' in subscription.get('description', '').lower()):
|
||||
text += f"\n📝 {subscription['description']}"
|
||||
|
||||
return text
|
||||
|
||||
def log_user_action(user_id: int, action: str, details: str = ""):
|
||||
logger.info(f"USER_ACTION: {user_id} - {action}" + (f" - {details}" if details else ""))
|
||||
|
||||
def bytes_to_gb(bytes_value: int) -> float:
|
||||
if not bytes_value or bytes_value == 0:
|
||||
return 0.0
|
||||
return round(bytes_value / (1024**3), 2)
|
||||
|
||||
def format_memory_usage(used_gb: float, total_gb: float) -> str:
|
||||
if total_gb == 0:
|
||||
return "N/A"
|
||||
|
||||
usage_percent = (used_gb / total_gb) * 100
|
||||
available_gb = total_gb - used_gb
|
||||
|
||||
return f"{used_gb:.1f}/{total_gb:.1f} ГБ ({usage_percent:.1f}%) • Доступно: {available_gb:.1f} ГБ"
|
||||
|
||||
def format_uptime(uptime_seconds: float) -> str:
|
||||
if uptime_seconds <= 0:
|
||||
return "N/A"
|
||||
|
||||
uptime_hours = int(uptime_seconds // 3600)
|
||||
uptime_days = uptime_hours // 24
|
||||
uptime_hours = uptime_hours % 24
|
||||
uptime_minutes = int((uptime_seconds % 3600) // 60)
|
||||
|
||||
if uptime_days > 0:
|
||||
return f"{uptime_days}д {uptime_hours}ч"
|
||||
elif uptime_hours > 0:
|
||||
return f"{uptime_hours}ч {uptime_minutes}м"
|
||||
else:
|
||||
return f"{uptime_minutes}м"
|
||||
|
||||