Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b14baca51 | |||
| eb1a75c9f5 | |||
| 5e79106134 | |||
| ee45ec356e | |||
| 9275bff828 | |||
| 9150c70618 | |||
| b792e749d6 | |||
| 9871572740 | |||
| 52c1cb3cbc | |||
| d5b2b371c8 | |||
| d5cb022b02 | |||
| 021f62fd30 | |||
| 684e398798 | |||
| 12b64e9978 |
+7
-2
@@ -1,17 +1,22 @@
|
||||
# Bot Configuration
|
||||
BOT_TOKEN=
|
||||
|
||||
# настройки реф программы юзернейм без @!
|
||||
BOT_USERNAME=
|
||||
REFERRAL_FIRST_REWARD=100.0
|
||||
REFERRAL_REFERRED_BONUS=100.0
|
||||
REFERRAL_THRESHOLD=200.0
|
||||
REFERRAL_PERCENTAGE=0.2
|
||||
|
||||
# RemnaWave API Configuration
|
||||
REMNAWAVE_URL=https://example.com
|
||||
REMNAWAVE_MODE=remote
|
||||
REMNAWAVE_TOKEN=
|
||||
SUBSCRIPTION_BASE_URL=https://example.com
|
||||
|
||||
# Admin Configuration (comma-separated list of Telegram user IDs)
|
||||
ADMIN_IDS=123445567,12342123
|
||||
|
||||
# Support Configuration
|
||||
DEFAULT_LANGUAGE=ru
|
||||
SUPPORT_USERNAME=support
|
||||
|
||||
# Database Configuration (optional, defaults to SQLite)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 15 13" src="https://github.com/user-attachments/assets/91098622-1bce-4f27-afef-60a3c5b5061f" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 22" src="https://github.com/user-attachments/assets/46b87e75-b420-4ac6-91b9-8c7e9bcffb2a" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 39" src="https://github.com/user-attachments/assets/ca97811f-ca00-4133-a120-1c11f0efa0fc" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 45" src="https://github.com/user-attachments/assets/258e1adb-2c39-4126-82a7-7791b56d42db" /><img width="906" height="496" alt="Снимок экрана 2025-08-05 в 03 14 53" src="https://github.com/user-attachments/assets/073455fc-f42d-4d70-839d-59042add2d94" /><img width="906" height="316" alt="Снимок экрана 2025-08-05 в 03 16 00" src="https://github.com/user-attachments/assets/2034dde8-a48b-4149-a23f-b788aa40e0b1" /><img width="894" height="317" alt="Снимок экрана 2025-08-05 в 15 32 32" src="https://github.com/user-attachments/assets/a96337cf-f58a-488e-9600-c94a92bdbfc2" /><img width="906" height="366" alt="Снимок экрана 2025-08-05 в 03 16 18" src="https://github.com/user-attachments/assets/3a3d1e0a-92fc-4573-a48c-f36481d6d0de" /><img width="906" height="842" alt="Снимок экрана 2025-08-05 в 03 17 24" src="https://github.com/user-attachments/assets/8b407f69-6861-4810-822e-c3f7b8f63629" /><img width="906" height="274" alt="Снимок экрана 2025-08-05 в 03 17 43" src="https://github.com/user-attachments/assets/923a945a-5ef8-4dcb-9804-fffc37ab8887" /><img width="936" height="364" alt="Снимок экрана 2025-08-05 в 03 20 03" src="https://github.com/user-attachments/assets/1faecdfe-f80c-4ac2-ad38-81a30fc6623d" /><img width="892" height="486" alt="Снимок экрана 2025-08-07 в 07 43 47" src="https://github.com/user-attachments/assets/0dd6cb8e-fd2f-4a98-8920-aadceee09fd0" /><img width="892" height="762" alt="Снимок экрана 2025-08-07 в 07 44 20" src="https://github.com/user-attachments/assets/d7c95e3e-cf04-40bc-9422-d7289447625d" /><img width="892" height="823" alt="Снимок экрана 2025-08-07 в 07 46 45" src="https://github.com/user-attachments/assets/9ab2c378-0abc-447d-9e95-a3ab8dab2f18" />
|
||||
<img width="892" height="501" alt="Снимок экрана 2025-08-07 в 07 42 07" src="https://github.com/user-attachments/assets/839c02da-4461-4127-894a-772e66175e23" /><img width="892" height="805" alt="Снимок экрана 2025-08-07 в 07 57 01" src="https://github.com/user-attachments/assets/bc35f79d-0b0d-4c81-8623-696b708642ad" /><img width="892" height="834" alt="Снимок экрана 2025-08-07 в 07 41 09" src="https://github.com/user-attachments/assets/d4731a79-0171-4254-aa78-2e4c7305829b" /><img width="631" height="606" alt="Снимок экрана 2025-08-06 в 18 48 31" src="https://github.com/user-attachments/assets/c44548b0-f27b-4f67-b3c0-2c002ae33979" />
|
||||
|
||||
|
||||
<img width="892" height="501" alt="Снимок экрана 2025-08-07 в 07 42 07" src="https://github.com/user-attachments/assets/839c02da-4461-4127-894a-772e66175e23" /><img width="892" height="805" alt="Снимок экрана 2025-08-07 в 07 57 01" src="https://github.com/user-attachments/assets/bc35f79d-0b0d-4c81-8623-696b708642ad" /><img width="892" height="834" alt="Снимок экрана 2025-08-07 в 07 41 09" src="https://github.com/user-attachments/assets/d4731a79-0171-4254-aa78-2e4c7305829b" /><img width="631" height="606" alt="Снимок экрана 2025-08-06 в 18 48 31" src="https://github.com/user-attachments/assets/c44548b0-f27b-4f67-b3c0-2c002ae33979" /><img width="866" height="655" alt="Снимок экрана 2025-08-08 в 05 08 56" src="https://github.com/user-attachments/assets/2957b76c-8b2b-4a32-8ca4-44cbe3c8fa7b" /><img width="866" height="256" alt="Снимок экрана 2025-08-08 в 05 10 29" src="https://github.com/user-attachments/assets/39dbece4-6004-475e-815a-a520499c8839" /><img width="866" height="195" alt="Снимок экрана 2025-08-08 в 05 10 54" src="https://github.com/user-attachments/assets/b3196856-9f36-46d9-a1f5-93b71699174f" /><img width="894" height="212" alt="Снимок экрана 2025-08-08 в 05 11 18" src="https://github.com/user-attachments/assets/47b4b625-93d2-48c7-ae3a-a6a96811f935" /><img width="894" height="173" alt="Снимок экрана 2025-08-08 в 05 29 42" src="https://github.com/user-attachments/assets/600eecc7-e255-421f-a355-a410332e4313" />
|
||||
|
||||
#Описание
|
||||
|
||||
@@ -13,7 +11,7 @@ RemnaWave Bedolaga Telegram Bot — это многофункциональны
|
||||
|
||||
Мультиязычный интерфейс (на данный момент русский и английский языки)
|
||||
|
||||
Создание и покупка подписок с управлением трафиком, длительностью и ценой
|
||||
Создание и покупка подписок
|
||||
|
||||
Бесплатная тестовая подписка с заданными ограничениями(срок, лимит трафика, назначение сквада)
|
||||
|
||||
@@ -23,6 +21,8 @@ RemnaWave Bedolaga Telegram Bot — это многофункциональны
|
||||
|
||||
Промокоды со скидками и ограничениями по использованию
|
||||
|
||||
Реф. программа - С несколькими уровнями: 1) награда первое пополнение приглашенному и владельцу реф ссылки. 2) Награда владельцу за каждое последующее пополнение в процентах от суммы пополнения
|
||||
|
||||
Полноценная админ-панель с контролем пользователей, созданием подписок(Подтягивает UUID сквада из панели), платежей и статистикой
|
||||
|
||||
Рассылка сообщений отдельным пользователям и всем сразу
|
||||
@@ -119,6 +119,16 @@ URL и токен RemnaWave API
|
||||
|
||||
BOT_TOKEN — токен Telegram бота от BotFather.
|
||||
|
||||
BOT_USERNAME= (Юзернейм бота для работы реф программы)
|
||||
|
||||
REFERRAL_FIRST_REWARD=100.0 (Награда за пополнение рефа)
|
||||
|
||||
REFERRAL_REFERRED_BONUS=100.0 (Награда рефу за первое пополнение)
|
||||
|
||||
REFERRAL_THRESHOLD=200.0 (Сумма на которую должен пополниться реф, чтобы выдало бонус всем)
|
||||
|
||||
REFERRAL_PERCENTAGE=0.2 (Процент за каждое последующее пополнение рефа, работает со второго пополнения)
|
||||
|
||||
REMNAWAVE_URL — URL API RemnaWave.
|
||||
|
||||
REMNAWAVE_MODE=remote
|
||||
@@ -223,9 +233,12 @@ run.sh — скрипт установки и управления ботом (
|
||||
Код колхозный и не без вайбкодинга тут обошлось, но будет допиливаться, текущая реализация работает - уже хорошо
|
||||
1) Дописать службу для оповещения об истечении срока подписки и контроля - Done v1.1.0
|
||||
2) Подключить различные шлюзы для пополнения баланса
|
||||
3) Синхранизацию с Remnawave между пользователями по тг id
|
||||
3) Синхранизацию с Remnawave между пользователями по тг id - Done v1.2.0
|
||||
4) Полнофункциональную панель упарвления
|
||||
5) Добавить возможность удаление промокодов - In progress
|
||||
6) Доработать алгоритм удаления подписок ибо удаление(А НЕ деактивация) сейчас - скроект эту подписку у всех юзеров которые ее купили, так что удаляйте на свой страх и риск я предупредил) - In progress
|
||||
8) Отправка уведомлений административных в другие чаты-топики
|
||||
9) Рефка (как по мне беспонтовая штука, сервера нормальные хостите, сервис нормальный делайте и будут клиенты - не ебите мозги, но если будет не лень, то допилю)
|
||||
9) Рефка (как по мне беспонтовая штука, сервера нормальные хостите, сервис нормальный делайте и будут клиенты - не ебите мозги, но если будет не лень, то допилю), ладно трех запросов в личку мне хватило для того. чтобы пересмотреть свое мнение и вкорячить эту шляпу приоритетом - Done v 1.3.0
|
||||
10) Интегрировать панелья управления Remnawave прямо в бота - Done v1.1.0
|
||||
|
||||
А так, други, буду рад баг репортам, по настройке тоже не проблема подсказать, стучите в лс или @fringg
|
||||
|
||||
+89
-12
@@ -13,6 +13,7 @@ from keyboards import *
|
||||
from translations import t
|
||||
from utils import *
|
||||
from handlers import BotStates
|
||||
from referral_utils import process_referral_rewards
|
||||
try:
|
||||
from api_error_handlers import (
|
||||
APIErrorHandler, safe_get_nodes, safe_get_system_users,
|
||||
@@ -152,7 +153,7 @@ async def create_subscription_callback(callback: CallbackQuery, user: User, stat
|
||||
async def handle_sub_name(message: Message, state: FSMContext, user: User, **kwargs):
|
||||
"""Handle subscription name input"""
|
||||
name = message.text.strip()
|
||||
if len(name) < 3 or len(name) > 100:
|
||||
if not (3 <= len(name) <= 100):
|
||||
await message.answer("❌ Название должно быть от 3 до 100 символов")
|
||||
return
|
||||
|
||||
@@ -920,7 +921,7 @@ async def noop_callback(callback: CallbackQuery, **kwargs):
|
||||
# Payment approval handlers
|
||||
@admin_router.callback_query(F.data.startswith("approve_payment_"))
|
||||
async def approve_payment(callback: CallbackQuery, user: User, db: Database, **kwargs):
|
||||
"""Approve payment"""
|
||||
"""Approve payment with referral rewards - ИСПРАВЛЕНА"""
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
@@ -944,12 +945,14 @@ async def approve_payment(callback: CallbackQuery, user: User, db: Database, **k
|
||||
payment.status = 'completed'
|
||||
await db.update_payment(payment)
|
||||
|
||||
bot = kwargs.get('bot')
|
||||
await process_referral_rewards(payment.user_id, payment.amount, payment.id, db, bot)
|
||||
|
||||
await callback.message.edit_text(
|
||||
f"✅ Платеж одобрен!\n💰 Пользователю {payment.user_id} добавлено {payment.amount} руб."
|
||||
)
|
||||
|
||||
# Notify user about successful payment
|
||||
bot = kwargs.get('bot')
|
||||
if bot:
|
||||
try:
|
||||
await bot.send_message(
|
||||
@@ -3338,11 +3341,11 @@ async def sync_remnawave_callback(callback: CallbackQuery, user: User, **kwargs)
|
||||
def sync_remnawave_keyboard(language: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Keyboard for RemnaWave sync options"""
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text="👥 Синхронизировать пользователей", callback_data="sync_users_remnawave")],
|
||||
[InlineKeyboardButton(text="📋 Синхронизировать подписки", callback_data="sync_subscriptions_remnawave")],
|
||||
#[InlineKeyboardButton(text="👥 Синхронизировать пользователей", callback_data="sync_users_remnawave")],
|
||||
#[InlineKeyboardButton(text="📋 Синхронизировать подписки", callback_data="sync_subscriptions_remnawave")],
|
||||
[InlineKeyboardButton(text="🔄 Полная синхронизация", callback_data="sync_full_remnawave")],
|
||||
[InlineKeyboardButton(text="🌍 ИМПОРТ ВСЕХ по Telegram ID", callback_data="import_all_by_telegram")],
|
||||
[InlineKeyboardButton(text="👤 Синхронизировать одного", callback_data="sync_single_user")],
|
||||
[InlineKeyboardButton(text="🌍 ИМПОРТ ВСЕХ по Telegram ID", callback_data="import_all_by_telegram")],
|
||||
[InlineKeyboardButton(text="📋 Просмотр планов", callback_data="view_imported_plans")],
|
||||
[InlineKeyboardButton(text="📊 Статус синхронизации", callback_data="sync_status_remnawave")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="admin_system")]
|
||||
@@ -3890,24 +3893,24 @@ async def sync_status_remnawave_callback(callback: CallbackQuery, user: User, ap
|
||||
# Build status text
|
||||
text = "📊 **Статус синхронизации**\n\n"
|
||||
|
||||
text += "**RemnaWave:**\n"
|
||||
text += "RemnaWave:\n"
|
||||
text += f"• Всего пользователей: {len(remna_users)}\n"
|
||||
text += f"• С Telegram ID: {remna_with_tg}\n"
|
||||
text += f"• Без Telegram ID: {remna_without_tg}\n\n"
|
||||
|
||||
text += "**Бот:**\n"
|
||||
text += "Бот:\n"
|
||||
text += f"• Всего пользователей: {len(bot_users)}\n"
|
||||
text += f"• С RemnaWave UUID: {bot_with_uuid}\n"
|
||||
text += f"• Без RemnaWave UUID: {bot_without_uuid}\n\n"
|
||||
|
||||
text += "**Подписки:**\n"
|
||||
text += "Подписки:\n"
|
||||
text += f"• Всего в боте: {total_bot_subs}\n"
|
||||
text += f"• Синхронизировано: {synced_subs}\n"
|
||||
text += f"• Не синхронизировано: {total_bot_subs - synced_subs}\n\n"
|
||||
|
||||
# Recommendations
|
||||
if bot_without_uuid > 0 or remna_without_tg > 0 or (total_bot_subs - synced_subs) > 0:
|
||||
text += "⚠️ **Рекомендации:**\n"
|
||||
text += "⚠️ Рекомендации:\n"
|
||||
if bot_without_uuid > 0:
|
||||
text += f"• {bot_without_uuid} пользователей бота не связаны с RemnaWave\n"
|
||||
if remna_without_tg > 0:
|
||||
@@ -3916,7 +3919,7 @@ async def sync_status_remnawave_callback(callback: CallbackQuery, user: User, ap
|
||||
text += f"• {total_bot_subs - synced_subs} подписок не синхронизированы\n"
|
||||
text += "\n💡 Рекомендуется выполнить полную синхронизацию\n"
|
||||
else:
|
||||
text += "✅ **Все данные синхронизированы**\n"
|
||||
text += "✅ Все данные синхронизированы\n"
|
||||
|
||||
text += f"\n🕐 _Проверено: {format_datetime(datetime.now(), user.language)}_"
|
||||
|
||||
@@ -3935,7 +3938,6 @@ async def sync_status_remnawave_callback(callback: CallbackQuery, user: User, ap
|
||||
# User filtering handlers
|
||||
@admin_router.callback_query(F.data == "filter_users_active")
|
||||
async def filter_users_active_callback(callback: CallbackQuery, user: User, api: RemnaWaveAPI = None, **kwargs):
|
||||
"""Show only active users - ИСПРАВЛЕНО"""
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
@@ -5638,3 +5640,78 @@ async def debug_all_plans_callback(callback: CallbackQuery, user: User, db: Data
|
||||
except Exception as e:
|
||||
logger.error(f"Error debugging all plans: {e}")
|
||||
await callback.answer("❌ Ошибка анализа планов", show_alert=True)
|
||||
|
||||
@admin_router.callback_query(F.data == "admin_referrals")
|
||||
async def admin_referrals_callback(callback: CallbackQuery, user: User, **kwargs):
|
||||
"""Show referral management"""
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
"👥 Управление реферальной программой",
|
||||
reply_markup=admin_referrals_keyboard(user.language)
|
||||
)
|
||||
|
||||
@admin_router.callback_query(F.data == "referral_statistics")
|
||||
async def referral_statistics_callback(callback: CallbackQuery, user: User, db: Database, **kwargs):
|
||||
"""Show referral statistics"""
|
||||
if not await check_admin_access(callback, user):
|
||||
return
|
||||
|
||||
try:
|
||||
# Получаем общую статистику
|
||||
async with db.session_factory() as session:
|
||||
from sqlalchemy import select, func
|
||||
|
||||
# Общее количество рефералов
|
||||
total_referrals = await session.execute(
|
||||
select(func.count(ReferralProgram.id))
|
||||
)
|
||||
total_referrals = total_referrals.scalar() or 0
|
||||
|
||||
# Активные рефералы (получившие первую награду)
|
||||
active_referrals = await session.execute(
|
||||
select(func.count(ReferralProgram.id))
|
||||
.where(ReferralProgram.first_reward_paid == True)
|
||||
)
|
||||
active_referrals = active_referrals.scalar() or 0
|
||||
|
||||
# Общая сумма выплат
|
||||
total_paid = await session.execute(
|
||||
select(func.sum(ReferralEarning.amount))
|
||||
)
|
||||
total_paid = total_paid.scalar() or 0.0
|
||||
|
||||
# Топ рефереров
|
||||
top_referrers = await session.execute(
|
||||
select(ReferralProgram.referrer_id, func.count(ReferralProgram.id).label('count'))
|
||||
.group_by(ReferralProgram.referrer_id)
|
||||
.order_by(func.count(ReferralProgram.id).desc())
|
||||
.limit(5)
|
||||
)
|
||||
top_referrers = list(top_referrers.fetchall())
|
||||
|
||||
text = "📊 Статистика реферальной программы\n\n"
|
||||
text += f"👥 Всего рефералов: {total_referrals}\n"
|
||||
text += f"✅ Активных рефералов: {active_referrals}\n"
|
||||
text += f"💰 Выплачено всего: {total_paid:.2f}₽\n"
|
||||
text += f"📈 Конверсия: {(active_referrals/total_referrals*100):.1f}%" if total_referrals > 0 else "📈 Конверсия: 0%"
|
||||
|
||||
if top_referrers:
|
||||
text += f"\n\n🏆 Топ рефереров:\n"
|
||||
for i, (referrer_id, count) in enumerate(top_referrers, 1):
|
||||
referrer = await db.get_user_by_telegram_id(referrer_id)
|
||||
username = referrer.username if referrer and referrer.username else "Unknown"
|
||||
text += f"{i}. @{username}: {count} рефералов\n"
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=back_keyboard("admin_referrals", user.language)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting referral statistics: {e}")
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка получения статистики",
|
||||
reply_markup=back_keyboard("admin_referrals", user.language)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# api_error_handlers.py - Дополнительные утилиты для обработки ошибок API
|
||||
|
||||
import logging
|
||||
from typing import Optional, Dict, Any, Callable
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
@@ -1,68 +1,172 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
import logging
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
print("✅ .env файл загружен успешно")
|
||||
except ImportError:
|
||||
print("⚠️ python-dotenv не установлен. Используются системные переменные окружения.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Ошибка загрузки .env файла: {e}")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
BOT_TOKEN: str
|
||||
REMNAWAVE_URL: str
|
||||
REMNAWAVE_TOKEN: str
|
||||
REMNAWAVE_MODE: str
|
||||
DATABASE_URL: str
|
||||
ADMIN_IDS: List[int]
|
||||
DEFAULT_LANGUAGE: str
|
||||
SUPPORT_USERNAME: str
|
||||
# Bot settings
|
||||
BOT_TOKEN: str = field(default="")
|
||||
DATABASE_URL: str = field(default="sqlite+aiosqlite:///bot.db")
|
||||
|
||||
# Subscription URL settings - ТЕПЕРЬ ОПЦИОНАЛЬНЫЙ
|
||||
SUBSCRIPTION_BASE_URL: Optional[str]
|
||||
# RemnaWave API settings
|
||||
REMNAWAVE_URL: str = field(default="")
|
||||
REMNAWAVE_TOKEN: str = field(default="")
|
||||
SUBSCRIPTION_BASE_URL: str = field(default="")
|
||||
|
||||
# Admin settings
|
||||
ADMIN_IDS: List[int] = field(default_factory=list)
|
||||
SUPPORT_USERNAME: str = field(default="support")
|
||||
|
||||
# Bot settings
|
||||
DEFAULT_LANGUAGE: str = field(default="ru")
|
||||
BOT_USERNAME: str = field(default="") # ИМЯ БОТА ДЛЯ РЕФЕРАЛЬНЫХ ССЫЛОК
|
||||
|
||||
# Trial subscription settings
|
||||
TRIAL_ENABLED: bool
|
||||
TRIAL_DURATION_DAYS: int
|
||||
TRIAL_TRAFFIC_GB: int
|
||||
TRIAL_SQUAD_UUID: str
|
||||
TRIAL_PRICE: float
|
||||
TRIAL_ENABLED: bool = field(default=False)
|
||||
TRIAL_DURATION_DAYS: int = field(default=3)
|
||||
TRIAL_TRAFFIC_GB: int = field(default=2)
|
||||
TRIAL_SQUAD_UUID: str = field(default="")
|
||||
|
||||
# Monitor service settings
|
||||
MONITOR_CHECK_INTERVAL: int
|
||||
MONITOR_DAILY_CHECK_HOUR: int
|
||||
MONITOR_WARNING_DAYS: int
|
||||
|
||||
MONITOR_WARNING_DAYS: int = field(default=3)
|
||||
MONITOR_CHECK_INTERVAL: int = field(default=3600) # 1 час
|
||||
MONITOR_DAILY_CHECK_HOUR: int = field(default=9) # 9 утра
|
||||
|
||||
# Referral program settings
|
||||
REFERRAL_FIRST_REWARD: float = field(default=150.0) # Первая награда
|
||||
REFERRAL_REFERRED_BONUS: float = field(default=150.0) # Бонус приглашенному
|
||||
REFERRAL_THRESHOLD: float = field(default=300.0) # Порог для получения бонуса
|
||||
REFERRAL_PERCENTAGE: float = field(default=0.25) # 25% с платежей
|
||||
|
||||
def parse_admin_ids(admin_ids_str: str) -> List[int]:
|
||||
"""Parse admin IDs from string"""
|
||||
if not admin_ids_str:
|
||||
return []
|
||||
|
||||
try:
|
||||
# Support both comma and space separated
|
||||
ids_str = admin_ids_str.replace(',', ' ').strip()
|
||||
return [int(id_str.strip()) for id_str in ids_str.split() if id_str.strip().isdigit()]
|
||||
except ValueError as e:
|
||||
logger.error(f"Error parsing admin IDs: {e}")
|
||||
return []
|
||||
|
||||
def str_to_bool(value: str) -> bool:
|
||||
"""Convert string to boolean"""
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return value.lower() in ('true', '1', 'yes', 'on', 'enabled')
|
||||
|
||||
def load_config() -> Config:
|
||||
"""Load configuration from environment variables"""
|
||||
# Parse admin IDs
|
||||
admin_ids_str = os.getenv('ADMIN_IDS', '')
|
||||
admin_ids = []
|
||||
if admin_ids_str:
|
||||
try:
|
||||
admin_ids = [int(x.strip()) for x in admin_ids_str.split(',') if x.strip()]
|
||||
except ValueError:
|
||||
admin_ids = []
|
||||
|
||||
subscription_base_url = os.getenv('SUBSCRIPTION_BASE_URL')
|
||||
# Дебаг: проверяем что переменные загружены
|
||||
print(f"🔍 BOT_USERNAME из env: '{os.getenv('BOT_USERNAME', 'НЕ НАЙДЕН')}'")
|
||||
print(f"🔍 REFERRAL_FIRST_REWARD из env: '{os.getenv('REFERRAL_FIRST_REWARD', 'НЕ НАЙДЕН')}'")
|
||||
|
||||
return Config(
|
||||
BOT_TOKEN=os.getenv('BOT_TOKEN', ''),
|
||||
REMNAWAVE_URL=os.getenv('REMNAWAVE_URL', ''),
|
||||
REMNAWAVE_TOKEN=os.getenv('REMNAWAVE_TOKEN', ''),
|
||||
REMNAWAVE_MODE=os.getenv('REMNAWAVE_MODE', 'local'),
|
||||
DATABASE_URL=os.getenv('DATABASE_URL', 'sqlite+aiosqlite:///bot.db'),
|
||||
ADMIN_IDS=admin_ids,
|
||||
DEFAULT_LANGUAGE=os.getenv('DEFAULT_LANGUAGE', 'ru'),
|
||||
SUPPORT_USERNAME=os.getenv('SUPPORT_USERNAME', 'support'),
|
||||
config = Config(
|
||||
# Bot settings
|
||||
BOT_TOKEN=os.getenv("BOT_TOKEN", ""),
|
||||
DATABASE_URL=os.getenv("DATABASE_URL", "sqlite+aiosqlite:///bot.db"),
|
||||
|
||||
# Subscription URL - ТЕПЕРЬ МОЖЕТ БЫТЬ None
|
||||
SUBSCRIPTION_BASE_URL=subscription_base_url,
|
||||
# RemnaWave API settings
|
||||
REMNAWAVE_URL=os.getenv("REMNAWAVE_URL", ""),
|
||||
REMNAWAVE_TOKEN=os.getenv("REMNAWAVE_TOKEN", ""),
|
||||
SUBSCRIPTION_BASE_URL=os.getenv("SUBSCRIPTION_BASE_URL", ""),
|
||||
|
||||
# Trial subscription settings
|
||||
TRIAL_ENABLED=os.getenv('TRIAL_ENABLED', 'true').lower() == 'true',
|
||||
TRIAL_DURATION_DAYS=int(os.getenv('TRIAL_DURATION_DAYS', '3')),
|
||||
TRIAL_TRAFFIC_GB=int(os.getenv('TRIAL_TRAFFIC_GB', '2')),
|
||||
TRIAL_SQUAD_UUID=os.getenv('TRIAL_SQUAD_UUID', '19bd5bde-5eea-4368-809c-6ba1ffb93897'),
|
||||
TRIAL_PRICE=float(os.getenv('TRIAL_PRICE', '0.0')),
|
||||
# Admin settings
|
||||
ADMIN_IDS=parse_admin_ids(os.getenv("ADMIN_IDS", "")),
|
||||
SUPPORT_USERNAME=os.getenv("SUPPORT_USERNAME", "support"),
|
||||
|
||||
# Monitor service settings
|
||||
MONITOR_CHECK_INTERVAL=int(os.getenv('MONITOR_CHECK_INTERVAL', '3600')),
|
||||
MONITOR_DAILY_CHECK_HOUR=int(os.getenv('MONITOR_DAILY_CHECK_HOUR', '10')),
|
||||
MONITOR_WARNING_DAYS=int(os.getenv('MONITOR_WARNING_DAYS', '2'))
|
||||
# Bot settings
|
||||
DEFAULT_LANGUAGE=os.getenv("DEFAULT_LANGUAGE", "ru"),
|
||||
BOT_USERNAME=os.getenv("BOT_USERNAME", ""), # ИСПРАВЛЕНО: берется из env
|
||||
|
||||
# Trial settings
|
||||
TRIAL_ENABLED=str_to_bool(os.getenv("TRIAL_ENABLED", "false")),
|
||||
TRIAL_DURATION_DAYS=int(os.getenv("TRIAL_DURATION_DAYS", "3")),
|
||||
TRIAL_TRAFFIC_GB=int(os.getenv("TRIAL_TRAFFIC_GB", "2")),
|
||||
TRIAL_SQUAD_UUID=os.getenv("TRIAL_SQUAD_UUID", ""),
|
||||
|
||||
MONITOR_WARNING_DAYS=int(os.getenv("MONITOR_WARNING_DAYS", "3")),
|
||||
MONITOR_CHECK_INTERVAL=int(os.getenv("MONITOR_CHECK_INTERVAL", "3600")),
|
||||
MONITOR_DAILY_CHECK_HOUR=int(os.getenv("MONITOR_DAILY_CHECK_HOUR", "9")),
|
||||
|
||||
# Referral settings
|
||||
REFERRAL_FIRST_REWARD=float(os.getenv("REFERRAL_FIRST_REWARD", "150.0")),
|
||||
REFERRAL_REFERRED_BONUS=float(os.getenv("REFERRAL_REFERRED_BONUS", "150.0")),
|
||||
REFERRAL_THRESHOLD=float(os.getenv("REFERRAL_THRESHOLD", "300.0")),
|
||||
REFERRAL_PERCENTAGE=float(os.getenv("REFERRAL_PERCENTAGE", "0.25")),
|
||||
)
|
||||
|
||||
print(f"✅ Config BOT_USERNAME: '{config.BOT_USERNAME}'")
|
||||
print(f"✅ Config REFERRAL_FIRST_REWARD: {config.REFERRAL_FIRST_REWARD}")
|
||||
|
||||
return config
|
||||
|
||||
def load_config_manual_dotenv() -> Config:
|
||||
"""Load config with manual .env parsing"""
|
||||
|
||||
# Загружаем .env файл вручную если python-dotenv не доступен
|
||||
env_path = ".env"
|
||||
if os.path.exists(env_path):
|
||||
try:
|
||||
with open(env_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
# Убираем кавычки если есть
|
||||
value = value.strip('"\'')
|
||||
os.environ[key] = value
|
||||
print(f"✅ Файл {env_path} загружен вручную")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Ошибка загрузки {env_path}: {e}")
|
||||
|
||||
return load_config()
|
||||
|
||||
def debug_environment():
|
||||
"""Debug environment variables loading"""
|
||||
print("\n=== DEBUG ENVIRONMENT VARIABLES ===")
|
||||
|
||||
# Ключевые переменные для проверки
|
||||
key_vars = [
|
||||
'BOT_TOKEN', 'BOT_USERNAME', 'REMNAWAVE_URL', 'REMNAWAVE_TOKEN',
|
||||
'ADMIN_IDS', 'REFERRAL_FIRST_REWARD', 'REFERRAL_THRESHOLD'
|
||||
]
|
||||
|
||||
for var in key_vars:
|
||||
value = os.getenv(var, 'НЕ УСТАНОВЛЕНА')
|
||||
print(f"{var}: {value}")
|
||||
|
||||
print("=" * 40)
|
||||
|
||||
# Проверяем файл .env
|
||||
if os.path.exists('.env'):
|
||||
print("📁 Файл .env найден")
|
||||
try:
|
||||
with open('.env', 'r') as f:
|
||||
lines = f.readlines()
|
||||
print(f"📄 Строк в .env: {len(lines)}")
|
||||
for i, line in enumerate(lines[:5], 1): # Первые 5 строк
|
||||
if 'TOKEN' not in line: # Не показываем токены
|
||||
print(f" {i}: {line.strip()}")
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка чтения .env: {e}")
|
||||
else:
|
||||
print("❌ Файл .env не найден в текущей директории")
|
||||
print(f"📍 Текущая директория: {os.getcwd()}")
|
||||
|
||||
print("=" * 40 + "\n")
|
||||
|
||||
+404
-1
@@ -2,7 +2,7 @@ from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sess
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
from sqlalchemy import BigInteger, String, Float, DateTime, Boolean, Text, Integer, text
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from typing import Optional, List, Dict
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -10,6 +10,29 @@ logger = logging.getLogger(__name__)
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
class ReferralProgram(Base):
|
||||
__tablename__ = 'referral_programs'
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
referrer_id: Mapped[int] = mapped_column(BigInteger, index=True) # Кто пригласил
|
||||
referred_id: Mapped[int] = mapped_column(BigInteger, unique=True, index=True) # Кто был приглашен
|
||||
referral_code: Mapped[str] = mapped_column(String(20), index=True) # Промокод реферера
|
||||
first_reward_paid: Mapped[bool] = mapped_column(Boolean, default=False) # Выплачена ли разовая награда
|
||||
total_earned: Mapped[float] = mapped_column(Float, default=0.0) # Всего заработано
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
first_reward_at: Mapped[Optional[datetime]] = mapped_column(DateTime) # Когда выплатили первую награду
|
||||
|
||||
class ReferralEarning(Base):
|
||||
__tablename__ = 'referral_earnings'
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
referrer_id: Mapped[int] = mapped_column(BigInteger, index=True) # Кто получил награду
|
||||
referred_id: Mapped[int] = mapped_column(BigInteger, index=True) # От кого получена награда
|
||||
amount: Mapped[float] = mapped_column(Float) # Размер награды
|
||||
earning_type: Mapped[str] = mapped_column(String(20)) # 'first_reward', 'percentage'
|
||||
related_payment_id: Mapped[Optional[int]] = mapped_column(Integer) # Связанный платеж
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = 'users'
|
||||
|
||||
@@ -106,6 +129,49 @@ class Database:
|
||||
# Выполняем миграции
|
||||
await self.migrate_user_subscriptions()
|
||||
await self.migrate_subscription_imported_field()
|
||||
await self.migrate_referral_tables() # НОВАЯ МИГРАЦИЯ
|
||||
|
||||
async def migrate_referral_tables(self):
|
||||
"""Create referral system tables if they don't exist"""
|
||||
try:
|
||||
async with self.engine.begin() as conn:
|
||||
# Создаем таблицы реферальной системы
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS referral_programs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
referrer_id BIGINT NOT NULL,
|
||||
referred_id BIGINT UNIQUE NOT NULL,
|
||||
referral_code VARCHAR(20) NOT NULL,
|
||||
first_reward_paid BOOLEAN DEFAULT FALSE,
|
||||
total_earned DOUBLE PRECISION DEFAULT 0.0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
first_reward_at TIMESTAMP,
|
||||
|
||||
INDEX idx_referrer (referrer_id),
|
||||
INDEX idx_referred (referred_id),
|
||||
INDEX idx_referral_code (referral_code)
|
||||
)
|
||||
"""))
|
||||
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS referral_earnings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
referrer_id BIGINT NOT NULL,
|
||||
referred_id BIGINT NOT NULL,
|
||||
amount DOUBLE PRECISION NOT NULL,
|
||||
earning_type VARCHAR(20) NOT NULL,
|
||||
related_payment_id INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_referrer_earnings (referrer_id),
|
||||
INDEX idx_referred_earnings (referred_id),
|
||||
INDEX idx_earning_type (earning_type)
|
||||
)
|
||||
"""))
|
||||
|
||||
logger.info("Successfully created referral system tables")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating referral tables: {e}")
|
||||
|
||||
async def close(self):
|
||||
await self.engine.dispose()
|
||||
@@ -733,3 +799,340 @@ class Database:
|
||||
logger.error(f"Error deleting user subscription {user_subscription_id}: {e}")
|
||||
await session.rollback()
|
||||
return False
|
||||
|
||||
async def create_referral(self, referrer_id: int, referred_id: int, referral_code: str) -> Optional[ReferralProgram]:
|
||||
"""Create referral relationship - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
from sqlalchemy import select
|
||||
|
||||
if referred_id == 0:
|
||||
# Генерируем уникальный placeholder ID для хранения кода
|
||||
placeholder_id = 999999999 - referrer_id # Уникальный ID на основе referrer_id
|
||||
|
||||
# Проверяем что код еще не существует для этого пользователя
|
||||
existing = await session.execute(
|
||||
select(ReferralProgram).where(
|
||||
ReferralProgram.referrer_id == referrer_id,
|
||||
ReferralProgram.referred_id == placeholder_id
|
||||
)
|
||||
)
|
||||
existing_referral = existing.scalar_one_or_none()
|
||||
|
||||
if existing_referral:
|
||||
logger.info(f"Referral code already exists for user {referrer_id}")
|
||||
return existing_referral
|
||||
|
||||
# Создаем запись для хранения кода
|
||||
referral = ReferralProgram(
|
||||
referrer_id=referrer_id,
|
||||
referred_id=placeholder_id, # Уникальный placeholder
|
||||
referral_code=referral_code
|
||||
)
|
||||
session.add(referral)
|
||||
await session.commit()
|
||||
await session.refresh(referral)
|
||||
logger.info(f"Created referral code storage for user {referrer_id}")
|
||||
return referral
|
||||
|
||||
# Обычная логика для реальных рефералов
|
||||
existing = await session.execute(
|
||||
select(ReferralProgram).where(
|
||||
ReferralProgram.referred_id == referred_id,
|
||||
ReferralProgram.referred_id < 900000000, # Исключаем placeholder
|
||||
ReferralProgram.referred_id > 0 # Исключаем нулевые
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
logger.info(f"User {referred_id} already has a real referrer")
|
||||
return None
|
||||
|
||||
# Проверяем что пользователь не приглашает сам себя
|
||||
if referrer_id == referred_id:
|
||||
logger.warning(f"User {referrer_id} tried to refer themselves")
|
||||
return None
|
||||
|
||||
# Создаем реальную реферальную связь
|
||||
referral = ReferralProgram(
|
||||
referrer_id=referrer_id,
|
||||
referred_id=referred_id,
|
||||
referral_code=referral_code
|
||||
)
|
||||
session.add(referral)
|
||||
await session.commit()
|
||||
await session.refresh(referral)
|
||||
|
||||
logger.info(f"Created real referral: {referrer_id} -> {referred_id}")
|
||||
return referral
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating referral: {e}")
|
||||
await session.rollback()
|
||||
return None
|
||||
|
||||
async def get_referral_by_referred_id(self, referred_id: int) -> Optional[ReferralProgram]:
|
||||
"""Get referral info by referred user ID"""
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
from sqlalchemy import select
|
||||
result = await session.execute(
|
||||
select(ReferralProgram).where(ReferralProgram.referred_id == referred_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting referral: {e}")
|
||||
return None
|
||||
|
||||
async def get_user_referrals(self, referrer_id: int) -> List[ReferralProgram]:
|
||||
"""Get all referrals for a user - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
from sqlalchemy import select, and_
|
||||
result = await session.execute(
|
||||
select(ReferralProgram).where(
|
||||
and_(
|
||||
ReferralProgram.referrer_id == referrer_id,
|
||||
# ИСПРАВЛЕНО: исключаем placeholder записи для хранения кодов
|
||||
ReferralProgram.referred_id < 900000000,
|
||||
ReferralProgram.referred_id > 0
|
||||
)
|
||||
)
|
||||
)
|
||||
referrals = list(result.scalars().all())
|
||||
logger.debug(f"Found {len(referrals)} real referrals for user {referrer_id}")
|
||||
return referrals
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user referrals: {e}")
|
||||
return []
|
||||
|
||||
async def create_referral(self, referrer_id: int, referred_id: int, referral_code: str) -> Optional[ReferralProgram]:
|
||||
"""Create referral relationship - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
from sqlalchemy import select
|
||||
|
||||
# Специальный случай: создание кода для самого пользователя
|
||||
if referred_id == 0:
|
||||
# Генерируем уникальный placeholder ID для хранения кода
|
||||
placeholder_id = 999999999 - referrer_id # Уникальный ID на основе referrer_id
|
||||
|
||||
# Проверяем что код еще не существует для этого пользователя
|
||||
existing = await session.execute(
|
||||
select(ReferralProgram).where(
|
||||
ReferralProgram.referrer_id == referrer_id,
|
||||
ReferralProgram.referred_id == placeholder_id
|
||||
)
|
||||
)
|
||||
existing_referral = existing.scalar_one_or_none()
|
||||
|
||||
if existing_referral:
|
||||
logger.info(f"Referral code already exists for user {referrer_id}")
|
||||
return existing_referral
|
||||
|
||||
# Создаем запись для хранения кода
|
||||
referral = ReferralProgram(
|
||||
referrer_id=referrer_id,
|
||||
referred_id=placeholder_id, # Уникальный placeholder
|
||||
referral_code=referral_code
|
||||
)
|
||||
session.add(referral)
|
||||
await session.commit()
|
||||
await session.refresh(referral)
|
||||
return referral
|
||||
|
||||
# Обычная логика для реальных рефералов
|
||||
existing = await session.execute(
|
||||
select(ReferralProgram).where(ReferralProgram.referred_id == referred_id)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
logger.info(f"User {referred_id} already has a referrer")
|
||||
return None
|
||||
|
||||
# Проверяем что пользователь не приглашает сам себя
|
||||
if referrer_id == referred_id:
|
||||
logger.warning(f"User {referrer_id} tried to refer themselves")
|
||||
return None
|
||||
|
||||
referral = ReferralProgram(
|
||||
referrer_id=referrer_id,
|
||||
referred_id=referred_id,
|
||||
referral_code=referral_code
|
||||
)
|
||||
session.add(referral)
|
||||
await session.commit()
|
||||
await session.refresh(referral)
|
||||
return referral
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating referral: {e}")
|
||||
await session.rollback()
|
||||
return None
|
||||
|
||||
async def get_user_referral_stats(self, user_id: int) -> Dict:
|
||||
"""Get user referral statistics - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
from sqlalchemy import select, func, and_, or_
|
||||
|
||||
placeholder_id = 999999999 - user_id
|
||||
|
||||
# Количество приглашенных (исключаем только конкретный placeholder этого пользователя)
|
||||
referrals_count = await session.execute(
|
||||
select(func.count(ReferralProgram.id))
|
||||
.where(
|
||||
and_(
|
||||
ReferralProgram.referrer_id == user_id,
|
||||
ReferralProgram.referred_id != placeholder_id, # Исключаем только наш placeholder
|
||||
ReferralProgram.referred_id != 0 # Исключаем нулевые записи
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Количество тех, кто получил первую награду (исключаем placeholder)
|
||||
active_referrals = await session.execute(
|
||||
select(func.count(ReferralProgram.id))
|
||||
.where(
|
||||
and_(
|
||||
ReferralProgram.referrer_id == user_id,
|
||||
ReferralProgram.first_reward_paid == True,
|
||||
ReferralProgram.referred_id != placeholder_id, # Исключаем только наш placeholder
|
||||
ReferralProgram.referred_id != 0 # Исключаем нулевые записи
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Общий заработок
|
||||
total_earned = await session.execute(
|
||||
select(func.sum(ReferralEarning.amount))
|
||||
.where(ReferralEarning.referrer_id == user_id)
|
||||
)
|
||||
|
||||
result = {
|
||||
'total_referrals': referrals_count.scalar() or 0,
|
||||
'active_referrals': active_referrals.scalar() or 0,
|
||||
'total_earned': total_earned.scalar() or 0.0
|
||||
}
|
||||
|
||||
logger.info(f"Referral stats for user {user_id}: {result}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting referral stats: {e}")
|
||||
return {
|
||||
'total_referrals': 0,
|
||||
'active_referrals': 0,
|
||||
'total_earned': 0.0
|
||||
}
|
||||
|
||||
async def generate_unique_referral_code(self, user_id: int) -> str:
|
||||
"""Generate unique referral code for user"""
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
import secrets
|
||||
import string
|
||||
|
||||
# Сначала пытаемся создать код на основе user_id
|
||||
base_code = f"REF{user_id}"
|
||||
|
||||
from sqlalchemy import select
|
||||
existing = await session.execute(
|
||||
select(ReferralProgram).where(ReferralProgram.referral_code == base_code)
|
||||
)
|
||||
|
||||
if not existing.scalar_one_or_none():
|
||||
return base_code
|
||||
|
||||
# Если код уже существует, добавляем случайные символы
|
||||
for _ in range(10):
|
||||
random_suffix = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(4))
|
||||
code = f"REF{user_id}{random_suffix}"
|
||||
|
||||
existing = await session.execute(
|
||||
select(ReferralProgram).where(ReferralProgram.referral_code == code)
|
||||
)
|
||||
|
||||
if not existing.scalar_one_or_none():
|
||||
return code
|
||||
|
||||
# Если все еще не удалось, используем полностью случайный код
|
||||
return f"REF{''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(8))}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating referral code: {e}")
|
||||
return f"REF{user_id}ERR"
|
||||
|
||||
async def get_user_referrals(self, referrer_id: int) -> List[ReferralProgram]:
|
||||
"""Get all referrals for a user - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
from sqlalchemy import select, and_
|
||||
|
||||
placeholder_id = 999999999 - referrer_id
|
||||
|
||||
result = await session.execute(
|
||||
select(ReferralProgram).where(
|
||||
and_(
|
||||
ReferralProgram.referrer_id == referrer_id,
|
||||
ReferralProgram.referred_id != placeholder_id, # Исключаем только наш placeholder
|
||||
ReferralProgram.referred_id != 0 # Исключаем нулевые записи
|
||||
)
|
||||
).order_by(ReferralProgram.created_at.desc()) # Сортируем по дате создания
|
||||
)
|
||||
referrals = list(result.scalars().all())
|
||||
|
||||
logger.info(f"Found {len(referrals)} real referrals for user {referrer_id} (excluding placeholder {placeholder_id})")
|
||||
|
||||
# Дополнительно логируем каждого реферала для отладки
|
||||
for ref in referrals:
|
||||
logger.debug(f"Referral: referrer={ref.referrer_id}, referred={ref.referred_id}, "
|
||||
f"first_reward_paid={ref.first_reward_paid}, total_earned={ref.total_earned}")
|
||||
|
||||
return referrals
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user referrals: {e}")
|
||||
return []
|
||||
|
||||
async def create_referral_earning(self, referrer_id: int, referred_id: int,
|
||||
amount: float, earning_type: str,
|
||||
related_payment_id: Optional[int] = None) -> bool:
|
||||
"""Create referral earning record"""
|
||||
async with self.session_factory() as session:
|
||||
try:
|
||||
earning = ReferralEarning(
|
||||
referrer_id=referrer_id,
|
||||
referred_id=referred_id,
|
||||
amount=amount,
|
||||
earning_type=earning_type,
|
||||
related_payment_id=related_payment_id
|
||||
)
|
||||
session.add(earning)
|
||||
|
||||
# Обновляем общий заработок и статус первой награды в реферальной программе
|
||||
from sqlalchemy import select, update
|
||||
|
||||
# Найти запись реферальной программы
|
||||
referral = await session.execute(
|
||||
select(ReferralProgram).where(
|
||||
ReferralProgram.referrer_id == referrer_id,
|
||||
ReferralProgram.referred_id == referred_id
|
||||
)
|
||||
)
|
||||
referral_record = referral.scalar_one_or_none()
|
||||
|
||||
if referral_record:
|
||||
# Обновляем total_earned
|
||||
referral_record.total_earned += amount
|
||||
|
||||
# Если это первая награда, помечаем как выплаченную
|
||||
if earning_type == 'first_reward':
|
||||
referral_record.first_reward_paid = True
|
||||
referral_record.first_reward_at = datetime.utcnow()
|
||||
|
||||
await session.merge(referral_record)
|
||||
|
||||
await session.commit()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating referral earning: {e}")
|
||||
await session.rollback()
|
||||
return False
|
||||
|
||||
+522
-90
@@ -13,7 +13,15 @@ from remnawave_api import RemnaWaveAPI
|
||||
from keyboards import *
|
||||
from translations import t
|
||||
from utils import *
|
||||
from config import Config
|
||||
from config import *
|
||||
import base64
|
||||
import json
|
||||
from referral_utils import (
|
||||
process_referral_rewards,
|
||||
create_referral_from_start_param,
|
||||
create_referral_from_promocode,
|
||||
generate_referral_link
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -70,32 +78,160 @@ router = Router()
|
||||
# Start command
|
||||
@router.message(Command("start"))
|
||||
async def start_command(message: Message, state: FSMContext, db: Database, **kwargs):
|
||||
"""Handle /start command"""
|
||||
"""Handle /start command with referral support and language memory - ИСПРАВЛЕНО"""
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
|
||||
# If user is None, try to create a minimal response
|
||||
if not user:
|
||||
logger.error(f"User is None for telegram_id {message.from_user.id}")
|
||||
await message.answer("❌ Ошибка инициализации пользователя. Попробуйте позже.")
|
||||
return
|
||||
|
||||
# Clear any existing state
|
||||
# Обрабатываем реферальный параметр только если есть параметры
|
||||
if message.text and len(message.text.split()) > 1:
|
||||
start_param = message.text.split()[1]
|
||||
|
||||
# Проверяем реферальную ссылку
|
||||
if start_param.startswith("ref_"):
|
||||
try:
|
||||
referrer_id = int(start_param.replace("ref_", ""))
|
||||
|
||||
# ЗАЩИТА: Проверяем взаимное реферальство
|
||||
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(
|
||||
"❌ Нельзя использовать ссылку человека, которого вы пригласили!\n\n"
|
||||
"Взаимные рефералы не допускаются."
|
||||
)
|
||||
else:
|
||||
bot = kwargs.get('bot')
|
||||
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'))
|
||||
|
||||
await message.answer(
|
||||
"🎁 Добро пожаловать!\n\n"
|
||||
f"Вы перешли по реферальной ссылке! После пополнения баланса на {threshold:.0f}₽ "
|
||||
f"вы получите бонус {referred_bonus:.0f}₽!"
|
||||
)
|
||||
elif not success:
|
||||
# Проверяем причину неудачи
|
||||
existing_referral = await db.get_referral_by_referred_id(user.telegram_id)
|
||||
if existing_referral:
|
||||
await message.answer("ℹ️ Вы уже использовали реферальную ссылку ранее.")
|
||||
except (ValueError, TypeError):
|
||||
# Неверный формат ссылки, просто игнорируем
|
||||
pass
|
||||
|
||||
# Очищаем состояние
|
||||
await state.clear()
|
||||
|
||||
if not user.language or user.language == 'ru': # Default handling
|
||||
await message.answer(
|
||||
t('select_language'),
|
||||
reply_markup=language_keyboard()
|
||||
)
|
||||
await state.set_state(BotStates.waiting_language)
|
||||
if not user.language or user.language == 'ru' or user.language == '':
|
||||
# НОВОЕ: Проверяем, это первый запуск или пользователь уже выбирал язык
|
||||
if user.language == '' or user.language is None:
|
||||
# Первый запуск - показываем выбор языка
|
||||
await message.answer(
|
||||
t('select_language'),
|
||||
reply_markup=language_keyboard()
|
||||
)
|
||||
await state.set_state(BotStates.waiting_language)
|
||||
return
|
||||
else:
|
||||
# Язык уже выбран (ru) - показываем главное меню
|
||||
await show_main_menu(message, user.language, user.is_admin, user.telegram_id, db, config)
|
||||
else:
|
||||
# У пользователя уже установлен язык - показываем главное меню
|
||||
await show_main_menu(message, user.language, user.is_admin, user.telegram_id, db, config)
|
||||
|
||||
|
||||
async def process_referral_rewards(user_id: int, amount: float, payment_id: int, db: Database, bot=None):
|
||||
"""Process referral rewards after successful payment"""
|
||||
try:
|
||||
# Проверяем есть ли у пользователя реферер
|
||||
referral = await db.get_referral_by_referred_id(user_id)
|
||||
|
||||
if not referral:
|
||||
return
|
||||
|
||||
# Получаем информацию о пользователе
|
||||
user = await db.get_user_by_telegram_id(user_id)
|
||||
if not user:
|
||||
return
|
||||
|
||||
# Проверяем первую награду (если баланс стал >= 300 и награда еще не выплачена)
|
||||
if not referral.first_reward_paid and user.balance >= 300:
|
||||
# Выплачиваем первую награду рефереру (150₽)
|
||||
success = await db.create_referral_earning(
|
||||
referrer_id=referral.referrer_id,
|
||||
referred_id=user_id,
|
||||
amount=150.0,
|
||||
earning_type='first_reward',
|
||||
related_payment_id=payment_id
|
||||
)
|
||||
|
||||
if success and bot:
|
||||
try:
|
||||
# Уведомляем реферера
|
||||
await bot.send_message(
|
||||
referral.referrer_id,
|
||||
f"🎉 Поздравляем! Ваш реферал пополнил баланс на 300₽+\n\n"
|
||||
f"💰 Вам начислено 150₽ за приведенного друга!\n"
|
||||
f"Также вы будете получать 25% с каждого его платежа."
|
||||
)
|
||||
|
||||
# Уведомляем самого пользователя
|
||||
await bot.send_message(
|
||||
user_id,
|
||||
f"🎁 Бонус активирован! Вам начислено 150₽ за переход по реферальной ссылке!"
|
||||
)
|
||||
|
||||
# Добавляем бонус рефералу
|
||||
await db.add_balance(user_id, 150.0)
|
||||
await db.create_payment(
|
||||
user_id=user_id,
|
||||
amount=150.0,
|
||||
payment_type='referral',
|
||||
description='Бонус за переход по реферальной ссылке',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send referral notifications: {e}")
|
||||
|
||||
# Выплачиваем процент с платежа (25%)
|
||||
if amount > 0: # Только с положительных платежей
|
||||
percentage_reward = amount * 0.25
|
||||
|
||||
success = await db.create_referral_earning(
|
||||
referrer_id=referral.referrer_id,
|
||||
referred_id=user_id,
|
||||
amount=percentage_reward,
|
||||
earning_type='percentage',
|
||||
related_payment_id=payment_id
|
||||
)
|
||||
|
||||
if success and bot and percentage_reward >= 1.0: # Уведомляем только если сумма >= 1₽
|
||||
try:
|
||||
await bot.send_message(
|
||||
referral.referrer_id,
|
||||
f"💰 Реферальный доход!\n\n"
|
||||
f"Ваш реферал совершил платеж на {amount:.2f}₽\n"
|
||||
f"Вам начислено: {percentage_reward:.2f}₽ (25%)"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send percentage notification: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing referral rewards: {e}")
|
||||
|
||||
# Language selection
|
||||
@router.callback_query(F.data.startswith("lang_"))
|
||||
async def language_callback(callback: CallbackQuery, state: FSMContext, db: Database, **kwargs):
|
||||
"""Handle language selection"""
|
||||
"""Handle language selection - ИСПРАВЛЕНО: правильное сохранение языка"""
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
|
||||
@@ -107,22 +243,26 @@ async def language_callback(callback: CallbackQuery, state: FSMContext, db: Data
|
||||
|
||||
# Update user language
|
||||
try:
|
||||
# ИСПРАВЛЕНО: Сохраняем выбранный язык в базе
|
||||
user.language = lang
|
||||
await db.update_user(user)
|
||||
logger.info(f"Updated language for user {user.telegram_id} to {lang}")
|
||||
|
||||
# Check if this is initial language selection or language change
|
||||
current_state = await state.get_state()
|
||||
is_initial_setup = current_state == BotStates.waiting_language.state
|
||||
|
||||
if is_initial_setup:
|
||||
# Первоначальная настройка языка
|
||||
await callback.message.edit_text(
|
||||
t('language_selected', lang),
|
||||
reply_markup=None
|
||||
)
|
||||
await show_main_menu(callback.message, lang, user.is_admin, user.telegram_id, db, config)
|
||||
# ВАЖНО: Очищаем состояние и показываем главное меню
|
||||
await state.clear()
|
||||
await show_main_menu(callback.message, lang, user.is_admin, user.telegram_id, db, config)
|
||||
else:
|
||||
# This is a language change from main menu
|
||||
# Смена языка из главного меню
|
||||
# Проверяем, доступна ли тестовая подписка
|
||||
show_trial = False
|
||||
if config and config.TRIAL_ENABLED and db:
|
||||
@@ -622,7 +762,7 @@ async def buy_subscription_detail(callback: CallbackQuery, db: Database, **kwarg
|
||||
|
||||
@router.callback_query(F.data.startswith("confirm_buy_"))
|
||||
async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
|
||||
"""Confirm subscription purchase - ДОБАВЛЕНА ПОДДЕРЖКА URL ИЗ API"""
|
||||
"""Confirm subscription purchase - ПОЛНОСТЬЮ ИСПРАВЛЕННАЯ ВЕРСИЯ"""
|
||||
user = kwargs.get('user')
|
||||
api = kwargs.get('api')
|
||||
|
||||
@@ -643,15 +783,17 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
|
||||
await callback.answer(t('insufficient_balance', user.language))
|
||||
return
|
||||
|
||||
# Get API from kwargs
|
||||
if not api:
|
||||
logger.error("API not available in kwargs")
|
||||
await callback.message.edit_text(
|
||||
t('purchase_error', user.language),
|
||||
"❌ Временная ошибка сервиса. Попробуйте позже.",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
return
|
||||
|
||||
# Показываем индикатор прогресса
|
||||
await callback.answer("⏳ Создаю подписку...")
|
||||
|
||||
# Создаем нового пользователя в RemnaWave для каждой подписки
|
||||
username = generate_username()
|
||||
password = generate_password()
|
||||
@@ -678,27 +820,25 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
|
||||
else:
|
||||
logger.error(f"Invalid API response structure: {remna_user}")
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка создания пользователя в системе",
|
||||
"❌ Ошибка создания подписки. Средства не списаны.",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
return
|
||||
|
||||
if user_uuid:
|
||||
# Обновляем основного пользователя только если у него еще нет remnawave_uuid
|
||||
if not user.remnawave_uuid:
|
||||
user.remnawave_uuid = user_uuid
|
||||
await db.update_user(user)
|
||||
|
||||
# Если shortUuid не получен, запрашиваем его отдельно
|
||||
if not short_uuid:
|
||||
user_details = await api.get_user_by_uuid(user_uuid)
|
||||
if user_details and 'shortUuid' in user_details:
|
||||
short_uuid = user_details['shortUuid']
|
||||
try:
|
||||
user_details = await api.get_user_by_uuid(user_uuid)
|
||||
if user_details and 'shortUuid' in user_details:
|
||||
short_uuid = user_details['shortUuid']
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get shortUuid: {e}")
|
||||
|
||||
if not short_uuid:
|
||||
logger.error(f"Failed to get shortUuid for new user")
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка получения данных подписки",
|
||||
"❌ Ошибка получения данных подписки. Средства не списаны.",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
return
|
||||
@@ -707,34 +847,39 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
|
||||
else:
|
||||
logger.error("Failed to create user in RemnaWave")
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка создания пользователя",
|
||||
"❌ Ошибка создания подписки. Средства не списаны.",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
return
|
||||
else:
|
||||
logger.error("Failed to create user in RemnaWave API")
|
||||
await callback.message.edit_text(
|
||||
"❌ Ошибка создания пользователя в системе",
|
||||
"❌ Ошибка создания подписки. Средства не списаны.",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
return
|
||||
|
||||
# Deduct balance
|
||||
# Deduct balance ТОЛЬКО после успешного создания пользователя
|
||||
user.balance -= subscription.price
|
||||
await db.update_user(user)
|
||||
|
||||
# Create user subscription record
|
||||
expires_at = datetime.utcnow() + timedelta(days=subscription.duration_days)
|
||||
|
||||
await db.create_user_subscription(
|
||||
user_subscription = await db.create_user_subscription(
|
||||
user_id=user.telegram_id,
|
||||
subscription_id=subscription.id,
|
||||
short_uuid=short_uuid,
|
||||
expires_at=expires_at
|
||||
)
|
||||
|
||||
# Обновляем основного пользователя только если у него еще нет remnawave_uuid
|
||||
if not user.remnawave_uuid:
|
||||
user.remnawave_uuid = user_uuid
|
||||
await db.update_user(user)
|
||||
|
||||
# Create payment record
|
||||
await db.create_payment(
|
||||
payment = await db.create_payment(
|
||||
user_id=user.telegram_id,
|
||||
amount=-subscription.price,
|
||||
payment_type='subscription',
|
||||
@@ -742,11 +887,16 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
|
||||
status='completed'
|
||||
)
|
||||
|
||||
# ИСПРАВЛЕНО: убираем config из вызова process_referral_rewards
|
||||
bot = kwargs.get('bot')
|
||||
await process_referral_rewards(user.telegram_id, subscription.price, payment.id, db, bot)
|
||||
|
||||
# Формируем сообщение с URL из API
|
||||
success_text = f"✅ Подписка успешно создана!\n\n"
|
||||
success_text += f"📋 Подписка: {subscription.name}\n"
|
||||
success_text += f"⏰ Действует до: {format_date(expires_at, user.language)}\n"
|
||||
success_text += f"💰 Стоимость: {subscription.price} руб.\n\n"
|
||||
success_text += f"💰 Стоимость: {subscription.price} руб.\n"
|
||||
success_text += f"💳 Остаток: {user.balance} руб.\n\n"
|
||||
|
||||
# Получаем subscription URL из API
|
||||
try:
|
||||
@@ -770,16 +920,16 @@ async def confirm_purchase(callback: CallbackQuery, db: Database, **kwargs):
|
||||
log_user_action(user.telegram_id, "subscription_purchased", f"Sub: {subscription.name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error purchasing subscription: {e}")
|
||||
logger.error(f"Error purchasing subscription: {e}", exc_info=True)
|
||||
await callback.message.edit_text(
|
||||
t('purchase_error', user.language),
|
||||
"❌ Произошла ошибка при создании подписки. Если средства были списаны, обратитесь в поддержку.",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
# My subscriptions
|
||||
@router.callback_query(F.data == "my_subscriptions")
|
||||
async def my_subscriptions_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
"""Show user's subscriptions with URLs from API"""
|
||||
"""Show user's subscriptions with URLs from API - ИСПРАВЛЕНО: пометки для импортированных"""
|
||||
user = kwargs.get('user')
|
||||
api = kwargs.get('api')
|
||||
|
||||
@@ -814,7 +964,12 @@ async def my_subscriptions_callback(callback: CallbackQuery, db: Database, **kwa
|
||||
days_left = (user_sub.expires_at - now).days
|
||||
status = f"✅ Активна ({days_left} дн.)"
|
||||
|
||||
text += f"{i}. {subscription.name}\n"
|
||||
# НОВОЕ: Помечаем импортированные подписки
|
||||
subscription_name = subscription.name
|
||||
if subscription.is_imported or subscription.name == "Старая подписка":
|
||||
subscription_name += " 🔄" # Добавляем иконку импорта
|
||||
|
||||
text += f"{i}. {subscription_name}\n"
|
||||
text += f" {status}\n"
|
||||
text += f" До: {format_date(user_sub.expires_at, user.language)}\n"
|
||||
|
||||
@@ -832,14 +987,22 @@ async def my_subscriptions_callback(callback: CallbackQuery, db: Database, **kwa
|
||||
|
||||
text += "\n"
|
||||
|
||||
# НОВОЕ: Добавляем пояснение об иконках
|
||||
text += "ℹ️ 🔄 - импортированная из старой системы\n"
|
||||
text += " (продление недоступно)\n\n"
|
||||
|
||||
# Convert to old format for keyboard
|
||||
sub_list = []
|
||||
for user_sub in user_subs:
|
||||
subscription = await db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if subscription:
|
||||
display_name = subscription.name
|
||||
if subscription.is_imported or subscription.name == "Старая подписка":
|
||||
display_name += " 🔄"
|
||||
|
||||
sub_list.append({
|
||||
'id': user_sub.id,
|
||||
'name': subscription.name
|
||||
'name': display_name
|
||||
})
|
||||
|
||||
await callback.message.edit_text(
|
||||
@@ -855,7 +1018,7 @@ async def my_subscriptions_callback(callback: CallbackQuery, db: Database, **kwa
|
||||
|
||||
@router.callback_query(F.data.startswith("view_sub_"))
|
||||
async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwargs):
|
||||
"""View subscription details with URL from API"""
|
||||
"""View subscription details with URL from API - ИСПРАВЛЕНО: блокировка продления импортированных подписок"""
|
||||
user = kwargs.get('user')
|
||||
api = kwargs.get('api')
|
||||
|
||||
@@ -890,9 +1053,16 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
|
||||
now = datetime.utcnow()
|
||||
days_until_expiry = (user_sub.expires_at - now).days
|
||||
|
||||
# ИСПРАВЛЕНО: Проверяем является ли подписка импортированной
|
||||
is_imported = subscription.is_imported or subscription.price == 0
|
||||
is_trial = subscription.is_trial
|
||||
|
||||
# Показываем кнопку продления только для обычных платных подписок
|
||||
show_extend = (0 <= days_until_expiry <= 3 and
|
||||
user_sub.is_active and
|
||||
not subscription.is_trial)
|
||||
not is_trial and
|
||||
not is_imported and # НОВОЕ: блокируем продление импортированных
|
||||
subscription.price > 0) # НОВОЕ: блокируем продление бесплатных
|
||||
|
||||
text = format_user_subscription_info(user_sub.__dict__, sub_dict, user_sub.expires_at, user.language)
|
||||
|
||||
@@ -905,15 +1075,20 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get subscription URL: {e}")
|
||||
|
||||
# Add expiry warning if subscription expires soon
|
||||
if show_extend:
|
||||
# ИСПРАВЛЕНО: Разные сообщения для разных типов подписок
|
||||
if is_imported and 0 <= days_until_expiry <= 3:
|
||||
text += f"\n\n⚠️ Это импортированная подписка из старой системы.\n"
|
||||
text += f"📅 Истекает через {days_until_expiry} дн.\n"
|
||||
text += f"🛒 Для продолжения работы приобретите новый тарифный план."
|
||||
elif is_trial and 0 <= days_until_expiry <= 3:
|
||||
text += f"\n\nℹ️ Тестовая подписка истекает через {days_until_expiry} дн.\n"
|
||||
text += f"🛒 Для продолжения работы приобретите полный тарифный план."
|
||||
elif show_extend:
|
||||
text += f"\n\n⚠️ {t('subscription_expires_soon', user.language, days=days_until_expiry)}"
|
||||
elif subscription.is_trial and 0 <= days_until_expiry <= 3:
|
||||
text += f"\n\nℹ️ Тестовая подписка истекает через {days_until_expiry} дн. Продление недоступно."
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=user_subscription_detail_keyboard(user_sub_id, user.language, show_extend),
|
||||
reply_markup=user_subscription_detail_keyboard(user_sub_id, user.language, show_extend, is_imported),
|
||||
parse_mode='HTML',
|
||||
disable_web_page_preview=True
|
||||
)
|
||||
@@ -924,7 +1099,6 @@ async def view_subscription_detail(callback: CallbackQuery, db: Database, **kwar
|
||||
|
||||
@router.callback_query(F.data.startswith("extend_sub_"))
|
||||
async def extend_subscription_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
"""Show subscription extension confirmation"""
|
||||
user = kwargs.get('user')
|
||||
|
||||
if not user:
|
||||
@@ -946,10 +1120,23 @@ async def extend_subscription_callback(callback: CallbackQuery, db: Database, **
|
||||
await callback.answer(t('subscription_not_found', user.language))
|
||||
return
|
||||
|
||||
# НОВОЕ: Проверяем тип подписки
|
||||
if subscription.is_trial:
|
||||
await callback.answer("❌ Тестовую подписку нельзя продлить")
|
||||
return
|
||||
|
||||
if subscription.is_imported or subscription.price == 0:
|
||||
await callback.message.edit_text(
|
||||
"🚫 Импортированные подписки нельзя продлить\n\n"
|
||||
"Эта подписка была перенесена из старой системы.\n"
|
||||
"После истечения срока действия приобретите новый тарифный план.",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🛒 Купить новую подписку", callback_data="buy_subscription")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data=f"view_sub_{user_sub_id}")]
|
||||
])
|
||||
)
|
||||
return
|
||||
|
||||
if user.balance < subscription.price:
|
||||
needed = subscription.price - user.balance
|
||||
text = f"❌ Недостаточно средств для продления!\n\n"
|
||||
@@ -1218,7 +1405,7 @@ async def promocode_callback(callback: CallbackQuery, state: FSMContext, **kwarg
|
||||
|
||||
@router.message(StateFilter(BotStates.waiting_promocode))
|
||||
async def handle_promocode(message: Message, state: FSMContext, db: Database, **kwargs):
|
||||
"""Handle promocode input"""
|
||||
"""Handle promocode input with referral support and mutual protection"""
|
||||
user = kwargs.get('user')
|
||||
if not user:
|
||||
await message.answer("❌ Ошибка пользователя")
|
||||
@@ -1231,54 +1418,95 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
|
||||
return
|
||||
|
||||
try:
|
||||
# Сначала проверяем обычные промокоды
|
||||
promocode = await db.get_promocode_by_code(code)
|
||||
|
||||
if not promocode:
|
||||
await message.answer(t('promocode_not_found', user.language))
|
||||
if promocode and promocode.is_active:
|
||||
# Проверяем условия промокода
|
||||
if promocode.expires_at and promocode.expires_at < datetime.utcnow():
|
||||
await message.answer(t('promocode_expired', user.language))
|
||||
return
|
||||
|
||||
if promocode.used_count >= promocode.usage_limit:
|
||||
await message.answer(t('promocode_limit', user.language))
|
||||
return
|
||||
|
||||
# Используем промокод
|
||||
success = await db.use_promocode(user.telegram_id, promocode)
|
||||
|
||||
if not success:
|
||||
await message.answer(t('promocode_used', user.language))
|
||||
return
|
||||
|
||||
# Добавляем на баланс
|
||||
await db.add_balance(user.telegram_id, promocode.discount_amount)
|
||||
|
||||
# Создаем запись о платеже
|
||||
await db.create_payment(
|
||||
user_id=user.telegram_id,
|
||||
amount=promocode.discount_amount,
|
||||
payment_type='promocode',
|
||||
description=f'Промокод: {code}',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
discount_text = f"{promocode.discount_amount} руб."
|
||||
await message.answer(
|
||||
t('promocode_success', user.language, discount=discount_text),
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
log_user_action(user.telegram_id, "promocode_used", code)
|
||||
return
|
||||
|
||||
# Check if promocode is active
|
||||
if not promocode.is_active:
|
||||
await message.answer(t('promocode_not_found', user.language))
|
||||
return
|
||||
# Если обычный промокод не найден, проверяем реферальные коды
|
||||
if code.startswith("REF"):
|
||||
bot = kwargs.get('bot')
|
||||
|
||||
# Сначала проверяем на взаимное реферальство
|
||||
async with db.session_factory() as session:
|
||||
from sqlalchemy import select
|
||||
result = await session.execute(
|
||||
select(ReferralProgram).where(ReferralProgram.referral_code == code)
|
||||
)
|
||||
referral_record = result.scalar_one_or_none()
|
||||
|
||||
if referral_record:
|
||||
referrer_id = referral_record.referrer_id
|
||||
|
||||
# Проверяем взаимное реферальство
|
||||
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(
|
||||
"❌ Нельзя использовать код человека, которого вы пригласили!\n\n"
|
||||
"Взаимные рефералы не допускаются."
|
||||
)
|
||||
return
|
||||
|
||||
success = await create_referral_from_promocode(user.telegram_id, code, db, bot)
|
||||
|
||||
if success:
|
||||
await message.answer(
|
||||
"🎉 Реферальный код активирован!\n\n"
|
||||
"После пополнения баланса на 200₽ вы получите бонус 150₽!",
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
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("❌ Вы уже использовали реферальный код!")
|
||||
else:
|
||||
await message.answer("❌ Неверный реферальный код!")
|
||||
return
|
||||
|
||||
# Check expiry
|
||||
if promocode.expires_at and promocode.expires_at < datetime.utcnow():
|
||||
await message.answer(t('promocode_expired', user.language))
|
||||
return
|
||||
|
||||
# Check usage limit
|
||||
if promocode.used_count >= promocode.usage_limit:
|
||||
await message.answer(t('promocode_limit', user.language))
|
||||
return
|
||||
|
||||
# Check if user already used this promocode
|
||||
success = await db.use_promocode(user.telegram_id, promocode)
|
||||
|
||||
if not success:
|
||||
await message.answer(t('promocode_used', user.language))
|
||||
return
|
||||
|
||||
# Add to balance
|
||||
await db.add_balance(user.telegram_id, promocode.discount_amount)
|
||||
|
||||
# Create payment record
|
||||
await db.create_payment(
|
||||
user_id=user.telegram_id,
|
||||
amount=promocode.discount_amount,
|
||||
payment_type='promocode',
|
||||
description=f'Промокод: {code}',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
discount_text = f"{promocode.discount_amount} руб."
|
||||
await message.answer(
|
||||
t('promocode_success', user.language, discount=discount_text),
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
log_user_action(user.telegram_id, "promocode_used", code)
|
||||
# Если ничего не найдено
|
||||
await message.answer(t('promocode_not_found', user.language))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling promocode: {e}")
|
||||
@@ -1286,4 +1514,208 @@ async def handle_promocode(message: Message, state: FSMContext, db: Database, **
|
||||
t('error_occurred', user.language),
|
||||
reply_markup=main_menu_keyboard(user.language, user.is_admin)
|
||||
)
|
||||
await state.clear()
|
||||
|
||||
await state.clear()
|
||||
|
||||
@router.callback_query(F.data == "referral_program")
|
||||
async def referral_program_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
"""Show referral program info - ФИНАЛЬНАЯ ВЕРСИЯ с конфигурацией"""
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
# Получаем статистику пользователя
|
||||
stats = await db.get_user_referral_stats(user.telegram_id)
|
||||
|
||||
referral_code = await get_or_create_referral_code(user.telegram_id, db)
|
||||
|
||||
# Создаем реферальную ссылку
|
||||
bot_username = config.BOT_USERNAME if config and config.BOT_USERNAME else ""
|
||||
referral_link = ""
|
||||
if bot_username:
|
||||
referral_link = f"https://t.me/{bot_username}?start=ref_{user.telegram_id}"
|
||||
|
||||
# Добавляем текущее время для обновления
|
||||
from datetime import datetime
|
||||
current_time = datetime.now().strftime("%H:%M")
|
||||
|
||||
text = "🎁 **Реферальная программа**\n\n"
|
||||
|
||||
# Условия программы - ИСПРАВЛЕНО: используем значения из конфигурации
|
||||
text += "**📋 Условия программы:**\n"
|
||||
|
||||
# Получаем значения из конфигурации с fallback
|
||||
first_reward = config.REFERRAL_FIRST_REWARD if config else 150.0
|
||||
referred_bonus = config.REFERRAL_REFERRED_BONUS if config else 150.0
|
||||
threshold = config.REFERRAL_THRESHOLD if config else 300.0
|
||||
percentage = config.REFERRAL_PERCENTAGE if config else 0.25
|
||||
|
||||
text += f"• Приведи друга и получи **{first_reward:.0f}₽** на баланс\n"
|
||||
text += f"• Твой друг получит **{referred_bonus:.0f}₽** после пополнения на {threshold:.0f}₽\n"
|
||||
text += f"• С каждого платежа друга ты получаешь **{percentage*100:.0f}%**\n\n"
|
||||
|
||||
# Статистика пользователя
|
||||
text += "**📊 Твоя статистика:**\n"
|
||||
text += f"• Приглашено: {stats['total_referrals']} человек\n"
|
||||
text += f"• Активных рефералов: {stats['active_referrals']}\n"
|
||||
text += f"• Заработано всего: {stats['total_earned']:.2f}₽\n\n"
|
||||
|
||||
# Ссылка и промокод
|
||||
if referral_link:
|
||||
text += "**🔗 Твоя реферальная ссылка:**\n"
|
||||
text += f"`{referral_link}`\n\n"
|
||||
else:
|
||||
text += "⚠️ Реферальная ссылка недоступна (не установлен BOT_USERNAME)\n\n"
|
||||
|
||||
text += f"**🎫 Твой промокод:** `{referral_code}`\n\n"
|
||||
text += "Отправь ссылку или промокод друзьям!"
|
||||
|
||||
# Добавляем время обновления чтобы избежать ошибки "message is not modified"
|
||||
text += f"\n\n🕐 _Обновлено: {current_time}_"
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📊 Мои рефералы", callback_data="my_referrals")],
|
||||
[InlineKeyboardButton(text="🔄 Обновить статистику", callback_data="referral_program")],
|
||||
[InlineKeyboardButton(text="🔙 Назад", callback_data="main_menu")]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing referral program: {e}")
|
||||
# Если не можем отредактировать сообщение, просто отвечаем на callback
|
||||
try:
|
||||
await callback.answer("✅ Статистика обновлена", show_alert=False)
|
||||
except:
|
||||
pass
|
||||
|
||||
async def get_or_create_referral_code(user_id: int, db: Database) -> str:
|
||||
"""Get existing referral code or create new one - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
|
||||
try:
|
||||
async with db.session_factory() as session:
|
||||
from sqlalchemy import select, text
|
||||
|
||||
# Ищем код в любой записи где пользователь - реферер
|
||||
result = await session.execute(
|
||||
text("SELECT referral_code FROM referral_programs WHERE referrer_id = :user_id LIMIT 1"),
|
||||
{"user_id": user_id}
|
||||
)
|
||||
|
||||
existing_code = result.scalar_one_or_none()
|
||||
|
||||
if existing_code:
|
||||
logger.info(f"Found existing referral code {existing_code} for user {user_id}")
|
||||
return existing_code
|
||||
|
||||
# Если кода нет, генерируем новый
|
||||
referral_code = await db.generate_unique_referral_code(user_id)
|
||||
|
||||
# Создаем запись для сохранения кода
|
||||
referral = await db.create_referral(user_id, 0, referral_code)
|
||||
|
||||
if referral:
|
||||
logger.info(f"Created new referral code {referral_code} for user {user_id}")
|
||||
return referral_code
|
||||
else:
|
||||
logger.warning(f"Failed to create referral code for user {user_id}")
|
||||
return f"REF{user_id}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting/creating referral code for user {user_id}: {e}")
|
||||
return f"REF{user_id}"
|
||||
|
||||
@router.callback_query(F.data == "my_referrals")
|
||||
async def my_referrals_callback(callback: CallbackQuery, db: Database, **kwargs):
|
||||
user = kwargs.get('user')
|
||||
config = kwargs.get('config')
|
||||
|
||||
if not user:
|
||||
await callback.answer("❌ Ошибка пользователя")
|
||||
return
|
||||
|
||||
try:
|
||||
referrals = await db.get_user_referrals(user.telegram_id)
|
||||
|
||||
placeholder_id = 999999999 - user.telegram_id
|
||||
real_referrals = []
|
||||
|
||||
for referral in referrals:
|
||||
# Пропускаем только конкретный placeholder этого пользователя
|
||||
if referral.referred_id == placeholder_id or referral.referred_id == 0:
|
||||
continue
|
||||
|
||||
real_referrals.append(referral)
|
||||
|
||||
if not real_referrals:
|
||||
text = "👥 У вас пока нет рефералов\n\n"
|
||||
text += "Поделитесь своей реферальной ссылкой с друзьями!"
|
||||
else:
|
||||
text = f"👥 Ваши рефералы ({len(real_referrals)}):\n\n"
|
||||
|
||||
threshold = config.REFERRAL_THRESHOLD if config else 300.0
|
||||
|
||||
for i, referral in enumerate(real_referrals[:10], 1): # Показываем первых 10
|
||||
# Получаем полную информацию о реферале
|
||||
referred_user = await db.get_user_by_telegram_id(referral.referred_id)
|
||||
|
||||
if referred_user:
|
||||
# Формируем красивое отображение имени
|
||||
display_name = ""
|
||||
if referred_user.first_name:
|
||||
display_name = referred_user.first_name
|
||||
if referred_user.last_name:
|
||||
display_name += f" {referred_user.last_name}"
|
||||
|
||||
if referred_user.username:
|
||||
if display_name:
|
||||
display_name += f" (@{referred_user.username})"
|
||||
else:
|
||||
display_name = f"@{referred_user.username}"
|
||||
|
||||
if not display_name:
|
||||
display_name = f"Пользователь #{referred_user.telegram_id}"
|
||||
|
||||
else:
|
||||
# Если пользователь не найден в базе бота
|
||||
display_name = f"Пользователь ID:{referral.referred_id}"
|
||||
|
||||
# Статус реферала
|
||||
status_icon = "✅" if referral.first_reward_paid else "⏳"
|
||||
status_text = "Активен" if referral.first_reward_paid else "Ожидает активации"
|
||||
|
||||
# Сумма заработка
|
||||
earned_text = ""
|
||||
if referral.total_earned > 0:
|
||||
earned_text = f" (+{referral.total_earned:.0f}₽)"
|
||||
|
||||
text += f"{i}. {status_icon} {display_name}{earned_text}\n"
|
||||
text += f" 📅 Присоединился: {format_date(referral.created_at)}\n"
|
||||
text += f" 📊 Статус: {status_text}\n"
|
||||
|
||||
if referral.first_reward_paid and referral.first_reward_at:
|
||||
text += f" 💰 Первая награда: {format_date(referral.first_reward_at)}\n"
|
||||
elif not referral.first_reward_paid:
|
||||
text += f" ⏳ Нужно пополнить баланс на {threshold:.0f}₽\n"
|
||||
|
||||
text += "\n"
|
||||
|
||||
if len(real_referrals) > 10:
|
||||
text += f"... и еще {len(real_referrals) - 10} рефералов"
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔙 К программе", callback_data="referral_program")]
|
||||
])
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=keyboard)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error showing referrals: {e}")
|
||||
await callback.answer("❌ Ошибка загрузки")
|
||||
|
||||
+34
-11
@@ -31,9 +31,12 @@ def main_menu_keyboard(lang: str = 'ru', is_admin: bool = False, show_trial: boo
|
||||
|
||||
# Добавляем остальные кнопки
|
||||
buttons.extend([
|
||||
# Дополнительные функции
|
||||
# Реферальная программа и дополнительные функции
|
||||
[
|
||||
InlineKeyboardButton(text="🎁 " + t('promocode', lang), callback_data="promocode"),
|
||||
InlineKeyboardButton(text="👥 Рефералы", callback_data="referral_program") # НОВАЯ КНОПКА
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="💬 " + t('support', lang), callback_data="support")
|
||||
],
|
||||
# Последний ряд - настройки
|
||||
@@ -59,7 +62,7 @@ def balance_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
def topup_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Beautiful top up balance keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💳 " + t('topup_card', lang), callback_data="topup_card")],
|
||||
# [InlineKeyboardButton(text="💳 " + t('topup_card', lang), callback_data="topup_card")],
|
||||
[InlineKeyboardButton(text="👨💼 " + t('topup_support', lang), callback_data="topup_support")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="balance")]
|
||||
])
|
||||
@@ -122,16 +125,22 @@ 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) -> InlineKeyboardMarkup:
|
||||
"""Beautiful user's subscription detail keyboard with connection and optional extend button"""
|
||||
def user_subscription_detail_keyboard(subscription_id: int, lang: str = 'ru', show_extend: bool = False, is_imported: bool = False) -> InlineKeyboardMarkup:
|
||||
"""Beautiful user's subscription detail keyboard with connection and optional extend button - ИСПРАВЛЕНО"""
|
||||
buttons = []
|
||||
|
||||
# Add extend button if subscription is expiring soon
|
||||
if show_extend:
|
||||
buttons.append([InlineKeyboardButton(text="⏰ " + t('extend_subscription', lang), callback_data=f"extend_sub_{subscription_id}")])
|
||||
|
||||
# Connection button (главная кнопка)
|
||||
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
|
||||
# НОВОЕ: Разные кнопки в зависимости от типа подписки
|
||||
if is_imported:
|
||||
# Для импортированных подписок - только ссылка подключения и покупка новой
|
||||
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}")])
|
||||
|
||||
# Connection button (главная кнопка)
|
||||
buttons.append([InlineKeyboardButton(text="🔗 Получить ссылку подключения", callback_data=f"get_connection_{subscription_id}")])
|
||||
|
||||
# Back button
|
||||
buttons.append([InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="my_subscriptions")])
|
||||
@@ -180,7 +189,10 @@ def admin_menu_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
# Третий ряд - коммуникации и система
|
||||
[
|
||||
InlineKeyboardButton(text="📨 " + t('send_message', lang), callback_data="admin_messages"),
|
||||
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system") # НОВОЕ!
|
||||
InlineKeyboardButton(text="👥 Рефералы", callback_data="admin_referrals") # НОВАЯ КНОПКА
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(text="🖥 Система RemnaWave", callback_data="admin_system")
|
||||
],
|
||||
# Четвертый ряд - мониторинг и статистика
|
||||
[
|
||||
@@ -505,3 +517,14 @@ def confirm_restart_keyboard(node_id: str = None, lang: str = 'ru') -> InlineKey
|
||||
]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
def admin_referrals_keyboard(lang: str = 'ru') -> InlineKeyboardMarkup:
|
||||
"""Admin referral management keyboard"""
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📊 Статистика рефералов", callback_data="referral_statistics")],
|
||||
[InlineKeyboardButton(text="👥 Список рефереров", callback_data="list_referrers")],
|
||||
[InlineKeyboardButton(text="💰 История выплат", callback_data="referral_payments")],
|
||||
[InlineKeyboardButton(text="⚙️ Настройки программы", callback_data="referral_settings")],
|
||||
[InlineKeyboardButton(text="🔙 " + t('back', lang), callback_data="admin_panel")]
|
||||
])
|
||||
return keyboard
|
||||
|
||||
@@ -7,8 +7,20 @@ from aiogram.fsm.storage.memory import MemoryStorage
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.enums import ParseMode
|
||||
|
||||
# Import our modules
|
||||
from config import load_config
|
||||
# ДОБАВЛЯЕМ ДЕБАГ ЗАГРУЗКИ .env ПЕРЕД ИМПОРТОМ CONFIG
|
||||
print("🚀 Запуск бота...")
|
||||
print(f"📍 Рабочая директория: {os.getcwd()}")
|
||||
print(f"📁 Файлы в директории: {os.listdir('.')}")
|
||||
|
||||
# Проверяем наличие .env файла
|
||||
if os.path.exists('.env'):
|
||||
print("✅ Файл .env найден")
|
||||
else:
|
||||
print("❌ Файл .env НЕ НАЙДЕН!")
|
||||
print("💡 Создайте файл .env в корне проекта")
|
||||
|
||||
# Import our modules ПОСЛЕ проверки .env
|
||||
from config import load_config, debug_environment
|
||||
from database import Database
|
||||
from remnawave_api import RemnaWaveAPI
|
||||
from subscription_monitor import create_subscription_monitor
|
||||
@@ -40,9 +52,19 @@ class BotApplication:
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize all components"""
|
||||
|
||||
# ДЕБАГ: проверяем переменные окружения
|
||||
debug_environment()
|
||||
|
||||
# Load configuration
|
||||
self.config = load_config()
|
||||
|
||||
# ДЕБАГ: проверяем что загрузилось
|
||||
print(f"🔧 Загруженная конфигурация:")
|
||||
print(f" BOT_USERNAME: '{self.config.BOT_USERNAME}'")
|
||||
print(f" REFERRAL_FIRST_REWARD: {self.config.REFERRAL_FIRST_REWARD}")
|
||||
print(f" ADMIN_IDS: {self.config.ADMIN_IDS}")
|
||||
|
||||
# Validate required environment variables
|
||||
if not self.config.BOT_TOKEN:
|
||||
logger.error("BOT_TOKEN is required")
|
||||
@@ -52,9 +74,15 @@ class BotApplication:
|
||||
logger.error("REMNAWAVE_URL and REMNAWAVE_TOKEN are required")
|
||||
raise ValueError("REMNAWAVE_URL and REMNAWAVE_TOKEN are required")
|
||||
|
||||
# ПРЕДУПРЕЖДЕНИЕ если BOT_USERNAME не установлен
|
||||
if not self.config.BOT_USERNAME:
|
||||
logger.warning("⚠️ BOT_USERNAME не установлен! Реферальные ссылки работать не будут!")
|
||||
print("💡 Добавьте BOT_USERNAME=your_bot_username в .env файл")
|
||||
|
||||
logger.info("Starting RemnaWave Bot...")
|
||||
logger.info(f"RemnaWave URL: {self.config.REMNAWAVE_URL}")
|
||||
logger.info(f"Admin IDs: {self.config.ADMIN_IDS}")
|
||||
logger.info(f"Bot Username: {self.config.BOT_USERNAME}")
|
||||
|
||||
# Initialize database
|
||||
self.db = Database(self.config.DATABASE_URL)
|
||||
@@ -117,6 +145,13 @@ class BotApplication:
|
||||
try:
|
||||
bot_info = await self.bot.get_me()
|
||||
logger.info(f"Bot started: @{bot_info.username} ({bot_info.first_name})")
|
||||
|
||||
# АВТОМАТИЧЕСКИ обновляем BOT_USERNAME если он не был установлен
|
||||
if not self.config.BOT_USERNAME and bot_info.username:
|
||||
self.config.BOT_USERNAME = bot_info.username
|
||||
logger.info(f"✅ BOT_USERNAME автоматически установлен: {bot_info.username}")
|
||||
print("💡 Добавьте BOT_USERNAME в .env файл для постоянного сохранения")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Invalid bot token or network error: {e}")
|
||||
raise
|
||||
@@ -180,6 +215,13 @@ class BotApplication:
|
||||
async def start(self):
|
||||
"""Start bot polling"""
|
||||
logger.info("Bot polling started successfully")
|
||||
|
||||
# ФИНАЛЬНАЯ ПРОВЕРКА конфигурации
|
||||
if self.config.BOT_USERNAME:
|
||||
logger.info(f"🎁 Реферальная система активна! Ссылки: https://t.me/{self.config.BOT_USERNAME}?start=ref_USERID")
|
||||
else:
|
||||
logger.warning("⚠️ Реферальная система неактивна! Установите BOT_USERNAME")
|
||||
|
||||
try:
|
||||
await self.dp.start_polling(self.bot)
|
||||
except Exception as e:
|
||||
|
||||
+5
-5
@@ -23,7 +23,7 @@ class DatabaseMiddleware(BaseMiddleware):
|
||||
return await handler(event, data)
|
||||
|
||||
class UserMiddleware(BaseMiddleware):
|
||||
"""Middleware for user management"""
|
||||
"""Middleware for user management - ИСПРАВЛЕНО: правильная инициализация языка"""
|
||||
|
||||
def __init__(self, db: Database, config: Config):
|
||||
self.db = db
|
||||
@@ -46,16 +46,16 @@ class UserMiddleware(BaseMiddleware):
|
||||
# Check if user is admin
|
||||
is_admin = telegram_user.id in self.config.ADMIN_IDS
|
||||
|
||||
# Create new user
|
||||
# ИСПРАВЛЕНО: Создаем пользователя с пустым языком для первоначальной настройки
|
||||
user = await self.db.create_user(
|
||||
telegram_id=telegram_user.id,
|
||||
username=telegram_user.username,
|
||||
first_name=telegram_user.first_name,
|
||||
last_name=telegram_user.last_name,
|
||||
language=self.config.DEFAULT_LANGUAGE,
|
||||
language='', # ИЗМЕНЕНО: пустой язык для нового пользователя
|
||||
is_admin=is_admin
|
||||
)
|
||||
logger.info(f"Created new user: {telegram_user.id}")
|
||||
logger.info(f"Created new user: {telegram_user.id} without language")
|
||||
else:
|
||||
# Update user info if changed
|
||||
updated = False
|
||||
@@ -79,7 +79,7 @@ class UserMiddleware(BaseMiddleware):
|
||||
await self.db.update_user(user)
|
||||
|
||||
data['user'] = user
|
||||
data['lang'] = user.language if user else self.config.DEFAULT_LANGUAGE
|
||||
data['lang'] = user.language if user.language and user.language != '' else self.config.DEFAULT_LANGUAGE
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in UserMiddleware: {e}")
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@ class RemnaWaveAPI:
|
||||
def __init__(self, base_url: str, token: str, subscription_base_url: str = None):
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.token = token
|
||||
self.subscription_base_url = subscription_base_url
|
||||
# УДАЛЯЕМ зависимость от subscription_base_url - теперь берем из API
|
||||
self.subscription_base_url = subscription_base_url # Оставляем для обратной совместимости
|
||||
self.session = None
|
||||
|
||||
async def _get_session(self):
|
||||
|
||||
+224
-418
@@ -1,503 +1,309 @@
|
||||
"""
|
||||
Subscription Monitor Service
|
||||
Сервис для мониторинга подписок, уведомлений пользователей и предложений продления
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional
|
||||
from dataclasses import dataclass
|
||||
import traceback
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
from database import Database, UserSubscription, Subscription, User
|
||||
from typing import Optional, List
|
||||
from database import Database
|
||||
from remnawave_api import RemnaWaveAPI
|
||||
from translations import t
|
||||
from keyboards import extend_subscription_keyboard, main_menu_keyboard
|
||||
from utils import format_datetime, log_user_action
|
||||
from config import Config
|
||||
|
||||
# Настройка логирования
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@dataclass
|
||||
class NotificationResult:
|
||||
"""Результат отправки уведомления"""
|
||||
success: bool
|
||||
user_id: int
|
||||
message: str
|
||||
error: Optional[str] = None
|
||||
|
||||
class SubscriptionMonitorService:
|
||||
"""Сервис мониторинга подписок"""
|
||||
"""Service for monitoring subscriptions and sending notifications"""
|
||||
|
||||
def __init__(self, bot: Bot, db: Database, config: Config, api: Optional[RemnaWaveAPI] = None):
|
||||
def __init__(self, bot, db: Database, config, api: RemnaWaveAPI = None):
|
||||
self.bot = bot
|
||||
self.db = db
|
||||
self.config = config
|
||||
self.api = api
|
||||
self.is_running = False
|
||||
self._monitor_task: Optional[asyncio.Task] = None
|
||||
|
||||
# Настройки уведомлений из конфига
|
||||
self.WARNING_DAYS = config.MONITOR_WARNING_DAYS # За сколько дней предупреждать
|
||||
self.CHECK_INTERVAL = config.MONITOR_CHECK_INTERVAL # Интервал проверки (в секундах)
|
||||
self.DAILY_CHECK_HOUR = config.MONITOR_DAILY_CHECK_HOUR # В какой час дня делать основную
|
||||
self._monitor_task = None
|
||||
self._daily_task = None
|
||||
|
||||
async def start(self):
|
||||
"""Запуск сервиса мониторинга"""
|
||||
"""Start the monitoring service"""
|
||||
if self.is_running:
|
||||
logger.warning("Subscription monitor service is already running")
|
||||
logger.warning("Monitor service is already running")
|
||||
return
|
||||
|
||||
|
||||
self.is_running = True
|
||||
|
||||
# Start periodic monitoring task
|
||||
self._monitor_task = asyncio.create_task(self._monitor_loop())
|
||||
|
||||
# Start daily cleanup task
|
||||
self._daily_task = asyncio.create_task(self._daily_loop())
|
||||
|
||||
logger.info("Subscription monitor service started")
|
||||
|
||||
async def stop(self):
|
||||
"""Остановка сервиса мониторинга"""
|
||||
"""Stop the monitoring service"""
|
||||
if not self.is_running:
|
||||
return
|
||||
|
||||
|
||||
self.is_running = False
|
||||
|
||||
# Cancel tasks
|
||||
if self._monitor_task:
|
||||
self._monitor_task.cancel()
|
||||
try:
|
||||
await self._monitor_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
|
||||
if self._daily_task:
|
||||
self._daily_task.cancel()
|
||||
try:
|
||||
await self._daily_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
logger.info("Subscription monitor service stopped")
|
||||
|
||||
async def _monitor_loop(self):
|
||||
"""Основной цикл мониторинга"""
|
||||
logger.info(f"Starting monitor loop with {self.CHECK_INTERVAL}s interval")
|
||||
|
||||
"""Main monitoring loop"""
|
||||
while self.is_running:
|
||||
try:
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
# Основная проверка раз в день в определенное время
|
||||
if current_time.hour == self.DAILY_CHECK_HOUR:
|
||||
await self._daily_check()
|
||||
|
||||
# Дополнительная проверка каждый час для критических случаев
|
||||
await self._hourly_check()
|
||||
|
||||
# Ожидание до следующей проверки
|
||||
await asyncio.sleep(self.CHECK_INTERVAL)
|
||||
|
||||
await self._check_expiring_subscriptions()
|
||||
await asyncio.sleep(self.config.MONITOR_CHECK_INTERVAL)
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Monitor loop cancelled")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error in monitor loop: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
# Продолжаем работу даже при ошибках
|
||||
await asyncio.sleep(60) # Короткая пауза при ошибке
|
||||
await asyncio.sleep(60) # Wait 1 minute before retry
|
||||
|
||||
async def _daily_check(self):
|
||||
"""Ежедневная проверка всех подписок"""
|
||||
logger.info("Starting daily subscription check")
|
||||
|
||||
async def _daily_loop(self):
|
||||
"""Daily cleanup loop"""
|
||||
while self.is_running:
|
||||
try:
|
||||
now = datetime.now()
|
||||
|
||||
# Wait until the specified hour
|
||||
if now.hour == self.config.MONITOR_DAILY_CHECK_HOUR:
|
||||
await self.force_daily_check()
|
||||
|
||||
# Wait until next day
|
||||
tomorrow = now.replace(hour=self.config.MONITOR_DAILY_CHECK_HOUR, minute=0, second=0, microsecond=0) + timedelta(days=1)
|
||||
sleep_time = (tomorrow - now).total_seconds()
|
||||
else:
|
||||
# Calculate time until next check
|
||||
target_time = now.replace(hour=self.config.MONITOR_DAILY_CHECK_HOUR, minute=0, second=0, microsecond=0)
|
||||
if target_time < now:
|
||||
target_time += timedelta(days=1)
|
||||
sleep_time = (target_time - now).total_seconds()
|
||||
|
||||
await asyncio.sleep(min(sleep_time, 3600)) # Check at least every hour
|
||||
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error in daily loop: {e}")
|
||||
await asyncio.sleep(3600) # Wait 1 hour before retry
|
||||
|
||||
async def _check_expiring_subscriptions(self):
|
||||
"""Check for expiring subscriptions and send warnings"""
|
||||
try:
|
||||
# Получаем все активные подписки пользователей
|
||||
# Get all users
|
||||
all_users = await self.db.get_all_users()
|
||||
total_notifications = 0
|
||||
|
||||
for user in all_users:
|
||||
try:
|
||||
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
|
||||
active_subs = [sub for sub in user_subs if sub.is_active]
|
||||
# Get expiring subscriptions for this user
|
||||
expiring_subs = await self.db.get_expiring_subscriptions(
|
||||
user.telegram_id,
|
||||
self.config.MONITOR_WARNING_DAYS
|
||||
)
|
||||
|
||||
for user_sub in active_subs:
|
||||
# Проверяем каждую подписку
|
||||
notification_sent = await self._check_and_notify_subscription(user, user_sub)
|
||||
if notification_sent:
|
||||
total_notifications += 1
|
||||
|
||||
# Небольшая пауза между уведомлениями
|
||||
await asyncio.sleep(0.1)
|
||||
for user_sub in expiring_subs:
|
||||
await self._send_expiry_warning(user, user_sub)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking subscriptions for user {user.telegram_id}: {e}")
|
||||
continue
|
||||
|
||||
logger.info(f"Daily check completed. Sent {total_notifications} notifications")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in daily check: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
async def _hourly_check(self):
|
||||
"""Часовая проверка критических подписок (истекают сегодня)"""
|
||||
try:
|
||||
now = datetime.utcnow()
|
||||
tomorrow = now + timedelta(days=1)
|
||||
|
||||
# Получаем подписки, которые истекают в ближайшие 24 часа
|
||||
all_users = await self.db.get_all_users()
|
||||
|
||||
for user in all_users:
|
||||
try:
|
||||
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
|
||||
|
||||
for user_sub in user_subs:
|
||||
if (user_sub.is_active and
|
||||
user_sub.expires_at <= tomorrow and
|
||||
user_sub.expires_at > now):
|
||||
|
||||
await self._check_and_notify_subscription(user, user_sub, urgent=True)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in hourly check for user {user.telegram_id}: {e}")
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in hourly check: {e}")
|
||||
logger.error(f"Error in check_expiring_subscriptions: {e}")
|
||||
|
||||
async def _check_and_notify_subscription(self, user: User, user_sub: UserSubscription, urgent: bool = False) -> bool:
|
||||
"""
|
||||
Проверить подписку и отправить уведомление если нужно
|
||||
Returns: True если уведомление было отправлено
|
||||
"""
|
||||
async def _send_expiry_warning(self, user, user_subscription):
|
||||
"""Send expiry warning to user"""
|
||||
try:
|
||||
now = datetime.utcnow()
|
||||
days_until_expiry = (user_sub.expires_at - now).days
|
||||
hours_until_expiry = (user_sub.expires_at - now).total_seconds() / 3600
|
||||
days_left = (user_subscription.expires_at - datetime.utcnow()).days
|
||||
|
||||
# Получаем информацию о подписке
|
||||
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
|
||||
subscription = await self.db.get_subscription_by_id(user_subscription.subscription_id)
|
||||
if not subscription:
|
||||
logger.warning(f"Subscription {user_sub.subscription_id} not found")
|
||||
return False
|
||||
return
|
||||
|
||||
notification_type = None
|
||||
# Don't send warnings for trial subscriptions
|
||||
if subscription.is_trial:
|
||||
return
|
||||
|
||||
# Определяем тип уведомления
|
||||
if user_sub.expires_at <= now:
|
||||
# Подписка истекла
|
||||
notification_type = "expired"
|
||||
elif days_until_expiry <= 0 and hours_until_expiry <= 24:
|
||||
# Истекает сегодня
|
||||
notification_type = "expires_today"
|
||||
elif days_until_expiry == 1:
|
||||
# Истекает завтра
|
||||
notification_type = "expires_tomorrow"
|
||||
elif days_until_expiry == self.WARNING_DAYS:
|
||||
# Предупреждение за 2 дня
|
||||
notification_type = "warning"
|
||||
elif urgent and days_until_expiry <= 1:
|
||||
# Срочное уведомление
|
||||
notification_type = "urgent"
|
||||
message = self._format_expiry_message(subscription.name, days_left, user.language)
|
||||
|
||||
if notification_type:
|
||||
return await self._send_notification(user, user_sub, subscription, notification_type)
|
||||
|
||||
return False
|
||||
await self.bot.send_message(user.telegram_id, message)
|
||||
logger.info(f"Sent expiry warning to user {user.telegram_id} for subscription {subscription.name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking subscription {user_sub.id}: {e}")
|
||||
return False
|
||||
logger.error(f"Error sending expiry warning: {e}")
|
||||
|
||||
async def _send_notification(self, user: User, user_sub: UserSubscription,
|
||||
subscription: Subscription, notification_type: str) -> bool:
|
||||
"""Отправить уведомление пользователю"""
|
||||
try:
|
||||
# Проверяем, не является ли подписка тестовой (для тестовых другая логика)
|
||||
if subscription.is_trial and notification_type in ["warning", "expires_tomorrow"]:
|
||||
# Для тестовых подписок не предлагаем продление
|
||||
return await self._send_trial_expiry_notification(user, user_sub, subscription, notification_type)
|
||||
|
||||
# Формируем текст уведомления
|
||||
message_text = self._format_notification_message(user, user_sub, subscription, notification_type)
|
||||
|
||||
# Формируем клавиатуру
|
||||
keyboard = self._create_notification_keyboard(user, user_sub, subscription, notification_type)
|
||||
|
||||
# Отправляем уведомление
|
||||
await self.bot.send_message(
|
||||
chat_id=user.telegram_id,
|
||||
text=message_text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
# Логируем действие
|
||||
log_user_action(user.telegram_id, f"notification_sent_{notification_type}", f"Sub: {subscription.name}")
|
||||
|
||||
logger.info(f"Sent {notification_type} notification to user {user.telegram_id} for subscription {subscription.name}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending notification to user {user.telegram_id}: {e}")
|
||||
return False
|
||||
|
||||
async def _send_trial_expiry_notification(self, user: User, user_sub: UserSubscription,
|
||||
subscription: Subscription, notification_type: str) -> bool:
|
||||
"""Отправить уведомление об истечении тестовой подписки"""
|
||||
try:
|
||||
now = datetime.utcnow()
|
||||
days_until_expiry = (user_sub.expires_at - now).days
|
||||
hours_until_expiry = (user_sub.expires_at - now).total_seconds() / 3600
|
||||
|
||||
if notification_type == "expires_today" or hours_until_expiry <= 24:
|
||||
message_text = (
|
||||
f"⏰ *Ваша тестовая подписка истекает сегодня!*\n\n"
|
||||
f"📋 Подписка: *{subscription.name}*\n"
|
||||
f"⏳ Осталось: *{int(hours_until_expiry)} часов*\n\n"
|
||||
f"💡 Чтобы продолжить пользоваться сервисом, приобретите полную подписку!"
|
||||
)
|
||||
elif notification_type == "expires_tomorrow" or days_until_expiry == 1:
|
||||
message_text = (
|
||||
f"⚠️ *Ваша тестовая подписка истекает завтра!*\n\n"
|
||||
f"📋 Подписка: *{subscription.name}*\n"
|
||||
f"📅 Истекает: *{format_datetime(user_sub.expires_at, user.language)}*\n\n"
|
||||
f"💡 Не забудьте приобрести полную подписку, чтобы продолжить пользоваться сервисом!"
|
||||
)
|
||||
def _format_expiry_message(self, subscription_name: str, days_left: int, language: str = 'ru') -> str:
|
||||
"""Format expiry warning message"""
|
||||
if language == 'ru':
|
||||
if days_left <= 0:
|
||||
return f"⚠️ Ваша подписка '{subscription_name}' истекла!\n\nДля продления перейдите в раздел 'Мои подписки'."
|
||||
elif days_left == 1:
|
||||
return f"⚠️ Ваша подписка '{subscription_name}' истекает завтра!\n\nНе забудьте продлить её в разделе 'Мои подписки'."
|
||||
else:
|
||||
return False
|
||||
|
||||
# Клавиатура для тестовой подписки
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(
|
||||
text="💳 Купить подписку",
|
||||
callback_data="buy_subscription"
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text="💰 Пополнить баланс",
|
||||
callback_data="topup_balance"
|
||||
)],
|
||||
[InlineKeyboardButton(
|
||||
text="🏠 Главное меню",
|
||||
callback_data="main_menu"
|
||||
)]
|
||||
])
|
||||
|
||||
await self.bot.send_message(
|
||||
chat_id=user.telegram_id,
|
||||
text=message_text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode='Markdown'
|
||||
)
|
||||
|
||||
log_user_action(user.telegram_id, f"trial_notification_sent_{notification_type}", f"Sub: {subscription.name}")
|
||||
logger.info(f"Sent trial {notification_type} notification to user {user.telegram_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending trial notification to user {user.telegram_id}: {e}")
|
||||
return False
|
||||
|
||||
def _format_notification_message(self, user: User, user_sub: UserSubscription,
|
||||
subscription: Subscription, notification_type: str) -> str:
|
||||
"""Форматировать текст уведомления"""
|
||||
now = datetime.utcnow()
|
||||
days_until_expiry = (user_sub.expires_at - now).days
|
||||
hours_until_expiry = (user_sub.expires_at - now).total_seconds() / 3600
|
||||
|
||||
base_info = (
|
||||
f"📋 Подписка: *{subscription.name}*\n"
|
||||
f"📅 Истекает: *{format_datetime(user_sub.expires_at, user.language)}*\n"
|
||||
f"💰 Цена продления: *{subscription.price} руб.*"
|
||||
)
|
||||
|
||||
if notification_type == "expired":
|
||||
return (
|
||||
f"❌ *Ваша подписка истекла!*\n\n"
|
||||
f"{base_info}\n\n"
|
||||
f"🔄 Продлите подписку, чтобы продолжить пользоваться сервисом."
|
||||
)
|
||||
elif notification_type == "expires_today" or notification_type == "urgent":
|
||||
return (
|
||||
f"⏰ *Ваша подписка истекает сегодня!*\n\n"
|
||||
f"{base_info}\n"
|
||||
f"⏳ Осталось: *{int(hours_until_expiry)} часов*\n\n"
|
||||
f"🔄 Продлите подписку прямо сейчас!"
|
||||
)
|
||||
elif notification_type == "expires_tomorrow":
|
||||
return (
|
||||
f"⚠️ *Ваша подписка истекает завтра!*\n\n"
|
||||
f"{base_info}\n\n"
|
||||
f"🔄 Рекомендуем продлить подписку заранее."
|
||||
)
|
||||
elif notification_type == "warning":
|
||||
return (
|
||||
f"📢 *Напоминание о подписке*\n\n"
|
||||
f"{base_info}\n"
|
||||
f"⏳ Осталось: *{days_until_expiry} дней*\n\n"
|
||||
f"💡 Не забудьте продлить подписку вовремя!"
|
||||
)
|
||||
return f"⚠️ Ваша подписка '{subscription_name}' истекает через {days_left} дн.!\n\nРекомендуем продлить её заранее в разделе 'Мои подписки'."
|
||||
else:
|
||||
return f"🔔 Уведомление о подписке *{subscription.name}*"
|
||||
|
||||
def _create_notification_keyboard(self, user: User, user_sub: UserSubscription,
|
||||
subscription: Subscription, notification_type: str) -> InlineKeyboardMarkup:
|
||||
"""Создать клавиатуру для уведомления"""
|
||||
buttons = []
|
||||
|
||||
# Кнопка продления (только для не-тестовых подписок)
|
||||
if not subscription.is_trial:
|
||||
if user.balance >= subscription.price:
|
||||
buttons.append([InlineKeyboardButton(
|
||||
text=f"🔄 Продлить за {subscription.price} руб.",
|
||||
callback_data=f"extend_sub_{user_sub.id}"
|
||||
)])
|
||||
if days_left <= 0:
|
||||
return f"⚠️ Your subscription '{subscription_name}' has expired!\n\nTo renew, go to 'My Subscriptions'."
|
||||
elif days_left == 1:
|
||||
return f"⚠️ Your subscription '{subscription_name}' expires tomorrow!\n\nDon't forget to renew it in 'My Subscriptions'."
|
||||
else:
|
||||
buttons.append([InlineKeyboardButton(
|
||||
text=f"💰 Пополнить баланс (нужно {subscription.price - user.balance} руб.)",
|
||||
callback_data="topup_balance"
|
||||
)])
|
||||
|
||||
# Кнопка покупки новой подписки
|
||||
buttons.append([InlineKeyboardButton(
|
||||
text="💳 Купить подписку",
|
||||
callback_data="buy_subscription"
|
||||
)])
|
||||
|
||||
# Кнопка "Мои подписки"
|
||||
buttons.append([InlineKeyboardButton(
|
||||
text="📋 Мои подписки",
|
||||
callback_data="my_subscriptions"
|
||||
)])
|
||||
|
||||
# Кнопка главного меню
|
||||
buttons.append([InlineKeyboardButton(
|
||||
text="🏠 Главное меню",
|
||||
callback_data="main_menu"
|
||||
)])
|
||||
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
async def check_single_user(self, user_id: int) -> List[NotificationResult]:
|
||||
"""Проверить подписки конкретного пользователя (для тестирования)"""
|
||||
results = []
|
||||
|
||||
try:
|
||||
user = await self.db.get_user_by_telegram_id(user_id)
|
||||
if not user:
|
||||
return [NotificationResult(False, user_id, "User not found")]
|
||||
|
||||
user_subs = await self.db.get_user_subscriptions(user_id)
|
||||
|
||||
for user_sub in user_subs:
|
||||
if user_sub.is_active:
|
||||
try:
|
||||
sent = await self._check_and_notify_subscription(user, user_sub)
|
||||
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
|
||||
sub_name = subscription.name if subscription else "Unknown"
|
||||
|
||||
results.append(NotificationResult(
|
||||
success=sent,
|
||||
user_id=user_id,
|
||||
message=f"Subscription: {sub_name}, Sent: {sent}"
|
||||
))
|
||||
except Exception as e:
|
||||
results.append(NotificationResult(
|
||||
success=False,
|
||||
user_id=user_id,
|
||||
message=f"Error checking subscription {user_sub.id}",
|
||||
error=str(e)
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
results.append(NotificationResult(
|
||||
success=False,
|
||||
user_id=user_id,
|
||||
message="Error checking user",
|
||||
error=str(e)
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
async def get_service_status(self) -> dict:
|
||||
"""Получить статус сервиса"""
|
||||
return {
|
||||
"is_running": self.is_running,
|
||||
"check_interval": self.CHECK_INTERVAL,
|
||||
"daily_check_hour": self.DAILY_CHECK_HOUR,
|
||||
"warning_days": self.WARNING_DAYS,
|
||||
"last_check": datetime.utcnow().isoformat() if self.is_running else None
|
||||
}
|
||||
|
||||
return f"⚠️ Your subscription '{subscription_name}' expires in {days_left} days!\n\nWe recommend renewing it in advance in 'My Subscriptions'."
|
||||
|
||||
async def force_daily_check(self):
|
||||
"""Принудительно запустить ежедневную проверку"""
|
||||
logger.info("Force starting daily check")
|
||||
await self._daily_check()
|
||||
|
||||
async def deactivate_expired_subscriptions(self):
|
||||
"""Деактивировать истекшие подписки"""
|
||||
"""Force daily check and cleanup"""
|
||||
try:
|
||||
now = datetime.utcnow()
|
||||
logger.info("Starting daily check and cleanup")
|
||||
|
||||
# Deactivate expired subscriptions
|
||||
deactivated_count = await self.deactivate_expired_subscriptions()
|
||||
|
||||
# Send final expiry notifications
|
||||
await self._send_final_expiry_notifications()
|
||||
|
||||
logger.info(f"Daily check completed. Deactivated {deactivated_count} expired subscriptions")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in force_daily_check: {e}")
|
||||
|
||||
async def deactivate_expired_subscriptions(self) -> int:
|
||||
"""Deactivate expired subscriptions"""
|
||||
try:
|
||||
count = 0
|
||||
all_users = await self.db.get_all_users()
|
||||
deactivated_count = 0
|
||||
|
||||
for user in all_users:
|
||||
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
|
||||
|
||||
for user_sub in user_subs:
|
||||
if user_sub.is_active and user_sub.expires_at <= now:
|
||||
# Деактивируем подписку
|
||||
if user_sub.is_active and user_sub.expires_at < datetime.utcnow():
|
||||
user_sub.is_active = False
|
||||
await self.db.update_user_subscription(user_sub)
|
||||
count += 1
|
||||
|
||||
# Деактивируем в RemnaWave если API доступно
|
||||
# Also try to deactivate in RemnaWave if possible
|
||||
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:
|
||||
# Блокируем пользователя в RemnaWave
|
||||
await self.api.update_user(user_uuid, {"enable": False})
|
||||
logger.info(f"Disabled user {user_uuid} in RemnaWave")
|
||||
user_data = await self.api.get_user_by_short_uuid(user_sub.short_uuid)
|
||||
if user_data and user_data.get('uuid'):
|
||||
await self.api.update_user(user_data['uuid'], {'status': 'EXPIRED'})
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to disable user in RemnaWave: {e}")
|
||||
|
||||
deactivated_count += 1
|
||||
log_user_action(user.telegram_id, "subscription_expired", f"SubID: {user_sub.id}")
|
||||
|
||||
logger.info(f"Deactivated {deactivated_count} expired subscriptions")
|
||||
return deactivated_count
|
||||
logger.warning(f"Could not deactivate user in RemnaWave: {e}")
|
||||
|
||||
return count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deactivating expired subscriptions: {e}")
|
||||
return 0
|
||||
|
||||
async def _send_final_expiry_notifications(self):
|
||||
"""Send final notifications for just-expired subscriptions"""
|
||||
try:
|
||||
# Get subscriptions that expired today
|
||||
all_users = await self.db.get_all_users()
|
||||
|
||||
for user in all_users:
|
||||
user_subs = await self.db.get_user_subscriptions(user.telegram_id)
|
||||
|
||||
for user_sub in user_subs:
|
||||
# Check if subscription expired today (within last 24 hours)
|
||||
time_since_expiry = datetime.utcnow() - user_sub.expires_at
|
||||
|
||||
if (time_since_expiry.total_seconds() > 0 and
|
||||
time_since_expiry.total_seconds() <= 86400): # 24 hours
|
||||
|
||||
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
|
||||
if subscription and not subscription.is_trial:
|
||||
message = self._format_expiry_message(subscription.name, 0, user.language)
|
||||
await self.bot.send_message(user.telegram_id, message)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending final expiry notifications: {e}")
|
||||
|
||||
async def check_single_user(self, user_id: int):
|
||||
"""Check subscriptions for a single user (for testing)"""
|
||||
try:
|
||||
results = []
|
||||
|
||||
user = await self.db.get_user_by_telegram_id(user_id)
|
||||
if not user:
|
||||
results.append({
|
||||
'success': False,
|
||||
'message': f'User {user_id} not found',
|
||||
'error': None
|
||||
})
|
||||
return results
|
||||
|
||||
# Get user subscriptions
|
||||
user_subs = await self.db.get_user_subscriptions(user_id)
|
||||
|
||||
if not user_subs:
|
||||
results.append({
|
||||
'success': True,
|
||||
'message': f'User {user_id} has no subscriptions',
|
||||
'error': None
|
||||
})
|
||||
return results
|
||||
|
||||
for user_sub in user_subs:
|
||||
try:
|
||||
subscription = await self.db.get_subscription_by_id(user_sub.subscription_id)
|
||||
|
||||
days_left = (user_sub.expires_at - datetime.utcnow()).days
|
||||
|
||||
if days_left <= self.config.MONITOR_WARNING_DAYS:
|
||||
# Send test notification
|
||||
message = self._format_expiry_message(subscription.name, days_left, user.language)
|
||||
await self.bot.send_message(user_id, f"[ТЕСТ] {message}")
|
||||
|
||||
results.append({
|
||||
'success': True,
|
||||
'message': f'Sent warning for subscription "{subscription.name}" (expires in {days_left} days)',
|
||||
'error': None
|
||||
})
|
||||
else:
|
||||
results.append({
|
||||
'success': True,
|
||||
'message': f'Subscription "{subscription.name}" is OK (expires in {days_left} days)',
|
||||
'error': None
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
results.append({
|
||||
'success': False,
|
||||
'message': f'Error checking subscription ID {user_sub.id}',
|
||||
'error': str(e)
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
return [{
|
||||
'success': False,
|
||||
'message': f'Error checking user {user_id}',
|
||||
'error': str(e)
|
||||
}]
|
||||
|
||||
async def get_service_status(self) -> dict:
|
||||
"""Get service status information"""
|
||||
return {
|
||||
'is_running': self.is_running,
|
||||
'check_interval': self.config.MONITOR_CHECK_INTERVAL,
|
||||
'daily_check_hour': self.config.MONITOR_DAILY_CHECK_HOUR,
|
||||
'warning_days': self.config.MONITOR_WARNING_DAYS,
|
||||
'last_check': datetime.now().strftime("%Y-%m-%d %H:%M:%S") if self.is_running else None
|
||||
}
|
||||
|
||||
|
||||
# Функция для инициализации и запуска сервиса
|
||||
async def create_subscription_monitor(bot: Bot, db: Database, config: Config,
|
||||
api: Optional[RemnaWaveAPI] = None) -> SubscriptionMonitorService:
|
||||
"""Создать и настроить сервис мониторинга подписок"""
|
||||
service = SubscriptionMonitorService(bot, db, config, api)
|
||||
return service
|
||||
|
||||
|
||||
# Пример использования в основном файле бота
|
||||
"""
|
||||
from subscription_monitor import create_subscription_monitor
|
||||
|
||||
async def main():
|
||||
# Инициализация бота, базы данных, конфига
|
||||
bot = Bot(token=config.BOT_TOKEN)
|
||||
db = Database(config.DATABASE_URL)
|
||||
api = RemnaWaveAPI(config.REMNAWAVE_API_URL, config.REMNAWAVE_API_KEY)
|
||||
|
||||
# Создание и запуск сервиса мониторинга
|
||||
monitor_service = await create_subscription_monitor(bot, db, config, api)
|
||||
await monitor_service.start()
|
||||
|
||||
try:
|
||||
# Запуск бота
|
||||
await dp.start_polling(bot)
|
||||
finally:
|
||||
# Остановка сервиса при завершении
|
||||
await monitor_service.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
"""
|
||||
async def create_subscription_monitor(bot, db: Database, config, api: RemnaWaveAPI = None) -> SubscriptionMonitorService:
|
||||
"""Create and return subscription monitor service"""
|
||||
return SubscriptionMonitorService(bot, db, config, api)
|
||||
|
||||
@@ -81,6 +81,20 @@ TRANSLATIONS = {
|
||||
'promocode_expired': '❌ Промокод истек',
|
||||
'promocode_used': '❌ Промокод уже использован',
|
||||
'promocode_limit': '❌ Лимит использований исчерпан',
|
||||
'referral_program': 'Реферальная программа',
|
||||
'referral_conditions': 'Условия программы',
|
||||
'referral_link': 'Ваша реферальная ссылка',
|
||||
'referral_code': 'Ваш промокод',
|
||||
'referral_stats': 'Статистика',
|
||||
'total_referrals': 'Всего приглашено',
|
||||
'active_referrals': 'Активных рефералов',
|
||||
'total_earned': 'Заработано',
|
||||
'my_referrals': 'Мои рефералы',
|
||||
'referral_reward_referrer': 'За каждого приглашенного: 150₽',
|
||||
'referral_reward_referred': 'Бонус новичку: 150₽ после пополнения на 300₽',
|
||||
'referral_percentage': 'Процент с платежей: 25%',
|
||||
'no_referrals': 'У вас пока нет рефералов',
|
||||
'referral_welcome': 'Добро пожаловать по реферальной ссылке!',
|
||||
|
||||
# Purchase
|
||||
'insufficient_balance': '❌ Недостаточно средств на балансе',
|
||||
@@ -197,6 +211,20 @@ TRANSLATIONS = {
|
||||
'payment_card_info': '💳 For card payment contact @{support}',
|
||||
'no_payments': '❌ Payment history is empty',
|
||||
'payment_item': '{date}: ${amount:.2f} - {description} ({status})',
|
||||
'referral_program': 'Referral Program',
|
||||
'referral_conditions': 'Program Conditions',
|
||||
'referral_link': 'Your referral link',
|
||||
'referral_code': 'Your promo code',
|
||||
'referral_stats': 'Statistics',
|
||||
'total_referrals': 'Total invited',
|
||||
'active_referrals': 'Active referrals',
|
||||
'total_earned': 'Total earned',
|
||||
'my_referrals': 'My referrals',
|
||||
'referral_reward_referrer': 'For each invited: $150',
|
||||
'referral_reward_referred': 'Newbie bonus: $150 after topping up $300',
|
||||
'referral_percentage': 'Percentage from payments: 25%',
|
||||
'no_referrals': 'You have no referrals yet',
|
||||
'referral_welcome': 'Welcome via referral link!',
|
||||
|
||||
# Promocodes
|
||||
'enter_promocode': '🎁 Enter promocode:',
|
||||
|
||||
@@ -1,191 +1,131 @@
|
||||
import re
|
||||
import uuid
|
||||
import logging
|
||||
import secrets
|
||||
import string
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List, Dict, Any, Tuple
|
||||
import logging
|
||||
from typing import Tuple, Optional, Dict, Any
|
||||
from database import Database
|
||||
from database import ReferralProgram, ReferralEarning # ДОБАВЛЕНО: импорт моделей
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def generate_username() -> str:
|
||||
"""Generate random username for RemnaWave"""
|
||||
return f"user_{secrets.token_hex(8)}"
|
||||
|
||||
def generate_password() -> str:
|
||||
"""Generate random password"""
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(12))
|
||||
|
||||
def generate_promocode() -> str:
|
||||
"""Generate random promocode"""
|
||||
alphabet = string.ascii_uppercase + string.digits
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(8))
|
||||
|
||||
def is_valid_email(email: str) -> bool:
|
||||
"""Validate email format"""
|
||||
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
return re.match(pattern, email) is not None
|
||||
|
||||
def is_valid_amount(amount_str: str) -> Tuple[bool, float]:
|
||||
"""Validate and parse amount"""
|
||||
def is_valid_amount(text: str) -> Tuple[bool, float]:
|
||||
"""Validate amount input"""
|
||||
try:
|
||||
amount = float(amount_str.replace(',', '.'))
|
||||
# Remove spaces and replace comma with dot
|
||||
text = text.strip().replace(' ', '').replace(',', '.')
|
||||
|
||||
amount = float(text)
|
||||
|
||||
# Check if amount is positive and reasonable
|
||||
if amount <= 0:
|
||||
return False, 0
|
||||
if amount > 100000: # Max amount limit
|
||||
return False, 0
|
||||
return False, 0.0
|
||||
|
||||
if amount > 1000000: # Max 1M rubles
|
||||
return False, 0.0
|
||||
|
||||
# Round to 2 decimal places
|
||||
amount = round(amount, 2)
|
||||
|
||||
return True, amount
|
||||
except ValueError:
|
||||
return False, 0
|
||||
|
||||
def format_date(date: datetime, lang: str = 'ru') -> str:
|
||||
"""Format date for display"""
|
||||
if lang == 'ru':
|
||||
months = [
|
||||
'января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
|
||||
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'
|
||||
]
|
||||
return f"{date.day} {months[date.month-1]} {date.year}"
|
||||
else:
|
||||
return date.strftime("%B %d, %Y")
|
||||
|
||||
def format_datetime(date: datetime, lang: str = 'ru') -> str:
|
||||
"""Format datetime for display"""
|
||||
if lang == 'ru':
|
||||
return date.strftime("%d.%m.%Y %H:%M")
|
||||
else:
|
||||
return date.strftime("%m/%d/%Y %H:%M")
|
||||
|
||||
def calculate_expiry_date(days: int) -> str:
|
||||
"""Calculate expiry date in ISO format"""
|
||||
expiry = datetime.utcnow() + timedelta(days=days)
|
||||
return expiry.isoformat() + 'Z'
|
||||
|
||||
def parse_telegram_id(text: str) -> Optional[int]:
|
||||
"""Parse Telegram ID from text"""
|
||||
try:
|
||||
telegram_id = int(text.strip())
|
||||
if telegram_id > 0:
|
||||
return telegram_id
|
||||
except ValueError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def format_traffic(gb: int, lang: str = 'ru') -> str:
|
||||
"""Format traffic limit for display"""
|
||||
if gb == 0:
|
||||
return "Безлимитный" if lang == 'ru' else "Unlimited"
|
||||
else:
|
||||
return f"{gb} ГБ" if lang == 'ru' else f"{gb} GB"
|
||||
|
||||
def paginate_list(items: List[Any], page: int, per_page: int = 10) -> Tuple[List[Any], int]:
|
||||
"""Paginate list of items"""
|
||||
total_pages = (len(items) + per_page - 1) // per_page
|
||||
start_idx = (page - 1) * per_page
|
||||
end_idx = start_idx + per_page
|
||||
return items[start_idx:end_idx], total_pages
|
||||
|
||||
def escape_markdown(text: str) -> str:
|
||||
"""Escape markdown special characters"""
|
||||
special_chars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']
|
||||
for char in special_chars:
|
||||
text = text.replace(char, f'\\{char}')
|
||||
return text
|
||||
|
||||
def truncate_text(text: str, max_length: int = 4000) -> str:
|
||||
"""Truncate text to fit Telegram message limits"""
|
||||
if len(text) <= max_length:
|
||||
return text
|
||||
return text[:max_length-3] + "..."
|
||||
|
||||
def validate_squad_uuid(uuid_str: str) -> bool:
|
||||
"""Validate UUID format"""
|
||||
try:
|
||||
uuid.UUID(uuid_str)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def format_subscription_info(subscription: Dict[str, Any], lang: str = 'ru') -> str:
|
||||
"""Format subscription information for display"""
|
||||
from translations import t
|
||||
|
||||
traffic = format_traffic(subscription['traffic_limit_gb'], lang)
|
||||
|
||||
info = t('subscription_info', lang,
|
||||
name=subscription['name'],
|
||||
price=subscription['price'],
|
||||
days=subscription['duration_days'],
|
||||
traffic=traffic,
|
||||
description=subscription.get('description', '')
|
||||
)
|
||||
|
||||
return info
|
||||
|
||||
def format_user_subscription_info(user_sub: Dict[str, Any], subscription: Dict[str, Any],
|
||||
expires_at: datetime, lang: str = 'ru') -> str:
|
||||
"""Format user subscription information"""
|
||||
from translations import t
|
||||
|
||||
traffic = format_traffic(subscription['traffic_limit_gb'], lang)
|
||||
|
||||
# Check if expired
|
||||
now = datetime.utcnow()
|
||||
if expires_at < now:
|
||||
status = t('subscription_expired', lang)
|
||||
else:
|
||||
status = t('subscription_active', lang, date=format_date(expires_at, lang))
|
||||
|
||||
info = f"📋 {subscription['name']}\n"
|
||||
info += f"⏱ {subscription['duration_days']} дней\n" if lang == 'ru' else f"⏱ {subscription['duration_days']} days\n"
|
||||
info += f"📊 {traffic}\n"
|
||||
info += f"🕒 {status}\n"
|
||||
|
||||
if subscription.get('description'):
|
||||
info += f"\n{subscription['description']}"
|
||||
|
||||
return info
|
||||
|
||||
except (ValueError, TypeError):
|
||||
return False, 0.0
|
||||
|
||||
def validate_promocode_format(code: str) -> bool:
|
||||
"""Validate promocode format"""
|
||||
if not code:
|
||||
return False
|
||||
|
||||
# Remove spaces and convert to uppercase
|
||||
code = code.strip().upper()
|
||||
|
||||
# Check length
|
||||
if len(code) < 3 or len(code) > 20:
|
||||
return False
|
||||
if not re.match(r'^[A-Z0-9]+$', code.upper()):
|
||||
|
||||
# Check that code contains only letters and numbers
|
||||
if not code.replace('_', '').isalnum():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def calculate_discount(original_price: float, promocode: Dict[str, Any]) -> float:
|
||||
"""Calculate discount amount"""
|
||||
if promocode.get('discount_percent'):
|
||||
return original_price * (promocode['discount_percent'] / 100)
|
||||
def validate_squad_uuid(uuid: str) -> bool:
|
||||
"""Validate squad UUID format"""
|
||||
if not uuid or not isinstance(uuid, str):
|
||||
return False
|
||||
|
||||
uuid = uuid.strip()
|
||||
|
||||
# Basic UUID format validation
|
||||
if len(uuid) < 8:
|
||||
return False
|
||||
|
||||
# UUID should contain only hex characters and hyphens
|
||||
allowed_chars = set('0123456789abcdefABCDEF-')
|
||||
if not all(c in allowed_chars for c in uuid):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def parse_telegram_id(text: str) -> Optional[int]:
|
||||
"""Parse Telegram ID from text"""
|
||||
try:
|
||||
# Remove spaces and common prefixes
|
||||
text = text.strip().replace(' ', '')
|
||||
|
||||
# Remove common prefixes
|
||||
if text.startswith('@'):
|
||||
text = text[1:]
|
||||
|
||||
if text.startswith('id'):
|
||||
text = text[2:]
|
||||
|
||||
# Try to convert to int
|
||||
telegram_id = int(text)
|
||||
|
||||
# Validate that it's a reasonable Telegram ID
|
||||
if telegram_id <= 0 or telegram_id > 9999999999: # Max 10 digits
|
||||
return None
|
||||
|
||||
return telegram_id
|
||||
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
def generate_username() -> str:
|
||||
"""Generate random username for RemnaWave"""
|
||||
prefix = "user_"
|
||||
random_part = ''.join(secrets.choice(string.ascii_lowercase + string.digits) for _ in range(8))
|
||||
return prefix + random_part
|
||||
|
||||
def generate_password() -> str:
|
||||
"""Generate random password for RemnaWave"""
|
||||
return ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(12))
|
||||
|
||||
def calculate_expiry_date(days: int) -> str:
|
||||
"""Calculate expiry date for subscription"""
|
||||
expiry_date = datetime.now() + timedelta(days=days)
|
||||
return expiry_date.isoformat() + 'Z'
|
||||
|
||||
def format_datetime(dt: datetime, language: str = 'ru') -> str:
|
||||
"""Format datetime for display"""
|
||||
if not dt:
|
||||
return "N/A"
|
||||
|
||||
if language == 'ru':
|
||||
return dt.strftime('%d.%m.%Y %H:%M')
|
||||
else:
|
||||
return min(promocode.get('discount_amount', 0), original_price)
|
||||
return dt.strftime('%Y-%m-%d %H:%M')
|
||||
|
||||
def format_payment_status(status: str, lang: str = 'ru') -> str:
|
||||
"""Format payment status for display"""
|
||||
status_map = {
|
||||
'pending': 'В ожидании' if lang == 'ru' else 'Pending',
|
||||
'completed': 'Завершен' if lang == 'ru' else 'Completed',
|
||||
'cancelled': 'Отменен' if lang == 'ru' else 'Cancelled',
|
||||
'failed': 'Ошибка' if lang == 'ru' else 'Failed'
|
||||
}
|
||||
return status_map.get(status, status)
|
||||
|
||||
def clean_phone_number(phone: str) -> str:
|
||||
"""Clean and format phone number"""
|
||||
# Remove all non-digit characters
|
||||
digits = re.sub(r'\D', '', phone)
|
||||
def format_date(dt: datetime, language: str = 'ru') -> str:
|
||||
"""Format date for display"""
|
||||
if not dt:
|
||||
return "N/A"
|
||||
|
||||
# Handle Russian phone numbers
|
||||
if digits.startswith('8') and len(digits) == 11:
|
||||
digits = '7' + digits[1:]
|
||||
elif digits.startswith('9') and len(digits) == 10:
|
||||
digits = '7' + digits
|
||||
|
||||
return digits
|
||||
if language == 'ru':
|
||||
return dt.strftime('%d.%m.%Y')
|
||||
else:
|
||||
return dt.strftime('%Y-%m-%d')
|
||||
|
||||
def format_bytes(bytes_value: int) -> str:
|
||||
"""Format bytes to human readable format"""
|
||||
@@ -200,121 +140,410 @@ def format_bytes(bytes_value: int) -> str:
|
||||
value /= 1024
|
||||
unit_index += 1
|
||||
|
||||
if unit_index == 0:
|
||||
return f"{int(value)} {units[unit_index]}"
|
||||
else:
|
||||
if value >= 100:
|
||||
return f"{value:.0f} {units[unit_index]}"
|
||||
elif value >= 10:
|
||||
return f"{value:.1f} {units[unit_index]}"
|
||||
|
||||
# УДАЛЯЕМ функцию get_subscription_connection_url - теперь URL берется из API
|
||||
# def get_subscription_connection_url(base_url: str, short_uuid: str) -> str:
|
||||
# """Generate subscription connection URL"""
|
||||
# return f"{base_url.rstrip('/')}/api/sub/{short_uuid}"
|
||||
|
||||
def log_user_action(telegram_id: int, action: str, details: str = None):
|
||||
"""Log user action for audit"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
log_message = f"Admin action by {telegram_id}: {action}"
|
||||
if details:
|
||||
log_message += f" - {details}"
|
||||
|
||||
logger.info(log_message)
|
||||
|
||||
def format_subscription_status(expires_at: datetime, lang: str = 'ru') -> str:
|
||||
"""Format subscription status with emoji"""
|
||||
now = datetime.utcnow()
|
||||
|
||||
if expires_at < now:
|
||||
return "❌ Истекла" if lang == 'ru' else "❌ Expired"
|
||||
|
||||
days_left = (expires_at - now).days
|
||||
|
||||
if days_left == 0:
|
||||
return "⚠️ Истекает сегодня" if lang == 'ru' else "⚠️ Expires today"
|
||||
elif days_left == 1:
|
||||
return "⚠️ Истекает завтра" if lang == 'ru' else "⚠️ Expires tomorrow"
|
||||
elif days_left <= 3:
|
||||
return f"🔶 Осталось {days_left} дней" if lang == 'ru' else f"🔶 {days_left} days left"
|
||||
else:
|
||||
return f"✅ Активна ({days_left} дней)" if lang == 'ru' else f"✅ Active ({days_left} days)"
|
||||
return f"{value:.2f} {units[unit_index]}"
|
||||
|
||||
def format_monitor_notification_type(notification_type: str, lang: str = 'ru') -> str:
|
||||
"""Format notification type for display"""
|
||||
type_map = {
|
||||
'expired': 'Истекла' if lang == 'ru' else 'Expired',
|
||||
'expires_today': 'Истекает сегодня' if lang == 'ru' else 'Expires today',
|
||||
'expires_tomorrow': 'Истекает завтра' if lang == 'ru' else 'Expires tomorrow',
|
||||
'warning': 'Предупреждение' if lang == 'ru' else 'Warning',
|
||||
'urgent': 'Срочно' if lang == 'ru' else 'Urgent'
|
||||
def format_payment_status(status: str, language: str = 'ru') -> str:
|
||||
"""Format payment status for display"""
|
||||
status_map = {
|
||||
'ru': {
|
||||
'pending': 'Ожидает',
|
||||
'completed': 'Завершен',
|
||||
'cancelled': 'Отменен',
|
||||
'failed': 'Ошибка'
|
||||
},
|
||||
'en': {
|
||||
'pending': 'Pending',
|
||||
'completed': 'Completed',
|
||||
'cancelled': 'Cancelled',
|
||||
'failed': 'Failed'
|
||||
}
|
||||
}
|
||||
return type_map.get(notification_type, notification_type)
|
||||
|
||||
def calculate_days_until_expiry(expires_at: datetime) -> int:
|
||||
"""Calculate days until expiry"""
|
||||
now = datetime.utcnow()
|
||||
delta = expires_at - now
|
||||
return max(0, delta.days)
|
||||
|
||||
def is_subscription_expiring_soon(expires_at: datetime, warning_days: int = 2) -> bool:
|
||||
"""Check if subscription is expiring soon"""
|
||||
days_left = calculate_days_until_expiry(expires_at)
|
||||
return days_left <= warning_days
|
||||
|
||||
# НОВАЯ ФУНКЦИЯ: Извлечение subscription URL из данных пользователя
|
||||
def extract_subscription_url(user_data: Dict[str, Any]) -> Optional[str]:
|
||||
"""Extract subscription URL from user data with fallback logic"""
|
||||
if not user_data:
|
||||
return None
|
||||
|
||||
# Пробуем разные возможные поля для URL
|
||||
url_fields = [
|
||||
'subscriptionUrl',
|
||||
'subscription_url',
|
||||
'url',
|
||||
'link',
|
||||
'connectionUrl',
|
||||
'connection_url'
|
||||
]
|
||||
|
||||
for field in url_fields:
|
||||
url = user_data.get(field)
|
||||
if url and isinstance(url, str) and url.strip():
|
||||
return url.strip()
|
||||
|
||||
return None
|
||||
return status_map.get(language, status_map['ru']).get(status, status)
|
||||
|
||||
def format_subscription_url_display(subscription_url: str, lang: str = 'ru') -> str:
|
||||
"""Format subscription URL for display in messages"""
|
||||
if not subscription_url:
|
||||
return "❌ URL недоступен" if lang == 'ru' else "❌ URL unavailable"
|
||||
def format_subscription_info(subscription: Dict[str, Any], language: str = 'ru') -> str:
|
||||
"""Format subscription information for display"""
|
||||
text = ""
|
||||
|
||||
# Показываем только домен для безопасности
|
||||
if language == 'ru':
|
||||
text += f"📋 **Подписка: {subscription['name']}**\n\n"
|
||||
text += f"💰 Цена: {subscription['price']} руб.\n"
|
||||
text += f"⏱ Длительность: {subscription['duration_days']} дн.\n"
|
||||
|
||||
if subscription['traffic_limit_gb'] > 0:
|
||||
text += f"📊 Лимит трафика: {subscription['traffic_limit_gb']} ГБ\n"
|
||||
else:
|
||||
text += f"📊 Лимит трафика: Безлимит\n"
|
||||
|
||||
if subscription.get('description'):
|
||||
text += f"\n📝 Описание:\n{subscription['description']}"
|
||||
else:
|
||||
text += f"📋 **Subscription: {subscription['name']}**\n\n"
|
||||
text += f"💰 Price: ${subscription['price']}\n"
|
||||
text += f"⏱ Duration: {subscription['duration_days']} days\n"
|
||||
|
||||
if subscription['traffic_limit_gb'] > 0:
|
||||
text += f"📊 Traffic limit: {subscription['traffic_limit_gb']} GB\n"
|
||||
else:
|
||||
text += f"📊 Traffic limit: Unlimited\n"
|
||||
|
||||
if subscription.get('description'):
|
||||
text += f"\n📝 Description:\n{subscription['description']}"
|
||||
|
||||
return text
|
||||
|
||||
def format_user_subscription_info(user_sub: Dict[str, Any], subscription: Dict[str, Any],
|
||||
expires_at: datetime, language: str = 'ru') -> str:
|
||||
"""Format user subscription information for display - ИСПРАВЛЕНО: добавлены пометки для импортированных"""
|
||||
text = ""
|
||||
|
||||
if language == 'ru':
|
||||
text += f"📋 **{subscription['name']}**\n\n"
|
||||
|
||||
# Status
|
||||
now = datetime.utcnow()
|
||||
if expires_at < now:
|
||||
status = "❌ Истекла"
|
||||
days_left = 0
|
||||
elif not user_sub.get('is_active', True):
|
||||
status = "⏸ Приостановлена"
|
||||
days_left = (expires_at - now).days
|
||||
else:
|
||||
days_left = (expires_at - now).days
|
||||
status = f"✅ Активна"
|
||||
|
||||
text += f"🔘 Статус: {status}\n"
|
||||
text += f"📅 Истекает: {format_datetime(expires_at, language)}\n"
|
||||
|
||||
if days_left > 0:
|
||||
text += f"⏰ Осталось: {days_left} дн.\n"
|
||||
|
||||
# Traffic info
|
||||
if subscription['traffic_limit_gb'] > 0:
|
||||
text += f"📊 Лимит трафика: {subscription['traffic_limit_gb']} ГБ\n"
|
||||
else:
|
||||
text += f"📊 Лимит трафика: Безлимит\n"
|
||||
|
||||
if subscription.get('name') == "Старая подписка" or (subscription.get('description') and 'импорт' in subscription.get('description', '').lower()):
|
||||
text += f"\n🔄 Тип: Импортированная из старой системы\n"
|
||||
text += f"ℹ️ Продление недоступно"
|
||||
|
||||
# Description
|
||||
if subscription.get('description') and not ('импорт' in subscription.get('description', '').lower()):
|
||||
text += f"\n📝 {subscription['description']}"
|
||||
else:
|
||||
text += f"📋 **{subscription['name']}**\n\n"
|
||||
|
||||
# Status
|
||||
now = datetime.utcnow()
|
||||
if expires_at < now:
|
||||
status = "❌ Expired"
|
||||
days_left = 0
|
||||
elif not user_sub.get('is_active', True):
|
||||
status = "⏸ Suspended"
|
||||
days_left = (expires_at - now).days
|
||||
else:
|
||||
days_left = (expires_at - now).days
|
||||
status = f"✅ Active"
|
||||
|
||||
text += f"🔘 Status: {status}\n"
|
||||
text += f"📅 Expires: {format_datetime(expires_at, language)}\n"
|
||||
|
||||
if days_left > 0:
|
||||
text += f"⏰ Days left: {days_left}\n"
|
||||
|
||||
# Traffic info
|
||||
if subscription['traffic_limit_gb'] > 0:
|
||||
text += f"📊 Traffic limit: {subscription['traffic_limit_gb']} GB\n"
|
||||
else:
|
||||
text += f"📊 Traffic limit: Unlimited\n"
|
||||
|
||||
if subscription.get('name') == "Старая подписка" or (subscription.get('description') and 'import' in subscription.get('description', '').lower()):
|
||||
text += f"\n🔄 Type: Imported from old system\n"
|
||||
text += f"ℹ️ Extension not available"
|
||||
|
||||
# Description
|
||||
if subscription.get('description') and not ('import' in subscription.get('description', '').lower()):
|
||||
text += f"\n📝 {subscription['description']}"
|
||||
|
||||
return text
|
||||
|
||||
def log_user_action(user_id: int, action: str, details: str = ""):
|
||||
"""Log user action"""
|
||||
logger.info(f"USER_ACTION: {user_id} - {action}" + (f" - {details}" if details else ""))
|
||||
|
||||
async def process_referral_rewards(user_id: int, amount: float, payment_id: int, db: Database, bot=None):
|
||||
"""Process referral rewards after successful payment - ИСПРАВЛЕНА СИГНАТУРА"""
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(subscription_url)
|
||||
domain = parsed.netloc or parsed.path.split('/')[0]
|
||||
return f"🔗 {domain}"
|
||||
except Exception:
|
||||
return "🔗 Ссылка готова" if lang == 'ru' else "🔗 Link ready"
|
||||
# Получаем конфигурацию из переменных окружения напрямую
|
||||
import os
|
||||
|
||||
threshold = float(os.getenv('REFERRAL_THRESHOLD', '300.0'))
|
||||
first_reward = float(os.getenv('REFERRAL_FIRST_REWARD', '150.0'))
|
||||
referred_bonus = float(os.getenv('REFERRAL_REFERRED_BONUS', '150.0'))
|
||||
percentage = float(os.getenv('REFERRAL_PERCENTAGE', '0.25'))
|
||||
|
||||
# Проверяем есть ли у пользователя реферер
|
||||
referral = await db.get_referral_by_referred_id(user_id)
|
||||
|
||||
if not referral:
|
||||
logger.debug(f"No referral found for user {user_id}")
|
||||
return
|
||||
|
||||
# Получаем информацию о пользователе
|
||||
user = await db.get_user_by_telegram_id(user_id)
|
||||
if not user:
|
||||
logger.error(f"User {user_id} not found")
|
||||
return
|
||||
|
||||
logger.info(f"Processing referral rewards for user {user_id}, amount {amount}, referrer {referral.referrer_id}")
|
||||
|
||||
if not referral.first_reward_paid and user.balance >= threshold:
|
||||
logger.info(f"Processing first reward for referral {referral.id} (threshold: {threshold}, reward: {first_reward})")
|
||||
|
||||
await db.add_balance(referral.referrer_id, first_reward)
|
||||
|
||||
# Создаем запись о платеже для реферера
|
||||
await db.create_payment(
|
||||
user_id=referral.referrer_id,
|
||||
amount=first_reward,
|
||||
payment_type='referral',
|
||||
description=f'Первая награда за реферала ID:{user_id}',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
# Выплачиваем первую награду рефереру (записываем в историю)
|
||||
success = await db.create_referral_earning(
|
||||
referrer_id=referral.referrer_id,
|
||||
referred_id=user_id,
|
||||
amount=first_reward,
|
||||
earning_type='first_reward',
|
||||
related_payment_id=payment_id
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"First reward paid: {first_reward}₽ to referrer {referral.referrer_id}")
|
||||
|
||||
if bot:
|
||||
try:
|
||||
# Уведомляем реферера
|
||||
await bot.send_message(
|
||||
referral.referrer_id,
|
||||
f"🎉 Поздравляем! Ваш реферал пополнил баланс на {threshold}₽+\n\n"
|
||||
f"💰 Вам начислено {first_reward}₽ за приведенного друга!\n"
|
||||
f"Теперь вы будете получать {percentage*100:.0f}% с каждого его следующего платежа."
|
||||
)
|
||||
|
||||
# Уведомляем самого пользователя
|
||||
await bot.send_message(
|
||||
user_id,
|
||||
f"🎁 Бонус активирован! Вам начислено {referred_bonus}₽ за переход по реферальной ссылке!"
|
||||
)
|
||||
|
||||
# Добавляем бонус рефералу
|
||||
await db.add_balance(user_id, referred_bonus)
|
||||
await db.create_payment(
|
||||
user_id=user_id,
|
||||
amount=referred_bonus,
|
||||
payment_type='referral',
|
||||
description='Бонус за переход по реферальной ссылке',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
logger.info(f"Referral bonus notifications sent and balance updated")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send referral notifications: {e}")
|
||||
else:
|
||||
logger.error(f"Failed to create first reward earning")
|
||||
|
||||
if amount > 0 and referral.first_reward_paid: # ИЗМЕНЕНО: добавлено условие first_reward_paid
|
||||
percentage_reward = amount * percentage
|
||||
|
||||
if percentage_reward >= 0.01: # Минимум 1 копейка
|
||||
await db.add_balance(referral.referrer_id, percentage_reward)
|
||||
|
||||
# Создаем запись о платеже для реферера
|
||||
await db.create_payment(
|
||||
user_id=referral.referrer_id,
|
||||
amount=percentage_reward,
|
||||
payment_type='referral',
|
||||
description=f'{percentage*100:.0f}% дохода от реферала ID:{user_id}',
|
||||
status='completed'
|
||||
)
|
||||
|
||||
success = await db.create_referral_earning(
|
||||
referrer_id=referral.referrer_id,
|
||||
referred_id=user_id,
|
||||
amount=percentage_reward,
|
||||
earning_type='percentage',
|
||||
related_payment_id=payment_id
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"Percentage reward paid: {percentage_reward:.2f}₽ ({percentage*100:.0f}%) to referrer {referral.referrer_id}")
|
||||
|
||||
if bot and percentage_reward >= 1.0: # Уведомляем только если сумма >= 1₽
|
||||
try:
|
||||
await bot.send_message(
|
||||
referral.referrer_id,
|
||||
f"💰 Реферальный доход!\n\n"
|
||||
f"Ваш реферал совершил платеж на {amount:.2f}₽\n"
|
||||
f"Вам начислено: {percentage_reward:.2f}₽ ({percentage*100:.0f}%)"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send percentage notification: {e}")
|
||||
else:
|
||||
logger.error(f"Failed to create percentage earning")
|
||||
elif amount > 0 and not referral.first_reward_paid:
|
||||
logger.info(f"Skipping percentage reward for user {user_id} - first reward not yet paid")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing referral rewards: {e}")
|
||||
|
||||
class States:
|
||||
"""State constants for FSM"""
|
||||
WAITING_LANGUAGE = "waiting_language"
|
||||
WAITING_AMOUNT = "waiting_amount"
|
||||
WAITING_PROMOCODE = "waiting_promocode"
|
||||
async def create_referral_from_start_param(user_telegram_id: int, start_param: str, db: Database, bot=None):
|
||||
"""Create referral relationship from start parameter"""
|
||||
try:
|
||||
if not start_param.startswith("ref_"):
|
||||
return False
|
||||
|
||||
referrer_id = int(start_param.replace("ref_", ""))
|
||||
|
||||
# Проверяем что это не тот же пользователь
|
||||
if referrer_id == user_telegram_id:
|
||||
logger.warning(f"User {user_telegram_id} tried to refer themselves")
|
||||
return False
|
||||
|
||||
# Проверяем что у пользователя еще нет реферера
|
||||
existing_referral = await db.get_referral_by_referred_id(user_telegram_id)
|
||||
|
||||
if existing_referral:
|
||||
logger.info(f"User {user_telegram_id} already has referrer")
|
||||
return False
|
||||
|
||||
# Генерируем промокод реферера
|
||||
referral_code = await db.generate_unique_referral_code(referrer_id)
|
||||
|
||||
# Создаем реферальную связь
|
||||
referral = await db.create_referral(referrer_id, user_telegram_id, referral_code)
|
||||
|
||||
if referral:
|
||||
logger.info(f"Created referral: {referrer_id} -> {user_telegram_id} with code {referral_code}")
|
||||
|
||||
# Уведомляем реферера
|
||||
if bot:
|
||||
try:
|
||||
referrer = await db.get_user_by_telegram_id(referrer_id)
|
||||
if referrer:
|
||||
await bot.send_message(
|
||||
referrer_id,
|
||||
f"🎉 Отлично! По вашей ссылке зарегистрировался новый пользователь!\n\n"
|
||||
f"Вы получите 150₽ после того, как он пополнит баланс на 300₽.\n"
|
||||
f"И будете получать 25% с каждого его платежа!"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to notify referrer: {e}")
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.warning(f"Invalid referral parameter: {start_param}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating referral from start param: {e}")
|
||||
return False
|
||||
|
||||
async def create_referral_from_promocode(user_telegram_id: int, referral_code: str, db: Database, bot=None):
|
||||
"""Create referral relationship from promocode - ИСПРАВЛЕННАЯ ВЕРСИЯ"""
|
||||
try:
|
||||
if not referral_code.startswith("REF"):
|
||||
return False
|
||||
|
||||
logger.info(f"Trying to use referral code {referral_code} for user {user_telegram_id}")
|
||||
|
||||
# Ищем реферера по коду - ИСПРАВЛЕНО: используем правильный импорт
|
||||
async with db.session_factory() as session:
|
||||
from sqlalchemy import select
|
||||
result = await session.execute(
|
||||
select(ReferralProgram).where(ReferralProgram.referral_code == referral_code)
|
||||
)
|
||||
referral_record = result.scalar_one_or_none()
|
||||
|
||||
if not referral_record:
|
||||
logger.warning(f"No referrer found for code {referral_code}")
|
||||
return False
|
||||
|
||||
referrer_id = referral_record.referrer_id
|
||||
|
||||
# Проверяем что пользователь не пытается использовать свой код
|
||||
if referrer_id == user_telegram_id:
|
||||
logger.warning(f"User {user_telegram_id} tried to use own referral code")
|
||||
return False
|
||||
|
||||
# Проверяем что у пользователя еще нет реферера
|
||||
existing_referral = await db.get_referral_by_referred_id(user_telegram_id)
|
||||
|
||||
if existing_referral:
|
||||
logger.info(f"User {user_telegram_id} already has referrer")
|
||||
return False
|
||||
|
||||
# Создаем реферальную связь
|
||||
referral = await db.create_referral(referrer_id, user_telegram_id, referral_code)
|
||||
|
||||
if referral:
|
||||
logger.info(f"Created referral from promocode: {referrer_id} -> {user_telegram_id}")
|
||||
|
||||
# Уведомляем реферера
|
||||
if bot:
|
||||
try:
|
||||
await bot.send_message(
|
||||
referrer_id,
|
||||
f"🎉 По вашему промокоду {referral_code} зарегистрировался новый пользователь!\n\n"
|
||||
f"Вы получите 150₽ после того, как он пополнит баланс на 300₽."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to notify referrer: {e}")
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating referral from promocode: {e}")
|
||||
return False
|
||||
|
||||
def generate_referral_link(bot_username: str, user_id: int) -> str:
|
||||
"""Generate referral link for user"""
|
||||
if not bot_username:
|
||||
return ""
|
||||
|
||||
# Admin states
|
||||
ADMIN_CREATE_SUB_NAME = "admin_create_sub_name"
|
||||
ADMIN_CREATE_SUB_DESC = "admin_create_sub_desc"
|
||||
ADMIN_CREATE_SUB_PRICE = "admin_create_sub_price"
|
||||
ADMIN_CREATE_SUB_DAYS = "admin_create_sub_days"
|
||||
ADMIN_CREATE_SUB_TRAFFIC = "admin_create_sub_traffic"
|
||||
ADMIN_CREATE_SUB_SQUAD = "admin_create_sub_squad"
|
||||
if bot_username.startswith('@'):
|
||||
bot_username = bot_username[1:]
|
||||
|
||||
ADMIN_ADD_BALANCE_USER = "admin_add_balance_user"
|
||||
ADMIN_ADD_BALANCE_AMOUNT = "admin_add_balance_amount"
|
||||
|
||||
ADMIN_CREATE_PROMO_CODE = "admin_create_promo_code"
|
||||
ADMIN_CREATE_PROMO_DISCOUNT = "admin_create_promo_discount"
|
||||
ADMIN_CREATE_PROMO_LIMIT = "admin_create_promo_limit"
|
||||
return f"https://t.me/{bot_username}?start=ref_{user_id}"
|
||||
|
||||
def validate_referral_code(code: str) -> bool:
|
||||
"""Validate referral code format"""
|
||||
if not code or not code.startswith("REF"):
|
||||
return False
|
||||
if len(code) < 4 or len(code) > 20:
|
||||
return False
|
||||
return True
|
||||
|
||||
def format_referral_stats(stats: dict, lang: str = 'ru') -> str:
|
||||
"""Format referral statistics for display"""
|
||||
if lang == 'ru':
|
||||
return (f"👥 Приглашено: {stats['total_referrals']}\n"
|
||||
f"✅ Активных: {stats['active_referrals']}\n"
|
||||
f"💰 Заработано: {stats['total_earned']:.2f}₽")
|
||||
else:
|
||||
return (f"👥 Invited: {stats['total_referrals']}\n"
|
||||
f"✅ Active: {stats['active_referrals']}\n"
|
||||
f"💰 Earned: ${stats['total_earned']:.2f}")
|
||||
|
||||
Reference in New Issue
Block a user