Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64b5d90d66 | |||
| 5a1fb32e1d | |||
| c4d8108e8c | |||
| 1350d80083 | |||
| e0cc218007 | |||
| bcaf8f2247 | |||
| d1190ed9dc | |||
| 513d6037d4 | |||
| e86012b2ab | |||
| 214e381dc5 | |||
| aa79cb670b | |||
| f0106e15fa | |||
| 1d194c6be2 | |||
| 6917890007 | |||
| 1c69de7ecd | |||
| 62188793d9 | |||
| 585ee2d42b | |||
| 2a58cfd45c | |||
| 93003c5fe0 | |||
| 91b0787d72 | |||
| 315277d4b0 | |||
| 1821c0d89d |
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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 "$@"
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
@@ -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': 'Пополнить баланс',
|
||||
|
||||
Reference in New Issue
Block a user