Compare commits

...

61 Commits

Author SHA1 Message Date
Fr1ngg aa79cb670b Update .env.example 2025-08-10 10:30:33 +03:00
Fr1ngg f0106e15fa Create stars_handlers.py 2025-08-10 10:26:28 +03:00
Fr1ngg 1d194c6be2 Update .env.example 2025-08-10 10:18:03 +03:00
Fr1ngg 6917890007 Update admin_handlers.py 2025-08-10 10:15:34 +03:00
Fr1ngg 1c69de7ecd Update config.py 2025-08-10 10:13:14 +03:00
Fr1ngg 62188793d9 Update database.py 2025-08-10 10:12:21 +03:00
Fr1ngg 585ee2d42b Update handlers.py 2025-08-10 10:11:12 +03:00
Fr1ngg 2a58cfd45c Update keyboards.py 2025-08-10 10:10:54 +03:00
Fr1ngg 93003c5fe0 Update main.py 2025-08-10 10:10:27 +03:00
Fr1ngg 91b0787d72 Update README.md 2025-08-10 07:04:17 +03:00
Fr1ngg 315277d4b0 Create bedolaga.sh 2025-08-10 06:56:04 +03:00
Fr1ngg 1821c0d89d Delete run.sh 2025-08-10 06:54:40 +03:00
Fr1ngg 5650da0c5f Update README.md 2025-08-10 03:56:42 +03:00
Fr1ngg 0ca8b73cef Update .env.example 2025-08-10 03:51:10 +03:00
Fr1ngg 6bab26ab27 Update lucky_game.py 2025-08-10 03:44:57 +03:00
Fr1ngg a72243e7fa Update keyboards.py 2025-08-10 03:42:56 +03:00
Fr1ngg 5bd86d9cf7 Update config.py 2025-08-10 03:40:36 +03:00
Fr1ngg 00fd3fbadb Update database.py 2025-08-10 03:39:51 +03:00
Fr1ngg b806f40797 Update middlewares.py 2025-08-10 03:38:20 +03:00
Fr1ngg c82aa1d253 Update main.py 2025-08-10 03:37:51 +03:00
Fr1ngg 071919f0db Update remnawave_api.py 2025-08-10 03:36:51 +03:00
Fr1ngg ab41e96ba5 Update subscription_monitor.py 2025-08-10 03:34:52 +03:00
Fr1ngg 95a470ba7b Update admin_handlers.py 2025-08-10 03:32:41 +03:00
Fr1ngg 1e0653ed33 Update referral_utils.py 2025-08-10 03:27:15 +03:00
Fr1ngg 3f0df08031 Update handlers.py 2025-08-10 03:25:53 +03:00
Fr1ngg 8c3bc8a5ba Update utils.py 2025-08-10 03:22:42 +03:00
Fr1ngg 3a5a8f48e4 Update README.md 2025-08-09 11:31:02 +03:00
Fr1ngg f4b99ffb09 Rename logo.svg to logo2.svg 2025-08-09 11:30:57 +03:00
Fr1ngg 0c6afdebc3 Update README.md 2025-08-09 11:30:17 +03:00
Fr1ngg 98b382ba2f Update logo.svg 2025-08-09 11:29:57 +03:00
Fr1ngg 54a24385b1 Update README.md 2025-08-09 08:54:57 +03:00
Fr1ngg 3eeb84cdae Update README.md 2025-08-09 08:29:02 +03:00
Fr1ngg 204a10ae14 Update README.md 2025-08-09 08:26:02 +03:00
Fr1ngg ba1dd7547e Create logo.svg 2025-08-09 08:24:25 +03:00
Fr1ngg 238585c257 Update .gitignore 2025-08-09 08:24:02 +03:00
Fr1ngg d8ebe1ad13 Add files via upload 2025-08-09 08:18:01 +03:00
Fr1ngg 819df3e93a Create 1 2025-08-09 08:13:36 +03:00
Fr1ngg 99e46feafa Delete screens/sub/1 2025-08-09 08:07:05 +03:00
Fr1ngg a432c8ab54 Add files via upload 2025-08-09 08:06:52 +03:00
Fr1ngg d740b7560c Create 1 2025-08-09 08:06:17 +03:00
Fr1ngg 91c4c73d7e Delete screens/Balance/1 2025-08-09 08:05:33 +03:00
Fr1ngg 4ce2384f27 Update handlers.py 2025-08-09 08:04:43 +03:00
Fr1ngg 02a6289d56 Add files via upload 2025-08-09 07:57:18 +03:00
Fr1ngg 52ae6eba81 Create 1 2025-08-09 07:55:29 +03:00
Fr1ngg 080b6e74a2 Add files via upload 2025-08-09 07:54:08 +03:00
Fr1ngg ad7d32023c Delete screens/Luckgame/1 2025-08-09 07:53:17 +03:00
Fr1ngg 1baf8fbe37 Add files via upload 2025-08-09 07:53:03 +03:00
Fr1ngg 758ca6a373 Create 1 2025-08-09 07:52:38 +03:00
Fr1ngg 65504d0a23 Delete screens/1 2025-08-09 07:50:15 +03:00
Fr1ngg 0409f4e2e2 Delete screens/Рефералы/1 2025-08-09 07:49:57 +03:00
Fr1ngg 8ca63e939a Add files via upload 2025-08-09 07:49:38 +03:00
Fr1ngg 25a621fd51 Delete screens/Главное меню/1 2025-08-09 07:43:58 +03:00
Fr1ngg 3f71a8aff7 Rename Снимок экрана 2025-08-09 в 07.42.12.png to Mainmenu.png 2025-08-09 07:43:45 +03:00
Fr1ngg 6ec81c37b9 Add files via upload 2025-08-09 07:42:41 +03:00
Fr1ngg 8a9ff98da8 Create 1 2025-08-09 07:40:31 +03:00
Fr1ngg bb7e29eeac Create 1 2025-08-09 07:40:05 +03:00
Fr1ngg 27d8783104 Create 1 2025-08-09 07:39:34 +03:00
Fr1ngg 9a01e6db43 Create .gitignore 2025-08-09 07:37:50 +03:00
Fr1ngg b341ddf9d9 Update README.md 2025-08-09 07:33:26 +03:00
Fr1ngg 3c8d0ac859 Update README.md 2025-08-09 07:33:03 +03:00
Fr1ngg 3f529c0dee Update README.md 2025-08-09 07:19:13 +03:00
48 changed files with 3147 additions and 773 deletions
+33 -8
View File
@@ -19,18 +19,43 @@ ADMIN_IDS=123445567,12342123
# Support Configuration
SUPPORT_USERNAME=support
TRIAL_ENABLED=true
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_GB=2
TRIAL_SQUAD_UUID=
TRIAL_PRICE=0.0
TRIAL_ENABLED=true # Включить триал
TRIAL_DURATION_DAYS=3 # Дней триала
TRIAL_TRAFFIC_GB=2 # Лимит трафика у триал подписки
TRIAL_SQUAD_UUID= # UUID сквада из панели remnawave /dashboard/management/internal-squads
TRIAL_PRICE=0.0 # Оставить 0!
# Monitor Service Settings (дополнительные настройки)
MONITOR_CHECK_INTERVAL=3600
MONITOR_DAILY_CHECK_HOUR=10
MONITOR_WARNING_DAYS=2
MONITOR_CHECK_INTERVAL=21600 # Промежуток проверики (3600 - будет раз в час проверять и слать уведомления)
MONITOR_DAILY_CHECK_HOUR=10 # Ежедневный чек в 10 утра
MONITOR_WARNING_DAYS=2 # За сколько начинать до окончания подписки слать уведомления
DELETE_EXPIRED_TRIAL_DAYS=1 # Через сколько дней после истечения удалять триал подписки
DELETE_EXPIRED_REGULAR_DAYS=7 # Через сколько дней после истечения удалять обычные подписки
AUTO_DELETE_ENABLED=true # Включить автоматическое удаление при ежедневной проверке
LUCKY_GAME_ENABLED=true # Включить/выключить игру удачи
LUCKY_GAME_REWARD=50.0 # Размер награды за выигрыш в рублях
LUCKY_GAME_NUMBERS=30 # Общее количество чисел для выбора (1-30)
LUCKY_GAME_WINNING_COUNT=25 # Количество выигрышных чисел из общего количества
# ================================
# TELEGRAM STARS НАСТРОЙКИ
# ================================
# Включить/выключить пополнение через Telegram Stars
STARS_ENABLED=true
# Курсы обмена звезд на рубли
# Формат: STARS_{количество_звезд}_RATE={рубли}
# 100 звёзд
STARS_100_RATE=110
# 150 звёзд
STARS_150_RATE=165
# 250 звёзд
STARS_250_RATE=280
# 350 звёзд
STARS_350_RATE=355
# 500 звёзд
STARS_500_RATE=550
# 750 звёзд
STARS_750_RATE=800
# 1000 звёзд
STARS_1000_RATE=1000
+2
View File
@@ -0,0 +1,2 @@
/screens
/assets
+168 -5
View File
@@ -5,6 +5,8 @@
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-blue?logo=postgresql)](https://postgresql.org)
[![License](https://img.shields.io/badge/License-MIT-green)](LICENSE)
![RemnaWave Bedolaga Bot Logo](./assets/logo2.svg)
Современный Telegram бот для управления VPN подписками через RemnaWave API с полным функционалом управления пользователями, платежами и администрированием.
## 🚀 Особенности
@@ -27,7 +29,7 @@
- 🖥 **Мониторинг системы** - состояние нод, пользователей RemnaWave
- 🔄 **Синхронизация данных** - импорт из RemnaWave
- 📨 **Рассылки** - уведомления пользователям
- 🔍 **Мониторинг подписок** - автоматические уведомления об истечении
- 🔍 **Мониторинг подписок** - автоматические уведомления об истечении и зачистка подписок с истекщим сроком для триал и обычных подписок
## 📋 Требования
@@ -46,8 +48,8 @@
### 1. Клонирование репозитория
```bash
git clone https://github.com/your-repo/remnawave-telegram-bot.git
cd remnawave-telegram-bot
git clone https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot.git
cd remnawave-bedolaga-telegram-bot
```
### 2. Настройка переменных окружения
@@ -68,6 +70,7 @@ nano .env
| `BOT_TOKEN` | Токен Telegram бота | `123456:ABC-DEF1234ghIkl-zyx` |
| `BOT_USERNAME` | Username бота (без @) | `your_bot_username` |
| `REMNAWAVE_URL` | URL панели RemnaWave | `https://panel.example.com` |
| `REMNAWAVE_MODE` | Тип подключения | `remote/local` |
| `REMNAWAVE_TOKEN` | API токен RemnaWave | `your_api_token` |
| `ADMIN_IDS` | ID администраторов (через запятую) | `123456789,987654321` |
@@ -78,7 +81,7 @@ nano .env
| `REFERRAL_FIRST_REWARD` | Награда за первого реферала | `100.0` |
| `REFERRAL_REFERRED_BONUS` | Бонус приглашенному | `100.0` |
| `REFERRAL_THRESHOLD` | Порог активации (руб.) | `200.0` |
| `REFERRAL_PERCENTAGE` | Процент с платежей | `0.2` (20%) |
| `REFERRAL_PERCENTAGE` | Процент со второго и послед. платежей | `0.2` (20%) |
#### 🎰 Игра удачи
@@ -93,11 +96,168 @@ nano .env
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `TRIAL_ENABLED` | Включить тестовую подписку | `true` |
| `TRIAL_ENABLED` | Включить тестовую подписку | `true/false` |
| `TRIAL_DURATION_DAYS` | Длительность (дни) | `3` |
| `TRIAL_TRAFFIC_GB` | Лимит трафика (ГБ) | `2` |
| `TRIAL_SQUAD_UUID` | UUID squad для тестовых | `uuid-here` |
#### 💬 Сервис мониторинга сообщений
| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `MONITOR_ENABLED` | Включить сервис подписку | `true/false` |
| `MONITOR_CHECK_INTERVAL` | Интервал проверки в секундах (1800 = 30 минут) (будет срабатывать через этот промежуток автоматически и слать уведомления) | `1800` |
| `MONITOR_DAILY_CHECK_HOUR` | Час ежедневной проверки (0-23, по умолчанию 10:00) | `10` |
| `DELETE_EXPIRED_TRIAL_DAYS` | Через сколько дней после истечения удалять триал подписки | `1` |
| `DELETE_EXPIRED_REGULAR_DAYS=7` | Через сколько дней после истечения удалять обычняе подписки | `7` |
| `MONITOR_WARNING_DAYS` | За сколько дней предупреждать (по умолчанию 3) | `3` |
| `AUTO_DELETE_ENABLED` | Включить автоматическое удаление при ежедневной проверке(true/false) | `true` |
### Максимально быстрый старт через скрипт управления ботом
# 🚀 Быстрая установка RemnaWave Bedolaga Bot
Автоматический установщик для Ubuntu сервера с полным управлением через интерактивное меню.
#### 📋 Требования
- **Ubuntu 18.04+** (рекомендуется Ubuntu 20.04 или 22.04)
- **Root доступ** или sudo права
- **Интернет соединение** для загрузки Docker образов
#### ⚡ Установка одной командой
##### Способ 1: Скачать и запустить
```bash
wget https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/raw/main/bedolaga.sh
chmod +x bedolaga.sh
sudo ./bedolaga.sh
```
##### Способ 2: Прямой запуск
```bash
curl -sSL https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/raw/main/bedolaga.sh | sudo bash
```
##### Способ 3: Клонирование репозитория
```bash
git clone https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot.git
cd remnawave-bedolaga-telegram-bot
sudo ./bedolaga.sh
```
#### 🔧 Что делает установщик
1. **Обновляет систему Ubuntu**
2. **Устанавливает Docker** и необходимые зависимости
3. **Создает структуру проекта** в `/opt/bedolaga-bot`
4. **Настраивает Docker Compose** (минимальная или расширенная конфигурация)
5. **Интерактивно настраивает .env файл** с параметрами бота
6. **Создает systemd службу** (опционально)
7. **Запускает интерактивное меню управления**
#### 📱 Интерактивное меню управления
После установки вы получите удобное меню для управления ботом:
##### Когда бот запущен:
- 🛑 Выключить бот
- 🔄 Перезапустить бот
- 📺 Посмотреть логи в реальном времени
- ⬆️ Обновить бот
- 📋 Посмотреть логи
- 💾 Создать резервную копию БД
- ♻️ Восстановить базу данных
- ✏️ Редактировать .env файл
- 🩺 Диагностика базы данных
- 🗑️ Удалить базу данных
- ❌ Полностью удалить бота
##### Когда бот остановлен:
- ▶️ Запустить бот
- ⬆️ Обновить бот
- 📋 Посмотреть логи
- 💾 Создать резервную копию БД
- ♻️ Восстановить базу данных
- ✏️ Редактировать .env файл
- 🩺 Диагностика базы данных
- 🗑️ Удалить базы данных
- ❌ Полностью удалить бота
#### 🔧 Ручное управление через командную строку
```bash
# Перейти в папку проекта
cd /opt/bedolaga-bot
# Управление через Docker Compose
docker compose up -d # Запустить
docker compose down # Остановить
docker compose restart # Перезапустить
docker compose logs -f bot # Логи в реальном времени
# Управление через systemd (если служба создана)
sudo systemctl start bedolaga-bot
sudo systemctl stop bedolaga-bot
sudo systemctl restart bedolaga-bot
sudo systemctl status bedolaga-bot
```
#### 📂 Структура установки
```
/opt/bedolaga-bot/
├── docker-compose.yml # Конфигурация контейнеров
├── .env # Настройки бота
├── logs/ # Логи бота
├── data/ # Данные бота
└── backup_*.sql # Резервные копии БД
```
#### 🔄 Повторный запуск установщика
Если бот уже установлен, скрипт автоматически перейдет в режим управления без переустановки:
```bash
sudo ./bedolaga.sh
```
#### ❓ Решение проблем
### Бот не запускается
1. Проверьте логи: `docker compose logs bot`
2. Используйте диагностику в меню установщика
3. Убедитесь что все параметры в `.env` корректны
##### Проблемы с базой данных
1. Запустите диагностику базы данных из меню
2. Проверьте логи PostgreSQL: `docker compose logs postgres`
3. При необходимости создайте новую базу (удалите старую через меню)
##### Проблемы с Docker
```bash
# Перезапуск Docker
sudo systemctl restart docker
# Очистка неиспользуемых ресурсов
docker system prune -f
```
#### 📝 Дополнительные команды
```bash
# Обновление только бота (без остановки БД)
docker compose pull bot && docker compose up -d bot
# Резервная копия БД вручную
docker compose exec postgres pg_dump -U remnawave_user -d remnawave_bot > backup.sql
# Просмотр использования ресурсов
docker stats
# Просмотр всех volumes
docker volume ls
```
### 4. Docker Compose конфигурация
#### 🚀 Минимальная конфигурация (рекомендуется)
@@ -514,6 +674,9 @@ docker-compose ps
| 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 | Дабы не плодить лишние записи в базе бота и панели ремны, да и в целом это логично)( |
### 🚀 Хотите помочь?
+506 -34
View File
@@ -7,7 +7,7 @@ from datetime import datetime, timedelta, timezone
import logging
from typing import List, Dict
from database import Database, User
from database import Database, User, ReferralProgram, ReferralEarning
from remnawave_api import RemnaWaveAPI
from keyboards import *
from translations import t
@@ -51,7 +51,6 @@ logger = logging.getLogger(__name__)
admin_router = Router()
# Admin panel access check
async def check_admin_access(callback: CallbackQuery, user: User) -> bool:
"""Check if user has admin access"""
if not user.is_admin:
@@ -59,7 +58,6 @@ async def check_admin_access(callback: CallbackQuery, user: User) -> bool:
return False
return True
# Admin panel main menu
@admin_router.callback_query(F.data == "admin_panel")
async def admin_panel_callback(callback: CallbackQuery, user: User, **kwargs):
"""Show admin panel"""
@@ -71,7 +69,6 @@ async def admin_panel_callback(callback: CallbackQuery, user: User, **kwargs):
reply_markup=admin_menu_keyboard(user.language)
)
# Statistics
@admin_router.callback_query(F.data == "admin_stats")
async def admin_stats_callback(callback: CallbackQuery, user: User, db: Database, api: RemnaWaveAPI = None, **kwargs):
"""Show statistics"""
@@ -79,10 +76,8 @@ async def admin_stats_callback(callback: CallbackQuery, user: User, db: Database
return
try:
# Get database stats
db_stats = await db.get_stats()
# Get RemnaWave system stats (optional)
system_stats = None
nodes_stats = None
@@ -124,10 +119,8 @@ async def admin_stats_callback(callback: CallbackQuery, user: User, db: Database
reply_markup=back_keyboard("admin_panel", user.language)
)
# Subscription management
@admin_router.callback_query(F.data == "admin_subscriptions")
async def admin_subscriptions_callback(callback: CallbackQuery, user: User, **kwargs):
"""Show subscription management"""
if not await check_admin_access(callback, user):
return
@@ -577,7 +570,6 @@ async def delete_subscription_confirm(callback: CallbackQuery, user: User, **kwa
@admin_router.callback_query(F.data.startswith("confirm_delete_sub_"))
async def delete_subscription(callback: CallbackQuery, user: User, db: Database, **kwargs):
"""Delete subscription"""
if not await check_admin_access(callback, user):
return
@@ -640,7 +632,6 @@ async def list_users_callback(callback: CallbackQuery, user: User, db: Database,
text = t('user_list', user.language) + "\n\n"
# Show first 20 users
for u in users[:20]:
username = u.username or "N/A"
text += t('user_item', user.language,
@@ -661,7 +652,6 @@ async def list_users_callback(callback: CallbackQuery, user: User, db: Database,
logger.error(f"Error listing users: {e}")
await callback.answer(t('error_occurred', user.language))
# Balance management
@admin_router.callback_query(F.data == "admin_balance")
async def admin_balance_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
@@ -674,7 +664,6 @@ async def admin_balance_callback(callback: CallbackQuery, user: User, **kwargs):
@admin_router.callback_query(F.data == "admin_add_balance")
async def admin_add_balance_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
"""Start adding balance to user"""
if not await check_admin_access(callback, user):
return
@@ -686,7 +675,6 @@ async def admin_add_balance_callback(callback: CallbackQuery, user: User, state:
@admin_router.message(StateFilter(BotStates.admin_add_balance_user))
async def handle_balance_user_id(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
"""Handle user ID input for balance addition"""
telegram_id = parse_telegram_id(message.text)
if not telegram_id:
@@ -708,7 +696,6 @@ async def handle_balance_user_id(message: Message, state: FSMContext, user: User
@admin_router.message(StateFilter(BotStates.admin_add_balance_amount))
async def handle_balance_amount(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
"""Handle balance amount input"""
is_valid, amount = is_valid_amount(message.text)
if not is_valid:
@@ -719,19 +706,27 @@ async def handle_balance_amount(message: Message, state: FSMContext, user: User,
target_user_id = data['target_user_id']
try:
# Add balance
success = await db.add_balance(target_user_id, amount)
if success:
# Create payment record
await db.create_payment(
payment = await db.create_payment(
user_id=target_user_id,
amount=amount,
payment_type='admin_topup',
payment_type='admin_topup',
description=f'Пополнение администратором (ID: {user.telegram_id})',
status='completed'
)
bot = kwargs.get('bot')
await process_referral_rewards(
target_user_id,
amount,
payment.id,
db,
bot,
payment_type='admin_topup'
)
await message.answer(
t('balance_added', user.language),
reply_markup=admin_menu_keyboard(user.language)
@@ -867,7 +862,6 @@ async def payment_history_page_callback(callback: CallbackQuery, user: User, db:
async def noop_callback(callback: CallbackQuery, **kwargs):
await callback.answer()
# Payment approval handlers
@admin_router.callback_query(F.data.startswith("approve_payment_"))
async def approve_payment(callback: CallbackQuery, user: User, db: Database, **kwargs):
if not await check_admin_access(callback, user):
@@ -892,7 +886,14 @@ async def approve_payment(callback: CallbackQuery, user: User, db: Database, **k
await db.update_payment(payment)
bot = kwargs.get('bot')
await process_referral_rewards(payment.user_id, payment.amount, payment.id, db, bot)
await process_referral_rewards(
payment.user_id,
payment.amount,
payment.id,
db,
bot,
payment_type=payment.payment_type
)
await callback.message.edit_text(
f"✅ Платеж одобрен!\n💰 Пользователю {payment.user_id} добавлено {payment.amount} руб."
@@ -955,7 +956,6 @@ async def reject_payment(callback: CallbackQuery, user: User, db: Database, **kw
logger.error(f"Error rejecting payment: {e}")
await callback.answer(t('error_occurred', user.language))
# Promocode management
@admin_router.callback_query(F.data == "admin_promocodes")
async def admin_promocodes_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
@@ -1809,19 +1809,46 @@ async def monitor_status_callback(callback: CallbackQuery, user: User, **kwargs)
try:
status = await monitor_service.get_service_status()
status_text = "🔍 Статус сервиса мониторинга:\n\n"
status_text += f"🟢 Работает: {'Да' if status['is_running'] else 'Нет'}\n"
status_text += f"⏱ Интервал проверки: {status['check_interval']} сек\n"
status_text += f"🕙 Время ежедневной проверки: {status['daily_check_hour']}:00\n"
status_text += f"⚠️ Предупреждение за: {status['warning_days']} дней\n"
status_text = "🔍 **Статус сервиса мониторинга:**\n\n"
if status['is_running']:
status_text += "✅ **Статус:** Работает\n"
else:
status_text += "❌ **Статус:** Остановлен\n"
status_text += f"⚙️ **Настройки:**\n"
status_text += f"• Включен: {'' if status['monitor_enabled'] else ''}\n"
status_text += f"• Интервал проверки: {status['check_interval']} сек\n"
status_text += f"• Ежедневная проверка: {status['daily_check_hour']}:00\n"
status_text += f"• Предупреждение за: {status['warning_days']} дн.\n\n"
status_text += f"🗑️ **Настройки удаления:**\n"
status_text += f"• Удалять триальные через: {status['delete_trial_days']} дн.\n"
status_text += f"• Удалять обычные через: {status['delete_regular_days']} дн.\n"
status_text += f"• Автоудаление: {'' if status['auto_delete_enabled'] else ''}\n\n"
status_text += f"📊 **Состояние задач:**\n"
status_text += f"• Мониторинг: {status['task_status']['monitor_task']}\n"
status_text += f"• Ежедневная: {status['task_status']['daily_task']}\n"
if status['last_check']:
status_text += f"🕐 Последняя проверка: {status['last_check']}"
status_text += f"\n⏰ **Последняя проверка:** {status['last_check']}"
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
await callback.message.edit_text(
status_text,
reply_markup=back_keyboard("admin_monitor", user.language)
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="🧪 Тест пользователя", callback_data="monitor_test_user"),
InlineKeyboardButton(text="🚀 Принудительная проверка", callback_data="monitor_force_check")
],
[
InlineKeyboardButton(text="🗑️ Удалить истекшие триальные", callback_data="delete_expired_trials"),
InlineKeyboardButton(text="🗑️ Удалить истекшие обычные", callback_data="delete_expired_regular")
],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_monitor")]
])
await callback.message.edit_text(status_text, reply_markup=keyboard)
except Exception as e:
logger.error(f"Error getting monitor status: {e}")
@@ -1830,6 +1857,197 @@ async def monitor_status_callback(callback: CallbackQuery, user: User, **kwargs)
reply_markup=back_keyboard("admin_monitor", user.language)
)
@admin_router.callback_query(F.data == "delete_expired_trials")
async def delete_expired_trials_handler(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
monitor_service = kwargs.get('monitor_service')
if not monitor_service:
await callback.answer("❌ Сервис мониторинга недоступен", show_alert=True)
return
try:
text = "⚠️ **УДАЛЕНИЕ ИСТЕКШИХ ТРИАЛЬНЫХ ПОДПИСОК**\n\n"
text += "Вы собираетесь удалить все истекшие триальные подписки.\n\n"
text += "🗑️ **Что будет удалено:**\n"
text += f"• Триальные подписки, истекшие более {getattr(monitor_service.config, 'DELETE_EXPIRED_TRIAL_DAYS', 1)} дн. назад\n"
text += "• Данные будут удалены из базы бота\n"
text += "• Пользователи будут удалены из панели RemnaWave\n\n"
text += "❗ **Это действие НЕОБРАТИМО!**\n\n"
text += "Продолжить?"
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да, удалить", callback_data="confirm_delete_trials"),
InlineKeyboardButton(text="❌ Отмена", callback_data="monitor_status")
]
])
await callback.message.edit_text(text, reply_markup=keyboard)
await callback.answer()
except Exception as e:
logger.error(f"Error in delete_expired_trials_handler: {e}")
await callback.answer("❌ Ошибка", show_alert=True)
@admin_router.callback_query(F.data == "confirm_delete_trials")
async def confirm_delete_trials_handler(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
monitor_service = kwargs.get('monitor_service')
if not monitor_service:
await callback.answer("❌ Сервис мониторинга недоступен", show_alert=True)
return
try:
processing_text = "🗑️ **Удаление истекших триальных подписок...**\n\n"
processing_text += "⏳ Поиск и удаление подписок...\n"
processing_text += "Пожалуйста, подождите..."
await callback.message.edit_text(processing_text)
await callback.answer("🗑️ Начинаю удаление триальных подписок...")
result = await monitor_service.delete_expired_trial_subscriptions(force=False)
text = "🗑️ **Удаление триальных подписок завершено**\n\n"
text += f"📊 **Результаты:**\n"
text += f"• Проверено: {result['total_checked']}\n"
text += f"• Удалено из БД: {result['deleted_from_db']}\n"
text += f"• Удалено из API: {result['deleted_from_api']}\n"
text += f"• Ошибки: {len(result['errors'])}\n\n"
if result['deleted_subscriptions']:
text += f"✅ **Удаленные подписки:**\n"
for sub in result['deleted_subscriptions'][:10]:
text += f"{sub['subscription_name']} (пользователь {sub['user_id']})\n"
if len(result['deleted_subscriptions']) > 10:
text += f"• ... и еще {len(result['deleted_subscriptions']) - 10}\n"
if result['errors']:
text += f"\n❌ **Ошибки:**\n"
for error in result['errors'][:5]:
text += f"{error}\n"
if len(result['errors']) > 5:
text += f"• ... и еще {len(result['errors']) - 5}\n"
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 К статусу мониторинга", callback_data="monitor_status")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
except Exception as e:
logger.error(f"Error in confirm_delete_trials_handler: {e}")
error_text = f"❌ **Ошибка удаления триальных подписок**\n\n{str(e)}"
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 К статусу мониторинга", callback_data="monitor_status")]
])
await callback.message.edit_text(error_text, reply_markup=keyboard)
@admin_router.callback_query(F.data == "delete_expired_regular")
async def delete_expired_regular_handler(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
monitor_service = kwargs.get('monitor_service')
if not monitor_service:
await callback.answer("❌ Сервис мониторинга недоступен", show_alert=True)
return
try:
text = "⚠️ **УДАЛЕНИЕ ИСТЕКШИХ ОБЫЧНЫХ ПОДПИСОК**\n\n"
text += "Вы собираетесь удалить все истекшие обычные подписки.\n\n"
text += "🗑️ **Что будет удалено:**\n"
text += f"• Обычные подписки, истекшие более {getattr(monitor_service.config, 'DELETE_EXPIRED_REGULAR_DAYS', 7)} дн. назад\n"
text += "• Данные будут удалены из базы бота\n"
text += "• Пользователи будут удалены из панели RemnaWave\n"
text += "• Импортированные подписки НЕ затрагиваются\n\n"
text += "❗ **Это действие НЕОБРАТИМО!**\n\n"
text += "Продолжить?"
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да, удалить", callback_data="confirm_delete_regular"),
InlineKeyboardButton(text="❌ Отмена", callback_data="monitor_status")
]
])
await callback.message.edit_text(text, reply_markup=keyboard)
await callback.answer()
except Exception as e:
logger.error(f"Error in delete_expired_regular_handler: {e}")
await callback.answer("❌ Ошибка", show_alert=True)
@admin_router.callback_query(F.data == "confirm_delete_regular")
async def confirm_delete_regular_handler(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
return
monitor_service = kwargs.get('monitor_service')
if not monitor_service:
await callback.answer("❌ Сервис мониторинга недоступен", show_alert=True)
return
try:
processing_text = "🗑️ **Удаление истекших обычных подписок...**\n\n"
processing_text += "⏳ Поиск и удаление подписок...\n"
processing_text += "Пожалуйста, подождите..."
await callback.message.edit_text(processing_text)
await callback.answer("🗑️ Начинаю удаление обычных подписок...")
result = await monitor_service.delete_expired_regular_subscriptions(force=False)
text = "🗑️ **Удаление обычных подписок завершено**\n\n"
text += f"📊 **Результаты:**\n"
text += f"• Проверено: {result['total_checked']}\n"
text += f"• Удалено из БД: {result['deleted_from_db']}\n"
text += f"• Удалено из API: {result['deleted_from_api']}\n"
text += f"• Ошибки: {len(result['errors'])}\n\n"
if result['deleted_subscriptions']:
text += f"✅ **Удаленные подписки:**\n"
for sub in result['deleted_subscriptions'][:10]:
text += f"{sub['subscription_name']} (пользователь {sub['user_id']})\n"
if len(result['deleted_subscriptions']) > 10:
text += f"• ... и еще {len(result['deleted_subscriptions']) - 10}\n"
if result['errors']:
text += f"\n❌ **Ошибки:**\n"
for error in result['errors'][:5]:
text += f"{error}\n"
if len(result['errors']) > 5:
text += f"• ... и еще {len(result['errors']) - 5}\n"
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 К статусу мониторинга", callback_data="monitor_status")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
except Exception as e:
logger.error(f"Error in confirm_delete_regular_handler: {e}")
error_text = f"❌ **Ошибка удаления обычных подписок**\n\n{str(e)}"
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔙 К статусу мониторинга", callback_data="monitor_status")]
])
await callback.message.edit_text(error_text, reply_markup=keyboard)
@admin_router.callback_query(F.data == "monitor_force_check")
async def monitor_force_check_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
@@ -2053,7 +2271,6 @@ async def handle_broadcast_message(message: Message, state: FSMContext, user: Us
await state.clear()
# System management handlers
@admin_router.callback_query(F.data == "admin_system")
async def admin_system_callback(callback: CallbackQuery, user: User, **kwargs):
if not await check_admin_access(callback, user):
@@ -5796,7 +6013,7 @@ async def view_imported_plans_callback(callback: CallbackQuery, user: User, db:
if getattr(plan, 'is_imported', False):
imported_plans.append(plan)
elif plan.is_trial:
continue # Пропускаем триальные
continue
elif (plan.name.startswith(('Import_', 'Auto_', 'Imported_')) or
(plan.price == 0 and any(keyword in plan.name.lower() for keyword in
['импорт', 'default', 'squad', 'user_']))):
@@ -6068,6 +6285,8 @@ async def referral_statistics_callback(callback: CallbackQuery, user: User, db:
async with db.session_factory() as session:
from sqlalchemy import select, func, and_
from database import ReferralProgram, ReferralEarning
total_referrals = await session.execute(
select(func.count(ReferralProgram.id)).where(
and_(
@@ -6184,6 +6403,8 @@ async def list_referrers_callback(callback: CallbackQuery, user: User, db: Datab
async with db.session_factory() as session:
from sqlalchemy import select, func, and_, case
from database import ReferralProgram
top_referrers = await session.execute(
select(
ReferralProgram.referrer_id,
@@ -6215,7 +6436,6 @@ async def list_referrers_callback(callback: CallbackQuery, user: User, db: Datab
referrer = await db.get_user_by_telegram_id(referrer_id)
if referrer:
# Формируем имя
display_name = ""
if referrer.first_name:
display_name = referrer.first_name[:15]
@@ -6263,6 +6483,8 @@ async def referral_payments_callback(callback: CallbackQuery, user: User, db: Da
async with db.session_factory() as session:
from sqlalchemy import select, desc
from database import ReferralEarning
recent_earnings = await session.execute(
select(ReferralEarning)
.order_by(desc(ReferralEarning.created_at))
@@ -6377,3 +6599,253 @@ async def referral_settings_callback(callback: CallbackQuery, user: User, **kwar
"❌ Ошибка получения настроек",
reply_markup=back_keyboard("admin_referrals", user.language)
)
@admin_router.callback_query(F.data == "admin_stars_payments")
async def admin_stars_payments_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
"""Управление платежами через Stars"""
if not await check_admin_access(callback, user):
return
await callback.message.edit_text(
"⭐ Управление Telegram Stars платежами",
reply_markup=admin_stars_keyboard(user.language)
)
def admin_stars_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
"""Клавиатура управления Stars"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📊 Статистика Stars", callback_data="admin_stars_stats")],
[InlineKeyboardButton(text="📋 Последние платежи", callback_data="admin_stars_recent")],
[InlineKeyboardButton(text="⚙️ Настройки курсов", callback_data="admin_stars_settings")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_balance")]
])
return keyboard
@admin_router.callback_query(F.data == "admin_stars_stats")
async def admin_stars_stats_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
"""Статистика Stars платежей"""
if not await check_admin_access(callback, user):
return
try:
async with db.session_factory() as session:
from sqlalchemy import text
stats_query = await session.execute(text("""
SELECT
COUNT(*) as total_payments,
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_payments,
COUNT(CASE WHEN status = 'pending' THEN 1 END) as pending_payments,
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) as cancelled_payments,
SUM(CASE WHEN status = 'completed' THEN stars_amount ELSE 0 END) as total_stars,
SUM(CASE WHEN status = 'completed' THEN rub_amount ELSE 0 END) as total_rubles
FROM star_payments
"""))
stats = stats_query.fetchone()
daily_query = await session.execute(text("""
SELECT
DATE(created_at) as payment_date,
COUNT(*) as daily_count,
SUM(CASE WHEN status = 'completed' THEN rub_amount ELSE 0 END) as daily_amount
FROM star_payments
WHERE created_at >= (CURRENT_DATE - INTERVAL '7 days')
GROUP BY DATE(created_at)
ORDER BY payment_date DESC
"""))
daily_stats = daily_query.fetchall()
text = "📊 Статистика Telegram Stars\n\n"
if stats:
text += "💫 Общая статистика:\n"
text += f"• Всего платежей: {stats.total_payments or 0}\n"
text += f"• Завершенных: {stats.completed_payments or 0}\n"
text += f"В ожидании: {stats.pending_payments or 0}\n"
text += f"• Отмененных: {stats.cancelled_payments or 0}\n\n"
text += f"💰 Финансовая статистика:\n"
text += f"• Всего звезд получено: {stats.total_stars or 0}\n"
text += f"• Общая сумма: {stats.total_rubles or 0:.0f}\n\n"
if stats.total_payments and stats.total_payments > 0:
conversion = (stats.completed_payments or 0) / stats.total_payments * 100
text += f"📈 Конверсия: {conversion:.1f}%\n\n"
if daily_stats:
text += "📅 За последние 7 дней:\n"
for day in daily_stats:
date_str = day.payment_date.strftime('%d.%m')
text += f"{date_str}: {day.daily_count} платежей на {day.daily_amount:.0f}\n"
else:
text += "📅 За последние 7 дней: нет данных\n"
text += f"\n🕐 Обновлено: {format_datetime(datetime.now(), user.language)}"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_stars_stats")],
[InlineKeyboardButton(text="📋 Последние платежи", callback_data="admin_stars_recent")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_stars_payments")]
])
await callback.message.edit_text(
text,
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Error getting stars stats: {e}")
try:
await callback.message.edit_text(
"❌ Ошибка получения статистики",
reply_markup=admin_stars_keyboard(user.language)
)
except Exception as edit_error:
await callback.answer("❌ Ошибка получения статистики", show_alert=True)
@admin_router.callback_query(F.data == "admin_stars_recent")
async def admin_stars_recent_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
"""Последние Stars платежи"""
if not await check_admin_access(callback, user):
return
try:
async with db.session_factory() as session:
from sqlalchemy import select, desc
from database import StarPayment, User
query = select(
StarPayment,
User.username,
User.first_name
).outerjoin(
User, StarPayment.user_id == User.telegram_id
).order_by(
desc(StarPayment.created_at)
).limit(15)
result = await session.execute(query)
payments_data = result.fetchall()
if not payments_data:
text = "📋 История Stars платежей пуста"
else:
text = f"📋 Последние Stars платежи ({len(payments_data)}):\n\n"
for row in payments_data:
payment = row[0]
username = row[1]
first_name = row[2]
if payment.status == 'completed':
status_emoji = ""
elif payment.status == 'pending':
status_emoji = ""
elif payment.status == 'cancelled':
status_emoji = ""
else:
status_emoji = ""
user_name = "Unknown"
if first_name:
user_name = first_name.replace('*', '').replace('_', '').replace('[', '').replace(']', '').replace('`', '')
if username:
clean_username = username.replace('*', '').replace('_', '').replace('[', '').replace(']', '').replace('`', '')
if user_name != "Unknown":
user_name += f" (@{clean_username})"
else:
user_name = f"@{clean_username}"
payment_date = payment.completed_at if payment.completed_at else payment.created_at
date_str = payment_date.strftime('%d.%m %H:%M')
text += f"{status_emoji} {payment.stars_amount} ⭐ → {payment.rub_amount:.0f}\n"
text += f" 👤 {user_name} (ID: {payment.user_id})\n"
text += f" 📅 {date_str}\n"
if payment.telegram_payment_charge_id:
charge_short = payment.telegram_payment_charge_id[:20] + "..."
text += f" 🧾 {charge_short}\n"
text += "\n"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Обновить", callback_data="admin_stars_recent")],
[InlineKeyboardButton(text="📊 Статистика", callback_data="admin_stars_stats")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_stars_payments")]
])
await callback.message.edit_text(
text,
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Error getting recent stars payments: {e}")
try:
await callback.message.edit_text(
"❌ Ошибка получения платежей",
reply_markup=admin_stars_keyboard(user.language)
)
except Exception as edit_error:
await callback.answer("❌ Ошибка получения платежей", show_alert=True)
@admin_router.callback_query(F.data == "admin_stars_settings")
async def admin_stars_settings_callback(callback: CallbackQuery, user: User, **kwargs):
"""Настройки курсов Stars"""
if not await check_admin_access(callback, user):
return
config = kwargs.get('config')
text = "⚙️ Настройки Telegram Stars\n\n"
if config and config.STARS_ENABLED:
text += "✅ Статус: Включено\n\n"
if config.STARS_RATES:
text += "💱 Текущие курсы:\n"
sorted_rates = sorted(config.STARS_RATES.items())
for stars, rubles in sorted_rates:
rate_per_star = rubles / stars
text += f"{stars} ⭐ = {rubles:.0f}₽ (курс: {rate_per_star:.2f}₽/⭐)\n"
text += "\n📈 Анализ выгодности:\n"
base_rate = sorted_rates[0][1] / sorted_rates[0][0] if sorted_rates else 0
for stars, rubles in sorted_rates:
current_rate = rubles / stars
if current_rate < base_rate:
savings = (base_rate - current_rate) / base_rate * 100
text += f"{stars} ⭐: выгода {savings:.1f}%\n"
else:
text += "❌ Курсы не настроены\n"
else:
text += "❌ Статус: Отключено\n"
text += "\n⚙️ Настройка через .env файл:\n"
text += "\nSTARS_ENABLED=true\n"
text += "STARS_100_RATE=150\n"
text += "STARS_150_RATE=220\n"
text += "STARS_250_RATE=400\n"
text += "STARS_350_RATE=500\n"
text += "STARS_500_RATE=800\n"
text += "STARS_750_RATE=1150\n"
text += "STARS_1000_RATE=1500\n"
text += "\n💡 Рекомендации:\n"
text += "• Большие пакеты должны быть выгоднее\n"
text += "• Курс должен покрывать комиссии Telegram\n"
text += "• Регулярно анализируйте конверсию\n"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📊 Статистика", callback_data="admin_stars_stats")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_stars_payments")]
])
await callback.message.edit_text(
text,
reply_markup=keyboard
)
+295
View File
@@ -0,0 +1,295 @@
<svg viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Градиенты -->
<linearGradient id="mainGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
<stop offset="50%" style="stop-color:#764ba2;stop-opacity:1" />
<stop offset="100%" style="stop-color:#f093fb;stop-opacity:1" />
</linearGradient>
<linearGradient id="skinGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#d4a574;stop-opacity:1" />
<stop offset="100%" style="stop-color:#b8935f;stop-opacity:1" />
</linearGradient>
<linearGradient id="redSkinGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#e8b598;stop-opacity:1" />
<stop offset="100%" style="stop-color:#d4a574;stop-opacity:1" />
</linearGradient>
<linearGradient id="clothesGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3a3a3a;stop-opacity:1" />
<stop offset="100%" style="stop-color:#595959;stop-opacity:1" />
</linearGradient>
<linearGradient id="bottleGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2d5016;stop-opacity:0.8" />
<stop offset="100%" style="stop-color:#1a2f0d;stop-opacity:0.9" />
</linearGradient>
<!-- Фильтры -->
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="dropshadow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="4"/>
<feOffset dx="2" dy="4" result="offset" />
<feComponentTransfer>
<feFuncA type="linear" slope="0.3"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Фон -->
<rect width="400" height="400" fill="#1a1a2e" rx="50"/>
<!-- Декоративные элементы фона -->
<circle cx="50" cy="80" r="30" fill="url(#mainGradient)" opacity="0.1"/>
<circle cx="350" cy="120" r="40" fill="#8B4513" opacity="0.1"/>
<!-- Волны RemnaWave в фоне -->
<g transform="translate(200,200)" opacity="0.2">
<path d="M-120,-100 Q-60,-130 0,-100 Q60,-70 120,-100"
stroke="url(#mainGradient)"
stroke-width="4"
fill="none">
<animate attributeName="d"
values="M-120,-100 Q-60,-130 0,-100 Q60,-70 120,-100;M-120,-100 Q-60,-70 0,-100 Q60,-130 120,-100;M-120,-100 Q-60,-130 0,-100 Q60,-70 120,-100"
dur="4s"
repeatCount="indefinite"/>
</path>
</g>
<!-- Главный персонаж - Бомж-алкаш -->
<g transform="translate(200,180)" filter="url(#dropshadow)">
<!-- Тело в рваной куртке -->
<ellipse cx="0" cy="30" rx="45" ry="60" fill="url(#clothesGradient)"/>
<!-- Рваные края одежды -->
<path d="M-45,70 L-40,75 L-35,70 L-30,75 L-25,70 L-20,75 L-15,70 L-10,75 L-5,70 L0,75 L5,70 L10,75 L15,70 L20,75 L25,70 L30,75 L35,70 L40,75 L45,70"
stroke="none" fill="#2a2a2a"/>
<!-- Заплатки -->
<rect x="-20" y="10" width="12" height="10" fill="#5a4a3a" opacity="0.7" rx="2"/>
<rect x="15" y="35" width="10" height="8" fill="#4a4a4a" opacity="0.8" rx="1"/>
<rect x="-10" y="45" width="8" height="12" fill="#6a5a4a" opacity="0.6" rx="1"/>
<!-- Шея -->
<rect x="-12" y="-25" width="24" height="20" fill="url(#redSkinGradient)" rx="8"/>
<!-- Голова (более человечная форма) -->
<ellipse cx="0" cy="-45" rx="28" ry="32" fill="url(#redSkinGradient)"/>
<!-- Сальные волосы -->
<path d="M-25,-70 Q-20,-75 -15,-72 Q-10,-78 -5,-73 Q0,-80 5,-73 Q10,-78 15,-72 Q20,-75 25,-70"
stroke="#3a3a3a" stroke-width="4" fill="#3a3a3a" stroke-linecap="round"/>
<path d="M-28,-65 Q-25,-70 -20,-67" stroke="#3a3a3a" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M28,-65 Q25,-70 20,-67" stroke="#3a3a3a" stroke-width="3" fill="none" stroke-linecap="round"/>
<!-- Левое ухо -->
<ellipse cx="-28" cy="-45" rx="6" ry="10" fill="url(#skinGradient)"/>
<!-- Правое ухо -->
<ellipse cx="28" cy="-45" rx="6" ry="10" fill="url(#skinGradient)"/>
<!-- Опухшие глаза -->
<ellipse cx="-10" cy="-50" rx="8" ry="5" fill="white"/>
<ellipse cx="10" cy="-50" rx="8" ry="5" fill="white"/>
<!-- Зрачки (косящие) -->
<g id="eyes">
<circle cx="-8" cy="-49" r="3" fill="#333">
<animate attributeName="cx" values="-8;-10;-8" dur="3s" repeatCount="indefinite"/>
</circle>
<circle cx="12" cy="-49" r="3" fill="#333">
<animate attributeName="cx" values="12;10;12" dur="3s" repeatCount="indefinite"/>
</circle>
</g>
<!-- Красные глаза -->
<path d="M-18,-50 L-15,-51" stroke="red" stroke-width="1" opacity="0.7"/>
<path d="M-17,-48 L-14,-49" stroke="red" stroke-width="0.5" opacity="0.7"/>
<path d="M18,-50 L15,-51" stroke="red" stroke-width="1" opacity="0.7"/>
<path d="M17,-48 L14,-49" stroke="red" stroke-width="0.5" opacity="0.7"/>
<!-- Мешки под глазами -->
<ellipse cx="-10" cy="-44" rx="8" ry="3" fill="#8a6a4a" opacity="0.5"/>
<ellipse cx="10" cy="-44" rx="8" ry="3" fill="#8a6a4a" opacity="0.5"/>
<!-- Красный нос алкаша -->
<ellipse cx="0" cy="-38" rx="6" ry="5" fill="#d67070"/>
<circle cx="-2" cy="-39" r="1" fill="#e08080" opacity="0.8"/>
<!-- Небритость -->
<g opacity="0.6">
<circle cx="-15" cy="-30" r="1" fill="#555"/>
<circle cx="-12" cy="-28" r="0.8" fill="#555"/>
<circle cx="-8" cy="-30" r="0.8" fill="#555"/>
<circle cx="15" cy="-30" r="1" fill="#555"/>
<circle cx="12" cy="-28" r="0.8" fill="#555"/>
<circle cx="8" cy="-30" r="0.8" fill="#555"/>
<circle cx="0" cy="-28" r="0.8" fill="#555"/>
<circle cx="-5" cy="-26" r="0.8" fill="#555"/>
<circle cx="5" cy="-26" r="0.8" fill="#555"/>
</g>
<!-- Рот с сигаретой/бутылкой (анимированный) -->
<g id="mouth-group">
<!-- Основной рот -->
<path d="M-12,-20 Q0,-18 12,-20" stroke="#444" stroke-width="2" fill="none" stroke-linecap="round"/>
<!-- Сигарета (появляется и исчезает) -->
<g id="cigarette">
<rect x="10" y="-22" width="25" height="3" fill="white" rx="1">
<animate attributeName="opacity" values="0;0;0;1;1;1;0;0;0" dur="8s" repeatCount="indefinite"/>
</rect>
<rect x="30" y="-22" width="5" height="3" fill="#ff6b35">
<animate attributeName="opacity" values="0;0;0;1;1;1;0;0;0" dur="8s" repeatCount="indefinite"/>
</rect>
<!-- Дым от сигареты -->
<g opacity="0.4">
<circle cx="35" cy="-22" r="2" fill="#999">
<animate attributeName="opacity" values="0;0;0;0.6;0.3;0;0;0;0" dur="8s" repeatCount="indefinite"/>
<animate attributeName="cy" values="-22;-22;-22;-28;-35;-40;-22;-22;-22" dur="8s" repeatCount="indefinite"/>
<animate attributeName="r" values="2;2;2;4;6;8;2;2;2" dur="8s" repeatCount="indefinite"/>
</circle>
</g>
</g>
</g>
<!-- Левая рука с бутылкой -->
<g id="left-arm">
<!-- Рука -->
<line x1="-45" y1="10" x2="-55" y2="-5" stroke="url(#skinGradient)" stroke-width="8" stroke-linecap="round">
<animate attributeName="x2" values="-55;-50;-55" dur="8s" repeatCount="indefinite"/>
<animate attributeName="y2" values="-5;-20;-5" dur="8s" repeatCount="indefinite"/>
</line>
<!-- Кисть -->
<circle cx="-55" cy="-5" r="6" fill="url(#skinGradient)">
<animate attributeName="cx" values="-55;-50;-55" dur="8s" repeatCount="indefinite"/>
<animate attributeName="cy" values="-5;-20;-5" dur="8s" repeatCount="indefinite"/>
</circle>
<!-- Бутылка -->
<g id="bottle">
<rect x="-62" y="-25" width="14" height="30" fill="url(#bottleGradient)" rx="2">
<animate attributeName="x" values="-62;-57;-62" dur="8s" repeatCount="indefinite"/>
<animate attributeName="y" values="-25;-40;-25" dur="8s" repeatCount="indefinite"/>
<animate attributeName="transform" values="rotate(0 -55 -10);rotate(-30 -50 -25);rotate(0 -55 -10)" dur="8s" repeatCount="indefinite"/>
</rect>
<rect x="-58" y="-30" width="6" height="8" fill="#2d5016">
<animate attributeName="x" values="-58;-53;-58" dur="8s" repeatCount="indefinite"/>
<animate attributeName="y" values="-30;-45;-30" dur="8s" repeatCount="indefinite"/>
</rect>
<!-- Этикетка -->
<rect x="-60" y="-15" width="10" height="8" fill="white" opacity="0.3">
<animate attributeName="x" values="-60;-55;-60" dur="8s" repeatCount="indefinite"/>
<animate attributeName="y" values="-15;-30;-15" dur="8s" repeatCount="indefinite"/>
</rect>
</g>
</g>
<!-- Правая рука (свободная, дрожащая) -->
<g id="right-arm">
<line x1="45" y1="10" x2="55" y2="20" stroke="url(#skinGradient)" stroke-width="8" stroke-linecap="round">
<animate attributeName="x2" values="55;57;55;53;55" dur="2s" repeatCount="indefinite"/>
</line>
<circle cx="55" cy="20" r="6" fill="url(#skinGradient)">
<animate attributeName="cx" values="55;57;55;53;55" dur="2s" repeatCount="indefinite"/>
</circle>
</g>
<!-- Ноги в рваных штанах -->
<rect x="-15" y="85" width="12" height="35" fill="#4a4a5a" rx="2"/>
<rect x="3" y="85" width="12" height="35" fill="#4a4a5a" rx="2"/>
<!-- Дырки в штанах -->
<ellipse cx="-9" cy="95" rx="3" ry="4" fill="#1a1a2e"/>
<ellipse cx="9" cy="105" rx="2" ry="3" fill="#1a1a2e"/>
<!-- Старые ботинки -->
<ellipse cx="-9" cy="125" rx="10" ry="6" fill="#2a2a2a"/>
<ellipse cx="9" cy="125" rx="10" ry="6" fill="#2a2a2a"/>
<!-- Дырки в ботинках -->
<circle cx="-6" cy="123" r="2" fill="#1a1a2e"/>
<circle cx="12" cy="123" r="1.5" fill="#1a1a2e"/>
<!-- Значок "B" на груди -->
<circle cx="0" cy="25" r="20" fill="url(#mainGradient)" opacity="0.8" stroke="white" stroke-width="1"/>
<text x="0" y="35"
font-family="Arial Black, sans-serif"
font-size="28"
font-weight="bold"
text-anchor="middle"
fill="white">
B
</text>
</g>
<!-- Упавшие бутылки вокруг -->
<g opacity="0.6">
<rect x="80" y="340" width="8" height="20" fill="#2d5016" rx="1" transform="rotate(75 84 350)"/>
<rect x="310" y="330" width="7" height="18" fill="#3a5a1a" rx="1" transform="rotate(-60 313 339)"/>
<rect x="150" y="350" width="6" height="16" fill="#2d5016" rx="1" transform="rotate(45 153 358)"/>
</g>
<!-- Окурки -->
<g opacity="0.7">
<rect x="120" y="360" width="12" height="2" fill="white" rx="1"/>
<rect x="130" y="360" width="2" height="2" fill="#ff6b35"/>
<rect x="250" y="355" width="10" height="2" fill="white" rx="1" transform="rotate(30 255 356)"/>
<rect x="258" y="355" width="2" height="2" fill="#ff6b35" transform="rotate(30 259 356)"/>
</g>
<!-- Текст логотипа -->
<text x="200" y="320"
font-family="Arial Black, sans-serif"
font-size="20"
font-weight="bold"
text-anchor="middle"
fill="url(#mainGradient)"
filter="url(#glow)">
RemnaWave
</text>
<text x="200" y="345"
font-family="Arial Black, sans-serif"
font-size="18"
font-weight="bold"
text-anchor="middle"
fill="#d67070">
Bedolaga Bot
</text>
<!-- Подпись -->
<text x="200" y="365"
font-family="Arial, sans-serif"
font-size="11"
text-anchor="middle"
fill="#8892b0"
opacity="0.8">
Telegram VPN Manager
</text>
<!-- Мухи летающие вокруг (признак антисанитарии) -->
<g opacity="0.5">
<circle cx="150" cy="150" r="1" fill="#333">
<animateTransform attributeName="transform" type="rotate" values="0 200 180;360 200 180" dur="6s" repeatCount="indefinite"/>
</circle>
<circle cx="250" cy="150" r="1" fill="#333">
<animateTransform attributeName="transform" type="rotate" values="360 200 180;0 200 180" dur="5s" repeatCount="indefinite"/>
</circle>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

+965
View File
@@ -0,0 +1,965 @@
#!/bin/bash
# Цвета для вывода
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
BOT_DIR="/opt/bedolaga-bot"
COMPOSE_FILE="$BOT_DIR/docker-compose.yml"
ENV_FILE="$BOT_DIR/.env"
SERVICE_FILE="/etc/systemd/system/bedolaga-bot.service"
# Функция для логирования
log() {
echo -e "${GREEN}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Проверка является ли пользователь root
check_root() {
if [ "$EUID" -ne 0 ]; then
error "Этот скрипт должен быть запущен от имени root (используйте sudo)"
exit 1
fi
}
# Проверка установки бота
check_installation() {
if [ -d "$BOT_DIR" ] && [ -f "$COMPOSE_FILE" ] && [ -f "$ENV_FILE" ]; then
return 0
else
return 1
fi
}
# Обновление системы
update_system() {
log "Обновление системы Ubuntu..."
apt update && apt upgrade -y
log "Система обновлена успешно"
}
# Установка Docker
install_docker() {
log "Установка Docker..."
# Удаление старых версий
apt remove -y docker docker-engine docker.io containerd runc
# Установка зависимостей
apt install -y apt-transport-https ca-certificates curl gnupg lsb-release nano
# Добавление GPG ключа Docker
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Добавление репозитория
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
# Установка Docker
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Запуск Docker
systemctl enable docker
systemctl start docker
log "Docker установлен успешно"
}
# Проверка и установка nano
ensure_nano() {
if ! command -v nano &> /dev/null; then
log "Установка nano..."
apt update
apt install -y nano
log "Nano установлен успешно"
fi
}
# Создание папки и структуры проекта
create_project_structure() {
log "Создание структуры проекта..."
mkdir -p "$BOT_DIR"
mkdir -p "$BOT_DIR/logs"
mkdir -p "$BOT_DIR/data"
log "Структура проекта создана в $BOT_DIR"
}
# Создание docker-compose.yml
create_docker_compose() {
log "Создание docker-compose.yml..."
echo "Выберите конфигурацию Docker Compose:"
echo "1) Минимальная (рекомендуется) - только PostgreSQL и бот"
echo "2) Расширенная - с Redis и Nginx"
while true; do
read -p "Ваш выбор (1-2): " choice
case $choice in
1)
create_minimal_compose
break
;;
2)
create_full_compose
break
;;
*)
error "Неверный выбор. Попробуйте снова."
;;
esac
done
}
create_minimal_compose() {
cat > "$COMPOSE_FILE" << 'EOF'
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: remnawave_bot_db
restart: unless-stopped
environment:
POSTGRES_DB: remnawave_bot
POSTGRES_USER: remnawave_user
POSTGRES_PASSWORD: secure_password_123
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U remnawave_user -d remnawave_bot"]
interval: 15s
timeout: 10s
retries: 5
start_period: 30s
# RemnaWave Bot
bot:
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
container_name: remnawave_bot
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
env_file:
- .env
environment:
DATABASE_URL: postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
volumes:
- ./logs:/app/logs
- ./data:/app/data
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "python -c 'print(\"Bot is running\")'"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
postgres_data:
driver: local
networks:
bot_network:
driver: bridge
EOF
log "Минимальная конфигурация Docker Compose создана"
}
create_full_compose() {
cat > "$COMPOSE_FILE" << 'EOF'
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: remnawave_bot_db
restart: unless-stopped
environment:
POSTGRES_DB: remnawave_bot
POSTGRES_USER: remnawave_user
POSTGRES_PASSWORD: secure_password_123
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C"
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U remnawave_user -d remnawave_bot"]
interval: 15s
timeout: 10s
retries: 5
start_period: 30s
# RemnaWave Bot
bot:
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
container_name: remnawave_bot
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
env_file:
- .env
environment:
DATABASE_URL: postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot
volumes:
- ./logs:/app/logs
- ./data:/app/data
networks:
- bot_network
healthcheck:
test: ["CMD-SHELL", "python -c 'print(\"Bot is running\")'"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
# Redis (для кэширования и улучшения производительности)
redis:
image: redis:7-alpine
container_name: remnawave_bot_redis
restart: unless-stopped
command: redis-server --appendonly yes --requirepass redis_password_123
volumes:
- redis_data:/data
ports:
- "6379:6379"
networks:
- bot_network
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 10s
timeout: 3s
retries: 5
profiles:
- with-redis
# Nginx (для статических файлов или веб-интерфейса)
nginx:
image: nginx:alpine
container_name: remnawave_bot_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
- ./static:/usr/share/nginx/html:ro
networks:
- bot_network
depends_on:
- bot
profiles:
- with-nginx
volumes:
postgres_data:
driver: local
redis_data:
driver: local
networks:
bot_network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
EOF
log "Расширенная конфигурация Docker Compose создана"
}
# Создание .env файла
create_env_file() {
log "Настройка .env файла..."
# Основные настройки бота
read -p "Введите BOT_TOKEN: " BOT_TOKEN
read -p "Введите BOT_USERNAME (без @): " BOT_USERNAME
read -p "Введите ADMIN_IDS (через запятую): " ADMIN_IDS
# Настройки RemnaWave
echo "Выберите режим RemnaWave:"
echo "1) remote"
echo "2) local"
while true; do
read -p "Ваш выбор (1-2): " remna_choice
case $remna_choice in
1)
REMNAWAVE_MODE="remote"
break
;;
2)
REMNAWAVE_MODE="local"
break
;;
*)
error "Неверный выбор. Попробуйте снова."
;;
esac
done
read -p "Введите REMNAWAVE_URL: " REMNAWAVE_URL
read -p "Введите REMNAWAVE_TOKEN: " REMNAWAVE_TOKEN
# Настройки триала
while true; do
read -p "Включить триал? (y/n): " trial_enabled
case $trial_enabled in
[Yy]*)
TRIAL_ENABLED="true"
read -p "Введите TRIAL_DURATION_DAYS: " TRIAL_DURATION_DAYS
read -p "Введите TRIAL_TRAFFIC_GB: " TRIAL_TRAFFIC_GB
read -p "Введите TRIAL_SQUAD_UUID: " TRIAL_SQUAD_UUID
break
;;
[Nn]*)
TRIAL_ENABLED="false"
TRIAL_DURATION_DAYS=""
TRIAL_TRAFFIC_GB=""
TRIAL_SQUAD_UUID=""
break
;;
*)
error "Пожалуйста, ответьте y или n."
;;
esac
done
# Настройки реферальной системы
read -p "Введите REFERRAL_FIRST_REWARD (сумму с .0 на конце): " REFERRAL_FIRST_REWARD
read -p "Введите REFERRAL_REFERRED_BONUS (сумму с .0 на конце): " REFERRAL_REFERRED_BONUS
read -p "Введите REFERRAL_THRESHOLD (сумму с .0 на конце): " REFERRAL_THRESHOLD
read -p "Введите REFERRAL_PERCENTAGE (с 0. в начале): " REFERRAL_PERCENTAGE
# Настройки мониторинга
read -p "Введите DELETE_EXPIRED_TRIAL_DAYS: " DELETE_EXPIRED_TRIAL_DAYS
read -p "Введите DELETE_EXPIRED_REGULAR_DAYS: " DELETE_EXPIRED_REGULAR_DAYS
# Создание .env файла
cat > "$ENV_FILE" << EOF
BOT_TOKEN=$BOT_TOKEN
BOT_USERNAME=$BOT_USERNAME
ADMIN_IDS=$ADMIN_IDS
REMNAWAVE_MODE=$REMNAWAVE_MODE
REMNAWAVE_URL=$REMNAWAVE_URL
REMNAWAVE_TOKEN=$REMNAWAVE_TOKEN
TRIAL_ENABLED=$TRIAL_ENABLED
EOF
if [ "$TRIAL_ENABLED" = "true" ]; then
cat >> "$ENV_FILE" << EOF
TRIAL_DURATION_DAYS=$TRIAL_DURATION_DAYS
TRIAL_TRAFFIC_GB=$TRIAL_TRAFFIC_GB
TRIAL_SQUAD_UUID=$TRIAL_SQUAD_UUID
EOF
fi
cat >> "$ENV_FILE" << EOF
TRIAL_PRICE=0.0
REFERRAL_FIRST_REWARD=$REFERRAL_FIRST_REWARD
REFERRAL_REFERRED_BONUS=$REFERRAL_REFERRED_BONUS
REFERRAL_THRESHOLD=$REFERRAL_THRESHOLD
REFERRAL_PERCENTAGE=$REFERRAL_PERCENTAGE
# Monitor Service Settings
MONITOR_ENABLED=true
MONITOR_CHECK_INTERVAL=21600
MONITOR_DAILY_CHECK_HOUR=12
MONITOR_WARNING_DAYS=2
DELETE_EXPIRED_TRIAL_DAYS=$DELETE_EXPIRED_TRIAL_DAYS
DELETE_EXPIRED_REGULAR_DAYS=$DELETE_EXPIRED_REGULAR_DAYS
AUTO_DELETE_ENABLED=true
LUCKY_GAME_ENABLED=true
LUCKY_GAME_REWARD=50.0
LUCKY_GAME_NUMBERS=30
LUCKY_GAME_WINNING_COUNT=5
EOF
log ".env файл создан успешно"
}
# Создание службы systemd
create_service() {
while true; do
read -p "Создать службу для запуска бота? (y/n): " create_service_choice
case $create_service_choice in
[Yy]*)
log "Создание службы systemd..."
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=RemnaWave Bedolaga Bot
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=$BOT_DIR
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable bedolaga-bot.service
log "Служба bedolaga-bot создана и включена"
break
;;
[Nn]*)
log "Служба не будет создана"
break
;;
*)
error "Пожалуйста, ответьте y или n."
;;
esac
done
}
# Проверка статуса бота
check_bot_status() {
if docker compose -f "$COMPOSE_FILE" ps --services --filter "status=running" | grep -q "bot"; then
return 0 # Запущен
else
return 1 # Не запущен
fi
}
# Проверка подключения к RemnaWave API
check_remnawave_connection() {
if [ -f "$ENV_FILE" ]; then
source "$ENV_FILE"
if [ ! -z "$REMNAWAVE_URL" ]; then
if curl -s --connect-timeout 5 "$REMNAWAVE_URL/api/auth/status" > /dev/null; then
return 0 # Подключен
else
return 1 # Не подключен
fi
else
return 1
fi
else
return 1
fi
}
# Запуск бота
start_bot() {
log "Запуск бота..."
cd "$BOT_DIR"
docker compose up -d
log "Бот запущен"
}
# Остановка бота
stop_bot() {
log "Остановка бота..."
cd "$BOT_DIR"
docker compose down
log "Бот остановлен"
}
# Перезапуск бота
restart_bot() {
log "Перезапуск бота..."
cd "$BOT_DIR"
docker compose restart
log "Бот перезапущен"
}
# Обновление бота
update_bot() {
log "Обновление бота..."
cd "$BOT_DIR"
docker compose down
docker compose pull bot
docker compose up -d
log "Бот обновлен"
}
# Просмотр логов
view_logs() {
cd "$BOT_DIR"
docker compose logs bot
}
# Просмотр логов в реальном времени
view_live_logs() {
cd "$BOT_DIR"
docker compose logs -f bot
}
# Создание резервной копии базы данных
backup_database() {
log "Создание резервной копии базы данных..."
cd "$BOT_DIR"
# Проверяем, запущен ли контейнер postgres
if ! docker compose ps postgres | grep -q "Up"; then
log "Контейнер PostgreSQL не запущен. Запускаем PostgreSQL..."
docker compose up -d postgres
# Ждем готовности базы данных
log "Ожидание готовности базы данных..."
for i in {1..30}; do
if docker compose exec postgres pg_isready -U remnawave_user -d remnawave_bot &>/dev/null; then
log "PostgreSQL готов к работе"
break
fi
if [ $i -eq 30 ]; then
error "PostgreSQL не запустился в течение 60 секунд"
return 1
fi
sleep 2
echo -n "."
done
echo ""
fi
# Проверяем доступность базы данных
log "Проверка подключения к базе данных..."
if ! docker compose exec postgres pg_isready -U remnawave_user -d remnawave_bot &>/dev/null; then
error "База данных недоступна. Проверьте логи: docker compose logs postgres"
return 1
fi
BACKUP_FILE="$BOT_DIR/backup_$(date +%Y%m%d_%H%M%S).sql"
log "Создание дампа базы данных..."
# Используем docker compose exec без -T и с правильным выводом
if docker compose exec postgres pg_dump -U remnawave_user -d remnawave_bot --verbose --no-owner --no-privileges > "$BACKUP_FILE" 2>/dev/null; then
# Проверяем размер созданного файла
if [ -s "$BACKUP_FILE" ]; then
FILE_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
log "Резервная копия создана успешно: $(basename "$BACKUP_FILE")"
echo -e "${GREEN}Размер файла: $FILE_SIZE${NC}"
echo -e "${GREEN}Путь: $BACKUP_FILE${NC}"
# Показываем краткую информацию о содержимом
LINES_COUNT=$(wc -l < "$BACKUP_FILE")
echo -e "${BLUE}Количество строк в дампе: $LINES_COUNT${NC}"
else
error "Резервная копия создана, но файл пустой. Возможные причины:"
echo " - База данных пуста (бот еще не создал таблицы)"
echo " - Нет прав доступа к базе данных"
echo " - Неправильные параметры подключения"
rm -f "$BACKUP_FILE"
return 1
fi
else
error "Ошибка при создании резервной копии"
# Показываем подробную информацию об ошибке
echo "Попытка диагностики проблемы..."
docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "\dt" 2>&1 || {
echo "Не удается подключиться к базе данных."
echo "Проверьте логи контейнера: docker compose logs postgres"
}
rm -f "$BACKUP_FILE" 2>/dev/null
return 1
fi
}
# Восстановление базы данных
restore_database() {
log "Восстановление базы данных из резервной копии"
# Поиск файлов резервных копий
BACKUP_FILES=($(find "$BOT_DIR" -name "backup_*.sql" -type f 2>/dev/null))
if [ ${#BACKUP_FILES[@]} -eq 0 ]; then
error "Файлы резервных копий не найдены в $BOT_DIR"
echo "Поместите файл резервной копии (.sql) в папку $BOT_DIR"
return 1
fi
echo -e "${YELLOW}Найденные резервные копии:${NC}"
for i in "${!BACKUP_FILES[@]}"; do
BACKUP_FILE="${BACKUP_FILES[$i]}"
FILE_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
FILE_DATE=$(basename "$BACKUP_FILE" | sed 's/backup_\([0-9]\{8\}_[0-9]\{6\}\)\.sql/\1/' | sed 's/_/ /')
echo "$((i+1))) $(basename "$BACKUP_FILE") - Размер: $FILE_SIZE - Дата: $FILE_DATE"
done
echo "$((${#BACKUP_FILES[@]}+1))) Указать путь к другому файлу"
echo "0) Отмена"
while true; do
read -p "Выберите файл для восстановления: " choice
if [ "$choice" = "0" ]; then
log "Операция отменена"
return 0
elif [ "$choice" = "$((${#BACKUP_FILES[@]}+1))" ]; then
read -p "Введите полный путь к файлу резервной копии: " SELECTED_BACKUP
if [ ! -f "$SELECTED_BACKUP" ]; then
error "Файл не найден: $SELECTED_BACKUP"
continue
fi
break
elif [ "$choice" -ge 1 ] && [ "$choice" -le "${#BACKUP_FILES[@]}" ]; then
SELECTED_BACKUP="${BACKUP_FILES[$((choice-1))]}"
break
else
error "Неверный выбор. Попробуйте снова."
fi
done
warn "ВНИМАНИЕ! Это действие перезапишет текущую базу данных!"
warn "Убедитесь, что у вас есть резервная копия текущих данных!"
read -p "Продолжить восстановление? Введите 'YES' для подтверждения: " confirm
if [ "$confirm" != "YES" ]; then
log "Операция отменена"
return 0
fi
cd "$BOT_DIR"
# Проверяем, запущен ли контейнер postgres
if ! docker compose ps postgres | grep -q "Up"; then
log "Запуск контейнера PostgreSQL..."
docker compose up -d postgres
# Ждем готовности базы данных
log "Ожидание готовности базы данных..."
for i in {1..30}; do
if docker compose exec postgres pg_isready -U remnawave_user -d remnawave_bot &>/dev/null; then
break
fi
sleep 2
echo -n "."
done
echo ""
fi
log "Очистка текущей базы данных..."
if docker compose exec -T postgres psql -U remnawave_user -d remnawave_bot -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" &>/dev/null; then
log "База данных очищена"
else
error "Ошибка при очистке базы данных"
return 1
fi
log "Восстановление данных из файла: $(basename "$SELECTED_BACKUP")"
if docker compose exec -T postgres psql -U remnawave_user -d remnawave_bot < "$SELECTED_BACKUP"; then
log "База данных успешно восстановлена!"
log "Перезапуск бота для применения изменений..."
docker compose restart bot
log "Восстановление завершено успешно"
else
error "Ошибка при восстановлении базы данных"
return 1
fi
}
# Диагностика базы данных
diagnose_database() {
log "Диагностика состояния базы данных..."
cd "$BOT_DIR"
# Проверяем состояние контейнера
echo -e "${YELLOW}Состояние контейнеров:${NC}"
docker compose ps
echo ""
# Проверяем, запущен ли PostgreSQL, если нет - запускаем
if ! docker compose ps postgres | grep -q "Up"; then
log "PostgreSQL не запущен. Запускаем..."
docker compose up -d postgres
# Ждем готовности базы данных
log "Ожидание готовности базы данных..."
for i in {1..30}; do
if docker compose exec postgres pg_isready -U remnawave_user -d remnawave_bot &>/dev/null; then
log "PostgreSQL готов к работе"
break
fi
if [ $i -eq 30 ]; then
error "PostgreSQL не запустился в течение 60 секунд"
return 1
fi
sleep 2
echo -n "."
done
echo ""
fi
# Проверяем доступность PostgreSQL
echo -e "${YELLOW}Проверка доступности PostgreSQL:${NC}"
if docker compose exec postgres pg_isready -U remnawave_user -d remnawave_bot; then
echo -e "${GREEN}✓ PostgreSQL доступен${NC}"
else
echo -e "${RED}✗ PostgreSQL недоступен${NC}"
echo "Проверьте логи: docker compose logs postgres"
return 1
fi
echo ""
# Проверяем подключение к базе данных
echo -e "${YELLOW}Информация о базе данных:${NC}"
docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "
SELECT
current_database() as database_name,
current_user as current_user,
version() as postgresql_version;
" 2>/dev/null || {
echo -e "${RED}Ошибка подключения к базе данных${NC}"
return 1
}
echo ""
# Показываем список таблиц
echo -e "${YELLOW}Таблицы в базе данных:${NC}"
TABLES_OUTPUT=$(docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "\dt" 2>/dev/null)
if echo "$TABLES_OUTPUT" | grep -q "No relations found"; then
echo -e "${YELLOW}База данных пуста - таблицы еще не созданы${NC}"
echo -e "${BLUE}Это нормально если бот еще ни разу не запускался${NC}"
elif [ -z "$TABLES_OUTPUT" ]; then
echo -e "${YELLOW}Не удается получить список таблиц${NC}"
else
echo "$TABLES_OUTPUT"
fi
echo ""
# Показываем размер базы данных
echo -e "${YELLOW}Размер базы данных:${NC}"
docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "
SELECT
pg_database.datname,
pg_size_pretty(pg_database_size(pg_database.datname)) AS size
FROM pg_database
WHERE datname = 'remnawave_bot';
" 2>/dev/null || {
echo -e "${RED}Не удается получить размер базы данных${NC}"
}
}
edit_env_file() {
ensure_nano
if [ ! -f "$ENV_FILE" ]; then
error "Файл .env не найден: $ENV_FILE"
return 1
fi
log "Открытие .env файла для редактирования..."
log "После изменений перезапустите бота для применения настроек"
# Создаем резервную копию .env файла
cp "$ENV_FILE" "$ENV_FILE.backup.$(date +%Y%m%d_%H%M%S)"
nano "$ENV_FILE"
log "Редактирование завершено"
echo -e "${YELLOW}Не забудьте перезапустить бота для применения изменений!${NC}"
}
# Удаление базы данных
remove_database() {
warn "ВНИМАНИЕ! Это действие удалит всю базу данных!"
warn "Все данные бота (пользователи, подписки, настройки) будут потеряны!"
read -p "Вы уверены? Введите 'YES' для подтверждения: " confirm
if [ "$confirm" = "YES" ]; then
log "Удаление базы данных..."
cd "$BOT_DIR"
# Останавливаем все сервисы если они запущены
if docker compose ps --services --filter "status=running" | grep -q "."; then
log "Остановка всех контейнеров..."
docker compose down
fi
# Удаляем volume с данными PostgreSQL
log "Удаление volume с данными PostgreSQL..."
VOLUME_NAME=$(docker compose config --volumes 2>/dev/null | grep postgres || echo "bedolaga-bot_postgres_data")
# Пробуем удалить volume разными способами
if docker volume ls | grep -q "$VOLUME_NAME"; then
if docker volume rm "$VOLUME_NAME" 2>/dev/null; then
log "Volume $VOLUME_NAME успешно удален"
else
log "Принудительное удаление volume..."
docker volume rm "$VOLUME_NAME" --force 2>/dev/null || true
fi
fi
# Также пробуем удалить стандартное имя volume
docker volume rm "$(basename $BOT_DIR)_postgres_data" 2>/dev/null || true
# Удаляем все неиспользуемые volumes связанные с проектом
log "Очистка неиспользуемых volumes..."
docker volume prune -f 2>/dev/null || true
log "База данных удалена"
log "При следующем запуске бота будет создана новая пустая база данных"
else
log "Операция отменена"
fi
}
# Полное удаление бота
remove_bot() {
warn "ВНИМАНИЕ! Это действие полностью удалит бота и все данные!"
read -p "Вы уверены? Введите 'YES' для подтверждения: " confirm
if [ "$confirm" = "YES" ]; then
log "Удаление бота..."
cd "$BOT_DIR"
docker compose down -v
systemctl disable bedolaga-bot.service 2>/dev/null || true
rm -f "$SERVICE_FILE"
rm -rf "$BOT_DIR"
log "Бот полностью удален"
exit 0
else
log "Операция отменена"
fi
}
# Главное меню
show_menu() {
clear
echo -e "${BLUE}=== RemnaWave Bedolaga Bot Management ===${NC}"
echo ""
# Показать статус бота
echo -e "${YELLOW}Статус бота:${NC}"
if check_bot_status; then
echo -e "🟢 Бот: ${GREEN}ЗАПУЩЕН${NC}"
else
echo -e "🔴 Бот: ${RED}ОСТАНОВЛЕН${NC}"
fi
# Показать статус подключения к API
echo -e "${YELLOW}Подключение к RemnaWave API:${NC}"
if check_remnawave_connection; then
echo -e "🟢 API: ${GREEN}ПОДКЛЮЧЕН${NC}"
else
echo -e "🔴 API: ${RED}НЕ ПОДКЛЮЧЕН${NC}"
fi
echo ""
echo -e "${YELLOW}Доступные действия:${NC}"
if check_bot_status; then
# Бот запущен
echo "1) Выключить бот"
echo "2) Перезапустить бот"
echo "3) Посмотреть логи в реальном времени"
echo "4) Обновить бот"
echo "5) Посмотреть логи"
echo "6) Создать резервную копию БД"
echo "7) Восстановить базу данных"
echo "8) Редактировать .env файл"
echo "9) Диагностика базы данных"
echo "10) Удалить базу данных"
echo "11) Полностью удалить бота"
echo "0) Выход"
read -p "Выберите действие: " choice
case $choice in
1) stop_bot; read -p "Нажмите Enter для продолжения..."; ;;
2) restart_bot; read -p "Нажмите Enter для продолжения..."; ;;
3) view_live_logs; ;;
4) update_bot; read -p "Нажмите Enter для продолжения..."; ;;
5) view_logs; read -p "Нажмите Enter для продолжения..."; ;;
6) backup_database; read -p "Нажмите Enter для продолжения..."; ;;
7) restore_database; read -p "Нажмите Enter для продолжения..."; ;;
8) edit_env_file; read -p "Нажмите Enter для продолжения..."; ;;
9) diagnose_database; read -p "Нажмите Enter для продолжения..."; ;;
10) remove_database; read -p "Нажмите Enter для продолжения..."; ;;
11) remove_bot; ;;
0) exit 0; ;;
*) error "Неверный выбор"; read -p "Нажмите Enter для продолжения..."; ;;
esac
else
# Бот остановлен
echo "1) Запустить бот"
echo "2) Обновить бот"
echo "3) Посмотреть логи"
echo "4) Создать резервную копию БД"
echo "5) Восстановить базу данных"
echo "6) Редактировать .env файл"
echo "7) Диагностика базы данных"
echo "8) Удалить базу данных"
echo "9) Полностью удалить бота"
echo "0) Выход"
read -p "Выберите действие: " choice
case $choice in
1) start_bot; read -p "Нажмите Enter для продолжения..."; ;;
2) update_bot; read -p "Нажмите Enter для продолжения..."; ;;
3) view_logs; read -p "Нажмите Enter для продолжения..."; ;;
4) backup_database; read -p "Нажмите Enter для продолжения..."; ;;
5) restore_database; read -p "Нажмите Enter для продолжения..."; ;;
6) edit_env_file; read -p "Нажмите Enter для продолжения..."; ;;
7) diagnose_database; read -p "Нажмите Enter для продолжения..."; ;;
8) remove_database; read -p "Нажмите Enter для продолжения..."; ;;
9) remove_bot; ;;
0) exit 0; ;;
*) error "Неверный выбор"; read -p "Нажмите Enter для продолжения..."; ;;
esac
fi
}
# Основная функция установки
install_bot() {
log "Начало установки RemnaWave Bedolaga Bot"
update_system
install_docker
create_project_structure
create_docker_compose
create_env_file
create_service
log "Установка завершена!"
log "Бот установлен в: $BOT_DIR"
log "Для управления ботом используйте это меню или systemctl"
read -p "Нажмите Enter для перехода в меню управления..."
}
# Главная функция
main() {
check_root
if check_installation; then
log "Бот уже установлен. Переход в меню управления..."
ensure_nano # Убеждаемся что nano установлен
else
install_bot
fi
# Основной цикл меню
while true; do
show_menu
done
}
# Запуск скрипта
main "$@"
+47 -4
View File
@@ -1,6 +1,6 @@
import os
from dataclasses import dataclass
from typing import List
from typing import List, Dict
@dataclass
class Config:
@@ -38,11 +38,19 @@ class Config:
MONITOR_CHECK_INTERVAL: int = 3600 # 1 час
MONITOR_DAILY_CHECK_HOUR: int = 10 # 10:00 утра
MONITOR_WARNING_DAYS: int = 3 # предупреждать за 3 дня
# Новые настройки для удаления истекших подписок
DELETE_EXPIRED_TRIAL_DAYS: int = 1 # Удалять триальные через 1 день после истечения
DELETE_EXPIRED_REGULAR_DAYS: int = 7 # Удалять обычные через 7 дней после истечения
AUTO_DELETE_ENABLED: bool = False # Автоматическое удаление при ежедневной проверке
LUCKY_GAME_ENABLED: bool = True
LUCKY_GAME_REWARD: float = 50.0 # Размер награды за выигрыш
LUCKY_GAME_NUMBERS: int = 30 # Общее количество чисел (1-30)
LUCKY_GAME_WINNING_COUNT: int = 3 # Количество выигрышных чисел
STARS_ENABLED: bool = True
STARS_RATES: Dict[int, float] = None
def load_config() -> Config:
@@ -70,8 +78,32 @@ 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', ''),
@@ -105,11 +137,20 @@ def load_config() -> Config:
MONITOR_DAILY_CHECK_HOUR=get_int('MONITOR_DAILY_CHECK_HOUR', 10), # 10 утра
MONITOR_WARNING_DAYS=get_int('MONITOR_WARNING_DAYS', 3), # за 3 дня
# Новые настройки для удаления истекших подписок
DELETE_EXPIRED_TRIAL_DAYS=get_int('DELETE_EXPIRED_TRIAL_DAYS', 1),
DELETE_EXPIRED_REGULAR_DAYS=get_int('DELETE_EXPIRED_REGULAR_DAYS', 7),
AUTO_DELETE_ENABLED=get_bool('AUTO_DELETE_ENABLED', False),
# Игра удачи
LUCKY_GAME_ENABLED=get_bool('LUCKY_GAME_ENABLED', True),
LUCKY_GAME_REWARD=get_float('LUCKY_GAME_REWARD', 50.0),
LUCKY_GAME_NUMBERS=get_int('LUCKY_GAME_NUMBERS', 30),
LUCKY_GAME_WINNING_COUNT=get_int('LUCKY_GAME_WINNING_COUNT', 3)
LUCKY_GAME_WINNING_COUNT=get_int('LUCKY_GAME_WINNING_COUNT', 3),
# Telegram Stars
STARS_ENABLED=get_bool('STARS_ENABLED', True),
STARS_RATES=parse_stars_rates()
)
def debug_environment():
@@ -118,7 +159,9 @@ def debug_environment():
'REMNAWAVE_URL', 'REMNAWAVE_TOKEN', 'ADMIN_IDS',
'REFERRAL_FIRST_REWARD', 'REFERRAL_THRESHOLD',
'MONITOR_ENABLED', 'MONITOR_CHECK_INTERVAL',
'LUCKY_GAME_ENABLED', 'LUCKY_GAME_REWARD'
'DELETE_EXPIRED_TRIAL_DAYS', 'DELETE_EXPIRED_REGULAR_DAYS', 'AUTO_DELETE_ENABLED',
'LUCKY_GAME_ENABLED', 'LUCKY_GAME_REWARD',
'STARS_ENABLED', 'STARS_100_RATE', 'STARS_500_RATE', 'STARS_1000_RATE'
]
print("📋 Environment variables:")
+189 -15
View File
@@ -14,23 +14,23 @@ class ReferralProgram(Base):
__tablename__ = 'referral_programs'
id: Mapped[int] = mapped_column(primary_key=True)
referrer_id: Mapped[int] = mapped_column(BigInteger, index=True) # Кто пригласил
referred_id: Mapped[int] = mapped_column(BigInteger, unique=True, index=True) # Кто был приглашен
referral_code: Mapped[str] = mapped_column(String(20), index=True) # Промокод реферера
first_reward_paid: Mapped[bool] = mapped_column(Boolean, default=False) # Выплачена ли разовая награда
total_earned: Mapped[float] = mapped_column(Float, default=0.0) # Всего заработано
referrer_id: Mapped[int] = mapped_column(BigInteger, index=True)
referred_id: Mapped[int] = mapped_column(BigInteger, unique=True, index=True)
referral_code: Mapped[str] = mapped_column(String(20), index=True)
first_reward_paid: Mapped[bool] = mapped_column(Boolean, default=False)
total_earned: Mapped[float] = mapped_column(Float, default=0.0)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
first_reward_at: Mapped[Optional[datetime]] = mapped_column(DateTime) # Когда выплатили первую награду
first_reward_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
class ReferralEarning(Base):
__tablename__ = 'referral_earnings'
id: Mapped[int] = mapped_column(primary_key=True)
referrer_id: Mapped[int] = mapped_column(BigInteger, index=True) # Кто получил награду
referred_id: Mapped[int] = mapped_column(BigInteger, index=True) # От кого получена награда
amount: Mapped[float] = mapped_column(Float) # Размер награды
earning_type: Mapped[str] = mapped_column(String(20)) # 'first_reward', 'percentage'
related_payment_id: Mapped[Optional[int]] = mapped_column(Integer) # Связанный платеж
referrer_id: Mapped[int] = mapped_column(BigInteger, index=True)
referred_id: Mapped[int] = mapped_column(BigInteger, index=True)
amount: Mapped[float] = mapped_column(Float)
earning_type: Mapped[str] = mapped_column(String(20))
related_payment_id: Mapped[Optional[int]] = mapped_column(Integer)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class User(Base):
@@ -114,11 +114,23 @@ class LuckyGame(Base):
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, index=True)
chosen_number: Mapped[int] = mapped_column(Integer)
winning_numbers: Mapped[str] = mapped_column(String(255)) # JSON строка с выигрышными номерами
winning_numbers: Mapped[str] = mapped_column(String(255))
is_winner: Mapped[bool] = mapped_column(Boolean, default=False)
reward_amount: Mapped[float] = mapped_column(Float, default=0.0)
played_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
class StarPayment(Base):
__tablename__ = 'star_payments'
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(BigInteger, index=True)
stars_amount: Mapped[int] = mapped_column(Integer) # Количество звезд
rub_amount: Mapped[float] = mapped_column(Float) # Сумма в рублях
status: Mapped[str] = mapped_column(String(50), default='pending') # pending, completed, cancelled
telegram_payment_charge_id: Mapped[Optional[str]] = mapped_column(String(255)) # ID платежа от Telegram
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
class Database:
def __init__(self, database_url: str):
self.engine = create_async_engine(
@@ -140,6 +152,7 @@ class Database:
await self.migrate_user_subscriptions()
await self.migrate_subscription_imported_field()
await self.migrate_referral_tables()
await self.migrate_star_payments_table()
async def migrate_referral_tables(self):
try:
@@ -1356,18 +1369,16 @@ class Database:
try:
from sqlalchemy import select, func, and_
# Общее количество игр
games_count = await session.execute(
select(func.count(LuckyGame.id))
.where(LuckyGame.user_id == user_id)
)
total_games = games_count.scalar() or 0
# Количество выигрышей
wins_count = await session.execute(
select(func.count(LuckyGame.id))
.where(
and_( # Вот здесь используется and_
and_(
LuckyGame.user_id == user_id,
LuckyGame.is_winner == True
)
@@ -1434,3 +1445,166 @@ class Database:
except Exception as e:
logger.error(f"Error checking can play today: {e}")
return True
async def create_star_payment(self, user_id: int, stars_amount: int, rub_amount: float) -> StarPayment:
"""Создать платеж через звезды"""
async with self.session_factory() as session:
try:
payment = StarPayment(
user_id=user_id,
stars_amount=stars_amount,
rub_amount=rub_amount,
status='pending'
)
session.add(payment)
await session.commit()
await session.refresh(payment)
return payment
except Exception as e:
logger.error(f"Error creating star payment: {e}")
await session.rollback()
raise
async def get_star_payment_by_id(self, payment_id: int) -> Optional[StarPayment]:
"""Получить платеж через звезды по ID"""
async with self.session_factory() as session:
try:
from sqlalchemy import select
result = await session.execute(
select(StarPayment).where(StarPayment.id == payment_id)
)
return result.scalar_one_or_none()
except Exception as e:
logger.error(f"Error getting star payment {payment_id}: {e}")
return None
async def complete_star_payment(self, payment_id: int, telegram_payment_charge_id: str) -> bool:
"""Завершить платеж через звезды"""
async with self.session_factory() as session:
try:
from sqlalchemy import update
result = await session.execute(
update(StarPayment)
.where(StarPayment.id == payment_id)
.values(
status='completed',
telegram_payment_charge_id=telegram_payment_charge_id,
completed_at=datetime.utcnow()
)
)
if result.rowcount == 0:
return False
payment_result = await session.execute(
select(StarPayment).where(StarPayment.id == payment_id)
)
payment = payment_result.scalar_one_or_none()
if not payment:
return False
balance_result = await session.execute(
update(User)
.where(User.telegram_id == payment.user_id)
.values(balance=User.balance + payment.rub_amount)
)
regular_payment = Payment(
user_id=payment.user_id,
amount=payment.rub_amount,
payment_type='stars',
description=f'Пополнение через Telegram Stars ({payment.stars_amount} ⭐)',
status='completed'
)
session.add(regular_payment)
await session.commit()
return True
except Exception as e:
logger.error(f"Error completing star payment {payment_id}: {e}")
await session.rollback()
return False
async def cancel_star_payment(self, payment_id: int) -> bool:
"""Отменить платеж через звезды"""
async with self.session_factory() as session:
try:
from sqlalchemy import update
result = await session.execute(
update(StarPayment)
.where(StarPayment.id == payment_id)
.values(status='cancelled')
)
await session.commit()
return result.rowcount > 0
except Exception as e:
logger.error(f"Error cancelling star payment {payment_id}: {e}")
await session.rollback()
return False
async def get_user_star_payments(self, user_id: int, limit: int = 10) -> List[StarPayment]:
"""Получить платежи пользователя через звезды"""
async with self.session_factory() as session:
try:
from sqlalchemy import select, desc
result = await session.execute(
select(StarPayment)
.where(StarPayment.user_id == user_id)
.order_by(desc(StarPayment.created_at))
.limit(limit)
)
return list(result.scalars().all())
except Exception as e:
logger.error(f"Error getting user star payments for {user_id}: {e}")
return []
async def migrate_star_payments_table(self):
"""Создание таблицы для платежей через звезды"""
try:
async with self.engine.begin() as conn:
db_type = str(conn.get_dialect().name).lower()
if db_type == 'postgresql':
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS star_payments (
id SERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
stars_amount INTEGER NOT NULL,
rub_amount DOUBLE PRECISION NOT NULL,
status VARCHAR(50) DEFAULT 'pending',
telegram_payment_charge_id VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP
)
"""))
await conn.execute(text("""
CREATE INDEX IF NOT EXISTS idx_star_payments_user ON star_payments(user_id)
"""))
await conn.execute(text("""
CREATE INDEX IF NOT EXISTS idx_star_payments_status ON star_payments(status)
"""))
else:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS star_payments (
id SERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
stars_amount INTEGER NOT NULL,
rub_amount DOUBLE PRECISION NOT NULL,
status VARCHAR(50) DEFAULT 'pending',
telegram_payment_charge_id VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMP,
INDEX idx_star_payments_user (user_id),
INDEX idx_star_payments_status (status)
)
"""))
logger.info("Successfully created star_payments table")
except Exception as e:
logger.error(f"Error creating star_payments table: {e}")
pass
+72 -100
View File
@@ -8,12 +8,13 @@ import logging
import secrets
from typing import Optional, Dict, Any
from database import Database, User
from database import Database, User, ReferralProgram
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 (
@@ -122,79 +123,6 @@ async def start_command(message: Message, state: FSMContext, db: Database, **kwa
else:
await show_main_menu(message, user.language, user.is_admin, user.telegram_id, db, config)
async def process_referral_rewards(user_id: int, amount: float, payment_id: int, db: Database, bot=None):
try:
referral = await db.get_referral_by_referred_id(user_id)
if not referral:
return
user = await db.get_user_by_telegram_id(user_id)
if not user:
return
if not referral.first_reward_paid and user.balance >= 300:
success = await db.create_referral_earning(
referrer_id=referral.referrer_id,
referred_id=user_id,
amount=150.0,
earning_type='first_reward',
related_payment_id=payment_id
)
if success and bot:
try:
await bot.send_message(
referral.referrer_id,
f"🎉 Поздравляем! Ваш реферал пополнил баланс на 300₽+\n\n"
f"💰 Вам начислено 150₽ за приведенного друга!\n"
f"Также вы будете получать 25% с каждого его платежа."
)
await bot.send_message(
user_id,
f"🎁 Бонус активирован! Вам начислено 150₽ за переход по реферальной ссылке!"
)
await db.add_balance(user_id, 150.0)
await db.create_payment(
user_id=user_id,
amount=150.0,
payment_type='referral',
description='Бонус за переход по реферальной ссылке',
status='completed'
)
except Exception as e:
logger.error(f"Failed to send referral notifications: {e}")
if amount > 0:
percentage_reward = amount * 0.25
success = await db.create_referral_earning(
referrer_id=referral.referrer_id,
referred_id=user_id,
amount=percentage_reward,
earning_type='percentage',
related_payment_id=payment_id
)
if success and bot and percentage_reward >= 1.0:
try:
await bot.send_message(
referral.referrer_id,
f"💰 Реферальный доход!\n\n"
f"Ваш реферал совершил платеж на {amount:.2f}\n"
f"Вам начислено: {percentage_reward:.2f}₽ (25%)"
)
except Exception as e:
logger.error(f"Failed to send percentage notification: {e}")
except Exception as e:
logger.error(f"Error processing referral rewards: {e}")
# Language selection
@router.callback_query(F.data.startswith("lang_"))
async def language_callback(callback: CallbackQuery, state: FSMContext, db: Database, **kwargs):
user = kwargs.get('user')
@@ -242,13 +170,12 @@ async def language_callback(callback: CallbackQuery, state: FSMContext, db: Data
async def show_main_menu(message: Message, lang: str, is_admin: bool = False, user_id: int = None, db: Database = None, config: Config = None):
try:
show_trial = False
show_lucky_game = True # По умолчанию показываем игру
show_lucky_game = True
if config and config.TRIAL_ENABLED and user_id and db:
has_used = await db.has_used_trial(user_id)
show_trial = not has_used
# Проверяем, включена ли игра удачи в конфиге
if config:
show_lucky_game = getattr(config, 'LUCKY_GAME_ENABLED', True)
@@ -489,13 +416,25 @@ 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
# Проверяем, включены ли Telegram Stars
stars_enabled = config and config.STARS_ENABLED and config.STARS_RATES
text = t('topup_balance', user.language)
if stars_enabled:
text += "\n\n⭐ **Новинка!** Теперь можно пополнять баланс через Telegram Stars!"
text += "\n💎 Быстро, безопасно, без комиссий!"
await callback.message.edit_text(
t('topup_balance', user.language),
reply_markup=topup_keyboard(user.language)
text,
reply_markup=topup_keyboard(user.language),
parse_mode='Markdown'
)
@router.callback_query(F.data == "topup_card")
@@ -546,7 +485,7 @@ async def handle_amount(message: Message, state: FSMContext, db: Database, **kwa
payment = await db.create_payment(
user_id=user.telegram_id,
amount=amount,
payment_type='topup',
payment_type='topup',
description=f'Пополнение баланса на {amount} руб.'
)
@@ -594,21 +533,56 @@ async def payment_history_callback(callback: CallbackQuery, db: Database, **kwar
return
try:
# Получаем обычные платежи
payments = await db.get_user_payments(user.telegram_id)
if not payments:
# Получаем платежи через звезды
star_payments = await db.get_user_star_payments(user.telegram_id, 5)
if not payments and not star_payments:
text = t('no_payments', user.language)
else:
text = "📊 " + t('payment_history', user.language) + ":\n\n"
for payment in payments[:10]: # Show last 10 payments
date_str = format_datetime(payment.created_at, user.language)
status = format_payment_status(payment.status, user.language)
text += t('payment_item', user.language,
date=date_str,
amount=payment.amount,
description=payment.description,
status=status
) + "\n"
# Объединяем и сортируем все платежи по дате
all_payments = []
for payment in payments[:10]:
all_payments.append({
'type': 'regular',
'date': payment.created_at,
'amount': payment.amount,
'description': payment.description,
'status': payment.status
})
for star_payment in star_payments:
if star_payment.status == 'completed':
all_payments.append({
'type': 'stars',
'date': star_payment.completed_at or star_payment.created_at,
'amount': star_payment.rub_amount,
'description': f'Пополнение через Stars ({star_payment.stars_amount} ⭐)',
'status': star_payment.status,
'stars': star_payment.stars_amount
})
# Сортируем по дате (новые сначала)
all_payments.sort(key=lambda x: x['date'], reverse=True)
# Показываем последние 10 платежей
for payment in all_payments[:10]:
date_str = format_datetime(payment['date'], user.language)
status = format_payment_status(payment['status'], user.language)
if payment['type'] == 'stars':
text += f"⭐ +{payment['amount']:.0f}₽ ({payment['stars']} ⭐)\n"
else:
amount_str = f"+{payment['amount']}" if payment['amount'] > 0 else str(payment['amount'])
text += f"💳 {amount_str}\n"
text += f" 📅 {date_str} | {status}\n"
text += f" 📝 {payment['description']}\n\n"
await callback.message.edit_text(
text,
@@ -800,14 +774,12 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
payment = await db.create_payment(
user_id=user.telegram_id,
amount=-subscription.price,
payment_type='subscription',
amount=-subscription.price,
payment_type='subscription',
description=f'Покупка подписки: {subscription.name}',
status='completed'
)
bot = kwargs.get('bot')
await process_referral_rewards(user.telegram_id, subscription.price, payment.id, db, bot)
success_text = f"✅ Подписка успешно создана!\n\n"
success_text += f"📋 Подписка: {subscription.name}\n"
@@ -879,7 +851,7 @@ async def my_subscriptions_callback(callback: CallbackQuery, db: Database, **kwa
subscription_name = subscription.name
if subscription.is_imported or subscription.name == "Старая подписка":
subscription_name += " 🔄" # Добавляем иконку импорта
subscription_name += " 🔄"
text += f"{i}. {subscription_name}\n"
text += f" {status}\n"
@@ -977,8 +949,7 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
logger.warning(f"Could not get subscription URL: {e}")
if is_imported and 0 <= days_until_expiry <= 3:
text += f"\n\n⚠️ Это импортированная подписка из старой системы.\n"
text += f"📅 Истекает через {days_until_expiry} дн.\n"
text += f"\n\n📅 Истекает через {days_until_expiry} дн.\n"
text += f"🛒 Для продолжения работы приобретите новый тарифный план."
elif is_trial and 0 <= days_until_expiry <= 3:
text += f"\n\nℹ️ Тестовая подписка истекает через {days_until_expiry} дн.\n"
@@ -1154,14 +1125,15 @@ async def confirm_extend_subscription_callback(callback: CallbackQuery, db: Data
user.balance -= subscription.price
await db.update_user(user)
await db.create_payment(
payment = await db.create_payment(
user_id=user.telegram_id,
amount=-subscription.price,
payment_type='subscription_extend',
amount=-subscription.price,
payment_type='subscription_extend',
description=f'Продление подписки: {subscription.name}',
status='completed'
)
success_text = f"✅ Подписка успешно продлена!\n\n"
success_text += f"📋 Подписка: {subscription.name}\n"
success_text += f"📅 Новая дата истечения: {format_datetime(new_expiry, user.language)}\n"
@@ -1424,7 +1396,7 @@ async def referral_program_callback(callback: CallbackQuery, db: Database, **kwa
text += f"• Приведи друга и получи **{first_reward:.0f}₽** на баланс\n"
text += f"• Твой друг получит **{referred_bonus:.0f}₽** после пополнения на {threshold:.0f}\n"
text += f"С каждого платежа друга ты получаешь **{percentage*100:.0f}%**\n\n"
text += f"С каждого **пополнения баланса** друга ты получаешь **{percentage*100:.0f}%**\n\n"
text += "**📊 Твоя статистика:**\n"
text += f"• Приглашено: {stats['total_referrals']} человек\n"
+49 -3
View File
@@ -192,6 +192,7 @@ def admin_balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💸 Пополнить баланс пользователю", callback_data="admin_add_balance")],
[InlineKeyboardButton(text="📊 История платежей", callback_data="admin_payment_history")],
[InlineKeyboardButton(text="⭐ Telegram Stars", callback_data="admin_stars_payments")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
@@ -410,9 +411,6 @@ def trial_subscription_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
def admin_monitor_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📊 Статус сервиса", callback_data="monitor_status")],
[InlineKeyboardButton(text="🔄 Принудительная проверка", callback_data="monitor_force_check")],
[InlineKeyboardButton(text="⚰️ Деактивировать истекшие", callback_data="monitor_deactivate_expired")],
[InlineKeyboardButton(text="👤 Тест для пользователя", callback_data="monitor_test_user")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
@@ -605,3 +603,51 @@ 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())
# Создаем кнопки по 2 в ряд
for i in range(0, len(sorted_rates), 2):
row = []
for j in range(2):
if i + j < len(sorted_rates):
stars, rubles = sorted_rates[i + j]
# Определяем выгодность предложения
if stars >= 500:
emoji = "🔥" # Выгодное предложение
elif stars >= 250:
emoji = "💎" # Хорошее предложение
else:
emoji = "" # Базовое предложение
button_text = f"{emoji} {stars} ⭐ → {rubles:.0f}"
row.append(InlineKeyboardButton(
text=button_text,
callback_data=f"buy_stars_{stars}"
))
buttons.append(row)
# Добавляем кнопку назад
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="topup_balance")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def stars_payment_keyboard(stars_amount: int, rub_amount: float, lang: str = 'ru') -> InlineKeyboardMarkup:
"""Клавиатура подтверждения платежа через звезды (не используется в send_invoice)"""
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отмена", callback_data="topup_stars")]
])
return keyboard
+1 -23
View File
@@ -25,7 +25,6 @@ lucky_game_router = Router()
@lucky_game_router.callback_query(F.data == "lucky_game")
async def lucky_game_menu_callback(callback: CallbackQuery, db: Database, **kwargs):
"""Главное меню игры удачи"""
user = kwargs.get('user')
config = kwargs.get('config')
@@ -34,15 +33,12 @@ async def lucky_game_menu_callback(callback: CallbackQuery, db: Database, **kwar
return
try:
# Получаем настройки игры из конфига
reward_amount = getattr(config, 'LUCKY_GAME_REWARD', 50.0)
numbers_count = getattr(config, 'LUCKY_GAME_NUMBERS', 30)
winning_numbers = getattr(config, 'LUCKY_GAME_WINNING_COUNT', 3)
# Проверяем, можно ли играть сегодня
can_play, next_game_time = await check_can_play_today(db, user.telegram_id)
# Получаем статистику игр пользователя
games_played, total_won, win_count = await get_user_game_stats(db, user.telegram_id)
text = "🎰 **Проверь свою удачу!**\n\n"
@@ -61,13 +57,11 @@ async def lucky_game_menu_callback(callback: CallbackQuery, db: Database, **kwar
win_rate = (win_count / games_played) * 100
text += f"• Процент побед: {win_rate:.1f}%\n"
# Создаем клавиатуру
buttons = []
if can_play:
buttons.append([InlineKeyboardButton(text="🎲 Играть!", callback_data="start_lucky_game")])
else:
# Вычисляем оставшееся время
now = datetime.utcnow()
time_left = next_game_time - now
hours_left = int(time_left.total_seconds() // 3600)
@@ -108,19 +102,16 @@ async def start_lucky_game_callback(callback: CallbackQuery, db: Database, state
return
try:
# Проверяем, можно ли играть
can_play, next_game_time = await check_can_play_today(db, user.telegram_id)
if not can_play:
await callback.answer("⏰ Вы уже играли сегодня! Приходите завтра.", show_alert=True)
return
# Получаем настройки
numbers_count = getattr(config, 'LUCKY_GAME_NUMBERS', 30)
winning_numbers = getattr(config, 'LUCKY_GAME_WINNING_COUNT', 3)
reward_amount = getattr(config, 'LUCKY_GAME_REWARD', 50.0)
# Генерируем выигрышные числа заранее и сохраняем в состоянии
winning_nums = random.sample(range(1, numbers_count + 1), winning_numbers)
await state.update_data(
@@ -135,7 +126,6 @@ async def start_lucky_game_callback(callback: CallbackQuery, db: Database, state
text += f"🍀 Удачных чисел: {winning_numbers} из {numbers_count}\n\n"
text += "Нажмите на число, чтобы испытать удачу!"
# Создаем кнопки с числами (5 в ряд)
buttons = []
for i in range(0, numbers_count, 5):
row = []
@@ -175,24 +165,18 @@ async def choose_number_callback(callback: CallbackQuery, db: Database, state: F
try:
chosen_number = int(callback.data.split("_")[2])
# Получаем данные из состояния
state_data = await state.get_data()
winning_numbers = state_data['winning_numbers']
reward_amount = state_data['reward_amount']
numbers_count = state_data['numbers_count']
# Проверяем, выиграл ли пользователь
is_winner = chosen_number in winning_numbers
# Сохраняем результат игры в БД
await save_game_result(db, user.telegram_id, chosen_number, winning_numbers, is_winner, reward_amount if is_winner else 0.0)
# Формируем сообщение с результатом
if is_winner:
# Начисляем награду
await db.add_balance(user.telegram_id, reward_amount)
# Создаем платеж
await db.create_payment(
user_id=user.telegram_id,
amount=reward_amount,
@@ -211,7 +195,6 @@ async def choose_number_callback(callback: CallbackQuery, db: Database, state: F
text += f"🍀 Выигрышные числа: **{', '.join(map(str, sorted(winning_numbers)))}**\n\n"
text += "🔄 Попробуйте завтра!"
# Показываем все выигрышные числа
winning_nums_str = ', '.join(map(str, sorted(winning_numbers)))
text += f"\n🎲 Сегодняшние удачные числа: {winning_nums_str}"
text += f"\n\n⏰ Следующая игра будет доступна завтра!"
@@ -227,7 +210,6 @@ async def choose_number_callback(callback: CallbackQuery, db: Database, state: F
parse_mode='Markdown'
)
# Логируем действие
log_user_action(user.telegram_id, "lucky_game_played",
f"Number: {chosen_number}, Winner: {is_winner}, Reward: {reward_amount if is_winner else 0}")
@@ -239,7 +221,6 @@ async def choose_number_callback(callback: CallbackQuery, db: Database, state: F
@lucky_game_router.callback_query(F.data == "lucky_game_history")
async def lucky_game_history_callback(callback: CallbackQuery, db: Database, **kwargs):
"""Показать историю игр пользователя"""
user = kwargs.get('user')
if not user:
@@ -247,7 +228,6 @@ async def lucky_game_history_callback(callback: CallbackQuery, db: Database, **k
return
try:
# Получаем последние 10 игр
games = await get_user_game_history(db, user.telegram_id, limit=10)
if not games:
@@ -289,8 +269,7 @@ async def lucky_game_history_callback(callback: CallbackQuery, db: Database, **k
async def noop_callback(callback: CallbackQuery, **kwargs):
"""Пустая операция для неактивных кнопок"""
await callback.answer()
# Вспомогательные функции
async def check_can_play_today(db: Database, user_id: int) -> tuple[bool, Optional[datetime]]:
"""Проверяет, может ли пользователь играть сегодня"""
@@ -298,7 +277,6 @@ async def check_can_play_today(db: Database, user_id: int) -> tuple[bool, Option
can_play = await db.can_play_lucky_game_today(user_id)
if not can_play:
# Вычисляем время следующей игры (завтра в 00:00)
now = datetime.utcnow()
tomorrow = now.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta(days=1)
return False, tomorrow
+20
View File
@@ -7,6 +7,7 @@ from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from lucky_game import lucky_game_router
from stars_handlers import stars_router
print("🚀 Запуск бота...")
print(f"📍 Рабочая директория: {os.getcwd()}")
@@ -96,6 +97,24 @@ class BotApplication:
self._setup_dispatcher()
await self._init_monitor_service()
if self.config.STARS_ENABLED:
logger.info("✅ Telegram Stars пополнение включено")
logger.info(f"📊 Настроенные курсы: {self.config.STARS_RATES}")
if not self.config.STARS_RATES or len(self.config.STARS_RATES) == 0:
logger.warning("⚠️ STARS_RATES пустые! Telegram Stars будут недоступны")
self.config.STARS_ENABLED = False
else:
valid_rates = all(
isinstance(stars, int) and isinstance(rubles, (int, float)) and stars > 0 and rubles > 0
for stars, rubles in self.config.STARS_RATES.items()
)
if not valid_rates:
logger.warning("⚠️ Неверные STARS_RATES! Telegram Stars будут недоступны")
self.config.STARS_ENABLED = False
else:
logger.info("❌ Telegram Stars пополнение отключено")
async def _init_database(self):
max_retries = 3
@@ -167,6 +186,7 @@ class BotApplication:
self.dp.include_router(router)
self.dp.include_router(admin_router)
self.dp.include_router(lucky_game_router)
self.dp.include_router(stars_router)
async def _init_monitor_service(self):
try:
+17 -9
View File
@@ -5,10 +5,18 @@ from database import ReferralProgram, ReferralEarning
logger = logging.getLogger(__name__)
async def process_referral_rewards(user_id: int, amount: float, payment_id: int, db: Database, bot=None):
async def process_referral_rewards(user_id: int, amount: float, payment_id: int, db: Database, bot=None, payment_type: str = None):
try:
import os
if payment_type not in ['topup', 'admin_topup']:
logger.debug(f"Skipping referral rewards for payment type: {payment_type}")
return
if amount <= 0:
logger.debug(f"Skipping referral rewards for non-positive amount: {amount}")
return
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
first_reward = float(os.getenv('REFERRAL_FIRST_REWARD', '150.0'))
referred_bonus = float(os.getenv('REFERRAL_REFERRED_BONUS', '150.0'))
@@ -25,7 +33,7 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
logger.error(f"User {user_id} not found")
return
logger.info(f"Processing referral rewards for user {user_id}, amount {amount}, referrer {referral.referrer_id}")
logger.info(f"Processing referral rewards for topup: user {user_id}, amount {amount}, referrer {referral.referrer_id}")
if not referral.first_reward_paid and user.balance >= threshold:
logger.info(f"Processing first reward for referral {referral.id} (threshold: {threshold}, reward: {first_reward})")
@@ -57,7 +65,7 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
referral.referrer_id,
f"🎉 Поздравляем! Ваш реферал пополнил баланс на {threshold}₽+\n\n"
f"💰 Вам начислено {first_reward}₽ за приведенного друга!\n"
f"Теперь вы будете получать {percentage*100:.0f}% с каждого его следующего платежа."
f"Теперь вы будете получать {percentage*100:.0f}% с каждого его пополнения баланса."
)
await bot.send_message(
@@ -81,7 +89,7 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
else:
logger.error(f"Failed to create first reward earning")
if amount > 0 and referral.first_reward_paid:
if referral.first_reward_paid:
percentage_reward = amount * percentage
if percentage_reward >= 0.01:
@@ -91,7 +99,7 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
user_id=referral.referrer_id,
amount=percentage_reward,
payment_type='referral',
description=f'{percentage*100:.0f}% дохода от реферала ID:{user_id}',
description=f'{percentage*100:.0f}% от пополнения реферала ID:{user_id}',
status='completed'
)
@@ -106,19 +114,19 @@ async def process_referral_rewards(user_id: int, amount: float, payment_id: int,
if success:
logger.info(f"Percentage reward paid: {percentage_reward:.2f}₽ ({percentage*100:.0f}%) to referrer {referral.referrer_id}")
if bot and percentage_reward >= 1.0:
if bot and percentage_reward >= 1.0:
try:
await bot.send_message(
referral.referrer_id,
f"💰 Реферальный доход!\n\n"
f"Ваш реферал совершил платеж на {amount:.2f}\n"
f"Ваш реферал пополнил баланс на {amount:.2f}\n"
f"Вам начислено: {percentage_reward:.2f}₽ ({percentage*100:.0f}%)"
)
except Exception as e:
logger.error(f"Failed to send percentage notification: {e}")
else:
logger.error(f"Failed to create percentage earning")
elif amount > 0 and not referral.first_reward_paid:
else:
logger.info(f"Skipping percentage reward for user {user_id} - first reward not yet paid")
except Exception as e:
@@ -166,7 +174,7 @@ async def create_referral_from_start_param(user_telegram_id: int, start_param: s
referrer_id,
f"🎉 Отлично! По вашей ссылке зарегистрировался новый пользователь!\n\n"
f"Вы получите {first_reward:.0f}₽ после того, как он пополнит баланс на {threshold:.0f}₽.\n"
f"И будете получать {percentage*100:.0f}% с каждого его платежа!"
f"И будете получать {percentage*100:.0f}% с каждого его пополнения баланса!"
)
except Exception as e:
logger.error(f"Failed to notify referrer: {e}")
+214 -104
View File
@@ -52,7 +52,7 @@ class RemnaWaveAPI:
logger.debug(f"HTML Response (first 500 chars): {response_text[:500]}")
return None
if response.status in [200, 201]:
if response.status in [200, 201, 204]:
if response_text:
try:
return await response.json()
@@ -60,7 +60,7 @@ class RemnaWaveAPI:
logger.error(f"Failed to decode JSON from {url}: {e}")
logger.debug(f"Raw response: {response_text[:500]}")
return None
return None
return {'success': True} # Для статуса 204 (No Content)
elif response.status == 404:
logger.warning(f"API 404 for {endpoint}")
return None
@@ -74,121 +74,115 @@ class RemnaWaveAPI:
logger.error(f"Request error for {endpoint}: {e}")
return None
async def get_subscription_info(self, short_uuid: str) -> Optional[Dict]:
async def delete_user(self, uuid: str) -> Optional[Dict]:
"""
Удаляет пользователя по полному UUID
"""
try:
logger.info(f"Getting subscription info for short_uuid: {short_uuid}")
logger.info(f"Deleting user with UUID: {uuid}")
result = await self._make_request('DELETE', f'/api/users/{uuid}')
endpoints_to_try = [
f'/api/subscriptions/{short_uuid}',
f'/api/sub/{short_uuid}',
f'/api/subscription/{short_uuid}'
]
for endpoint in endpoints_to_try:
logger.debug(f"Trying endpoint: {endpoint}")
result = await self._make_request('GET', endpoint)
if result:
logger.info(f"Successfully got subscription info from {endpoint}")
subscription_data = None
if 'response' in result:
subscription_data = result['response']
elif 'data' in result:
subscription_data = result['data']
elif 'subscription' in result:
subscription_data = result['subscription']
else:
subscription_data = result
if subscription_data and (
'subscriptionUrl' in subscription_data or
'url' in subscription_data or
'link' in subscription_data
):
return subscription_data
logger.warning(f"Could not get subscription info for {short_uuid} from any endpoint")
return None
except Exception as e:
logger.error(f"Error getting subscription info for {short_uuid}: {e}")
return None
async def get_subscription_url(self, short_uuid: str) -> str:
try:
logger.info(f"Getting subscription URL for short_uuid: {short_uuid}")
subscription_info = await self.get_subscription_info(short_uuid)
if subscription_info:
subscription_url = (
subscription_info.get('subscriptionUrl') or
subscription_info.get('url') or
subscription_info.get('link') or
subscription_info.get('subscription_url')
)
if subscription_url:
logger.info(f"Got subscription URL from API: {subscription_url}")
return subscription_url
user_data = await self.get_user_by_short_uuid(short_uuid)
if user_data and 'subscriptionUrl' in user_data:
logger.info(f"Got subscription URL from user data: {user_data['subscriptionUrl']}")
return user_data['subscriptionUrl']
if self.subscription_base_url:
fallback_url = f"{self.subscription_base_url.rstrip('/')}/sub/{short_uuid}"
logger.warning(f"Using fallback URL: {fallback_url}")
return fallback_url
if result is not None:
logger.info(f"Successfully deleted user {uuid}")
return result
else:
fallback_url = f"{self.base_url.rstrip('/')}/sub/{short_uuid}"
logger.warning(f"Using base_url fallback: {fallback_url}")
return fallback_url
logger.error(f"Failed to delete user {uuid}")
return None
except Exception as e:
logger.error(f"Failed to get subscription URL for {short_uuid}: {e}")
fallback_url = f"{self.base_url.rstrip('/')}/sub/{short_uuid}"
return fallback_url
logger.error(f"Error deleting user {uuid}: {e}")
return None
async def get_all_subscriptions_with_urls(self) -> Optional[List]:
async def delete_user_by_short_uuid(self, short_uuid: str) -> Optional[Dict]:
"""
Удаляет пользователя по short_uuid (сначала получает полный UUID, затем удаляет)
"""
try:
logger.info("Fetching all subscriptions with URLs from API")
result = await self._make_request('GET', '/api/subscriptions')
logger.info(f"Deleting user by short UUID: {short_uuid}")
if not result:
logger.error("Empty response from subscriptions API")
return []
# Сначала получаем полную информацию о пользователе
user_data = await self.get_user_by_short_uuid(short_uuid)
subscriptions_list = []
if not user_data:
logger.warning(f"User with short_uuid {short_uuid} not found")
return None
if 'response' in result and 'subscriptions' in result['response']:
subscriptions_list = result['response']['subscriptions']
elif 'subscriptions' in result:
subscriptions_list = result['subscriptions']
elif 'data' in result:
subscriptions_list = result['data']
elif isinstance(result, list):
subscriptions_list = result
full_uuid = user_data.get('uuid')
if not full_uuid:
logger.error(f"Could not get full UUID for short_uuid {short_uuid}")
return None
processed_subscriptions = []
for subscription in subscriptions_list:
if subscription.get('isFound') and 'user' in subscription:
user_data = subscription['user']
if 'subscriptionUrl' not in subscription and user_data.get('shortUuid'):
subscription['subscriptionUrl'] = await self.get_subscription_url(user_data['shortUuid'])
processed_subscriptions.append(subscription)
# Удаляем пользователя по полному UUID
result = await self.delete_user(full_uuid)
logger.info(f"Processed {len(processed_subscriptions)} subscriptions with URLs")
return processed_subscriptions
if result:
logger.info(f"Successfully deleted user {short_uuid} (UUID: {full_uuid})")
return {
'success': True,
'short_uuid': short_uuid,
'full_uuid': full_uuid,
'username': user_data.get('username', 'unknown')
}
else:
logger.error(f"Failed to delete user {short_uuid}")
return None
except Exception as e:
logger.error(f"Exception in get_all_subscriptions_with_urls: {e}", exc_info=True)
return []
logger.error(f"Error deleting user by short UUID {short_uuid}: {e}")
return None
async def bulk_delete_users_by_short_uuids(self, short_uuids: List[str]) -> Dict[str, Any]:
"""
Массовое удаление пользователей по списку short_uuid
"""
try:
logger.info(f"Bulk deleting {len(short_uuids)} users by short UUIDs")
results = {
'total': len(short_uuids),
'success': 0,
'failed': 0,
'errors': [],
'deleted_users': []
}
for short_uuid in short_uuids:
try:
result = await self.delete_user_by_short_uuid(short_uuid)
if result and result.get('success'):
results['success'] += 1
results['deleted_users'].append({
'short_uuid': short_uuid,
'full_uuid': result.get('full_uuid'),
'username': result.get('username')
})
logger.debug(f"Successfully deleted user {short_uuid}")
else:
results['failed'] += 1
results['errors'].append(f"Failed to delete {short_uuid}")
logger.warning(f"Failed to delete user {short_uuid}")
# Небольшая пауза между запросами, чтобы не перегружать API
await asyncio.sleep(0.1)
except Exception as e:
results['failed'] += 1
results['errors'].append(f"Error deleting {short_uuid}: {str(e)}")
logger.error(f"Error deleting user {short_uuid}: {e}")
logger.info(f"Bulk deletion completed: {results['success']} success, {results['failed']} failed")
return results
except Exception as e:
logger.error(f"Error in bulk_delete_users_by_short_uuids: {e}")
return {
'total': len(short_uuids),
'success': 0,
'failed': len(short_uuids),
'errors': [f"Critical error: {str(e)}"],
'deleted_users': []
}
async def create_user(self, username: str, password: str = None,
traffic_limit: int = 0, expiry_time: str = None,
@@ -298,7 +292,86 @@ class RemnaWaveAPI:
return user_data
return None
async def get_subscription_info(self, short_uuid: str) -> Optional[Dict]:
try:
logger.info(f"Getting subscription info for short_uuid: {short_uuid}")
endpoints_to_try = [
f'/api/subscriptions/{short_uuid}',
f'/api/sub/{short_uuid}',
f'/api/subscription/{short_uuid}'
]
for endpoint in endpoints_to_try:
logger.debug(f"Trying endpoint: {endpoint}")
result = await self._make_request('GET', endpoint)
if result:
logger.info(f"Successfully got subscription info from {endpoint}")
subscription_data = None
if 'response' in result:
subscription_data = result['response']
elif 'data' in result:
subscription_data = result['data']
elif 'subscription' in result:
subscription_data = result['subscription']
else:
subscription_data = result
if subscription_data and (
'subscriptionUrl' in subscription_data or
'url' in subscription_data or
'link' in subscription_data
):
return subscription_data
logger.warning(f"Could not get subscription info for {short_uuid} from any endpoint")
return None
except Exception as e:
logger.error(f"Error getting subscription info for {short_uuid}: {e}")
return None
async def get_subscription_url(self, short_uuid: str) -> str:
try:
logger.info(f"Getting subscription URL for short_uuid: {short_uuid}")
subscription_info = await self.get_subscription_info(short_uuid)
if subscription_info:
subscription_url = (
subscription_info.get('subscriptionUrl') or
subscription_info.get('url') or
subscription_info.get('link') or
subscription_info.get('subscription_url')
)
if subscription_url:
logger.info(f"Got subscription URL from API: {subscription_url}")
return subscription_url
user_data = await self.get_user_by_short_uuid(short_uuid)
if user_data and 'subscriptionUrl' in user_data:
logger.info(f"Got subscription URL from user data: {user_data['subscriptionUrl']}")
return user_data['subscriptionUrl']
if self.subscription_base_url:
fallback_url = f"{self.subscription_base_url.rstrip('/')}/sub/{short_uuid}"
logger.warning(f"Using fallback URL: {fallback_url}")
return fallback_url
else:
fallback_url = f"{self.base_url.rstrip('/')}/sub/{short_uuid}"
logger.warning(f"Using base_url fallback: {fallback_url}")
return fallback_url
except Exception as e:
logger.error(f"Failed to get subscription URL for {short_uuid}: {e}")
fallback_url = f"{self.base_url.rstrip('/')}/sub/{short_uuid}"
return fallback_url
async def get_user_by_short_uuid(self, short_uuid: str) -> Optional[Dict]:
logger.debug(f"Getting user by short UUID: {short_uuid}")
result = await self._make_request('GET', f'/api/users/by-short-uuid/{short_uuid}')
@@ -321,6 +394,43 @@ class RemnaWaveAPI:
return user_data
return None
async def get_all_subscriptions_with_urls(self) -> Optional[List]:
try:
logger.info("Fetching all subscriptions with URLs from API")
result = await self._make_request('GET', '/api/subscriptions')
if not result:
logger.error("Empty response from subscriptions API")
return []
subscriptions_list = []
if 'response' in result and 'subscriptions' in result['response']:
subscriptions_list = result['response']['subscriptions']
elif 'subscriptions' in result:
subscriptions_list = result['subscriptions']
elif 'data' in result:
subscriptions_list = result['data']
elif isinstance(result, list):
subscriptions_list = result
processed_subscriptions = []
for subscription in subscriptions_list:
if subscription.get('isFound') and 'user' in subscription:
user_data = subscription['user']
if 'subscriptionUrl' not in subscription and user_data.get('shortUuid'):
subscription['subscriptionUrl'] = await self.get_subscription_url(user_data['shortUuid'])
processed_subscriptions.append(subscription)
logger.info(f"Processed {len(processed_subscriptions)} subscriptions with URLs")
return processed_subscriptions
except Exception as e:
logger.error(f"Exception in get_all_subscriptions_with_urls: {e}", exc_info=True)
return []
async def update_user(self, uuid: str, data: Dict) -> Optional[Dict]:
update_data = {'uuid': uuid}
-218
View File
@@ -1,218 +0,0 @@
#!/bin/bash
# === RemnaWave Bot Setup & Management Script ===
SERVICE_NAME="remnawave-bot"
BOT_FILE="main.py"
ENV_FILE=".env"
VENV_DIR="venv"
# Select language
echo "🌐 Select language / Выберите язык:"
echo "1) English"
echo "2) Русский"
read -p ">>> " LANG_CHOICE
case $LANG_CHOICE in
2) LANG_CODE="ru" ;;
*) LANG_CODE="en" ;;
esac
# === Internationalized messages ===
msg() {
case $1 in
check_python) [[ $LANG_CODE == "ru" ]] && echo "🔍 Проверка Python..." || echo "🔍 Checking Python..." ;;
python_missing) [[ $LANG_CODE == "ru" ]] && echo "❌ Python 3 не установлен. Установите Python 3.8+." || echo "❌ Python 3 is not installed. Please install Python 3.8+." ;;
version) echo "📋 Python version: $PYTHON_VERSION" ;;
creating_venv) [[ $LANG_CODE == "ru" ]] && echo "🔧 Создание виртуального окружения..." || echo "🔧 Creating virtual environment..." ;;
activating_venv) [[ $LANG_CODE == "ru" ]] && echo "🔧 Активация виртуального окружения..." || echo "🔧 Activating virtual environment..." ;;
upgrading_pip) [[ $LANG_CODE == "ru" ]] && echo "📦 Обновление pip..." || echo "📦 Upgrading pip..." ;;
installing_requirements) [[ $LANG_CODE == "ru" ]] && echo "📦 Установка зависимостей..." || echo "📦 Installing requirements..." ;;
env_missing) [[ $LANG_CODE == "ru" ]] && echo "⚠️ Файл .env не найден!" || echo "⚠️ .env file not found!" ;;
env_created) [[ $LANG_CODE == "ru" ]] && echo "📋 Создан .env файл. Пожалуйста, отредактируйте его." || echo "📋 .env created. Please edit it." ;;
validating) [[ $LANG_CODE == "ru" ]] && echo "🔍 Проверка конфигурации..." || echo "🔍 Validating configuration..." ;;
config_ok) [[ $LANG_CODE == "ru" ]] && echo "✅ Конфигурация прошла проверку." || echo "✅ Configuration validated successfully." ;;
creating_service) [[ $LANG_CODE == "ru" ]] && echo "🔧 Создание systemd-сервиса..." || echo "🔧 Creating systemd service..." ;;
service_created) [[ $LANG_CODE == "ru" ]] && echo "✅ Сервис создан и включён." || echo "✅ Service created and enabled." ;;
start_prompt) [[ $LANG_CODE == "ru" ]] && echo "🚀 Запуск бота..." || echo "🚀 Starting the bot..." ;;
already_running) [[ $LANG_CODE == "ru" ]] && echo "🟢 Бот уже запущен!" || echo "🟢 Bot is already running!" ;;
last_logs) [[ $LANG_CODE == "ru" ]] && echo "📄 Последние 20 строк лога:" || echo "📄 Last 20 log lines:" ;;
action_menu)
[[ $LANG_CODE == "ru" ]] && {
echo -e "\nЧто вы хотите сделать?"
echo "1) Остановить бота"
echo "2) Перезапустить бота"
echo "3) Выйти"
} || {
echo -e "\nWhat would you like to do?"
echo "1) Stop the bot"
echo "2) Restart the bot"
echo "3) Exit"
}
;;
restarting) [[ $LANG_CODE == "ru" ]] && echo "🔄 Перезапуск бота..." || echo "🔄 Restarting bot..." ;;
completed)
echo ""
[[ $LANG_CODE == "ru" ]] && {
echo "🎉 Установка завершена!"
echo "📋 Возможности бота:"
echo " ✅ Мультиязычный интерфейс"
echo " ✅ Управление балансом и подписками"
echo " ✅ Промокоды и поддержка"
echo ""
} || {
echo "🎉 Installation complete!"
echo "📋 Bot Features:"
echo " ✅ Multi-language support"
echo " ✅ Subscription and balance management"
echo " ✅ Promocode system and support"
echo ""
}
;;
esac
}
# === Check if bot is running ===
if systemctl is-active --quiet "$SERVICE_NAME"; then
msg already_running
msg last_logs
sudo journalctl -u "$SERVICE_NAME" -n 20 --no-pager
msg action_menu
read -p ">>> " choice
case $choice in
1)
[[ $LANG_CODE == "ru" ]] && echo "🛑 Остановка бота..." || echo "🛑 Stopping the bot..."
sudo systemctl stop "$SERVICE_NAME"
[[ $LANG_CODE == "ru" ]] && echo "✅ Бот остановлен." || echo "✅ Bot stopped."
exit 0
;;
2)
msg restarting
sudo systemctl restart "$SERVICE_NAME"
[[ $LANG_CODE == "ru" ]] && echo "✅ Бот перезапущен." || echo "✅ Bot restarted."
exit 0
;;
3)
[[ $LANG_CODE == "ru" ]] && echo "👋 Выход." || echo "👋 Exiting."
exit 0
;;
*)
[[ $LANG_CODE == "ru" ]] && echo "⚠️ Неверный выбор." || echo "⚠️ Invalid choice."
exit 1
;;
esac
fi
# === Install/Setup ===
msg check_python
if ! command -v python3 &> /dev/null; then
msg python_missing
exit 1
fi
PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
msg version
if [ ! -d "$VENV_DIR" ]; then
msg creating_venv
python3 -m venv "$VENV_DIR"
fi
msg activating_venv
source "$VENV_DIR/bin/activate"
msg upgrading_pip
pip install --upgrade pip
msg installing_requirements
pip install -r requirements.txt
# Create .env if needed
if [ ! -f "$ENV_FILE" ]; then
msg env_missing
cat > "$ENV_FILE" <<EOF
# Bot Configuration
BOT_TOKEN=
# RemnaWave API Configuration
REMNAWAVE_URL=
REMNAWAVE_MODE=
REMNAWAVE_TOKEN=
# Admin Configuration (comma-separated list of Telegram user IDs)
ADMIN_IDS=
# Support Configuration
SUPPORT_USERNAME=support
# Database Configuration (optional, defaults to SQLite)
# DATABASE_URL=sqlite+aiosqlite:///bot.db
# Trial Configuration
TRIAL_ENABLED=true
TRIAL_DURATION_DAYS=3
TRIAL_TRAFFIC_GB=2
TRIAL_SQUAD_UUID=
TRIAL_PRICE=0.0
EOF
msg env_created
echo ""
read -p "🔧 Press Enter after configuring .env / Нажмите Enter после настройки .env..."
fi
# Load env
export $(grep -v '^#' "$ENV_FILE" | xargs)
msg validating
if [[ -z "$BOT_TOKEN" || "$BOT_TOKEN" == "your_telegram_bot_token_here" ]]; then
echo "❌ BOT_TOKEN not set properly."
exit 1
fi
if [[ -z "$REMNAWAVE_URL" || "$REMNAWAVE_URL" == "https://your-panel.com" ]]; then
echo "❌ REMNAWAVE_URL not set properly."
exit 1
fi
if [[ -z "$REMNAWAVE_TOKEN" || "$REMNAWAVE_TOKEN" == "your_jwt_token_here" ]]; then
echo "❌ REMNAWAVE_TOKEN not set properly."
exit 1
fi
msg config_ok
# Create systemd service
read -p "🔧 Create systemd service? (y/n): " CREATE_SERVICE
if [[ "$CREATE_SERVICE" == "y" || "$CREATE_SERVICE" == "Y" ]]; then
msg creating_service
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
CURRENT_DIR=$(pwd)
CURRENT_USER=$(whoami)
sudo tee "$SERVICE_FILE" > /dev/null <<EOF
[Unit]
Description=RemnaWave Bot
After=network.target
[Service]
Type=simple
User=$CURRENT_USER
WorkingDirectory=$CURRENT_DIR
Environment=PATH=$CURRENT_DIR/$VENV_DIR/bin
EnvironmentFile=$CURRENT_DIR/.env
ExecStart=$CURRENT_DIR/$VENV_DIR/bin/python3 $BOT_FILE
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable "$SERVICE_NAME"
msg service_created
fi
msg completed
msg start_prompt
python3 "$BOT_FILE"
Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

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

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

+348
View File
@@ -0,0 +1,348 @@
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, LabeledPrice, PreCheckoutQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.exceptions import TelegramBadRequest
from datetime import datetime
import logging
from typing import Dict
from database import Database, User
from keyboards import stars_topup_keyboard, stars_payment_keyboard, balance_keyboard
from translations import t
from utils import log_user_action
from referral_utils import process_referral_rewards
logger = logging.getLogger(__name__)
stars_router = Router()
@stars_router.callback_query(F.data == "topup_stars")
async def topup_stars_callback(callback: CallbackQuery, **kwargs):
"""Показать варианты пополнения через звезды"""
user = kwargs.get('user')
config = kwargs.get('config')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
if not config or not config.STARS_ENABLED:
await callback.message.edit_text(
"❌ Пополнение через Telegram Stars временно недоступно",
reply_markup=balance_keyboard(user.language)
)
return
if not config.STARS_RATES:
await callback.message.edit_text(
"❌ Курсы звезд не настроены",
reply_markup=balance_keyboard(user.language)
)
return
text = "⭐ **Пополнение через Telegram Stars**\n\n"
text += "🚀 **Преимущества:**\n"
text += "• Мгновенное зачисление\n"
text += "• Безопасные платежи через Telegram\n"
text += "• Без комиссий и скрытых платежей\n\n"
text += "💎 **Доступные варианты:**\n"
sorted_rates = sorted(config.STARS_RATES.items())
for stars, rubles in sorted_rates:
rate_per_star = rubles / stars
if stars >= 500:
bonus_text = " 🔥 Выгодно!"
elif stars >= 250:
bonus_text = " 💎 Хорошо!"
else:
bonus_text = ""
text += f"{stars} ⭐ → {rubles:.0f}{bonus_text}\n"
text += f"\n💡 Выберите подходящий вариант:"
await callback.message.edit_text(
text,
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
@stars_router.callback_query(F.data.startswith("buy_stars_"))
async def buy_stars_callback(callback: CallbackQuery, db: Database, **kwargs):
"""Обработка покупки звезд"""
user = kwargs.get('user')
config = kwargs.get('config')
bot = kwargs.get('bot')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
if not config or not config.STARS_ENABLED or not config.STARS_RATES:
await callback.answer("❌ Пополнение недоступно")
return
try:
stars_amount = int(callback.data.split("_")[2])
if stars_amount not in config.STARS_RATES:
await callback.answer("❌ Неверное количество звезд")
return
rub_amount = config.STARS_RATES[stars_amount]
star_payment = await db.create_star_payment(
user_id=user.telegram_id,
stars_amount=stars_amount,
rub_amount=rub_amount
)
prices = [LabeledPrice(label=f"Пополнение на {rub_amount:.0f}", amount=stars_amount)]
try:
await callback.answer("💳 Создаю платеж...")
await bot.send_invoice(
chat_id=callback.message.chat.id,
title=f"Пополнение баланса",
description=f"Пополнение баланса на {rub_amount:.0f}₽ за {stars_amount}",
payload=f"star_payment_{star_payment.id}",
currency="XTR",
prices=prices
)
try:
await callback.message.edit_text(
f"💳 **Оплата через Telegram Stars**\n\n"
f"⭐ Количество звезд: {stars_amount}\n"
f"💰 Сумма пополнения: {rub_amount:.0f}\n\n"
f"👆 Нажмите кнопку \"Оплатить\" в инвойсе выше\n\n"
f"❌ Если передумали - нажмите кнопку ниже",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отменить платеж", callback_data=f"cancel_star_payment_{star_payment.id}")],
[InlineKeyboardButton(text="🔙 Назад к выбору", callback_data="topup_stars")]
]),
parse_mode='Markdown'
)
except TelegramBadRequest:
await callback.message.answer(
f"💳 **Оплата через Telegram Stars**\n\n"
f"⭐ Количество звезд: {stars_amount}\n"
f"💰 Сумма пополнения: {rub_amount:.0f}\n\n"
f"👆 Нажмите кнопку \"Оплатить\" в инвойсе выше",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отменить платеж", callback_data=f"cancel_star_payment_{star_payment.id}")],
[InlineKeyboardButton(text="🔙 Назад к выбору", callback_data="topup_stars")]
]),
parse_mode='Markdown'
)
except TelegramBadRequest as e:
logger.error(f"Failed to send invoice: {e}")
await callback.answer("❌ Ошибка создания платежа", show_alert=True)
try:
await callback.message.edit_text(
"❌ Ошибка создания платежа. Попробуйте позже.",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language)
)
except TelegramBadRequest:
await callback.message.answer(
"❌ Ошибка создания платежа. Попробуйте позже.",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language)
)
await db.cancel_star_payment(star_payment.id)
except (ValueError, IndexError) as e:
logger.error(f"Error processing stars amount: {e}")
await callback.answer("❌ Ошибка обработки")
except Exception as e:
logger.error(f"Error in buy_stars_callback: {e}")
await callback.answer("❌ Произошла ошибка")
@stars_router.pre_checkout_query()
async def pre_checkout_query_handler(pre_checkout_query: PreCheckoutQuery, db: Database, **kwargs):
"""Обработка pre-checkout запроса для звезд"""
try:
if not pre_checkout_query.invoice_payload.startswith("star_payment_"):
await pre_checkout_query.answer(ok=False, error_message="Неверный формат платежа")
return
payment_id = int(pre_checkout_query.invoice_payload.split("_")[2])
star_payment = await db.get_star_payment_by_id(payment_id)
if not star_payment:
await pre_checkout_query.answer(ok=False, error_message="Платеж не найден")
return
if star_payment.status != 'pending':
await pre_checkout_query.answer(ok=False, error_message="Платеж уже обработан")
return
if pre_checkout_query.total_amount != star_payment.stars_amount:
await pre_checkout_query.answer(ok=False, error_message="Неверная сумма платежа")
return
await pre_checkout_query.answer(ok=True)
except Exception as e:
logger.error(f"Error in pre_checkout_query_handler: {e}")
await pre_checkout_query.answer(ok=False, error_message="Внутренняя ошибка")
@stars_router.message(F.successful_payment)
async def successful_payment_handler(message: Message, db: Database, **kwargs):
"""Обработка успешного платежа звездами"""
user = kwargs.get('user')
bot = kwargs.get('bot')
if not user:
logger.error("User not found in successful payment handler")
return
try:
payment_info = message.successful_payment
if not payment_info.invoice_payload.startswith("star_payment_"):
logger.error(f"Invalid payment payload: {payment_info.invoice_payload}")
return
payment_id = int(payment_info.invoice_payload.split("_")[2])
success = await db.complete_star_payment(
payment_id=payment_id,
telegram_payment_charge_id=payment_info.telegram_payment_charge_id
)
if success:
star_payment = await db.get_star_payment_by_id(payment_id)
if star_payment:
updated_user = await db.get_user_by_telegram_id(user.telegram_id)
success_text = "✅ **Платеж успешно обработан!**\n\n"
success_text += f"⭐ Оплачено звезд: {star_payment.stars_amount}\n"
success_text += f"💰 Зачислено: {star_payment.rub_amount:.0f}\n"
success_text += f"💳 Текущий баланс: {updated_user.balance:.0f}\n\n"
success_text += "🎉 Средства уже доступны на вашем балансе!"
await message.answer(
success_text,
parse_mode='Markdown'
)
if bot:
try:
regular_payment = await db.create_payment(
user_id=user.telegram_id,
amount=star_payment.rub_amount,
payment_type='stars',
description=f'Пополнение через Telegram Stars ({star_payment.stars_amount} ⭐)',
status='completed'
)
await process_referral_rewards(
user.telegram_id,
star_payment.rub_amount,
regular_payment.id,
db,
bot,
payment_type='stars'
)
except Exception as ref_error:
logger.error(f"Error processing referral rewards for stars payment: {ref_error}")
log_user_action(user.telegram_id, "stars_payment_completed",
f"Stars: {star_payment.stars_amount}, Amount: {star_payment.rub_amount}")
else:
await message.answer("❌ Ошибка получения данных платежа")
else:
await message.answer("❌ Ошибка обработки платежа. Обратитесь в поддержку.")
logger.error(f"Failed to complete star payment {payment_id}")
except Exception as e:
logger.error(f"Error in successful_payment_handler: {e}")
await message.answer("❌ Произошла ошибка при обработке платежа. Обратитесь в поддержку.")
def get_stars_rate_info(stars_rates: Dict[int, float], lang: str = 'ru') -> str:
"""Получить информацию о курсах звезд"""
if not stars_rates:
return "Курсы не настроены"
text = "⭐ **Курсы Telegram Stars:**\n\n"
sorted_rates = sorted(stars_rates.items())
for stars, rubles in sorted_rates:
rate_per_star = rubles / stars
text += f"{stars} ⭐ = {rubles:.0f}₽ (курс: {rate_per_star:.2f}₽ за ⭐)\n"
return text
@stars_router.callback_query(F.data.startswith("cancel_star_payment_"))
async def cancel_star_payment_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
payment_id = int(callback.data.split("_")[-1])
star_payment = await db.get_star_payment_by_id(payment_id)
if not star_payment:
await callback.answer("❌ Платеж не найден")
return
if star_payment.user_id != user.telegram_id:
await callback.answer("❌ Нет доступа к этому платежу")
return
if star_payment.status != 'pending':
await callback.answer("❌ Платеж уже обработан")
return
success = await db.cancel_star_payment(payment_id)
if success:
await callback.answer("✅ Платеж отменен", show_alert=True)
if config and config.STARS_RATES:
try:
await callback.message.edit_text(
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
except TelegramBadRequest:
await callback.message.answer(
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
else:
try:
await callback.message.edit_text(
"❌ Платеж отменен",
reply_markup=balance_keyboard(user.language)
)
except TelegramBadRequest:
await callback.message.answer(
"❌ Платеж отменен",
reply_markup=balance_keyboard(user.language)
)
log_user_action(user.telegram_id, "stars_payment_cancelled", f"Payment ID: {payment_id}")
else:
await callback.answer("❌ Ошибка отмены платежа")
except (ValueError, IndexError) as e:
logger.error(f"Error parsing payment ID from callback: {e}")
await callback.answer("❌ Ошибка обработки")
except Exception as e:
logger.error(f"Error in cancel_star_payment_callback: {e}")
await callback.answer("❌ Произошла ошибка")
+220 -2
View File
@@ -1,7 +1,7 @@
import asyncio
import logging
from datetime import datetime, timedelta, timezone
from typing import Optional, List
from typing import Optional, List, Dict, Any
from database import Database
from remnawave_api import RemnaWaveAPI
@@ -148,6 +148,206 @@ class SubscriptionMonitorService:
except Exception as e:
logger.error(f"❌ Error in daily loop: {e}", exc_info=True)
await asyncio.sleep(3600) # Wait 1 hour before retry
async def delete_expired_trial_subscriptions(self, force: bool = False) -> Dict[str, Any]:
"""
Удаляет истекшие триальные подписки
"""
try:
logger.info("🗑️ Starting deletion of expired trial subscriptions...")
now_utc = datetime.utcnow()
delete_threshold_days = getattr(self.config, 'DELETE_EXPIRED_TRIAL_DAYS', 1)
cutoff_date = now_utc - timedelta(days=delete_threshold_days)
logger.info(f"🗑️ Deleting trial subscriptions expired before: {cutoff_date} (older than {delete_threshold_days} days)")
results = {
'total_checked': 0,
'deleted_from_db': 0,
'deleted_from_api': 0,
'errors': [],
'deleted_subscriptions': []
}
all_users = await self.db.get_all_users()
for user in all_users:
try:
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
for user_sub in user_subs:
results['total_checked'] += 1
# Получаем информацию о подписке
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if not subscription or not subscription.is_trial:
continue
# Проверяем, истекла ли подписка достаточно давно
expires_at_utc = user_sub.expires_at
if expires_at_utc.tzinfo is None:
expires_at_utc = expires_at_utc.replace(tzinfo=None)
else:
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
if expires_at_utc > cutoff_date and not force:
continue # Подписка еще не готова к удалению
logger.info(f"🗑️ Deleting expired trial subscription '{subscription.name}' for user {user.telegram_id} "
f"(expired: {expires_at_utc}, cutoff: {cutoff_date})")
# Удаляем из RemnaWave API
api_deleted = False
if self.api and user_sub.short_uuid:
try:
api_result = await self.api.delete_user_by_short_uuid(user_sub.short_uuid)
if api_result and api_result.get('success'):
api_deleted = True
results['deleted_from_api'] += 1
logger.info(f"✅ Deleted from RemnaWave API: {user_sub.short_uuid}")
else:
results['errors'].append(f"Failed to delete {user_sub.short_uuid} from API")
logger.warning(f"⚠️ Failed to delete {user_sub.short_uuid} from API")
except Exception as api_error:
results['errors'].append(f"API error for {user_sub.short_uuid}: {str(api_error)}")
logger.error(f"❌ API error deleting {user_sub.short_uuid}: {api_error}")
# Удаляем из базы данных
db_deleted = await self.db.delete_user_subscription(user_sub.id)
if db_deleted:
results['deleted_from_db'] += 1
results['deleted_subscriptions'].append({
'user_id': user.telegram_id,
'subscription_name': subscription.name,
'short_uuid': user_sub.short_uuid,
'expired_at': expires_at_utc.isoformat(),
'deleted_from_api': api_deleted,
'deleted_from_db': True
})
logger.info(f"✅ Deleted from database: subscription ID {user_sub.id}")
else:
results['errors'].append(f"Failed to delete subscription ID {user_sub.id} from database")
logger.error(f"❌ Failed to delete subscription ID {user_sub.id} from database")
except Exception as user_error:
results['errors'].append(f"Error processing user {user.telegram_id}: {str(user_error)}")
logger.error(f"❌ Error processing user {user.telegram_id}: {user_error}")
logger.info(f"🗑️ Trial deletion completed: {results['deleted_from_db']} from DB, {results['deleted_from_api']} from API")
return results
except Exception as e:
logger.error(f"❌ Error in delete_expired_trial_subscriptions: {e}", exc_info=True)
return {
'total_checked': 0,
'deleted_from_db': 0,
'deleted_from_api': 0,
'errors': [f"Critical error: {str(e)}"],
'deleted_subscriptions': []
}
async def delete_expired_regular_subscriptions(self, force: bool = False) -> Dict[str, Any]:
"""
Удаляет истекшие обычные подписки
"""
try:
logger.info("🗑️ Starting deletion of expired regular subscriptions...")
now_utc = datetime.utcnow()
delete_threshold_days = getattr(self.config, 'DELETE_EXPIRED_REGULAR_DAYS', 7)
cutoff_date = now_utc - timedelta(days=delete_threshold_days)
logger.info(f"🗑️ Deleting regular subscriptions expired before: {cutoff_date} (older than {delete_threshold_days} days)")
results = {
'total_checked': 0,
'deleted_from_db': 0,
'deleted_from_api': 0,
'errors': [],
'deleted_subscriptions': []
}
all_users = await self.db.get_all_users()
for user in all_users:
try:
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
for user_sub in user_subs:
results['total_checked'] += 1
# Получаем информацию о подписке
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if not subscription or subscription.is_trial:
continue
# Пропускаем импортированные подписки (они не удаляются)
if getattr(subscription, 'is_imported', False) or subscription.name == "Старая подписка":
continue
# Проверяем, истекла ли подписка достаточно давно
expires_at_utc = user_sub.expires_at
if expires_at_utc.tzinfo is None:
expires_at_utc = expires_at_utc.replace(tzinfo=None)
else:
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
if expires_at_utc > cutoff_date and not force:
continue # Подписка еще не готова к удалению
logger.info(f"🗑️ Deleting expired regular subscription '{subscription.name}' for user {user.telegram_id} "
f"(expired: {expires_at_utc}, cutoff: {cutoff_date})")
# Удаляем из RemnaWave API
api_deleted = False
if self.api and user_sub.short_uuid:
try:
api_result = await self.api.delete_user_by_short_uuid(user_sub.short_uuid)
if api_result and api_result.get('success'):
api_deleted = True
results['deleted_from_api'] += 1
logger.info(f"✅ Deleted from RemnaWave API: {user_sub.short_uuid}")
else:
results['errors'].append(f"Failed to delete {user_sub.short_uuid} from API")
logger.warning(f"⚠️ Failed to delete {user_sub.short_uuid} from API")
except Exception as api_error:
results['errors'].append(f"API error for {user_sub.short_uuid}: {str(api_error)}")
logger.error(f"❌ API error deleting {user_sub.short_uuid}: {api_error}")
# Удаляем из базы данных
db_deleted = await self.db.delete_user_subscription(user_sub.id)
if db_deleted:
results['deleted_from_db'] += 1
results['deleted_subscriptions'].append({
'user_id': user.telegram_id,
'subscription_name': subscription.name,
'short_uuid': user_sub.short_uuid,
'expired_at': expires_at_utc.isoformat(),
'deleted_from_api': api_deleted,
'deleted_from_db': True
})
logger.info(f"✅ Deleted from database: subscription ID {user_sub.id}")
else:
results['errors'].append(f"Failed to delete subscription ID {user_sub.id} from database")
logger.error(f"❌ Failed to delete subscription ID {user_sub.id} from database")
except Exception as user_error:
results['errors'].append(f"Error processing user {user.telegram_id}: {str(user_error)}")
logger.error(f"❌ Error processing user {user.telegram_id}: {user_error}")
logger.info(f"🗑️ Regular deletion completed: {results['deleted_from_db']} from DB, {results['deleted_from_api']} from API")
return results
except Exception as e:
logger.error(f"❌ Error in delete_expired_regular_subscriptions: {e}", exc_info=True)
return {
'total_checked': 0,
'deleted_from_db': 0,
'deleted_from_api': 0,
'errors': [f"Critical error: {str(e)}"],
'deleted_subscriptions': []
}
async def _check_expiring_subscriptions(self):
try:
@@ -336,11 +536,26 @@ class SubscriptionMonitorService:
deactivated_count = await self.deactivate_expired_subscriptions()
logger.info(f"🔄 Deactivated {deactivated_count} expired subscriptions")
# Автоматическое удаление, если включено в конфиге
deleted_trials = 0
deleted_regular = 0
if getattr(self.config, 'AUTO_DELETE_ENABLED', False):
logger.info("🗑️ Auto-deletion enabled, deleting expired subscriptions...")
trial_result = await self.delete_expired_trial_subscriptions()
deleted_trials = trial_result.get('deleted_from_db', 0)
regular_result = await self.delete_expired_regular_subscriptions()
deleted_regular = regular_result.get('deleted_from_db', 0)
logger.info(f"🗑️ Auto-deleted {deleted_trials} trial and {deleted_regular} regular subscriptions")
logger.info("📩 Sending final expiry notifications...")
await self._send_final_expiry_notifications()
logger.info("📩 Final notifications sent")
logger.info(f"✅ Daily check completed successfully. Warnings: {warnings_sent}, Deactivated: {deactivated_count}")
logger.info(f"✅ Daily check completed successfully. Warnings: {warnings_sent}, Deactivated: {deactivated_count}, "
f"Deleted trials: {deleted_trials}, Deleted regular: {deleted_regular}")
return deactivated_count
except Exception as e:
@@ -589,6 +804,9 @@ class SubscriptionMonitorService:
'check_interval': self.config.MONITOR_CHECK_INTERVAL,
'daily_check_hour': self.config.MONITOR_DAILY_CHECK_HOUR,
'warning_days': self.config.MONITOR_WARNING_DAYS,
'delete_trial_days': getattr(self.config, 'DELETE_EXPIRED_TRIAL_DAYS', 1),
'delete_regular_days': getattr(self.config, 'DELETE_EXPIRED_REGULAR_DAYS', 7),
'auto_delete_enabled': getattr(self.config, 'AUTO_DELETE_ENABLED', False),
'last_check': self.last_check_time.strftime("%Y-%m-%d %H:%M:%S") if self.last_check_time else None,
'has_tasks': {
'monitor_task': monitor_running,
-248
View File
@@ -3,8 +3,6 @@ import secrets
import string
from datetime import datetime, timedelta
from typing import Tuple, Optional, Dict, Any
from database import Database
from database import ReferralProgram, ReferralEarning
logger = logging.getLogger(__name__)
@@ -245,252 +243,6 @@ def format_user_subscription_info(user_sub: Dict[str, Any], subscription: Dict[s
def log_user_action(user_id: int, action: str, details: str = ""):
logger.info(f"USER_ACTION: {user_id} - {action}" + (f" - {details}" if details else ""))
async def process_referral_rewards(user_id: int, amount: float, payment_id: int, db: Database, bot=None):
try:
import os
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
first_reward = float(os.getenv('REFERRAL_FIRST_REWARD', '150.0'))
referred_bonus = float(os.getenv('REFERRAL_REFERRED_BONUS', '150.0'))
percentage = float(os.getenv('REFERRAL_PERCENTAGE', '0.25'))
referral = await db.get_referral_by_referred_id(user_id)
if not referral:
logger.debug(f"No referral found for user {user_id}")
return
user = await db.get_user_by_telegram_id(user_id)
if not user:
logger.error(f"User {user_id} not found")
return
logger.info(f"Processing referral rewards for user {user_id}, amount {amount}, referrer {referral.referrer_id}")
if not referral.first_reward_paid and user.balance >= threshold:
logger.info(f"Processing first reward for referral {referral.id} (threshold: {threshold}, reward: {first_reward})")
await db.add_balance(referral.referrer_id, first_reward)
await db.create_payment(
user_id=referral.referrer_id,
amount=first_reward,
payment_type='referral',
description=f'Первая награда за реферала ID:{user_id}',
status='completed'
)
success = await db.create_referral_earning(
referrer_id=referral.referrer_id,
referred_id=user_id,
amount=first_reward,
earning_type='first_reward',
related_payment_id=payment_id
)
if success:
logger.info(f"First reward paid: {first_reward}₽ to referrer {referral.referrer_id}")
if bot:
try:
await bot.send_message(
referral.referrer_id,
f"🎉 Поздравляем! Ваш реферал пополнил баланс на {threshold}₽+\n\n"
f"💰 Вам начислено {first_reward}₽ за приведенного друга!\n"
f"Теперь вы будете получать {percentage*100:.0f}% с каждого его следующего платежа."
)
await bot.send_message(
user_id,
f"🎁 Бонус активирован! Вам начислено {referred_bonus}₽ за переход по реферальной ссылке!"
)
await db.add_balance(user_id, referred_bonus)
await db.create_payment(
user_id=user_id,
amount=referred_bonus,
payment_type='referral',
description='Бонус за переход по реферальной ссылке',
status='completed'
)
logger.info(f"Referral bonus notifications sent and balance updated")
except Exception as e:
logger.error(f"Failed to send referral notifications: {e}")
else:
logger.error(f"Failed to create first reward earning")
if amount > 0 and referral.first_reward_paid:
percentage_reward = amount * percentage
if percentage_reward >= 0.01:
await db.add_balance(referral.referrer_id, percentage_reward)
await db.create_payment(
user_id=referral.referrer_id,
amount=percentage_reward,
payment_type='referral',
description=f'{percentage*100:.0f}% дохода от реферала ID:{user_id}',
status='completed'
)
success = await db.create_referral_earning(
referrer_id=referral.referrer_id,
referred_id=user_id,
amount=percentage_reward,
earning_type='percentage',
related_payment_id=payment_id
)
if success:
logger.info(f"Percentage reward paid: {percentage_reward:.2f}₽ ({percentage*100:.0f}%) to referrer {referral.referrer_id}")
if bot and percentage_reward >= 1.0:
try:
await bot.send_message(
referral.referrer_id,
f"💰 Реферальный доход!\n\n"
f"Ваш реферал совершил платеж на {amount:.2f}\n"
f"Вам начислено: {percentage_reward:.2f}₽ ({percentage*100:.0f}%)"
)
except Exception as e:
logger.error(f"Failed to send percentage notification: {e}")
else:
logger.error(f"Failed to create percentage earning")
elif amount > 0 and not referral.first_reward_paid:
logger.info(f"Skipping percentage reward for user {user_id} - first reward not yet paid")
except Exception as e:
logger.error(f"Error processing referral rewards: {e}")
async def create_referral_from_start_param(user_telegram_id: int, start_param: str, db: Database, bot=None):
try:
if not start_param.startswith("ref_"):
return False
referrer_id = int(start_param.replace("ref_", ""))
if referrer_id == user_telegram_id:
logger.warning(f"User {user_telegram_id} tried to refer themselves")
return False
existing_referral = await db.get_referral_by_referred_id(user_telegram_id)
if existing_referral:
logger.info(f"User {user_telegram_id} already has referrer")
return False
referral_code = await db.generate_unique_referral_code(referrer_id)
referral = await db.create_referral(referrer_id, user_telegram_id, referral_code)
if referral:
logger.info(f"Created referral: {referrer_id} -> {user_telegram_id} with code {referral_code}")
if bot:
try:
referrer = await db.get_user_by_telegram_id(referrer_id)
if referrer:
await bot.send_message(
referrer_id,
f"🎉 Отлично! По вашей ссылке зарегистрировался новый пользователь!\n\n"
f"Вы получите 150₽ после того, как он пополнит баланс на 300₽.\n"
f"И будете получать 25% с каждого его платежа!"
)
except Exception as e:
logger.error(f"Failed to notify referrer: {e}")
return True
return False
except (ValueError, TypeError) as e:
logger.warning(f"Invalid referral parameter: {start_param}")
return False
except Exception as e:
logger.error(f"Error creating referral from start param: {e}")
return False
async def create_referral_from_promocode(user_telegram_id: int, referral_code: str, db: Database, bot=None):
try:
if not referral_code.startswith("REF"):
return False
logger.info(f"Trying to use referral code {referral_code} for user {user_telegram_id}")
async with db.session_factory() as session:
from sqlalchemy import select
result = await session.execute(
select(ReferralProgram).where(ReferralProgram.referral_code == referral_code)
)
referral_record = result.scalar_one_or_none()
if not referral_record:
logger.warning(f"No referrer found for code {referral_code}")
return False
referrer_id = referral_record.referrer_id
if referrer_id == user_telegram_id:
logger.warning(f"User {user_telegram_id} tried to use own referral code")
return False
existing_referral = await db.get_referral_by_referred_id(user_telegram_id)
if existing_referral:
logger.info(f"User {user_telegram_id} already has referrer")
return False
referral = await db.create_referral(referrer_id, user_telegram_id, referral_code)
if referral:
logger.info(f"Created referral from promocode: {referrer_id} -> {user_telegram_id}")
if bot:
try:
await bot.send_message(
referrer_id,
f"🎉 По вашему промокоду {referral_code} зарегистрировался новый пользователь!\n\n"
f"Вы получите 150₽ после того, как он пополнит баланс на 300₽."
)
except Exception as e:
logger.error(f"Failed to notify referrer: {e}")
return True
return False
except Exception as e:
logger.error(f"Error creating referral from promocode: {e}")
return False
def generate_referral_link(bot_username: str, user_id: int) -> str:
if not bot_username:
return ""
if bot_username.startswith('@'):
bot_username = bot_username[1:]
return f"https://t.me/{bot_username}?start=ref_{user_id}"
def validate_referral_code(code: str) -> bool:
if not code or not code.startswith("REF"):
return False
if len(code) < 4 or len(code) > 20:
return False
return True
def format_referral_stats(stats: dict, lang: str = 'ru') -> str:
if lang == 'ru':
return (f"👥 Приглашено: {stats['total_referrals']}\n"
f"✅ Активных: {stats['active_referrals']}\n"
f"💰 Заработано: {stats['total_earned']:.2f}")
else:
return (f"👥 Invited: {stats['total_referrals']}\n"
f"✅ Active: {stats['active_referrals']}\n"
f"💰 Earned: ${stats['total_earned']:.2f}")
def bytes_to_gb(bytes_value: int) -> float:
if not bytes_value or bytes_value == 0:
return 0.0