Compare commits

...

22 Commits

Author SHA1 Message Date
Fr1ngg 64b5d90d66 Update README.md 2025-08-11 03:00:54 +03:00
Fr1ngg 5a1fb32e1d Update admin_handlers.py 2025-08-11 02:58:19 +03:00
Fr1ngg c4d8108e8c Update config.py 2025-08-11 02:53:37 +03:00
Fr1ngg 1350d80083 Update database.py 2025-08-11 02:51:28 +03:00
Fr1ngg e0cc218007 Update handlers.py 2025-08-11 02:49:06 +03:00
Fr1ngg bcaf8f2247 Update keyboards.py 2025-08-11 02:48:36 +03:00
Fr1ngg d1190ed9dc Update stars_handlers.py 2025-08-11 02:47:05 +03:00
Fr1ngg 513d6037d4 Update subscription_monitor.py 2025-08-11 02:46:40 +03:00
Fr1ngg e86012b2ab Update translations.py 2025-08-11 02:46:11 +03:00
Fr1ngg 214e381dc5 Update README.md 2025-08-10 10:41:22 +03:00
Fr1ngg aa79cb670b Update .env.example 2025-08-10 10:30:33 +03:00
Fr1ngg f0106e15fa Create stars_handlers.py 2025-08-10 10:26:28 +03:00
Fr1ngg 1d194c6be2 Update .env.example 2025-08-10 10:18:03 +03:00
Fr1ngg 6917890007 Update admin_handlers.py 2025-08-10 10:15:34 +03:00
Fr1ngg 1c69de7ecd Update config.py 2025-08-10 10:13:14 +03:00
Fr1ngg 62188793d9 Update database.py 2025-08-10 10:12:21 +03:00
Fr1ngg 585ee2d42b Update handlers.py 2025-08-10 10:11:12 +03:00
Fr1ngg 2a58cfd45c Update keyboards.py 2025-08-10 10:10:54 +03:00
Fr1ngg 93003c5fe0 Update main.py 2025-08-10 10:10:27 +03:00
Fr1ngg 91b0787d72 Update README.md 2025-08-10 07:04:17 +03:00
Fr1ngg 315277d4b0 Create bedolaga.sh 2025-08-10 06:56:04 +03:00
Fr1ngg 1821c0d89d Delete run.sh 2025-08-10 06:54:40 +03:00
13 changed files with 3188 additions and 343 deletions
+22
View File
@@ -37,3 +37,25 @@ 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
+153 -3
View File
@@ -12,7 +12,7 @@
## 🚀 Особенности
### 👤 Для пользователей
- 💰 **Управление балансом** - пополнение через поддержку
- 💰 **Управление балансом** - Пополнение через Telegram Stars + пополнение через поддержку p2p
- 🛒 **Покупка подписок** - различные тарифные планы c возможностью индивидуальной настройки сквада у плана
- 📱 **Управление подписками** - просмотр, продление, получение ссылок
- 🎁 **Система промокодов** - денежные коды
@@ -20,6 +20,7 @@
- 🎰 **Игра удачи** - выигрывай бонусы каждые 24 часа
- 🆓 **Тестовая подписка** - бесплатная пробная версия с детальной конфигурацией
- 🌐 **Мультиязычность** - русский и английский
- 📋 **Правила сервиса**
### ⚙️ Для администраторов
- 📊 **Полная статистика** - пользователи, платежи, подписки
@@ -30,6 +31,7 @@
- 🔄 **Синхронизация данных** - импорт из RemnaWave
- 📨 **Рассылки** - уведомления пользователям
- 🔍 **Мониторинг подписок** - автоматические уведомления об истечении и зачистка подписок с истекщим сроком для триал и обычных подписок
- 📋 **Правила сервиса** - Настройка страниц из админ панели
## 📋 Требования
@@ -113,6 +115,151 @@ nano .env
| `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 конфигурация
#### 🚀 Минимальная конфигурация (рекомендуется)
@@ -521,8 +668,10 @@ docker-compose ps
| № | Задача | Статус | Версия | Приоритет | Описание |
|---|---------|--------|--------|-----------|----------|
| 1 | Мониторинг подписок | ✅ Done | v1.1.0 | 🔴 High | Служба оповещения об истечении срока и автоконтроль |
| 2 | Платежные шлюзы | 🎯 Planned | v1.4.0+ | 🔴 High | ЮKassa, Сбербанк, Tinkoff, Crypto, Звезды |
| 3 | Синхронизация RemnaWave | ✅ Done | v1.1.0 | 🟡 Medium | Импорт пользователей по Telegram ID |
| 2 | Платежные шлюзы | ✅ Done | v1.3.2 | 🔴 High | Звезды |
| 2.1 | Платежные шлюзы | 🎯 Planned | v1.4.0+ | 🔴 High | ЮKassa |
| 2.2 | Платежные шлюзы | 🎯 Planned | v1.4.0+ | 🟡 Medium | Сбербанк, Tinkoff, Crypto, Others |
| 3.4 | Синхронизация RemnaWave | ✅ Done | v1.1.0 | 🟡 Medium | Импорт пользователей по Telegram ID |
| 4 | Веб-панель управления | 🎯 Planned | v1.4.0+ | 🟡 Medium | Полнофункциональный веб-интерфейс |
| 5 | Управление промокодами | ✅ Done | v1.3.0 | 🟢 Low | Удаление, редактирование |
| 6 | Безопасное удаление подписок | 🚧 In Progress | v1.4.0 | 🟡 Medium | Архивирование вместо удаления |
@@ -531,6 +680,7 @@ docker-compose ps
| 9 | Рулетка | ✅ Done | v1.3.0 | 🟢 Low | Лудочка |
| 10 | Блокировка | 🚧 In Progress | v1.3.0+ | 🟢 Low | Бан/разбан юзеров |
| 11 | Удаление истекшик подписок из бота и панели | ✅ Done | v1.3.1 | 🔴 High | Дабы не плодить лишние записи в базе бота и панели ремны, да и в целом это логично)( |
| 12 | Правила сервиса | ✅ Done | v1.3.3 | 🟢 Low | Вывод правил сервиса в боте с конфигурацией через админку |
### 🚀 Хотите помочь?
+741 -8
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, ServiceRule
from remnawave_api import RemnaWaveAPI
from keyboards import *
from translations import t
@@ -430,7 +430,6 @@ async def toggle_subscription(callback: CallbackQuery, user: User, db: Database,
status = t('enabled', user.language) if sub.is_active else t('disabled', user.language)
await callback.answer(f"✅ Подписка «{sub.name}» {status}")
# Update the list
subs = await db.get_all_subscriptions(include_inactive=True)
await callback.message.edit_reply_markup(
reply_markup=admin_subscriptions_list_keyboard(subs, user.language)
@@ -589,7 +588,6 @@ async def delete_subscription(callback: CallbackQuery, user: User, db: Database,
else:
await callback.answer("❌ Ошибка удаления")
# Return to list
subs = await db.get_all_subscriptions(include_inactive=True)
if subs:
await callback.message.edit_text(
@@ -681,7 +679,6 @@ async def handle_balance_user_id(message: Message, state: FSMContext, user: User
await message.answer("❌ Неверный Telegram ID")
return
# Check if user exists
target_user = await db.get_user_by_telegram_id(telegram_id)
if not target_user:
await message.answer(t('user_not_found', user.language))
@@ -1046,7 +1043,7 @@ async def handle_promo_expiry(message: Message, state: FSMContext, user: User, d
else:
try:
days = int(expiry_input)
if days <= 0 or days > 3650: # Максимум 10 лет
if days <= 0 or days > 3650:
await message.answer("❌ Количество дней должно быть от 1 до 3650")
return
expires_at = datetime.utcnow() + timedelta(days=days)
@@ -1733,8 +1730,22 @@ async def confirm_deactivate_all_callback(callback: CallbackQuery, user: User, d
BotStates.admin_edit_user_expiry,
BotStates.admin_edit_user_traffic,
BotStates.admin_test_monitor_user,
BotStates.admin_rename_plans_confirm
BotStates.admin_rename_plans_confirm,
BotStates.waiting_rule_title,
BotStates.waiting_rule_content,
BotStates.waiting_rule_order,
BotStates.waiting_rule_edit_title,
BotStates.waiting_rule_edit_content,
BotStates.waiting_rule_edit_order
))
async def cancel_rule_editing(callback: CallbackQuery, state: FSMContext, user: User, **kwargs):
"""Отмена редактирования правил"""
await state.clear()
await callback.message.edit_text(
t('main_menu', user.language),
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
async def cancel_admin_action(callback: CallbackQuery, state: FSMContext, user: User, **kwargs):
await state.clear()
await callback.message.edit_text(
@@ -5627,7 +5638,7 @@ async def import_all_by_telegram_callback(callback: CallbackQuery, user: User, a
errors += 1
continue
expire_dt_naive = datetime.now() + timedelta(days=30) # Дефолт
expire_dt_naive = datetime.now() + timedelta(days=30)
if expire_at:
try:
if expire_at.endswith('Z'):
@@ -6285,6 +6296,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_(
@@ -6401,6 +6414,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,
@@ -6432,7 +6447,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]
@@ -6480,6 +6494,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))
@@ -6594,3 +6610,720 @@ 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
)
@admin_router.callback_query(F.data == "admin_rules")
async def admin_rules_callback(callback: CallbackQuery, user: User, **kwargs):
"""Главное меню управления правилами"""
if not await check_admin_access(callback, user):
return
await callback.message.edit_text(
"📜 Управление правилами сервиса\n\n"
"Здесь вы можете создавать, редактировать и управлять страницами правил сервиса, "
"которые видят пользователи в главном меню.",
reply_markup=admin_rules_keyboard(user.language)
)
@admin_router.callback_query(F.data == "admin_rules_list")
async def admin_rules_list_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
if not await check_admin_access(callback, user):
return
try:
rules = await db.get_all_service_rules(active_only=False)
if not rules:
await callback.message.edit_text(
"📜 Правила сервиса не созданы\n\n"
"Создайте первую страницу правил для пользователей.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="➕ Создать первую страницу", callback_data="admin_rules_create")],
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_rules")]
])
)
return
text = f"📜 Список правил сервиса ({len(rules)} страниц)\n\n"
for rule in rules:
status = "🟢 Активна" if rule.is_active else "🔴 Отключена"
text += f"{rule.page_order}. **{rule.title}**\n"
text += f" {status}\n"
text += f" Создано: {rule.created_at.strftime('%d.%m.%Y %H:%M')}\n\n"
await callback.message.edit_text(
text,
reply_markup=admin_rules_list_keyboard(rules, user.language),
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error listing service rules: {e}")
await callback.answer("❌ Ошибка загрузки правил")
@admin_router.callback_query(F.data == "admin_rules_create")
async def admin_rules_create_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
if not await check_admin_access(callback, user):
return
await callback.message.edit_text(
"📝 Создание новой страницы правил\n\n"
"Введите заголовок страницы (например: 'Общие положения', 'Правила использования'):",
reply_markup=cancel_keyboard(user.language)
)
await state.set_state(BotStates.waiting_rule_title)
@admin_router.message(StateFilter(BotStates.waiting_rule_title))
async def handle_rule_title(message: Message, state: FSMContext, user: User, **kwargs):
title = message.text.strip()
if len(title) < 3 or len(title) > 200:
await message.answer("❌ Заголовок должен быть от 3 до 200 символов")
return
await state.update_data(rule_title=title)
await message.answer(
f"✅ Заголовок установлен: **{title}**\n\n"
"📝 Теперь введите содержимое страницы правил:\n\n"
"💡 Вы можете использовать форматирование Markdown:\n"
"• **жирный текст**\n"
"• *курсив*\n"
"• `код`\n"
"• [ссылка](url)\n\n"
"Максимальная длина: 3500 символов",
reply_markup=cancel_keyboard(user.language),
parse_mode='Markdown'
)
await state.set_state(BotStates.waiting_rule_content)
@admin_router.message(StateFilter(BotStates.waiting_rule_content))
async def handle_rule_content(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
content = message.text.strip()
if len(content) < 10:
await message.answer("❌ Содержимое должно быть не менее 10 символов")
return
if len(content) > 3500:
await message.answer("❌ Содержимое слишком длинное. Максимум 3500 символов.")
return
try:
data = await state.get_data()
title = data.get('rule_title')
rule = await db.create_service_rule(title=title, content=content)
await message.answer(
f"✅ Страница правил создана!\n\n"
f"📋 Заголовок: {title}\n"
f"📄 Порядок: {rule.page_order}\n"
f"📊 Статус: {'🟢 Активна' if rule.is_active else '🔴 Отключена'}\n\n"
f"Пользователи смогут увидеть эту страницу в меню 'Правила сервиса'.",
reply_markup=admin_menu_keyboard(user.language)
)
log_user_action(user.telegram_id, "service_rule_created", f"Title: {title}")
except Exception as e:
logger.error(f"Error creating service rule: {e}")
await message.answer(
"❌ Ошибка создания страницы правил",
reply_markup=admin_menu_keyboard(user.language)
)
await state.clear()
@admin_router.callback_query(F.data.startswith("admin_rule_view_"))
async def admin_rule_view_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
if not await check_admin_access(callback, user):
return
try:
rule_id = int(callback.data.split("_")[-1])
rule = await db.get_service_rule_by_id(rule_id)
if not rule:
await callback.answer("❌ Правило не найдено")
return
safe_title = rule.title.replace('*', '\\*').replace('_', '\\_').replace('[', '\\[').replace(']', '\\]').replace('`', '\\`')
text = f"📜 **{safe_title}**\n\n"
text += f"📄 Порядок: {rule.page_order}\n"
text += f"📊 Статус: {'🟢 Активна' if rule.is_active else '🔴 Отключена'}\n"
created_date = rule.created_at.strftime('%d.%m.%Y %H:%M') if rule.created_at else 'N/A'
updated_date = rule.updated_at.strftime('%d.%m.%Y %H:%M') if rule.updated_at else 'N/A'
text += f"📅 Создано: {created_date}\n"
text += f"📝 Изменено: {updated_date}\n\n"
content_preview = rule.content[:200]
safe_preview = (content_preview
.replace('*', '')
.replace('_', '')
.replace('[', '')
.replace(']', '')
.replace('`', '')
.replace('#', ''))
if len(rule.content) > 200:
safe_preview += "..."
text += f"**Превью содержимого:**\n{safe_preview}"
try:
await callback.message.edit_text(
text,
reply_markup=admin_rule_edit_keyboard(rule_id, user.language),
parse_mode='Markdown'
)
except Exception as markdown_error:
logger.warning(f"Markdown parsing failed, sending without formatting: {markdown_error}")
simple_text = f"📜 {rule.title}\n\n"
simple_text += f"📄 Порядок: {rule.page_order}\n"
simple_text += f"📊 Статус: {'🟢 Активна' if rule.is_active else '🔴 Отключена'}\n"
simple_text += f"📅 Создано: {created_date}\n"
simple_text += f"📝 Изменено: {updated_date}\n\n"
simple_text += f"Превью содержимого:\n{safe_preview}"
await callback.message.edit_text(
simple_text,
reply_markup=admin_rule_edit_keyboard(rule_id, user.language)
)
except Exception as e:
logger.error(f"Error viewing service rule: {e}")
await callback.answer("❌ Ошибка загрузки правила")
@admin_router.callback_query(F.data.startswith("admin_rule_edit_title_"))
async def admin_rule_edit_title_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
if not await check_admin_access(callback, user):
return
rule_id = int(callback.data.split("_")[-1])
await state.update_data(edit_rule_id=rule_id)
await callback.message.edit_text(
"✏️ Редактирование заголовка\n\n"
"Введите новый заголовок страницы (3-200 символов):",
reply_markup=cancel_keyboard(user.language)
)
await state.set_state(BotStates.waiting_rule_edit_title)
@admin_router.message(StateFilter(BotStates.waiting_rule_edit_title))
async def handle_rule_edit_title(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
new_title = message.text.strip()
if len(new_title) < 3 or len(new_title) > 200:
await message.answer("❌ Заголовок должен быть от 3 до 200 символов")
return
try:
data = await state.get_data()
rule_id = data.get('edit_rule_id')
rule = await db.get_service_rule_by_id(rule_id)
if not rule:
await message.answer("❌ Правило не найдено")
await state.clear()
return
old_title = rule.title
rule.title = new_title
success = await db.update_service_rule(rule)
if success:
await message.answer(
f"✅ Заголовок обновлен!\n\n"
f"Было: {old_title}\n"
f"Стало: {new_title}",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📜 К правилу", callback_data=f"admin_rule_view_{rule_id}")],
[InlineKeyboardButton(text="📋 К списку", callback_data="admin_rules_list")]
])
)
log_user_action(user.telegram_id, "service_rule_title_edited",
f"ID: {rule_id}, New: {new_title}")
else:
await message.answer("❌ Ошибка обновления заголовка")
except Exception as e:
logger.error(f"Error updating rule title: {e}")
await message.answer("❌ Ошибка обновления")
await state.clear()
@admin_router.callback_query(F.data.startswith("admin_rule_edit_content_"))
async def admin_rule_edit_content_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
if not await check_admin_access(callback, user):
return
rule_id = int(callback.data.split("_")[-1])
await state.update_data(edit_rule_id=rule_id)
await callback.message.edit_text(
"📝 Редактирование содержимого\n\n"
"Введите новое содержимое страницы правил:\n\n"
"💡 Поддерживается Markdown форматирование\n"
"Максимальная длина: 3500 символов",
reply_markup=cancel_keyboard(user.language)
)
await state.set_state(BotStates.waiting_rule_edit_content)
@admin_router.message(StateFilter(BotStates.waiting_rule_edit_content))
async def handle_rule_edit_content(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
new_content = message.text.strip()
if len(new_content) < 10:
await message.answer("❌ Содержимое должно быть не менее 10 символов")
return
if len(new_content) > 3500:
await message.answer("❌ Содержимое слишком длинное. Максимум 3500 символов.")
return
try:
data = await state.get_data()
rule_id = data.get('edit_rule_id')
rule = await db.get_service_rule_by_id(rule_id)
if not rule:
await message.answer("❌ Правило не найдено")
await state.clear()
return
rule.content = new_content
success = await db.update_service_rule(rule)
if success:
await message.answer(
f"✅ Содержимое обновлено!\n\n"
f"📜 Правило: {rule.title}\n"
f"📝 Новый размер: {len(new_content)} символов",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📜 К правилу", callback_data=f"admin_rule_view_{rule_id}")],
[InlineKeyboardButton(text="📋 К списку", callback_data="admin_rules_list")]
])
)
log_user_action(user.telegram_id, "service_rule_content_edited",
f"ID: {rule_id}, Length: {len(new_content)}")
else:
await message.answer("❌ Ошибка обновления содержимого")
except Exception as e:
logger.error(f"Error updating rule content: {e}")
await message.answer("❌ Ошибка обновления")
await state.clear()
@admin_router.callback_query(F.data.startswith("admin_rule_edit_order_"))
async def admin_rule_edit_order_callback(callback: CallbackQuery, user: User, state: FSMContext, **kwargs):
if not await check_admin_access(callback, user):
return
rule_id = int(callback.data.split("_")[-1])
await state.update_data(edit_rule_id=rule_id)
await callback.message.edit_text(
"🔄 Изменение порядка страницы\n\n"
"Введите новый номер позиции страницы (число от 1 до 100):\n\n"
"💡 Страницы с меньшим номером показываются раньше",
reply_markup=cancel_keyboard(user.language)
)
await state.set_state(BotStates.waiting_rule_edit_order)
@admin_router.message(StateFilter(BotStates.waiting_rule_edit_order))
async def handle_rule_edit_order(message: Message, state: FSMContext, user: User, db: Database, **kwargs):
try:
new_order = int(message.text.strip())
if new_order < 1 or new_order > 100:
await message.answer("❌ Порядок должен быть от 1 до 100")
return
data = await state.get_data()
rule_id = data.get('edit_rule_id')
rule = await db.get_service_rule_by_id(rule_id)
if not rule:
await message.answer("❌ Правило не найдено")
await state.clear()
return
old_order = rule.page_order
rule.page_order = new_order
success = await db.update_service_rule(rule)
if success:
await message.answer(
f"✅ Порядок страницы изменен!\n\n"
f"📜 Правило: {rule.title}\n"
f"📄 Было: {old_order}\n"
f"📄 Стало: {new_order}",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📜 К правилу", callback_data=f"admin_rule_view_{rule_id}")],
[InlineKeyboardButton(text="📋 К списку", callback_data="admin_rules_list")]
])
)
log_user_action(user.telegram_id, "service_rule_order_changed",
f"ID: {rule_id}, Order: {old_order}->{new_order}")
else:
await message.answer("❌ Ошибка изменения порядка")
except ValueError:
await message.answer("❌ Введите корректное число")
except Exception as e:
logger.error(f"Error updating rule order: {e}")
await message.answer("❌ Ошибка обновления")
await state.clear()
@admin_router.callback_query(F.data.startswith("admin_rule_toggle_"))
async def admin_rule_toggle_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
if not await check_admin_access(callback, user):
return
try:
rule_id = int(callback.data.split("_")[-1])
rule = await db.get_service_rule_by_id(rule_id)
if not rule:
await callback.answer("❌ Правило не найдено")
return
rule.is_active = not rule.is_active
success = await db.update_service_rule(rule)
if success:
status_text = "активирована" if rule.is_active else "отключена"
await callback.answer(f"✅ Страница '{rule.title}' {status_text}")
await admin_rule_view_callback(callback, user, db, **kwargs)
log_user_action(user.telegram_id, "service_rule_toggled",
f"ID: {rule_id}, Active: {rule.is_active}")
else:
await callback.answer("❌ Ошибка обновления статуса")
except Exception as e:
logger.error(f"Error toggling service rule: {e}")
await callback.answer("❌ Ошибка изменения статуса")
@admin_router.callback_query(F.data.startswith("admin_rule_delete_"))
async def admin_rule_delete_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
if not await check_admin_access(callback, user):
return
try:
rule_id = int(callback.data.split("_")[-1])
rule = await db.get_service_rule_by_id(rule_id)
if not rule:
await callback.answer("❌ Правило не найдено")
return
await callback.message.edit_text(
f"⚠️ Удаление страницы правил\n\n"
f"📜 Заголовок: **{rule.title}**\n"
f"📄 Порядок: {rule.page_order}\n\n"
f"❗️ Это действие нельзя отменить!\n"
f"Пользователи больше не увидят эту страницу.",
reply_markup=admin_rule_delete_confirm_keyboard(rule_id, user.language),
parse_mode='Markdown'
)
except Exception as e:
logger.error(f"Error showing delete confirmation: {e}")
await callback.answer("❌ Ошибка")
@admin_router.callback_query(F.data.startswith("admin_rule_confirm_delete_"))
async def admin_rule_confirm_delete_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
"""Окончательное удаление страницы правил"""
if not await check_admin_access(callback, user):
return
try:
rule_id = int(callback.data.split("_")[-1])
rule = await db.get_service_rule_by_id(rule_id)
if not rule:
await callback.answer("❌ Правило не найдено")
return
rule_title = rule.title
success = await db.delete_service_rule(rule_id)
if success:
await callback.message.edit_text(
f"✅ Страница правил удалена\n\n"
f"📜 Была удалена: {rule_title}",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📋 К списку правил", callback_data="admin_rules_list")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
)
log_user_action(user.telegram_id, "service_rule_deleted", f"Title: {rule_title}")
else:
await callback.answer("❌ Ошибка удаления")
except Exception as e:
logger.error(f"Error deleting service rule: {e}")
await callback.answer("❌ Ошибка удаления")
+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 "$@"
+45 -46
View File
@@ -1,53 +1,41 @@
import os
from dataclasses import dataclass
from typing import List
from typing import List, Dict
@dataclass
class Config:
# Bot configuration
BOT_TOKEN: str
BOT_USERNAME: str
# Database
DATABASE_URL: str
# RemnaWave API
REMNAWAVE_URL: str
REMNAWAVE_TOKEN: str
SUBSCRIPTION_BASE_URL: str
# Admin configuration
ADMIN_IDS: List[int]
SUPPORT_USERNAME: str
DEFAULT_LANGUAGE: str
# Trial subscription
TRIAL_ENABLED: bool
TRIAL_DURATION_DAYS: int
TRIAL_TRAFFIC_GB: int
TRIAL_SQUAD_UUID: str
# Referral system
REFERRAL_FIRST_REWARD: float
REFERRAL_REFERRED_BONUS: float
REFERRAL_THRESHOLD: float
REFERRAL_PERCENTAGE: float
# Конфигурация мониторинга подписок
MONITOR_ENABLED: bool = True
MONITOR_CHECK_INTERVAL: int = 3600 # 1 час
MONITOR_DAILY_CHECK_HOUR: int = 10 # 10:00 утра
MONITOR_WARNING_DAYS: int = 3 # предупреждать за 3 дня
# Новые настройки для удаления истекших подписок
DELETE_EXPIRED_TRIAL_DAYS: int = 1 # Удалять триальные через 1 день после истечения
DELETE_EXPIRED_REGULAR_DAYS: int = 7 # Удалять обычные через 7 дней после истечения
AUTO_DELETE_ENABLED: bool = False # Автоматическое удаление при ежедневной проверке
MONITOR_CHECK_INTERVAL: int = 3600
MONITOR_DAILY_CHECK_HOUR: int = 10
MONITOR_WARNING_DAYS: int = 3
DELETE_EXPIRED_TRIAL_DAYS: int = 1
DELETE_EXPIRED_REGULAR_DAYS: int = 7
AUTO_DELETE_ENABLED: bool = False
LUCKY_GAME_ENABLED: bool = True
LUCKY_GAME_REWARD: float = 50.0 # Размер награды за выигрыш
LUCKY_GAME_NUMBERS: int = 30 # Общее количество чисел (1-30)
LUCKY_GAME_WINNING_COUNT: int = 3 # Количество выигрышных чисел
LUCKY_GAME_REWARD: float = 50.0
LUCKY_GAME_NUMBERS: int = 30
LUCKY_GAME_WINNING_COUNT: int = 3
STARS_ENABLED: bool = True
STARS_RATES: Dict[int, float] = None
def load_config() -> Config:
@@ -75,51 +63,61 @@ def load_config() -> Config:
except (ValueError, TypeError):
return default
def parse_stars_rates() -> Dict[int, float]:
default_rates = {
100: 150.0,
150: 220.0,
250: 400.0,
350: 500.0,
500: 800.0,
750: 1150.0,
1000: 1500.0
}
custom_rates = {}
for stars in [100, 150, 250, 350, 500, 750, 1000]:
env_key = f"STARS_{stars}_RATE"
rate = get_float(env_key, 0.0)
if rate > 0:
custom_rates[stars] = rate
if custom_rates:
return custom_rates
return default_rates
return Config(
# Bot configuration
BOT_TOKEN=os.getenv('BOT_TOKEN', ''),
BOT_USERNAME=os.getenv('BOT_USERNAME', ''),
# Database
DATABASE_URL=os.getenv('DATABASE_URL', 'sqlite+aiosqlite:///bot.db'),
# RemnaWave API
REMNAWAVE_URL=os.getenv('REMNAWAVE_URL', ''),
REMNAWAVE_TOKEN=os.getenv('REMNAWAVE_TOKEN', ''),
SUBSCRIPTION_BASE_URL=os.getenv('SUBSCRIPTION_BASE_URL', ''),
# Admin configuration
ADMIN_IDS=parse_admin_ids(os.getenv('ADMIN_IDS', '')),
SUPPORT_USERNAME=os.getenv('SUPPORT_USERNAME', 'support'),
DEFAULT_LANGUAGE=os.getenv('DEFAULT_LANGUAGE', 'ru'),
# Trial subscription
TRIAL_ENABLED=get_bool('TRIAL_ENABLED', False),
TRIAL_DURATION_DAYS=get_int('TRIAL_DURATION_DAYS', 3),
TRIAL_TRAFFIC_GB=get_int('TRIAL_TRAFFIC_GB', 2),
TRIAL_SQUAD_UUID=os.getenv('TRIAL_SQUAD_UUID', ''),
# Referral system
REFERRAL_FIRST_REWARD=get_float('REFERRAL_FIRST_REWARD', 150.0),
REFERRAL_REFERRED_BONUS=get_float('REFERRAL_REFERRED_BONUS', 150.0),
REFERRAL_THRESHOLD=get_float('REFERRAL_THRESHOLD', 300.0),
REFERRAL_PERCENTAGE=get_float('REFERRAL_PERCENTAGE', 0.25),
MONITOR_ENABLED=get_bool('MONITOR_ENABLED', True),
MONITOR_CHECK_INTERVAL=get_int('MONITOR_CHECK_INTERVAL', 3600), # 1 час по умолчанию
MONITOR_DAILY_CHECK_HOUR=get_int('MONITOR_DAILY_CHECK_HOUR', 10), # 10 утра
MONITOR_WARNING_DAYS=get_int('MONITOR_WARNING_DAYS', 3), # за 3 дня
# Новые настройки для удаления истекших подписок
MONITOR_CHECK_INTERVAL=get_int('MONITOR_CHECK_INTERVAL', 3600),
MONITOR_DAILY_CHECK_HOUR=get_int('MONITOR_DAILY_CHECK_HOUR', 10),
MONITOR_WARNING_DAYS=get_int('MONITOR_WARNING_DAYS', 3),
DELETE_EXPIRED_TRIAL_DAYS=get_int('DELETE_EXPIRED_TRIAL_DAYS', 1),
DELETE_EXPIRED_REGULAR_DAYS=get_int('DELETE_EXPIRED_REGULAR_DAYS', 7),
AUTO_DELETE_ENABLED=get_bool('AUTO_DELETE_ENABLED', False),
# Игра удачи
LUCKY_GAME_ENABLED=get_bool('LUCKY_GAME_ENABLED', True),
LUCKY_GAME_REWARD=get_float('LUCKY_GAME_REWARD', 50.0),
LUCKY_GAME_NUMBERS=get_int('LUCKY_GAME_NUMBERS', 30),
LUCKY_GAME_WINNING_COUNT=get_int('LUCKY_GAME_WINNING_COUNT', 3)
LUCKY_GAME_WINNING_COUNT=get_int('LUCKY_GAME_WINNING_COUNT', 3),
STARS_ENABLED=get_bool('STARS_ENABLED', True),
STARS_RATES=parse_stars_rates()
)
def debug_environment():
@@ -129,7 +127,8 @@ def debug_environment():
'REFERRAL_FIRST_REWARD', 'REFERRAL_THRESHOLD',
'MONITOR_ENABLED', 'MONITOR_CHECK_INTERVAL',
'DELETE_EXPIRED_TRIAL_DAYS', 'DELETE_EXPIRED_REGULAR_DAYS', 'AUTO_DELETE_ENABLED',
'LUCKY_GAME_ENABLED', 'LUCKY_GAME_REWARD'
'LUCKY_GAME_ENABLED', 'LUCKY_GAME_REWARD',
'STARS_ENABLED', 'STARS_100_RATE', 'STARS_500_RATE', 'STARS_1000_RATE'
]
print("📋 Environment variables:")
+389 -8
View File
@@ -1,6 +1,6 @@
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import BigInteger, String, Float, DateTime, Boolean, Text, Integer, text, select, func, and_
from sqlalchemy import BigInteger, String, Float, DateTime, Boolean, Text, Integer, text, select, func, and_, Column
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
import logging
@@ -56,7 +56,7 @@ class Subscription(Base):
description: Mapped[Optional[str]] = mapped_column(Text)
price: Mapped[float] = mapped_column(Float)
duration_days: Mapped[int] = mapped_column(Integer)
traffic_limit_gb: Mapped[int] = mapped_column(Integer, default=0) # 0 = unlimited
traffic_limit_gb: Mapped[int] = mapped_column(Integer, default=0)
squad_uuid: Mapped[str] = mapped_column(String(255))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
@@ -119,6 +119,29 @@ class LuckyGame(Base):
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')
telegram_payment_charge_id: Mapped[Optional[str]] = mapped_column(String(255))
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime)
class ServiceRule(Base):
__tablename__ = 'service_rules'
id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
content = Column(Text, nullable=False)
page_order = Column(Integer, nullable=False, default=1)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
class Database:
def __init__(self, database_url: str):
self.engine = create_async_engine(
@@ -140,6 +163,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:
@@ -599,10 +623,8 @@ class Database:
async def update_user_subscription(self, user_subscription: UserSubscription) -> bool:
async with self.session_factory() as session:
try:
# Устанавливаем время обновления
user_subscription.updated_at = datetime.utcnow()
# Обновляем подписку
await session.merge(user_subscription)
await session.commit()
return True
@@ -1203,7 +1225,7 @@ class Database:
update(Promocode)
.where(
and_(
~Promocode.code.startswith('REF'), # Исключаем реферальные
~Promocode.code.startswith('REF'),
Promocode.is_active == True
)
)
@@ -1356,18 +1378,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 +1454,364 @@ 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
async def create_service_rule(self, title: str, content: str, page_order: int = None) -> ServiceRule:
async with self.session_factory() as session:
if page_order is None:
result = await session.execute(
select(func.max(ServiceRule.page_order)).where(ServiceRule.is_active == True)
)
max_order = result.scalar() or 0
page_order = max_order + 1
rule = ServiceRule(
title=title,
content=content,
page_order=page_order
)
session.add(rule)
await session.commit()
await session.refresh(rule)
return rule
async def get_all_service_rules(self, active_only: bool = True) -> List[ServiceRule]:
async with self.session_factory() as session:
query = select(ServiceRule).order_by(ServiceRule.page_order)
if active_only:
query = query.where(ServiceRule.is_active == True)
result = await session.execute(query)
return result.scalars().all()
async def get_service_rule_by_id(self, rule_id: int) -> Optional[ServiceRule]:
async with self.session_factory() as session:
result = await session.execute(
select(ServiceRule).where(ServiceRule.id == rule_id)
)
return result.scalar_one_or_none()
async def update_service_rule(self, rule: ServiceRule) -> bool:
try:
async with self.session_factory() as session:
await session.merge(rule)
await session.commit()
return True
except Exception as e:
logger.error(f"Error updating service rule: {e}")
return False
async def delete_service_rule(self, rule_id: int) -> bool:
try:
async with self.session_factory() as session:
result = await session.execute(
select(ServiceRule).where(ServiceRule.id == rule_id)
)
rule = result.scalar_one_or_none()
if rule:
await session.delete(rule)
await session.commit()
return True
return False
except Exception as e:
logger.error(f"Error deleting service rule: {e}")
return False
async def reorder_service_rules(self, rule_orders: List[tuple]) -> bool:
try:
async with self.session_factory() as session:
for rule_id, new_order in rule_orders:
await session.execute(
update(ServiceRule).where(ServiceRule.id == rule_id).values(page_order=new_order)
)
await session.commit()
return True
except Exception as e:
logger.error(f"Error reordering service rules: {e}")
return False
async def migrate_service_rules_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 service_rules (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
page_order INTEGER NOT NULL DEFAULT 1,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""))
await conn.execute(text("""
CREATE INDEX IF NOT EXISTS idx_service_rules_order ON service_rules(page_order)
"""))
await conn.execute(text("""
CREATE INDEX IF NOT EXISTS idx_service_rules_active ON service_rules(is_active)
"""))
await conn.execute(text("""
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$ language 'plpgsql'
"""))
await conn.execute(text("""
DROP TRIGGER IF EXISTS update_service_rules_updated_at ON service_rules
"""))
await conn.execute(text("""
CREATE TRIGGER update_service_rules_updated_at
BEFORE UPDATE ON service_rules
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column()
"""))
check_result = await conn.execute(text("SELECT COUNT(*) FROM service_rules"))
count = check_result.scalar()
if count == 0:
await conn.execute(text("""
INSERT INTO service_rules (title, content, page_order) VALUES
('Общие положения',
'**1. Общие положения**
Настоящие Правила определяют условия использования VPN-сервиса.
**1.1** Используя наш сервис, вы соглашаетесь с данными правилами.
**1.2** Мы оставляем за собой право изменять правила в любое время.
**1.3** Продолжение использования сервиса после изменений означает ваше согласие с новыми условиями.',
1),
('Права и обязанности',
'**2. Права и обязанности пользователей**
**2.1 Права пользователя:**
• Использовать VPN-сервис в соответствии с тарифным планом
• Получать техническую поддержку
• Защиту персональных данных
**2.2 Обязанности пользователя:**
• Не использовать сервис для незаконной деятельности
• Не передавать данные доступа третьим лицам
• Своевременно оплачивать услуги
**2.3 Запрещается:**
• Попытки взлома или нарушения работы сервиса
• Спам и рассылка нежелательных сообщений
• Нарушение авторских прав',
2),
('Оплата и возврат средств',
'**3. Условия оплаты и возврата**
**3.1 Оплата:**
• Все платежи производятся в российских рублях
• Доступны различные способы оплаты
• Средства зачисляются автоматически или в течение 24 часов
**3.2 Возврат средств:**
• Возврат возможен в течение 7 дней с момента оплаты
• При технических проблемах возврат производится полностью
• Обращайтесь в поддержку для возврата
**3.3 Скидки и промокоды:**
• Действуют ограничения по времени и количеству использований
• Нельзя комбинировать несколько скидок',
3)
"""))
logger.info("Inserted default service rules")
else:
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS service_rules (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
page_order INTEGER NOT NULL DEFAULT 1,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_service_rules_order (page_order),
INDEX idx_service_rules_active (is_active)
)
"""))
logger.info("Successfully created service_rules table")
except Exception as e:
logger.error(f"Error creating service_rules table: {e}")
pass
+315 -38
View File
@@ -8,12 +8,13 @@ import logging
import secrets
from typing import Optional, Dict, Any
from database import Database, User, ReferralProgram
from database import Database, User, ReferralProgram, ServiceRule
from remnawave_api import RemnaWaveAPI
from keyboards import *
from translations import t
from utils import *
from config import *
from stars_handlers import stars_router
import base64
import json
from referral_utils import (
@@ -59,6 +60,12 @@ class BotStates(StatesGroup):
admin_debug_user_structure = State()
admin_rename_plans_confirm = State()
waiting_number_choice = State()
waiting_rule_title = State()
waiting_rule_content = State()
waiting_rule_order = State()
waiting_rule_edit_title = State()
waiting_rule_edit_content = State()
waiting_rule_edit_order = State()
router = Router()
@@ -91,10 +98,9 @@ async def start_command(message: Message, state: FSMContext, db: Database, **kwa
success = await create_referral_from_start_param(user.telegram_id, start_param, db, bot)
if success:
import os
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
referred_bonus = float(os.getenv('REFERRAL_REFERRED_BONUS', '150.0'))
threshold = config.REFERRAL_THRESHOLD if config else 300.0
referred_bonus = config.REFERRAL_REFERRED_BONUS if config else 150.0
await message.answer(
"🎁 Добро пожаловать!\n\n"
f"Вы перешли по реферальной ссылке! После пополнения баланса на {threshold:.0f}"
@@ -415,13 +421,24 @@ async def balance_callback(callback: CallbackQuery, **kwargs):
@router.callback_query(F.data == "topup_balance")
async def topup_balance_callback(callback: CallbackQuery, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
stars_enabled = config and config.STARS_ENABLED and config.STARS_RATES
text = t('topup_balance', user.language)
if stars_enabled:
text += "\n\n⭐ **Новинка!** Теперь можно пополнять баланс через Telegram Stars!"
text += "\n💎 Быстро, безопасно, без комиссий!"
await callback.message.edit_text(
t('topup_balance', user.language),
reply_markup=topup_keyboard(user.language)
text,
reply_markup=topup_keyboard(user.language),
parse_mode='Markdown'
)
@router.callback_query(F.data == "topup_card")
@@ -522,19 +539,49 @@ async def payment_history_callback(callback: CallbackQuery, db: Database, **kwar
try:
payments = await db.get_user_payments(user.telegram_id)
if not payments:
star_payments = await db.get_user_star_payments(user.telegram_id, 5)
if not payments and not star_payments:
text = t('no_payments', user.language)
else:
text = "📊 " + t('payment_history', user.language) + ":\n\n"
for payment in payments[:10]:
date_str = format_datetime(payment.created_at, user.language)
status = format_payment_status(payment.status, user.language)
text += t('payment_item', user.language,
date=date_str,
amount=payment.amount,
description=payment.description,
status=status
) + "\n"
all_payments = []
for payment in payments[:10]:
all_payments.append({
'type': 'regular',
'date': payment.created_at,
'amount': payment.amount,
'description': payment.description,
'status': payment.status
})
for star_payment in star_payments:
if star_payment.status == 'completed':
all_payments.append({
'type': 'stars',
'date': star_payment.completed_at or star_payment.created_at,
'amount': star_payment.rub_amount,
'description': f'Пополнение через Stars ({star_payment.stars_amount} ⭐)',
'status': star_payment.status,
'stars': star_payment.stars_amount
})
all_payments.sort(key=lambda x: x['date'], reverse=True)
for payment in all_payments[:10]:
date_str = format_datetime(payment['date'], user.language)
status = format_payment_status(payment['status'], user.language)
if payment['type'] == 'stars':
text += f"⭐ +{payment['amount']:.0f}₽ ({payment['stars']} ⭐)\n"
else:
amount_str = f"+{payment['amount']}" if payment['amount'] > 0 else str(payment['amount'])
text += f"💳 {amount_str}\n"
text += f" 📅 {date_str} | {status}\n"
text += f" 📝 {payment['description']}\n\n"
await callback.message.edit_text(
text,
@@ -1206,23 +1253,48 @@ async def promocode_callback(callback: CallbackQuery, state: FSMContext, **kwarg
await callback.answer("❌ Ошибка пользователя")
return
await callback.message.edit_text(
t('enter_promocode', user.language),
await state.clear()
edited_message = await callback.message.edit_text(
"🎁 Введите промокод:\n\n"
"• Обычные промокоды (скидки)\n"
"• Реферальные коды (REF...)\n\n"
"ℹ️ После ввода вы вернетесь в главное меню",
reply_markup=cancel_keyboard(user.language)
)
await state.update_data(promocode_message_id=edited_message.message_id)
await state.set_state(BotStates.waiting_promocode)
@router.message(StateFilter(BotStates.waiting_promocode))
async def handle_promocode(message: Message, state: FSMContext, db: Database, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
bot = kwargs.get('bot')
if not user:
await message.answer("❌ Ошибка пользователя")
await state.clear()
return
code = message.text.strip().upper()
state_data = await state.get_data()
promocode_message_id = state_data.get('promocode_message_id')
if not validate_promocode_format(code):
await message.answer(t('invalid_input', user.language))
response_msg = await message.answer(
"❌ Неверный формат промокода",
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
except Exception as e:
logger.warning(f"Could not delete promocode request message: {e}")
await state.clear()
return
try:
@@ -1230,17 +1302,50 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
if promocode and promocode.is_active:
if promocode.expires_at and promocode.expires_at < datetime.utcnow():
await message.answer(t('promocode_expired', user.language))
response_msg = await message.answer(
"❌ Промокод истек",
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
except:
pass
await state.clear()
return
if promocode.used_count >= promocode.usage_limit:
await message.answer(t('promocode_limit', user.language))
response_msg = await message.answer(
"❌ Лимит использования промокода исчерпан",
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
except:
pass
await state.clear()
return
success = await db.use_promocode(user.telegram_id, promocode)
if not success:
await message.answer(t('promocode_used', user.language))
response_msg = await message.answer(
"❌ Вы уже использовали этот промокод",
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
except:
pass
await state.clear()
return
await db.add_balance(user.telegram_id, promocode.discount_amount)
@@ -1254,18 +1359,23 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
)
discount_text = f"{promocode.discount_amount} руб."
await message.answer(
success_msg = await message.answer(
t('promocode_success', user.language, discount=discount_text),
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
logger.info(f"Deleted promocode request message {promocode_message_id}")
except Exception as e:
logger.warning(f"Could not delete promocode request message: {e}")
await state.clear()
log_user_action(user.telegram_id, "promocode_used", code)
return
if code.startswith("REF"):
bot = kwargs.get('bot')
async with db.session_factory() as session:
from sqlalchemy import select
result = await session.execute(
@@ -1278,42 +1388,93 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
existing_reverse_referral = await db.get_referral_by_referred_id(referrer_id)
if existing_reverse_referral and existing_reverse_referral.referrer_id == user.telegram_id:
await message.answer(
response_msg = await message.answer(
"❌ Нельзя использовать код человека, которого вы пригласили!\n\n"
"Взаимные рефералы не допускаются."
"Взаимные рефералы не допускаются.",
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
except:
pass
await state.clear()
return
success = await create_referral_from_promocode(user.telegram_id, code, db, bot)
if success:
await message.answer(
"🎉 Реферальный код активирован!\n\n"
"После пополнения баланса на 200₽ вы получите бонус 150₽!",
threshold = config.REFERRAL_THRESHOLD if config else 300.0
referred_bonus = config.REFERRAL_REFERRED_BONUS if config else 150.0
success_msg = await message.answer(
f"🎉 Реферальный код активирован!\n\n"
f"После пополнения баланса на {threshold:.0f}₽ вы получите бонус {referred_bonus:.0f}₽!",
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
logger.info(f"Deleted promocode request message {promocode_message_id}")
except Exception as e:
logger.warning(f"Could not delete promocode request message: {e}")
await state.clear()
log_user_action(user.telegram_id, "referral_code_used", code)
return
else:
existing_referral = await db.get_referral_by_referred_id(user.telegram_id)
if existing_referral:
await message.answer("❌ Вы уже использовали реферальный код!")
error_text = "❌ Вы уже использовали реферальный код!"
else:
await message.answer("❌ Неверный реферальный код!")
error_text = "❌ Неверный реферальный код!"
response_msg = await message.answer(
error_text,
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
except:
pass
await state.clear()
return
await message.answer(t('promocode_not_found', user.language))
response_msg = await message.answer(
"❌ Промокод не найден\n\n"
"Проверьте правильность ввода и попробуйте снова через главное меню.",
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
logger.info(f"Deleted promocode request message {promocode_message_id}")
except Exception as e:
logger.warning(f"Could not delete promocode request message: {e}")
await state.clear()
except Exception as e:
logger.error(f"Error handling promocode: {e}")
await message.answer(
t('error_occurred', user.language),
response_msg = await message.answer(
"❌ Произошла ошибка при обработке промокода",
reply_markup=main_menu_keyboard(user.language, user.is_admin)
)
await state.clear()
if bot and promocode_message_id:
try:
await bot.delete_message(chat_id=message.chat.id, message_id=promocode_message_id)
except:
pass
await state.clear()
@router.callback_query(F.data == "referral_program")
async def referral_program_callback(callback: CallbackQuery, db: Database, **kwargs):
@@ -1497,3 +1658,119 @@ async def my_referrals_callback(callback: CallbackQuery, db: Database, **kwargs)
except Exception as e:
logger.error(f"Error showing referrals: {e}")
await callback.answer("❌ Ошибка загрузки")
@router.callback_query(F.data == "service_rules")
async def service_rules_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
rules = await db.get_all_service_rules(active_only=True)
if not rules:
await callback.message.edit_text(
"📜 Правила сервиса пока не добавлены администратором.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
)
return
await show_rules_page(callback, rules, 0, user.language)
except Exception as e:
logger.error(f"Error showing service rules: {e}")
await callback.answer("❌ Ошибка загрузки правил")
async def show_rules_page(callback: CallbackQuery, rules: List[ServiceRule], page_index: int, lang: str = 'ru'):
if page_index < 0 or page_index >= len(rules):
page_index = 0
rule = rules[page_index]
total_pages = len(rules)
safe_title = rule.title.replace('*', '\\*').replace('_', '\\_').replace('[', '\\[').replace(']', '\\]').replace('`', '\\`')
text = f"📜 **{safe_title}**\n\n"
text += rule.content
if total_pages > 1:
text += f"\n\n📄 Страница {page_index + 1} из {total_pages}"
if len(text) > 4000:
text = text[:3950] + "\n\n... (текст обрезан)"
try:
await callback.message.edit_text(
text,
reply_markup=service_rules_keyboard(page_index, total_pages, lang),
parse_mode='Markdown'
)
except Exception as markdown_error:
logger.warning(f"Markdown parsing failed in user rules, retrying without markdown: {markdown_error}")
try:
clean_text = text.replace('**', '').replace('*', '').replace('_', '').replace('`', '')
await callback.message.edit_text(
clean_text,
reply_markup=service_rules_keyboard(page_index, total_pages, lang)
)
except Exception as edit_error:
await callback.message.answer(
clean_text,
reply_markup=service_rules_keyboard(page_index, total_pages, lang)
)
@router.callback_query(F.data.startswith("rules_page_"))
async def rules_page_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
page_index = int(callback.data.split("_")[-1])
rules = await db.get_all_service_rules(active_only=True)
if not rules:
await callback.answer("❌ Правила не найдены")
return
await show_rules_page(callback, rules, page_index, user.language)
except (ValueError, IndexError) as e:
logger.error(f"Error parsing rules page number: {e}")
await callback.answer("❌ Ошибка навигации")
except Exception as e:
logger.error(f"Error switching rules page: {e}")
await callback.answer("❌ Ошибка загрузки страницы")
@router.callback_query(F.data == "cancel", StateFilter(BotStates.waiting_promocode))
async def cancel_promocode_callback(callback: CallbackQuery, state: FSMContext, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
db = kwargs.get('db')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
await state.clear()
show_trial = False
show_lucky_game = True
if config and config.TRIAL_ENABLED and db:
try:
has_used = await db.has_used_trial(user.telegram_id)
show_trial = not has_used
except Exception as e:
logger.error(f"Error checking trial availability: {e}")
if config:
show_lucky_game = getattr(config, 'LUCKY_GAME_ENABLED', True)
await callback.message.edit_text(
t('main_menu', user.language),
reply_markup=main_menu_keyboard(user.language, user.is_admin, show_trial, show_lucky_game)
)
+118 -1
View File
@@ -35,7 +35,8 @@ def main_menu_keyboard(lang: str = 'ru', is_admin: bool = False, show_trial: boo
[
InlineKeyboardButton(text="💬 " + t('support', lang), callback_data="support")
],
[InlineKeyboardButton(text="🌐 " + t('change_language', lang), callback_data="change_language")]
[InlineKeyboardButton(text="🌐 " + t('change_language', lang), callback_data="change_language")],
[InlineKeyboardButton(text="📜 Правила сервиса", callback_data="service_rules")]
])
if is_admin:
@@ -162,6 +163,7 @@ def admin_menu_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
InlineKeyboardButton(text="👥 Рефералы", callback_data="admin_referrals") # НОВАЯ КНОПКА
],
[
InlineKeyboardButton(text="📜 Правила сервиса", callback_data="admin_rules"), # НОВАЯ КНОПКА
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system")
],
[
@@ -192,6 +194,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
@@ -602,3 +605,117 @@ def lucky_game_result_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
[InlineKeyboardButton(text="📈 История игр", callback_data="lucky_game_history")],
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
])
def topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="⭐ Telegram Stars", callback_data="topup_stars")],
[InlineKeyboardButton(text="👨‍💼 " + t('topup_support', lang), callback_data="topup_support")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="balance")]
])
return keyboard
def stars_topup_keyboard(stars_rates: Dict[int, float], lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
sorted_rates = sorted(stars_rates.items())
for i in range(0, len(sorted_rates), 2):
row = []
for j in range(2):
if i + j < len(sorted_rates):
stars, rubles = sorted_rates[i + j]
if stars >= 500:
emoji = "🔥" # Выгодное предложение
elif stars >= 250:
emoji = "💎" # Хорошее предложение
else:
emoji = "" # Базовое предложение
button_text = f"{emoji} {stars} ⭐ → {rubles:.0f}"
row.append(InlineKeyboardButton(
text=button_text,
callback_data=f"buy_stars_{stars}"
))
buttons.append(row)
# Добавляем кнопку назад
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="topup_balance")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def stars_payment_keyboard(stars_amount: int, rub_amount: float, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отмена", callback_data="topup_stars")]
])
return keyboard
def service_rules_keyboard(current_page: int, total_pages: int, lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
if total_pages > 1:
nav_row = []
if current_page > 0:
nav_row.append(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"rules_page_{current_page - 1}"))
nav_row.append(InlineKeyboardButton(text=f"{current_page + 1}/{total_pages}", callback_data="noop"))
if current_page < total_pages - 1:
nav_row.append(InlineKeyboardButton(text="Вперед ➡️", callback_data=f"rules_page_{current_page + 1}"))
buttons.append(nav_row)
buttons.append([InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def admin_rules_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="📋 Список правил", callback_data="admin_rules_list")],
[InlineKeyboardButton(text=" Добавить страницу", callback_data="admin_rules_create")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
])
return keyboard
def admin_rules_list_keyboard(rules, lang: str = 'ru') -> InlineKeyboardMarkup:
buttons = []
for rule in rules:
status_emoji = "🟢" if rule.is_active else "🔴"
buttons.append([
InlineKeyboardButton(
text=f"{status_emoji} {rule.page_order}. {rule.title}",
callback_data=f"admin_rule_view_{rule.id}"
)
])
buttons.extend([
[InlineKeyboardButton(text=" Добавить страницу", callback_data="admin_rules_create")],
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_rules")]
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
def admin_rule_edit_keyboard(rule_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✏️ Редактировать заголовок", callback_data=f"admin_rule_edit_title_{rule_id}"),
InlineKeyboardButton(text="📝 Редактировать содержимое", callback_data=f"admin_rule_edit_content_{rule_id}")
],
[
InlineKeyboardButton(text="🔄 Изменить порядок", callback_data=f"admin_rule_edit_order_{rule_id}"),
InlineKeyboardButton(text="🟢/🔴 Вкл/Выкл", callback_data=f"admin_rule_toggle_{rule_id}")
],
[InlineKeyboardButton(text="🗑 Удалить", callback_data=f"admin_rule_delete_{rule_id}")],
[InlineKeyboardButton(text="🔙 К списку", callback_data="admin_rules_list")]
])
return keyboard
def admin_rule_delete_confirm_keyboard(rule_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да, удалить", callback_data=f"admin_rule_confirm_delete_{rule_id}"),
InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_rule_view_{rule_id}")
]
])
return keyboard
+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:
-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"
+415
View File
@@ -0,0 +1,415 @@
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, LabeledPrice, PreCheckoutQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.exceptions import TelegramBadRequest
from datetime import datetime
import logging
from typing import Dict
from database import Database, User
from keyboards import stars_topup_keyboard, stars_payment_keyboard, balance_keyboard
from translations import t
from utils import log_user_action
from referral_utils import process_referral_rewards
logger = logging.getLogger(__name__)
stars_router = Router()
@stars_router.callback_query(F.data == "topup_stars")
async def topup_stars_callback(callback: CallbackQuery, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
if not config or not config.STARS_ENABLED:
await callback.message.edit_text(
"❌ Пополнение через Telegram Stars временно недоступно",
reply_markup=balance_keyboard(user.language)
)
return
if not config.STARS_RATES:
await callback.message.edit_text(
"❌ Курсы звезд не настроены",
reply_markup=balance_keyboard(user.language)
)
return
text = "⭐ **Пополнение через Telegram Stars**\n\n"
text += "🚀 **Преимущества:**\n"
text += "• Мгновенное зачисление\n"
text += "• Безопасные платежи через Telegram\n"
text += "• Без комиссий и скрытых платежей\n\n"
text += "💎 **Доступные варианты:**\n"
sorted_rates = sorted(config.STARS_RATES.items())
for stars, rubles in sorted_rates:
rate_per_star = rubles / stars
if stars >= 500:
bonus_text = " 🔥 Выгодно!"
elif stars >= 250:
bonus_text = " 💎 Хорошо!"
else:
bonus_text = ""
text += f"{stars} ⭐ → {rubles:.0f}{bonus_text}\n"
text += f"\n💡 Выберите подходящий вариант:"
await callback.message.edit_text(
text,
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
@stars_router.callback_query(F.data.startswith("buy_stars_"))
async def buy_stars_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
bot = kwargs.get('bot')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
if not config or not config.STARS_ENABLED or not config.STARS_RATES:
await callback.answer("❌ Пополнение недоступно")
return
try:
stars_amount = int(callback.data.split("_")[2])
if stars_amount not in config.STARS_RATES:
await callback.answer("❌ Неверное количество звезд")
return
rub_amount = config.STARS_RATES[stars_amount]
star_payment = await db.create_star_payment(
user_id=user.telegram_id,
stars_amount=stars_amount,
rub_amount=rub_amount
)
prices = [LabeledPrice(label=f"Пополнение на {rub_amount:.0f}", amount=stars_amount)]
try:
await callback.answer("💳 Создаю платеж...")
await bot.send_invoice(
chat_id=callback.message.chat.id,
title=f"Пополнение баланса",
description=f"Пополнение баланса на {rub_amount:.0f}₽ за {stars_amount}",
payload=f"star_payment_{star_payment.id}",
currency="XTR",
prices=prices
)
try:
await callback.message.edit_text(
f"💳 **Оплата через Telegram Stars**\n\n"
f"⭐ Количество звезд: {stars_amount}\n"
f"💰 Сумма пополнения: {rub_amount:.0f}\n\n"
f"👆 Нажмите кнопку \"Оплатить\" в инвойсе ниже⤵️\n\n"
f"Если передумали - нажмите кнопку ниже \n\n❌ Отменить платеж",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отменить платеж", callback_data=f"cancel_star_payment_{star_payment.id}")],
[InlineKeyboardButton(text="🔙 Назад к выбору", callback_data="topup_stars")]
]),
parse_mode='Markdown'
)
except TelegramBadRequest:
await callback.message.answer(
f"💳 **Оплата через Telegram Stars**\n\n"
f"⭐ Количество звезд: {stars_amount}\n"
f"💰 Сумма пополнения: {rub_amount:.0f}\n\n"
f"👆 Нажмите кнопку \"Оплатить\" в инвойсе ниже⤵️",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="❌ Отменить платеж", callback_data=f"cancel_star_payment_{star_payment.id}")],
[InlineKeyboardButton(text="🔙 Назад к выбору", callback_data="topup_stars")]
]),
parse_mode='Markdown'
)
except TelegramBadRequest as e:
logger.error(f"Failed to send invoice: {e}")
await callback.answer("❌ Ошибка создания платежа", show_alert=True)
try:
await callback.message.edit_text(
"❌ Ошибка создания платежа. Попробуйте позже.",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language)
)
except TelegramBadRequest:
await callback.message.answer(
"❌ Ошибка создания платежа. Попробуйте позже.",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language)
)
await db.cancel_star_payment(star_payment.id)
except (ValueError, IndexError) as e:
logger.error(f"Error processing stars amount: {e}")
await callback.answer("❌ Ошибка обработки")
except Exception as e:
logger.error(f"Error in buy_stars_callback: {e}")
await callback.answer("❌ Произошла ошибка")
@stars_router.pre_checkout_query()
async def pre_checkout_query_handler(pre_checkout_query: PreCheckoutQuery, db: Database, **kwargs):
try:
if not pre_checkout_query.invoice_payload.startswith("star_payment_"):
await pre_checkout_query.answer(ok=False, error_message="Неверный формат платежа")
return
payment_id = int(pre_checkout_query.invoice_payload.split("_")[2])
star_payment = await db.get_star_payment_by_id(payment_id)
if not star_payment:
await pre_checkout_query.answer(ok=False, error_message="Платеж не найден")
return
if star_payment.status != 'pending':
await pre_checkout_query.answer(ok=False, error_message="Платеж уже обработан")
return
if pre_checkout_query.total_amount != star_payment.stars_amount:
await pre_checkout_query.answer(ok=False, error_message="Неверная сумма платежа")
return
await pre_checkout_query.answer(ok=True)
except Exception as e:
logger.error(f"Error in pre_checkout_query_handler: {e}")
await pre_checkout_query.answer(ok=False, error_message="Внутренняя ошибка")
@stars_router.message(F.successful_payment)
async def successful_payment_handler(message: Message, db: Database, **kwargs):
user = kwargs.get('user')
bot = kwargs.get('bot')
if not user:
logger.error("User not found in successful payment handler")
return
try:
payment_info = message.successful_payment
if not payment_info.invoice_payload.startswith("star_payment_"):
logger.error(f"Invalid payment payload: {payment_info.invoice_payload}")
return
payment_id = int(payment_info.invoice_payload.split("_")[2])
success = await db.complete_star_payment(
payment_id=payment_id,
telegram_payment_charge_id=payment_info.telegram_payment_charge_id
)
if success:
star_payment = await db.get_star_payment_by_id(payment_id)
if star_payment:
updated_user = await db.get_user_by_telegram_id(user.telegram_id)
success_text = "✅ **Платеж успешно обработан!**\n\n"
success_text += f"⭐ Оплачено звезд: {star_payment.stars_amount}\n"
success_text += f"💰 Зачислено: {star_payment.rub_amount:.0f}\n"
success_text += f"💳 Текущий баланс: {updated_user.balance:.0f}\n\n"
success_text += "🎉 Средства уже доступны на вашем балансе!"
await message.answer(
success_text,
parse_mode='Markdown'
)
if bot:
try:
regular_payment = await db.create_payment(
user_id=user.telegram_id,
amount=star_payment.rub_amount,
payment_type='stars',
description=f'Пополнение через Telegram Stars ({star_payment.stars_amount} ⭐)',
status='completed'
)
await process_referral_rewards(
user.telegram_id,
star_payment.rub_amount,
regular_payment.id,
db,
bot,
payment_type='stars'
)
except Exception as ref_error:
logger.error(f"Error processing referral rewards for stars payment: {ref_error}")
log_user_action(user.telegram_id, "stars_payment_completed",
f"Stars: {star_payment.stars_amount}, Amount: {star_payment.rub_amount}")
else:
await message.answer("❌ Ошибка получения данных платежа")
else:
await message.answer("❌ Ошибка обработки платежа. Обратитесь в поддержку.")
logger.error(f"Failed to complete star payment {payment_id}")
except Exception as e:
logger.error(f"Error in successful_payment_handler: {e}")
await message.answer("❌ Произошла ошибка при обработке платежа. Обратитесь в поддержку.")
def get_stars_rate_info(stars_rates: Dict[int, float], lang: str = 'ru') -> str:
if not stars_rates:
return "Курсы не настроены"
text = "⭐ **Курсы Telegram Stars:**\n\n"
sorted_rates = sorted(stars_rates.items())
for stars, rubles in sorted_rates:
rate_per_star = rubles / stars
text += f"{stars} ⭐ = {rubles:.0f}₽ (курс: {rate_per_star:.2f}₽ за ⭐)\n"
return text
@stars_router.callback_query(F.data.startswith("cancel_star_payment_"))
async def cancel_star_payment_callback(callback: CallbackQuery, db: Database, **kwargs):
user = kwargs.get('user')
config = kwargs.get('config')
bot = kwargs.get('bot')
if not user:
await callback.answer("❌ Ошибка пользователя")
return
try:
payment_id = int(callback.data.split("_")[-1])
star_payment = await db.get_star_payment_by_id(payment_id)
if not star_payment:
await callback.answer("❌ Платеж не найден")
return
if star_payment.user_id != user.telegram_id:
await callback.answer("❌ Нет доступа к этому платежу")
return
if star_payment.status != 'pending':
await callback.answer("❌ Платеж уже обработан")
return
success = await db.cancel_star_payment(payment_id)
if success:
await callback.answer("✅ Платеж отменен", show_alert=True)
if bot:
invoice_deleted = False
try:
await bot.delete_message(
chat_id=callback.message.chat.id,
message_id=callback.message.message_id
)
invoice_deleted = True
logger.info(f"Deleted invoice message {callback.message.message_id} for cancelled payment {payment_id}")
except TelegramBadRequest as e:
logger.warning(f"Failed to delete invoice message: {e}")
except Exception as e:
logger.error(f"Unexpected error deleting invoice message: {e}")
if invoice_deleted:
try:
if config and config.STARS_RATES:
text = "❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\n"
text += "🚀 **Преимущества:**\n"
text += "• Мгновенное зачисление\n"
text += "• Безопасные платежи через Telegram\n"
text += "• Без комиссий и скрытых платежей\n\n"
text += "💎 **Доступные варианты:**\n"
sorted_rates = sorted(config.STARS_RATES.items())
for stars, rubles in sorted_rates:
if stars >= 500:
bonus_text = " 🔥 Выгодно!"
elif stars >= 250:
bonus_text = " 💎 Хорошо!"
else:
bonus_text = ""
text += f"{stars} ⭐ → {rubles:.0f}{bonus_text}\n"
text += f"\n💡 Выберите другой вариант:"
await bot.send_message(
chat_id=callback.message.chat.id,
text=text,
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
else:
await bot.send_message(
chat_id=callback.message.chat.id,
text="❌ Платеж отменен\n\n💰 Управление балансом:",
reply_markup=balance_keyboard(user.language)
)
except Exception as e:
logger.error(f"Failed to send menu after invoice deletion: {e}")
else:
try:
if config and config.STARS_RATES:
await callback.message.edit_text(
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
else:
await callback.message.edit_text(
"❌ Платеж отменен",
reply_markup=balance_keyboard(user.language)
)
except TelegramBadRequest:
try:
if config and config.STARS_RATES:
await bot.send_message(
chat_id=callback.message.chat.id,
text="❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
else:
await bot.send_message(
chat_id=callback.message.chat.id,
text="❌ Платеж отменен",
reply_markup=balance_keyboard(user.language)
)
except Exception as e:
logger.error(f"Failed to send fallback message: {e}")
else:
try:
if config and config.STARS_RATES:
await callback.message.edit_text(
"❌ Платеж отменен\n\n⭐ **Пополнение через Telegram Stars**\n\nВыберите другой вариант:",
reply_markup=stars_topup_keyboard(config.STARS_RATES, user.language),
parse_mode='Markdown'
)
else:
await callback.message.edit_text(
"❌ Платеж отменен",
reply_markup=balance_keyboard(user.language)
)
except TelegramBadRequest:
logger.warning("Failed to edit message when bot instance not available")
log_user_action(user.telegram_id, "stars_payment_cancelled", f"Payment ID: {payment_id}")
else:
await callback.answer("❌ Ошибка отмены платежа")
except (ValueError, IndexError) as e:
logger.error(f"Error parsing payment ID from callback: {e}")
await callback.answer("❌ Ошибка обработки")
except Exception as e:
logger.error(f"Error in cancel_star_payment_callback: {e}")
await callback.answer("❌ Произошла ошибка")
+3 -19
View File
@@ -147,12 +147,9 @@ class SubscriptionMonitorService:
break
except Exception as e:
logger.error(f"❌ Error in daily loop: {e}", exc_info=True)
await asyncio.sleep(3600) # Wait 1 hour before retry
await asyncio.sleep(3600)
async def delete_expired_trial_subscriptions(self, force: bool = False) -> Dict[str, Any]:
"""
Удаляет истекшие триальные подписки
"""
try:
logger.info("🗑️ Starting deletion of expired trial subscriptions...")
@@ -179,12 +176,10 @@ class SubscriptionMonitorService:
for user_sub in user_subs:
results['total_checked'] += 1
# Получаем информацию о подписке
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if not subscription or not subscription.is_trial:
continue
# Проверяем, истекла ли подписка достаточно давно
expires_at_utc = user_sub.expires_at
if expires_at_utc.tzinfo is None:
expires_at_utc = expires_at_utc.replace(tzinfo=None)
@@ -192,12 +187,11 @@ class SubscriptionMonitorService:
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
if expires_at_utc > cutoff_date and not force:
continue # Подписка еще не готова к удалению
continue
logger.info(f"🗑️ Deleting expired trial subscription '{subscription.name}' for user {user.telegram_id} "
f"(expired: {expires_at_utc}, cutoff: {cutoff_date})")
# Удаляем из RemnaWave API
api_deleted = False
if self.api and user_sub.short_uuid:
try:
@@ -213,7 +207,6 @@ class SubscriptionMonitorService:
results['errors'].append(f"API error for {user_sub.short_uuid}: {str(api_error)}")
logger.error(f"❌ API error deleting {user_sub.short_uuid}: {api_error}")
# Удаляем из базы данных
db_deleted = await self.db.delete_user_subscription(user_sub.id)
if db_deleted:
results['deleted_from_db'] += 1
@@ -248,9 +241,6 @@ class SubscriptionMonitorService:
}
async def delete_expired_regular_subscriptions(self, force: bool = False) -> Dict[str, Any]:
"""
Удаляет истекшие обычные подписки
"""
try:
logger.info("🗑️ Starting deletion of expired regular subscriptions...")
@@ -277,16 +267,13 @@ class SubscriptionMonitorService:
for user_sub in user_subs:
results['total_checked'] += 1
# Получаем информацию о подписке
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
if not subscription or subscription.is_trial:
continue
# Пропускаем импортированные подписки (они не удаляются)
if getattr(subscription, 'is_imported', False) or subscription.name == "Старая подписка":
continue
# Проверяем, истекла ли подписка достаточно давно
expires_at_utc = user_sub.expires_at
if expires_at_utc.tzinfo is None:
expires_at_utc = expires_at_utc.replace(tzinfo=None)
@@ -294,12 +281,11 @@ class SubscriptionMonitorService:
expires_at_utc = expires_at_utc.astimezone(timezone.utc).replace(tzinfo=None)
if expires_at_utc > cutoff_date and not force:
continue # Подписка еще не готова к удалению
continue
logger.info(f"🗑️ Deleting expired regular subscription '{subscription.name}' for user {user.telegram_id} "
f"(expired: {expires_at_utc}, cutoff: {cutoff_date})")
# Удаляем из RemnaWave API
api_deleted = False
if self.api and user_sub.short_uuid:
try:
@@ -315,7 +301,6 @@ class SubscriptionMonitorService:
results['errors'].append(f"API error for {user_sub.short_uuid}: {str(api_error)}")
logger.error(f"❌ API error deleting {user_sub.short_uuid}: {api_error}")
# Удаляем из базы данных
db_deleted = await self.db.delete_user_subscription(user_sub.id)
if db_deleted:
results['deleted_from_db'] += 1
@@ -536,7 +521,6 @@ 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):
+2 -2
View File
@@ -11,9 +11,9 @@ TRANSLATIONS = {
'admin_panel': 'Админ панель',
'trial_subscription': '🆓 Тестовая подписка',
'trial_not_available': '❌ Тестовая подписка недоступна',
'trial_success': '🎉 Тестовая подписка успешно активирована!\n\nТеперь вы можете найти её в разделе "Мои подписки".',
'trial_success': '🎉 Тестовая подписка успешно активирована!\n\nТеперь вы можете найти её в разделе: \n\n"Мои подписки"',
'trial_error': '❌ Ошибка при создании тестовой подписки',
'trial_info': '🧪 Тестовая подписка выдается на три дня!\n\nНа тарифе действует ограничение в 3 дня\n\nОграничение по трафику - 2гб',
'trial_info': 'Попробуйте наш сервис бесплатно! \n\n🧪 Тестовая подписка выдается на {days} дня!\n\nОграничение по трафику - {traffic} гб',
'subscriptions_list': '📋 Список подписок в продаже:',
'your_balance': '💰 Ваш баланс: {balance:.2f} руб.',
'topup_balance': 'Пополнить баланс',