Compare commits

...

44 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
Fr1ngg edc891c93d Update README.md 2025-08-11 22:06:49 +03:00
Fr1ngg 2d706a4be0 Update keyboards.py 2025-08-11 22:01:38 +03:00
Fr1ngg 32cb81ef34 Update handlers.py 2025-08-11 22:01:09 +03:00
Fr1ngg 88aa3c4a3d Update database.py
autopay mod
2025-08-11 22:00:43 +03:00
Fr1ngg e813d3ccb7 Create autopay_service.py 2025-08-11 22:00:19 +03:00
Fr1ngg a24d7a4e24 Update main.py 2025-08-11 21:59:51 +03:00
Fr1ngg 39987b9717 Update admin_handlers.py
autopay mod
2025-08-11 21:59:31 +03:00
Fr1ngg c77a1ea4f8 Update README.md 2025-08-11 17:57:35 +03:00
30 changed files with 3194 additions and 249 deletions
+86 -29
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 с полным функционалом управления пользователями, платежами и администрированием.
## 🚀 Особенности
@@ -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` |
### Диагностика
@@ -671,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 | Архивирование вместо удаления |
@@ -681,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.
---
### 🚀 Хотите помочь?
**Разработчикам:**
@@ -704,14 +763,12 @@ docker-compose ps
---
**⚠️ Важные замечания от автора:**
> 🗑 **Осторожно с удалением подписок!** Сейчас удаление плана подписки скроет эту подписку у всех пользователей, которые ее купили. Используйте деактивацию вместо удаления. Эта проблема будет исправлена в v1.4.0.
---
**⭐ Если проект был полезен, поставьте звездочку на GitHub!**
---
*Создано с ❤️ для RemnaWave сообщества*
### PS
Автор не является проф. разработчиком, да и без вайбкодинга не обошлось, но я прикладываю все усилия, чтобы сделать удобного публичного бота для ваших сервисов)(
*Создано с ❤️ для Remnawave сообщества*
+977 -1
View File
@@ -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)
+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

+284
View File
@@ -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
}
+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
+821 -201
View File
File diff suppressed because it is too large Load Diff
+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())
+245 -1
View File
@@ -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("❌ Ошибка операции")
+215 -6
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=[
@@ -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
+43
View File
@@ -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:
+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'
}
}