Compare commits

...

36 Commits

Author SHA1 Message Date
Egor 526f142dea Update remnawave_api.py 2025-08-12 18:12:34 +03:00
Egor ca431d36e6 Add files via upload 2025-08-12 03:49:13 +03:00
Egor 68a81bdd89 Delete screens/уведомления/Снимок экрана 2025-08-12 в 01.45.14.png 2025-08-12 03:48:59 +03:00
Fr1ngg 68a3d713ef Update README.md 2025-08-12 03:19:42 +03:00
Fr1ngg 50d3d08e38 Update README.md 2025-08-12 03:14:42 +03:00
Fr1ngg 9a2fec4d8f Update README.md 2025-08-12 03:10:34 +03:00
Fr1ngg e68b1d2092 Add files via upload 2025-08-12 02:48:15 +03:00
Fr1ngg 6dfedef81e Update README.md 2025-08-12 02:36:41 +03:00
Fr1ngg 317e39c823 Update keyboards.py 2025-08-12 02:34:19 +03:00
Fr1ngg 0072a3cb71 Update translations.py 2025-08-12 02:34:03 +03:00
Fr1ngg 8d021c1530 Update database.py 2025-08-12 02:33:44 +03:00
Fr1ngg 23606dc2d4 Update admin_handlers.py 2025-08-12 02:33:23 +03:00
Fr1ngg 0838c4196d Add files via upload 2025-08-12 01:47:52 +03:00
Fr1ngg 65605a0de2 Create 1 2025-08-12 01:47:38 +03:00
Fr1ngg a20ac80281 Delete screens/уведомления 2025-08-12 01:47:25 +03:00
Fr1ngg 02e83fb0d0 Create уведомления 2025-08-12 01:47:13 +03:00
Fr1ngg eddad0db6e Add files via upload 2025-08-12 01:46:00 +03:00
Fr1ngg ffad0afb61 Delete screens/Balance/Снимок экрана 2025-08-09 в 07.54.52.png 2025-08-12 01:44:40 +03:00
Fr1ngg 83eed8e1cd Add files via upload 2025-08-12 01:44:17 +03:00
Fr1ngg 0c7c8abade Add files via upload 2025-08-12 01:42:18 +03:00
Fr1ngg a064890ed8 Delete screens/adminpanel/Снимок экрана 2025-08-09 в 08.09.05.png 2025-08-12 01:41:33 +03:00
Fr1ngg f310f4a35e Delete screens/sub/Снимок экрана 2025-08-09 в 08.05.47.png 2025-08-12 01:41:14 +03:00
Fr1ngg 0268e09882 Delete screens/Главное меню/Mainmenu.png 2025-08-12 01:40:23 +03:00
Fr1ngg 449814d0df Add files via upload 2025-08-12 01:40:09 +03:00
Fr1ngg a3d3577103 Update README.md 2025-08-12 01:37:43 +03:00
Fr1ngg a3086fdd2c Update README.md 2025-08-12 01:34:57 +03:00
Fr1ngg daafc6a41e Update README.md 2025-08-12 01:31:42 +03:00
Fr1ngg c27ee116f7 Update README.md 2025-08-12 01:28:56 +03:00
Fr1ngg 5167cde443 Update bedolaga.sh 2025-08-12 01:25:30 +03:00
Fr1ngg 34542d3a9c Update bedolaga.sh 2025-08-12 01:09:56 +03:00
Fr1ngg e73d1df4cc Update logo2.svg 2025-08-12 01:05:39 +03:00
Fr1ngg f9a95c3d48 Update README.md 2025-08-12 01:04:53 +03:00
Fr1ngg 2dd2b5fdbf Update README.md 2025-08-12 01:04:37 +03:00
Fr1ngg 896f1db9d4 Update README.md 2025-08-11 23:09:23 +03:00
Fr1ngg a1bd2d775f Create emergency_fix.py 2025-08-11 22:54:05 +03:00
Fr1ngg 8cbe23f9ed Update database.py 2025-08-11 22:31:17 +03:00
27 changed files with 1548 additions and 62 deletions
+72 -28
View File
@@ -1,13 +1,13 @@
# 🤖 RemnaWave Telegram Bot
# 🤖 Remnawave Telegram Bot
[![Docker Image](https://img.shields.io/badge/Docker-fr1ngg/remnawave--bedolaga--telegram--bot-blue?logo=docker)](https://hub.docker.com/r/fr1ngg/remnawave-bedolaga-telegram-bot)
[![Python](https://img.shields.io/badge/Python-3.11+-blue?logo=python)](https://python.org)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-blue?logo=postgresql)](https://postgresql.org)
[![License](https://img.shields.io/badge/License-MIT-green)](LICENSE)
![RemnaWave Bedolaga Bot Logo](./assets/logo2.svg)
![Remnawave Bedolaga Bot Logo](./assets/logo2.svg)
Современный Telegram бот для управления VPN подписками через RemnaWave API с полным функционалом управления пользователями, платежами и администрированием.
Современный Telegram бот для управления VPN подписками через Remnawave API с полным функционалом управления пользователями, платежами и администрированием.
## 🚀 Особенности
@@ -28,8 +28,9 @@
- 👥 **Управление пользователями** - поиск, редактирование, баланс
- 💳 **Управление платежами** - одобрение, отклонение, история
- 🎫 **Управление промокодами** - создание, редактирование, статистика
- 🖥 **Мониторинг системы** - состояние нод, пользователей RemnaWave
- 🔄 **Синхронизация данных** - импорт из RemnaWave
- ✍️ **Просмотр подписок юзеров**
- 🖥 **Мониторинг системы** - состояние нод, пользователей Remnawave
- 🔄 **Синхронизация данных** - импорт из Remnawave
- 📨 **Рассылки** - уведомления пользователям
- 🔍 **Мониторинг подписок** - автоматические уведомления об истечении и зачистка подписок с истекщим сроком для триал и обычных подписок
- 📋 **Правила сервиса** - Настройка страниц из админ панели
@@ -44,7 +45,7 @@
### Внешние сервисы
- **Telegram Bot Token** - создайте бота через [@BotFather](https://t.me/BotFather)
- **RemnaWave API** - доступ к панели RemnaWave
- **Remnawave API** - доступ к панели Remnawave
- **PostgreSQL** - автоматически разворачивается через Docker
## 🛠 Установка и запуск
@@ -73,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` |
#### 🎁 Реферальная программа
@@ -129,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
@@ -184,6 +187,7 @@ sudo ./bedolaga.sh
- ♻️ Восстановить базу данных
- ✏️ Редактировать .env файл
- 🩺 Диагностика базы данных
- Восстановление бд после обновления 1.3.3 -> 1.3.4
- 🗑️ Удалить базу данных
- ❌ Полностью удалить бота
@@ -195,6 +199,7 @@ sudo ./bedolaga.sh
- ♻️ Восстановить базу данных
- ✏️ Редактировать .env файл
- 🩺 Диагностика базы данных
- Восстановление бд после обновления 1.3.3 -> 1.3.4
- 🗑️ Удалить базы данных
- ❌ Полностью удалить бота
@@ -302,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
@@ -364,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
@@ -582,15 +587,14 @@ docker build -t remnawave-bot .
- **💰 Управление балансом** - пополнение балансов
- **🎁 Управление промокодами** - создание скидок
- **📨 Рассылки** - отправка сообщений
- **🖥 Система RemnaWave** - мониторинг нод и синхронизация
- **🖥 Система Remnawave** - мониторинг нод и синхронизация
## 🔒 Безопасность
### Рекомендации
- ✅ Измените пароли PostgreSQL в `docker-compose.yml`
-Не публикуйте файл `.env` в репозитории
- ✅ Используйте HTTPS для RemnaWave API
-Используйте HTTPS для Remnawave API
- ✅ Регулярно создавайте backup базы данных
- ✅ Ограничьте доступ к серверу через firewall
@@ -610,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` |
### Диагностика
@@ -684,7 +688,8 @@ docker-compose ps
| 2 | Платежные шлюзы | ✅ Done | v1.3.2 | 🔴 High | Звезды |
| 2.1 | Платежные шлюзы | 🎯 Planned | v1.4.0+ | 🔴 High | ЮKassa |
| 2.2 | Платежные шлюзы | 🎯 Planned | v1.4.0+ | 🟡 Medium | Сбербанк, Tinkoff, Crypto, Others |
| 3.4 | Синхронизация RemnaWave | ✅ Done | v1.1.0 | 🟡 Medium | Импорт пользователей по Telegram ID |
| 3 | Синхронизация RemnaWave | ✅ Done | v1.1.0 | 🟡 Medium | Вывод информации о статусе панели remnawave и нод прямо в бота |
| 3 | Синхронизация RemnaWave | ✅ Done | v1.1.0 | 🟡 Medium | Импорт пользователей по Telegram ID + Синхранизация Панель->Бот |
| 4 | Веб-панель управления | 🎯 Planned | v1.4.0+ | 🟡 Medium | Полнофункциональный веб-интерфейс |
| 5 | Управление промокодами | ✅ Done | v1.3.0 | 🟢 Low | Удаление, редактирование |
| 6 | Безопасное удаление подписок | 🚧 In Progress | v1.4.0 | 🟡 Medium | Архивирование вместо удаления |
@@ -694,8 +699,49 @@ 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)**
**⚠️ Важные замечания от автора:**
> 🗑 **Осторожно с удалением подписок!** Сейчас удаление плана подписки скроет эту подписку у всех пользователей, которые ее купили. Используйте деактивацию вместо удаления. Эта проблема будет исправлена в v1.4.0.
---
### 🚀 Хотите помочь?
**Разработчикам:**
@@ -717,14 +763,12 @@ docker-compose ps
---
**⚠️ Важные замечания от автора:**
> 🗑 **Осторожно с удалением подписок!** Сейчас удаление плана подписки скроет эту подписку у всех пользователей, которые ее купили. Используйте деактивацию вместо удаления. Эта проблема будет исправлена в v1.4.0.
---
**⭐ Если проект был полезен, поставьте звездочку на GitHub!**
---
*Создано с ❤️ для RemnaWave сообщества*
### PS
Автор не является проф. разработчиком, да и без вайбкодинга не обошлось, но я прикладываю все усилия, чтобы сделать удобного публичного бота для ваших сервисов)(
*Создано с ❤️ для Remnawave сообщества*
+564
View File
@@ -7739,3 +7739,567 @@ async def autopay_user_detail_callback(callback: CallbackQuery, user: User, **kw
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)
+1 -1
View File
@@ -261,7 +261,7 @@
text-anchor="middle"
fill="url(#mainGradient)"
filter="url(#glow)">
RemnaWave
Remnawave
</text>
<text x="200" y="345"

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+333 -8
View File
@@ -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
+315 -23
View File
@@ -1885,32 +1885,38 @@ class Database:
async with self.engine.begin() as conn:
try:
await conn.execute(text("SELECT auto_pay_enabled FROM user_subscriptions LIMIT 1"))
logger.info("Autopay fields already exist")
return
logger.info("auto_pay_enabled field already exists")
except Exception:
pass
try:
await conn.execute(text("""
ALTER TABLE user_subscriptions
ADD COLUMN auto_pay_enabled BOOLEAN DEFAULT FALSE
"""))
logger.info("Added auto_pay_enabled column")
except Exception as e:
logger.error(f"Error adding auto_pay_enabled column: {e}")
pass
async with self.engine.begin() as conn:
try:
await conn.execute(text("""
ALTER TABLE user_subscriptions
ADD COLUMN auto_pay_enabled BOOLEAN DEFAULT FALSE
"""))
logger.info("Added auto_pay_enabled column")
except Exception as e:
logger.warning(f"Column auto_pay_enabled may already exist: {e}")
try:
await conn.execute(text("""
ALTER TABLE user_subscriptions
ADD COLUMN auto_pay_days_before INTEGER DEFAULT 3
"""))
logger.info("Added auto_pay_days_before column")
except Exception as e:
logger.warning(f"Column auto_pay_days_before may already exist: {e}")
logger.info("Successfully added autopay fields to user_subscriptions table")
await conn.execute(text("SELECT auto_pay_days_before FROM user_subscriptions LIMIT 1"))
logger.info("auto_pay_days_before field already exists")
except Exception:
try:
await conn.execute(text("""
ALTER TABLE user_subscriptions
ADD COLUMN auto_pay_days_before INTEGER DEFAULT 3
"""))
logger.info("Added auto_pay_days_before column")
except Exception as e:
logger.error(f"Error adding auto_pay_days_before column: {e}")
pass
logger.info("Successfully completed autopay fields migration")
except Exception as e:
logger.error(f"Error adding autopay fields: {e}")
logger.error(f"Error during autopay migration: {e}")
async def get_autopay_history(self, limit: int = 50) -> List[Dict[str, Any]]:
async with self.session_factory() as session:
@@ -2143,3 +2149,289 @@ class Database:
except Exception as e:
logger.error(f"Error getting users with insufficient autopay balance: {e}")
return []
async def get_all_user_subscriptions_admin(self, offset: int = 0, limit: int = 20,
filter_type: str = "all") -> tuple[List[Dict], int]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, func, desc, and_, or_
from datetime import datetime, timedelta
base_query = select(
UserSubscription,
Subscription.name.label('subscription_name'),
Subscription.price.label('subscription_price'),
Subscription.is_trial.label('is_trial'),
Subscription.is_imported.label('is_imported'),
User.username.label('user_username'),
User.first_name.label('user_first_name'),
User.telegram_id.label('user_telegram_id')
).select_from(
UserSubscription.__table__.join(
Subscription.__table__, UserSubscription.subscription_id == Subscription.id
).join(
User.__table__, UserSubscription.user_id == User.telegram_id
)
)
current_time = datetime.utcnow()
if filter_type == "active":
base_query = base_query.where(
and_(
UserSubscription.is_active == True,
UserSubscription.expires_at > current_time
)
)
elif filter_type == "expired":
base_query = base_query.where(
or_(
UserSubscription.is_active == False,
UserSubscription.expires_at <= current_time
)
)
elif filter_type == "expiring":
expiring_date = current_time + timedelta(days=7)
base_query = base_query.where(
and_(
UserSubscription.is_active == True,
UserSubscription.expires_at > current_time,
UserSubscription.expires_at <= expiring_date
)
)
elif filter_type == "autopay":
base_query = base_query.where(UserSubscription.auto_pay_enabled == True)
elif filter_type == "trial":
base_query = base_query.where(Subscription.is_trial == True)
elif filter_type == "imported":
base_query = base_query.where(Subscription.is_imported == True)
count_query = select(func.count()).select_from(base_query.subquery())
total_count_result = await session.execute(count_query)
total_count = total_count_result.scalar() or 0
data_query = base_query.order_by(desc(UserSubscription.created_at)).offset(offset).limit(limit)
result = await session.execute(data_query)
subscriptions_data = []
for row in result.fetchall():
user_sub = row[0]
if user_sub.expires_at <= current_time:
status = "expired"
elif not user_sub.is_active:
status = "inactive"
else:
days_left = (user_sub.expires_at - current_time).days
if days_left <= 3:
status = "expiring_soon"
elif days_left <= 7:
status = "expiring"
else:
status = "active"
subscriptions_data.append({
'id': user_sub.id,
'user_id': row.user_telegram_id,
'user_username': row.user_username or 'N/A',
'user_first_name': row.user_first_name or 'N/A',
'subscription_name': row.subscription_name,
'subscription_price': row.subscription_price,
'short_uuid': user_sub.short_uuid,
'expires_at': user_sub.expires_at,
'is_active': user_sub.is_active,
'auto_pay_enabled': user_sub.auto_pay_enabled,
'auto_pay_days_before': user_sub.auto_pay_days_before,
'created_at': user_sub.created_at,
'is_trial': row.is_trial,
'is_imported': row.is_imported,
'status': status,
'days_left': (user_sub.expires_at - current_time).days if user_sub.expires_at > current_time else 0
})
return subscriptions_data, total_count
except Exception as e:
logger.error(f"Error getting admin user subscriptions: {e}")
return [], 0
async def get_user_subscription_detail_admin(self, subscription_id: int) -> Optional[Dict]:
async with self.session_factory() as session:
try:
from sqlalchemy import select
query = select(
UserSubscription,
Subscription.name.label('subscription_name'),
Subscription.description.label('subscription_description'),
Subscription.price.label('subscription_price'),
Subscription.duration_days.label('subscription_duration'),
Subscription.traffic_limit_gb.label('subscription_traffic_limit'),
Subscription.is_trial.label('is_trial'),
Subscription.is_imported.label('is_imported'),
User.username.label('user_username'),
User.first_name.label('user_first_name'),
User.telegram_id.label('user_telegram_id'),
User.balance.label('user_balance')
).select_from(
UserSubscription.__table__.join(
Subscription.__table__, UserSubscription.subscription_id == Subscription.id
).join(
User.__table__, UserSubscription.user_id == User.telegram_id
)
).where(UserSubscription.id == subscription_id)
result = await session.execute(query)
row = result.fetchone()
if not row:
return None
user_sub = row[0]
current_time = datetime.utcnow()
if user_sub.expires_at <= current_time:
status = "expired"
status_emoji = ""
elif not user_sub.is_active:
status = "inactive"
status_emoji = ""
else:
days_left = (user_sub.expires_at - current_time).days
if days_left <= 1:
status = "expiring_today"
status_emoji = "🔴"
elif days_left <= 3:
status = "expiring_soon"
status_emoji = "🟡"
elif days_left <= 7:
status = "expiring"
status_emoji = "🟠"
else:
status = "active"
status_emoji = "🟢"
return {
'id': user_sub.id,
'user_id': row.user_telegram_id,
'user_username': row.user_username or 'N/A',
'user_first_name': row.user_first_name or 'N/A',
'user_balance': row.user_balance,
'subscription_name': row.subscription_name,
'subscription_description': row.subscription_description,
'subscription_price': row.subscription_price,
'subscription_duration': row.subscription_duration,
'subscription_traffic_limit': row.subscription_traffic_limit,
'short_uuid': user_sub.short_uuid,
'expires_at': user_sub.expires_at,
'is_active': user_sub.is_active,
'auto_pay_enabled': user_sub.auto_pay_enabled,
'auto_pay_days_before': user_sub.auto_pay_days_before,
'created_at': user_sub.created_at,
'updated_at': user_sub.updated_at,
'is_trial': row.is_trial,
'is_imported': row.is_imported,
'status': status,
'status_emoji': status_emoji,
'days_left': (user_sub.expires_at - current_time).days if user_sub.expires_at > current_time else 0
}
except Exception as e:
logger.error(f"Error getting user subscription detail: {e}")
return None
async def get_user_subscriptions_stats_admin(self) -> Dict[str, Any]:
async with self.session_factory() as session:
try:
from sqlalchemy import select, func, and_, or_
from datetime import datetime, timedelta
current_time = datetime.utcnow()
total_subs = await session.execute(
select(func.count(UserSubscription.id))
)
total_subscriptions = total_subs.scalar() or 0
active_subs = await session.execute(
select(func.count(UserSubscription.id)).where(
and_(
UserSubscription.is_active == True,
UserSubscription.expires_at > current_time
)
)
)
active_subscriptions = active_subs.scalar() or 0
expired_subs = await session.execute(
select(func.count(UserSubscription.id)).where(
or_(
UserSubscription.is_active == False,
UserSubscription.expires_at <= current_time
)
)
)
expired_subscriptions = expired_subs.scalar() or 0
autopay_subs = await session.execute(
select(func.count(UserSubscription.id)).where(
UserSubscription.auto_pay_enabled == True
)
)
autopay_subscriptions = autopay_subs.scalar() or 0
expiring_date = current_time + timedelta(days=7)
expiring_subs = await session.execute(
select(func.count(UserSubscription.id)).where(
and_(
UserSubscription.is_active == True,
UserSubscription.expires_at > current_time,
UserSubscription.expires_at <= expiring_date
)
)
)
expiring_subscriptions = expiring_subs.scalar() or 0
trial_subs = await session.execute(
select(func.count(UserSubscription.id))
.select_from(
UserSubscription.__table__.join(
Subscription.__table__, UserSubscription.subscription_id == Subscription.id
)
)
.where(Subscription.is_trial == True)
)
trial_subscriptions = trial_subs.scalar() or 0
imported_subs = await session.execute(
select(func.count(UserSubscription.id))
.select_from(
UserSubscription.__table__.join(
Subscription.__table__, UserSubscription.subscription_id == Subscription.id
)
)
.where(Subscription.is_imported == True)
)
imported_subscriptions = imported_subs.scalar() or 0
return {
'total_subscriptions': total_subscriptions,
'active_subscriptions': active_subscriptions,
'expired_subscriptions': expired_subscriptions,
'autopay_subscriptions': autopay_subscriptions,
'expiring_subscriptions': expiring_subscriptions,
'trial_subscriptions': trial_subscriptions,
'imported_subscriptions': imported_subscriptions
}
except Exception as e:
logger.error(f"Error getting user subscriptions stats: {e}")
return {
'total_subscriptions': 0,
'active_subscriptions': 0,
'expired_subscriptions': 0,
'autopay_subscriptions': 0,
'expiring_subscriptions': 0,
'trial_subscriptions': 0,
'imported_subscriptions': 0
}
+100
View File
@@ -0,0 +1,100 @@
"""
Экстренное исправление проблемы с отображением подписок
Этот патч добавляет недостающие поля в таблицу user_subscriptions
"""
import asyncio
import sys
import os
from pathlib import Path
# Добавляем корневую директорию в путь
sys.path.insert(0, str(Path(__file__).parent))
from config import load_config
from database import Database
from sqlalchemy import text
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def check_and_add_column(db, column_name, column_definition):
"""Проверяет и добавляет колонку в таблицу"""
try:
# Отдельная транзакция для проверки
async with db.engine.begin() as conn:
await conn.execute(text(f"SELECT {column_name} FROM user_subscriptions LIMIT 1"))
logger.info(f"✅ Поле {column_name} уже существует")
return True
except Exception:
# Отдельная транзакция для добавления колонки
try:
async with db.engine.begin() as conn:
logger.info(f" Добавляю поле {column_name}...")
await conn.execute(text(f"""
ALTER TABLE user_subscriptions
ADD COLUMN {column_name} {column_definition}
"""))
logger.info(f"✅ Поле {column_name} добавлено")
return True
except Exception as e:
logger.error(f"❌ Ошибка при добавлении {column_name}: {e}")
return False
async def emergency_fix():
"""Экстренное исправление базы данных"""
try:
# Загружаем конфигурацию
config = load_config()
# Подключаемся к базе данных
db = Database(config.DATABASE_URL)
logger.info("🔧 Выполняю экстренное исправление базы данных...")
# Проверяем существование таблицы user_subscriptions
try:
async with db.engine.begin() as conn:
result = await conn.execute(text("SELECT COUNT(*) FROM user_subscriptions"))
count = result.scalar()
logger.info(f"📊 Найдено {count} подписок в таблице user_subscriptions")
except Exception as e:
logger.error(f"❌ Таблица user_subscriptions не найдена: {e}")
await db.close()
return
# Добавляем поля по одному в отдельных транзакциях
success1 = await check_and_add_column(db, "auto_pay_enabled", "BOOLEAN DEFAULT FALSE")
success2 = await check_and_add_column(db, "auto_pay_days_before", "INTEGER DEFAULT 3")
# Финальная проверка в отдельной транзакции
if success1 and success2:
try:
async with db.engine.begin() as conn:
result = await conn.execute(text("""
SELECT id, auto_pay_enabled, auto_pay_days_before
FROM user_subscriptions LIMIT 1
"""))
row = result.fetchone()
if row:
logger.info("✅ Все поля доступны для чтения")
logger.info(f"🔍 Пример записи: id={row[0]}, auto_pay_enabled={row[1]}, auto_pay_days_before={row[2]}")
else:
logger.info("✅ Все поля доступны, но таблица пуста")
except Exception as e:
logger.error(f"❌ Поля все еще недоступны: {e}")
else:
logger.error("❌ Не удалось добавить все необходимые поля")
await db.close()
logger.info("🎉 Экстренное исправление завершено!")
except Exception as e:
logger.error(f"💥 Критическая ошибка: {e}")
raise
if __name__ == "__main__":
asyncio.run(emergency_fix())
+74
View File
@@ -2,6 +2,7 @@ from database import Subscription
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from typing import List, Optional, Dict
from translations import t
from datetime import datetime
def language_keyboard() -> InlineKeyboardMarkup:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
@@ -218,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")],
+3 -1
View File
@@ -19,7 +19,9 @@ class RemnaWaveAPI:
headers = {
'Authorization': f'Bearer {self.token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
'Accept': 'application/json',
'X-Forwarded-Proto': 'https',
'X-Forwarded-For': '127.0.0.1'
}
timeout = aiohttp.ClientTimeout(total=30)
self.session = aiohttp.ClientSession(
Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

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

After

Width:  |  Height:  |  Size: 211 KiB

+85 -1
View File
@@ -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'
}
}