Compare commits

...

76 Commits

Author SHA1 Message Date
Egor 2a0a2dccff Update README.md 2025-08-13 06:55:22 +03:00
Egor 66a6f32d4c Update .env.example 2025-08-13 06:51:35 +03:00
Egor 02b3f61953 Update translations.py 2025-08-13 06:37:10 +03:00
Egor dc08526255 Update config.py 2025-08-13 06:36:52 +03:00
Egor 2ca339610b Update subscription_monitor.py 2025-08-13 06:36:32 +03:00
Egor ffe65937c1 Update README.md 2025-08-12 19:16:27 +03:00
Egor 4b6f03eef5 Update README.md 2025-08-12 19:15:02 +03:00
Egor d2f530a7fe Update bedolaga.sh 2025-08-12 19:10:37 +03:00
Egor 44ea4b6b13 Update .env.example 2025-08-12 18:55:22 +03:00
Egor ebc8421a70 Update README.md 2025-08-12 18:52:20 +03:00
Egor 526f142dea Update remnawave_api.py 2025-08-12 18:12:34 +03:00
Egor ca431d36e6 Add files via upload 2025-08-12 03:49:13 +03:00
Egor 68a81bdd89 Delete screens/уведомления/Снимок экрана 2025-08-12 в 01.45.14.png 2025-08-12 03:48:59 +03:00
Fr1ngg 68a3d713ef Update README.md 2025-08-12 03:19:42 +03:00
Fr1ngg 50d3d08e38 Update README.md 2025-08-12 03:14:42 +03:00
Fr1ngg 9a2fec4d8f Update README.md 2025-08-12 03:10:34 +03:00
Fr1ngg e68b1d2092 Add files via upload 2025-08-12 02:48:15 +03:00
Fr1ngg 6dfedef81e Update README.md 2025-08-12 02:36:41 +03:00
Fr1ngg 317e39c823 Update keyboards.py 2025-08-12 02:34:19 +03:00
Fr1ngg 0072a3cb71 Update translations.py 2025-08-12 02:34:03 +03:00
Fr1ngg 8d021c1530 Update database.py 2025-08-12 02:33:44 +03:00
Fr1ngg 23606dc2d4 Update admin_handlers.py 2025-08-12 02:33:23 +03:00
Fr1ngg 0838c4196d Add files via upload 2025-08-12 01:47:52 +03:00
Fr1ngg 65605a0de2 Create 1 2025-08-12 01:47:38 +03:00
Fr1ngg a20ac80281 Delete screens/уведомления 2025-08-12 01:47:25 +03:00
Fr1ngg 02e83fb0d0 Create уведомления 2025-08-12 01:47:13 +03:00
Fr1ngg eddad0db6e Add files via upload 2025-08-12 01:46:00 +03:00
Fr1ngg ffad0afb61 Delete screens/Balance/Снимок экрана 2025-08-09 в 07.54.52.png 2025-08-12 01:44:40 +03:00
Fr1ngg 83eed8e1cd Add files via upload 2025-08-12 01:44:17 +03:00
Fr1ngg 0c7c8abade Add files via upload 2025-08-12 01:42:18 +03:00
Fr1ngg a064890ed8 Delete screens/adminpanel/Снимок экрана 2025-08-09 в 08.09.05.png 2025-08-12 01:41:33 +03:00
Fr1ngg f310f4a35e Delete screens/sub/Снимок экрана 2025-08-09 в 08.05.47.png 2025-08-12 01:41:14 +03:00
Fr1ngg 0268e09882 Delete screens/Главное меню/Mainmenu.png 2025-08-12 01:40:23 +03:00
Fr1ngg 449814d0df Add files via upload 2025-08-12 01:40:09 +03:00
Fr1ngg a3d3577103 Update README.md 2025-08-12 01:37:43 +03:00
Fr1ngg a3086fdd2c Update README.md 2025-08-12 01:34:57 +03:00
Fr1ngg daafc6a41e Update README.md 2025-08-12 01:31:42 +03:00
Fr1ngg c27ee116f7 Update README.md 2025-08-12 01:28:56 +03:00
Fr1ngg 5167cde443 Update bedolaga.sh 2025-08-12 01:25:30 +03:00
Fr1ngg 34542d3a9c Update bedolaga.sh 2025-08-12 01:09:56 +03:00
Fr1ngg e73d1df4cc Update logo2.svg 2025-08-12 01:05:39 +03:00
Fr1ngg f9a95c3d48 Update README.md 2025-08-12 01:04:53 +03:00
Fr1ngg 2dd2b5fdbf Update README.md 2025-08-12 01:04:37 +03:00
Fr1ngg 896f1db9d4 Update README.md 2025-08-11 23:09:23 +03:00
Fr1ngg a1bd2d775f Create emergency_fix.py 2025-08-11 22:54:05 +03:00
Fr1ngg 8cbe23f9ed Update database.py 2025-08-11 22:31:17 +03:00
Fr1ngg edc891c93d Update README.md 2025-08-11 22:06:49 +03:00
Fr1ngg 2d706a4be0 Update keyboards.py 2025-08-11 22:01:38 +03:00
Fr1ngg 32cb81ef34 Update handlers.py 2025-08-11 22:01:09 +03:00
Fr1ngg 88aa3c4a3d Update database.py
autopay mod
2025-08-11 22:00:43 +03:00
Fr1ngg e813d3ccb7 Create autopay_service.py 2025-08-11 22:00:19 +03:00
Fr1ngg a24d7a4e24 Update main.py 2025-08-11 21:59:51 +03:00
Fr1ngg 39987b9717 Update admin_handlers.py
autopay mod
2025-08-11 21:59:31 +03:00
Fr1ngg c77a1ea4f8 Update README.md 2025-08-11 17:57:35 +03:00
Fr1ngg 64b5d90d66 Update README.md 2025-08-11 03:00:54 +03:00
Fr1ngg 5a1fb32e1d Update admin_handlers.py 2025-08-11 02:58:19 +03:00
Fr1ngg c4d8108e8c Update config.py 2025-08-11 02:53:37 +03:00
Fr1ngg 1350d80083 Update database.py 2025-08-11 02:51:28 +03:00
Fr1ngg e0cc218007 Update handlers.py 2025-08-11 02:49:06 +03:00
Fr1ngg bcaf8f2247 Update keyboards.py 2025-08-11 02:48:36 +03:00
Fr1ngg d1190ed9dc Update stars_handlers.py 2025-08-11 02:47:05 +03:00
Fr1ngg 513d6037d4 Update subscription_monitor.py 2025-08-11 02:46:40 +03:00
Fr1ngg e86012b2ab Update translations.py 2025-08-11 02:46:11 +03:00
Fr1ngg 214e381dc5 Update README.md 2025-08-10 10:41:22 +03:00
Fr1ngg aa79cb670b Update .env.example 2025-08-10 10:30:33 +03:00
Fr1ngg f0106e15fa Create stars_handlers.py 2025-08-10 10:26:28 +03:00
Fr1ngg 1d194c6be2 Update .env.example 2025-08-10 10:18:03 +03:00
Fr1ngg 6917890007 Update admin_handlers.py 2025-08-10 10:15:34 +03:00
Fr1ngg 1c69de7ecd Update config.py 2025-08-10 10:13:14 +03:00
Fr1ngg 62188793d9 Update database.py 2025-08-10 10:12:21 +03:00
Fr1ngg 585ee2d42b Update handlers.py 2025-08-10 10:11:12 +03:00
Fr1ngg 2a58cfd45c Update keyboards.py 2025-08-10 10:10:54 +03:00
Fr1ngg 93003c5fe0 Update main.py 2025-08-10 10:10:27 +03:00
Fr1ngg 91b0787d72 Update README.md 2025-08-10 07:04:17 +03:00
Fr1ngg 315277d4b0 Create bedolaga.sh 2025-08-10 06:56:04 +03:00
Fr1ngg 1821c0d89d Delete run.sh 2025-08-10 06:54:40 +03:00
35 changed files with 6683 additions and 828 deletions
+26 -1
View File
@@ -10,7 +10,6 @@ REFERRAL_PERCENTAGE=0.2
# RemnaWave API Configuration
REMNAWAVE_URL=https://example.com
REMNAWAVE_MODE=remote
REMNAWAVE_TOKEN=
# Admin Configuration (comma-separated list of Telegram user IDs)
@@ -24,6 +23,9 @@ TRIAL_DURATION_DAYS=3 # Дней триала
TRIAL_TRAFFIC_GB=2 # Лимит трафика у триал подписки
TRIAL_SQUAD_UUID= # UUID сквада из панели remnawave /dashboard/management/internal-squads
TRIAL_PRICE=0.0 # Оставить 0!
TRIAL_NOTIFICATION_ENABLED=true # Уведомление об истекшей триальной подписке
TRIAL_NOTIFICATION_HOURS_AFTER=1 # Через сколько отсылать сообщение
TRIAL_NOTIFICATION_HOURS_WINDOW=23 # Через сколько выслать повторно
# Monitor Service Settings (дополнительные настройки)
MONITOR_CHECK_INTERVAL=21600 # Промежуток проверики (3600 - будет раз в час проверять и слать уведомления)
@@ -33,7 +35,30 @@ DELETE_EXPIRED_TRIAL_DAYS=1 # Через сколько дней после и
DELETE_EXPIRED_REGULAR_DAYS=7 # Через сколько дней после истечения удалять обычные подписки
AUTO_DELETE_ENABLED=true # Включить автоматическое удаление при ежедневной проверке
LUCKY_GAME_ENABLED=true # Включить/выключить игру удачи
LUCKY_GAME_REWARD=50.0 # Размер награды за выигрыш в рублях
LUCKY_GAME_NUMBERS=30 # Общее количество чисел для выбора (1-30)
LUCKY_GAME_WINNING_COUNT=25 # Количество выигрышных чисел из общего количества
# ================================
# TELEGRAM STARS НАСТРОЙКИ
# ================================
# Включить/выключить пополнение через Telegram Stars
STARS_ENABLED=true
# Курсы обмена звезд на рубли
# Формат: STARS_{количество_звезд}_RATE={рубли}
# 100 звёзд
STARS_100_RATE=110
# 150 звёзд
STARS_150_RATE=165
# 250 звёзд
STARS_250_RATE=280
# 350 звёзд
STARS_350_RATE=355
# 500 звёзд
STARS_500_RATE=550
# 750 звёзд
STARS_750_RATE=800
# 1000 звёзд
STARS_1000_RATE=1000
+415 -414
View File
@@ -1,127 +1,246 @@
# 🤖 RemnaWave Telegram Bot
# 🤖 Remnawave Bedolaga 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)
<div align="center">
[![Docker Image](https://img.shields.io/badge/Docker-fr1ngg/remnawave--bedolaga--telegram--bot-blue?logo=docker&logoColor=white)](https://hub.docker.com/r/fr1ngg/remnawave-bedolaga-telegram-bot)
[![Python](https://img.shields.io/badge/Python-3.11+-blue?logo=python&logoColor=white)](https://python.org)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-blue?logo=postgresql&logoColor=white)](https://postgresql.org)
[![License](https://img.shields.io/badge/License-MIT-green)](LICENSE)
[![Stars](https://img.shields.io/github/stars/Fr1ngg/remnawave-bedolaga-telegram-bot?style=social)](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/stargazers)
![RemnaWave Bedolaga Bot Logo](./assets/logo2.svg)
![Logo](./assets/logo2.svg)
Современный Telegram бот для управления VPN подписками через RemnaWave API с полным функционалом управления пользователями, платежами и администрированием.
**Современный Telegram-бот для управления VPN подписками через Remnawave API**
## 🚀 Особенности
*Полнофункциональное решение с управлением пользователями, платежами и администрированием*
### 👤 Для пользователей
- 💰 **Управление балансом** - пополнение через поддержку
- 🛒 **Покупка подписок** - различные тарифные планы c возможностью индивидуальной настройки сквада у плана
- 📱 **Управление подписками** - просмотр, продление, получение ссылок
- 🎁 **Система промокодов** - денежные коды
[🚀 Быстрый старт](#-быстрый-старт) • [📖 Документация](#-документация) • [💬 Поддержка](#-поддержка) • [🤝 Вклад](#-вклад-в-проект)
</div>
---
## ✨ Основные возможности
<table>
<tr>
<td width="50%">
### 👤 **Для пользователей**
- 💰 **Управление балансом** - Telegram Stars + P2P через поддержку
- 🛒 **Покупка подписок** - различные тарифы с настройкой squad
- 📱 **Управление подписками** - просмотр, продление, ссылки
- 🎁 **Промокоды** - денежные бонусы
- 👥 **Реферальная программа** - зарабатывай с друзей
- 🎰 **Игра удачи** - выигрывай бонусы каждые 24 часа
- 🆓 **Тестовая подписка** - бесплатная пробная версия с детальной конфигурацией
- 🎰 **Игра удачи** - ежедневные бонусы
- 🆓 **Тестовая подписка** - бесплатная пробная версия
- ♾️ **Автопродление** - настраиваемое автообновление
- 🌐 **Мультиязычность** - русский и английский
### ⚙️ Для администраторов
- 📊 **Полная статистика** - пользователи, платежи, подписки
</td>
<td width="50%">
### ⚙️ **Для администраторов**
- 📊 **Детальная статистика** - пользователи, платежи, подписки
- 👥 **Управление пользователями** - поиск, редактирование, баланс
- 💳 **Управление платежами** - одобрение, отклонение, история
- 🎫 **Управление промокодами** - создание, редактирование, статистика
- 🖥 **Мониторинг системы** - состояние нод, пользователей RemnaWave
- 🔄 **Синхронизация данных** - импорт из RemnaWave
- 💳 **Управление платежами** - одобрение, история, Telegram Stars
- 🎫 **Управление промокодами** - создание, статистика, массовые операции
- 🖥 **Мониторинг системы** - состояние нод, синхронизация с Remnawave
- 📨 **Рассылки** - уведомления пользователям
- 🔍 **Мониторинг подписок** - автоматические уведомления об истечении и зачистка подписок с истекщим сроком для триал и обычных подписок
- 🔍 **Мониторинг подписок** - автоуведомления об истечении
- 📋 **Правила сервиса** - настройка через админ-панель
- ♾️ **Статистика автопродления** - полный контроль
## 📋 Требования
</td>
</tr>
</table>
### Системные требования
- Docker и Docker Compose
- 1+ GB RAM
- 10+ GB свободного места
---
### Внешние сервисы
- **Telegram Bot Token** - создайте бота через [@BotFather](https://t.me/BotFather)
- **RemnaWave API** - доступ к панели RemnaWave
- **PostgreSQL** - автоматически разворачивается через Docker
## 🚀 Быстрый старт
## 🛠 Установка и запуск
### ⚡ Автоматическая установка (Ubuntu)
### 1. Клонирование репозитория
Установите бота **одной командой** с интерактивным мастером настройки:
```bash
git clone https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot.git
cd remnawave-bedolaga-telegram-bot
curl -sSL https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/raw/main/bedolaga.sh | sudo bash
```
### 2. Настройка переменных окружения
<details>
<summary>🔧 Что делает установщик</summary>
Создайте файл `.env` на основе примера:
- ✅ Обновляет систему Ubuntu
- ✅ Устанавливает Docker и зависимости
- ✅ Создает структуру проекта в `/opt/bedolaga-bot`
- ✅ Интерактивная настройка всех параметров
- ✅ Создает systemd службу (опционально)
- ✅ Запускает удобное меню управления
```bash
cp .env.example .env
nano .env
```
</details>
### 3. Конфигурация
### 🎛 Интерактивное меню управления
#### 📋 Основные настройки
После установки вы получаете полнофункциональное меню:
<img width="500" alt="Интерфейс управления ботом" src="https://github.com/user-attachments/assets/9ab876f5-2758-4c52-93dd-6c9c654a07aa">
**Доступные действия:**
- 🚀 Запуск/остановка/перезапуск бота
- 📺 Просмотр логов в реальном времени
- 💾 Создание и восстановление резервных копий БД
- ✏️ Редактирование конфигурации
- 🩺 Диагностика и устранение проблем
- 🔄 Автообновление бота
---
## 📖 Документация
### 🎯 Варианты установки
<table>
<tr>
<th>Сценарий</th>
<th>Описание</th>
<th>Команда</th>
</tr>
<tr>
<td>🏠 <strong>Локальная установка</strong></td>
<td>Панель + бот на одном сервере</td>
<td><code>./bedolaga.sh</code> → вариант 2</td>
</tr>
<tr>
<td>🌐 <strong>Удаленная установка</strong></td>
<td>Бот отдельно от панели</td>
<td><code>./bedolaga.sh</code> → вариант 1</td>
</tr>
<tr>
<td>⚡ <strong>Расширенная</strong></td>
<td>С Redis и Nginx</td>
<td><code>./bedolaga.sh</code> → вариант 3</td>
</tr>
</table>
### ⚙️ Ключевые настройки
<details>
<summary>📋 Основные параметры</summary>
| Переменная | Описание | Пример |
|------------|----------|---------|
| `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` |
| `BOT_TOKEN` | Токен от [@BotFather](https://t.me/BotFather) | `123456:ABC-DEF...` |
| `BOT_USERNAME` | Username бота (без @) | `your_bot` |
| `REMNAWAVE_URL` | URL панели Remnawave | `https://panel.com` |
| `REMNAWAVE_TOKEN` | API токен панели | `your_api_token` |
| `ADMIN_IDS` | ID администраторов | `123456789,987654321` |
#### 🎁 Реферальная программа
</details>
<details>
<summary>🎁 Реферальная программа</summary>
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `REFERRAL_FIRST_REWARD` | Награда за первого реферала | `100.0` |
| `REFERRAL_REFERRED_BONUS` | Бонус приглашенному | `100.0` |
| `REFERRAL_THRESHOLD` | Порог активации (руб.) | `200.0` |
| `REFERRAL_PERCENTAGE` | Процент со второго и послед. платежей | `0.2` (20%) |
| `REFERRAL_PERCENTAGE` | % с последующих платежей | `0.2` (20%) |
#### 🎰 Игра удачи
</details>
<details>
<summary>⭐ Telegram Stars</summary>
| Переменная | Описание | Пример |
|------------|----------|---------|
| `STARS_ENABLED` | Включить оплату звездами | `true` |
| `STARS_100_RATE` | Курс 100 звезд → рублей | `110` |
| `STARS_250_RATE` | Курс 250 звезд → рублей | `280` |
| `STARS_500_RATE` | Курс 500 звезд → рублей | `550` |
</details>
<details>
<summary>🔍 Мониторинг подписок</summary>
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `LUCKY_GAME_ENABLED` | Включить игру | `true` |
| `LUCKY_GAME_REWARD` | Награда за выигрыш | `50.0` |
| `LUCKY_GAME_NUMBERS` | Всего чисел | `30` |
| `LUCKY_GAME_WINNING_COUNT` | Выигрышных чисел | `3` |
| `MONITOR_ENABLED` | Включить мониторинг | `true` |
| `MONITOR_CHECK_INTERVAL` | Интервал проверки (сек) | `1800` (30 мин) |
| `MONITOR_WARNING_DAYS` | За сколько дней предупреждать | `3` |
| `AUTO_DELETE_ENABLED` | Автоудаление истекших | `true` |
#### 🆓 Тестовая подписка
</details>
<details>
<summary> Триал</summary>
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `TRIAL_ENABLED` | Включить тестовую подписку | `true/false` |
| `TRIAL_DURATION_DAYS` | Длительность (дни) | `3` |
| `TRIAL_TRAFFIC_GB` | Лимит трафика (ГБ) | `2` |
| `TRIAL_SQUAD_UUID` | UUID squad для тестовых | `uuid-here` |
| `TRIAL_ENABLED` | Включить триал | `true` |
| `TRIAL_DURATION_DAYS` | Дней триала | `3` |
| `TRIAL_SQUAD_UUID` | Сквад триальной подписки | `` |
| `TRIAL_NOTIFICATION_ENABLED` | Уведомление об истечении триал подписки | `true` |
| `TRIAL_NOTIFICATION_HOURS_AFTER` | Через сколько отсылать сообщение | `1` |
| `TRIAL_NOTIFICATION_HOURS_WINDOW` | Через сколько выслать повторно | `` |
#### 💬 Сервис мониторинга сообщений
</details>
### 🐳 Docker Compose примеры
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `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` для базового запуска:
<details>
<summary>🏠 Для локальной установки (панель + бот)</summary>
```yaml
services:
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
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- remnawave-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U remnawave_user -d 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:
- remnawave-network
volumes:
postgres_data:
networks:
remnawave-network:
name: remnawave-network
external: true
```
</details>
<details>
<summary>🌐 Для удаленной установки</summary>
```yaml
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: remnawave_bot_db
@@ -130,19 +249,13 @@ services:
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
@@ -159,207 +272,97 @@ services:
- ./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
```
#### ⚡ Полная конфигурация (с дополнительными сервисами)
</details>
Для расширенной настройки с 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
1. **Запуск** → Найдите бота и нажмите `/start`
2. **Язык** → Выберите русский или английский
3. **Баланс** → "💰 Баланс" → "💳 Пополнить" → Telegram Stars
4. **Подписка** → "🛒 Купить подписку" → выбор тарифа → оплата
5. **Управление** → "📋 Мои подписки" → выбор → получение ссылки
6. **Рефералы** → "👥 Рефералы" → поделиться ссылкой
# 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
```
- **📦 Управление подписками** → создание и настройка тарифов
- **👥 Управление пользователями** → поиск, редактирование балансов
- **💰 Управление платежами** → одобрение Telegram Stars платежей
- **🎁 Промокоды** → создание денежных бонусов
- **📨 Рассылки** → уведомления пользователям
- **🖥 Система Remnawave** → мониторинг нод, синхронизация
- **📊 Статистика** → подробная аналитика
### 5. Варианты запуска
---
#### 🚀 Быстрый старт (минимальная конфигурация)
## 📊 Производительность
### 💪 Рекомендуемые ресурсы
| Пользователей | RAM | CPU | Диск | Описание |
|---------------|-----|-----|------|----------|
| **До 500** | 1GB | 1 vCPU | 10GB | Начальная конфигурация |
| **До 1,000** | 2GB | 1 vCPU | 20GB | Малый бизнес |
| **До 10,000** | 4GB | 2 vCPU | 50GB | Средний бизнес |
| **До 50,000** | 8GB | 4 vCPU | 100GB | Крупный бизнес |
### ⚡ Оптимизация
- **Redis** → включите для кэширования (`--profile with-redis`)
- **PostgreSQL** → настройте для production нагрузок
- **Nginx** → используйте как reverse proxy (`--profile with-nginx`)
- **Мониторинг** → отслеживайте через `docker stats`
---
## 🔧 Управление
### 📋 Основные команды
```bash
# Запуск только бота и базы данных
docker-compose up -d
# Переход в директорию
cd /opt/bedolaga-bot
# Просмотр логов
docker-compose logs -f bot
# Управление через Docker Compose
docker compose up -d # Запуск
docker compose down # Остановка
docker compose restart bot # Перезапуск бота
docker compose logs -f bot # Логи в реальном времени
# Управление через systemd (если настроено)
sudo systemctl start bedolaga-bot
sudo systemctl stop bedolaga-bot
sudo systemctl restart bedolaga-bot
```
#### ⚡ Расширенный запуск
### 🔄 Обновление
```bash
# Запуск с Redis для улучшения производительности
docker-compose --profile with-redis up -d
# Автоматическое через меню
sudo ./bedolaga.sh
# Запуск с 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
# Ручное обновление
docker compose down
docker compose pull bot
docker compose 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` | Все сервисы |
## 📊 Мониторинг и управление
### Просмотр логов
```bash
# Логи бота
docker-compose logs -f bot
# Логи базы данных
docker-compose logs -f postgres
# Все логи
docker-compose logs -f
```
### Проверка состояния
```bash
# Статус контейнеров
docker-compose ps
# Использование ресурсов
docker stats
```
### Backup базы данных
### 💾 Резервное копирование
```bash
# Создание backup
@@ -369,199 +372,197 @@ docker exec remnawave_bot_db pg_dump -U remnawave_user remnawave_bot > backup.sq
docker exec -i remnawave_bot_db psql -U remnawave_user remnawave_bot < backup.sql
```
## 🔧 Обновление
### Обновление Docker образа
```bash
# Остановить сервисы
docker-compose down
# Обновить образ
docker-compose pull bot
# Запустить снова
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
# Собрать образ
docker build -t remnawave-bot .
# Изменить docker-compose.yml
# bot:
# image: remnawave-bot # вместо fr1ngg/remnawave-bedolaga-telegram-bot
```
## 🎯 Использование
### Для пользователей
1. **Запуск бота** - найдите бота по username и нажмите `/start`
2. **Выбор языка** - выберите русский или английский
3. **Пополнение баланса** - через меню "💰 Баланс" → "💳 Пополнить"
4. **Покупка подписки** - "🛒 Купить подписку" → выбор тарифа → оплата
5. **Управление подписками** - "📋 Мои подписки" → выбор подписки
6. **Реферальная программа** - "👥 Рефералы" → поделиться ссылкой
### Для администраторов
Администратор видит дополнительную кнопку "⚙️ Админ панель" с возможностями:
- **📦 Управление подписками** - создание тарифов
- **👥 Управление пользователями** - поиск, редактирование
- **💰 Управление балансом** - пополнение балансов
- **🎁 Управление промокодами** - создание скидок
- **📨 Рассылки** - отправка сообщений
- **🖥 Система RemnaWave** - мониторинг нод и синхронизация
## 🔒 Безопасность
### Рекомендации
- ✅ Измените пароли PostgreSQL в `docker-compose.yml`
- ✅ Не публикуйте файл `.env` в репозитории
- ✅ Используйте HTTPS для RemnaWave API
- ✅ Регулярно создавайте backup базы данных
- ✅ Ограничьте доступ к серверу через firewall
### Переменные безопасности
```bash
# Генерация безопасных паролей
openssl rand -base64 32 # для PostgreSQL
openssl rand -hex 16 # для Redis
```
---
## 🐛 Устранение неполадок
### Частые проблемы
### Частые проблемы
| Проблема | Решение |
|----------|---------|
| Бот не отвечает | Проверьте `BOT_TOKEN` и интернет |
| Ошибка подключения к БД | Проверьте статус PostgreSQL контейнера |
| Ошибки RemnaWave API | Проверьте `REMNAWAVE_URL` и `REMNAWAVE_TOKEN` |
| Игра удачи не работает | Проверьте `LUCKY_GAME_ENABLED=true` |
<details>
<summary>🤖 Бот не отвечает</summary>
### Диагностика
**Проверьте:**
- ✅ Правильность `BOT_TOKEN`
- ✅ Интернет соединение
- ✅ Логи: `docker compose logs bot`
**Решение:**
```bash
# Проверка переменных окружения
docker exec remnawave_bot env | grep BOT_
# Перезапуск бота
docker compose restart bot
# Подключение к базе данных
# Проверка токена
docker exec remnawave_bot env | grep BOT_TOKEN
```
</details>
<details>
<summary>🗄️ Ошибки базы данных</summary>
**Симптомы:**
- SQL ошибки в логах
- Бот не сохраняет данные
**Решение:**
```bash
# Проверка PostgreSQL
docker compose logs postgres
# Подключение к БД
docker exec -it remnawave_bot_db psql -U remnawave_user remnawave_bot
# Перезапуск бота
docker-compose restart bot
# Экстренное восстановление через меню
sudo ./bedolaga.sh → "10) Экстренное исправление БД"
```
### Логи и отладка
</details>
<details>
<summary>🔌 Проблемы с Remnawave API</summary>
**Проверьте:**
- ✅ Доступность `REMNAWAVE_URL`
- ✅ Валидность `REMNAWAVE_TOKEN`
- ✅ Сетевое подключение
**Диагностика:**
```bash
# Детальные логи
docker-compose logs -f --tail 100 bot
# Проверка URL
curl -I https://your-panel.com
# Ошибки базы данных
docker-compose logs postgres | grep ERROR
# Проверка health checks
docker-compose ps
# Тест API из контейнера
docker exec remnawave_bot curl -I http://remnawave:3000
```
## 📈 Производительность
</details>
### Рекомендуемые ресурсы
<details>
<summary>⚠️ Проблема обновления 1.3.3 → 1.3.4</summary>
| Пользователей | 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 |
**Симптом:** SQL ошибки после обновления связанные с автоплатежами
### Оптимизация
**Решение через меню:**
```bash
sudo ./bedolaga.sh
# Выберите: "10) Экстренное исправление БД (Python)"
# Или: "11) Экстренное исправление БД (SQL)"
```
- Включите Redis для кэширования (профиль `with-redis`)
- Настройте PostgreSQL для production
- Используйте nginx как reverse proxy
- Мониторьте ресурсы через `docker stats`
**Ручное решение:**
1. Скачайте `emergency_fix.py`
2. Добавьте в `docker-compose.yml`:
```yaml
emergency-fix:
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
volumes:
- ./emergency_fix.py:/app/emergency_fix.py
environment:
- DATABASE_URL=postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
networks:
- bot_network
profiles:
- emergency
command: python emergency_fix.py
```
3. Запустите: `docker compose run --rm emergency-fix`
## 🤝 Поддержка
</details>
### Получить помощь
---
- 💬 **Telegram**: @fringg
## 🗺️ Roadmap
### Вклад в проект
### ✅ Реализовано
1. Fork репозитория
2. Создайте feature branch
3. Внесите изменения
4. Создайте Pull Request
- ✅ **Мониторинг подписок** - автоуведомления и контроль
- ✅ **Telegram Stars** - пополнение баланса звездами
- ✅ **Синхронизация Remnawave** - импорт пользователей и статистика
- ✅ **Реферальная система** - полнофункциональная программа
- ✅ **Игра удачи** - ежедневные розыгрыши бонусов
- ✅ **Управление промокодами** - создание, редактирование, статистика
- ✅ **Правила сервиса** - настройка через админ-панель
- ✅ **Автоплатежи** - настраиваемое автопродление подписок
- ✅ **Просмотр подписок пользователей** - детальная статистика
### 🎯 В планах
| Версия | Функция | Приоритет | Описание |
|--------|---------|-----------|----------|
| **v1.4.0** | ЮKassa интеграция | 🔴 High | Автоматические платежи |
| **v1.4.0** | Веб-панель управления | 🟡 Medium | Полный веб-интерфейс |
| **v1.4.0** | Безопасное удаление | 🟡 Medium | Архивирование вместо удаления |
| **v1.5.0** | Дополнительные платежи | 🟡 Medium | Сбербанк, Tinkoff, Crypto |
| **v1.5.0** | Уведомления | 🟡 Medium | Webhook, Email, другие чаты |
| **v1.6.0** | Система блокировок | 🟢 Low | Бан/разбан пользователей |
### 💡 Хотите добавить функцию?
- 🐛 [Сообщите о баге](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/issues)
- ✨ [Предложите улучшение](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/discussions)
- 🔧 [Создайте Pull Request](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/pulls)
---
## 🤝 Вклад в проект
### 💪 Как помочь
**🔧 Разработчикам:**
- Fork репозитория
- Создайте feature branch: `git checkout -b feature/amazing-feature`
- Внесите изменения и сделайте commit: `git commit -m 'Add amazing feature'`
- Push в branch: `git push origin feature/amazing-feature`
- Создайте Pull Request
**🐞 Пользователям:**
- Сообщайте о багах в [Issues](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/issues)
- Предлагайте идеи в [Discussions](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/discussions)
- Ставьте ⭐ проекту
- Рассказывайте друзьям
**💰 Спонсорам:**
- Поддержите разработку
- Закажите приоритетные функции
- Получите корпоративную поддержку
---
## 💬 Поддержка
### 📞 Контакты
- **Telegram:** [@fringg](https://t.me/fringg)
- **Issues:** [GitHub Issues](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/issues)
- **Discussions:** [GitHub Discussions](https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/discussions)
### 📚 Полезные ссылки
- [📖 Документация Remnawave](https://docs.remna.st)
- [🤖 Создание Telegram бота](https://t.me/BotFather)
- [🐳 Docker документация](https://docs.docker.com)
- [🐘 PostgreSQL документация](https://www.postgresql.org/docs)
---
## 📄 Лицензия
Этот проект распространяется под лицензией MIT. См. файл [LICENSE](LICENSE) для подробностей.
## 🗺 Roadmap & TODO
### 📋 Общий план развития
| № | Задача | Статус | Версия | Приоритет | Описание |
|---|---------|--------|--------|-----------|----------|
| 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 | Дабы не плодить лишние записи в базе бота и панели ремны, да и в целом это логично)( |
### 🚀 Хотите помочь?
**Разработчикам:**
- 🐛 Найдите и исправьте баги
- ✨ Предложите новые функции
- 📝 Улучшите документацию
- 🧪 Напишите тесты
**Пользователям:**
- 💭 Поделитесь идеями в Issues
- 🐞 Сообщите о найденных проблемах
- ⭐ Поставьте звезду проекту
- 📢 Расскажите друзьям
**Спонсорам:**
- 💰 Поддержите разработку
- 🎯 Закажите приоритетные функции
- 🏢 Корпоративная поддержка
Проект распространяется под лицензией **MIT**. Подробности в файле [LICENSE](LICENSE).
---
**⚠️ Важные замечания от автора:**
<div align="center">
> 🗑 **Осторожно с удалением подписок!** Сейчас удаление плана подписки скроет эту подписку у всех пользователей, которые ее купили. Используйте деактивацию вместо удаления. Эта проблема будет исправлена в v1.4.0.
### 🌟 Понравился проект? Поставьте звезду!
[![Star History Chart](https://api.star-history.com/svg?repos=Fr1ngg/remnawave-bedolaga-telegram-bot&type=Date)](https://star-history.com/#Fr1ngg/remnawave-bedolaga-telegram-bot&Date)
---
**⭐ Если проект был полезен, поставьте звездочку на GitHub!**
**💝 Создано с любовью для Remnawave сообщества**
---
*Автор не является профессиональным разработчиком, но прикладывает все усилия для создания удобного бота для ваших сервисов* 💪
*Создано с ❤️ для RemnaWave сообщества*
[🔝 Вернуться наверх](#-remnawave-bedolaga-bot)
</div>
+1717 -8
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -261,7 +261,7 @@
text-anchor="middle"
fill="url(#mainGradient)"
filter="url(#glow)">
RemnaWave
Remnawave
</text>
<text x="200" y="345"

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+284
View File
@@ -0,0 +1,284 @@
import asyncio
import logging
from datetime import datetime, timedelta
from typing import List, Optional
from database import Database, UserSubscription, User, Subscription
from remnawave_api import RemnaWaveAPI
from referral_utils import process_referral_rewards
logger = logging.getLogger(__name__)
class AutoPayService:
def __init__(self, db: Database, api: Optional[RemnaWaveAPI] = None, bot=None):
self.db = db
self.api = api
self.bot = bot
self.is_running = False
self.check_task = None
async def start(self):
if self.is_running:
logger.warning("AutoPay service is already running")
return
self.is_running = True
logger.info("🔄 Starting AutoPay service...")
self.check_task = asyncio.create_task(self._periodic_check())
async def stop(self):
if not self.is_running:
return
self.is_running = False
logger.info("⏹ Stopping AutoPay service...")
if self.check_task:
self.check_task.cancel()
try:
await self.check_task
except asyncio.CancelledError:
pass
async def _periodic_check(self):
while self.is_running:
try:
await self.process_autopayments()
# Проверяем каждые 30 минут
await asyncio.sleep(1800)
except asyncio.CancelledError:
logger.info("AutoPay periodic check cancelled")
break
except Exception as e:
logger.error(f"Error in AutoPay periodic check: {e}")
await asyncio.sleep(300)
async def process_autopayments(self) -> dict:
logger.info("🔄 Processing autopayments...")
try:
subscriptions_to_pay = await self.db.get_subscriptions_for_autopay()
if not subscriptions_to_pay:
logger.info("No subscriptions ready for autopay")
return {
'processed': 0,
'successful': 0,
'failed': 0,
'insufficient_balance': 0,
'errors': []
}
logger.info(f"Found {len(subscriptions_to_pay)} subscriptions for autopay")
stats = {
'processed': 0,
'successful': 0,
'failed': 0,
'insufficient_balance': 0,
'errors': []
}
for user_sub in subscriptions_to_pay:
try:
result = await self._process_single_autopayment(user_sub)
stats['processed'] += 1
if result['success']:
stats['successful'] += 1
logger.info(f"✅ Autopay successful for user {user_sub.user_id}, subscription {user_sub.id}")
elif result['reason'] == 'insufficient_balance':
stats['insufficient_balance'] += 1
logger.info(f"💳 Insufficient balance for user {user_sub.user_id}, subscription {user_sub.id}")
else:
stats['failed'] += 1
stats['errors'].append(f"User {user_sub.user_id}: {result['reason']}")
logger.warning(f"❌ Autopay failed for user {user_sub.user_id}: {result['reason']}")
except Exception as e:
stats['processed'] += 1
stats['failed'] += 1
error_msg = f"User {user_sub.user_id}: {str(e)}"
stats['errors'].append(error_msg)
logger.error(f"Error processing autopay for user {user_sub.user_id}: {e}")
logger.info(f"📊 Autopay processing complete: {stats['successful']} successful, "
f"{stats['failed']} failed, {stats['insufficient_balance']} insufficient balance")
return stats
except Exception as e:
logger.error(f"Error in process_autopayments: {e}")
return {
'processed': 0,
'successful': 0,
'failed': 0,
'insufficient_balance': 0,
'errors': [str(e)]
}
async def _process_single_autopayment(self, user_sub: UserSubscription) -> dict:
try:
user = await self.db.get_user_by_telegram_id(user_sub.user_id)
if not user:
return {'success': False, 'reason': 'User not found'}
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if not subscription:
return {'success': False, 'reason': 'Subscription plan not found'}
if subscription.is_trial:
logger.info(f"Skipping autopay for trial subscription: user {user_sub.user_id}")
return {'success': False, 'reason': 'Trial subscriptions are not eligible for autopay'}
if not user_sub.is_active or not user_sub.auto_pay_enabled:
return {'success': False, 'reason': 'Subscription inactive or autopay disabled'}
if user.balance < subscription.price:
await self._notify_insufficient_balance(user, subscription, user_sub)
return {'success': False, 'reason': 'insufficient_balance'}
return await self._execute_autopayment(user, subscription, user_sub)
except Exception as e:
logger.error(f"Error in _process_single_autopayment: {e}")
return {'success': False, 'reason': str(e)}
async def _execute_autopayment(self, user: User, subscription: Subscription, user_sub: UserSubscription) -> dict:
try:
user.balance -= subscription.price
await self.db.update_user(user)
now = datetime.utcnow()
if user_sub.expires_at > now:
new_expiry = user_sub.expires_at + timedelta(days=subscription.duration_days)
else:
new_expiry = now + timedelta(days=subscription.duration_days)
user_sub.expires_at = new_expiry
user_sub.is_active = True
await self.db.update_user_subscription(user_sub)
if self.api and user_sub.short_uuid:
try:
remna_user_details = await self.api.get_user_by_short_uuid(user_sub.short_uuid)
if remna_user_details:
user_uuid = remna_user_details.get('uuid')
if user_uuid:
expiry_str = new_expiry.isoformat() + 'Z'
update_data = {
'enable': True,
'expireAt': expiry_str
}
await self.api.update_user(user_uuid, update_data)
logger.info(f"Updated RemnaWave expiry for user {user_sub.user_id}")
except Exception as e:
logger.warning(f"Failed to update RemnaWave expiry: {e}")
payment = await self.db.create_payment(
user_id=user_sub.user_id,
amount=-subscription.price,
payment_type='autopay',
description=f'Автоплатеж: {subscription.name}',
status='completed'
)
if self.bot:
try:
await process_referral_rewards(
user_sub.user_id,
subscription.price,
payment.id,
self.db,
self.bot,
payment_type='autopay'
)
except Exception as e:
logger.warning(f"Failed to process referral rewards for autopay: {e}")
await self._notify_successful_autopay(user, subscription, user_sub, new_expiry)
return {'success': True, 'reason': 'Payment processed successfully'}
except Exception as e:
logger.error(f"Error executing autopayment: {e}")
try:
user.balance += subscription.price
await self.db.update_user(user)
except:
pass
return {'success': False, 'reason': str(e)}
async def _notify_successful_autopay(self, user: User, subscription: Subscription,
user_sub: UserSubscription, new_expiry: datetime):
if not self.bot:
return
try:
from utils import format_datetime
text = f"✅ Автоматическое продление подписки\n\n"
text += f"📋 Подписка: {subscription.name}\n"
text += f"💰 Списано: {subscription.price} руб.\n"
text += f"📅 Продлено до: {format_datetime(new_expiry, user.language)}\n"
text += f"💳 Остаток на балансе: {user.balance} руб.\n\n"
text += f"🔄 Следующее продление произойдет автоматически за {user_sub.auto_pay_days_before} дн. до истечения.\n\n"
text += f"ℹ️ Для отключения автоплатежа перейдите в 'Мои подписки' → выберите подписку"
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")],
[InlineKeyboardButton(text="💰 Баланс", callback_data="balance")]
])
await self.bot.send_message(
user.telegram_id,
text,
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Failed to send autopay success notification: {e}")
async def _notify_insufficient_balance(self, user: User, subscription: Subscription, user_sub: UserSubscription):
if not self.bot:
return
try:
from utils import format_datetime
needed = subscription.price - user.balance
days_left = (user_sub.expires_at - datetime.utcnow()).days
text = f"⚠️ Не удалось автоматически продлить подписку\n\n"
text += f"📋 Подписка: {subscription.name}\n"
text += f"💰 Нужно для продления: {subscription.price} руб.\n"
text += f"💳 Ваш баланс: {user.balance} руб.\n"
text += f"💸 Недостает: {needed} руб.\n\n"
text += f"📅 Подписка истекает: {format_datetime(user_sub.expires_at, user.language)}\n"
text += f"⏰ Осталось дней: {days_left}\n\n"
text += f"💡 Пополните баланс для автоматического продления"
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="topup_balance")],
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")]
])
await self.bot.send_message(
user.telegram_id,
text,
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Failed to send insufficient balance notification: {e}")
async def get_service_status(self) -> dict:
return {
'is_running': self.is_running,
'check_interval': 1800, # 30 минут
'has_api': self.api is not None,
'has_bot': self.bot is not None
}
+1434
View File
File diff suppressed because it is too large Load Diff
+51 -46
View File
@@ -1,53 +1,44 @@
import os
from dataclasses import dataclass
from typing import List
from typing import List, Dict
@dataclass
class Config:
# Bot configuration
BOT_TOKEN: str
BOT_USERNAME: str
# Database
DATABASE_URL: str
# RemnaWave API
REMNAWAVE_URL: str
REMNAWAVE_TOKEN: str
SUBSCRIPTION_BASE_URL: str
# Admin configuration
ADMIN_IDS: List[int]
SUPPORT_USERNAME: str
DEFAULT_LANGUAGE: str
# Trial subscription
TRIAL_ENABLED: bool
TRIAL_DURATION_DAYS: int
TRIAL_TRAFFIC_GB: int
TRIAL_SQUAD_UUID: str
# Referral system
REFERRAL_FIRST_REWARD: float
REFERRAL_REFERRED_BONUS: float
REFERRAL_THRESHOLD: float
REFERRAL_PERCENTAGE: float
# Конфигурация мониторинга подписок
MONITOR_ENABLED: bool = True
MONITOR_CHECK_INTERVAL: int = 3600 # 1 час
MONITOR_DAILY_CHECK_HOUR: int = 10 # 10:00 утра
MONITOR_WARNING_DAYS: int = 3 # предупреждать за 3 дня
# Новые настройки для удаления истекших подписок
DELETE_EXPIRED_TRIAL_DAYS: int = 1 # Удалять триальные через 1 день после истечения
DELETE_EXPIRED_REGULAR_DAYS: int = 7 # Удалять обычные через 7 дней после истечения
AUTO_DELETE_ENABLED: bool = False # Автоматическое удаление при ежедневной проверке
MONITOR_CHECK_INTERVAL: int = 3600
MONITOR_DAILY_CHECK_HOUR: int = 10
MONITOR_WARNING_DAYS: int = 3
DELETE_EXPIRED_TRIAL_DAYS: int = 1
DELETE_EXPIRED_REGULAR_DAYS: int = 7
AUTO_DELETE_ENABLED: bool = False
TRIAL_NOTIFICATION_ENABLED: bool = True
TRIAL_NOTIFICATION_HOURS_AFTER: int = 2
TRIAL_NOTIFICATION_HOURS_WINDOW: int = 22
LUCKY_GAME_ENABLED: bool = True
LUCKY_GAME_REWARD: float = 50.0 # Размер награды за выигрыш
LUCKY_GAME_NUMBERS: int = 30 # Общее количество чисел (1-30)
LUCKY_GAME_WINNING_COUNT: int = 3 # Количество выигрышных чисел
LUCKY_GAME_REWARD: float = 50.0
LUCKY_GAME_NUMBERS: int = 30
LUCKY_GAME_WINNING_COUNT: int = 3
STARS_ENABLED: bool = True
STARS_RATES: Dict[int, float] = None
def load_config() -> Config:
@@ -75,51 +66,64 @@ def load_config() -> Config:
except (ValueError, TypeError):
return default
def parse_stars_rates() -> Dict[int, float]:
default_rates = {
100: 150.0,
150: 220.0,
250: 400.0,
350: 500.0,
500: 800.0,
750: 1150.0,
1000: 1500.0
}
custom_rates = {}
for stars in [100, 150, 250, 350, 500, 750, 1000]:
env_key = f"STARS_{stars}_RATE"
rate = get_float(env_key, 0.0)
if rate > 0:
custom_rates[stars] = rate
if custom_rates:
return custom_rates
return default_rates
return Config(
# Bot configuration
BOT_TOKEN=os.getenv('BOT_TOKEN', ''),
BOT_USERNAME=os.getenv('BOT_USERNAME', ''),
# Database
DATABASE_URL=os.getenv('DATABASE_URL', 'sqlite+aiosqlite:///bot.db'),
# RemnaWave API
REMNAWAVE_URL=os.getenv('REMNAWAVE_URL', ''),
REMNAWAVE_TOKEN=os.getenv('REMNAWAVE_TOKEN', ''),
SUBSCRIPTION_BASE_URL=os.getenv('SUBSCRIPTION_BASE_URL', ''),
# Admin configuration
ADMIN_IDS=parse_admin_ids(os.getenv('ADMIN_IDS', '')),
SUPPORT_USERNAME=os.getenv('SUPPORT_USERNAME', 'support'),
DEFAULT_LANGUAGE=os.getenv('DEFAULT_LANGUAGE', 'ru'),
# Trial subscription
TRIAL_ENABLED=get_bool('TRIAL_ENABLED', False),
TRIAL_DURATION_DAYS=get_int('TRIAL_DURATION_DAYS', 3),
TRIAL_TRAFFIC_GB=get_int('TRIAL_TRAFFIC_GB', 2),
TRIAL_SQUAD_UUID=os.getenv('TRIAL_SQUAD_UUID', ''),
# Referral system
REFERRAL_FIRST_REWARD=get_float('REFERRAL_FIRST_REWARD', 150.0),
REFERRAL_REFERRED_BONUS=get_float('REFERRAL_REFERRED_BONUS', 150.0),
REFERRAL_THRESHOLD=get_float('REFERRAL_THRESHOLD', 300.0),
REFERRAL_PERCENTAGE=get_float('REFERRAL_PERCENTAGE', 0.25),
MONITOR_ENABLED=get_bool('MONITOR_ENABLED', True),
MONITOR_CHECK_INTERVAL=get_int('MONITOR_CHECK_INTERVAL', 3600), # 1 час по умолчанию
MONITOR_DAILY_CHECK_HOUR=get_int('MONITOR_DAILY_CHECK_HOUR', 10), # 10 утра
MONITOR_WARNING_DAYS=get_int('MONITOR_WARNING_DAYS', 3), # за 3 дня
# Новые настройки для удаления истекших подписок
MONITOR_CHECK_INTERVAL=get_int('MONITOR_CHECK_INTERVAL', 3600),
MONITOR_DAILY_CHECK_HOUR=get_int('MONITOR_DAILY_CHECK_HOUR', 10),
MONITOR_WARNING_DAYS=get_int('MONITOR_WARNING_DAYS', 3),
DELETE_EXPIRED_TRIAL_DAYS=get_int('DELETE_EXPIRED_TRIAL_DAYS', 1),
DELETE_EXPIRED_REGULAR_DAYS=get_int('DELETE_EXPIRED_REGULAR_DAYS', 7),
AUTO_DELETE_ENABLED=get_bool('AUTO_DELETE_ENABLED', False),
# Игра удачи
TRIAL_NOTIFICATION_ENABLED=get_bool('TRIAL_NOTIFICATION_ENABLED', True),
TRIAL_NOTIFICATION_HOURS_AFTER=get_int('TRIAL_NOTIFICATION_HOURS_AFTER', 2),
TRIAL_NOTIFICATION_HOURS_WINDOW=get_int('TRIAL_NOTIFICATION_HOURS_WINDOW', 22),
LUCKY_GAME_ENABLED=get_bool('LUCKY_GAME_ENABLED', True),
LUCKY_GAME_REWARD=get_float('LUCKY_GAME_REWARD', 50.0),
LUCKY_GAME_NUMBERS=get_int('LUCKY_GAME_NUMBERS', 30),
LUCKY_GAME_WINNING_COUNT=get_int('LUCKY_GAME_WINNING_COUNT', 3)
LUCKY_GAME_WINNING_COUNT=get_int('LUCKY_GAME_WINNING_COUNT', 3),
STARS_ENABLED=get_bool('STARS_ENABLED', True),
STARS_RATES=parse_stars_rates()
)
def debug_environment():
@@ -129,7 +133,8 @@ def debug_environment():
'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'
'LUCKY_GAME_ENABLED', 'LUCKY_GAME_REWARD',
'STARS_ENABLED', 'STARS_100_RATE', 'STARS_500_RATE', 'STARS_1000_RATE'
]
print("📋 Environment variables:")
+1058 -57
View File
File diff suppressed because it is too large Load Diff
+100
View File
@@ -0,0 +1,100 @@
"""
Экстренное исправление проблемы с отображением подписок
Этот патч добавляет недостающие поля в таблицу user_subscriptions
"""
import asyncio
import sys
import os
from pathlib import Path
# Добавляем корневую директорию в путь
sys.path.insert(0, str(Path(__file__).parent))
from config import load_config
from database import Database
from sqlalchemy import text
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def check_and_add_column(db, column_name, column_definition):
"""Проверяет и добавляет колонку в таблицу"""
try:
# Отдельная транзакция для проверки
async with db.engine.begin() as conn:
await conn.execute(text(f"SELECT {column_name} FROM user_subscriptions LIMIT 1"))
logger.info(f"✅ Поле {column_name} уже существует")
return True
except Exception:
# Отдельная транзакция для добавления колонки
try:
async with db.engine.begin() as conn:
logger.info(f" Добавляю поле {column_name}...")
await conn.execute(text(f"""
ALTER TABLE user_subscriptions
ADD COLUMN {column_name} {column_definition}
"""))
logger.info(f"✅ Поле {column_name} добавлено")
return True
except Exception as e:
logger.error(f"❌ Ошибка при добавлении {column_name}: {e}")
return False
async def emergency_fix():
"""Экстренное исправление базы данных"""
try:
# Загружаем конфигурацию
config = load_config()
# Подключаемся к базе данных
db = Database(config.DATABASE_URL)
logger.info("🔧 Выполняю экстренное исправление базы данных...")
# Проверяем существование таблицы user_subscriptions
try:
async with db.engine.begin() as conn:
result = await conn.execute(text("SELECT COUNT(*) FROM user_subscriptions"))
count = result.scalar()
logger.info(f"📊 Найдено {count} подписок в таблице user_subscriptions")
except Exception as e:
logger.error(f"❌ Таблица user_subscriptions не найдена: {e}")
await db.close()
return
# Добавляем поля по одному в отдельных транзакциях
success1 = await check_and_add_column(db, "auto_pay_enabled", "BOOLEAN DEFAULT FALSE")
success2 = await check_and_add_column(db, "auto_pay_days_before", "INTEGER DEFAULT 3")
# Финальная проверка в отдельной транзакции
if success1 and success2:
try:
async with db.engine.begin() as conn:
result = await conn.execute(text("""
SELECT id, auto_pay_enabled, auto_pay_days_before
FROM user_subscriptions LIMIT 1
"""))
row = result.fetchone()
if row:
logger.info("✅ Все поля доступны для чтения")
logger.info(f"🔍 Пример записи: id={row[0]}, auto_pay_enabled={row[1]}, auto_pay_days_before={row[2]}")
else:
logger.info("✅ Все поля доступны, но таблица пуста")
except Exception as e:
logger.error(f"❌ Поля все еще недоступны: {e}")
else:
logger.error("❌ Не удалось добавить все необходимые поля")
await db.close()
logger.info("🎉 Экстренное исправление завершено!")
except Exception as e:
logger.error(f"💥 Критическая ошибка: {e}")
raise
if __name__ == "__main__":
asyncio.run(emergency_fix())
+560 -39
View File
@@ -8,12 +8,13 @@ import logging
import secrets
from typing import Optional, Dict, Any
from database import Database, User, ReferralProgram
from database import Database, User, ReferralProgram, ServiceRule
from remnawave_api import RemnaWaveAPI
from keyboards import *
from translations import t
from utils import *
from config import *
from stars_handlers import stars_router
import base64
import json
from referral_utils import (
@@ -59,6 +60,12 @@ class BotStates(StatesGroup):
admin_debug_user_structure = State()
admin_rename_plans_confirm = State()
waiting_number_choice = State()
waiting_rule_title = State()
waiting_rule_content = State()
waiting_rule_order = State()
waiting_rule_edit_title = State()
waiting_rule_edit_content = State()
waiting_rule_edit_order = State()
router = Router()
@@ -91,10 +98,9 @@ async def start_command(message: Message, state: FSMContext, db: Database, **kwa
success = await create_referral_from_start_param(user.telegram_id, start_param, db, bot)
if success:
import os
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
referred_bonus = float(os.getenv('REFERRAL_REFERRED_BONUS', '150.0'))
threshold = config.REFERRAL_THRESHOLD if config else 300.0
referred_bonus = config.REFERRAL_REFERRED_BONUS if config else 150.0
await message.answer(
"🎁 Добро пожаловать!\n\n"
f"Вы перешли по реферальной ссылке! После пополнения баланса на {threshold:.0f}"
@@ -415,13 +421,24 @@ async def balance_callback(callback: CallbackQuery, **kwargs):
@router.callback_query(F.data == "topup_balance")
async def topup_balance_callback(callback: CallbackQuery, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
stars_enabled = config and config.STARS_ENABLED and config.STARS_RATES
text = t('topup_balance', user.language)
if stars_enabled:
text += "\n\n⭐ **Новинка!** Теперь можно пополнять баланс через Telegram Stars!"
text += "\n💎 Быстро, безопасно, без комиссий!"
await callback.message.edit_text(
t('topup_balance', user.language),
reply_markup=topup_keyboard(user.language)
text,
reply_markup=topup_keyboard(user.language),
parse_mode='Markdown'
)
@router.callback_query(F.data == "topup_card")
@@ -522,19 +539,49 @@ async def payment_history_callback(callback: CallbackQuery, db: Database, **kwar
try:
payments = await db.get_user_payments(user.telegram_id)
if not payments:
star_payments = await db.get_user_star_payments(user.telegram_id, 5)
if not payments and not star_payments:
text = t('no_payments', user.language)
else:
text = "📊 " + t('payment_history', user.language) + ":\n\n"
for payment in payments[:10]:
date_str = format_datetime(payment.created_at, user.language)
status = format_payment_status(payment.status, user.language)
text += t('payment_item', user.language,
date=date_str,
amount=payment.amount,
description=payment.description,
status=status
) + "\n"
all_payments = []
for payment in payments[:10]:
all_payments.append({
'type': 'regular',
'date': payment.created_at,
'amount': payment.amount,
'description': payment.description,
'status': payment.status
})
for star_payment in star_payments:
if star_payment.status == 'completed':
all_payments.append({
'type': 'stars',
'date': star_payment.completed_at or star_payment.created_at,
'amount': star_payment.rub_amount,
'description': f'Пополнение через Stars ({star_payment.stars_amount} ⭐)',
'status': star_payment.status,
'stars': star_payment.stars_amount
})
all_payments.sort(key=lambda x: x['date'], reverse=True)
for payment in all_payments[:10]:
date_str = format_datetime(payment['date'], user.language)
status = format_payment_status(payment['status'], user.language)
if payment['type'] == 'stars':
text += f"⭐ +{payment['amount']:.0f}₽ ({payment['stars']} ⭐)\n"
else:
amount_str = f"+{payment['amount']}" if payment['amount'] > 0 else str(payment['amount'])
text += f"💳 {amount_str}\n"
text += f" 📅 {date_str} | {status}\n"
text += f" 📝 {payment['description']}\n\n"
await callback.message.edit_text(
text,
@@ -892,6 +939,20 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
text = format_user_subscription_info(user_sub.__dict__, sub_dict, user_sub.expires_at, user.language)
if not is_trial and not is_imported and subscription.price > 0:
text += f"\n\n🔄 **Автоплатеж:**\n"
if user_sub.auto_pay_enabled:
text += f"✅ Включен (за {user_sub.auto_pay_days_before} дн. до истечения)\n"
text += f"💰 Стоимость продления: {subscription.price} руб.\n"
text += f"💳 Ваш баланс: {user.balance} руб."
if user.balance < subscription.price:
needed = subscription.price - user.balance
text += f"\n⚠️ Недостаточно средств! Нужно еще {needed} руб."
else:
text += f"❌ Отключен\n"
text += f"💡 Включите для автоматического продления"
if user_sub.short_uuid and api:
try:
subscription_url = await api.get_subscription_url(user_sub.short_uuid)
@@ -911,7 +972,14 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
await callback.message.edit_text(
text,
reply_markup=user_subscription_detail_keyboard(user_sub_id, user.language, show_extend, is_imported),
reply_markup=user_subscription_detail_keyboard(
user_sub_id,
user.language,
show_extend,
is_imported,
is_trial,
user_sub.auto_pay_enabled
),
parse_mode='HTML',
disable_web_page_preview=True
)
@@ -1206,23 +1274,48 @@ async def promocode_callback(callback: CallbackQuery, state: FSMContext, **kwarg
await callback.answer("❌ Ошибка пользователя")
return
await callback.message.edit_text(
t('enter_promocode', user.language),
await state.clear()
edited_message = await callback.message.edit_text(
"🎁 Введите промокод:\n\n"
"• Обычные промокоды (скидки)\n"
"• Реферальные коды (REF...)\n\n"
"ℹ️ После ввода вы вернетесь в главное меню",
reply_markup=cancel_keyboard(user.language)
)
await state.update_data(promocode_message_id=edited_message.message_id)
await state.set_state(BotStates.waiting_promocode)
@router.message(StateFilter(BotStates.waiting_promocode))
async def handle_promocode(message: Message, state: FSMContext, db: Database, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
bot = kwargs.get('bot')
if not user:
await message.answer("❌ Ошибка пользователя")
await state.clear()
return
code = message.text.strip().upper()
state_data = await state.get_data()
promocode_message_id = state_data.get('promocode_message_id')
if not validate_promocode_format(code):
await message.answer(t('invalid_input', user.language))
response_msg = await message.answer(
"❌ Неверный формат промокода",
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
except Exception as e:
logger.warning(f"Could not delete promocode request message: {e}")
await state.clear()
return
try:
@@ -1230,17 +1323,50 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
if promocode and promocode.is_active:
if promocode.expires_at and promocode.expires_at < datetime.utcnow():
await message.answer(t('promocode_expired', user.language))
response_msg = await message.answer(
"❌ Промокод истек",
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
except:
pass
await state.clear()
return
if promocode.used_count >= promocode.usage_limit:
await message.answer(t('promocode_limit', user.language))
response_msg = await message.answer(
"❌ Лимит использования промокода исчерпан",
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
except:
pass
await state.clear()
return
success = await db.use_promocode(user.telegram_id, promocode)
if not success:
await message.answer(t('promocode_used', user.language))
response_msg = await message.answer(
"❌ Вы уже использовали этот промокод",
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
except:
pass
await state.clear()
return
await db.add_balance(user.telegram_id, promocode.discount_amount)
@@ -1254,18 +1380,23 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
)
discount_text = f"{promocode.discount_amount} руб."
await message.answer(
success_msg = await message.answer(
t('promocode_success', user.language, discount=discount_text),
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
logger.info(f"Deleted promocode request message {promocode_message_id}")
except Exception as e:
logger.warning(f"Could not delete promocode request message: {e}")
await state.clear()
log_user_action(user.telegram_id, "promocode_used", code)
return
if code.startswith("REF"):
bot = kwargs.get('bot')
async with db.session_factory() as session:
from sqlalchemy import select
result = await session.execute(
@@ -1278,42 +1409,93 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
existing_reverse_referral = await db.get_referral_by_referred_id(referrer_id)
if existing_reverse_referral and existing_reverse_referral.referrer_id == user.telegram_id:
await message.answer(
response_msg = await message.answer(
"❌ Нельзя использовать код человека, которого вы пригласили!\n\n"
"Взаимные рефералы не допускаются."
"Взаимные рефералы не допускаются.",
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
except:
pass
await state.clear()
return
success = await create_referral_from_promocode(user.telegram_id, code, db, bot)
if success:
await message.answer(
"🎉 Реферальный код активирован!\n\n"
"После пополнения баланса на 200₽ вы получите бонус 150₽!",
threshold = config.REFERRAL_THRESHOLD if config else 300.0
referred_bonus = config.REFERRAL_REFERRED_BONUS if config else 150.0
success_msg = await message.answer(
f"🎉 Реферальный код активирован!\n\n"
f"После пополнения баланса на {threshold:.0f}₽ вы получите бонус {referred_bonus:.0f}₽!",
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
logger.info(f"Deleted promocode request message {promocode_message_id}")
except Exception as e:
logger.warning(f"Could not delete promocode request message: {e}")
await state.clear()
log_user_action(user.telegram_id, "referral_code_used", code)
return
else:
existing_referral = await db.get_referral_by_referred_id(user.telegram_id)
if existing_referral:
await message.answer("❌ Вы уже использовали реферальный код!")
error_text = "❌ Вы уже использовали реферальный код!"
else:
await message.answer("❌ Неверный реферальный код!")
error_text = "❌ Неверный реферальный код!"
response_msg = await message.answer(
error_text,
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
except:
pass
await state.clear()
return
await message.answer(t('promocode_not_found', user.language))
response_msg = await message.answer(
"❌ Промокод не найден\n\n"
"Проверьте правильность ввода и попробуйте снова через главное меню.",
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
logger.info(f"Deleted promocode request message {promocode_message_id}")
except Exception as e:
logger.warning(f"Could not delete promocode request message: {e}")
await state.clear()
except Exception as e:
logger.error(f"Error handling promocode: {e}")
await message.answer(
t('error_occurred', user.language),
response_msg = await message.answer(
"❌ Произошла ошибка при обработке промокода",
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
await state.clear()
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
except:
pass
await state.clear()
@router.callback_query(F.data == "referral_program")
async def referral_program_callback(callback: CallbackQuery, db: Database, **kwargs):
@@ -1497,3 +1679,342 @@ async def my_referrals_callback(callback: CallbackQuery, db: Database, **kwargs)
except Exception as e:
logger.error(f"Error showing referrals: {e}")
await callback.answer("❌ Ошибка загрузки")
@router.callback_query(F.data == "service_rules")
async def service_rules_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
rules = await db.get_all_service_rules(active_only=True)
if not rules:
await callback.message.edit_text(
"📜 Правила сервиса пока не добавлены администратором.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
)
return
await show_rules_page(callback, rules, 0, user.language)
except Exception as e:
logger.error(f"Error showing service rules: {e}")
await callback.answer("❌ Ошибка загрузки правил")
async def show_rules_page(callback: CallbackQuery, rules: List[ServiceRule], page_index: int, lang: str = 'ru'):
if page_index < 0 or page_index >= len(rules):
page_index = 0
rule = rules[page_index]
total_pages = len(rules)
safe_title = rule.title.replace('*', '\\*').replace('_', '\\_').replace('[', '\\[').replace(']', '\\]').replace('`', '\\`')
text = f"📜 **{safe_title}**\n\n"
text += rule.content
if total_pages > 1:
text += f"\n\n📄 Страница {page_index + 1} из {total_pages}"
if len(text) > 4000:
text = text[:3950] + "\n\n... (текст обрезан)"
try:
await callback.message.edit_text(
text,
reply_markup=service_rules_keyboard(page_index, total_pages, lang),
parse_mode='Markdown'
)
except Exception as markdown_error:
logger.warning(f"Markdown parsing failed in user rules, retrying without markdown: {markdown_error}")
try:
clean_text = text.replace('**', '').replace('*', '').replace('_', '').replace('`', '')
await callback.message.edit_text(
clean_text,
reply_markup=service_rules_keyboard(page_index, total_pages, lang)
)
except Exception as edit_error:
await callback.message.answer(
clean_text,
reply_markup=service_rules_keyboard(page_index, total_pages, lang)
)
@router.callback_query(F.data.startswith("rules_page_"))
async def rules_page_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
page_index = int(callback.data.split("_")[-1])
rules = await db.get_all_service_rules(active_only=True)
if not rules:
await callback.answer("❌ Правила не найдены")
return
await show_rules_page(callback, rules, page_index, user.language)
except (ValueError, IndexError) as e:
logger.error(f"Error parsing rules page number: {e}")
await callback.answer("❌ Ошибка навигации")
except Exception as e:
logger.error(f"Error switching rules page: {e}")
await callback.answer("❌ Ошибка загрузки страницы")
@router.callback_query(F.data == "cancel", StateFilter(BotStates.waiting_promocode))
async def cancel_promocode_callback(callback: CallbackQuery, state: FSMContext, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
db = kwargs.get('db')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
await state.clear()
show_trial = False
show_lucky_game = True
if config and config.TRIAL_ENABLED and db:
try:
has_used = await db.has_used_trial(user.telegram_id)
show_trial = not has_used
except Exception as e:
logger.error(f"Error checking trial availability: {e}")
if config:
show_lucky_game = getattr(config, 'LUCKY_GAME_ENABLED', True)
await callback.message.edit_text(
t('main_menu', user.language),
reply_markup=main_menu_keyboard(user.language, user.is_admin, show_trial, show_lucky_game)
)
@router.callback_query(F.data.startswith("toggle_autopay_"))
async def toggle_autopay_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
user_sub_id = int(callback.data.split("_")[2])
user_subs = await db.get_user_subscriptions(user.telegram_id)
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
if not user_sub:
await callback.answer("❌ Подписка не найдена")
return
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
if not subscription:
await callback.answer("❌ План подписки не найден")
return
if subscription.is_trial:
await callback.answer("❌ Автоплатеж недоступен для тестовых подписок")
return
if subscription.is_imported or subscription.price == 0:
await callback.answer("❌ Автоплатеж недоступен для импортированных подписок")
return
new_state = not user_sub.auto_pay_enabled
success = await db.toggle_autopay(user_sub_id, new_state)
if success:
status_text = "включен" if new_state else "отключен"
await callback.answer(f"✅ Автоплатеж {status_text}")
updated_subs = await db.get_user_subscriptions(user.telegram_id)
updated_sub = next((sub for sub in updated_subs if sub.id == user_sub_id), None)
if updated_sub:
await autopay_settings_callback(callback, db, user=user)
else:
await callback.answer("❌ Ошибка изменения настроек")
except Exception as e:
logger.error(f"Error toggling autopay: {e}")
await callback.answer("❌ Ошибка операции")
@router.callback_query(F.data == "autopay_help")
async def autopay_help_callback(callback: CallbackQuery, **kwargs):
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
text = "🔄 **Автоплатеж - как это работает?**\n\n"
text += "**🎯 Что такое автоплатеж:**\n"
text += "Автоматическое продление подписки за несколько дней до истечения срока действия.\n\n"
text += "**⚙️ Как настроить:**\n"
text += "1. Перейдите в 'Мои подписки'\n"
text += "2. Выберите нужную подписку\n"
text += "3. Нажмите 'Настроить автоплатеж'\n"
text += "4. Включите автоплатеж и выберите за сколько дней до истечения продлевать\n\n"
text += "**💰 Требования:**\n"
text += "• На балансе должно быть достаточно средств\n"
text += "• Подписка должна быть активной\n"
text += "• Автоплатеж работает только с обычными подписками\n\n"
text += "**📅 Варианты продления:**\n"
text += "• За 1 день - для тех, кто следит за балансом\n"
text += "• За 3 дня - рекомендуемый вариант\n"
text += "• За 5 дней - для подстраховки\n"
text += "• За 7 дней - максимальный запас времени\n\n"
text += "**🔔 Уведомления:**\n"
text += "Вы получите уведомление при успешном продлении или если недостаточно средств.\n\n"
text += "**❓ Проблемы:**\n"
text += "Если автоплатеж не сработал, проверьте баланс и обратитесь в поддержку."
await callback.message.edit_text(
text,
reply_markup=autopay_help_keyboard(user.language),
parse_mode='Markdown'
)
@router.callback_query(F.data == "autopay_insufficient_balance_help")
async def autopay_insufficient_balance_help_callback(callback: CallbackQuery, **kwargs):
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
text = "⚠️ **Недостаточно средств для автоплатежа**\n\n"
text += f"💳 **Ваш текущий баланс:** {user.balance}\n\n"
text += "**🔄 Что происходит:**\n"
text += "Система пыталась автоматически продлить вашу подписку, но на балансе недостаточно средств.\n\n"
text += "**✅ Что нужно сделать:**\n"
text += "1. Пополните баланс на нужную сумму\n"
text += "2. Автоплатеж попробует снова при следующей проверке\n"
text += "3. Или продлите подписку вручную\n\n"
text += "**⏰ Важно:**\n"
text += "Пополните баланс до истечения срока подписки, иначе доступ будет приостановлен."
await callback.message.edit_text(
text,
reply_markup=autopay_help_keyboard(user.language),
parse_mode='Markdown'
)
@router.callback_query(F.data.startswith("autopay_settings_"))
async def autopay_settings_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
user_sub_id = int(callback.data.split("_")[2])
user_subs = await db.get_user_subscriptions(user.telegram_id)
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
if not user_sub:
await callback.answer("❌ Подписка не найдена")
return
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
if not subscription:
await callback.answer("❌ План подписки не найден")
return
if subscription.is_trial:
await callback.answer("❌ Автоплатеж недоступен для тестовых подписок")
return
if subscription.is_imported or subscription.price == 0:
await callback.answer("❌ Автоплатеж недоступен для импортированных подписок")
return
from datetime import datetime
now = datetime.utcnow()
days_left = (user_sub.expires_at - now).days
text = f"⚙️ Настройки автоплатежа\n\n"
text += f"📋 Подписка: {subscription.name}\n"
text += f"💰 Цена продления: {subscription.price} руб.\n"
text += f"⏱ Длительность: {subscription.duration_days} дн.\n"
text += f"📅 Истекает через: {days_left} дн.\n\n"
status_emoji = "" if user_sub.auto_pay_enabled else ""
status_text = "Включен" if user_sub.auto_pay_enabled else "Отключен"
text += f"🔄 Автоплатеж: {status_emoji} {status_text}\n"
if user_sub.auto_pay_enabled:
text += f"📅 Продлять за: {user_sub.auto_pay_days_before} дн. до истечения\n\n"
if user.balance >= subscription.price:
text += f"✅ На балансе достаточно средств ({user.balance}₽)\n"
text += f"💡 Подписка будет автоматически продлена"
else:
needed = subscription.price - user.balance
text += f"⚠️ Недостаточно средств на балансе!\n"
text += f"💳 Ваш баланс: {user.balance}\n"
text += f"💸 Нужно еще: {needed}\n"
text += f"💡 Пополните баланс для автоматического продления"
else:
text += f"\n💡 Включите автоплатеж для автоматического продления подписки\n"
text += f"🎯 Рекомендуем включить за 3 дня до истечения"
await callback.message.edit_text(
text,
reply_markup=autopay_settings_keyboard(user_sub_id, user_sub, user.language)
)
except Exception as e:
logger.error(f"Error showing autopay settings: {e}")
await callback.answer("❌ Ошибка загрузки настроек")
@router.callback_query(F.data.startswith("autopay_days_"))
async def autopay_days_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
parts = callback.data.split("_")
user_sub_id = int(parts[2])
days = int(parts[3])
user_subs = await db.get_user_subscriptions(user.telegram_id)
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
if not user_sub:
await callback.answer("❌ Подписка не найдена")
return
success = await db.set_autopay_days(user_sub_id, days)
if success:
await callback.answer(f"✅ Установлено: продлять за {days} дн.")
updated_subs = await db.get_user_subscriptions(user.telegram_id)
updated_sub = next((sub for sub in updated_subs if sub.id == user_sub_id), None)
if updated_sub:
await autopay_settings_callback(callback, db, user=user)
else:
await callback.answer("❌ Ошибка обновления настроек")
except Exception as e:
logger.error(f"Error setting autopay days: {e}")
await callback.answer("❌ Ошибка операции")
+331 -5
View File
@@ -2,6 +2,7 @@ from database import Subscription
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from typing import List, Optional, Dict
from translations import t
from datetime import datetime
def language_keyboard() -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
@@ -35,7 +36,8 @@ def main_menu_keyboard(lang: str = 'ru', is_admin: bool = False, show_trial: boo
[
InlineKeyboardButton(text="💬 " + t('support', lang), callback_data="support")
],
[InlineKeyboardButton(text="🌐 " + t('change_language', lang), callback_data="change_language")]
[InlineKeyboardButton(text="🌐 " + t('change_language', lang), callback_data="change_language")],
[InlineKeyboardButton(text="📜 Правила сервиса", callback_data="service_rules")]
])
if is_admin:
@@ -109,22 +111,52 @@ def user_subscriptions_keyboard(user_subscriptions: List[dict], lang: str = 'ru'
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def user_subscription_detail_keyboard(subscription_id: int, lang: str = 'ru', show_extend: bool = False, is_imported: bool = False) -> InlineKeyboardMarkup:
def user_subscription_detail_keyboard(subscription_id: int, lang: str = 'ru',
show_extend: bool = False, is_imported: bool = False,
is_trial: bool = False, autopay_enabled: bool = False) -> InlineKeyboardMarkup:
buttons = []
if is_imported:
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
buttons.append([InlineKeyboardButton(text="🛒 Купить новую подписку", callback_data="buy_subscription")])
elif is_trial:
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
buttons.append([InlineKeyboardButton(text="🛒 Купить полную подписку", callback_data="buy_subscription")])
else:
if show_extend:
buttons.append([InlineKeyboardButton(text="" + t('extend_subscription', lang), callback_data=f"extend_sub_{subscription_id}")])
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
if autopay_enabled:
autopay_text = "🔄✅ Настроить автоплатеж"
else:
autopay_text = "🔄❌ Настроить автоплатеж"
buttons.append([InlineKeyboardButton(text=autopay_text, callback_data=f"autopay_settings_{subscription_id}")])
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="my_subscriptions")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def autopay_confirmation_keyboard(subscription_id: int, action: str, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_autopay_{action}_{subscription_id}"),
InlineKeyboardButton(text="❌ Нет", callback_data=f"autopay_settings_{subscription_id}")
]
])
return keyboard
def autopay_help_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Клавиатура помощи по автоплатежам"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="topup_balance")],
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
return keyboard
def extend_subscription_keyboard(subscription_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
@@ -159,13 +191,17 @@ def admin_menu_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
],
[
InlineKeyboardButton(text="📨 " + t('send_message', lang), callback_data="admin_messages"),
InlineKeyboardButton(text="👥 Рефералы", callback_data="admin_referrals") # НОВАЯ КНОПКА
InlineKeyboardButton(text="👥 Рефералы", callback_data="admin_referrals")
],
[
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system")
InlineKeyboardButton(text="📜 Правила сервиса", callback_data="admin_rules"),
InlineKeyboardButton(text="🔄 Автоплатежи", callback_data="admin_autopay")
],
[
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system"),
InlineKeyboardButton(text="🔍 Мониторинг подписок", callback_data="admin_monitor")
],
[
InlineKeyboardButton(text="🔍 Мониторинг подписок", callback_data="admin_monitor"),
InlineKeyboardButton(text="📊 " + t('statistics', lang), callback_data="admin_stats")
],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")]
@@ -183,15 +219,89 @@ def admin_subscriptions_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
def admin_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="👥 Список пользователей", callback_data="list_users")],
[InlineKeyboardButton(text="📋 Все подписки пользователей", callback_data="admin_user_subscriptions_all")],
[InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="search_user")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def admin_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="👥 Список пользователей", callback_data="list_users")],
[InlineKeyboardButton(text="📋 Все подписки пользователей", callback_data="admin_user_subscriptions_all")],
[InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="search_user")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def admin_user_subscriptions_filters_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Клавиатура фильтров для подписок пользователей"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="🟢 Активные", callback_data="filter_subs_active"),
InlineKeyboardButton(text="🔴 Истекшие", callback_data="filter_subs_expired")
],
[
InlineKeyboardButton(text="🔄✅ С автоплатежом", callback_data="filter_subs_autopay"),
InlineKeyboardButton(text="⏰ Истекают скоро", callback_data="filter_subs_expiring")
],
[
InlineKeyboardButton(text="🆓 Триальные", callback_data="filter_subs_trial"),
InlineKeyboardButton(text="📦 Импортированные", callback_data="filter_subs_imported")
],
[
InlineKeyboardButton(text="📋 Все подписки", callback_data="admin_user_subscriptions_all"),
InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")
]
])
return keyboard
def admin_user_subscription_detail_keyboard(subscription_id: int, user_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Клавиатура для детального просмотра подписки пользователя"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✏️ Редактировать", callback_data=f"edit_user_sub_{subscription_id}"),
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_user_sub_{subscription_id}")
],
[
InlineKeyboardButton(text="👤 К пользователю", callback_data=f"admin_user_detail_{user_id}"),
InlineKeyboardButton(text="📋 К списку подписок", callback_data="admin_user_subscriptions_all")
],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")]
])
return keyboard
def user_subscriptions_pagination_keyboard(current_page: int, total_pages: int,
filter_type: str = "all", lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
if total_pages > 1:
nav_row = []
if current_page > 0:
nav_row.append(InlineKeyboardButton(text="⬅️", callback_data=f"user_subs_page_{current_page - 1}_{filter_type}"))
nav_row.append(InlineKeyboardButton(text=f"{current_page + 1}/{total_pages}", callback_data="noop"))
if current_page < total_pages - 1:
nav_row.append(InlineKeyboardButton(text="➡️", callback_data=f"user_subs_page_{current_page + 1}_{filter_type}"))
buttons.append(nav_row)
buttons.append([
InlineKeyboardButton(text="🔍 Фильтры", callback_data="admin_user_subscriptions_filters"),
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_user_subs_{filter_type}")
])
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def admin_balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💸 Пополнить баланс пользователю", callback_data="admin_add_balance")],
[InlineKeyboardButton(text="📊 История платежей", callback_data="admin_payment_history")],
[InlineKeyboardButton(text="⭐ Telegram Stars", callback_data="admin_stars_payments")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
@@ -602,3 +712,219 @@ def lucky_game_result_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
[InlineKeyboardButton(text="📈 История игр", callback_data="lucky_game_history")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
def topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="⭐ Telegram Stars", callback_data="topup_stars")],
[InlineKeyboardButton(text="👨‍💼 " + t('topup_support', lang), callback_data="topup_support")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="balance")]
])
return keyboard
def stars_topup_keyboard(stars_rates: Dict[int, float], lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
sorted_rates = sorted(stars_rates.items())
for i in range(0, len(sorted_rates), 2):
row = []
for j in range(2):
if i + j < len(sorted_rates):
stars, rubles = sorted_rates[i + j]
if stars >= 500:
emoji = "🔥" # Выгодное предложение
elif stars >= 250:
emoji = "💎" # Хорошее предложение
else:
emoji = "" # Базовое предложение
button_text = f"{emoji} {stars} ⭐ → {rubles:.0f}"
row.append(InlineKeyboardButton(
text=button_text,
callback_data=f"buy_stars_{stars}"
))
buttons.append(row)
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="topup_balance")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def stars_payment_keyboard(stars_amount: int, rub_amount: float, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отмена", callback_data="topup_stars")]
])
return keyboard
def service_rules_keyboard(current_page: int, total_pages: int, lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
if total_pages > 1:
nav_row = []
if current_page > 0:
nav_row.append(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"rules_page_{current_page - 1}"))
nav_row.append(InlineKeyboardButton(text=f"{current_page + 1}/{total_pages}", callback_data="noop"))
if current_page < total_pages - 1:
nav_row.append(InlineKeyboardButton(text="Вперед ➡️", callback_data=f"rules_page_{current_page + 1}"))
buttons.append(nav_row)
buttons.append([InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def admin_rules_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📋 Список правил", callback_data="admin_rules_list")],
[InlineKeyboardButton(text=" Добавить страницу", callback_data="admin_rules_create")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def admin_rules_list_keyboard(rules, lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
for rule in rules:
status_emoji = "🟢" if rule.is_active else "🔴"
buttons.append([
InlineKeyboardButton(
text=f"{status_emoji} {rule.page_order}. {rule.title}",
callback_data=f"admin_rule_view_{rule.id}"
)
])
buttons.extend([
[InlineKeyboardButton(text=" Добавить страницу", callback_data="admin_rules_create")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_rules")]
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def admin_rule_edit_keyboard(rule_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✏️ Редактировать заголовок", callback_data=f"admin_rule_edit_title_{rule_id}"),
InlineKeyboardButton(text="📝 Редактировать содержимое", callback_data=f"admin_rule_edit_content_{rule_id}")
],
[
InlineKeyboardButton(text="🔄 Изменить порядок", callback_data=f"admin_rule_edit_order_{rule_id}"),
InlineKeyboardButton(text="🟢/🔴 Вкл/Выкл", callback_data=f"admin_rule_toggle_{rule_id}")
],
[InlineKeyboardButton(text="🗑 Удалить", callback_data=f"admin_rule_delete_{rule_id}")],
[InlineKeyboardButton(text="🔙 К списку", callback_data="admin_rules_list")]
])
return keyboard
def admin_rule_delete_confirm_keyboard(rule_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да, удалить", callback_data=f"admin_rule_confirm_delete_{rule_id}"),
InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_rule_view_{rule_id}")
]
])
return keyboard
def autopay_settings_keyboard(user_sub_id: int, user_sub, lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
if user_sub.auto_pay_enabled:
toggle_text = "❌ Отключить автоплатеж"
toggle_callback = f"toggle_autopay_{user_sub_id}"
else:
toggle_text = "✅ Включить автоплатеж"
toggle_callback = f"toggle_autopay_{user_sub_id}"
buttons.append([InlineKeyboardButton(text=toggle_text, callback_data=toggle_callback)])
if user_sub.auto_pay_enabled:
buttons.append([InlineKeyboardButton(text="📅 Настроить дни до продления", callback_data="noop")])
days_row = []
for days in [1, 3, 5, 7]:
emoji = "🔹" if user_sub.auto_pay_days_before == days else ""
days_row.append(InlineKeyboardButton(
text=f"{emoji} {days}д",
callback_data=f"autopay_days_{user_sub_id}_{days}"
))
buttons.append(days_row)
buttons.append([InlineKeyboardButton(text="🔙 К подписке", callback_data=f"view_sub_{user_sub_id}")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def autopay_status_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_status")],
[InlineKeyboardButton(text="🚀 Принудительная проверка", callback_data="autopay_force_check")],
[InlineKeyboardButton(text="📋 Список подписок", callback_data="autopay_subscriptions_list")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
])
return keyboard
def autopay_subscriptions_keyboard(subscriptions_data: List[Dict], lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
expired = [s for s in subscriptions_data if s['expires_in_days'] <= 0]
due_soon = [s for s in subscriptions_data if 0 < s['expires_in_days'] <= s['auto_pay_days_before']]
critical_subs = expired + due_soon
for sub_data in critical_subs[:8]:
username = sub_data['username'] if sub_data['username'] != 'N/A' else f"ID:{sub_data['user_id']}"
days = sub_data['expires_in_days']
if days <= 0:
status_emoji = ""
status_text = f"Истекла"
elif days <= sub_data['auto_pay_days_before']:
status_emoji = "⚠️"
status_text = f"Через {days}д"
else:
status_emoji = ""
status_text = f"Через {days}д"
button_text = f"{status_emoji} @{username} ({status_text})"
buttons.append([
InlineKeyboardButton(
text=button_text,
callback_data=f"autopay_user_detail_{sub_data['user_id']}"
)
])
if len(subscriptions_data) > 8:
buttons.append([
InlineKeyboardButton(
text=f"... и еще {len(subscriptions_data) - 8}",
callback_data="noop"
)
])
buttons.extend([
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_subscriptions_list")],
[InlineKeyboardButton(text="📊 Статистика", callback_data="autopay_statistics")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def autopay_user_detail_keyboard(user_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💳 Добавить баланс", callback_data=f"admin_add_balance_to_{user_id}")],
[InlineKeyboardButton(text="📋 Управление подписками", callback_data=f"admin_user_subscriptions_{user_id}")],
[InlineKeyboardButton(text="🔄 Обновить", callback_data=f"autopay_user_detail_{user_id}")],
[InlineKeyboardButton(text="🔙 К списку", callback_data="autopay_subscriptions_list")]
])
return keyboard
def autopay_statistics_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="⚠️ Недостаточно средств", callback_data="autopay_insufficient_balance_users")],
[InlineKeyboardButton(text="📋 Список подписок", callback_data="autopay_subscriptions_list")],
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_statistics")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
])
return keyboard
+63
View File
@@ -7,6 +7,8 @@ from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from lucky_game import lucky_game_router
from stars_handlers import stars_router
from autopay_service import AutoPayService
print("🚀 Запуск бота...")
print(f"📍 Рабочая директория: {os.getcwd()}")
@@ -45,6 +47,40 @@ class BotApplication:
self.bot = None
self.dp = None
self.monitor_service = None
self.autopay_service = None
async def _init_autopay_service(self):
"""Инициализирует сервис автоплатежей"""
try:
logger.info("🔧 Initializing autopay service...")
if not self.bot:
logger.error("❌ Bot instance is None, cannot initialize autopay")
return
if not self.db:
logger.error("❌ Database instance is None, cannot initialize autopay")
return
self.autopay_service = AutoPayService(self.db, self.api, self.bot)
self.dp.workflow_data["autopay_service"] = self.autopay_service
logger.info("✅ Autopay service added to workflow_data")
logger.info("🚀 Starting autopay service...")
await self.autopay_service.start()
status = await self.autopay_service.get_service_status()
if status['is_running']:
logger.info("✅ Autopay service started successfully")
logger.info(f"📊 Autopay status: interval=30min")
else:
logger.warning("⚠️ Autopay service created but not running")
except Exception as e:
logger.error(f"❌ Failed to initialize autopay service: {e}", exc_info=True)
logger.warning("⚠️ Continuing without autopay service")
self.autopay_service = None
async def initialize(self):
@@ -96,6 +132,25 @@ class BotApplication:
self._setup_dispatcher()
await self._init_monitor_service()
await self._init_autopay_service()
if self.config.STARS_ENABLED:
logger.info("✅ Telegram Stars пополнение включено")
logger.info(f"📊 Настроенные курсы: {self.config.STARS_RATES}")
if not self.config.STARS_RATES or len(self.config.STARS_RATES) == 0:
logger.warning("⚠️ STARS_RATES пустые! Telegram Stars будут недоступны")
self.config.STARS_ENABLED = False
else:
valid_rates = all(
isinstance(stars, int) and isinstance(rubles, (int, float)) and stars > 0 and rubles > 0
for stars, rubles in self.config.STARS_RATES.items()
)
if not valid_rates:
logger.warning("⚠️ Неверные STARS_RATES! Telegram Stars будут недоступны")
self.config.STARS_ENABLED = False
else:
logger.info("❌ Telegram Stars пополнение отключено")
async def _init_database(self):
max_retries = 3
@@ -167,6 +222,7 @@ class BotApplication:
self.dp.include_router(router)
self.dp.include_router(admin_router)
self.dp.include_router(lucky_game_router)
self.dp.include_router(stars_router)
async def _init_monitor_service(self):
try:
@@ -229,6 +285,13 @@ class BotApplication:
async def shutdown(self):
logger.info("Shutting down bot...")
if self.autopay_service:
try:
await self.autopay_service.stop()
logger.info("Autopay service stopped")
except Exception as e:
logger.error(f"Error stopping autopay service: {e}")
if self.monitor_service:
try:
+3 -1
View File
@@ -19,7 +19,9 @@ class RemnaWaveAPI:
headers = {
'Authorization': f'Bearer {self.token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
'Accept': 'application/json',
'X-Forwarded-Proto': 'https',
'X-Forwarded-For': '127.0.0.1'
}
timeout = aiohttp.ClientTimeout(total=30)
self.session = aiohttp.ClientSession(
-218
View File
@@ -1,218 +0,0 @@
#!/bin/bash
# === RemnaWave Bot Setup & Management Script ===
SERVICE_NAME="remnawave-bot"
BOT_FILE="main.py"
ENV_FILE=".env"
VENV_DIR="venv"
# Select language
echo "🌐 Select language / Выберите язык:"
echo "1) English"
echo "2) Русский"
read -p ">>> " LANG_CHOICE
case $LANG_CHOICE in
2) LANG_CODE="ru" ;;
*) LANG_CODE="en" ;;
esac
# === Internationalized messages ===
msg() {
case $1 in
check_python) [[ $LANG_CODE == "ru" ]] && echo "🔍 Проверка Python..." || echo "🔍 Checking Python..." ;;
python_missing) [[ $LANG_CODE == "ru" ]] && echo "❌ Python 3 не установлен. Установите Python 3.8+." || echo "❌ Python 3 is not installed. Please install Python 3.8+." ;;
version) echo "📋 Python version: $PYTHON_VERSION" ;;
creating_venv) [[ $LANG_CODE == "ru" ]] && echo "🔧 Создание виртуального окружения..." || echo "🔧 Creating virtual environment..." ;;
activating_venv) [[ $LANG_CODE == "ru" ]] && echo "🔧 Активация виртуального окружения..." || echo "🔧 Activating virtual environment..." ;;
upgrading_pip) [[ $LANG_CODE == "ru" ]] && echo "📦 Обновление pip..." || echo "📦 Upgrading pip..." ;;
installing_requirements) [[ $LANG_CODE == "ru" ]] && echo "📦 Установка зависимостей..." || echo "📦 Installing requirements..." ;;
env_missing) [[ $LANG_CODE == "ru" ]] && echo "⚠️ Файл .env не найден!" || echo "⚠️ .env file not found!" ;;
env_created) [[ $LANG_CODE == "ru" ]] && echo "📋 Создан .env файл. Пожалуйста, отредактируйте его." || echo "📋 .env created. Please edit it." ;;
validating) [[ $LANG_CODE == "ru" ]] && echo "🔍 Проверка конфигурации..." || echo "🔍 Validating configuration..." ;;
config_ok) [[ $LANG_CODE == "ru" ]] && echo "✅ Конфигурация прошла проверку." || echo "✅ Configuration validated successfully." ;;
creating_service) [[ $LANG_CODE == "ru" ]] && echo "🔧 Создание systemd-сервиса..." || echo "🔧 Creating systemd service..." ;;
service_created) [[ $LANG_CODE == "ru" ]] && echo "✅ Сервис создан и включён." || echo "✅ Service created and enabled." ;;
start_prompt) [[ $LANG_CODE == "ru" ]] && echo "🚀 Запуск бота..." || echo "🚀 Starting the bot..." ;;
already_running) [[ $LANG_CODE == "ru" ]] && echo "🟢 Бот уже запущен!" || echo "🟢 Bot is already running!" ;;
last_logs) [[ $LANG_CODE == "ru" ]] && echo "📄 Последние 20 строк лога:" || echo "📄 Last 20 log lines:" ;;
action_menu)
[[ $LANG_CODE == "ru" ]] && {
echo -e "\nЧто вы хотите сделать?"
echo "1) Остановить бота"
echo "2) Перезапустить бота"
echo "3) Выйти"
} || {
echo -e "\nWhat would you like to do?"
echo "1) Stop the bot"
echo "2) Restart the bot"
echo "3) Exit"
}
;;
restarting) [[ $LANG_CODE == "ru" ]] && echo "🔄 Перезапуск бота..." || echo "🔄 Restarting bot..." ;;
completed)
echo ""
[[ $LANG_CODE == "ru" ]] && {
echo "🎉 Установка завершена!"
echo "📋 Возможности бота:"
echo " ✅ Мультиязычный интерфейс"
echo " ✅ Управление балансом и подписками"
echo " ✅ Промокоды и поддержка"
echo ""
} || {
echo "🎉 Installation complete!"
echo "📋 Bot Features:"
echo " ✅ Multi-language support"
echo " ✅ Subscription and balance management"
echo " ✅ Promocode system and support"
echo ""
}
;;
esac
}
# === Check if bot is running ===
if systemctl is-active --quiet "$SERVICE_NAME"; then
msg already_running
msg last_logs
sudo journalctl -u "$SERVICE_NAME" -n 20 --no-pager
msg action_menu
read -p ">>> " choice
case $choice in
1)
[[ $LANG_CODE == "ru" ]] && echo "🛑 Остановка бота..." || echo "🛑 Stopping the bot..."
sudo systemctl stop "$SERVICE_NAME"
[[ $LANG_CODE == "ru" ]] && echo "✅ Бот остановлен." || echo "✅ Bot stopped."
exit 0
;;
2)
msg restarting
sudo systemctl restart "$SERVICE_NAME"
[[ $LANG_CODE == "ru" ]] && echo "✅ Бот перезапущен." || echo "✅ Bot restarted."
exit 0
;;
3)
[[ $LANG_CODE == "ru" ]] && echo "👋 Выход." || echo "👋 Exiting."
exit 0
;;
*)
[[ $LANG_CODE == "ru" ]] && echo "⚠️ Неверный выбор." || echo "⚠️ Invalid choice."
exit 1
;;
esac
fi
# === Install/Setup ===
msg check_python
if ! command -v python3 &> /dev/null; then
msg python_missing
exit 1
fi
PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
msg version
if [ ! -d "$VENV_DIR" ]; then
msg creating_venv
python3 -m venv "$VENV_DIR"
fi
msg activating_venv
source "$VENV_DIR/bin/activate"
msg upgrading_pip
pip install --upgrade pip
msg installing_requirements
pip install -r requirements.txt
# Create .env if needed
if [ ! -f "$ENV_FILE" ]; then
msg env_missing
cat > "$ENV_FILE" <<EOF
# Bot Configuration
BOT_TOKEN=
# RemnaWave API Configuration
REMNAWAVE_URL=
REMNAWAVE_MODE=
REMNAWAVE_TOKEN=
# Admin Configuration (comma-separated list of Telegram user IDs)
ADMIN_IDS=
# Support Configuration
SUPPORT_USERNAME=support
# Database Configuration (optional, defaults to SQLite)
# DATABASE_URL=sqlite+aiosqlite:///bot.db
# Trial Configuration
TRIAL_ENABLED=true
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_GB=2
TRIAL_SQUAD_UUID=
TRIAL_PRICE=0.0
EOF
msg env_created
echo ""
read -p "🔧 Press Enter after configuring .env / Нажмите Enter после настройки .env..."
fi
# Load env
export $(grep -v '^#' "$ENV_FILE" | xargs)
msg validating
if [[ -z "$BOT_TOKEN" || "$BOT_TOKEN" == "your_telegram_bot_token_here" ]]; then
echo "❌ BOT_TOKEN not set properly."
exit 1
fi
if [[ -z "$REMNAWAVE_URL" || "$REMNAWAVE_URL" == "https://your-panel.com" ]]; then
echo "❌ REMNAWAVE_URL not set properly."
exit 1
fi
if [[ -z "$REMNAWAVE_TOKEN" || "$REMNAWAVE_TOKEN" == "your_jwt_token_here" ]]; then
echo "❌ REMNAWAVE_TOKEN not set properly."
exit 1
fi
msg config_ok
# Create systemd service
read -p "🔧 Create systemd service? (y/n): " CREATE_SERVICE
if [[ "$CREATE_SERVICE" == "y" || "$CREATE_SERVICE" == "Y" ]]; then
msg creating_service
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
CURRENT_DIR=$(pwd)
CURRENT_USER=$(whoami)
sudo tee "$SERVICE_FILE" > /dev/null <<EOF
[Unit]
Description=RemnaWave Bot
After=network.target
[Service]
Type=simple
User=$CURRENT_USER
WorkingDirectory=$CURRENT_DIR
Environment=PATH=$CURRENT_DIR/$VENV_DIR/bin
EnvironmentFile=$CURRENT_DIR/.env
ExecStart=$CURRENT_DIR/$VENV_DIR/bin/python3 $BOT_FILE
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable "$SERVICE_NAME"
msg service_created
fi
msg completed
msg start_prompt
python3 "$BOT_FILE"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

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

After

Width:  |  Height:  |  Size: 211 KiB

+415
View File
@@ -0,0 +1,415 @@
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, LabeledPrice, PreCheckoutQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.exceptions import TelegramBadRequest
from datetime import datetime
import logging
from typing import Dict
from database import Database, User
from keyboards import stars_topup_keyboard, stars_payment_keyboard, balance_keyboard
from translations import t
from utils import log_user_action
from referral_utils import process_referral_rewards
logger = logging.getLogger(__name__)
stars_router = Router()
@stars_router.callback_query(F.data == "topup_stars")
async def topup_stars_callback(callback: CallbackQuery, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
if not config or not config.STARS_ENABLED:
await callback.message.edit_text(
"❌ Пополнение через Telegram Stars временно недоступно",
reply_markup=balance_keyboard(user.language)
)
return
if not config.STARS_RATES:
await callback.message.edit_text(
"❌ Курсы звезд не настроены",
reply_markup=balance_keyboard(user.language)
)
return
text = "⭐ **Пополнение через Telegram Stars**\n\n"
text += "🚀 **Преимущества:**\n"
text += "• Мгновенное зачисление\n"
text += "• Безопасные платежи через Telegram\n"
text += "• Без комиссий и скрытых платежей\n\n"
text += "💎 **Доступные варианты:**\n"
sorted_rates = sorted(config.STARS_RATES.items())
for stars, rubles in sorted_rates:
rate_per_star = rubles / stars
if stars >= 500:
bonus_text = " 🔥 Выгодно!"
elif stars >= 250:
bonus_text = " 💎 Хорошо!"
else:
bonus_text = ""
text += f"{stars} ⭐ → {rubles:.0f}{bonus_text}\n"
text += f"\n💡 Выберите подходящий вариант:"
await callback.message.edit_text(
text,
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
@stars_router.callback_query(F.data.startswith("buy_stars_"))
async def buy_stars_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
bot = kwargs.get('bot')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
if not config or not config.STARS_ENABLED or not config.STARS_RATES:
await callback.answer("❌ Пополнение недоступно")
return
try:
stars_amount = int(callback.data.split("_")[2])
if stars_amount not in config.STARS_RATES:
await callback.answer("❌ Неверное количество звезд")
return
rub_amount = config.STARS_RATES[stars_amount]
star_payment = await db.create_star_payment(
user_id=user.telegram_id,
stars_amount=stars_amount,
rub_amount=rub_amount
)
prices = [LabeledPrice(label=f"Пополнение на {rub_amount:.0f}", amount=stars_amount)]
try:
await callback.answer("💳 Создаю платеж...")
await bot.send_invoice(
chat_id=callback.message.chat.id,
title=f"Пополнение баланса",
description=f"Пополнение баланса на {rub_amount:.0f}₽ за {stars_amount}",
payload=f"star_payment_{star_payment.id}",
currency="XTR",
prices=prices
)
try:
await callback.message.edit_text(
f"💳 **Оплата через Telegram Stars**\n\n"
f"⭐ Количество звезд: {stars_amount}\n"
f"💰 Сумма пополнения: {rub_amount:.0f}\n\n"
f"👆 Нажмите кнопку \"Оплатить\" в инвойсе ниже⤵️\n\n"
f"Если передумали - нажмите кнопку ниже \n\n❌ Отменить платеж",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отменить платеж", callback_data=f"cancel_star_payment_{star_payment.id}")],
[InlineKeyboardButton(text="🔙 Назад к выбору", callback_data="topup_stars")]
]),
parse_mode='Markdown'
)
except TelegramBadRequest:
await callback.message.answer(
f"💳 **Оплата через Telegram Stars**\n\n"
f"⭐ Количество звезд: {stars_amount}\n"
f"💰 Сумма пополнения: {rub_amount:.0f}\n\n"
f"👆 Нажмите кнопку \"Оплатить\" в инвойсе ниже⤵️",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отменить платеж", callback_data=f"cancel_star_payment_{star_payment.id}")],
[InlineKeyboardButton(text="🔙 Назад к выбору", callback_data="topup_stars")]
]),
parse_mode='Markdown'
)
except TelegramBadRequest as e:
logger.error(f"Failed to send invoice: {e}")
await callback.answer("❌ Ошибка создания платежа", show_alert=True)
try:
await callback.message.edit_text(
"❌ Ошибка создания платежа. Попробуйте позже.",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language)
)
except TelegramBadRequest:
await callback.message.answer(
"❌ Ошибка создания платежа. Попробуйте позже.",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language)
)
await db.cancel_star_payment(star_payment.id)
except (ValueError, IndexError) as e:
logger.error(f"Error processing stars amount: {e}")
await callback.answer("❌ Ошибка обработки")
except Exception as e:
logger.error(f"Error in buy_stars_callback: {e}")
await callback.answer("❌ Произошла ошибка")
@stars_router.pre_checkout_query()
async def pre_checkout_query_handler(pre_checkout_query: PreCheckoutQuery, db: Database, **kwargs):
try:
if not pre_checkout_query.invoice_payload.startswith("star_payment_"):
await pre_checkout_query.answer(ok=False, error_message="Неверный формат платежа")
return
payment_id = int(pre_checkout_query.invoice_payload.split("_")[2])
star_payment = await db.get_star_payment_by_id(payment_id)
if not star_payment:
await pre_checkout_query.answer(ok=False, error_message="Платеж не найден")
return
if star_payment.status != 'pending':
await pre_checkout_query.answer(ok=False, error_message="Платеж уже обработан")
return
if pre_checkout_query.total_amount != star_payment.stars_amount:
await pre_checkout_query.answer(ok=False, error_message="Неверная сумма платежа")
return
await pre_checkout_query.answer(ok=True)
except Exception as e:
logger.error(f"Error in pre_checkout_query_handler: {e}")
await pre_checkout_query.answer(ok=False, error_message="Внутренняя ошибка")
@stars_router.message(F.successful_payment)
async def successful_payment_handler(message: Message, db: Database, **kwargs):
user = kwargs.get('user')
bot = kwargs.get('bot')
if not user:
logger.error("User not found in successful payment handler")
return
try:
payment_info = message.successful_payment
if not payment_info.invoice_payload.startswith("star_payment_"):
logger.error(f"Invalid payment payload: {payment_info.invoice_payload}")
return
payment_id = int(payment_info.invoice_payload.split("_")[2])
success = await db.complete_star_payment(
payment_id=payment_id,
telegram_payment_charge_id=payment_info.telegram_payment_charge_id
)
if success:
star_payment = await db.get_star_payment_by_id(payment_id)
if star_payment:
updated_user = await db.get_user_by_telegram_id(user.telegram_id)
success_text = "✅ **Платеж успешно обработан!**\n\n"
success_text += f"⭐ Оплачено звезд: {star_payment.stars_amount}\n"
success_text += f"💰 Зачислено: {star_payment.rub_amount:.0f}\n"
success_text += f"💳 Текущий баланс: {updated_user.balance:.0f}\n\n"
success_text += "🎉 Средства уже доступны на вашем балансе!"
await message.answer(
success_text,
parse_mode='Markdown'
)
if bot:
try:
regular_payment = await db.create_payment(
user_id=user.telegram_id,
amount=star_payment.rub_amount,
payment_type='stars',
description=f'Пополнение через Telegram Stars ({star_payment.stars_amount} ⭐)',
status='completed'
)
await process_referral_rewards(
user.telegram_id,
star_payment.rub_amount,
regular_payment.id,
db,
bot,
payment_type='stars'
)
except Exception as ref_error:
logger.error(f"Error processing referral rewards for stars payment: {ref_error}")
log_user_action(user.telegram_id, "stars_payment_completed",
f"Stars: {star_payment.stars_amount}, Amount: {star_payment.rub_amount}")
else:
await message.answer("❌ Ошибка получения данных платежа")
else:
await message.answer("❌ Ошибка обработки платежа. Обратитесь в поддержку.")
logger.error(f"Failed to complete star payment {payment_id}")
except Exception as e:
logger.error(f"Error in successful_payment_handler: {e}")
await message.answer("❌ Произошла ошибка при обработке платежа. Обратитесь в поддержку.")
def get_stars_rate_info(stars_rates: Dict[int, float], lang: str = 'ru') -> str:
if not stars_rates:
return "Курсы не настроены"
text = "⭐ **Курсы Telegram Stars:**\n\n"
sorted_rates = sorted(stars_rates.items())
for stars, rubles in sorted_rates:
rate_per_star = rubles / stars
text += f"{stars} ⭐ = {rubles:.0f}₽ (курс: {rate_per_star:.2f}₽ за ⭐)\n"
return text
@stars_router.callback_query(F.data.startswith("cancel_star_payment_"))
async def cancel_star_payment_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
bot = kwargs.get('bot')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
payment_id = int(callback.data.split("_")[-1])
star_payment = await db.get_star_payment_by_id(payment_id)
if not star_payment:
await callback.answer("❌ Платеж не найден")
return
if star_payment.user_id != user.telegram_id:
await callback.answer("❌ Нет доступа к этому платежу")
return
if star_payment.status != 'pending':
await callback.answer("❌ Платеж уже обработан")
return
success = await db.cancel_star_payment(payment_id)
if success:
await callback.answer("✅ Платеж отменен", show_alert=True)
if bot:
invoice_deleted = False
try:
await bot.delete_message(
chat_id=callback.message.chat.id,
message_id=callback.message.message_id
)
invoice_deleted = True
logger.info(f"Deleted invoice message {callback.message.message_id} for cancelled payment {payment_id}")
except TelegramBadRequest as e:
logger.warning(f"Failed to delete invoice message: {e}")
except Exception as e:
logger.error(f"Unexpected error deleting invoice message: {e}")
if invoice_deleted:
try:
if config and config.STARS_RATES:
text = "❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\n"
text += "🚀 **Преимущества:**\n"
text += "• Мгновенное зачисление\n"
text += "• Безопасные платежи через Telegram\n"
text += "• Без комиссий и скрытых платежей\n\n"
text += "💎 **Доступные варианты:**\n"
sorted_rates = sorted(config.STARS_RATES.items())
for stars, rubles in sorted_rates:
if stars >= 500:
bonus_text = " 🔥 Выгодно!"
elif stars >= 250:
bonus_text = " 💎 Хорошо!"
else:
bonus_text = ""
text += f"{stars} ⭐ → {rubles:.0f}{bonus_text}\n"
text += f"\n💡 Выберите другой вариант:"
await bot.send_message(
chat_id=callback.message.chat.id,
text=text,
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
else:
await bot.send_message(
chat_id=callback.message.chat.id,
text="❌ Платеж отменен\n\n💰 Управление балансом:",
reply_markup=balance_keyboard(user.language)
)
except Exception as e:
logger.error(f"Failed to send menu after invoice deletion: {e}")
else:
try:
if config and config.STARS_RATES:
await callback.message.edit_text(
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
else:
await callback.message.edit_text(
"❌ Платеж отменен",
reply_markup=balance_keyboard(user.language)
)
except TelegramBadRequest:
try:
if config and config.STARS_RATES:
await bot.send_message(
chat_id=callback.message.chat.id,
text="❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
else:
await bot.send_message(
chat_id=callback.message.chat.id,
text="❌ Платеж отменен",
reply_markup=balance_keyboard(user.language)
)
except Exception as e:
logger.error(f"Failed to send fallback message: {e}")
else:
try:
if config and config.STARS_RATES:
await callback.message.edit_text(
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
else:
await callback.message.edit_text(
"❌ Платеж отменен",
reply_markup=balance_keyboard(user.language)
)
except TelegramBadRequest:
logger.warning("Failed to edit message when bot instance not available")
log_user_action(user.telegram_id, "stars_payment_cancelled", f"Payment ID: {payment_id}")
else:
await callback.answer("❌ Ошибка отмены платежа")
except (ValueError, IndexError) as e:
logger.error(f"Error parsing payment ID from callback: {e}")
await callback.answer("❌ Ошибка обработки")
except Exception as e:
logger.error(f"Error in cancel_star_payment_callback: {e}")
await callback.answer("❌ Произошла ошибка")
+130 -34
View File
@@ -86,7 +86,7 @@ class SubscriptionMonitorService:
logger.info("Subscription monitor service stopped")
async def _monitor_loop(self):
logger.info("🔄 Starting monitor loop")
logger.info("🔥 Starting monitor loop")
logger.info("⏰ Initial check in 10 seconds...")
await asyncio.sleep(10)
@@ -96,8 +96,10 @@ class SubscriptionMonitorService:
logger.info("🔍 Running periodic subscription check...")
warnings_sent = await self._check_expiring_subscriptions()
if warnings_sent > 0:
logger.info(f"✅ Monitor check completed: {warnings_sent} warnings sent")
trial_notifications = await self._check_expired_trial_subscriptions()
if warnings_sent > 0 or trial_notifications > 0:
logger.info(f"✅ Monitor check completed: {warnings_sent} warnings sent, {trial_notifications} trial notifications sent")
else:
logger.info("✅ Monitor check completed: no warnings needed")
@@ -147,12 +149,96 @@ class SubscriptionMonitorService:
break
except Exception as e:
logger.error(f"❌ Error in daily loop: {e}", exc_info=True)
await asyncio.sleep(3600) # Wait 1 hour before retry
await asyncio.sleep(3600)
async def _check_expired_trial_subscriptions(self) -> int:
try:
logger.info("🆓 Checking for expired trial subscriptions...")
notifications_sent = 0
now_utc = datetime.utcnow()
all_users = await self.db.get_all_users()
for user in all_users:
try:
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
for user_sub in user_subs:
try:
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if not subscription:
continue
if not subscription.is_trial:
continue
expires_at_utc = user_sub.expires_at
if expires_at_utc.tzinfo is None:
expires_at_utc = expires_at_utc.replace(tzinfo=None)
else:
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
time_diff = expires_at_utc - now_utc
hours_since_expiry = -time_diff.total_seconds() / 3600
if 1 <= hours_since_expiry <= 24 and user_sub.is_active:
logger.info(f"🆓 Sending trial expiry notification to user {user.telegram_id}: "
f"trial '{subscription.name}' expired {hours_since_expiry:.1f} hours ago")
try:
await self._send_trial_expiry_notification(user, subscription)
notifications_sent += 1
logger.info(f"✅ Trial expiry notification sent to user {user.telegram_id}")
except Exception as notification_error:
logger.error(f"❌ Failed to send trial notification to user {user.telegram_id}: {notification_error}")
except Exception as sub_error:
logger.error(f"❌ Error checking trial subscription {user_sub.id}: {sub_error}")
except Exception as user_error:
logger.error(f"❌ Error checking trial subscriptions for user {user.telegram_id}: {user_error}")
if notifications_sent > 0:
logger.info(f"🆓 Trial expiry check completed: {notifications_sent} notifications sent")
return notifications_sent
except Exception as e:
logger.error(f"❌ Critical error in check_expired_trial_subscriptions: {e}", exc_info=True)
return 0
async def _send_trial_expiry_notification(self, user, subscription):
try:
if not self.bot:
logger.error("❌ Bot instance is None, cannot send trial notification")
return
from translations import t
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
message = t('trial_subscription_expired', user.language, name=subscription.name)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=t('buy_subscription_btn', user.language),
callback_data="buy_subscription"
)],
[InlineKeyboardButton(
text=t('my_subscriptions_btn', user.language),
callback_data="my_subscriptions"
)]
])
await self.bot.send_message(user.telegram_id, message, reply_markup=keyboard)
logger.info(f"✅ Trial expiry notification sent to user {user.telegram_id} for subscription '{subscription.name}'")
except Exception as e:
logger.error(f"❌ Error sending trial expiry notification to user {user.telegram_id}: {e}", exc_info=True)
raise
async def delete_expired_trial_subscriptions(self, force: bool = False) -> Dict[str, Any]:
"""
Удаляет истекшие триальные подписки
"""
try:
logger.info("🗑️ Starting deletion of expired trial subscriptions...")
@@ -179,12 +265,10 @@ class SubscriptionMonitorService:
for user_sub in user_subs:
results['total_checked'] += 1
# Получаем информацию о подписке
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if not subscription or not subscription.is_trial:
continue
# Проверяем, истекла ли подписка достаточно давно
expires_at_utc = user_sub.expires_at
if expires_at_utc.tzinfo is None:
expires_at_utc = expires_at_utc.replace(tzinfo=None)
@@ -192,12 +276,11 @@ class SubscriptionMonitorService:
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
if expires_at_utc > cutoff_date and not force:
continue # Подписка еще не готова к удалению
continue
logger.info(f"🗑️ Deleting expired trial subscription '{subscription.name}' for user {user.telegram_id} "
f"(expired: {expires_at_utc}, cutoff: {cutoff_date})")
# Удаляем из RemnaWave API
api_deleted = False
if self.api and user_sub.short_uuid:
try:
@@ -213,7 +296,6 @@ class SubscriptionMonitorService:
results['errors'].append(f"API error for {user_sub.short_uuid}: {str(api_error)}")
logger.error(f"❌ API error deleting {user_sub.short_uuid}: {api_error}")
# Удаляем из базы данных
db_deleted = await self.db.delete_user_subscription(user_sub.id)
if db_deleted:
results['deleted_from_db'] += 1
@@ -248,9 +330,6 @@ class SubscriptionMonitorService:
}
async def delete_expired_regular_subscriptions(self, force: bool = False) -> Dict[str, Any]:
"""
Удаляет истекшие обычные подписки
"""
try:
logger.info("🗑️ Starting deletion of expired regular subscriptions...")
@@ -277,16 +356,13 @@ class SubscriptionMonitorService:
for user_sub in user_subs:
results['total_checked'] += 1
# Получаем информацию о подписке
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if not subscription or subscription.is_trial:
continue
# Пропускаем импортированные подписки (они не удаляются)
if getattr(subscription, 'is_imported', False) or subscription.name == "Старая подписка":
continue
# Проверяем, истекла ли подписка достаточно давно
expires_at_utc = user_sub.expires_at
if expires_at_utc.tzinfo is None:
expires_at_utc = expires_at_utc.replace(tzinfo=None)
@@ -294,12 +370,11 @@ class SubscriptionMonitorService:
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
if expires_at_utc > cutoff_date and not force:
continue # Подписка еще не готова к удалению
continue
logger.info(f"🗑️ Deleting expired regular subscription '{subscription.name}' for user {user.telegram_id} "
f"(expired: {expires_at_utc}, cutoff: {cutoff_date})")
# Удаляем из RemnaWave API
api_deleted = False
if self.api and user_sub.short_uuid:
try:
@@ -315,7 +390,6 @@ class SubscriptionMonitorService:
results['errors'].append(f"API error for {user_sub.short_uuid}: {str(api_error)}")
logger.error(f"❌ API error deleting {user_sub.short_uuid}: {api_error}")
# Удаляем из базы данных
db_deleted = await self.db.delete_user_subscription(user_sub.id)
if db_deleted:
results['deleted_from_db'] += 1
@@ -403,11 +477,11 @@ class SubscriptionMonitorService:
f"threshold={self.config.MONITOR_WARNING_DAYS}")
if subscription.is_trial:
logger.debug(f" Skipping trial subscription '{subscription.name}'")
logger.debug(f" Skipping trial subscription '{subscription.name}'")
continue
if getattr(subscription, 'is_imported', False) or subscription.name == "Старая подписка":
logger.debug(f" Skipping imported subscription '{subscription.name}'")
logger.debug(f" Skipping imported subscription '{subscription.name}'")
continue
should_warn = (
@@ -532,11 +606,14 @@ class SubscriptionMonitorService:
warnings_sent = await self._check_expiring_subscriptions()
logger.info(f"📢 Sent {warnings_sent} expiry warnings")
logger.info("🔄 Deactivating expired subscriptions...")
deactivated_count = await self.deactivate_expired_subscriptions()
logger.info(f"🔄 Deactivated {deactivated_count} expired subscriptions")
logger.info("🆓 Checking expired trial subscriptions...")
trial_notifications = await self._check_expired_trial_subscriptions()
logger.info(f"🆓 Sent {trial_notifications} trial expiry notifications")
logger.info("🔥 Deactivating expired subscriptions...")
deactivated_count = await self.deactivate_expired_subscriptions()
logger.info(f"🔥 Deactivated {deactivated_count} expired subscriptions")
# Автоматическое удаление, если включено в конфиге
deleted_trials = 0
deleted_regular = 0
if getattr(self.config, 'AUTO_DELETE_ENABLED', False):
@@ -554,7 +631,7 @@ class SubscriptionMonitorService:
await self._send_final_expiry_notifications()
logger.info("📩 Final notifications sent")
logger.info(f"✅ Daily check completed successfully. Warnings: {warnings_sent}, Deactivated: {deactivated_count}, "
logger.info(f"✅ Daily check completed successfully. Warnings: {warnings_sent}, Trial notifications: {trial_notifications}, Deactivated: {deactivated_count}, "
f"Deleted trials: {deleted_trials}, Deleted regular: {deleted_regular}")
return deactivated_count
@@ -606,7 +683,7 @@ class SubscriptionMonitorService:
user_data = await self.api.get_user_by_short_uuid(user_sub.short_uuid)
if user_data and user_data.get('uuid'):
await self.api.update_user(user_data['uuid'], {'status': 'EXPIRED'})
logger.debug(f"🔄 Also deactivated user {user_data['uuid']} in RemnaWave")
logger.debug(f"🔥 Also deactivated user {user_data['uuid']} in RemnaWave")
except Exception as api_error:
logger.warning(f"⚠️ Could not deactivate user in RemnaWave: {api_error}")
else:
@@ -720,11 +797,30 @@ class SubscriptionMonitorService:
days_left = int(hours_left / 24)
if subscription.is_trial:
results.append({
'success': True,
'message': f'Trial subscription "{subscription.name}" skipped (no warnings for trials)',
'error': None
})
hours_since_expiry = -hours_left
if 1 <= hours_since_expiry <= 24 and user_sub.is_active:
test_message = f"🧪 [ТЕСТОВОЕ УВЕДОМЛЕНИЕ]\n\n🆓 Ваша триальная подписка '{subscription.name}' истекла! Купите новый тариф чтобы продолжить использование VPN."
if self.bot:
try:
await self.bot.send_message(user_id, test_message)
results.append({
'success': True,
'message': f'✅ Sent test trial expiry notification for "{subscription.name}" (expired {hours_since_expiry:.1f} hours ago)',
'error': None
})
except Exception as send_error:
results.append({
'success': False,
'message': f'❌ Failed to send test trial notification for "{subscription.name}"',
'error': str(send_error)
})
else:
results.append({
'success': True,
'message': f'Trial subscription "{subscription.name}" - no notification needed (expired {hours_since_expiry:.1f} hours ago)',
'error': None
})
continue
if getattr(subscription, 'is_imported', False) or subscription.name == "Старая подписка":
+94 -4
View File
@@ -11,9 +11,9 @@ TRANSLATIONS = {
'admin_panel': 'Админ панель',
'trial_subscription': '🆓 Тестовая подписка',
'trial_not_available': '❌ Тестовая подписка недоступна',
'trial_success': '🎉 Тестовая подписка успешно активирована!\n\nТеперь вы можете найти её в разделе "Мои подписки".',
'trial_success': '🎉 Тестовая подписка успешно активирована!\n\nТеперь вы можете найти её в разделе: \n\n"Мои подписки"',
'trial_error': '❌ Ошибка при создании тестовой подписки',
'trial_info': '🧪 Тестовая подписка выдается на три дня!\n\nНа тарифе действует ограничение в 3 дня\n\nОграничение по трафику - 2гб',
'trial_info': 'Попробуйте наш сервис бесплатно! \n\n🧪 Тестовая подписка выдается на {days} дня!\n\nОграничение по трафику - {traffic} гб',
'subscriptions_list': '📋 Список подписок в продаже:',
'your_balance': '💰 Ваш баланс: {balance:.2f} руб.',
'topup_balance': 'Пополнить баланс',
@@ -97,6 +97,48 @@ TRANSLATIONS = {
'manage_balance': 'Управление балансом',
'manage_promocodes': 'Управление промокодами',
'statistics': 'Статистика',
'admin_user_subscriptions': 'Подписки пользователей',
'all_user_subscriptions': 'Все подписки пользователей',
'user_subscription_filters': 'Фильтры подписок',
'active_subscriptions': 'Активные подписки',
'expired_subscriptions': 'Истекшие подписки',
'expiring_subscriptions': 'Истекающие подписки',
'autopay_subscriptions': 'Подписки с автоплатежом',
'trial_subscriptions': 'Триальные подписки',
'imported_subscriptions': 'Импортированные подписки',
'subscription_details': 'Детали подписки',
'user_info': 'Информация о пользователе',
'subscription_info': 'Информация о подписке',
'subscription_status': 'Статус подписки',
'subscription_dates': 'Временные рамки',
'autopay_settings': 'Настройки автоплатежа',
'subscription_type': 'Тип подписки',
'edit_subscription': 'Редактировать подписку',
'toggle_status': 'Изменить статус',
'edit_expiry': 'Изменить срок',
'edit_autopay': 'Настроить автоплатеж',
'subscription_activated': 'Подписка активирована',
'subscription_deactivated': 'Подписка деактивирована',
'insufficient_funds': 'Недостаточно средств',
'funds_needed': 'Нужно еще',
'created_date': 'Дата создания',
'expires_date': 'Дата истечения',
'updated_date': 'Дата обновления',
'days_left': 'Дней осталось',
'autopay_enabled': 'Автоплатеж включен',
'autopay_disabled': 'Автоплатеж отключен',
'renew_before_days': 'Продлять за дней',
'view_details': 'Просмотр деталей',
'back_to_list': 'К списку',
'refresh_data': 'Обновить данные',
'no_subscriptions_found': 'Подписки не найдены',
'subscription_not_found': 'Подписка не найдена',
'database_unavailable': 'База данных недоступна',
'error_loading_subscriptions': 'Ошибка загрузки подписок',
'error_loading_details': 'Ошибка загрузки деталей',
'pagination_error': 'Ошибка навигации',
'edit_error': 'Ошибка редактирования',
'status_toggle_error': 'Ошибка изменения статуса',
'not_admin': '❌ У вас нет прав администратора',
'create_subscription': 'Создать подписку',
'enter_sub_name': 'Введите название подписки:',
@@ -152,7 +194,10 @@ TRANSLATIONS = {
'lucky_game_games_played': 'Игр сыграно: {count}',
'lucky_game_wins': 'Выигрышей: {count}',
'lucky_game_total_won': 'Всего выиграно: {amount}',
'lucky_game_win_rate': 'Процент побед: {rate}%'
'lucky_game_win_rate': 'Процент побед: {rate}%',
'trial_subscription_expired': '🆓 Ваша триальная подписка "{name}" истекла!\n\n'
'💡 Чтобы продолжить использование VPN, купите полный тариф.\n\n'
'✨ Доступны различные планы подписки с выгодными ценами!'
},
'en': {
@@ -204,6 +249,9 @@ TRANSLATIONS = {
'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',
'trial_subscription_expired': '🆓 Your trial subscription "{name}" has expired!\n\n'
'💡 To continue using VPN, please purchase a full plan.\n\n'
'✨ Various subscription plans with great prices are available!',
'my_subscriptions_btn': '📋 My Subscriptions',
'buy_new_subscription_btn': '🛒 Buy New Subscription',
'restore_subscription_btn': '🔄 Restore Subscription',
@@ -294,7 +342,49 @@ TRANSLATIONS = {
'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}%'
'lucky_game_win_rate': 'Win rate: {rate}%',
'admin_user_subscriptions': 'User Subscriptions',
'all_user_subscriptions': 'All User Subscriptions',
'user_subscription_filters': 'Subscription Filters',
'active_subscriptions': 'Active Subscriptions',
'expired_subscriptions': 'Expired Subscriptions',
'expiring_subscriptions': 'Expiring Subscriptions',
'autopay_subscriptions': 'Autopay Subscriptions',
'trial_subscriptions': 'Trial Subscriptions',
'imported_subscriptions': 'Imported Subscriptions',
'subscription_details': 'Subscription Details',
'user_info': 'User Information',
'subscription_info': 'Subscription Information',
'subscription_status': 'Subscription Status',
'subscription_dates': 'Time Frames',
'autopay_settings': 'Autopay Settings',
'subscription_type': 'Subscription Type',
'edit_subscription': 'Edit Subscription',
'toggle_status': 'Toggle Status',
'edit_expiry': 'Edit Expiry',
'edit_autopay': 'Configure Autopay',
'subscription_activated': 'Subscription activated',
'subscription_deactivated': 'Subscription deactivated',
'insufficient_funds': 'Insufficient funds',
'funds_needed': 'Need more',
'created_date': 'Created Date',
'expires_date': 'Expiry Date',
'updated_date': 'Updated Date',
'days_left': 'Days Left',
'autopay_enabled': 'Autopay Enabled',
'autopay_disabled': 'Autopay Disabled',
'renew_before_days': 'Renew Before Days',
'view_details': 'View Details',
'back_to_list': 'Back to List',
'refresh_data': 'Refresh Data',
'no_subscriptions_found': 'No subscriptions found',
'subscription_not_found': 'Subscription not found',
'database_unavailable': 'Database unavailable',
'error_loading_subscriptions': 'Error loading subscriptions',
'error_loading_details': 'Error loading details',
'pagination_error': 'Navigation error',
'edit_error': 'Edit error',
'status_toggle_error': 'Status toggle error'
}
}