Compare commits

...

14 Commits

Author SHA1 Message Date
Fr1ngg 9b14baca51 Update README.md 2025-08-08 05:43:08 +03:00
Fr1ngg eb1a75c9f5 Update .env.example 2025-08-08 05:24:58 +03:00
Fr1ngg 5e79106134 Update admin_handlers.py 2025-08-08 05:22:43 +03:00
Fr1ngg ee45ec356e Update api_error_handlers.py 2025-08-08 05:22:20 +03:00
Fr1ngg 9275bff828 Update config.py 2025-08-08 05:21:51 +03:00
Fr1ngg 9150c70618 Update database.py 2025-08-08 05:21:25 +03:00
Fr1ngg b792e749d6 Update handlers.py 2025-08-08 05:21:00 +03:00
Fr1ngg 9871572740 Update keyboards.py 2025-08-08 05:20:37 +03:00
Fr1ngg 52c1cb3cbc Update main.py 2025-08-08 05:20:16 +03:00
Fr1ngg d5b2b371c8 Update middlewares.py 2025-08-08 05:19:23 +03:00
Fr1ngg d5cb022b02 Update remnawave_api.py 2025-08-08 05:19:06 +03:00
Fr1ngg 021f62fd30 Update subscription_monitor.py 2025-08-08 05:18:41 +03:00
Fr1ngg 684e398798 Update translations.py 2025-08-08 05:18:12 +03:00
Fr1ngg 12b64e9978 Update utils.py 2025-08-08 05:17:45 +03:00
14 changed files with 2035 additions and 874 deletions
+7 -2
View File
@@ -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)
+19 -6
View File
@@ -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
View File
@@ -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)
)
-2
View File
@@ -1,5 +1,3 @@
# api_error_handlers.py - Дополнительные утилиты для обработки ошибок API
import logging
from typing import Optional, Dict, Any, Callable
from aiogram.types import CallbackQuery
+155 -51
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+44 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+28
View File
@@ -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:',
+502 -273
View File
@@ -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}")