mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-25 13:51:50 +00:00
351 lines
13 KiB
Python
351 lines
13 KiB
Python
import logging
|
||
import secrets
|
||
import string
|
||
import re
|
||
from datetime import datetime, timedelta
|
||
from typing import Optional, Dict, List
|
||
from sqlalchemy import select, func, and_
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy.orm import selectinload
|
||
|
||
from app.database.models import User, ReferralEarning, Transaction, TransactionType
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
async def generate_unique_referral_code(db: AsyncSession, telegram_id: int) -> str:
|
||
max_attempts = 10
|
||
|
||
for _ in range(max_attempts):
|
||
code = f"ref{''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(8))}"
|
||
|
||
result = await db.execute(
|
||
select(User).where(User.referral_code == code)
|
||
)
|
||
if not result.scalar_one_or_none():
|
||
return code
|
||
|
||
timestamp = str(int(datetime.utcnow().timestamp()))[-6:]
|
||
return f"ref{timestamp}"
|
||
|
||
|
||
async def mark_user_as_had_paid_subscription(db: AsyncSession, user_id: int) -> bool:
|
||
try:
|
||
from app.database.crud.user import get_user_by_id
|
||
|
||
user = await get_user_by_id(db, user_id)
|
||
if not user:
|
||
logger.error(f"Пользователь {user_id} не найден")
|
||
return False
|
||
|
||
if not user.has_had_paid_subscription:
|
||
user.has_had_paid_subscription = True
|
||
user.updated_at = datetime.utcnow()
|
||
await db.commit()
|
||
logger.info(f"✅ Пользователь {user_id} отмечен как имевший платную подписку")
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка отметки пользователя {user_id} как имевшего платную подписку: {e}")
|
||
return False
|
||
|
||
|
||
async def get_user_referral_summary(db: AsyncSession, user_id: int) -> Dict:
|
||
try:
|
||
invited_count_result = await db.execute(
|
||
select(func.count(User.id)).where(User.referred_by_id == user_id)
|
||
)
|
||
invited_count = invited_count_result.scalar() or 0
|
||
|
||
referrals_result = await db.execute(
|
||
select(User).where(User.referred_by_id == user_id)
|
||
)
|
||
referrals = referrals_result.scalars().all()
|
||
|
||
paid_referrals_count = sum(1 for ref in referrals if ref.has_made_first_topup)
|
||
|
||
total_earnings_result = await db.execute(
|
||
select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0))
|
||
.where(ReferralEarning.user_id == user_id)
|
||
)
|
||
total_earned_kopeks = total_earnings_result.scalar() or 0
|
||
|
||
month_ago = datetime.utcnow() - timedelta(days=30)
|
||
month_earnings_result = await db.execute(
|
||
select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0))
|
||
.where(
|
||
and_(
|
||
ReferralEarning.user_id == user_id,
|
||
ReferralEarning.created_at >= month_ago
|
||
)
|
||
)
|
||
)
|
||
month_earned_kopeks = month_earnings_result.scalar() or 0
|
||
|
||
recent_earnings_result = await db.execute(
|
||
select(ReferralEarning)
|
||
.options(selectinload(ReferralEarning.referral))
|
||
.where(ReferralEarning.user_id == user_id)
|
||
.order_by(ReferralEarning.created_at.desc())
|
||
.limit(5)
|
||
)
|
||
recent_earnings_raw = recent_earnings_result.scalars().all()
|
||
|
||
recent_earnings = []
|
||
for earning in recent_earnings_raw:
|
||
if earning.referral:
|
||
recent_earnings.append({
|
||
'amount_kopeks': earning.amount_kopeks,
|
||
'reason': earning.reason,
|
||
'referral_name': earning.referral.full_name,
|
||
'created_at': earning.created_at
|
||
})
|
||
|
||
earnings_by_type = {}
|
||
earnings_by_type_result = await db.execute(
|
||
select(
|
||
ReferralEarning.reason,
|
||
func.count(ReferralEarning.id).label('count'),
|
||
func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0).label('total_amount')
|
||
)
|
||
.where(ReferralEarning.user_id == user_id)
|
||
.group_by(ReferralEarning.reason)
|
||
)
|
||
|
||
for row in earnings_by_type_result:
|
||
earnings_by_type[row.reason] = {
|
||
'count': row.count,
|
||
'total_amount_kopeks': row.total_amount
|
||
}
|
||
|
||
active_referrals_count = 0
|
||
for referral in referrals:
|
||
if referral.last_activity and referral.last_activity >= month_ago:
|
||
active_referrals_count += 1
|
||
|
||
return {
|
||
'invited_count': invited_count,
|
||
'paid_referrals_count': paid_referrals_count,
|
||
'active_referrals_count': active_referrals_count,
|
||
'total_earned_kopeks': total_earned_kopeks,
|
||
'month_earned_kopeks': month_earned_kopeks,
|
||
'recent_earnings': recent_earnings,
|
||
'earnings_by_type': earnings_by_type,
|
||
'conversion_rate': round((paid_referrals_count / invited_count * 100) if invited_count > 0 else 0, 1)
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка получения статистики рефералов для пользователя {user_id}: {e}")
|
||
return {
|
||
'invited_count': 0,
|
||
'paid_referrals_count': 0,
|
||
'active_referrals_count': 0,
|
||
'total_earned_kopeks': 0,
|
||
'month_earned_kopeks': 0,
|
||
'recent_earnings': [],
|
||
'earnings_by_type': {},
|
||
'conversion_rate': 0.0
|
||
}
|
||
|
||
|
||
async def get_detailed_referral_list(db: AsyncSession, user_id: int, limit: int = 20, offset: int = 0) -> Dict:
|
||
try:
|
||
referrals_result = await db.execute(
|
||
select(User)
|
||
.where(User.referred_by_id == user_id)
|
||
.order_by(User.created_at.desc())
|
||
.offset(offset)
|
||
.limit(limit)
|
||
)
|
||
referrals = referrals_result.scalars().all()
|
||
|
||
total_count_result = await db.execute(
|
||
select(func.count(User.id)).where(User.referred_by_id == user_id)
|
||
)
|
||
total_count = total_count_result.scalar() or 0
|
||
|
||
detailed_referrals = []
|
||
for referral in referrals:
|
||
earnings_result = await db.execute(
|
||
select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0))
|
||
.where(
|
||
and_(
|
||
ReferralEarning.user_id == user_id,
|
||
ReferralEarning.referral_id == referral.id
|
||
)
|
||
)
|
||
)
|
||
total_earned_from_referral = earnings_result.scalar() or 0
|
||
|
||
topups_result = await db.execute(
|
||
select(func.count(Transaction.id))
|
||
.where(
|
||
and_(
|
||
Transaction.user_id == referral.id,
|
||
Transaction.type == TransactionType.DEPOSIT.value,
|
||
Transaction.is_completed == True
|
||
)
|
||
)
|
||
)
|
||
topups_count = topups_result.scalar() or 0
|
||
|
||
days_since_registration = (datetime.utcnow() - referral.created_at).days
|
||
|
||
days_since_activity = None
|
||
if referral.last_activity:
|
||
days_since_activity = (datetime.utcnow() - referral.last_activity).days
|
||
|
||
detailed_referrals.append({
|
||
'id': referral.id,
|
||
'telegram_id': referral.telegram_id,
|
||
'full_name': referral.full_name,
|
||
'username': referral.username,
|
||
'created_at': referral.created_at,
|
||
'last_activity': referral.last_activity,
|
||
'has_made_first_topup': referral.has_made_first_topup,
|
||
'balance_kopeks': referral.balance_kopeks,
|
||
'total_earned_kopeks': total_earned_from_referral,
|
||
'topups_count': topups_count,
|
||
'days_since_registration': days_since_registration,
|
||
'days_since_activity': days_since_activity,
|
||
'status': 'active' if days_since_activity is not None and days_since_activity <= 30 else 'inactive'
|
||
})
|
||
|
||
return {
|
||
'referrals': detailed_referrals,
|
||
'total_count': total_count,
|
||
'has_next': offset + limit < total_count,
|
||
'has_prev': offset > 0,
|
||
'current_page': (offset // limit) + 1,
|
||
'total_pages': (total_count + limit - 1) // limit
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка получения списка рефералов для пользователя {user_id}: {e}")
|
||
return {
|
||
'referrals': [],
|
||
'total_count': 0,
|
||
'has_next': False,
|
||
'has_prev': False,
|
||
'current_page': 1,
|
||
'total_pages': 1
|
||
}
|
||
|
||
|
||
async def get_referral_analytics(db: AsyncSession, user_id: int) -> Dict:
|
||
try:
|
||
now = datetime.utcnow()
|
||
periods = {
|
||
'today': now.replace(hour=0, minute=0, second=0, microsecond=0),
|
||
'week': now - timedelta(days=7),
|
||
'month': now - timedelta(days=30),
|
||
'quarter': now - timedelta(days=90)
|
||
}
|
||
|
||
earnings_by_period = {}
|
||
for period_name, start_date in periods.items():
|
||
result = await db.execute(
|
||
select(func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0))
|
||
.where(
|
||
and_(
|
||
ReferralEarning.user_id == user_id,
|
||
ReferralEarning.created_at >= start_date
|
||
)
|
||
)
|
||
)
|
||
earnings_by_period[period_name] = result.scalar() or 0
|
||
|
||
top_referrals_result = await db.execute(
|
||
select(
|
||
ReferralEarning.referral_id,
|
||
func.coalesce(func.sum(ReferralEarning.amount_kopeks), 0).label('total_earned'),
|
||
func.count(ReferralEarning.id).label('earnings_count')
|
||
)
|
||
.where(ReferralEarning.user_id == user_id)
|
||
.group_by(ReferralEarning.referral_id)
|
||
.order_by(func.sum(ReferralEarning.amount_kopeks).desc())
|
||
.limit(5)
|
||
)
|
||
|
||
top_referrals = []
|
||
for row in top_referrals_result:
|
||
referral_result = await db.execute(
|
||
select(User).where(User.id == row.referral_id)
|
||
)
|
||
referral = referral_result.scalar_one_or_none()
|
||
if referral:
|
||
top_referrals.append({
|
||
'referral_name': referral.full_name,
|
||
'total_earned_kopeks': row.total_earned,
|
||
'earnings_count': row.earnings_count
|
||
})
|
||
|
||
return {
|
||
'earnings_by_period': earnings_by_period,
|
||
'top_referrals': top_referrals
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка получения аналитики рефералов для пользователя {user_id}: {e}")
|
||
return {
|
||
'earnings_by_period': {
|
||
'today': 0,
|
||
'week': 0,
|
||
'month': 0,
|
||
'quarter': 0
|
||
},
|
||
'top_referrals': []
|
||
}
|
||
|
||
|
||
def build_remnawave_username(user: User) -> str:
|
||
"""
|
||
Формирует имя пользователя для панели RemnaWave.
|
||
|
||
Собирает "Имя Фамилия username" из доступных данных Telegram.
|
||
Затем очищает результат от символов, не разрешённых в поле
|
||
RemnaWave (допустимы только буквы, цифры, подчёркивания и дефисы).
|
||
Если нет ни имени, ни фамилии, ни username, возвращается
|
||
"user_{telegram_id}".
|
||
"""
|
||
parts: List[str] = []
|
||
if user.first_name:
|
||
parts.append(user.first_name)
|
||
if user.last_name:
|
||
parts.append(user.last_name)
|
||
if user.username:
|
||
parts.append(user.username)
|
||
|
||
if parts:
|
||
raw = " ".join(parts)
|
||
transliterated = _transliterate_ru_to_latin(raw)
|
||
sanitized = re.sub(r"[^A-Za-z0-9_-]", "_", transliterated)
|
||
sanitized = re.sub(r"_+", "_", sanitized).strip("_")
|
||
return sanitized or f"user_{user.telegram_id}"
|
||
return f"user_{user.telegram_id}"
|
||
|
||
|
||
CYRILLIC_TO_LATIN = {
|
||
"а": "a", "б": "b", "в": "v", "г": "g", "д": "d", "е": "e",
|
||
"ё": "e", "ж": "zh", "з": "z", "и": "i", "й": "y", "к": "k",
|
||
"л": "l", "м": "m", "н": "n", "о": "o", "п": "p", "р": "r",
|
||
"с": "s", "т": "t", "у": "u", "ф": "f", "х": "h", "ц": "ts",
|
||
"ч": "ch", "ш": "sh", "щ": "shch", "ъ": "", "ы": "y",
|
||
"ь": "", "э": "e", "ю": "yu", "я": "ya",
|
||
}
|
||
|
||
|
||
def _transliterate_ru_to_latin(text: str) -> str:
|
||
result = []
|
||
for ch in text:
|
||
lower = ch.lower()
|
||
if lower in CYRILLIC_TO_LATIN:
|
||
repl = CYRILLIC_TO_LATIN[lower]
|
||
if ch.isupper():
|
||
repl = repl.capitalize() if len(repl) > 1 else repl.upper()
|
||
result.append(repl)
|
||
else:
|
||
result.append(ch)
|
||
return "".join(result)
|