Compare commits

...

10 Commits

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