Compare commits

...

102 Commits

Author SHA1 Message Date
Fr1ngg aa79cb670b Update .env.example 2025-08-10 10:30:33 +03:00
Fr1ngg f0106e15fa Create stars_handlers.py 2025-08-10 10:26:28 +03:00
Fr1ngg 1d194c6be2 Update .env.example 2025-08-10 10:18:03 +03:00
Fr1ngg 6917890007 Update admin_handlers.py 2025-08-10 10:15:34 +03:00
Fr1ngg 1c69de7ecd Update config.py 2025-08-10 10:13:14 +03:00
Fr1ngg 62188793d9 Update database.py 2025-08-10 10:12:21 +03:00
Fr1ngg 585ee2d42b Update handlers.py 2025-08-10 10:11:12 +03:00
Fr1ngg 2a58cfd45c Update keyboards.py 2025-08-10 10:10:54 +03:00
Fr1ngg 93003c5fe0 Update main.py 2025-08-10 10:10:27 +03:00
Fr1ngg 91b0787d72 Update README.md 2025-08-10 07:04:17 +03:00
Fr1ngg 315277d4b0 Create bedolaga.sh 2025-08-10 06:56:04 +03:00
Fr1ngg 1821c0d89d Delete run.sh 2025-08-10 06:54:40 +03:00
Fr1ngg 5650da0c5f Update README.md 2025-08-10 03:56:42 +03:00
Fr1ngg 0ca8b73cef Update .env.example 2025-08-10 03:51:10 +03:00
Fr1ngg 6bab26ab27 Update lucky_game.py 2025-08-10 03:44:57 +03:00
Fr1ngg a72243e7fa Update keyboards.py 2025-08-10 03:42:56 +03:00
Fr1ngg 5bd86d9cf7 Update config.py 2025-08-10 03:40:36 +03:00
Fr1ngg 00fd3fbadb Update database.py 2025-08-10 03:39:51 +03:00
Fr1ngg b806f40797 Update middlewares.py 2025-08-10 03:38:20 +03:00
Fr1ngg c82aa1d253 Update main.py 2025-08-10 03:37:51 +03:00
Fr1ngg 071919f0db Update remnawave_api.py 2025-08-10 03:36:51 +03:00
Fr1ngg ab41e96ba5 Update subscription_monitor.py 2025-08-10 03:34:52 +03:00
Fr1ngg 95a470ba7b Update admin_handlers.py 2025-08-10 03:32:41 +03:00
Fr1ngg 1e0653ed33 Update referral_utils.py 2025-08-10 03:27:15 +03:00
Fr1ngg 3f0df08031 Update handlers.py 2025-08-10 03:25:53 +03:00
Fr1ngg 8c3bc8a5ba Update utils.py 2025-08-10 03:22:42 +03:00
Fr1ngg 3a5a8f48e4 Update README.md 2025-08-09 11:31:02 +03:00
Fr1ngg f4b99ffb09 Rename logo.svg to logo2.svg 2025-08-09 11:30:57 +03:00
Fr1ngg 0c6afdebc3 Update README.md 2025-08-09 11:30:17 +03:00
Fr1ngg 98b382ba2f Update logo.svg 2025-08-09 11:29:57 +03:00
Fr1ngg 54a24385b1 Update README.md 2025-08-09 08:54:57 +03:00
Fr1ngg 3eeb84cdae Update README.md 2025-08-09 08:29:02 +03:00
Fr1ngg 204a10ae14 Update README.md 2025-08-09 08:26:02 +03:00
Fr1ngg ba1dd7547e Create logo.svg 2025-08-09 08:24:25 +03:00
Fr1ngg 238585c257 Update .gitignore 2025-08-09 08:24:02 +03:00
Fr1ngg d8ebe1ad13 Add files via upload 2025-08-09 08:18:01 +03:00
Fr1ngg 819df3e93a Create 1 2025-08-09 08:13:36 +03:00
Fr1ngg 99e46feafa Delete screens/sub/1 2025-08-09 08:07:05 +03:00
Fr1ngg a432c8ab54 Add files via upload 2025-08-09 08:06:52 +03:00
Fr1ngg d740b7560c Create 1 2025-08-09 08:06:17 +03:00
Fr1ngg 91c4c73d7e Delete screens/Balance/1 2025-08-09 08:05:33 +03:00
Fr1ngg 4ce2384f27 Update handlers.py 2025-08-09 08:04:43 +03:00
Fr1ngg 02a6289d56 Add files via upload 2025-08-09 07:57:18 +03:00
Fr1ngg 52ae6eba81 Create 1 2025-08-09 07:55:29 +03:00
Fr1ngg 080b6e74a2 Add files via upload 2025-08-09 07:54:08 +03:00
Fr1ngg ad7d32023c Delete screens/Luckgame/1 2025-08-09 07:53:17 +03:00
Fr1ngg 1baf8fbe37 Add files via upload 2025-08-09 07:53:03 +03:00
Fr1ngg 758ca6a373 Create 1 2025-08-09 07:52:38 +03:00
Fr1ngg 65504d0a23 Delete screens/1 2025-08-09 07:50:15 +03:00
Fr1ngg 0409f4e2e2 Delete screens/Рефералы/1 2025-08-09 07:49:57 +03:00
Fr1ngg 8ca63e939a Add files via upload 2025-08-09 07:49:38 +03:00
Fr1ngg 25a621fd51 Delete screens/Главное меню/1 2025-08-09 07:43:58 +03:00
Fr1ngg 3f71a8aff7 Rename Снимок экрана 2025-08-09 в 07.42.12.png to Mainmenu.png 2025-08-09 07:43:45 +03:00
Fr1ngg 6ec81c37b9 Add files via upload 2025-08-09 07:42:41 +03:00
Fr1ngg 8a9ff98da8 Create 1 2025-08-09 07:40:31 +03:00
Fr1ngg bb7e29eeac Create 1 2025-08-09 07:40:05 +03:00
Fr1ngg 27d8783104 Create 1 2025-08-09 07:39:34 +03:00
Fr1ngg 9a01e6db43 Create .gitignore 2025-08-09 07:37:50 +03:00
Fr1ngg b341ddf9d9 Update README.md 2025-08-09 07:33:26 +03:00
Fr1ngg 3c8d0ac859 Update README.md 2025-08-09 07:33:03 +03:00
Fr1ngg 3f529c0dee Update README.md 2025-08-09 07:19:13 +03:00
Fr1ngg a6734e542d Create lucky_game.py 2025-08-09 07:04:20 +03:00
Fr1ngg 2287a67dfd Create referral_utils.py 2025-08-09 07:03:30 +03:00
Fr1ngg 31dbaa80d7 Update admin_handlers.py 2025-08-09 07:02:43 +03:00
Fr1ngg b1846a3ea9 Update api_error_handlers.py 2025-08-09 07:02:24 +03:00
Fr1ngg e1a7e9e622 Update config.py 2025-08-09 07:02:00 +03:00
Fr1ngg 0ae48faedb Update database.py 2025-08-09 07:01:35 +03:00
Fr1ngg b8122222f8 Update handlers.py 2025-08-09 07:00:51 +03:00
Fr1ngg 5e5a39c22e Update keyboards.py 2025-08-09 07:00:27 +03:00
Fr1ngg 1e7cf214e9 Update main.py 2025-08-09 07:00:10 +03:00
Fr1ngg d7e7a2df9a Update middlewares.py 2025-08-09 06:59:52 +03:00
Fr1ngg 584aa35894 Update remnawave_api.py 2025-08-09 06:59:36 +03:00
Fr1ngg 652ae8057a Update subscription_monitor.py 2025-08-09 06:59:10 +03:00
Fr1ngg 0bc0a1354a Update translations.py 2025-08-09 06:58:45 +03:00
Fr1ngg 7d30838813 Update utils.py 2025-08-09 06:58:22 +03:00
Fr1ngg 3da178ed02 Update handlers.py 2025-08-09 06:58:00 +03:00
Fr1ngg e6ac3e3812 Update README.md 2025-08-09 06:53:32 +03:00
Fr1ngg 62087e20b5 Update README.md 2025-08-09 06:50:45 +03:00
Fr1ngg b7a47c6eb6 Update .env.example 2025-08-09 06:32:47 +03:00
Fr1ngg 9b14baca51 Update README.md 2025-08-08 05:43:08 +03:00
Fr1ngg eb1a75c9f5 Update .env.example 2025-08-08 05:24:58 +03:00
Fr1ngg 5e79106134 Update admin_handlers.py 2025-08-08 05:22:43 +03:00
Fr1ngg ee45ec356e Update api_error_handlers.py 2025-08-08 05:22:20 +03:00
Fr1ngg 9275bff828 Update config.py 2025-08-08 05:21:51 +03:00
Fr1ngg 9150c70618 Update database.py 2025-08-08 05:21:25 +03:00
Fr1ngg b792e749d6 Update handlers.py 2025-08-08 05:21:00 +03:00
Fr1ngg 9871572740 Update keyboards.py 2025-08-08 05:20:37 +03:00
Fr1ngg 52c1cb3cbc Update main.py 2025-08-08 05:20:16 +03:00
Fr1ngg d5b2b371c8 Update middlewares.py 2025-08-08 05:19:23 +03:00
Fr1ngg d5cb022b02 Update remnawave_api.py 2025-08-08 05:19:06 +03:00
Fr1ngg 021f62fd30 Update subscription_monitor.py 2025-08-08 05:18:41 +03:00
Fr1ngg 684e398798 Update translations.py 2025-08-08 05:18:12 +03:00
Fr1ngg 12b64e9978 Update utils.py 2025-08-08 05:17:45 +03:00
Fr1ngg 273df23f20 Update README.md 2025-08-07 09:32:39 +03:00
Fr1ngg a9b0dbf3eb Update handlers.py 2025-08-07 09:31:28 +03:00
Fr1ngg 5f56e068d3 Update remnawave_api.py 2025-08-07 09:27:59 +03:00
Fr1ngg 92b82132d8 Update remnawave_api.py 2025-08-07 09:27:42 +03:00
Fr1ngg 79ae9d3a1c Update config.py 2025-08-07 09:25:55 +03:00
Fr1ngg 86714a93f9 Update handlers.py 2025-08-07 09:25:40 +03:00
Fr1ngg 553f6f8cf6 Update handlers.py 2025-08-07 09:24:22 +03:00
Fr1ngg b561f67c12 Update utils.py 2025-08-07 09:23:52 +03:00
Fr1ngg d03f9b80c7 Update translations.py 2025-08-07 09:23:26 +03:00
51 changed files with 8413 additions and 2651 deletions
+45 -13
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
/screens
/assets
+639 -161
View File
@@ -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
[![Docker Image](https://img.shields.io/badge/Docker-fr1ngg/remnawave--bedolaga--telegram--bot-blue?logo=docker)](https://hub.docker.com/r/fr1ngg/remnawave-bedolaga-telegram-bot)
[![Python](https://img.shields.io/badge/Python-3.11+-blue?logo=python)](https://python.org)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-blue?logo=postgresql)](https://postgresql.org)
[![License](https://img.shields.io/badge/License-MIT-green)](LICENSE)
![RemnaWave Bedolaga Bot Logo](./assets/logo2.svg)
Современный 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 сообщества*
+1957 -746
View File
File diff suppressed because it is too large Load Diff
-32
View File
@@ -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
)
+295
View File
@@ -0,0 +1,295 @@
<svg viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Градиенты -->
<linearGradient id="mainGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
<stop offset="50%" style="stop-color:#764ba2;stop-opacity:1" />
<stop offset="100%" style="stop-color:#f093fb;stop-opacity:1" />
</linearGradient>
<linearGradient id="skinGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#d4a574;stop-opacity:1" />
<stop offset="100%" style="stop-color:#b8935f;stop-opacity:1" />
</linearGradient>
<linearGradient id="redSkinGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#e8b598;stop-opacity:1" />
<stop offset="100%" style="stop-color:#d4a574;stop-opacity:1" />
</linearGradient>
<linearGradient id="clothesGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3a3a3a;stop-opacity:1" />
<stop offset="100%" style="stop-color:#595959;stop-opacity:1" />
</linearGradient>
<linearGradient id="bottleGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2d5016;stop-opacity:0.8" />
<stop offset="100%" style="stop-color:#1a2f0d;stop-opacity:0.9" />
</linearGradient>
<!-- Фильтры -->
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="dropshadow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="4"/>
<feOffset dx="2" dy="4" result="offset" />
<feComponentTransfer>
<feFuncA type="linear" slope="0.3"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Фон -->
<rect width="400" height="400" fill="#1a1a2e" rx="50"/>
<!-- Декоративные элементы фона -->
<circle cx="50" cy="80" r="30" fill="url(#mainGradient)" opacity="0.1"/>
<circle cx="350" cy="120" r="40" fill="#8B4513" opacity="0.1"/>
<!-- Волны RemnaWave в фоне -->
<g transform="translate(200,200)" opacity="0.2">
<path d="M-120,-100 Q-60,-130 0,-100 Q60,-70 120,-100"
stroke="url(#mainGradient)"
stroke-width="4"
fill="none">
<animate attributeName="d"
values="M-120,-100 Q-60,-130 0,-100 Q60,-70 120,-100;M-120,-100 Q-60,-70 0,-100 Q60,-130 120,-100;M-120,-100 Q-60,-130 0,-100 Q60,-70 120,-100"
dur="4s"
repeatCount="indefinite"/>
</path>
</g>
<!-- Главный персонаж - Бомж-алкаш -->
<g transform="translate(200,180)" filter="url(#dropshadow)">
<!-- Тело в рваной куртке -->
<ellipse cx="0" cy="30" rx="45" ry="60" fill="url(#clothesGradient)"/>
<!-- Рваные края одежды -->
<path d="M-45,70 L-40,75 L-35,70 L-30,75 L-25,70 L-20,75 L-15,70 L-10,75 L-5,70 L0,75 L5,70 L10,75 L15,70 L20,75 L25,70 L30,75 L35,70 L40,75 L45,70"
stroke="none" fill="#2a2a2a"/>
<!-- Заплатки -->
<rect x="-20" y="10" width="12" height="10" fill="#5a4a3a" opacity="0.7" rx="2"/>
<rect x="15" y="35" width="10" height="8" fill="#4a4a4a" opacity="0.8" rx="1"/>
<rect x="-10" y="45" width="8" height="12" fill="#6a5a4a" opacity="0.6" rx="1"/>
<!-- Шея -->
<rect x="-12" y="-25" width="24" height="20" fill="url(#redSkinGradient)" rx="8"/>
<!-- Голова (более человечная форма) -->
<ellipse cx="0" cy="-45" rx="28" ry="32" fill="url(#redSkinGradient)"/>
<!-- Сальные волосы -->
<path d="M-25,-70 Q-20,-75 -15,-72 Q-10,-78 -5,-73 Q0,-80 5,-73 Q10,-78 15,-72 Q20,-75 25,-70"
stroke="#3a3a3a" stroke-width="4" fill="#3a3a3a" stroke-linecap="round"/>
<path d="M-28,-65 Q-25,-70 -20,-67" stroke="#3a3a3a" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M28,-65 Q25,-70 20,-67" stroke="#3a3a3a" stroke-width="3" fill="none" stroke-linecap="round"/>
<!-- Левое ухо -->
<ellipse cx="-28" cy="-45" rx="6" ry="10" fill="url(#skinGradient)"/>
<!-- Правое ухо -->
<ellipse cx="28" cy="-45" rx="6" ry="10" fill="url(#skinGradient)"/>
<!-- Опухшие глаза -->
<ellipse cx="-10" cy="-50" rx="8" ry="5" fill="white"/>
<ellipse cx="10" cy="-50" rx="8" ry="5" fill="white"/>
<!-- Зрачки (косящие) -->
<g id="eyes">
<circle cx="-8" cy="-49" r="3" fill="#333">
<animate attributeName="cx" values="-8;-10;-8" dur="3s" repeatCount="indefinite"/>
</circle>
<circle cx="12" cy="-49" r="3" fill="#333">
<animate attributeName="cx" values="12;10;12" dur="3s" repeatCount="indefinite"/>
</circle>
</g>
<!-- Красные глаза -->
<path d="M-18,-50 L-15,-51" stroke="red" stroke-width="1" opacity="0.7"/>
<path d="M-17,-48 L-14,-49" stroke="red" stroke-width="0.5" opacity="0.7"/>
<path d="M18,-50 L15,-51" stroke="red" stroke-width="1" opacity="0.7"/>
<path d="M17,-48 L14,-49" stroke="red" stroke-width="0.5" opacity="0.7"/>
<!-- Мешки под глазами -->
<ellipse cx="-10" cy="-44" rx="8" ry="3" fill="#8a6a4a" opacity="0.5"/>
<ellipse cx="10" cy="-44" rx="8" ry="3" fill="#8a6a4a" opacity="0.5"/>
<!-- Красный нос алкаша -->
<ellipse cx="0" cy="-38" rx="6" ry="5" fill="#d67070"/>
<circle cx="-2" cy="-39" r="1" fill="#e08080" opacity="0.8"/>
<!-- Небритость -->
<g opacity="0.6">
<circle cx="-15" cy="-30" r="1" fill="#555"/>
<circle cx="-12" cy="-28" r="0.8" fill="#555"/>
<circle cx="-8" cy="-30" r="0.8" fill="#555"/>
<circle cx="15" cy="-30" r="1" fill="#555"/>
<circle cx="12" cy="-28" r="0.8" fill="#555"/>
<circle cx="8" cy="-30" r="0.8" fill="#555"/>
<circle cx="0" cy="-28" r="0.8" fill="#555"/>
<circle cx="-5" cy="-26" r="0.8" fill="#555"/>
<circle cx="5" cy="-26" r="0.8" fill="#555"/>
</g>
<!-- Рот с сигаретой/бутылкой (анимированный) -->
<g id="mouth-group">
<!-- Основной рот -->
<path d="M-12,-20 Q0,-18 12,-20" stroke="#444" stroke-width="2" fill="none" stroke-linecap="round"/>
<!-- Сигарета (появляется и исчезает) -->
<g id="cigarette">
<rect x="10" y="-22" width="25" height="3" fill="white" rx="1">
<animate attributeName="opacity" values="0;0;0;1;1;1;0;0;0" dur="8s" repeatCount="indefinite"/>
</rect>
<rect x="30" y="-22" width="5" height="3" fill="#ff6b35">
<animate attributeName="opacity" values="0;0;0;1;1;1;0;0;0" dur="8s" repeatCount="indefinite"/>
</rect>
<!-- Дым от сигареты -->
<g opacity="0.4">
<circle cx="35" cy="-22" r="2" fill="#999">
<animate attributeName="opacity" values="0;0;0;0.6;0.3;0;0;0;0" dur="8s" repeatCount="indefinite"/>
<animate attributeName="cy" values="-22;-22;-22;-28;-35;-40;-22;-22;-22" dur="8s" repeatCount="indefinite"/>
<animate attributeName="r" values="2;2;2;4;6;8;2;2;2" dur="8s" repeatCount="indefinite"/>
</circle>
</g>
</g>
</g>
<!-- Левая рука с бутылкой -->
<g id="left-arm">
<!-- Рука -->
<line x1="-45" y1="10" x2="-55" y2="-5" stroke="url(#skinGradient)" stroke-width="8" stroke-linecap="round">
<animate attributeName="x2" values="-55;-50;-55" dur="8s" repeatCount="indefinite"/>
<animate attributeName="y2" values="-5;-20;-5" dur="8s" repeatCount="indefinite"/>
</line>
<!-- Кисть -->
<circle cx="-55" cy="-5" r="6" fill="url(#skinGradient)">
<animate attributeName="cx" values="-55;-50;-55" dur="8s" repeatCount="indefinite"/>
<animate attributeName="cy" values="-5;-20;-5" dur="8s" repeatCount="indefinite"/>
</circle>
<!-- Бутылка -->
<g id="bottle">
<rect x="-62" y="-25" width="14" height="30" fill="url(#bottleGradient)" rx="2">
<animate attributeName="x" values="-62;-57;-62" dur="8s" repeatCount="indefinite"/>
<animate attributeName="y" values="-25;-40;-25" dur="8s" repeatCount="indefinite"/>
<animate attributeName="transform" values="rotate(0 -55 -10);rotate(-30 -50 -25);rotate(0 -55 -10)" dur="8s" repeatCount="indefinite"/>
</rect>
<rect x="-58" y="-30" width="6" height="8" fill="#2d5016">
<animate attributeName="x" values="-58;-53;-58" dur="8s" repeatCount="indefinite"/>
<animate attributeName="y" values="-30;-45;-30" dur="8s" repeatCount="indefinite"/>
</rect>
<!-- Этикетка -->
<rect x="-60" y="-15" width="10" height="8" fill="white" opacity="0.3">
<animate attributeName="x" values="-60;-55;-60" dur="8s" repeatCount="indefinite"/>
<animate attributeName="y" values="-15;-30;-15" dur="8s" repeatCount="indefinite"/>
</rect>
</g>
</g>
<!-- Правая рука (свободная, дрожащая) -->
<g id="right-arm">
<line x1="45" y1="10" x2="55" y2="20" stroke="url(#skinGradient)" stroke-width="8" stroke-linecap="round">
<animate attributeName="x2" values="55;57;55;53;55" dur="2s" repeatCount="indefinite"/>
</line>
<circle cx="55" cy="20" r="6" fill="url(#skinGradient)">
<animate attributeName="cx" values="55;57;55;53;55" dur="2s" repeatCount="indefinite"/>
</circle>
</g>
<!-- Ноги в рваных штанах -->
<rect x="-15" y="85" width="12" height="35" fill="#4a4a5a" rx="2"/>
<rect x="3" y="85" width="12" height="35" fill="#4a4a5a" rx="2"/>
<!-- Дырки в штанах -->
<ellipse cx="-9" cy="95" rx="3" ry="4" fill="#1a1a2e"/>
<ellipse cx="9" cy="105" rx="2" ry="3" fill="#1a1a2e"/>
<!-- Старые ботинки -->
<ellipse cx="-9" cy="125" rx="10" ry="6" fill="#2a2a2a"/>
<ellipse cx="9" cy="125" rx="10" ry="6" fill="#2a2a2a"/>
<!-- Дырки в ботинках -->
<circle cx="-6" cy="123" r="2" fill="#1a1a2e"/>
<circle cx="12" cy="123" r="1.5" fill="#1a1a2e"/>
<!-- Значок "B" на груди -->
<circle cx="0" cy="25" r="20" fill="url(#mainGradient)" opacity="0.8" stroke="white" stroke-width="1"/>
<text x="0" y="35"
font-family="Arial Black, sans-serif"
font-size="28"
font-weight="bold"
text-anchor="middle"
fill="white">
B
</text>
</g>
<!-- Упавшие бутылки вокруг -->
<g opacity="0.6">
<rect x="80" y="340" width="8" height="20" fill="#2d5016" rx="1" transform="rotate(75 84 350)"/>
<rect x="310" y="330" width="7" height="18" fill="#3a5a1a" rx="1" transform="rotate(-60 313 339)"/>
<rect x="150" y="350" width="6" height="16" fill="#2d5016" rx="1" transform="rotate(45 153 358)"/>
</g>
<!-- Окурки -->
<g opacity="0.7">
<rect x="120" y="360" width="12" height="2" fill="white" rx="1"/>
<rect x="130" y="360" width="2" height="2" fill="#ff6b35"/>
<rect x="250" y="355" width="10" height="2" fill="white" rx="1" transform="rotate(30 255 356)"/>
<rect x="258" y="355" width="2" height="2" fill="#ff6b35" transform="rotate(30 259 356)"/>
</g>
<!-- Текст логотипа -->
<text x="200" y="320"
font-family="Arial Black, sans-serif"
font-size="20"
font-weight="bold"
text-anchor="middle"
fill="url(#mainGradient)"
filter="url(#glow)">
RemnaWave
</text>
<text x="200" y="345"
font-family="Arial Black, sans-serif"
font-size="18"
font-weight="bold"
text-anchor="middle"
fill="#d67070">
Bedolaga Bot
</text>
<!-- Подпись -->
<text x="200" y="365"
font-family="Arial, sans-serif"
font-size="11"
text-anchor="middle"
fill="#8892b0"
opacity="0.8">
Telegram VPN Manager
</text>
<!-- Мухи летающие вокруг (признак антисанитарии) -->
<g opacity="0.5">
<circle cx="150" cy="150" r="1" fill="#333">
<animateTransform attributeName="transform" type="rotate" values="0 200 180;360 200 180" dur="6s" repeatCount="indefinite"/>
</circle>
<circle cx="250" cy="150" r="1" fill="#333">
<animateTransform attributeName="transform" type="rotate" values="360 200 180;0 200 180" dur="5s" repeatCount="indefinite"/>
</circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

+965
View File
@@ -0,0 +1,965 @@
#!/bin/bash
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
BOT_DIR="/opt/bedolaga-bot"
COMPOSE_FILE="$BOT_DIR/docker-compose.yml"
ENV_FILE="$BOT_DIR/.env"
SERVICE_FILE="/etc/systemd/system/bedolaga-bot.service"
# Функция для логирования
log() {
echo -e "${GREEN}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Проверка является ли пользователь root
check_root() {
if [ "$EUID" -ne 0 ]; then
error "Этот скрипт должен быть запущен от имени root (используйте sudo)"
exit 1
fi
}
# Проверка установки бота
check_installation() {
if [ -d "$BOT_DIR" ] && [ -f "$COMPOSE_FILE" ] && [ -f "$ENV_FILE" ]; then
return 0
else
return 1
fi
}
# Обновление системы
update_system() {
log "Обновление системы Ubuntu..."
apt update && apt upgrade -y
log "Система обновлена успешно"
}
# Установка Docker
install_docker() {
log "Установка Docker..."
# Удаление старых версий
apt remove -y docker docker-engine docker.io containerd runc
# Установка зависимостей
apt install -y apt-transport-https ca-certificates curl gnupg lsb-release nano
# Добавление GPG ключа Docker
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Добавление репозитория
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
# Установка Docker
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Запуск Docker
systemctl enable docker
systemctl start docker
log "Docker установлен успешно"
}
# Проверка и установка nano
ensure_nano() {
if ! command -v nano &> /dev/null; then
log "Установка nano..."
apt update
apt install -y nano
log "Nano установлен успешно"
fi
}
# Создание папки и структуры проекта
create_project_structure() {
log "Создание структуры проекта..."
mkdir -p "$BOT_DIR"
mkdir -p "$BOT_DIR/logs"
mkdir -p "$BOT_DIR/data"
log "Структура проекта создана в $BOT_DIR"
}
# Создание docker-compose.yml
create_docker_compose() {
log "Создание docker-compose.yml..."
echo "Выберите конфигурацию Docker Compose:"
echo "1) Минимальная (рекомендуется) - только PostgreSQL и бот"
echo "2) Расширенная - с Redis и Nginx"
while true; do
read -p "Ваш выбор (1-2): " choice
case $choice in
1)
create_minimal_compose
break
;;
2)
create_full_compose
break
;;
*)
error "Неверный выбор. Попробуйте снова."
;;
esac
done
}
create_minimal_compose() {
cat > "$COMPOSE_FILE" << 'EOF'
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: remnawave_bot_db
restart: unless-stopped
environment:
POSTGRES_DB: remnawave_bot
POSTGRES_USER: remnawave_user
POSTGRES_PASSWORD: secure_password_123
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U remnawave_user -d remnawave_bot"]
interval: 15s
timeout: 10s
retries: 5
start_period: 30s
# RemnaWave Bot
bot:
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
container_name: remnawave_bot
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
env_file:
- .env
environment:
DATABASE_URL: postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
volumes:
- ./logs:/app/logs
- ./data:/app/data
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "python -c 'print(\"Bot is running\")'"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
postgres_data:
driver: local
networks:
bot_network:
driver: bridge
EOF
log "Минимальная конфигурация Docker Compose создана"
}
create_full_compose() {
cat > "$COMPOSE_FILE" << 'EOF'
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: remnawave_bot_db
restart: unless-stopped
environment:
POSTGRES_DB: remnawave_bot
POSTGRES_USER: remnawave_user
POSTGRES_PASSWORD: secure_password_123
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U remnawave_user -d remnawave_bot"]
interval: 15s
timeout: 10s
retries: 5
start_period: 30s
# RemnaWave Bot
bot:
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
container_name: remnawave_bot
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
env_file:
- .env
environment:
DATABASE_URL: postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
volumes:
- ./logs:/app/logs
- ./data:/app/data
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "python -c 'print(\"Bot is running\")'"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
# Redis (для кэширования и улучшения производительности)
redis:
image: redis:7-alpine
container_name: remnawave_bot_redis
restart: unless-stopped
command: redis-server --appendonly yes --requirepass redis_password_123
volumes:
- redis_data:/data
ports:
- "6379:6379"
networks:
- bot_network
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 10s
timeout: 3s
retries: 5
profiles:
- with-redis
# Nginx (для статических файлов или веб-интерфейса)
nginx:
image: nginx:alpine
container_name: remnawave_bot_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
- ./static:/usr/share/nginx/html:ro
networks:
- bot_network
depends_on:
- bot
profiles:
- with-nginx
volumes:
postgres_data:
driver: local
redis_data:
driver: local
networks:
bot_network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
EOF
log "Расширенная конфигурация Docker Compose создана"
}
# Создание .env файла
create_env_file() {
log "Настройка .env файла..."
# Основные настройки бота
read -p "Введите BOT_TOKEN: " BOT_TOKEN
read -p "Введите BOT_USERNAME (без @): " BOT_USERNAME
read -p "Введите ADMIN_IDS (через запятую): " ADMIN_IDS
# Настройки RemnaWave
echo "Выберите режим RemnaWave:"
echo "1) remote"
echo "2) local"
while true; do
read -p "Ваш выбор (1-2): " remna_choice
case $remna_choice in
1)
REMNAWAVE_MODE="remote"
break
;;
2)
REMNAWAVE_MODE="local"
break
;;
*)
error "Неверный выбор. Попробуйте снова."
;;
esac
done
read -p "Введите REMNAWAVE_URL: " REMNAWAVE_URL
read -p "Введите REMNAWAVE_TOKEN: " REMNAWAVE_TOKEN
# Настройки триала
while true; do
read -p "Включить триал? (y/n): " trial_enabled
case $trial_enabled in
[Yy]*)
TRIAL_ENABLED="true"
read -p "Введите TRIAL_DURATION_DAYS: " TRIAL_DURATION_DAYS
read -p "Введите TRIAL_TRAFFIC_GB: " TRIAL_TRAFFIC_GB
read -p "Введите TRIAL_SQUAD_UUID: " TRIAL_SQUAD_UUID
break
;;
[Nn]*)
TRIAL_ENABLED="false"
TRIAL_DURATION_DAYS=""
TRIAL_TRAFFIC_GB=""
TRIAL_SQUAD_UUID=""
break
;;
*)
error "Пожалуйста, ответьте y или n."
;;
esac
done
# Настройки реферальной системы
read -p "Введите REFERRAL_FIRST_REWARD (сумму с .0 на конце): " REFERRAL_FIRST_REWARD
read -p "Введите REFERRAL_REFERRED_BONUS (сумму с .0 на конце): " REFERRAL_REFERRED_BONUS
read -p "Введите REFERRAL_THRESHOLD (сумму с .0 на конце): " REFERRAL_THRESHOLD
read -p "Введите REFERRAL_PERCENTAGE (с 0. в начале): " REFERRAL_PERCENTAGE
# Настройки мониторинга
read -p "Введите DELETE_EXPIRED_TRIAL_DAYS: " DELETE_EXPIRED_TRIAL_DAYS
read -p "Введите DELETE_EXPIRED_REGULAR_DAYS: " DELETE_EXPIRED_REGULAR_DAYS
# Создание .env файла
cat > "$ENV_FILE" << EOF
BOT_TOKEN=$BOT_TOKEN
BOT_USERNAME=$BOT_USERNAME
ADMIN_IDS=$ADMIN_IDS
REMNAWAVE_MODE=$REMNAWAVE_MODE
REMNAWAVE_URL=$REMNAWAVE_URL
REMNAWAVE_TOKEN=$REMNAWAVE_TOKEN
TRIAL_ENABLED=$TRIAL_ENABLED
EOF
if [ "$TRIAL_ENABLED" = "true" ]; then
cat >> "$ENV_FILE" << EOF
TRIAL_DURATION_DAYS=$TRIAL_DURATION_DAYS
TRIAL_TRAFFIC_GB=$TRIAL_TRAFFIC_GB
TRIAL_SQUAD_UUID=$TRIAL_SQUAD_UUID
EOF
fi
cat >> "$ENV_FILE" << EOF
TRIAL_PRICE=0.0
REFERRAL_FIRST_REWARD=$REFERRAL_FIRST_REWARD
REFERRAL_REFERRED_BONUS=$REFERRAL_REFERRED_BONUS
REFERRAL_THRESHOLD=$REFERRAL_THRESHOLD
REFERRAL_PERCENTAGE=$REFERRAL_PERCENTAGE
# Monitor Service Settings
MONITOR_ENABLED=true
MONITOR_CHECK_INTERVAL=21600
MONITOR_DAILY_CHECK_HOUR=12
MONITOR_WARNING_DAYS=2
DELETE_EXPIRED_TRIAL_DAYS=$DELETE_EXPIRED_TRIAL_DAYS
DELETE_EXPIRED_REGULAR_DAYS=$DELETE_EXPIRED_REGULAR_DAYS
AUTO_DELETE_ENABLED=true
LUCKY_GAME_ENABLED=true
LUCKY_GAME_REWARD=50.0
LUCKY_GAME_NUMBERS=30
LUCKY_GAME_WINNING_COUNT=5
EOF
log ".env файл создан успешно"
}
# Создание службы systemd
create_service() {
while true; do
read -p "Создать службу для запуска бота? (y/n): " create_service_choice
case $create_service_choice in
[Yy]*)
log "Создание службы systemd..."
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=RemnaWave Bedolaga Bot
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=$BOT_DIR
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable bedolaga-bot.service
log "Служба bedolaga-bot создана и включена"
break
;;
[Nn]*)
log "Служба не будет создана"
break
;;
*)
error "Пожалуйста, ответьте y или n."
;;
esac
done
}
# Проверка статуса бота
check_bot_status() {
if docker compose -f "$COMPOSE_FILE" ps --services --filter "status=running" | grep -q "bot"; then
return 0 # Запущен
else
return 1 # Не запущен
fi
}
# Проверка подключения к RemnaWave API
check_remnawave_connection() {
if [ -f "$ENV_FILE" ]; then
source "$ENV_FILE"
if [ ! -z "$REMNAWAVE_URL" ]; then
if curl -s --connect-timeout 5 "$REMNAWAVE_URL/api/auth/status" > /dev/null; then
return 0 # Подключен
else
return 1 # Не подключен
fi
else
return 1
fi
else
return 1
fi
}
# Запуск бота
start_bot() {
log "Запуск бота..."
cd "$BOT_DIR"
docker compose up -d
log "Бот запущен"
}
# Остановка бота
stop_bot() {
log "Остановка бота..."
cd "$BOT_DIR"
docker compose down
log "Бот остановлен"
}
# Перезапуск бота
restart_bot() {
log "Перезапуск бота..."
cd "$BOT_DIR"
docker compose restart
log "Бот перезапущен"
}
# Обновление бота
update_bot() {
log "Обновление бота..."
cd "$BOT_DIR"
docker compose down
docker compose pull bot
docker compose up -d
log "Бот обновлен"
}
# Просмотр логов
view_logs() {
cd "$BOT_DIR"
docker compose logs bot
}
# Просмотр логов в реальном времени
view_live_logs() {
cd "$BOT_DIR"
docker compose logs -f bot
}
# Создание резервной копии базы данных
backup_database() {
log "Создание резервной копии базы данных..."
cd "$BOT_DIR"
# Проверяем, запущен ли контейнер postgres
if ! docker compose ps postgres | grep -q "Up"; then
log "Контейнер PostgreSQL не запущен. Запускаем PostgreSQL..."
docker compose up -d postgres
# Ждем готовности базы данных
log "Ожидание готовности базы данных..."
for i in {1..30}; do
if docker compose exec postgres pg_isready -U remnawave_user -d remnawave_bot &>/dev/null; then
log "PostgreSQL готов к работе"
break
fi
if [ $i -eq 30 ]; then
error "PostgreSQL не запустился в течение 60 секунд"
return 1
fi
sleep 2
echo -n "."
done
echo ""
fi
# Проверяем доступность базы данных
log "Проверка подключения к базе данных..."
if ! docker compose exec postgres pg_isready -U remnawave_user -d remnawave_bot &>/dev/null; then
error "База данных недоступна. Проверьте логи: docker compose logs postgres"
return 1
fi
BACKUP_FILE="$BOT_DIR/backup_$(date +%Y%m%d_%H%M%S).sql"
log "Создание дампа базы данных..."
# Используем docker compose exec без -T и с правильным выводом
if docker compose exec postgres pg_dump -U remnawave_user -d remnawave_bot --verbose --no-owner --no-privileges > "$BACKUP_FILE" 2>/dev/null; then
# Проверяем размер созданного файла
if [ -s "$BACKUP_FILE" ]; then
FILE_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
log "Резервная копия создана успешно: $(basename "$BACKUP_FILE")"
echo -e "${GREEN}Размер файла: $FILE_SIZE${NC}"
echo -e "${GREEN}Путь: $BACKUP_FILE${NC}"
# Показываем краткую информацию о содержимом
LINES_COUNT=$(wc -l < "$BACKUP_FILE")
echo -e "${BLUE}Количество строк в дампе: $LINES_COUNT${NC}"
else
error "Резервная копия создана, но файл пустой. Возможные причины:"
echo " - База данных пуста (бот еще не создал таблицы)"
echo " - Нет прав доступа к базе данных"
echo " - Неправильные параметры подключения"
rm -f "$BACKUP_FILE"
return 1
fi
else
error "Ошибка при создании резервной копии"
# Показываем подробную информацию об ошибке
echo "Попытка диагностики проблемы..."
docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "\dt" 2>&1 || {
echo "Не удается подключиться к базе данных."
echo "Проверьте логи контейнера: docker compose logs postgres"
}
rm -f "$BACKUP_FILE" 2>/dev/null
return 1
fi
}
# Восстановление базы данных
restore_database() {
log "Восстановление базы данных из резервной копии"
# Поиск файлов резервных копий
BACKUP_FILES=($(find "$BOT_DIR" -name "backup_*.sql" -type f 2>/dev/null))
if [ ${#BACKUP_FILES[@]} -eq 0 ]; then
error "Файлы резервных копий не найдены в $BOT_DIR"
echo "Поместите файл резервной копии (.sql) в папку $BOT_DIR"
return 1
fi
echo -e "${YELLOW}Найденные резервные копии:${NC}"
for i in "${!BACKUP_FILES[@]}"; do
BACKUP_FILE="${BACKUP_FILES[$i]}"
FILE_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
FILE_DATE=$(basename "$BACKUP_FILE" | sed 's/backup_\([0-9]\{8\}_[0-9]\{6\}\)\.sql/\1/' | sed 's/_/ /')
echo "$((i+1))) $(basename "$BACKUP_FILE") - Размер: $FILE_SIZE - Дата: $FILE_DATE"
done
echo "$((${#BACKUP_FILES[@]}+1))) Указать путь к другому файлу"
echo "0) Отмена"
while true; do
read -p "Выберите файл для восстановления: " choice
if [ "$choice" = "0" ]; then
log "Операция отменена"
return 0
elif [ "$choice" = "$((${#BACKUP_FILES[@]}+1))" ]; then
read -p "Введите полный путь к файлу резервной копии: " SELECTED_BACKUP
if [ ! -f "$SELECTED_BACKUP" ]; then
error "Файл не найден: $SELECTED_BACKUP"
continue
fi
break
elif [ "$choice" -ge 1 ] && [ "$choice" -le "${#BACKUP_FILES[@]}" ]; then
SELECTED_BACKUP="${BACKUP_FILES[$((choice-1))]}"
break
else
error "Неверный выбор. Попробуйте снова."
fi
done
warn "ВНИМАНИЕ! Это действие перезапишет текущую базу данных!"
warn "Убедитесь, что у вас есть резервная копия текущих данных!"
read -p "Продолжить восстановление? Введите 'YES' для подтверждения: " confirm
if [ "$confirm" != "YES" ]; then
log "Операция отменена"
return 0
fi
cd "$BOT_DIR"
# Проверяем, запущен ли контейнер postgres
if ! docker compose ps postgres | grep -q "Up"; then
log "Запуск контейнера PostgreSQL..."
docker compose up -d postgres
# Ждем готовности базы данных
log "Ожидание готовности базы данных..."
for i in {1..30}; do
if docker compose exec postgres pg_isready -U remnawave_user -d remnawave_bot &>/dev/null; then
break
fi
sleep 2
echo -n "."
done
echo ""
fi
log "Очистка текущей базы данных..."
if docker compose exec -T postgres psql -U remnawave_user -d remnawave_bot -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" &>/dev/null; then
log "База данных очищена"
else
error "Ошибка при очистке базы данных"
return 1
fi
log "Восстановление данных из файла: $(basename "$SELECTED_BACKUP")"
if docker compose exec -T postgres psql -U remnawave_user -d remnawave_bot < "$SELECTED_BACKUP"; then
log "База данных успешно восстановлена!"
log "Перезапуск бота для применения изменений..."
docker compose restart bot
log "Восстановление завершено успешно"
else
error "Ошибка при восстановлении базы данных"
return 1
fi
}
# Диагностика базы данных
diagnose_database() {
log "Диагностика состояния базы данных..."
cd "$BOT_DIR"
# Проверяем состояние контейнера
echo -e "${YELLOW}Состояние контейнеров:${NC}"
docker compose ps
echo ""
# Проверяем, запущен ли PostgreSQL, если нет - запускаем
if ! docker compose ps postgres | grep -q "Up"; then
log "PostgreSQL не запущен. Запускаем..."
docker compose up -d postgres
# Ждем готовности базы данных
log "Ожидание готовности базы данных..."
for i in {1..30}; do
if docker compose exec postgres pg_isready -U remnawave_user -d remnawave_bot &>/dev/null; then
log "PostgreSQL готов к работе"
break
fi
if [ $i -eq 30 ]; then
error "PostgreSQL не запустился в течение 60 секунд"
return 1
fi
sleep 2
echo -n "."
done
echo ""
fi
# Проверяем доступность PostgreSQL
echo -e "${YELLOW}Проверка доступности PostgreSQL:${NC}"
if docker compose exec postgres pg_isready -U remnawave_user -d remnawave_bot; then
echo -e "${GREEN}✓ PostgreSQL доступен${NC}"
else
echo -e "${RED}✗ PostgreSQL недоступен${NC}"
echo "Проверьте логи: docker compose logs postgres"
return 1
fi
echo ""
# Проверяем подключение к базе данных
echo -e "${YELLOW}Информация о базе данных:${NC}"
docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "
SELECT
current_database() as database_name,
current_user as current_user,
version() as postgresql_version;
" 2>/dev/null || {
echo -e "${RED}Ошибка подключения к базе данных${NC}"
return 1
}
echo ""
# Показываем список таблиц
echo -e "${YELLOW}Таблицы в базе данных:${NC}"
TABLES_OUTPUT=$(docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "\dt" 2>/dev/null)
if echo "$TABLES_OUTPUT" | grep -q "No relations found"; then
echo -e "${YELLOW}База данных пуста - таблицы еще не созданы${NC}"
echo -e "${BLUE}Это нормально если бот еще ни разу не запускался${NC}"
elif [ -z "$TABLES_OUTPUT" ]; then
echo -e "${YELLOW}Не удается получить список таблиц${NC}"
else
echo "$TABLES_OUTPUT"
fi
echo ""
# Показываем размер базы данных
echo -e "${YELLOW}Размер базы данных:${NC}"
docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "
SELECT
pg_database.datname,
pg_size_pretty(pg_database_size(pg_database.datname)) AS size
FROM pg_database
WHERE datname = 'remnawave_bot';
" 2>/dev/null || {
echo -e "${RED}Не удается получить размер базы данных${NC}"
}
}
edit_env_file() {
ensure_nano
if [ ! -f "$ENV_FILE" ]; then
error "Файл .env не найден: $ENV_FILE"
return 1
fi
log "Открытие .env файла для редактирования..."
log "После изменений перезапустите бота для применения настроек"
# Создаем резервную копию .env файла
cp "$ENV_FILE" "$ENV_FILE.backup.$(date +%Y%m%d_%H%M%S)"
nano "$ENV_FILE"
log "Редактирование завершено"
echo -e "${YELLOW}Не забудьте перезапустить бота для применения изменений!${NC}"
}
# Удаление базы данных
remove_database() {
warn "ВНИМАНИЕ! Это действие удалит всю базу данных!"
warn "Все данные бота (пользователи, подписки, настройки) будут потеряны!"
read -p "Вы уверены? Введите 'YES' для подтверждения: " confirm
if [ "$confirm" = "YES" ]; then
log "Удаление базы данных..."
cd "$BOT_DIR"
# Останавливаем все сервисы если они запущены
if docker compose ps --services --filter "status=running" | grep -q "."; then
log "Остановка всех контейнеров..."
docker compose down
fi
# Удаляем volume с данными PostgreSQL
log "Удаление volume с данными PostgreSQL..."
VOLUME_NAME=$(docker compose config --volumes 2>/dev/null | grep postgres || echo "bedolaga-bot_postgres_data")
# Пробуем удалить volume разными способами
if docker volume ls | grep -q "$VOLUME_NAME"; then
if docker volume rm "$VOLUME_NAME" 2>/dev/null; then
log "Volume $VOLUME_NAME успешно удален"
else
log "Принудительное удаление volume..."
docker volume rm "$VOLUME_NAME" --force 2>/dev/null || true
fi
fi
# Также пробуем удалить стандартное имя volume
docker volume rm "$(basename $BOT_DIR)_postgres_data" 2>/dev/null || true
# Удаляем все неиспользуемые volumes связанные с проектом
log "Очистка неиспользуемых volumes..."
docker volume prune -f 2>/dev/null || true
log "База данных удалена"
log "При следующем запуске бота будет создана новая пустая база данных"
else
log "Операция отменена"
fi
}
# Полное удаление бота
remove_bot() {
warn "ВНИМАНИЕ! Это действие полностью удалит бота и все данные!"
read -p "Вы уверены? Введите 'YES' для подтверждения: " confirm
if [ "$confirm" = "YES" ]; then
log "Удаление бота..."
cd "$BOT_DIR"
docker compose down -v
systemctl disable bedolaga-bot.service 2>/dev/null || true
rm -f "$SERVICE_FILE"
rm -rf "$BOT_DIR"
log "Бот полностью удален"
exit 0
else
log "Операция отменена"
fi
}
# Главное меню
show_menu() {
clear
echo -e "${BLUE}=== RemnaWave Bedolaga Bot Management ===${NC}"
echo ""
# Показать статус бота
echo -e "${YELLOW}Статус бота:${NC}"
if check_bot_status; then
echo -e "🟢 Бот: ${GREEN}ЗАПУЩЕН${NC}"
else
echo -e "🔴 Бот: ${RED}ОСТАНОВЛЕН${NC}"
fi
# Показать статус подключения к API
echo -e "${YELLOW}Подключение к RemnaWave API:${NC}"
if check_remnawave_connection; then
echo -e "🟢 API: ${GREEN}ПОДКЛЮЧЕН${NC}"
else
echo -e "🔴 API: ${RED}НЕ ПОДКЛЮЧЕН${NC}"
fi
echo ""
echo -e "${YELLOW}Доступные действия:${NC}"
if check_bot_status; then
# Бот запущен
echo "1) Выключить бот"
echo "2) Перезапустить бот"
echo "3) Посмотреть логи в реальном времени"
echo "4) Обновить бот"
echo "5) Посмотреть логи"
echo "6) Создать резервную копию БД"
echo "7) Восстановить базу данных"
echo "8) Редактировать .env файл"
echo "9) Диагностика базы данных"
echo "10) Удалить базу данных"
echo "11) Полностью удалить бота"
echo "0) Выход"
read -p "Выберите действие: " choice
case $choice in
1) stop_bot; read -p "Нажмите Enter для продолжения..."; ;;
2) restart_bot; read -p "Нажмите Enter для продолжения..."; ;;
3) view_live_logs; ;;
4) update_bot; read -p "Нажмите Enter для продолжения..."; ;;
5) view_logs; read -p "Нажмите Enter для продолжения..."; ;;
6) backup_database; read -p "Нажмите Enter для продолжения..."; ;;
7) restore_database; read -p "Нажмите Enter для продолжения..."; ;;
8) edit_env_file; read -p "Нажмите Enter для продолжения..."; ;;
9) diagnose_database; read -p "Нажмите Enter для продолжения..."; ;;
10) remove_database; read -p "Нажмите Enter для продолжения..."; ;;
11) remove_bot; ;;
0) exit 0; ;;
*) error "Неверный выбор"; read -p "Нажмите Enter для продолжения..."; ;;
esac
else
# Бот остановлен
echo "1) Запустить бот"
echo "2) Обновить бот"
echo "3) Посмотреть логи"
echo "4) Создать резервную копию БД"
echo "5) Восстановить базу данных"
echo "6) Редактировать .env файл"
echo "7) Диагностика базы данных"
echo "8) Удалить базу данных"
echo "9) Полностью удалить бота"
echo "0) Выход"
read -p "Выберите действие: " choice
case $choice in
1) start_bot; read -p "Нажмите Enter для продолжения..."; ;;
2) update_bot; read -p "Нажмите Enter для продолжения..."; ;;
3) view_logs; read -p "Нажмите Enter для продолжения..."; ;;
4) backup_database; read -p "Нажмите Enter для продолжения..."; ;;
5) restore_database; read -p "Нажмите Enter для продолжения..."; ;;
6) edit_env_file; read -p "Нажмите Enter для продолжения..."; ;;
7) diagnose_database; read -p "Нажмите Enter для продолжения..."; ;;
8) remove_database; read -p "Нажмите Enter для продолжения..."; ;;
9) remove_bot; ;;
0) exit 0; ;;
*) error "Неверный выбор"; read -p "Нажмите Enter для продолжения..."; ;;
esac
fi
}
# Основная функция установки
install_bot() {
log "Начало установки RemnaWave Bedolaga Bot"
update_system
install_docker
create_project_structure
create_docker_compose
create_env_file
create_service
log "Установка завершена!"
log "Бот установлен в: $BOT_DIR"
log "Для управления ботом используйте это меню или systemctl"
read -p "Нажмите Enter для перехода в меню управления..."
}
# Главная функция
main() {
check_root
if check_installation; then
log "Бот уже установлен. Переход в меню управления..."
ensure_nano # Убеждаемся что nano установлен
else
install_bot
fi
# Основной цикл меню
while true; do
show_menu
done
}
# Запуск скрипта
main "$@"
+142 -43
View File
@@ -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}")
+919 -44
View File
File diff suppressed because it is too large Load Diff
+589 -282
View File
File diff suppressed because it is too large Load Diff
+221 -75
View File
@@ -4,7 +4,6 @@ from typing import List, Optional, Dict
from translations import t
def language_keyboard() -> InlineKeyboardMarkup:
"""Language selection keyboard"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="🇷🇺 Русский", callback_data="lang_ru"),
@@ -13,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
+327
View File
@@ -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 []
+93 -40
View File
@@ -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()
+4 -19
View File
@@ -8,7 +8,6 @@ from config import Config
logger = logging.getLogger(__name__)
class DatabaseMiddleware(BaseMiddleware):
"""Middleware for database operations"""
def __init__(self, db: Database):
self.db = db
@@ -23,7 +22,6 @@ class DatabaseMiddleware(BaseMiddleware):
return await handler(event, data)
class UserMiddleware(BaseMiddleware):
"""Middleware for user management"""
def __init__(self, db: Database, config: Config):
self.db = db
@@ -39,25 +37,21 @@ class UserMiddleware(BaseMiddleware):
if telegram_user and not telegram_user.is_bot:
try:
# Get or create user
user = await self.db.get_user_by_telegram_id(telegram_user.id)
if not user:
# Check if user is admin
is_admin = telegram_user.id in self.config.ADMIN_IDS
# 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
+279
View File
@@ -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}")
+505 -244
View File
File diff suppressed because it is too large Load Diff
-218
View File
@@ -1,218 +0,0 @@
#!/bin/bash
# === RemnaWave Bot Setup & Management Script ===
SERVICE_NAME="remnawave-bot"
BOT_FILE="main.py"
ENV_FILE=".env"
VENV_DIR="venv"
# Select language
echo "🌐 Select language / Выберите язык:"
echo "1) English"
echo "2) Русский"
read -p ">>> " LANG_CHOICE
case $LANG_CHOICE in
2) LANG_CODE="ru" ;;
*) LANG_CODE="en" ;;
esac
# === Internationalized messages ===
msg() {
case $1 in
check_python) [[ $LANG_CODE == "ru" ]] && echo "🔍 Проверка Python..." || echo "🔍 Checking Python..." ;;
python_missing) [[ $LANG_CODE == "ru" ]] && echo "❌ Python 3 не установлен. Установите Python 3.8+." || echo "❌ Python 3 is not installed. Please install Python 3.8+." ;;
version) echo "📋 Python version: $PYTHON_VERSION" ;;
creating_venv) [[ $LANG_CODE == "ru" ]] && echo "🔧 Создание виртуального окружения..." || echo "🔧 Creating virtual environment..." ;;
activating_venv) [[ $LANG_CODE == "ru" ]] && echo "🔧 Активация виртуального окружения..." || echo "🔧 Activating virtual environment..." ;;
upgrading_pip) [[ $LANG_CODE == "ru" ]] && echo "📦 Обновление pip..." || echo "📦 Upgrading pip..." ;;
installing_requirements) [[ $LANG_CODE == "ru" ]] && echo "📦 Установка зависимостей..." || echo "📦 Installing requirements..." ;;
env_missing) [[ $LANG_CODE == "ru" ]] && echo "⚠️ Файл .env не найден!" || echo "⚠️ .env file not found!" ;;
env_created) [[ $LANG_CODE == "ru" ]] && echo "📋 Создан .env файл. Пожалуйста, отредактируйте его." || echo "📋 .env created. Please edit it." ;;
validating) [[ $LANG_CODE == "ru" ]] && echo "🔍 Проверка конфигурации..." || echo "🔍 Validating configuration..." ;;
config_ok) [[ $LANG_CODE == "ru" ]] && echo "✅ Конфигурация прошла проверку." || echo "✅ Configuration validated successfully." ;;
creating_service) [[ $LANG_CODE == "ru" ]] && echo "🔧 Создание systemd-сервиса..." || echo "🔧 Creating systemd service..." ;;
service_created) [[ $LANG_CODE == "ru" ]] && echo "✅ Сервис создан и включён." || echo "✅ Service created and enabled." ;;
start_prompt) [[ $LANG_CODE == "ru" ]] && echo "🚀 Запуск бота..." || echo "🚀 Starting the bot..." ;;
already_running) [[ $LANG_CODE == "ru" ]] && echo "🟢 Бот уже запущен!" || echo "🟢 Bot is already running!" ;;
last_logs) [[ $LANG_CODE == "ru" ]] && echo "📄 Последние 20 строк лога:" || echo "📄 Last 20 log lines:" ;;
action_menu)
[[ $LANG_CODE == "ru" ]] && {
echo -e "\nЧто вы хотите сделать?"
echo "1) Остановить бота"
echo "2) Перезапустить бота"
echo "3) Выйти"
} || {
echo -e "\nWhat would you like to do?"
echo "1) Stop the bot"
echo "2) Restart the bot"
echo "3) Exit"
}
;;
restarting) [[ $LANG_CODE == "ru" ]] && echo "🔄 Перезапуск бота..." || echo "🔄 Restarting bot..." ;;
completed)
echo ""
[[ $LANG_CODE == "ru" ]] && {
echo "🎉 Установка завершена!"
echo "📋 Возможности бота:"
echo " ✅ Мультиязычный интерфейс"
echo " ✅ Управление балансом и подписками"
echo " ✅ Промокоды и поддержка"
echo ""
} || {
echo "🎉 Installation complete!"
echo "📋 Bot Features:"
echo " ✅ Multi-language support"
echo " ✅ Subscription and balance management"
echo " ✅ Promocode system and support"
echo ""
}
;;
esac
}
# === Check if bot is running ===
if systemctl is-active --quiet "$SERVICE_NAME"; then
msg already_running
msg last_logs
sudo journalctl -u "$SERVICE_NAME" -n 20 --no-pager
msg action_menu
read -p ">>> " choice
case $choice in
1)
[[ $LANG_CODE == "ru" ]] && echo "🛑 Остановка бота..." || echo "🛑 Stopping the bot..."
sudo systemctl stop "$SERVICE_NAME"
[[ $LANG_CODE == "ru" ]] && echo "✅ Бот остановлен." || echo "✅ Bot stopped."
exit 0
;;
2)
msg restarting
sudo systemctl restart "$SERVICE_NAME"
[[ $LANG_CODE == "ru" ]] && echo "✅ Бот перезапущен." || echo "✅ Bot restarted."
exit 0
;;
3)
[[ $LANG_CODE == "ru" ]] && echo "👋 Выход." || echo "👋 Exiting."
exit 0
;;
*)
[[ $LANG_CODE == "ru" ]] && echo "⚠️ Неверный выбор." || echo "⚠️ Invalid choice."
exit 1
;;
esac
fi
# === Install/Setup ===
msg check_python
if ! command -v python3 &> /dev/null; then
msg python_missing
exit 1
fi
PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
msg version
if [ ! -d "$VENV_DIR" ]; then
msg creating_venv
python3 -m venv "$VENV_DIR"
fi
msg activating_venv
source "$VENV_DIR/bin/activate"
msg upgrading_pip
pip install --upgrade pip
msg installing_requirements
pip install -r requirements.txt
# Create .env if needed
if [ ! -f "$ENV_FILE" ]; then
msg env_missing
cat > "$ENV_FILE" <<EOF
# Bot Configuration
BOT_TOKEN=
# RemnaWave API Configuration
REMNAWAVE_URL=
REMNAWAVE_MODE=
REMNAWAVE_TOKEN=
# Admin Configuration (comma-separated list of Telegram user IDs)
ADMIN_IDS=
# Support Configuration
SUPPORT_USERNAME=support
# Database Configuration (optional, defaults to SQLite)
# DATABASE_URL=sqlite+aiosqlite:///bot.db
# Trial Configuration
TRIAL_ENABLED=true
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_GB=2
TRIAL_SQUAD_UUID=
TRIAL_PRICE=0.0
EOF
msg env_created
echo ""
read -p "🔧 Press Enter after configuring .env / Нажмите Enter после настройки .env..."
fi
# Load env
export $(grep -v '^#' "$ENV_FILE" | xargs)
msg validating
if [[ -z "$BOT_TOKEN" || "$BOT_TOKEN" == "your_telegram_bot_token_here" ]]; then
echo "❌ BOT_TOKEN not set properly."
exit 1
fi
if [[ -z "$REMNAWAVE_URL" || "$REMNAWAVE_URL" == "https://your-panel.com" ]]; then
echo "❌ REMNAWAVE_URL not set properly."
exit 1
fi
if [[ -z "$REMNAWAVE_TOKEN" || "$REMNAWAVE_TOKEN" == "your_jwt_token_here" ]]; then
echo "❌ REMNAWAVE_TOKEN not set properly."
exit 1
fi
msg config_ok
# Create systemd service
read -p "🔧 Create systemd service? (y/n): " CREATE_SERVICE
if [[ "$CREATE_SERVICE" == "y" || "$CREATE_SERVICE" == "Y" ]]; then
msg creating_service
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
CURRENT_DIR=$(pwd)
CURRENT_USER=$(whoami)
sudo tee "$SERVICE_FILE" > /dev/null <<EOF
[Unit]
Description=RemnaWave Bot
After=network.target
[Service]
Type=simple
User=$CURRENT_USER
WorkingDirectory=$CURRENT_DIR
Environment=PATH=$CURRENT_DIR/$VENV_DIR/bin
EnvironmentFile=$CURRENT_DIR/.env
ExecStart=$CURRENT_DIR/$VENV_DIR/bin/python3 $BOT_FILE
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable "$SERVICE_NAME"
msg service_created
fi
msg completed
msg start_prompt
python3 "$BOT_FILE"
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

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

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

+348
View File
@@ -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("❌ Произошла ошибка")
+754 -432
View File
File diff suppressed because it is too large Load Diff
+95 -61
View File
@@ -1,10 +1,7 @@
TRANSLATIONS = {
'ru': {
# Language selection
'select_language': '🌍 Выберите язык / Choose language',
'language_selected': '🇷🇺 Язык выбран: Русский',
# Main menu
'main_menu': '📱 Главное меню',
'balance': 'Баланс',
'my_subscriptions': 'Мои подписки',
@@ -18,9 +15,6 @@ TRANSLATIONS = {
'trial_error': '❌ Ошибка при создании тестовой подписки',
'trial_info': '🧪 Тестовая подписка выдается на три дня!\n\nНа тарифе действует ограничение в 3 дня\n\nОграничение по трафику - 2гб',
'subscriptions_list': '📋 Список подписок в продаже:',
# Balance menu
'your_balance': '💰 Ваш баланс: {balance:.2f} руб.',
'topup_balance': 'Пополнить баланс',
'payment_history': 'История платежей',
@@ -31,6 +25,11 @@ TRANSLATIONS = {
'nodes_management': 'Управление нодами',
'system_users': 'Системные пользователи',
'system_statistics': 'Системная статистика',
'subscription_expired_message': '❌ Ваша подписка \'{name}\' истекла!\n\n🔄 Для возобновления доступа продлите подписку в разделе \'Мои подписки\'.\n\n💡 Подписку можно продлить в любой момент - срок действия будет увеличен с текущего момента.',
'subscription_expires_today': '⚠️ Срочно! Ваша подписка \'{name}\' истекает сегодня!\n\n🔄 Продлите её прямо сейчас в разделе \'Мои подписки\', чтобы не потерять доступ.\n\n⏰ У вас есть несколько часов до истечения!',
'subscription_expires_tomorrow': '⚠️ Внимание! Ваша подписка \'{name}\' истекает завтра!\n\n🔄 Чтобы не потерять доступ, продлите её сейчас в разделе \'Мои подписки\'.\n\n✨ При продлении срок действия будет увеличен с момента истечения.',
'subscription_expires_day_after_tomorrow': '⏰ Ваша подписка \'{name}\' истекает послезавтра ({days} дня)!\n\n🔄 Рекомендуем продлить её заранее в разделе \'Мои подписки\'.\n\n💰 Проверьте баланс - возможно, стоит пополнить его заранее.',
'subscription_expires_in_days': '⏳ Ваша подписка \'{name}\' истекает через {days} дн.!\n\n🔄 Продлить подписку можно в разделе \'Мои подписки\'.\n\n💡 Продление заранее увеличивает срок с момента истечения текущей подписки.',
'restart_nodes': 'Перезагрузить ноды',
'bulk_operations': 'Массовые операции',
'search_user': 'Поиск пользователя',
@@ -38,7 +37,6 @@ TRANSLATIONS = {
'reset_traffic': 'Сбросить трафик',
'disable_user': 'Отключить пользователя',
'enable_user': 'Включить пользователя',
'send_message': 'Отправить сообщение',
'send_to_user': 'Отправить пользователю',
'send_to_all': 'Отправить всем',
@@ -53,8 +51,6 @@ TRANSLATIONS = {
'subscription_extended': '✅ Подписка продлена',
'extend_confirmation': 'Продлить подписку "{name}" на {days} дней за {price:.2f} руб.?',
'subscription_expires_soon': 'Подписка истекает через {days} дней',
# Subscriptions
'no_subscriptions': '❌ У вас нет активных подписок',
'subscription_info': '📋 Подписка: {name}\n💰 Цена: {price:.2f} руб.\n⏱ Длительность: {days} дней\n📊 Трафик: {traffic}\n\n{description}',
'unlimited_traffic': 'Безлимитный',
@@ -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:
+233 -241
View File
@@ -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}м"