Compare commits

...

90 Commits

Author SHA1 Message Date
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
48 changed files with 6343 additions and 2421 deletions
+23 -13
View File
@@ -1,29 +1,39 @@
# 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 # Количество выигрышных чисел из общего количества
+2
View File
@@ -0,0 +1,2 @@
/screens
/assets
+495 -162
View File
@@ -1,234 +1,567 @@
<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` |
### 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
```
Создание и покупка подписок с управлением трафиком, длительностью и ценой
### Проверка состояния
Бесплатная тестовая подписка с заданными ограничениями(срок, лимит трафика, назначение сквада)
```bash
# Статус контейнеров
docker-compose ps
Пополнение баланса: 1) Через саппорт в ручную 2) Отправка заявки с суммой админу (С возможность подтвердить/отклонить заявку)
# Использование ресурсов
docker stats
```
Управление балансом пользователей (пополнение, списание)
### Backup базы данных
Промокоды со скидками и ограничениями по использованию
```bash
# Создание backup
docker exec remnawave_bot_db pg_dump -U remnawave_user remnawave_bot > backup.sql
Полноценная админ-панель с контролем пользователей, созданием подписок(Подтягивает UUID сквада из панели), платежей и статистикой
# Восстановление
docker exec -i remnawave_bot_db psql -U remnawave_user remnawave_bot < backup.sql
```
Рассылка сообщений отдельным пользователям и всем сразу
## 🔧 Обновление
Сервис контроля истечения сроков действия подписки(Уведомляет об истечении за указанный в настройках срок), уведомления с предложением продления подписи. (NEW)
### Обновление Docker образа
Интеграция с RemnaWave API для управления подписками и пользователями RemnaWave
```bash
# Остановить сервисы
docker-compose down
Полная синхранизация Remnawave <--> Bot - Перенос подписок из панели Remnawave в бот по Telegram id
# Обновить образ
docker-compose pull bot
Управление системой Remnawave (NEW)
# Запустить снова
docker-compose up -d
Управление платежами (подтверждение, отклонение) + История платежей(Все действия с балансом и подписками в постраничной истории)
# Проверить логи
docker-compose logs -f bot
```
### Ручная сборка образа
#Требования
```bash
# Клонировать репозиторий
git clone https://github.com/your-repo/remnawave-telegram-bot.git
cd remnawave-telegram-bot
Pip
# Собрать образ
docker build -t remnawave-bot .
Python 3.8+
# Изменить docker-compose.yml
# bot:
# image: remnawave-bot # вместо fr1ngg/remnawave-bedolaga-telegram-bot
```
PostgreSQL, SQLite или другая поддерживаемая SQL-база данных
## 🎯 Использование
Токен Telegram-бота
### Для пользователей
URL и токен RemnaWave API
1. **Запуск бота** - найдите бота по username и нажмите `/start`
2. **Выбор языка** - выберите русский или английский
3. **Пополнение баланса** - через меню "💰 Баланс" → "💳 Пополнить"
4. **Покупка подписки** - "🛒 Купить подписку" → выбор тарифа → оплата
5. **Управление подписками** - "📋 Мои подписки" → выбор подписки
6. **Реферальная программа** - "👥 Рефералы" → поделиться ссылкой
Ссылки на подписку из ремны формата SUB_PUBLIC_DOMAIN=sub.example.com/sub
### Для администраторов
#Установка
Администратор видит дополнительную кнопку "⚙️ Админ панель" с возможностями:
1. Клонируйте репозиторий:
- **📦 Управление подписками** - создание тарифов
- **👥 Управление пользователями** - поиск, редактирование
- **💰 Управление балансом** - пополнение балансов
- **🎁 Управление промокодами** - создание скидок
- **📨 Рассылки** - отправка сообщений
- **🖥 Система RemnaWave** - мониторинг нод и синхронизация
git clone https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot
cd remnawave-bedolaga-telegram-bot
## 🔒 Безопасность
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
- ✅ Измените пароли PostgreSQL в `docker-compose.yml`
- ✅ Не публикуйте файл `.env` в репозитории
- ✅ Используйте HTTPS для RemnaWave API
- ✅ Регулярно создавайте backup базы данных
- ✅ Ограничьте доступ к серверу через firewall
4. Соберите образ (Makefile Dockerfile docker-compose):
### Переменные безопасности
make build
```bash
# Генерация безопасных паролей
openssl rand -base64 32 # для PostgreSQL
openssl rand -hex 16 # для Redis
```
5. Запуск:
## 🐛 Устранение неполадок
Запуск минимальной конфигурации (бот + база данных):
make up-min
### Частые проблемы
Или запуск с Redis:
make up
| Проблема | Решение |
|----------|---------|
| Бот не отвечает | Проверьте `BOT_TOKEN` и интернет |
| Ошибка подключения к БД | Проверьте статус PostgreSQL контейнера |
| Ошибки RemnaWave API | Проверьте `REMNAWAVE_URL` и `REMNAWAVE_TOKEN` |
| Игра удачи не работает | Проверьте `LUCKY_GAME_ENABLED=true` |
Или запуск со всеми сервисами включая Nginx:
make up-full
### Диагностика
5. Управление
```bash
# Проверка переменных окружения
docker exec remnawave_bot env | grep BOT_
Просмотр логов:
# Подключение к базе данных
docker exec -it remnawave_bot_db psql -U remnawave_user remnawave_bot
make logs-bot
# Перезапуск бота
docker-compose restart bot
```
Статус сервисов:
make status
### Логи и отладка
Перезапуск:
make restart
```bash
# Детальные логи
docker-compose logs -f --tail 100 bot
Остановка:
make down
#Конфигурация
# Ошибки базы данных
docker-compose logs postgres | grep ERROR
BOT_TOKEN — токен Telegram бота от BotFather.
# Проверка health checks
docker-compose ps
```
REMNAWAVE_URL — URL API RemnaWave.
## 📈 Производительность
REMNAWAVE_MODE=remote
### Рекомендуемые ресурсы
REMNAWAVE_TOKEN — токен доступа к API RemnaWave.
| Пользователей | 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 |
DATABASE_URL — строка подключения к базе данных.
### Оптимизация
SUBSCRIPTION_BASE_URL=https://sub.example.com (без / на конце)
- Включите Redis для кэширования (профиль `with-redis`)
- Настройте PostgreSQL для production
- Используйте nginx как reverse proxy
- Мониторьте ресурсы через `docker stats`
ADMIN_IDS — через запятую Telegram ID администраторов.
## 🤝 Поддержка
SUPPORT_USERNAME — ник поддержки, без @ указывать
### Получить помощь
Параметры тестовой подписки (включение, длительность, трафик, UUID squad, цена).
- 💬 **Telegram**: @fringg
TRIAL_ENABLED=true/false
### Вклад в проект
TRIAL_DURATION_DAYS=3 (дни)
1. Fork репозитория
2. Создайте feature branch
3. Внесите изменения
4. Создайте Pull Request
TRIAL_TRAFFIC_GB=2
## 📄 Лицензия
TRIAL_SQUAD_UUID=(УКазать UUID сквада из панели!)
Этот проект распространяется под лицензией MIT. См. файл [LICENSE](LICENSE) для подробностей.
TRIAL_PRICE=0.0(не трогать)
## 🗺 Roadmap & TODO
Monitor Service Settings (дополнительные настройки)
### 📋 Общий план развития
MONITOR_CHECK_INTERVAL=3600 (Запуск службы проверки)
| № | Задача | Статус | Версия | Приоритет | Описание |
|---|---------|--------|--------|-----------|----------|
| 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_DAILY_CHECK_HOUR=10 (Разовый чек в определенный промежуток дня)
MONITOR_WARNING_DAYS=2 (За сколько дней слать уведомления)
### 🚀 Хотите помочь?
**Разработчикам:**
- 🐛 Найдите и исправьте баги
- ✨ Предложите новые функции
- 📝 Улучшите документацию
- 🧪 Напишите тесты
#Использование
**Пользователям:**
- 💭 Поделитесь идеями в Issues
- 🐞 Сообщите о найденных проблемах
- ⭐ Поставьте звезду проекту
- 📢 Расскажите друзьям
/start - запуск
**Спонсорам:**
- 💰 Поддержите разработку
- 🎯 Закажите приоритетные функции
- 🏢 Корпоративная поддержка
#Синхронизация подписок
---
Вы можете перенести свои существующие подписки из панели Remnawave прямо в бота всего одним кликом.
Для этого в админ панеле реализован соостветствующий пункт: Админ панель - Система Remnawave - Синхронизация с Remnawave - Импорт всех по Telegram ID. После нажатия подтянет всех пользователей в бота, подпискам из панели будет назначено имя "Старая подписка" - такую подписку невозможно продлить.
**⚠️ Важные замечания от автора:**
ДОПОЛНИТЕЛЬНО:
Реализована возможность зачистки импортированных из панели подписок по тг айди Админ панель - Система Remnawave - Синхронизация с Remnawave - Просмотрт планов - Удалалить импортированные
> 🗑 **Осторожно с удалением подписок!** Сейчас удаление плана подписки скроет эту подписку у всех пользователей, которые ее купили. Используйте деактивацию вместо удаления. Эта проблема будет исправлена в v1.4.0.
Остальное трогать без понимания кода - не рекомендую.
---
#Структура проекта
**⭐ Если проект был полезен, поставьте звездочку на GitHub!**
main.py — главный файл запуска.
---
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 сообщества*
+1701 -745
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

+109 -42
View File
@@ -4,70 +4,137 @@ from typing import List
@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 # Количество выигрышных чисел
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
return Config(
# Bot configuration
BOT_TOKEN=os.getenv('BOT_TOKEN', ''),
BOT_USERNAME=os.getenv('BOT_USERNAME', ''),
# Database
DATABASE_URL=os.getenv('DATABASE_URL', 'sqlite+aiosqlite:///bot.db'),
# RemnaWave API
REMNAWAVE_URL=os.getenv('REMNAWAVE_URL', ''),
REMNAWAVE_TOKEN=os.getenv('REMNAWAVE_TOKEN', ''),
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)
)
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'
]
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}")
+745 -44
View File
File diff suppressed because it is too large Load Diff
+530 -271
View File
File diff suppressed because it is too large Load Diff
+172 -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,7 +189,6 @@ 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")],
@@ -220,16 +197,115 @@ def admin_balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
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 +315,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 +333,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 +345,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 +356,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 +370,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 +378,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 +393,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 +401,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 +408,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 +426,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 +435,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 +448,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 +465,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 +506,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 +523,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 +533,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 +543,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 +553,52 @@ 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")]
])
+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 []
+73 -40
View File
@@ -6,9 +6,19 @@ 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
# 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 +26,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 +37,6 @@ logging.basicConfig(
logger = logging.getLogger(__name__)
class BotApplication:
"""Main bot application class"""
def __init__(self):
self.config = None
@@ -39,11 +47,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 +65,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 +84,20 @@ 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()
async def _init_database(self):
"""Initialize database with retry logic"""
max_retries = 3
for attempt in range(max_retries):
try:
@@ -99,10 +109,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 +122,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 +164,61 @@ 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)
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 +228,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 +237,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 +244,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 +251,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 +261,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
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

+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}м"