Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dfedef81e | |||
| 317e39c823 | |||
| 0072a3cb71 | |||
| 8d021c1530 | |||
| 23606dc2d4 | |||
| 0838c4196d | |||
| 65605a0de2 | |||
| a20ac80281 | |||
| 02e83fb0d0 | |||
| eddad0db6e | |||
| ffad0afb61 | |||
| 83eed8e1cd | |||
| 0c7c8abade | |||
| a064890ed8 | |||
| f310f4a35e | |||
| 0268e09882 | |||
| 449814d0df | |||
| a3d3577103 | |||
| a3086fdd2c | |||
| daafc6a41e | |||
| c27ee116f7 | |||
| 5167cde443 | |||
| 34542d3a9c | |||
| e73d1df4cc | |||
| f9a95c3d48 | |||
| 2dd2b5fdbf | |||
| 896f1db9d4 | |||
| a1bd2d775f | |||
| 8cbe23f9ed | |||
| edc891c93d | |||
| 2d706a4be0 | |||
| 32cb81ef34 | |||
| 88aa3c4a3d | |||
| e813d3ccb7 | |||
| a24d7a4e24 | |||
| 39987b9717 | |||
| c77a1ea4f8 |
@@ -1,13 +1,13 @@
|
||||
# 🤖 RemnaWave Telegram Bot
|
||||
# 🤖 Remnawave Telegram Bot
|
||||
|
||||
[](https://hub.docker.com/r/fr1ngg/remnawave-bedolaga-telegram-bot)
|
||||
[](https://python.org)
|
||||
[](https://postgresql.org)
|
||||
[](LICENSE)
|
||||
|
||||

|
||||

|
||||
|
||||
Современный Telegram бот для управления VPN подписками через RemnaWave API с полным функционалом управления пользователями, платежами и администрированием.
|
||||
Современный Telegram бот для управления VPN подписками через Remnawave API с полным функционалом управления пользователями, платежами и администрированием.
|
||||
|
||||
## 🚀 Особенности
|
||||
|
||||
@@ -20,18 +20,21 @@
|
||||
- 🎰 **Игра удачи** - выигрывай бонусы каждые 24 часа
|
||||
- 🆓 **Тестовая подписка** - бесплатная пробная версия с детальной конфигурацией
|
||||
- 🌐 **Мультиязычность** - русский и английский
|
||||
- 📋 **Правила сервиса**
|
||||
- 📋 **Правила сервиса**
|
||||
- ♾️ **Автопродление** - С настройкой вкл/выкл и кол-вом дней до автопродления.
|
||||
|
||||
### ⚙️ Для администраторов
|
||||
- 📊 **Полная статистика** - пользователи, платежи, подписки
|
||||
- 👥 **Управление пользователями** - поиск, редактирование, баланс
|
||||
- 💳 **Управление платежами** - одобрение, отклонение, история
|
||||
- 🎫 **Управление промокодами** - создание, редактирование, статистика
|
||||
- 🖥 **Мониторинг системы** - состояние нод, пользователей RemnaWave
|
||||
- 🔄 **Синхронизация данных** - импорт из RemnaWave
|
||||
- ✍️ **Просмотр подписок юзеров**
|
||||
- 🖥 **Мониторинг системы** - состояние нод, пользователей Remnawave
|
||||
- 🔄 **Синхронизация данных** - импорт из Remnawave
|
||||
- 📨 **Рассылки** - уведомления пользователям
|
||||
- 🔍 **Мониторинг подписок** - автоматические уведомления об истечении и зачистка подписок с истекщим сроком для триал и обычных подписок
|
||||
- 📋 **Правила сервиса** - Настройка страниц из админ панели
|
||||
- ♾️ **Автопродление** - Статус сервиса, статистика автопродления, принудительное продление
|
||||
|
||||
## 📋 Требования
|
||||
|
||||
@@ -42,7 +45,7 @@
|
||||
|
||||
### Внешние сервисы
|
||||
- **Telegram Bot Token** - создайте бота через [@BotFather](https://t.me/BotFather)
|
||||
- **RemnaWave API** - доступ к панели RemnaWave
|
||||
- **Remnawave API** - доступ к панели Remnawave
|
||||
- **PostgreSQL** - автоматически разворачивается через Docker
|
||||
|
||||
## 🛠 Установка и запуск
|
||||
@@ -71,9 +74,9 @@ nano .env
|
||||
|------------|----------|---------|
|
||||
| `BOT_TOKEN` | Токен Telegram бота | `123456:ABC-DEF1234ghIkl-zyx` |
|
||||
| `BOT_USERNAME` | Username бота (без @) | `your_bot_username` |
|
||||
| `REMNAWAVE_URL` | URL панели RemnaWave | `https://panel.example.com` |
|
||||
| `REMNAWAVE_URL` | URL панели Remnawave | `https://panel.example.com` |
|
||||
| `REMNAWAVE_MODE` | Тип подключения | `remote/local` |
|
||||
| `REMNAWAVE_TOKEN` | API токен RemnaWave | `your_api_token` |
|
||||
| `REMNAWAVE_TOKEN` | API токен Remnawave | `your_api_token` |
|
||||
| `ADMIN_IDS` | ID администраторов (через запятую) | `123456789,987654321` |
|
||||
|
||||
#### 🎁 Реферальная программа
|
||||
@@ -103,6 +106,17 @@ nano .env
|
||||
| `TRIAL_TRAFFIC_GB` | Лимит трафика (ГБ) | `2` |
|
||||
| `TRIAL_SQUAD_UUID` | UUID squad для тестовых | `uuid-here` |
|
||||
|
||||
#### 🌟 Оплата за звезды
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|------------|----------|--------------|
|
||||
| `STARS_ENABLED=true` | Включить пополнение баланса за звезды | `true/false` |
|
||||
| `STARS_100_RATE` | Курс за 100 звезд | `100` |
|
||||
| `STARS_150_RATE` | Курс за 150 звезд | `150` |
|
||||
| `STARS_250_RATE` | Курс за 250 звезд | `250` |
|
||||
| `И тд` | Курс за XXX звезд | `XXXX` |
|
||||
|
||||
|
||||
#### 💬 Сервис мониторинга сообщений
|
||||
|
||||
| Переменная | Описание | По умолчанию |
|
||||
@@ -116,31 +130,33 @@ nano .env
|
||||
| `AUTO_DELETE_ENABLED` | Включить автоматическое удаление при ежедневной проверке(true/false) | `true` |
|
||||
|
||||
### Максимально быстрый старт через скрипт управления ботом
|
||||
# 🚀 Быстрая установка RemnaWave Bedolaga Bot
|
||||
#### 🚀 Быстрая установка Remnawave Bedolaga Bot
|
||||
<img width="419" height="348" alt="Снимок экрана 2025-08-12 в 01 24 57" src="https://github.com/user-attachments/assets/9ab876f5-2758-4c52-93dd-6c9c654a07aa" />
|
||||
|
||||
|
||||
Автоматический установщик для Ubuntu сервера с полным управлением через интерактивное меню.
|
||||
|
||||
#### 📋 Требования
|
||||
##### 📋 Требования
|
||||
|
||||
- **Ubuntu 18.04+** (рекомендуется Ubuntu 20.04 или 22.04)
|
||||
- **Root доступ** или sudo права
|
||||
- **Интернет соединение** для загрузки Docker образов
|
||||
|
||||
#### ⚡ Установка одной командой
|
||||
##### ⚡ Установка одной командой
|
||||
|
||||
##### Способ 1: Скачать и запустить
|
||||
###### Способ 1: Скачать и запустить
|
||||
```bash
|
||||
wget https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/raw/main/bedolaga.sh
|
||||
chmod +x bedolaga.sh
|
||||
sudo ./bedolaga.sh
|
||||
```
|
||||
|
||||
##### Способ 2: Прямой запуск
|
||||
###### Способ 2: Прямой запуск
|
||||
```bash
|
||||
curl -sSL https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot/raw/main/bedolaga.sh | sudo bash
|
||||
```
|
||||
|
||||
##### Способ 3: Клонирование репозитория
|
||||
###### Способ 3: Клонирование репозитория
|
||||
```bash
|
||||
git clone https://github.com/Fr1ngg/remnawave-bedolaga-telegram-bot.git
|
||||
cd remnawave-bedolaga-telegram-bot
|
||||
@@ -171,6 +187,7 @@ sudo ./bedolaga.sh
|
||||
- ♻️ Восстановить базу данных
|
||||
- ✏️ Редактировать .env файл
|
||||
- 🩺 Диагностика базы данных
|
||||
- Восстановление бд после обновления 1.3.3 -> 1.3.4
|
||||
- 🗑️ Удалить базу данных
|
||||
- ❌ Полностью удалить бота
|
||||
|
||||
@@ -182,6 +199,7 @@ sudo ./bedolaga.sh
|
||||
- ♻️ Восстановить базу данных
|
||||
- ✏️ Редактировать .env файл
|
||||
- 🩺 Диагностика базы данных
|
||||
- Восстановление бд после обновления 1.3.3 -> 1.3.4
|
||||
- 🗑️ Удалить базы данных
|
||||
- ❌ Полностью удалить бота
|
||||
|
||||
@@ -289,7 +307,7 @@ services:
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
# RemnaWave Bot
|
||||
# Remnawave Bot
|
||||
bot:
|
||||
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
|
||||
container_name: remnawave_bot
|
||||
@@ -351,7 +369,7 @@ services:
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
# RemnaWave Bot
|
||||
# Remnawave Bot
|
||||
bot:
|
||||
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest
|
||||
container_name: remnawave_bot
|
||||
@@ -569,15 +587,14 @@ docker build -t remnawave-bot .
|
||||
- **💰 Управление балансом** - пополнение балансов
|
||||
- **🎁 Управление промокодами** - создание скидок
|
||||
- **📨 Рассылки** - отправка сообщений
|
||||
- **🖥 Система RemnaWave** - мониторинг нод и синхронизация
|
||||
- **🖥 Система Remnawave** - мониторинг нод и синхронизация
|
||||
|
||||
## 🔒 Безопасность
|
||||
|
||||
### Рекомендации
|
||||
|
||||
- ✅ Измените пароли PostgreSQL в `docker-compose.yml`
|
||||
- ✅ Не публикуйте файл `.env` в репозитории
|
||||
- ✅ Используйте HTTPS для RemnaWave API
|
||||
- ✅ Используйте HTTPS для Remnawave API
|
||||
- ✅ Регулярно создавайте backup базы данных
|
||||
- ✅ Ограничьте доступ к серверу через firewall
|
||||
|
||||
@@ -597,7 +614,7 @@ openssl rand -hex 16 # для Redis
|
||||
|----------|---------|
|
||||
| Бот не отвечает | Проверьте `BOT_TOKEN` и интернет |
|
||||
| Ошибка подключения к БД | Проверьте статус PostgreSQL контейнера |
|
||||
| Ошибки RemnaWave API | Проверьте `REMNAWAVE_URL` и `REMNAWAVE_TOKEN` |
|
||||
| Ошибки Remnawave API | Проверьте `REMNAWAVE_URL` и `REMNAWAVE_TOKEN` |
|
||||
| Игра удачи не работает | Проверьте `LUCKY_GAME_ENABLED=true` |
|
||||
|
||||
### Диагностика
|
||||
@@ -681,8 +698,43 @@ docker-compose ps
|
||||
| 10 | Блокировка | 🚧 In Progress | v1.3.0+ | 🟢 Low | Бан/разбан юзеров |
|
||||
| 11 | Удаление истекшик подписок из бота и панели | ✅ Done | v1.3.1 | 🔴 High | Дабы не плодить лишние записи в базе бота и панели ремны, да и в целом это логично)( |
|
||||
| 12 | Правила сервиса | ✅ Done | v1.3.3 | 🟢 Low | Вывод правил сервиса в боте с конфигурацией через админку |
|
||||
| 13 | Автоплатежи | ✅ Done | v1.3.3 | 🟡 Medium | Возможность включать-выключать автоплатежи и настраивать их + админская статистика |
|
||||
| 14 | Все подписки юзеров | ✅ Done | v1.3.4 | 🟡 Medium | Возможность просматривать все подписки купленные юзерами в админ панели |
|
||||
|
||||
|
||||
### 🚨 Возможные проблемы и пути и решения
|
||||
|
||||
#### Переход с версии 1.3.3 на 1.3.4
|
||||
|
||||
Может такое пройзойти, что после обновления образа у вас сломается база с подписками и в консоль полетят sql ошибки, дело в том, что в патче 1.3.4 добавились автоплатежи с доп полями в бд, во время обновления миграция может пройти некорректно(Я еще не до конца разобрался в чем проблема, но имейте в виду).
|
||||
|
||||
Для фикса в репозитории скачайте файлик emergency_fix.py и положите его рядом с докер файлом
|
||||
|
||||
В докер добавьте сервис:
|
||||
|
||||
emergency-fix:
|
||||
image: fr1ngg/remnawave-bedolaga-telegram-bot:latest # Используем тот же образ что и бот
|
||||
volumes:
|
||||
- ./emergency_fix.py:/app/emergency_fix.py
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://remnawave_user:secure_password_123@postgres:5432/remnawave_bot #путь до DATABASE_URL
|
||||
networks:
|
||||
- bot_network
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
profiles:
|
||||
- emergency # Профиль, чтобы не запускался по умолчанию
|
||||
command: python emergency_fix.py
|
||||
|
||||
После выполните:
|
||||
|
||||
docker compose run --rm emergency-fix
|
||||
|
||||
Скрипт добавит необходимые поля и починит вашу базу с подписками. После можете удалить секцию с emergency-fix и скрипт из докер файла.
|
||||
|
||||
**Для тех кто запускает бота через скрипт управления ботом добавлены кнопки для восстановления бд, 10) Экстренное исправление БД (Python) 11) Экстренное исправление БД (SQL)**
|
||||
|
||||
### 🚀 Хотите помочь?
|
||||
|
||||
**Разработчикам:**
|
||||
@@ -714,4 +766,8 @@ docker-compose ps
|
||||
|
||||
---
|
||||
|
||||
*Создано с ❤️ для RemnaWave сообщества*
|
||||
### PS
|
||||
|
||||
Автор не является проф. разработчиком, да и без вайбкодинга не обошлось, но я прикладываю все усилия, чтобы сделать удобного публичного бота для ваших сервисов)(
|
||||
|
||||
*Создано с ❤️ для Remnawave сообщества*
|
||||
|
||||
@@ -7295,7 +7295,6 @@ async def admin_rule_delete_callback(callback: CallbackQuery, user: User, db: Da
|
||||
|
||||
@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
|
||||
|
||||
@@ -7327,3 +7326,980 @@ async def admin_rule_confirm_delete_callback(callback: CallbackQuery, user: User
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting service rule: {e}")
|
||||
await callback.answer("❌ Ошибка удаления")
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_autopay")
|
||||
async def admin_autopay_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_autopay_keyboard(user.language)
|
||||
)
|
||||
|
||||
@admin_router.callback_query(F.data == "autopay_status")
|
||||
async def autopay_status_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
autopay_service = kwargs.get('autopay_service')
|
||||
db = kwargs.get('db')
|
||||
|
||||
if not autopay_service:
|
||||
await callback.message.edit_text(
|
||||
"❌ Сервис автоплатежей недоступен",
|
||||
reply_markup=back_keyboard("admin_autopay", user.language)
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
status = await autopay_service.get_service_status()
|
||||
|
||||
subscriptions_with_autopay = await db.get_subscriptions_for_autopay()
|
||||
|
||||
text = "🔄 **Статус сервиса автоплатежей**\n\n"
|
||||
|
||||
if status['is_running']:
|
||||
text += "✅ **Статус:** Работает\n"
|
||||
else:
|
||||
text += "❌ **Статус:** Остановлен\n"
|
||||
|
||||
text += f"⚙️ **Настройки:**\n"
|
||||
text += f"• Интервал проверки: {status['check_interval']//60} мин\n"
|
||||
text += f"• API подключен: {'✅' if status['has_api'] else '❌'}\n"
|
||||
text += f"• Бот подключен: {'✅' if status['has_bot'] else '❌'}\n\n"
|
||||
|
||||
text += f"📊 **Статистика:**\n"
|
||||
text += f"• Подписок с автоплатежом: {len(subscriptions_with_autopay)}\n"
|
||||
|
||||
days_stats = {}
|
||||
for sub in subscriptions_with_autopay:
|
||||
days = sub.auto_pay_days_before
|
||||
days_stats[days] = days_stats.get(days, 0) + 1
|
||||
|
||||
if days_stats:
|
||||
text += f"• Распределение по дням:\n"
|
||||
for days in sorted(days_stats.keys()):
|
||||
text += f" - За {days} дн.: {days_stats[days]} подписок\n"
|
||||
|
||||
text += f"\n🕐 Обновлено: {format_datetime(datetime.now(), user.language)}"
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_status")],
|
||||
[InlineKeyboardButton(text="🚀 Принудительная проверка", callback_data="autopay_force_check")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=keyboard)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting autopay status: {e}")
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка получения статуса",
|
||||
reply_markup=back_keyboard("admin_autopay", user.language)
|
||||
)
|
||||
|
||||
@admin_router.callback_query(F.data == "autopay_force_check")
|
||||
async def autopay_force_check_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
autopay_service = kwargs.get('autopay_service')
|
||||
|
||||
if not autopay_service:
|
||||
await callback.answer("❌ Сервис автоплатежей недоступен")
|
||||
return
|
||||
|
||||
try:
|
||||
await callback.answer("⏳ Запускаю проверку автоплатежей...")
|
||||
|
||||
stats = await autopay_service.process_autopayments()
|
||||
|
||||
text = "✅ Принудительная проверка автоплатежей завершена!\n\n"
|
||||
text += f"📊 Результаты:\n"
|
||||
text += f"• Обработано: {stats['processed']}\n"
|
||||
text += f"• Успешно: {stats['successful']}\n"
|
||||
text += f"• Недостаточно средств: {stats['insufficient_balance']}\n"
|
||||
text += f"• Ошибки: {stats['failed']}\n"
|
||||
|
||||
if stats['errors']:
|
||||
text += f"\n❌ Детали ошибок:\n"
|
||||
for error in stats['errors'][:5]:
|
||||
text += f"• {error}\n"
|
||||
if len(stats['errors']) > 5:
|
||||
text += f"... и еще {len(stats['errors']) - 5}\n"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=back_keyboard("admin_autopay", user.language)
|
||||
)
|
||||
|
||||
log_user_action(user.telegram_id, "autopay_force_check",
|
||||
f"Processed: {stats['processed']}, Successful: {stats['successful']}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in force autopay check: {e}")
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка при выполнении проверки",
|
||||
reply_markup=back_keyboard("admin_autopay", user.language)
|
||||
)
|
||||
|
||||
def admin_autopay_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📊 Статус сервиса", callback_data="autopay_status")],
|
||||
[InlineKeyboardButton(text="🚀 Принудительная проверка", callback_data="autopay_force_check")],
|
||||
[InlineKeyboardButton(text="📈 Статистика автоплатежей", callback_data="autopay_statistics")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
@admin_router.callback_query(F.data == "autopay_statistics")
|
||||
async def autopay_statistics_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
db = kwargs.get('db')
|
||||
if not db:
|
||||
await callback.answer("❌ База данных недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
await callback.answer("📊 Собираю статистику автоплатежей...")
|
||||
|
||||
stats = await db.get_autopay_statistics()
|
||||
|
||||
insufficient_balance_users = await db.get_users_with_insufficient_autopay_balance()
|
||||
|
||||
autopay_history = await db.get_autopay_history(10)
|
||||
|
||||
text = "📈 **Статистика автоплатежей**\n\n"
|
||||
|
||||
text += "📊 **Общая информация:**\n"
|
||||
text += f"• Всего подписок с автоплатежом: {stats['total_autopay_subscriptions']}\n"
|
||||
text += f"• Активных: {stats['active_autopay_subscriptions']}\n"
|
||||
text += f"• Просроченных: {stats['expired_autopay_subscriptions']}\n\n"
|
||||
|
||||
if stats['ready_for_autopay']:
|
||||
text += "🔄 **Готовы к автоплатежу:**\n"
|
||||
total_ready = 0
|
||||
for ready_info in stats['ready_for_autopay']:
|
||||
count = ready_info['count']
|
||||
days = ready_info['days']
|
||||
total_ready += count
|
||||
if count > 0:
|
||||
text += f"• За {days} дн.: {count} подписок\n"
|
||||
|
||||
if total_ready == 0:
|
||||
text += "• Нет подписок, готовых к продлению\n"
|
||||
text += "\n"
|
||||
|
||||
if insufficient_balance_users:
|
||||
text += f"⚠️ **Недостаточно средств ({len(insufficient_balance_users)}):**\n"
|
||||
for user_info in insufficient_balance_users[:5]:
|
||||
username = user_info.get('username', 'N/A')
|
||||
needed = user_info['needed_amount']
|
||||
days = user_info['expires_in_days']
|
||||
text += f"• @{username}: нужно {needed:.0f}₽ (через {days}д)\n"
|
||||
|
||||
if len(insufficient_balance_users) > 5:
|
||||
text += f"• ... и еще {len(insufficient_balance_users) - 5}\n"
|
||||
text += "\n"
|
||||
|
||||
if autopay_history:
|
||||
text += f"💳 **Последние автоплатежи:**\n"
|
||||
for payment in autopay_history[:5]:
|
||||
username = payment.get('username', 'N/A')
|
||||
amount = abs(payment['amount'])
|
||||
date_str = payment['created_at'].strftime('%d.%m %H:%M')
|
||||
status_emoji = "✅" if payment['status'] == 'completed' else "❌"
|
||||
text += f"• {status_emoji} @{username}: {amount:.0f}₽ ({date_str})\n"
|
||||
text += "\n"
|
||||
|
||||
autopay_service = kwargs.get('autopay_service')
|
||||
if autopay_service:
|
||||
service_status = await autopay_service.get_service_status()
|
||||
status_emoji = "✅" if service_status['is_running'] else "❌"
|
||||
text += f"🔧 **Статус сервиса:** {status_emoji}\n"
|
||||
text += f"• Интервал проверки: {service_status['check_interval']//60} мин\n"
|
||||
else:
|
||||
text += f"🔧 **Статус сервиса:** ❌ Недоступен\n"
|
||||
|
||||
text += f"\n🕐 Обновлено: {format_datetime(datetime.now(), user.language)}"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=autopay_statistics_keyboard(user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting detailed autopay statistics: {e}")
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка получения статистики",
|
||||
reply_markup=back_keyboard("admin_autopay", user.language)
|
||||
)
|
||||
|
||||
@admin_router.callback_query(F.data == "autopay_insufficient_balance_users")
|
||||
async def autopay_insufficient_balance_users_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
db = kwargs.get('db')
|
||||
if not db:
|
||||
await callback.answer("❌ База данных недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
insufficient_users = await db.get_users_with_insufficient_autopay_balance()
|
||||
|
||||
if not insufficient_users:
|
||||
text = "✅ **Все пользователи с автоплатежом имеют достаточный баланс**\n\n"
|
||||
text += "Проблемных автоплатежей не обнаружено."
|
||||
else:
|
||||
text = f"⚠️ **Пользователи с недостаточным балансом ({len(insufficient_users)})**\n\n"
|
||||
|
||||
insufficient_users.sort(key=lambda x: x['expires_in_days'])
|
||||
|
||||
for user_info in insufficient_users:
|
||||
username = user_info.get('username', 'N/A')
|
||||
first_name = user_info.get('first_name', 'N/A')
|
||||
current_balance = user_info['current_balance']
|
||||
needed = user_info['needed_amount']
|
||||
price = user_info['subscription_price']
|
||||
days = user_info['expires_in_days']
|
||||
sub_name = user_info['subscription_name']
|
||||
|
||||
display_name = first_name
|
||||
if username != 'N/A':
|
||||
display_name += f" (@{username})"
|
||||
|
||||
urgency_emoji = "🔴" if days <= 1 else "🟡" if days <= 3 else "🟠"
|
||||
|
||||
text += f"{urgency_emoji} **{display_name}**\n"
|
||||
text += f" 💳 Баланс: {current_balance:.2f}₽ / {price:.2f}₽\n"
|
||||
text += f" 💸 Нужно: {needed:.2f}₽\n"
|
||||
text += f" 📋 {sub_name}\n"
|
||||
text += f" ⏰ Истекает через: {days} дн.\n\n"
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_insufficient_balance_users")],
|
||||
[InlineKeyboardButton(text="📊 Статистика", callback_data="autopay_statistics")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting insufficient balance users: {e}")
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка получения списка пользователей",
|
||||
reply_markup=back_keyboard("admin_autopay", user.language)
|
||||
)
|
||||
|
||||
@admin_router.callback_query(F.data == "autopay_subscriptions_list")
|
||||
async def autopay_subscriptions_list_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
db = kwargs.get('db')
|
||||
if not db:
|
||||
await callback.answer("❌ База данных недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
subscriptions_with_autopay = await db.get_subscriptions_for_autopay()
|
||||
|
||||
subscriptions_data = []
|
||||
|
||||
from datetime import datetime
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
for user_sub in subscriptions_with_autopay:
|
||||
try:
|
||||
user_obj = await db.get_user_by_telegram_id(user_sub.user_id)
|
||||
username = user_obj.username if user_obj else 'N/A'
|
||||
|
||||
expires_in_days = (user_sub.expires_at - current_time).days
|
||||
|
||||
subscriptions_data.append({
|
||||
'user_id': user_sub.user_id,
|
||||
'username': username,
|
||||
'auto_pay_days_before': user_sub.auto_pay_days_before,
|
||||
'expires_in_days': expires_in_days,
|
||||
'subscription_id': user_sub.id
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error processing subscription {user_sub.id}: {e}")
|
||||
continue
|
||||
|
||||
subscriptions_data.sort(key=lambda x: x['expires_in_days'])
|
||||
|
||||
text = f"📋 Подписки с автоплатежом ({len(subscriptions_data)})\n\n"
|
||||
|
||||
if subscriptions_data:
|
||||
expired = [s for s in subscriptions_data if s['expires_in_days'] <= 0]
|
||||
due_soon = [s for s in subscriptions_data if 0 < s['expires_in_days'] <= s['auto_pay_days_before']]
|
||||
normal = [s for s in subscriptions_data if s['expires_in_days'] > s['auto_pay_days_before']]
|
||||
|
||||
text += f"📊 Статус:\n"
|
||||
text += f"• ❌ Истекли: {len(expired)}\n"
|
||||
text += f"• ⚠️ Скоро продление: {len(due_soon)}\n"
|
||||
text += f"• ✅ Нормальные: {len(normal)}\n\n"
|
||||
|
||||
text += "👥 Нажмите на пользователя для подробностей:"
|
||||
else:
|
||||
text += "📭 Нет подписок с включенным автоплатежом"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=autopay_subscriptions_keyboard(subscriptions_data, user.language)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting autopay subscriptions: {e}")
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка получения списка подписок",
|
||||
reply_markup=back_keyboard("admin_autopay", user.language)
|
||||
)
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("autopay_user_detail_"))
|
||||
async def autopay_user_detail_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
db = kwargs.get('db')
|
||||
if not db:
|
||||
await callback.answer("❌ База данных недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
user_id = int(callback.data.split("_")[-1])
|
||||
|
||||
target_user = await db.get_user_by_telegram_id(user_id)
|
||||
if not target_user:
|
||||
await callback.answer("❌ Пользователь не найден")
|
||||
return
|
||||
|
||||
user_subs = await db.get_user_subscriptions(user_id)
|
||||
autopay_subs = [sub for sub in user_subs if sub.auto_pay_enabled]
|
||||
|
||||
from datetime import datetime
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
text = f"👤 Пользователь с автоплатежом\n\n"
|
||||
|
||||
display_name = target_user.first_name or "N/A"
|
||||
if target_user.username:
|
||||
display_name += f" (@{target_user.username})"
|
||||
|
||||
text += f"📛 Имя: {display_name}\n"
|
||||
text += f"🆔 ID: {user_id}\n"
|
||||
text += f"💰 Баланс: {target_user.balance:.2f}₽\n\n"
|
||||
|
||||
text += f"🔄 Подписки с автоплатежом ({len(autopay_subs)}):\n\n"
|
||||
|
||||
for sub in autopay_subs:
|
||||
subscription = await db.get_subscription_by_id(sub.subscription_id)
|
||||
if not subscription:
|
||||
continue
|
||||
|
||||
days_left = (sub.expires_at - current_time).days
|
||||
|
||||
if days_left <= 0:
|
||||
status = "❌ Истекла"
|
||||
elif days_left <= sub.auto_pay_days_before:
|
||||
status = "⚠️ Скоро продление"
|
||||
else:
|
||||
status = "✅ Активна"
|
||||
|
||||
text += f"📋 {subscription.name}\n"
|
||||
text += f" {status} (через {days_left} дн.)\n"
|
||||
text += f" 💰 Цена продления: {subscription.price}₽\n"
|
||||
text += f" 📅 Продлять за: {sub.auto_pay_days_before} дн.\n"
|
||||
|
||||
if target_user.balance < subscription.price:
|
||||
needed = subscription.price - target_user.balance
|
||||
text += f" ⚠️ Нужно еще {needed:.2f}₽\n"
|
||||
else:
|
||||
text += f" ✅ Средств достаточно\n"
|
||||
|
||||
text += "\n"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=autopay_user_detail_keyboard(user_id, user.language)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing autopay user detail: {e}")
|
||||
await callback.answer("❌ Ошибка получения информации")
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_user_subscriptions_all")
|
||||
async def admin_user_subscriptions_all_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
await show_user_subscriptions_admin(callback, user, page=0, filter_type="all", **kwargs)
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_user_subscriptions_filters")
|
||||
async def admin_user_subscriptions_filters_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
db = kwargs.get('db')
|
||||
if not db:
|
||||
await callback.answer("❌ База данных недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
stats = await db.get_user_subscriptions_stats_admin()
|
||||
|
||||
text = f"🔍 Фильтры подписок пользователей\n\n"
|
||||
text += f"📊 Статистика:\n"
|
||||
text += f"• Всего подписок: {stats['total_subscriptions']}\n"
|
||||
text += f"• 🟢 Активных: {stats['active_subscriptions']}\n"
|
||||
text += f"• 🔴 Истекших: {stats['expired_subscriptions']}\n"
|
||||
text += f"• ⏰ Истекают скоро: {stats['expiring_subscriptions']}\n"
|
||||
text += f"• 🔄 С автоплатежом: {stats['autopay_subscriptions']}\n"
|
||||
text += f"• 🆓 Триальных: {stats['trial_subscriptions']}\n"
|
||||
text += f"• 📦 Импортированных: {stats['imported_subscriptions']}\n\n"
|
||||
text += f"Выберите фильтр для просмотра:"
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=admin_user_subscriptions_filters_keyboard(user.language)
|
||||
)
|
||||
except Exception as edit_error:
|
||||
if "message is not modified" in str(edit_error).lower():
|
||||
await callback.answer("✅ Фильтры обновлены", show_alert=False)
|
||||
else:
|
||||
logger.error(f"Error editing filters message: {edit_error}")
|
||||
await callback.answer("❌ Ошибка отображения фильтров", show_alert=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing subscriptions filters: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки фильтров", show_alert=True)
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("filter_subs_"))
|
||||
async def filter_subscriptions_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
filter_type = callback.data.replace("filter_subs_", "")
|
||||
await show_user_subscriptions_admin(callback, user, page=0, filter_type=filter_type, **kwargs)
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("user_subs_page_"))
|
||||
async def user_subscriptions_page_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
try:
|
||||
parts = callback.data.split("_")
|
||||
page = int(parts[3])
|
||||
filter_type = parts[4] if len(parts) > 4 else "all"
|
||||
|
||||
await show_user_subscriptions_admin(callback, user, page=page, filter_type=filter_type, **kwargs)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in user subscriptions pagination: {e}")
|
||||
await callback.answer("❌ Ошибка навигации", show_alert=True)
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("refresh_user_subs_"))
|
||||
async def refresh_user_subscriptions_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
filter_type = callback.data.replace("refresh_user_subs_", "")
|
||||
await callback.answer("🔄 Обновляю список...")
|
||||
await show_user_subscriptions_admin(callback, user, page=0, filter_type=filter_type, **kwargs)
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("admin_user_sub_detail_"))
|
||||
async def admin_user_subscription_detail_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
db = kwargs.get('db')
|
||||
if not db:
|
||||
await callback.answer("❌ База данных недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
subscription_id = int(callback.data.replace("admin_user_sub_detail_", ""))
|
||||
|
||||
subscription_detail = await db.get_user_subscription_detail_admin(subscription_id)
|
||||
if not subscription_detail:
|
||||
await callback.answer("❌ Подписка не найдена", show_alert=True)
|
||||
return
|
||||
|
||||
def clean_text(text):
|
||||
if not text:
|
||||
return "N/A"
|
||||
return str(text).replace('*', '').replace('_', '').replace('[', '').replace(']', '').replace('`', '')
|
||||
|
||||
user_first_name = clean_text(subscription_detail['user_first_name'])
|
||||
user_username = clean_text(subscription_detail['user_username'])
|
||||
subscription_name = clean_text(subscription_detail['subscription_name'])
|
||||
short_uuid = clean_text(subscription_detail['short_uuid'])
|
||||
|
||||
text = f"📋 Детали подписки пользователя\n\n"
|
||||
|
||||
text += f"👤 Пользователь:\n"
|
||||
text += f"├ Имя: {user_first_name}\n"
|
||||
text += f"├ Username: @{user_username}\n"
|
||||
text += f"├ Telegram ID: {subscription_detail['user_id']}\n"
|
||||
text += f"└ Баланс: {subscription_detail['user_balance']:.2f}₽\n\n"
|
||||
|
||||
text += f"📦 Подписка:\n"
|
||||
text += f"├ Название: {subscription_name}\n"
|
||||
text += f"├ Цена: {subscription_detail['subscription_price']}₽\n"
|
||||
text += f"├ Длительность: {subscription_detail['subscription_duration']} дн.\n"
|
||||
text += f"└ Short UUID: {short_uuid}\n\n"
|
||||
|
||||
status_emoji = subscription_detail['status_emoji']
|
||||
text += f"🔘 Статус: {status_emoji} "
|
||||
|
||||
if subscription_detail['status'] == "active":
|
||||
text += f"Активна (осталось {subscription_detail['days_left']} дн.)\n"
|
||||
elif subscription_detail['status'] == "expiring_soon":
|
||||
text += f"Истекает через {subscription_detail['days_left']} дн.\n"
|
||||
elif subscription_detail['status'] == "expired":
|
||||
text += "Истекла\n"
|
||||
elif subscription_detail['status'] == "inactive":
|
||||
text += "Приостановлена\n"
|
||||
|
||||
text += f"📅 Временные рамки:\n"
|
||||
text += f"├ Создана: {format_datetime(subscription_detail['created_at'], user.language)}\n"
|
||||
text += f"├ Истекает: {format_datetime(subscription_detail['expires_at'], user.language)}\n"
|
||||
if subscription_detail['updated_at']:
|
||||
text += f"└ Обновлена: {format_datetime(subscription_detail['updated_at'], user.language)}\n"
|
||||
else:
|
||||
text += f"└ Обновлена: Никогда\n"
|
||||
|
||||
text += f"\n🔄 Автоплатеж:\n"
|
||||
if subscription_detail['auto_pay_enabled']:
|
||||
text += f"├ Статус: ✅ Включен\n"
|
||||
text += f"└ Продлять за: {subscription_detail['auto_pay_days_before']} дн. до истечения\n"
|
||||
|
||||
if subscription_detail['user_balance'] < subscription_detail['subscription_price']:
|
||||
needed = subscription_detail['subscription_price'] - subscription_detail['user_balance']
|
||||
text += f"⚠️ Недостаточно средств! Нужно еще {needed:.2f}₽\n"
|
||||
else:
|
||||
text += f"└ Статус: ❌ Отключен\n"
|
||||
|
||||
if subscription_detail['is_trial']:
|
||||
text += f"\n🆓 Тип: Триальная подписка\n"
|
||||
elif subscription_detail['is_imported']:
|
||||
text += f"\n📦 Тип: Импортированная подписка\n"
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=admin_user_subscription_detail_keyboard(
|
||||
subscription_id, subscription_detail['user_id'], user.language
|
||||
)
|
||||
)
|
||||
except Exception as edit_error:
|
||||
if "message is not modified" in str(edit_error).lower():
|
||||
await callback.answer("✅ Информация актуальна", show_alert=False)
|
||||
else:
|
||||
logger.error(f"Error editing detail message: {edit_error}")
|
||||
await callback.answer("❌ Ошибка отображения деталей", show_alert=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing user subscription detail: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки деталей", show_alert=True)
|
||||
|
||||
|
||||
async def show_user_subscriptions_admin(callback: CallbackQuery, user: User, page: int = 0,
|
||||
filter_type: str = "all", **kwargs):
|
||||
db = kwargs.get('db')
|
||||
if not db:
|
||||
await callback.answer("❌ База данных недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
page_size = 10
|
||||
offset = page * page_size
|
||||
|
||||
subscriptions_data, total_count = await db.get_all_user_subscriptions_admin(
|
||||
offset=offset, limit=page_size, filter_type=filter_type
|
||||
)
|
||||
|
||||
if not subscriptions_data and page == 0:
|
||||
filter_names = {
|
||||
"all": "подписок",
|
||||
"active": "активных подписок",
|
||||
"expired": "истекших подписок",
|
||||
"expiring": "истекающих подписок",
|
||||
"autopay": "подписок с автоплатежом",
|
||||
"trial": "триальных подписок",
|
||||
"imported": "импортированных подписок"
|
||||
}
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"📋 Список {filter_names.get(filter_type, 'подписок')} пуст",
|
||||
reply_markup=admin_user_subscriptions_filters_keyboard(user.language)
|
||||
)
|
||||
return
|
||||
|
||||
if not subscriptions_data and page > 0:
|
||||
await show_user_subscriptions_admin(callback, user, page - 1, filter_type, **kwargs)
|
||||
return
|
||||
|
||||
filter_titles = {
|
||||
"all": "Все подписки пользователей",
|
||||
"active": "Активные подписки",
|
||||
"expired": "Истекшие подписки",
|
||||
"expiring": "Истекающие подписки",
|
||||
"autopay": "Подписки с автоплатежом",
|
||||
"trial": "Триальные подписки",
|
||||
"imported": "Импортированные подписки"
|
||||
}
|
||||
|
||||
total_pages = (total_count + page_size - 1) // page_size
|
||||
|
||||
text = f"📋 {filter_titles.get(filter_type, 'Подписки')}\n"
|
||||
text += f"📄 Страница {page + 1} из {total_pages} • Всего: {total_count}\n\n"
|
||||
|
||||
for i, sub_data in enumerate(subscriptions_data, start=offset + 1):
|
||||
status_emojis = {
|
||||
"active": "🟢",
|
||||
"expiring": "🟡",
|
||||
"expiring_soon": "🚨",
|
||||
"expired": "❌",
|
||||
"inactive": "⏸"
|
||||
}
|
||||
status_emoji = status_emojis.get(sub_data['status'], "⚪")
|
||||
|
||||
user_display = sub_data['user_first_name'] or "Unknown"
|
||||
user_display = user_display.replace('*', '').replace('_', '').replace('[', '').replace(']', '').replace('`', '')
|
||||
|
||||
if sub_data['user_username'] != 'N/A':
|
||||
clean_username = sub_data['user_username'].replace('*', '').replace('_', '').replace('[', '').replace(']', '').replace('`', '')
|
||||
user_display += f" (@{clean_username})"
|
||||
|
||||
subscription_name = sub_data['subscription_name'].replace('*', '').replace('_', '').replace('[', '').replace(']', '').replace('`', '')
|
||||
|
||||
text += f"{i}. {status_emoji} {user_display}\n"
|
||||
text += f" 📋 {subscription_name} — {sub_data['subscription_price']}₽\n"
|
||||
text += f" 📅 Создана: {format_datetime(sub_data['created_at'], user.language)}\n"
|
||||
|
||||
if sub_data['status'] == "active":
|
||||
text += f" ⏰ Истекает через {sub_data['days_left']} дн.\n"
|
||||
elif sub_data['status'] in ["expiring", "expiring_soon"]:
|
||||
text += f" ⚠️ Истекает через {sub_data['days_left']} дн.\n"
|
||||
elif sub_data['status'] == "expired":
|
||||
text += f" ❌ Истекла\n"
|
||||
elif sub_data['status'] == "inactive":
|
||||
text += f" ⏸ Приостановлена\n"
|
||||
|
||||
if sub_data['auto_pay_enabled']:
|
||||
text += f" 🔄 Автоплатеж: за {sub_data['auto_pay_days_before']} дн.\n"
|
||||
|
||||
labels = []
|
||||
if sub_data['is_trial']:
|
||||
labels.append("🆓 Trial")
|
||||
if sub_data['is_imported']:
|
||||
labels.append("📦 Import")
|
||||
|
||||
if labels:
|
||||
text += f" 🏷 {' • '.join(labels)}\n"
|
||||
|
||||
text += "\n"
|
||||
|
||||
additional_buttons = []
|
||||
if len(subscriptions_data) <= 5:
|
||||
for sub_data in subscriptions_data:
|
||||
user_name = (sub_data['user_first_name'] or "User")[:10]
|
||||
user_name = user_name.replace('*', '').replace('_', '').replace('[', '').replace(']', '').replace('`', '')
|
||||
if len(user_name) > 10:
|
||||
user_name = user_name[:7] + "..."
|
||||
|
||||
additional_buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=f"👤 {user_name}",
|
||||
callback_data=f"admin_user_sub_detail_{sub_data['id']}"
|
||||
)
|
||||
])
|
||||
|
||||
if additional_buttons:
|
||||
text += "👆 Нажмите на кнопку для просмотра деталей:"
|
||||
|
||||
keyboard = user_subscriptions_pagination_keyboard(page, total_pages, filter_type, user.language)
|
||||
|
||||
if additional_buttons:
|
||||
nav_buttons = keyboard.inline_keyboard[0] if keyboard.inline_keyboard else []
|
||||
other_buttons = keyboard.inline_keyboard[1:] if len(keyboard.inline_keyboard) > 1 else []
|
||||
|
||||
new_keyboard_buttons = []
|
||||
if nav_buttons:
|
||||
new_keyboard_buttons.append(nav_buttons)
|
||||
|
||||
for i in range(0, len(additional_buttons), 2):
|
||||
row = []
|
||||
for j in range(2):
|
||||
if i + j < len(additional_buttons):
|
||||
row.extend(additional_buttons[i + j])
|
||||
if row:
|
||||
new_keyboard_buttons.append(row)
|
||||
|
||||
new_keyboard_buttons.extend(other_buttons)
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=new_keyboard_buttons)
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard
|
||||
)
|
||||
except Exception as edit_error:
|
||||
if "message is not modified" in str(edit_error).lower():
|
||||
await callback.answer("✅ Список обновлен", show_alert=False)
|
||||
else:
|
||||
logger.error(f"Error editing message: {edit_error}")
|
||||
try:
|
||||
await callback.message.answer(
|
||||
text,
|
||||
reply_markup=keyboard
|
||||
)
|
||||
except Exception as send_error:
|
||||
logger.error(f"Error sending new message: {send_error}")
|
||||
await callback.answer("❌ Ошибка отображения", show_alert=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing user subscriptions admin: {e}")
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка загрузки подписок",
|
||||
reply_markup=admin_user_subscriptions_filters_keyboard(user.language)
|
||||
)
|
||||
except:
|
||||
await callback.answer("❌ Ошибка загрузки подписок", show_alert=True)
|
||||
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("edit_user_sub_"))
|
||||
async def edit_user_subscription_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
db = kwargs.get('db')
|
||||
if not db:
|
||||
await callback.answer("❌ База данных недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
subscription_id = int(callback.data.replace("edit_user_sub_", ""))
|
||||
|
||||
subscription_detail = await db.get_user_subscription_detail_admin(subscription_id)
|
||||
if not subscription_detail:
|
||||
await callback.answer("❌ Подписка не найдена", show_alert=True)
|
||||
return
|
||||
|
||||
text = f"✏️ **Редактирование подписки**\n\n"
|
||||
text += f"👤 Пользователь: {subscription_detail['user_first_name']}\n"
|
||||
text += f"📋 Подписка: {subscription_detail['subscription_name']}\n\n"
|
||||
text += f"Что вы хотите изменить?"
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="📅 Срок действия", callback_data=f"edit_sub_expiry_{subscription_id}"),
|
||||
InlineKeyboardButton(text="🔘 Статус", callback_data=f"toggle_sub_status_{subscription_id}")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🔄 Автоплатеж", callback_data=f"edit_sub_autopay_{subscription_id}"),
|
||||
InlineKeyboardButton(text="📊 Трафик", callback_data=f"edit_sub_traffic_{subscription_id}")
|
||||
],
|
||||
[InlineKeyboardButton(text="🔙 К деталям", callback_data=f"admin_user_sub_detail_{subscription_id}")]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error editing user subscription: {e}")
|
||||
await callback.answer("❌ Ошибка редактирования", show_alert=True)
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("refresh_user_sub_"))
|
||||
async def refresh_user_subscription_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
subscription_id = callback.data.replace("refresh_user_sub_", "")
|
||||
await callback.answer("🔄 Обновляю информацию...")
|
||||
|
||||
new_callback_data = f"admin_user_sub_detail_{subscription_id}"
|
||||
callback.data = new_callback_data
|
||||
await admin_user_subscription_detail_callback(callback, user, **kwargs)
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("edit_sub_traffic_"))
|
||||
async def edit_sub_traffic_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
try:
|
||||
subscription_id = int(callback.data.split("_")[-1])
|
||||
|
||||
db = kwargs.get('db')
|
||||
if not db:
|
||||
await callback.answer("❌ База данных недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
subscription_detail = await db.get_user_subscription_detail_admin(subscription_id)
|
||||
if not subscription_detail:
|
||||
await callback.answer("❌ Подписка не найдена", show_alert=True)
|
||||
return
|
||||
|
||||
text = f"📊 **Изменение лимита трафика**\n\n"
|
||||
text += f"👤 Пользователь: {subscription_detail['user_first_name']}\n"
|
||||
text += f"📋 Подписка: {subscription_detail['subscription_name']}\n\n"
|
||||
text += f"Введите новый лимит трафика в ГБ (0 = безлимит):"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔙 К деталям", callback_data=f"admin_user_sub_detail_{subscription_id}")]
|
||||
]),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error editing subscription traffic: {e}")
|
||||
await callback.answer("❌ Ошибка редактирования", show_alert=True)
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("edit_sub_expiry_"))
|
||||
async def edit_sub_expiry_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
try:
|
||||
subscription_id = int(callback.data.split("_")[-1])
|
||||
|
||||
db = kwargs.get('db')
|
||||
if not db:
|
||||
await callback.answer("❌ База данных недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
subscription_detail = await db.get_user_subscription_detail_admin(subscription_id)
|
||||
if not subscription_detail:
|
||||
await callback.answer("❌ Подписка не найдена", show_alert=True)
|
||||
return
|
||||
|
||||
text = f"📅 **Изменение срока действия**\n\n"
|
||||
text += f"👤 Пользователь: {subscription_detail['user_first_name']}\n"
|
||||
text += f"📋 Подписка: {subscription_detail['subscription_name']}\n\n"
|
||||
text += f"Введите новую дату истечения (YYYY-MM-DD) или количество дней:"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔙 К деталям", callback_data=f"admin_user_sub_detail_{subscription_id}")]
|
||||
]),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error editing subscription expiry: {e}")
|
||||
await callback.answer("❌ Ошибка редактирования", show_alert=True)
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("toggle_sub_status_"))
|
||||
async def toggle_subscription_status_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
try:
|
||||
subscription_id = int(callback.data.split("_")[-1])
|
||||
|
||||
db = kwargs.get('db')
|
||||
if not db:
|
||||
await callback.answer("❌ База данных недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
async with db.session_factory() as session:
|
||||
from sqlalchemy import select, update
|
||||
from database import UserSubscription
|
||||
|
||||
result = await session.execute(
|
||||
select(UserSubscription).where(UserSubscription.id == subscription_id)
|
||||
)
|
||||
user_subscription = result.scalar_one_or_none()
|
||||
|
||||
if not user_subscription:
|
||||
await callback.answer("❌ Подписка не найдена", show_alert=True)
|
||||
return
|
||||
|
||||
new_status = not user_subscription.is_active
|
||||
|
||||
await session.execute(
|
||||
update(UserSubscription)
|
||||
.where(UserSubscription.id == subscription_id)
|
||||
.values(is_active=new_status)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
status_text = "активирована" if new_status else "деактивирована"
|
||||
await callback.answer(f"✅ Подписка {status_text}")
|
||||
|
||||
log_user_action(user.telegram_id, "subscription_status_toggled",
|
||||
f"SubID: {subscription_id}, Active: {new_status}")
|
||||
|
||||
await admin_user_subscription_detail_callback(callback, user, **kwargs)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error toggling subscription status: {e}")
|
||||
await callback.answer("❌ Ошибка изменения статуса", show_alert=True)
|
||||
|
||||
@admin_router.callback_query(F.data.startswith("edit_sub_autopay_"))
|
||||
async def edit_sub_autopay_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
try:
|
||||
subscription_id = int(callback.data.split("_")[-1])
|
||||
|
||||
db = kwargs.get('db')
|
||||
if not db:
|
||||
await callback.answer("❌ База данных недоступна", show_alert=True)
|
||||
return
|
||||
|
||||
subscription_detail = await db.get_user_subscription_detail_admin(subscription_id)
|
||||
if not subscription_detail:
|
||||
await callback.answer("❌ Подписка не найдена", show_alert=True)
|
||||
return
|
||||
|
||||
current_autopay = subscription_detail['auto_pay_enabled']
|
||||
autopay_days = subscription_detail['auto_pay_days_before']
|
||||
|
||||
text = f"🔄 **Настройки автоплатежа**\n\n"
|
||||
text += f"👤 Пользователь: {subscription_detail['user_first_name']}\n"
|
||||
text += f"📋 Подписка: {subscription_detail['subscription_name']}\n\n"
|
||||
text += f"Текущее состояние: {'✅ Включен' if current_autopay else '❌ Отключен'}\n"
|
||||
if current_autopay:
|
||||
text += f"Продлевать за: {autopay_days} дней до истечения\n\n"
|
||||
|
||||
buttons = []
|
||||
if current_autopay:
|
||||
buttons.append([InlineKeyboardButton(text="❌ Отключить автоплатеж", callback_data=f"disable_autopay_{subscription_id}")])
|
||||
buttons.append([InlineKeyboardButton(text="📅 Изменить дни", callback_data=f"change_autopay_days_{subscription_id}")])
|
||||
else:
|
||||
buttons.append([InlineKeyboardButton(text="✅ Включить автоплатеж", callback_data=f"enable_autopay_{subscription_id}")])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 К деталям", callback_data=f"admin_user_sub_detail_{subscription_id}")])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error editing subscription autopay: {e}")
|
||||
await callback.answer("❌ Ошибка редактирования", show_alert=True)
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
text-anchor="middle"
|
||||
fill="url(#mainGradient)"
|
||||
filter="url(#glow)">
|
||||
RemnaWave
|
||||
Remnawave
|
||||
</text>
|
||||
|
||||
<text x="200" y="345"
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,284 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional
|
||||
from database import Database, UserSubscription, User, Subscription
|
||||
from remnawave_api import RemnaWaveAPI
|
||||
from referral_utils import process_referral_rewards
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class AutoPayService:
|
||||
|
||||
def __init__(self, db: Database, api: Optional[RemnaWaveAPI] = None, bot=None):
|
||||
self.db = db
|
||||
self.api = api
|
||||
self.bot = bot
|
||||
self.is_running = False
|
||||
self.check_task = None
|
||||
|
||||
async def start(self):
|
||||
if self.is_running:
|
||||
logger.warning("AutoPay service is already running")
|
||||
return
|
||||
|
||||
self.is_running = True
|
||||
logger.info("🔄 Starting AutoPay service...")
|
||||
|
||||
self.check_task = asyncio.create_task(self._periodic_check())
|
||||
|
||||
async def stop(self):
|
||||
if not self.is_running:
|
||||
return
|
||||
|
||||
self.is_running = False
|
||||
logger.info("⏹ Stopping AutoPay service...")
|
||||
|
||||
if self.check_task:
|
||||
self.check_task.cancel()
|
||||
try:
|
||||
await self.check_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
async def _periodic_check(self):
|
||||
while self.is_running:
|
||||
try:
|
||||
await self.process_autopayments()
|
||||
# Проверяем каждые 30 минут
|
||||
await asyncio.sleep(1800)
|
||||
except asyncio.CancelledError:
|
||||
logger.info("AutoPay periodic check cancelled")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error in AutoPay periodic check: {e}")
|
||||
await asyncio.sleep(300)
|
||||
|
||||
async def process_autopayments(self) -> dict:
|
||||
logger.info("🔄 Processing autopayments...")
|
||||
|
||||
try:
|
||||
subscriptions_to_pay = await self.db.get_subscriptions_for_autopay()
|
||||
|
||||
if not subscriptions_to_pay:
|
||||
logger.info("No subscriptions ready for autopay")
|
||||
return {
|
||||
'processed': 0,
|
||||
'successful': 0,
|
||||
'failed': 0,
|
||||
'insufficient_balance': 0,
|
||||
'errors': []
|
||||
}
|
||||
|
||||
logger.info(f"Found {len(subscriptions_to_pay)} subscriptions for autopay")
|
||||
|
||||
stats = {
|
||||
'processed': 0,
|
||||
'successful': 0,
|
||||
'failed': 0,
|
||||
'insufficient_balance': 0,
|
||||
'errors': []
|
||||
}
|
||||
|
||||
for user_sub in subscriptions_to_pay:
|
||||
try:
|
||||
result = await self._process_single_autopayment(user_sub)
|
||||
stats['processed'] += 1
|
||||
|
||||
if result['success']:
|
||||
stats['successful'] += 1
|
||||
logger.info(f"✅ Autopay successful for user {user_sub.user_id}, subscription {user_sub.id}")
|
||||
elif result['reason'] == 'insufficient_balance':
|
||||
stats['insufficient_balance'] += 1
|
||||
logger.info(f"💳 Insufficient balance for user {user_sub.user_id}, subscription {user_sub.id}")
|
||||
else:
|
||||
stats['failed'] += 1
|
||||
stats['errors'].append(f"User {user_sub.user_id}: {result['reason']}")
|
||||
logger.warning(f"❌ Autopay failed for user {user_sub.user_id}: {result['reason']}")
|
||||
|
||||
except Exception as e:
|
||||
stats['processed'] += 1
|
||||
stats['failed'] += 1
|
||||
error_msg = f"User {user_sub.user_id}: {str(e)}"
|
||||
stats['errors'].append(error_msg)
|
||||
logger.error(f"Error processing autopay for user {user_sub.user_id}: {e}")
|
||||
|
||||
logger.info(f"📊 Autopay processing complete: {stats['successful']} successful, "
|
||||
f"{stats['failed']} failed, {stats['insufficient_balance']} insufficient balance")
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in process_autopayments: {e}")
|
||||
return {
|
||||
'processed': 0,
|
||||
'successful': 0,
|
||||
'failed': 0,
|
||||
'insufficient_balance': 0,
|
||||
'errors': [str(e)]
|
||||
}
|
||||
|
||||
async def _process_single_autopayment(self, user_sub: UserSubscription) -> dict:
|
||||
try:
|
||||
user = await self.db.get_user_by_telegram_id(user_sub.user_id)
|
||||
if not user:
|
||||
return {'success': False, 'reason': 'User not found'}
|
||||
|
||||
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription:
|
||||
return {'success': False, 'reason': 'Subscription plan not found'}
|
||||
|
||||
if subscription.is_trial:
|
||||
logger.info(f"Skipping autopay for trial subscription: user {user_sub.user_id}")
|
||||
return {'success': False, 'reason': 'Trial subscriptions are not eligible for autopay'}
|
||||
|
||||
if not user_sub.is_active or not user_sub.auto_pay_enabled:
|
||||
return {'success': False, 'reason': 'Subscription inactive or autopay disabled'}
|
||||
|
||||
if user.balance < subscription.price:
|
||||
await self._notify_insufficient_balance(user, subscription, user_sub)
|
||||
return {'success': False, 'reason': 'insufficient_balance'}
|
||||
|
||||
return await self._execute_autopayment(user, subscription, user_sub)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in _process_single_autopayment: {e}")
|
||||
return {'success': False, 'reason': str(e)}
|
||||
|
||||
async def _execute_autopayment(self, user: User, subscription: Subscription, user_sub: UserSubscription) -> dict:
|
||||
try:
|
||||
user.balance -= subscription.price
|
||||
await self.db.update_user(user)
|
||||
|
||||
now = datetime.utcnow()
|
||||
if user_sub.expires_at > now:
|
||||
new_expiry = user_sub.expires_at + timedelta(days=subscription.duration_days)
|
||||
else:
|
||||
new_expiry = now + timedelta(days=subscription.duration_days)
|
||||
|
||||
user_sub.expires_at = new_expiry
|
||||
user_sub.is_active = True
|
||||
await self.db.update_user_subscription(user_sub)
|
||||
|
||||
if self.api and user_sub.short_uuid:
|
||||
try:
|
||||
remna_user_details = await self.api.get_user_by_short_uuid(user_sub.short_uuid)
|
||||
if remna_user_details:
|
||||
user_uuid = remna_user_details.get('uuid')
|
||||
if user_uuid:
|
||||
expiry_str = new_expiry.isoformat() + 'Z'
|
||||
update_data = {
|
||||
'enable': True,
|
||||
'expireAt': expiry_str
|
||||
}
|
||||
await self.api.update_user(user_uuid, update_data)
|
||||
logger.info(f"Updated RemnaWave expiry for user {user_sub.user_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to update RemnaWave expiry: {e}")
|
||||
|
||||
payment = await self.db.create_payment(
|
||||
user_id=user_sub.user_id,
|
||||
amount=-subscription.price,
|
||||
payment_type='autopay',
|
||||
description=f'Автоплатеж: {subscription.name}',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
if self.bot:
|
||||
try:
|
||||
await process_referral_rewards(
|
||||
user_sub.user_id,
|
||||
subscription.price,
|
||||
payment.id,
|
||||
self.db,
|
||||
self.bot,
|
||||
payment_type='autopay'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to process referral rewards for autopay: {e}")
|
||||
|
||||
await self._notify_successful_autopay(user, subscription, user_sub, new_expiry)
|
||||
|
||||
return {'success': True, 'reason': 'Payment processed successfully'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing autopayment: {e}")
|
||||
try:
|
||||
user.balance += subscription.price
|
||||
await self.db.update_user(user)
|
||||
except:
|
||||
pass
|
||||
return {'success': False, 'reason': str(e)}
|
||||
|
||||
async def _notify_successful_autopay(self, user: User, subscription: Subscription,
|
||||
user_sub: UserSubscription, new_expiry: datetime):
|
||||
if not self.bot:
|
||||
return
|
||||
|
||||
try:
|
||||
from utils import format_datetime
|
||||
|
||||
text = f"✅ Автоматическое продление подписки\n\n"
|
||||
text += f"📋 Подписка: {subscription.name}\n"
|
||||
text += f"💰 Списано: {subscription.price} руб.\n"
|
||||
text += f"📅 Продлено до: {format_datetime(new_expiry, user.language)}\n"
|
||||
text += f"💳 Остаток на балансе: {user.balance} руб.\n\n"
|
||||
text += f"🔄 Следующее продление произойдет автоматически за {user_sub.auto_pay_days_before} дн. до истечения.\n\n"
|
||||
text += f"ℹ️ Для отключения автоплатежа перейдите в 'Мои подписки' → выберите подписку"
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")],
|
||||
[InlineKeyboardButton(text="💰 Баланс", callback_data="balance")]
|
||||
])
|
||||
|
||||
await self.bot.send_message(
|
||||
user.telegram_id,
|
||||
text,
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send autopay success notification: {e}")
|
||||
|
||||
async def _notify_insufficient_balance(self, user: User, subscription: Subscription, user_sub: UserSubscription):
|
||||
if not self.bot:
|
||||
return
|
||||
|
||||
try:
|
||||
from utils import format_datetime
|
||||
|
||||
needed = subscription.price - user.balance
|
||||
days_left = (user_sub.expires_at - datetime.utcnow()).days
|
||||
|
||||
text = f"⚠️ Не удалось автоматически продлить подписку\n\n"
|
||||
text += f"📋 Подписка: {subscription.name}\n"
|
||||
text += f"💰 Нужно для продления: {subscription.price} руб.\n"
|
||||
text += f"💳 Ваш баланс: {user.balance} руб.\n"
|
||||
text += f"💸 Недостает: {needed} руб.\n\n"
|
||||
text += f"📅 Подписка истекает: {format_datetime(user_sub.expires_at, user.language)}\n"
|
||||
text += f"⏰ Осталось дней: {days_left}\n\n"
|
||||
text += f"💡 Пополните баланс для автоматического продления"
|
||||
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="topup_balance")],
|
||||
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")]
|
||||
])
|
||||
|
||||
await self.bot.send_message(
|
||||
user.telegram_id,
|
||||
text,
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send insufficient balance notification: {e}")
|
||||
|
||||
async def get_service_status(self) -> dict:
|
||||
return {
|
||||
'is_running': self.is_running,
|
||||
'check_interval': 1800, # 30 минут
|
||||
'has_api': self.api is not None,
|
||||
'has_bot': self.bot is not None
|
||||
}
|
||||
@@ -352,6 +352,42 @@ create_env_file() {
|
||||
read -p "Введите REFERRAL_THRESHOLD (сумму с .0 на конце): " REFERRAL_THRESHOLD
|
||||
read -p "Введите REFERRAL_PERCENTAGE (с 0. в начале): " REFERRAL_PERCENTAGE
|
||||
|
||||
# Настройки оплаты звездами Telegram
|
||||
echo ""
|
||||
echo -e "${YELLOW}=== Настройка оплаты звездами Telegram ===${NC}"
|
||||
while true; do
|
||||
read -p "Включить оплату звездами Telegram? (y/n): " stars_enabled
|
||||
case $stars_enabled in
|
||||
[Yy]*)
|
||||
STARS_ENABLED="true"
|
||||
echo ""
|
||||
echo -e "${BLUE}Настройка курсов обмена звезд на рубли:${NC}"
|
||||
echo -e "${YELLOW}Введите курс обмена для каждого пакета звезд${NC}"
|
||||
echo -e "${YELLOW}(например, если 100 звезд = 150 рублей, введите 150)${NC}"
|
||||
echo ""
|
||||
|
||||
read -p "Курс для 100 звезд (в рублях): " STARS_100_RATE
|
||||
read -p "Курс для 150 звезд (в рублях): " STARS_150_RATE
|
||||
read -p "Курс для 250 звезд (в рублях): " STARS_250_RATE
|
||||
read -p "Курс для 350 звезд (в рублях): " STARS_350_RATE
|
||||
read -p "Курс для 500 звезд (в рублях): " STARS_500_RATE
|
||||
break
|
||||
;;
|
||||
[Nn]*)
|
||||
STARS_ENABLED="false"
|
||||
STARS_100_RATE=""
|
||||
STARS_150_RATE=""
|
||||
STARS_250_RATE=""
|
||||
STARS_350_RATE=""
|
||||
STARS_500_RATE=""
|
||||
break
|
||||
;;
|
||||
*)
|
||||
error "Пожалуйста, ответьте y или n."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Настройки мониторинга
|
||||
read -p "Введите DELETE_EXPIRED_TRIAL_DAYS: " DELETE_EXPIRED_TRIAL_DAYS
|
||||
read -p "Введите DELETE_EXPIRED_REGULAR_DAYS: " DELETE_EXPIRED_REGULAR_DAYS
|
||||
@@ -383,6 +419,43 @@ REFERRAL_REFERRED_BONUS=$REFERRAL_REFERRED_BONUS
|
||||
REFERRAL_THRESHOLD=$REFERRAL_THRESHOLD
|
||||
REFERRAL_PERCENTAGE=$REFERRAL_PERCENTAGE
|
||||
|
||||
# Telegram Stars Payment Settings
|
||||
STARS_ENABLED=$STARS_ENABLED
|
||||
EOF
|
||||
|
||||
# Добавляем курсы звезд только если включена оплата звездами
|
||||
if [ "$STARS_ENABLED" = "true" ]; then
|
||||
cat >> "$ENV_FILE" << EOF
|
||||
# Курсы обмена звезд на рубли
|
||||
# 100 звёзд
|
||||
STARS_100_RATE=$STARS_100_RATE
|
||||
# 150 звёзд
|
||||
STARS_150_RATE=$STARS_150_RATE
|
||||
# 250 звёзд
|
||||
STARS_250_RATE=$STARS_250_RATE
|
||||
# 350 звёзд
|
||||
STARS_350_RATE=$STARS_350_RATE
|
||||
# 500 звёзд
|
||||
STARS_500_RATE=$STARS_500_RATE
|
||||
EOF
|
||||
else
|
||||
cat >> "$ENV_FILE" << EOF
|
||||
# Курсы обмена звезд на рубли (отключено)
|
||||
# 100 звёзд
|
||||
STARS_100_RATE=
|
||||
# 150 звёзд
|
||||
STARS_150_RATE=
|
||||
# 250 звёзд
|
||||
STARS_250_RATE=
|
||||
# 350 звёзд
|
||||
STARS_350_RATE=
|
||||
# 500 звёзд
|
||||
STARS_500_RATE=
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat >> "$ENV_FILE" << EOF
|
||||
|
||||
# Monitor Service Settings
|
||||
MONITOR_ENABLED=true
|
||||
MONITOR_CHECK_INTERVAL=21600
|
||||
@@ -759,6 +832,250 @@ diagnose_database() {
|
||||
echo -e "${RED}Не удается получить размер базы данных${NC}"
|
||||
}
|
||||
}
|
||||
|
||||
# Функция экстренного исправления базы данных
|
||||
emergency_fix_database() {
|
||||
log "Экстренное исправление базы данных..."
|
||||
|
||||
cd "$BOT_DIR"
|
||||
|
||||
# Проверяем, запущен ли контейнер бота
|
||||
if ! docker compose ps bot | grep -q "Up"; then
|
||||
warn "Контейнер бота не запущен. Запускаем бота..."
|
||||
docker compose up -d bot
|
||||
|
||||
# Ждем готовности бота
|
||||
log "Ожидание готовности бота..."
|
||||
for i in {1..60}; do
|
||||
if docker compose logs bot 2>/dev/null | grep -q "Bot started successfully\|Application startup complete\|Bot is running"; then
|
||||
log "Бот готов к работе"
|
||||
break
|
||||
fi
|
||||
if [ $i -eq 60 ]; then
|
||||
warn "Бот не запустился полностью, но попробуем выполнить исправление"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
echo -n "."
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Создаем скрипт исправления во временном файле
|
||||
EMERGENCY_SCRIPT="$BOT_DIR/emergency_fix.py"
|
||||
|
||||
log "Создание скрипта экстренного исправления..."
|
||||
cat > "$EMERGENCY_SCRIPT" << 'EOF'
|
||||
"""
|
||||
Экстренное исправление проблемы с отображением подписок
|
||||
Этот патч добавляет недостающие поля в таблицу user_subscriptions
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Добавляем корневую директорию в путь
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
try:
|
||||
from config import load_config
|
||||
from database import Database
|
||||
except ImportError:
|
||||
# Если не можем импортировать, попробуем из app
|
||||
sys.path.insert(0, '/app')
|
||||
try:
|
||||
from config import load_config
|
||||
from database import Database
|
||||
except ImportError:
|
||||
print("❌ Не удается импортировать модули. Проверьте структуру проекта.")
|
||||
sys.exit(1)
|
||||
|
||||
from sqlalchemy import text
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def check_and_add_column(db, column_name, column_definition):
|
||||
"""Проверяет и добавляет колонку в таблицу"""
|
||||
try:
|
||||
# Отдельная транзакция для проверки
|
||||
async with db.engine.begin() as conn:
|
||||
await conn.execute(text(f"SELECT {column_name} FROM user_subscriptions LIMIT 1"))
|
||||
logger.info(f"✅ Поле {column_name} уже существует")
|
||||
return True
|
||||
except Exception:
|
||||
# Отдельная транзакция для добавления колонки
|
||||
try:
|
||||
async with db.engine.begin() as conn:
|
||||
logger.info(f"➕ Добавляю поле {column_name}...")
|
||||
await conn.execute(text(f"""
|
||||
ALTER TABLE user_subscriptions
|
||||
ADD COLUMN {column_name} {column_definition}
|
||||
"""))
|
||||
logger.info(f"✅ Поле {column_name} добавлено")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при добавлении {column_name}: {e}")
|
||||
return False
|
||||
|
||||
async def emergency_fix():
|
||||
"""Экстренное исправление базы данных"""
|
||||
|
||||
try:
|
||||
# Загружаем конфигурацию
|
||||
config = load_config()
|
||||
|
||||
# Подключаемся к базе данных
|
||||
db = Database(config.DATABASE_URL)
|
||||
|
||||
logger.info("🔧 Выполняю экстренное исправление базы данных...")
|
||||
|
||||
# Проверяем существование таблицы user_subscriptions
|
||||
try:
|
||||
async with db.engine.begin() as conn:
|
||||
result = await conn.execute(text("SELECT COUNT(*) FROM user_subscriptions"))
|
||||
count = result.scalar()
|
||||
logger.info(f"📊 Найдено {count} подписок в таблице user_subscriptions")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Таблица user_subscriptions не найдена: {e}")
|
||||
await db.close()
|
||||
return
|
||||
|
||||
# Добавляем поля по одному в отдельных транзакциях
|
||||
success1 = await check_and_add_column(db, "auto_pay_enabled", "BOOLEAN DEFAULT FALSE")
|
||||
success2 = await check_and_add_column(db, "auto_pay_days_before", "INTEGER DEFAULT 3")
|
||||
|
||||
# Финальная проверка в отдельной транзакции
|
||||
if success1 and success2:
|
||||
try:
|
||||
async with db.engine.begin() as conn:
|
||||
result = await conn.execute(text("""
|
||||
SELECT id, auto_pay_enabled, auto_pay_days_before
|
||||
FROM user_subscriptions LIMIT 1
|
||||
"""))
|
||||
row = result.fetchone()
|
||||
if row:
|
||||
logger.info("✅ Все поля доступны для чтения")
|
||||
logger.info(f"🔍 Пример записи: id={row[0]}, auto_pay_enabled={row[1]}, auto_pay_days_before={row[2]}")
|
||||
else:
|
||||
logger.info("✅ Все поля доступны, но таблица пуста")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Поля все еще недоступны: {e}")
|
||||
else:
|
||||
logger.error("❌ Не удалось добавить все необходимые поля")
|
||||
|
||||
await db.close()
|
||||
logger.info("🎉 Экстренное исправление завершено!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"💥 Критическая ошибка: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(emergency_fix())
|
||||
EOF
|
||||
|
||||
# Копируем скрипт в контейнер и запускаем
|
||||
log "Копирование скрипта в контейнер бота..."
|
||||
if docker compose exec bot test -d /app; then
|
||||
# Копируем скрипт в контейнер
|
||||
docker compose cp "$EMERGENCY_SCRIPT" bot:/app/emergency_fix.py
|
||||
|
||||
log "Запуск экстренного исправления в контейнере бота..."
|
||||
if docker compose exec bot python emergency_fix.py; then
|
||||
log "✅ Экстренное исправление выполнено успешно!"
|
||||
|
||||
# Перезапускаем бота для применения изменений
|
||||
log "Перезапуск бота для применения изменений..."
|
||||
docker compose restart bot
|
||||
log "✅ Бот перезапущен"
|
||||
else
|
||||
error "❌ Ошибка при выполнении экстренного исправления"
|
||||
echo "Проверьте логи бота: docker compose logs bot"
|
||||
fi
|
||||
|
||||
# Удаляем временный скрипт из контейнера
|
||||
docker compose exec bot rm -f /app/emergency_fix.py 2>/dev/null || true
|
||||
else
|
||||
error "❌ Не удается найти директорию /app в контейнере бота"
|
||||
echo "Проверьте, что контейнер бота запущен правильно"
|
||||
fi
|
||||
|
||||
# Удаляем временный скрипт с хоста
|
||||
rm -f "$EMERGENCY_SCRIPT"
|
||||
}
|
||||
|
||||
# Альтернативный метод экстренного исправления через SQL
|
||||
emergency_fix_database_sql() {
|
||||
log "Экстренное исправление базы данных через SQL..."
|
||||
|
||||
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 "Проверка существования полей в таблице user_subscriptions..."
|
||||
|
||||
# Проверяем auto_pay_enabled
|
||||
if docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "SELECT auto_pay_enabled FROM user_subscriptions LIMIT 1" &>/dev/null; then
|
||||
log "✅ Поле auto_pay_enabled уже существует"
|
||||
else
|
||||
log "➕ Добавление поля auto_pay_enabled..."
|
||||
if docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "ALTER TABLE user_subscriptions ADD COLUMN auto_pay_enabled BOOLEAN DEFAULT FALSE" &>/dev/null; then
|
||||
log "✅ Поле auto_pay_enabled добавлено"
|
||||
else
|
||||
error "❌ Ошибка при добавлении поля auto_pay_enabled"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Проверяем auto_pay_days_before
|
||||
if docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "SELECT auto_pay_days_before FROM user_subscriptions LIMIT 1" &>/dev/null; then
|
||||
log "✅ Поле auto_pay_days_before уже существует"
|
||||
else
|
||||
log "➕ Добавление поля auto_pay_days_before..."
|
||||
if docker compose exec postgres psql -U remnawave_user -d remnawave_bot -c "ALTER TABLE user_subscriptions ADD COLUMN auto_pay_days_before INTEGER DEFAULT 3" &>/dev/null; then
|
||||
log "✅ Поле auto_pay_days_before добавлено"
|
||||
else
|
||||
error "❌ Ошибка при добавлении поля auto_pay_days_before"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
log "✅ Экстренное исправление через SQL завершено!"
|
||||
|
||||
# Перезапускаем бота если он запущен
|
||||
if docker compose ps bot | grep -q "Up"; then
|
||||
log "Перезапуск бота для применения изменений..."
|
||||
docker compose restart bot
|
||||
log "✅ Бот перезапущен"
|
||||
fi
|
||||
}
|
||||
|
||||
# Редактирование .env файла
|
||||
edit_env_file() {
|
||||
ensure_nano
|
||||
|
||||
@@ -876,8 +1193,10 @@ show_menu() {
|
||||
echo "7) Восстановить базу данных"
|
||||
echo "8) Редактировать .env файл"
|
||||
echo "9) Диагностика базы данных"
|
||||
echo "10) Удалить базу данных"
|
||||
echo "11) Полностью удалить бота"
|
||||
echo "10) Экстренное исправление БД (Python)"
|
||||
echo "11) Экстренное исправление БД (SQL)"
|
||||
echo "12) Удалить базу данных"
|
||||
echo "13) Полностью удалить бота"
|
||||
echo "0) Выход"
|
||||
|
||||
read -p "Выберите действие: " choice
|
||||
@@ -891,8 +1210,10 @@ show_menu() {
|
||||
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; ;;
|
||||
10) emergency_fix_database; read -p "Нажмите Enter для продолжения..."; ;;
|
||||
11) emergency_fix_database_sql; read -p "Нажмите Enter для продолжения..."; ;;
|
||||
12) remove_database; read -p "Нажмите Enter для продолжения..."; ;;
|
||||
13) remove_bot; ;;
|
||||
0) exit 0; ;;
|
||||
*) error "Неверный выбор"; read -p "Нажмите Enter для продолжения..."; ;;
|
||||
esac
|
||||
@@ -905,8 +1226,10 @@ show_menu() {
|
||||
echo "5) Восстановить базу данных"
|
||||
echo "6) Редактировать .env файл"
|
||||
echo "7) Диагностика базы данных"
|
||||
echo "8) Удалить базу данных"
|
||||
echo "9) Полностью удалить бота"
|
||||
echo "8) Экстренное исправление БД (Python)"
|
||||
echo "9) Экстренное исправление БД (SQL)"
|
||||
echo "10) Удалить базу данных"
|
||||
echo "11) Полностью удалить бота"
|
||||
echo "0) Выход"
|
||||
|
||||
read -p "Выберите действие: " choice
|
||||
@@ -918,8 +1241,10 @@ show_menu() {
|
||||
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; ;;
|
||||
8) emergency_fix_database; read -p "Нажмите Enter для продолжения..."; ;;
|
||||
9) emergency_fix_database_sql; read -p "Нажмите Enter для продолжения..."; ;;
|
||||
10) remove_database; read -p "Нажмите Enter для продолжения..."; ;;
|
||||
11) remove_bot; ;;
|
||||
0) exit 0; ;;
|
||||
*) error "Неверный выбор"; read -p "Нажмите Enter для продолжения..."; ;;
|
||||
esac
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Экстренное исправление проблемы с отображением подписок
|
||||
Этот патч добавляет недостающие поля в таблицу user_subscriptions
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Добавляем корневую директорию в путь
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from config import load_config
|
||||
from database import Database
|
||||
from sqlalchemy import text
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def check_and_add_column(db, column_name, column_definition):
|
||||
"""Проверяет и добавляет колонку в таблицу"""
|
||||
try:
|
||||
# Отдельная транзакция для проверки
|
||||
async with db.engine.begin() as conn:
|
||||
await conn.execute(text(f"SELECT {column_name} FROM user_subscriptions LIMIT 1"))
|
||||
logger.info(f"✅ Поле {column_name} уже существует")
|
||||
return True
|
||||
except Exception:
|
||||
# Отдельная транзакция для добавления колонки
|
||||
try:
|
||||
async with db.engine.begin() as conn:
|
||||
logger.info(f"➕ Добавляю поле {column_name}...")
|
||||
await conn.execute(text(f"""
|
||||
ALTER TABLE user_subscriptions
|
||||
ADD COLUMN {column_name} {column_definition}
|
||||
"""))
|
||||
logger.info(f"✅ Поле {column_name} добавлено")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при добавлении {column_name}: {e}")
|
||||
return False
|
||||
|
||||
async def emergency_fix():
|
||||
"""Экстренное исправление базы данных"""
|
||||
|
||||
try:
|
||||
# Загружаем конфигурацию
|
||||
config = load_config()
|
||||
|
||||
# Подключаемся к базе данных
|
||||
db = Database(config.DATABASE_URL)
|
||||
|
||||
logger.info("🔧 Выполняю экстренное исправление базы данных...")
|
||||
|
||||
# Проверяем существование таблицы user_subscriptions
|
||||
try:
|
||||
async with db.engine.begin() as conn:
|
||||
result = await conn.execute(text("SELECT COUNT(*) FROM user_subscriptions"))
|
||||
count = result.scalar()
|
||||
logger.info(f"📊 Найдено {count} подписок в таблице user_subscriptions")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Таблица user_subscriptions не найдена: {e}")
|
||||
await db.close()
|
||||
return
|
||||
|
||||
# Добавляем поля по одному в отдельных транзакциях
|
||||
success1 = await check_and_add_column(db, "auto_pay_enabled", "BOOLEAN DEFAULT FALSE")
|
||||
success2 = await check_and_add_column(db, "auto_pay_days_before", "INTEGER DEFAULT 3")
|
||||
|
||||
# Финальная проверка в отдельной транзакции
|
||||
if success1 and success2:
|
||||
try:
|
||||
async with db.engine.begin() as conn:
|
||||
result = await conn.execute(text("""
|
||||
SELECT id, auto_pay_enabled, auto_pay_days_before
|
||||
FROM user_subscriptions LIMIT 1
|
||||
"""))
|
||||
row = result.fetchone()
|
||||
if row:
|
||||
logger.info("✅ Все поля доступны для чтения")
|
||||
logger.info(f"🔍 Пример записи: id={row[0]}, auto_pay_enabled={row[1]}, auto_pay_days_before={row[2]}")
|
||||
else:
|
||||
logger.info("✅ Все поля доступны, но таблица пуста")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Поля все еще недоступны: {e}")
|
||||
else:
|
||||
logger.error("❌ Не удалось добавить все необходимые поля")
|
||||
|
||||
await db.close()
|
||||
logger.info("🎉 Экстренное исправление завершено!")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"💥 Критическая ошибка: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(emergency_fix())
|
||||
@@ -939,6 +939,20 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
|
||||
|
||||
text = format_user_subscription_info(user_sub.__dict__, sub_dict, user_sub.expires_at, user.language)
|
||||
|
||||
if not is_trial and not is_imported and subscription.price > 0:
|
||||
text += f"\n\n🔄 **Автоплатеж:**\n"
|
||||
if user_sub.auto_pay_enabled:
|
||||
text += f"✅ Включен (за {user_sub.auto_pay_days_before} дн. до истечения)\n"
|
||||
text += f"💰 Стоимость продления: {subscription.price} руб.\n"
|
||||
text += f"💳 Ваш баланс: {user.balance} руб."
|
||||
|
||||
if user.balance < subscription.price:
|
||||
needed = subscription.price - user.balance
|
||||
text += f"\n⚠️ Недостаточно средств! Нужно еще {needed} руб."
|
||||
else:
|
||||
text += f"❌ Отключен\n"
|
||||
text += f"💡 Включите для автоматического продления"
|
||||
|
||||
if user_sub.short_uuid and api:
|
||||
try:
|
||||
subscription_url = await api.get_subscription_url(user_sub.short_uuid)
|
||||
@@ -958,7 +972,14 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=user_subscription_detail_keyboard(user_sub_id, user.language, show_extend, is_imported),
|
||||
reply_markup=user_subscription_detail_keyboard(
|
||||
user_sub_id,
|
||||
user.language,
|
||||
show_extend,
|
||||
is_imported,
|
||||
is_trial,
|
||||
user_sub.auto_pay_enabled
|
||||
),
|
||||
parse_mode='HTML',
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
@@ -1774,3 +1795,226 @@ async def cancel_promocode_callback(callback: CallbackQuery, state: FSMContext,
|
||||
t('main_menu', user.language),
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin, show_trial, show_lucky_game)
|
||||
)
|
||||
|
||||
@router.callback_query(F.data.startswith("toggle_autopay_"))
|
||||
async def toggle_autopay_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
user_sub_id = int(callback.data.split("_")[2])
|
||||
|
||||
user_subs = await db.get_user_subscriptions(user.telegram_id)
|
||||
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
|
||||
|
||||
if not user_sub:
|
||||
await callback.answer("❌ Подписка не найдена")
|
||||
return
|
||||
|
||||
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription:
|
||||
await callback.answer("❌ План подписки не найден")
|
||||
return
|
||||
|
||||
if subscription.is_trial:
|
||||
await callback.answer("❌ Автоплатеж недоступен для тестовых подписок")
|
||||
return
|
||||
|
||||
if subscription.is_imported or subscription.price == 0:
|
||||
await callback.answer("❌ Автоплатеж недоступен для импортированных подписок")
|
||||
return
|
||||
|
||||
new_state = not user_sub.auto_pay_enabled
|
||||
success = await db.toggle_autopay(user_sub_id, new_state)
|
||||
|
||||
if success:
|
||||
status_text = "включен" if new_state else "отключен"
|
||||
await callback.answer(f"✅ Автоплатеж {status_text}")
|
||||
|
||||
updated_subs = await db.get_user_subscriptions(user.telegram_id)
|
||||
updated_sub = next((sub for sub in updated_subs if sub.id == user_sub_id), None)
|
||||
|
||||
if updated_sub:
|
||||
await autopay_settings_callback(callback, db, user=user)
|
||||
else:
|
||||
await callback.answer("❌ Ошибка изменения настроек")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error toggling autopay: {e}")
|
||||
await callback.answer("❌ Ошибка операции")
|
||||
|
||||
@router.callback_query(F.data == "autopay_help")
|
||||
async def autopay_help_callback(callback: CallbackQuery, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
text = "🔄 **Автоплатеж - как это работает?**\n\n"
|
||||
|
||||
text += "**🎯 Что такое автоплатеж:**\n"
|
||||
text += "Автоматическое продление подписки за несколько дней до истечения срока действия.\n\n"
|
||||
|
||||
text += "**⚙️ Как настроить:**\n"
|
||||
text += "1. Перейдите в 'Мои подписки'\n"
|
||||
text += "2. Выберите нужную подписку\n"
|
||||
text += "3. Нажмите 'Настроить автоплатеж'\n"
|
||||
text += "4. Включите автоплатеж и выберите за сколько дней до истечения продлевать\n\n"
|
||||
|
||||
text += "**💰 Требования:**\n"
|
||||
text += "• На балансе должно быть достаточно средств\n"
|
||||
text += "• Подписка должна быть активной\n"
|
||||
text += "• Автоплатеж работает только с обычными подписками\n\n"
|
||||
|
||||
text += "**📅 Варианты продления:**\n"
|
||||
text += "• За 1 день - для тех, кто следит за балансом\n"
|
||||
text += "• За 3 дня - рекомендуемый вариант\n"
|
||||
text += "• За 5 дней - для подстраховки\n"
|
||||
text += "• За 7 дней - максимальный запас времени\n\n"
|
||||
|
||||
text += "**🔔 Уведомления:**\n"
|
||||
text += "Вы получите уведомление при успешном продлении или если недостаточно средств.\n\n"
|
||||
|
||||
text += "**❓ Проблемы:**\n"
|
||||
text += "Если автоплатеж не сработал, проверьте баланс и обратитесь в поддержку."
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=autopay_help_keyboard(user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
@router.callback_query(F.data == "autopay_insufficient_balance_help")
|
||||
async def autopay_insufficient_balance_help_callback(callback: CallbackQuery, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
text = "⚠️ **Недостаточно средств для автоплатежа**\n\n"
|
||||
|
||||
text += f"💳 **Ваш текущий баланс:** {user.balance}₽\n\n"
|
||||
|
||||
text += "**🔄 Что происходит:**\n"
|
||||
text += "Система пыталась автоматически продлить вашу подписку, но на балансе недостаточно средств.\n\n"
|
||||
|
||||
text += "**✅ Что нужно сделать:**\n"
|
||||
text += "1. Пополните баланс на нужную сумму\n"
|
||||
text += "2. Автоплатеж попробует снова при следующей проверке\n"
|
||||
text += "3. Или продлите подписку вручную\n\n"
|
||||
|
||||
text += "**⏰ Важно:**\n"
|
||||
text += "Пополните баланс до истечения срока подписки, иначе доступ будет приостановлен."
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=autopay_help_keyboard(user.language),
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
@router.callback_query(F.data.startswith("autopay_settings_"))
|
||||
async def autopay_settings_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
user_sub_id = int(callback.data.split("_")[2])
|
||||
|
||||
user_subs = await db.get_user_subscriptions(user.telegram_id)
|
||||
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
|
||||
|
||||
if not user_sub:
|
||||
await callback.answer("❌ Подписка не найдена")
|
||||
return
|
||||
|
||||
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if not subscription:
|
||||
await callback.answer("❌ План подписки не найден")
|
||||
return
|
||||
|
||||
if subscription.is_trial:
|
||||
await callback.answer("❌ Автоплатеж недоступен для тестовых подписок")
|
||||
return
|
||||
|
||||
if subscription.is_imported or subscription.price == 0:
|
||||
await callback.answer("❌ Автоплатеж недоступен для импортированных подписок")
|
||||
return
|
||||
|
||||
from datetime import datetime
|
||||
now = datetime.utcnow()
|
||||
days_left = (user_sub.expires_at - now).days
|
||||
|
||||
text = f"⚙️ Настройки автоплатежа\n\n"
|
||||
text += f"📋 Подписка: {subscription.name}\n"
|
||||
text += f"💰 Цена продления: {subscription.price} руб.\n"
|
||||
text += f"⏱ Длительность: {subscription.duration_days} дн.\n"
|
||||
text += f"📅 Истекает через: {days_left} дн.\n\n"
|
||||
|
||||
status_emoji = "✅" if user_sub.auto_pay_enabled else "❌"
|
||||
status_text = "Включен" if user_sub.auto_pay_enabled else "Отключен"
|
||||
text += f"🔄 Автоплатеж: {status_emoji} {status_text}\n"
|
||||
|
||||
if user_sub.auto_pay_enabled:
|
||||
text += f"📅 Продлять за: {user_sub.auto_pay_days_before} дн. до истечения\n\n"
|
||||
|
||||
if user.balance >= subscription.price:
|
||||
text += f"✅ На балансе достаточно средств ({user.balance}₽)\n"
|
||||
text += f"💡 Подписка будет автоматически продлена"
|
||||
else:
|
||||
needed = subscription.price - user.balance
|
||||
text += f"⚠️ Недостаточно средств на балансе!\n"
|
||||
text += f"💳 Ваш баланс: {user.balance}₽\n"
|
||||
text += f"💸 Нужно еще: {needed}₽\n"
|
||||
text += f"💡 Пополните баланс для автоматического продления"
|
||||
else:
|
||||
text += f"\n💡 Включите автоплатеж для автоматического продления подписки\n"
|
||||
text += f"🎯 Рекомендуем включить за 3 дня до истечения"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=autopay_settings_keyboard(user_sub_id, user_sub, user.language)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing autopay settings: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки настроек")
|
||||
|
||||
@router.callback_query(F.data.startswith("autopay_days_"))
|
||||
async def autopay_days_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
parts = callback.data.split("_")
|
||||
user_sub_id = int(parts[2])
|
||||
days = int(parts[3])
|
||||
|
||||
user_subs = await db.get_user_subscriptions(user.telegram_id)
|
||||
user_sub = next((sub for sub in user_subs if sub.id == user_sub_id), None)
|
||||
|
||||
if not user_sub:
|
||||
await callback.answer("❌ Подписка не найдена")
|
||||
return
|
||||
|
||||
success = await db.set_autopay_days(user_sub_id, days)
|
||||
|
||||
if success:
|
||||
await callback.answer(f"✅ Установлено: продлять за {days} дн.")
|
||||
|
||||
updated_subs = await db.get_user_subscriptions(user.telegram_id)
|
||||
updated_sub = next((sub for sub in updated_subs if sub.id == user_sub_id), None)
|
||||
|
||||
if updated_sub:
|
||||
await autopay_settings_callback(callback, db, user=user)
|
||||
else:
|
||||
await callback.answer("❌ Ошибка обновления настроек")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting autopay days: {e}")
|
||||
await callback.answer("❌ Ошибка операции")
|
||||
|
||||
@@ -2,6 +2,7 @@ from database import Subscription
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from typing import List, Optional, Dict
|
||||
from translations import t
|
||||
from datetime import datetime
|
||||
|
||||
def language_keyboard() -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
@@ -110,22 +111,52 @@ def user_subscriptions_keyboard(user_subscriptions: List[dict], lang: str = 'ru'
|
||||
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")])
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def user_subscription_detail_keyboard(subscription_id: int, lang: str = 'ru', show_extend: bool = False, is_imported: bool = False) -> InlineKeyboardMarkup:
|
||||
def user_subscription_detail_keyboard(subscription_id: int, lang: str = 'ru',
|
||||
show_extend: bool = False, is_imported: bool = False,
|
||||
is_trial: bool = False, autopay_enabled: bool = False) -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
if is_imported:
|
||||
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
|
||||
buttons.append([InlineKeyboardButton(text="🛒 Купить новую подписку", callback_data="buy_subscription")])
|
||||
elif is_trial:
|
||||
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
|
||||
buttons.append([InlineKeyboardButton(text="🛒 Купить полную подписку", callback_data="buy_subscription")])
|
||||
else:
|
||||
if show_extend:
|
||||
buttons.append([InlineKeyboardButton(text="⏰ " + t('extend_subscription', lang), callback_data=f"extend_sub_{subscription_id}")])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
|
||||
|
||||
if autopay_enabled:
|
||||
autopay_text = "🔄✅ Настроить автоплатеж"
|
||||
else:
|
||||
autopay_text = "🔄❌ Настроить автоплатеж"
|
||||
|
||||
buttons.append([InlineKeyboardButton(text=autopay_text, callback_data=f"autopay_settings_{subscription_id}")])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="my_subscriptions")])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def autopay_confirmation_keyboard(subscription_id: int, action: str, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_autopay_{action}_{subscription_id}"),
|
||||
InlineKeyboardButton(text="❌ Нет", callback_data=f"autopay_settings_{subscription_id}")
|
||||
]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def autopay_help_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Клавиатура помощи по автоплатежам"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💰 Пополнить баланс", callback_data="topup_balance")],
|
||||
[InlineKeyboardButton(text="📋 Мои подписки", callback_data="my_subscriptions")],
|
||||
[InlineKeyboardButton(text="🏠 Главное меню", callback_data="main_menu")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def extend_subscription_keyboard(subscription_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
@@ -160,14 +191,17 @@ def admin_menu_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📨 " + t('send_message', lang), callback_data="admin_messages"),
|
||||
InlineKeyboardButton(text="👥 Рефералы", callback_data="admin_referrals") # НОВАЯ КНОПКА
|
||||
InlineKeyboardButton(text="👥 Рефералы", callback_data="admin_referrals")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📜 Правила сервиса", callback_data="admin_rules"), # НОВАЯ КНОПКА
|
||||
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system")
|
||||
InlineKeyboardButton(text="📜 Правила сервиса", callback_data="admin_rules"),
|
||||
InlineKeyboardButton(text="🔄 Автоплатежи", callback_data="admin_autopay")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system"),
|
||||
InlineKeyboardButton(text="🔍 Мониторинг подписок", callback_data="admin_monitor")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🔍 Мониторинг подписок", callback_data="admin_monitor"),
|
||||
InlineKeyboardButton(text="📊 " + t('statistics', lang), callback_data="admin_stats")
|
||||
],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="main_menu")]
|
||||
@@ -185,11 +219,84 @@ def admin_subscriptions_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
def admin_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="👥 Список пользователей", callback_data="list_users")],
|
||||
[InlineKeyboardButton(text="📋 Все подписки пользователей", callback_data="admin_user_subscriptions_all")],
|
||||
[InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="search_user")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def admin_users_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="👥 Список пользователей", callback_data="list_users")],
|
||||
[InlineKeyboardButton(text="📋 Все подписки пользователей", callback_data="admin_user_subscriptions_all")],
|
||||
[InlineKeyboardButton(text="🔍 Поиск пользователя", callback_data="search_user")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def admin_user_subscriptions_filters_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Клавиатура фильтров для подписок пользователей"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="🟢 Активные", callback_data="filter_subs_active"),
|
||||
InlineKeyboardButton(text="🔴 Истекшие", callback_data="filter_subs_expired")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🔄✅ С автоплатежом", callback_data="filter_subs_autopay"),
|
||||
InlineKeyboardButton(text="⏰ Истекают скоро", callback_data="filter_subs_expiring")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🆓 Триальные", callback_data="filter_subs_trial"),
|
||||
InlineKeyboardButton(text="📦 Импортированные", callback_data="filter_subs_imported")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="📋 Все подписки", callback_data="admin_user_subscriptions_all"),
|
||||
InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")
|
||||
]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def admin_user_subscription_detail_keyboard(subscription_id: int, user_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Клавиатура для детального просмотра подписки пользователя"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(text="✏️ Редактировать", callback_data=f"edit_user_sub_{subscription_id}"),
|
||||
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_user_sub_{subscription_id}")
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="👤 К пользователю", callback_data=f"admin_user_detail_{user_id}"),
|
||||
InlineKeyboardButton(text="📋 К списку подписок", callback_data="admin_user_subscriptions_all")
|
||||
],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def user_subscriptions_pagination_keyboard(current_page: int, total_pages: int,
|
||||
filter_type: str = "all", lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
if total_pages > 1:
|
||||
nav_row = []
|
||||
|
||||
if current_page > 0:
|
||||
nav_row.append(InlineKeyboardButton(text="⬅️", callback_data=f"user_subs_page_{current_page - 1}_{filter_type}"))
|
||||
|
||||
nav_row.append(InlineKeyboardButton(text=f"{current_page + 1}/{total_pages}", callback_data="noop"))
|
||||
|
||||
if current_page < total_pages - 1:
|
||||
nav_row.append(InlineKeyboardButton(text="➡️", callback_data=f"user_subs_page_{current_page + 1}_{filter_type}"))
|
||||
|
||||
buttons.append(nav_row)
|
||||
|
||||
buttons.append([
|
||||
InlineKeyboardButton(text="🔍 Фильтры", callback_data="admin_user_subscriptions_filters"),
|
||||
InlineKeyboardButton(text="🔄 Обновить", callback_data=f"refresh_user_subs_{filter_type}")
|
||||
])
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 Назад", callback_data="admin_users")])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def admin_balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💸 Пополнить баланс пользователю", callback_data="admin_add_balance")],
|
||||
@@ -638,7 +745,6 @@ def stars_topup_keyboard(stars_rates: Dict[int, float], lang: str = 'ru') -> Inl
|
||||
))
|
||||
buttons.append(row)
|
||||
|
||||
# Добавляем кнопку назад
|
||||
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="topup_balance")])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
@@ -719,3 +825,106 @@ def admin_rule_delete_confirm_keyboard(rule_id: int, lang: str = 'ru') -> Inline
|
||||
]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def autopay_settings_keyboard(user_sub_id: int, user_sub, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
if user_sub.auto_pay_enabled:
|
||||
toggle_text = "❌ Отключить автоплатеж"
|
||||
toggle_callback = f"toggle_autopay_{user_sub_id}"
|
||||
else:
|
||||
toggle_text = "✅ Включить автоплатеж"
|
||||
toggle_callback = f"toggle_autopay_{user_sub_id}"
|
||||
|
||||
buttons.append([InlineKeyboardButton(text=toggle_text, callback_data=toggle_callback)])
|
||||
|
||||
if user_sub.auto_pay_enabled:
|
||||
buttons.append([InlineKeyboardButton(text="📅 Настроить дни до продления", callback_data="noop")])
|
||||
|
||||
days_row = []
|
||||
for days in [1, 3, 5, 7]:
|
||||
emoji = "🔹" if user_sub.auto_pay_days_before == days else "⚪"
|
||||
days_row.append(InlineKeyboardButton(
|
||||
text=f"{emoji} {days}д",
|
||||
callback_data=f"autopay_days_{user_sub_id}_{days}"
|
||||
))
|
||||
buttons.append(days_row)
|
||||
|
||||
buttons.append([InlineKeyboardButton(text="🔙 К подписке", callback_data=f"view_sub_{user_sub_id}")])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def autopay_status_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_status")],
|
||||
[InlineKeyboardButton(text="🚀 Принудительная проверка", callback_data="autopay_force_check")],
|
||||
[InlineKeyboardButton(text="📋 Список подписок", callback_data="autopay_subscriptions_list")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def autopay_subscriptions_keyboard(subscriptions_data: List[Dict], lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
buttons = []
|
||||
|
||||
expired = [s for s in subscriptions_data if s['expires_in_days'] <= 0]
|
||||
due_soon = [s for s in subscriptions_data if 0 < s['expires_in_days'] <= s['auto_pay_days_before']]
|
||||
|
||||
critical_subs = expired + due_soon
|
||||
|
||||
for sub_data in critical_subs[:8]:
|
||||
username = sub_data['username'] if sub_data['username'] != 'N/A' else f"ID:{sub_data['user_id']}"
|
||||
days = sub_data['expires_in_days']
|
||||
|
||||
if days <= 0:
|
||||
status_emoji = "❌"
|
||||
status_text = f"Истекла"
|
||||
elif days <= sub_data['auto_pay_days_before']:
|
||||
status_emoji = "⚠️"
|
||||
status_text = f"Через {days}д"
|
||||
else:
|
||||
status_emoji = "✅"
|
||||
status_text = f"Через {days}д"
|
||||
|
||||
button_text = f"{status_emoji} @{username} ({status_text})"
|
||||
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=button_text,
|
||||
callback_data=f"autopay_user_detail_{sub_data['user_id']}"
|
||||
)
|
||||
])
|
||||
|
||||
if len(subscriptions_data) > 8:
|
||||
buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=f"... и еще {len(subscriptions_data) - 8}",
|
||||
callback_data="noop"
|
||||
)
|
||||
])
|
||||
|
||||
buttons.extend([
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_subscriptions_list")],
|
||||
[InlineKeyboardButton(text="📊 Статистика", callback_data="autopay_statistics")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
|
||||
])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def autopay_user_detail_keyboard(user_id: int, lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💳 Добавить баланс", callback_data=f"admin_add_balance_to_{user_id}")],
|
||||
[InlineKeyboardButton(text="📋 Управление подписками", callback_data=f"admin_user_subscriptions_{user_id}")],
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data=f"autopay_user_detail_{user_id}")],
|
||||
[InlineKeyboardButton(text="🔙 К списку", callback_data="autopay_subscriptions_list")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
|
||||
def autopay_statistics_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="⚠️ Недостаточно средств", callback_data="autopay_insufficient_balance_users")],
|
||||
[InlineKeyboardButton(text="📋 Список подписок", callback_data="autopay_subscriptions_list")],
|
||||
[InlineKeyboardButton(text="🔄 Обновить", callback_data="autopay_statistics")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_autopay")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
@@ -8,6 +8,7 @@ from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.enums import ParseMode
|
||||
from lucky_game import lucky_game_router
|
||||
from stars_handlers import stars_router
|
||||
from autopay_service import AutoPayService
|
||||
|
||||
print("🚀 Запуск бота...")
|
||||
print(f"📍 Рабочая директория: {os.getcwd()}")
|
||||
@@ -46,6 +47,40 @@ class BotApplication:
|
||||
self.bot = None
|
||||
self.dp = None
|
||||
self.monitor_service = None
|
||||
self.autopay_service = None
|
||||
|
||||
async def _init_autopay_service(self):
|
||||
"""Инициализирует сервис автоплатежей"""
|
||||
try:
|
||||
logger.info("🔧 Initializing autopay service...")
|
||||
|
||||
if not self.bot:
|
||||
logger.error("❌ Bot instance is None, cannot initialize autopay")
|
||||
return
|
||||
|
||||
if not self.db:
|
||||
logger.error("❌ Database instance is None, cannot initialize autopay")
|
||||
return
|
||||
|
||||
self.autopay_service = AutoPayService(self.db, self.api, self.bot)
|
||||
|
||||
self.dp.workflow_data["autopay_service"] = self.autopay_service
|
||||
logger.info("✅ Autopay service added to workflow_data")
|
||||
|
||||
logger.info("🚀 Starting autopay service...")
|
||||
await self.autopay_service.start()
|
||||
|
||||
status = await self.autopay_service.get_service_status()
|
||||
if status['is_running']:
|
||||
logger.info("✅ Autopay service started successfully")
|
||||
logger.info(f"📊 Autopay status: interval=30min")
|
||||
else:
|
||||
logger.warning("⚠️ Autopay service created but not running")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Failed to initialize autopay service: {e}", exc_info=True)
|
||||
logger.warning("⚠️ Continuing without autopay service")
|
||||
self.autopay_service = None
|
||||
|
||||
async def initialize(self):
|
||||
|
||||
@@ -97,6 +132,7 @@ class BotApplication:
|
||||
self._setup_dispatcher()
|
||||
|
||||
await self._init_monitor_service()
|
||||
await self._init_autopay_service()
|
||||
|
||||
if self.config.STARS_ENABLED:
|
||||
logger.info("✅ Telegram Stars пополнение включено")
|
||||
@@ -249,6 +285,13 @@ class BotApplication:
|
||||
|
||||
async def shutdown(self):
|
||||
logger.info("Shutting down bot...")
|
||||
|
||||
if self.autopay_service:
|
||||
try:
|
||||
await self.autopay_service.stop()
|
||||
logger.info("Autopay service stopped")
|
||||
except Exception as e:
|
||||
logger.error(f"Error stopping autopay service: {e}")
|
||||
|
||||
if self.monitor_service:
|
||||
try:
|
||||
|
||||
|
Before Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 120 KiB |
@@ -0,0 +1 @@
|
||||
|
||||
|
After Width: | Height: | Size: 144 KiB |
@@ -97,6 +97,48 @@ TRANSLATIONS = {
|
||||
'manage_balance': 'Управление балансом',
|
||||
'manage_promocodes': 'Управление промокодами',
|
||||
'statistics': 'Статистика',
|
||||
'admin_user_subscriptions': 'Подписки пользователей',
|
||||
'all_user_subscriptions': 'Все подписки пользователей',
|
||||
'user_subscription_filters': 'Фильтры подписок',
|
||||
'active_subscriptions': 'Активные подписки',
|
||||
'expired_subscriptions': 'Истекшие подписки',
|
||||
'expiring_subscriptions': 'Истекающие подписки',
|
||||
'autopay_subscriptions': 'Подписки с автоплатежом',
|
||||
'trial_subscriptions': 'Триальные подписки',
|
||||
'imported_subscriptions': 'Импортированные подписки',
|
||||
'subscription_details': 'Детали подписки',
|
||||
'user_info': 'Информация о пользователе',
|
||||
'subscription_info': 'Информация о подписке',
|
||||
'subscription_status': 'Статус подписки',
|
||||
'subscription_dates': 'Временные рамки',
|
||||
'autopay_settings': 'Настройки автоплатежа',
|
||||
'subscription_type': 'Тип подписки',
|
||||
'edit_subscription': 'Редактировать подписку',
|
||||
'toggle_status': 'Изменить статус',
|
||||
'edit_expiry': 'Изменить срок',
|
||||
'edit_autopay': 'Настроить автоплатеж',
|
||||
'subscription_activated': 'Подписка активирована',
|
||||
'subscription_deactivated': 'Подписка деактивирована',
|
||||
'insufficient_funds': 'Недостаточно средств',
|
||||
'funds_needed': 'Нужно еще',
|
||||
'created_date': 'Дата создания',
|
||||
'expires_date': 'Дата истечения',
|
||||
'updated_date': 'Дата обновления',
|
||||
'days_left': 'Дней осталось',
|
||||
'autopay_enabled': 'Автоплатеж включен',
|
||||
'autopay_disabled': 'Автоплатеж отключен',
|
||||
'renew_before_days': 'Продлять за дней',
|
||||
'view_details': 'Просмотр деталей',
|
||||
'back_to_list': 'К списку',
|
||||
'refresh_data': 'Обновить данные',
|
||||
'no_subscriptions_found': 'Подписки не найдены',
|
||||
'subscription_not_found': 'Подписка не найдена',
|
||||
'database_unavailable': 'База данных недоступна',
|
||||
'error_loading_subscriptions': 'Ошибка загрузки подписок',
|
||||
'error_loading_details': 'Ошибка загрузки деталей',
|
||||
'pagination_error': 'Ошибка навигации',
|
||||
'edit_error': 'Ошибка редактирования',
|
||||
'status_toggle_error': 'Ошибка изменения статуса',
|
||||
'not_admin': '❌ У вас нет прав администратора',
|
||||
'create_subscription': 'Создать подписку',
|
||||
'enter_sub_name': 'Введите название подписки:',
|
||||
@@ -294,7 +336,49 @@ TRANSLATIONS = {
|
||||
'lucky_game_games_played': 'Games played: {count}',
|
||||
'lucky_game_wins': 'Wins: {count}',
|
||||
'lucky_game_total_won': 'Total won: ${amount}',
|
||||
'lucky_game_win_rate': 'Win rate: {rate}%'
|
||||
'lucky_game_win_rate': 'Win rate: {rate}%',
|
||||
'admin_user_subscriptions': 'User Subscriptions',
|
||||
'all_user_subscriptions': 'All User Subscriptions',
|
||||
'user_subscription_filters': 'Subscription Filters',
|
||||
'active_subscriptions': 'Active Subscriptions',
|
||||
'expired_subscriptions': 'Expired Subscriptions',
|
||||
'expiring_subscriptions': 'Expiring Subscriptions',
|
||||
'autopay_subscriptions': 'Autopay Subscriptions',
|
||||
'trial_subscriptions': 'Trial Subscriptions',
|
||||
'imported_subscriptions': 'Imported Subscriptions',
|
||||
'subscription_details': 'Subscription Details',
|
||||
'user_info': 'User Information',
|
||||
'subscription_info': 'Subscription Information',
|
||||
'subscription_status': 'Subscription Status',
|
||||
'subscription_dates': 'Time Frames',
|
||||
'autopay_settings': 'Autopay Settings',
|
||||
'subscription_type': 'Subscription Type',
|
||||
'edit_subscription': 'Edit Subscription',
|
||||
'toggle_status': 'Toggle Status',
|
||||
'edit_expiry': 'Edit Expiry',
|
||||
'edit_autopay': 'Configure Autopay',
|
||||
'subscription_activated': 'Subscription activated',
|
||||
'subscription_deactivated': 'Subscription deactivated',
|
||||
'insufficient_funds': 'Insufficient funds',
|
||||
'funds_needed': 'Need more',
|
||||
'created_date': 'Created Date',
|
||||
'expires_date': 'Expiry Date',
|
||||
'updated_date': 'Updated Date',
|
||||
'days_left': 'Days Left',
|
||||
'autopay_enabled': 'Autopay Enabled',
|
||||
'autopay_disabled': 'Autopay Disabled',
|
||||
'renew_before_days': 'Renew Before Days',
|
||||
'view_details': 'View Details',
|
||||
'back_to_list': 'Back to List',
|
||||
'refresh_data': 'Refresh Data',
|
||||
'no_subscriptions_found': 'No subscriptions found',
|
||||
'subscription_not_found': 'Subscription not found',
|
||||
'database_unavailable': 'Database unavailable',
|
||||
'error_loading_subscriptions': 'Error loading subscriptions',
|
||||
'error_loading_details': 'Error loading details',
|
||||
'pagination_error': 'Navigation error',
|
||||
'edit_error': 'Edit error',
|
||||
'status_toggle_error': 'Status toggle error'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||