Compare commits

...

47 Commits

Author SHA1 Message Date
Egor 03fc4abd2a Update subscription.py 2025-09-03 04:32:16 +03:00
Egor f280a37818 Update subscription.py 2025-09-03 04:25:31 +03:00
Egor f100ec62d4 Update subscription.py 2025-09-03 04:24:55 +03:00
Egor cd827612d9 Update subscription.py 2025-09-03 04:23:41 +03:00
Egor 50da2f82c8 Update subscription_service.py 2025-09-03 04:22:05 +03:00
Egor 4c814ec5f4 Update inline.py 2025-09-03 04:13:35 +03:00
Egor fb8c02d5ab Update subscription.py 2025-09-03 04:12:34 +03:00
Egor 465ad3d1ad Update inline.py 2025-09-03 04:10:55 +03:00
Egor 2bfaab7d16 Update subscription_service.py 2025-09-03 04:07:33 +03:00
Egor e356542df7 Update subscription.py 2025-09-03 00:46:11 +03:00
Egor a3dfbdce3b Update inline.py 2025-09-03 00:44:57 +03:00
Egor a666e8d248 Update subscription_service.py 2025-09-03 00:44:18 +03:00
Egor 9ef8df22a8 Update subscription.py 2025-09-03 00:43:54 +03:00
Egor a1471c8b9a Update subscription.py 2025-09-03 00:39:35 +03:00
Egor a99a14922c Update subscription_service.py 2025-09-03 00:17:11 +03:00
Egor ce954ad775 Update subscription.py 2025-09-03 00:15:42 +03:00
Egor efba7c10d0 Update subscription.py 2025-09-03 00:15:01 +03:00
Egor cd10c131b2 Update inline.py 2025-09-03 00:14:30 +03:00
Egor 3e4cb8dfa2 Update inline.py 2025-09-02 23:38:52 +03:00
Egor 6471ab547c Update subscription.py 2025-09-02 23:37:57 +03:00
Egor c10bf07124 Update inline.py 2025-09-02 23:32:14 +03:00
Egor 4e05882522 Update subscription.py 2025-09-02 23:30:51 +03:00
Egor 061b292d92 Update subscription.py 2025-09-02 23:28:30 +03:00
Egor 779a6c309a Update subscription.py 2025-09-02 23:24:16 +03:00
Egor c79c71f209 Update remnawave_api.py 2025-09-02 17:35:45 +03:00
Egor d4dbce5606 Update remnawave_api.py 2025-09-02 17:32:41 +03:00
Egor e53d99223d Update config.py 2025-09-02 15:32:45 +03:00
Egor ec7274e0c9 Update remnawave_api.py 2025-09-02 15:32:29 +03:00
Egor 8fe631a98e Update remnawave_api.py 2025-09-02 15:26:00 +03:00
Egor bd831bd92b Update config.py 2025-09-02 15:21:43 +03:00
Egor 9b31ba8f3d Update remnawave_api.py 2025-09-02 15:21:15 +03:00
Egor 3759be333a Update remnawave_api.py 2025-09-02 15:15:25 +03:00
Egor 555894b26c Update README.md 2025-09-02 14:55:47 +03:00
Egor bb636c003c Update .env.example 2025-09-02 14:50:48 +03:00
Egor 40c7b4aebd Update remnawave_api.py 2025-09-02 14:23:38 +03:00
Egor 028bd4888b Update remnawave_api.py 2025-09-02 14:10:01 +03:00
Egor 27d64e361a Update remnawave_api.py 2025-09-02 14:03:04 +03:00
Egor 6fc7dd49dd Update remnawave_api.py 2025-09-02 13:40:32 +03:00
Egor b835ba1d65 Update remnawave_api.py 2025-09-02 13:33:15 +03:00
Egor 811e985087 Update .env.example 2025-09-02 13:29:29 +03:00
Egor c16733b93d Update config.py 2025-09-02 13:29:13 +03:00
Egor 23ab3c5198 Update remnawave_service.py 2025-09-02 13:28:09 +03:00
Egor 9b8f121e69 Update remnawave_api.py 2025-09-02 13:25:49 +03:00
Egor 86b1b95fb9 Update subscription.py 2025-09-02 08:52:42 +03:00
Egor 9418681a00 Update subscription_service.py 2025-09-02 08:50:35 +03:00
Egor 7ebd937e2c Update README.md 2025-09-02 02:21:40 +03:00
Egor 25c404f999 Update README.md 2025-09-02 02:09:29 +03:00
9 changed files with 113 additions and 61 deletions
+3 -1
View File
@@ -21,8 +21,10 @@ POSTGRES_USER=remnawave_user
POSTGRES_PASSWORD=secure_password_123
# ===== REMNAWAVE API =====
REMNAWAVE_API_URL=
REMNAWAVE_API_URL=https://panel.example.com
REMNAWAVE_API_KEY=
# Для панелей установленных скриптом eGames прописывать ключ в формате XXXXXXX:DDDDDDDD - https://panel.example.com/auth/login?XXXXXXX=DDDDDDDD
REMNAWAVE_SECRET_KEY=your_secret_key_here
# ===== ТРИАЛ ПОДПИСКА =====
TRIAL_DURATION_DAYS=3
+27 -10
View File
@@ -51,6 +51,7 @@
- 📈 **Масштабируемость**
- 🔧 **Мониторинг** - Prometheus, Grafana, health checks
- 🔧 **Режим технических работ** - Ручное включение + Мониторинг системы, который в случае падении панели Remnawave переведет бота в режим технических работ и обратно - отключит его, если панель поднимется.
- Интеграция с системой защиты панели Remnawave через куки-аутентификацию, которая используется в [remnawave-reverse-proxy](https://github.com/eGamesAPI/remnawave-reverse-proxy) для скрытия панели от несанкционированного доступа.
---
@@ -113,8 +114,10 @@ POSTGRES_USER=remnawave_user
POSTGRES_PASSWORD=secure_password_123
# ===== REMNAWAVE API =====
REMNAWAVE_API_URL=
REMNAWAVE_API_URL=https://panel.example.com
REMNAWAVE_API_KEY=
# Для панелей установленных скриптом eGames прописывать ключ в формате XXXXXXX:DDDDDDDD - https://panel.example.com/auth/login?XXXXXXX=DDDDDDDD
REMNAWAVE_SECRET_KEY=your_secret_key_here
# ===== ТРИАЛ ПОДПИСКА =====
TRIAL_DURATION_DAYS=3
@@ -431,16 +434,16 @@ your-domain.com {
```bash
# Быстрый старт
docker compose up -d --build
docker compose up -d
# Статус сервисов
docker compose ps
# Логи
docker compose logs -f bot
docker compose logs
# Перезапуск
docker compose restart bot
docker compose restart
# Остановка
docker compose down
@@ -462,26 +465,30 @@ docker compose down -v --remove-orphans
🛒 **Умная покупка подписок**
- 📅 Выбор периода (14-360 дней) (С возможностью настройки выводимых периодов 14/30/60/90/180/360 дней)
- 📊 Настройка трафика (5GB - безлимит) (Можно откоючить данный шаг, всем подпискам выдавать единый)
- 🌍 Выбор стран через сквады
- 📱 Количество устройств (1-10)
- 🌍 Выбор стран через сквады (Доступно при наличие двух стран(сквадов) в продаже)
- 📱 Количество устройств (1-5)
🧪 **Тестовая подписка**
- Получение настраиваемой разовой тестовой подписки
- Уведомления об истечении с предложением о переходе н платную версию
💰 **Удобные платежи**
- ⭐ Telegram Stars
- 💳 Tribute
- 💳 YooKassa
- 🎁 Реферальные бонусы
- Детальная история транзакций
📱 **Управление подписками**
- 📈 Просмотр статистики использования
- 🔄 Автопродление с баланса
- 🔄 Сброс/увеличение трафика
- 🌍 Смена стран на лету
- Устройства с возможностью докупки/сброса устройств)
- 🌍 Смена стран на лету (Доступно при наличие двух стран(сквадов) в продаже)
- 📱 Настройка устройств с возможностью докупки(до 10ти устройств) + сброса устройств
🎁 **Бонусная система**
- 🎫 Промокоды на деньги/дни
- 👥 Реферальная программа
- 🆓 Бесплатный триал
- 🔔 Ежедневные уведомления
</td>
@@ -503,7 +510,7 @@ docker compose down -v --remove-orphans
🎫 **Промо-система**
- 🎁 Создание промокодов (деньги/дни/длинный триал)
- 📊 Статистика использования
- ⚙️ Полное редактирование промокодов
- ⚙️ Полное редактирование промокодов (Изминение условий, активация/деактивация/удаление + статистика применения)
🖥️ **Мониторинг системы**
- 💚 Состояние Remnawave панели
@@ -517,6 +524,16 @@ docker compose down -v --remove-orphans
- 🔔 Автоуведомления о продлении
- 💬 Система поддержки
- 📝 Настройка правил сервиса
- Поддержка HTML разметки
🚧 **Режим технических работ**
- Ручное включение/отключение с указанием причины
- Включение/выключение мониторинга состояния панели (проверяет соеденение до панели раз в 30 сек(можно изменить промежуток)
- Принудительаная проверка API
📖 **Правила сервиса**
- Настройка правил сервиса
- Просмотр текущих правил
</td>
</tr>
+1
View File
@@ -16,6 +16,7 @@ class Settings(BaseSettings):
REMNAWAVE_API_URL: str
REMNAWAVE_API_KEY: str
REMNAWAVE_SECRET_KEY: Optional[str] = None
TRIAL_DURATION_DAYS: int = 3
TRIAL_TRAFFIC_LIMIT_GB: int = 10
+1 -1
View File
@@ -512,7 +512,7 @@ async def get_subscription_renewal_cost(
if subscription.traffic_limit_gb > 0:
traffic_cost = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
devices_cost = max(0, subscription.device_limit - 1) * settings.PRICE_PER_DEVICE
devices_cost = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
total_cost = base_price + servers_cost + traffic_cost + devices_cost
+34 -11
View File
@@ -8,6 +8,7 @@ import aiohttp
import logging
from dataclasses import dataclass
from enum import Enum
from urllib.parse import urlparse, urljoin
logger = logging.getLogger(__name__)
@@ -33,7 +34,7 @@ class RemnaWaveUser:
username: str
status: UserStatus
used_traffic_bytes: int
lifetime_used_traffic_bytes: int # Новое поле
lifetime_used_traffic_bytes: int
traffic_limit_bytes: int
traffic_limit_strategy: TrafficLimitStrategy
expire_at: datetime
@@ -92,10 +93,12 @@ class RemnaWaveAPIError(Exception):
class RemnaWaveAPI:
def __init__(self, base_url: str, api_key: str):
def __init__(self, base_url: str, api_key: str, secret_key: Optional[str] = None):
self.base_url = base_url.rstrip('/')
self.api_key = api_key
self.secret_key = secret_key
self.session: Optional[aiohttp.ClientSession] = None
self.authenticated = False
def _detect_connection_type(self) -> str:
parsed = urlparse(self.base_url)
@@ -124,18 +127,29 @@ class RemnaWaveAPI:
headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Forwarded-Proto': 'https',
'X-Forwarded-For': '127.0.0.1',
'X-Real-IP': '127.0.0.1'
}
cookies = None
if self.secret_key:
if ':' in self.secret_key:
key_name, key_value = self.secret_key.split(':', 1)
cookies = {key_name: key_value}
logger.debug(f"🍪 Используем куки: {key_name}=***")
else:
cookies = {self.secret_key: self.secret_key}
logger.debug(f"🍪 Используем куки: {self.secret_key}=***")
connector_kwargs = {}
if conn_type == "local":
logger.debug("🏠 Использую локальные заголовки proxy")
headers.update({
'X-Forwarded-For': '127.0.0.1',
'X-Forwarded-Proto': 'https',
'X-Forwarded-Host': 'localhost',
'X-Real-IP': '127.0.0.1',
'Host': 'localhost'
})
@@ -152,11 +166,18 @@ class RemnaWaveAPI:
connector = aiohttp.TCPConnector(**connector_kwargs)
self.session = aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=30),
headers=headers,
connector=connector
)
session_kwargs = {
'timeout': aiohttp.ClientTimeout(total=30),
'headers': headers,
'connector': connector
}
if cookies:
session_kwargs['cookies'] = cookies
self.session = aiohttp.ClientSession(**session_kwargs)
self.authenticated = True
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
@@ -194,6 +215,8 @@ class RemnaWaveAPI:
if response.status >= 400:
error_message = response_data.get('message', f'HTTP {response.status}')
logger.error(f"API Error {response.status}: {error_message}")
logger.error(f"Response: {response_text[:500]}")
raise RemnaWaveAPIError(
error_message,
response.status,
+14 -23
View File
@@ -198,7 +198,7 @@ async def get_subscription_cost(subscription, db: AsyncSession) -> int:
)
traffic_cost = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
devices_cost = max(0, subscription.device_limit - 1) * settings.PRICE_PER_DEVICE
devices_cost = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
total_cost = base_cost + servers_cost + traffic_cost + devices_cost
@@ -328,7 +328,7 @@ async def start_subscription_purchase(
initial_data = {
'period_days': None,
'countries': [],
'devices': 1,
'devices': settings.DEFAULT_DEVICE_LIMIT,
'total_price': 0
}
@@ -412,11 +412,11 @@ async def get_countries_price_by_uuids_fallback(country_uuids: List[str], db: As
total_price += price
prices_list.append(price)
else:
default_price = 1000
default_price = 0
total_price += default_price
prices_list.append(default_price)
except Exception:
default_price = 1000
default_price = 0
total_price += default_price
prices_list.append(default_price)
@@ -424,7 +424,7 @@ async def get_countries_price_by_uuids_fallback(country_uuids: List[str], db: As
except Exception as e:
logger.error(f"Ошибка fallback функции: {e}")
default_prices = [1000] * len(country_uuids)
default_prices = [0] * len(country_uuids)
return sum(default_prices), default_prices
async def handle_manage_country(
@@ -1369,7 +1369,7 @@ async def select_devices(
if c['uuid'] in data['countries']
)
devices_price = (devices - 1) * settings.PRICE_PER_DEVICE
devices_price = max(0, devices - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
data['devices'] = devices
data['total_price'] = base_price + countries_price + devices_price
@@ -1414,7 +1414,7 @@ async def devices_continue(
else:
traffic_price = TRAFFIC_PRICES.get(data['traffic_gb'], 0)
devices_price = (data['devices'] - 1) * settings.PRICE_PER_DEVICE
devices_price = max(0, data['devices'] - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
total_price = base_price + traffic_price + countries_price + devices_price
data['total_price'] = total_price
@@ -1474,7 +1474,7 @@ async def confirm_purchase(
countries_price += country['price_kopeks']
server_prices.append(country['price_kopeks'])
devices_price = (data['devices'] - 1) * settings.PRICE_PER_DEVICE
devices_price = max(0, data['devices'] - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
if settings.is_traffic_fixed():
traffic_price = TRAFFIC_PRICES.get(settings.get_fixed_traffic_limit(), 0)
@@ -1885,7 +1885,7 @@ async def _get_available_countries():
countries.append({
"uuid": squad["uuid"],
"name": squad_name,
"price_kopeks": 1000,
"price_kopeks": 0,
"is_available": True
})
@@ -2243,7 +2243,7 @@ async def handle_connect_subscription(
subscription = db_user.subscription
if not subscription or not subscription.subscription_url:
await callback.answer(" У вас нет активной подписки или ссылка еще генерируется", show_alert=True)
await callback.answer(" У вас нет активной подписки или ссылка еще генерируется", show_alert=True)
return
connect_mode = settings.CONNECT_BUTTON_MODE
@@ -2252,13 +2252,10 @@ async def handle_connect_subscription(
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text="🔗 Открыть подписку",
text="🔗 Открыть подписку в мини-приложении",
web_app=types.WebAppInfo(url=subscription.subscription_url)
)
],
[
InlineKeyboardButton(text="📋 Показать ссылку", callback_data="open_subscription_link")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
]
@@ -2266,9 +2263,9 @@ async def handle_connect_subscription(
await callback.message.edit_text(
f"""
🔗 <b>Подключить подписку</b>
📱 <b>Подключить подписку</b>
📱 Нажмите кнопку ниже, чтобы открыть подписку в мини-приложении Telegram:
🚀 Нажмите кнопку ниже, чтобы открыть подписку в мини-приложении Telegram:
""",
reply_markup=keyboard,
parse_mode="HTML"
@@ -2276,7 +2273,7 @@ async def handle_connect_subscription(
elif connect_mode == "miniapp_custom":
if not settings.MINIAPP_CUSTOM_URL:
await callback.answer(" Кастомная ссылка для мини-приложения не настроена", show_alert=True)
await callback.answer(" Кастомная ссылка для мини-приложения не настроена", show_alert=True)
return
keyboard = InlineKeyboardMarkup(inline_keyboard=[
@@ -2286,9 +2283,6 @@ async def handle_connect_subscription(
web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL)
)
],
[
InlineKeyboardButton(text="📋 Показать ссылку подписки", callback_data="open_subscription_link")
],
[
InlineKeyboardButton(text="⬅️ Назад", callback_data="menu_subscription")
]
@@ -2299,9 +2293,6 @@ async def handle_connect_subscription(
🚀 <b>Подключить подписку</b>
📱 Нажмите кнопку ниже, чтобы открыть приложение:
📋 <b>Ссылка подписки:</b>
<code>{subscription.subscription_url}</code>
""",
reply_markup=keyboard,
parse_mode="HTML"
+27 -10
View File
@@ -1,4 +1,5 @@
from typing import List, Optional
from aiogram import types
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from app.config import settings, PERIOD_PRICES, TRAFFIC_PRICES
@@ -107,15 +108,28 @@ def get_subscription_keyboard(
connect_mode = settings.CONNECT_BUTTON_MODE
if connect_mode == "miniapp_subscription":
button_text = "🚀 Открыть в мини-приложении"
keyboard.append([
InlineKeyboardButton(
text="🚀 Открыть в мини-приложении",
web_app=types.WebAppInfo(url=subscription.subscription_url)
)
])
elif connect_mode == "miniapp_custom":
button_text = "🚀 Открыть приложение"
if settings.MINIAPP_CUSTOM_URL:
keyboard.append([
InlineKeyboardButton(
text="🚀 Открыть приложение",
web_app=types.WebAppInfo(url=settings.MINIAPP_CUSTOM_URL)
)
])
else:
keyboard.append([
InlineKeyboardButton(text="🔗 Подключиться", callback_data="subscription_connect")
])
else:
button_text = "🔗 Подключиться"
keyboard.append([
InlineKeyboardButton(text=button_text, callback_data="subscription_connect")
])
keyboard.append([
InlineKeyboardButton(text="🔗 Подключиться", callback_data="subscription_connect")
])
if not is_trial and subscription and subscription.days_left <= 3:
keyboard.append([
@@ -291,9 +305,12 @@ def get_devices_keyboard(current: int, language: str = "ru") -> InlineKeyboardMa
texts = get_texts(language)
keyboard = []
for devices in range(1, 6):
price = (devices - 1) * settings.PRICE_PER_DEVICE
price_text = f" (+{texts.format_price(price)})" if price > 0 else ""
start_devices = settings.DEFAULT_DEVICE_LIMIT
end_devices = max(start_devices + 3, 6)
for devices in range(start_devices, end_devices):
price = max(0, devices - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
price_text = f" (+{texts.format_price(price)})" if price > 0 else " (включено)"
emoji = "" if devices == current else ""
device_word = _get_device_declension(devices)
+2 -1
View File
@@ -25,7 +25,8 @@ class RemnaWaveService:
def __init__(self):
self.api = RemnaWaveAPI(
base_url=settings.REMNAWAVE_API_URL,
api_key=settings.REMNAWAVE_API_KEY
api_key=settings.REMNAWAVE_API_KEY,
secret_key=settings.REMNAWAVE_SECRET_KEY
)
def _parse_remnawave_date(self, date_str: str) -> datetime:
+4 -4
View File
@@ -249,7 +249,7 @@ class SubscriptionService:
server_prices.append(0)
logger.warning(f"⚠️ Сервер ID {server_id} недоступен")
devices_price = max(0, devices - 1) * settings.PRICE_PER_DEVICE
devices_price = max(0, devices - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
total_price = base_price + traffic_price + total_servers_price + devices_price
@@ -277,7 +277,7 @@ class SubscriptionService:
subscription.connected_squads, db
)
devices_price = max(0, subscription.device_limit - 1) * settings.PRICE_PER_DEVICE
devices_price = max(0, subscription.device_limit - settings.DEFAULT_DEVICE_LIMIT) * settings.PRICE_PER_DEVICE
traffic_price = TRAFFIC_PRICES.get(subscription.traffic_limit_gb, 0)
@@ -364,7 +364,7 @@ class SubscriptionService:
prices_list.append(price)
logger.debug(f"🏷️ Страна {server.display_name}: {price/100}")
else:
default_price = 1000
default_price = 0
total_price += default_price
prices_list.append(default_price)
logger.warning(f"⚠️ Сервер {country_uuid} недоступен, используем базовую цену: {default_price/100}")
@@ -374,7 +374,7 @@ class SubscriptionService:
except Exception as e:
logger.error(f"Ошибка получения цен стран: {e}")
default_prices = [1000] * len(country_uuids)
default_prices = [0] * len(country_uuids)
return sum(default_prices), default_prices
async def _get_countries_price(self, country_uuids: List[str], db: AsyncSession) -> int: