Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d8cf45bda | ||
|
|
df987fc52a | ||
|
|
07cb68457f | ||
|
|
1e666f90d3 | ||
|
|
6241f766a2 | ||
|
|
451581f792 | ||
|
|
a015946b20 | ||
|
|
d1d701bff3 | ||
|
|
ea50567a42 | ||
|
|
a8b62ccc03 | ||
|
|
0e03d72dd3 | ||
|
|
aa5eb74d75 |
+8
-6
@@ -16,6 +16,8 @@ DEFAULT_CURRENCY_SYMBOL="RUB" # e.g., RUB, USD, EUR
|
|||||||
SUPPORT_LINK=https://t.me/your_support_link
|
SUPPORT_LINK=https://t.me/your_support_link
|
||||||
SERVER_STATUS_URL=https://status.yourdomain.tld/status/your_service
|
SERVER_STATUS_URL=https://status.yourdomain.tld/status/your_service
|
||||||
TERMS_OF_SERVICE_URL=https://example.com/tos
|
TERMS_OF_SERVICE_URL=https://example.com/tos
|
||||||
|
SUBSCRIPTION_MINI_APP_URL=
|
||||||
|
START_COMMAND_DESCRIPTION=
|
||||||
|
|
||||||
# YooKassa Payment Gateway Configuration
|
# YooKassa Payment Gateway Configuration
|
||||||
YOOKASSA_SHOP_ID=your_shop_id
|
YOOKASSA_SHOP_ID=your_shop_id
|
||||||
@@ -78,13 +80,13 @@ REFEREE_BONUS_DAYS_12_MONTHS=15
|
|||||||
PANEL_API_URL=http://your_panel_api_url/api
|
PANEL_API_URL=http://your_panel_api_url/api
|
||||||
PANEL_API_KEY=your_panel_api_key
|
PANEL_API_KEY=your_panel_api_key
|
||||||
|
|
||||||
# Default settings for NEW panel users
|
# User traffic limits (applied for all users)
|
||||||
PANEL_USER_DEFAULT_EXPIRE_DAYS=1
|
# 0 means unlimited
|
||||||
PANEL_USER_DEFAULT_TRAFFIC_BYTES=0
|
USER_TRAFFIC_LIMIT_GB=0
|
||||||
PANEL_USER_DEFAULT_TRAFFIC_STRATEGY="NO_RESET"
|
USER_TRAFFIC_STRATEGY="NO_RESET"
|
||||||
|
|
||||||
# Default Inbounds for Panel Users (Optional, comma-separated UUIDs)
|
# Default Inbounds for Users (Optional, comma-separated UUIDs)
|
||||||
PANEL_USER_DEFAULT_INBOUND_UUIDS=uuid1,uuid2,uuid3
|
USER_INBOUND_UUIDS=uuid1,uuid2,uuid3
|
||||||
|
|
||||||
# Trial Settings
|
# Trial Settings
|
||||||
TRIAL_ENABLED=True
|
TRIAL_ENABLED=True
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ This Telegram bot is designed to automate the sale and management of subscriptio
|
|||||||
* `DEFAULT_CURRENCY_SYMBOL`: e.g., `RUB`, `USD`.
|
* `DEFAULT_CURRENCY_SYMBOL`: e.g., `RUB`, `USD`.
|
||||||
* `SUPPORT_LINK`: (Optional) URL for a support chat/contact (e.g., `https://t.me/your_support`).
|
* `SUPPORT_LINK`: (Optional) URL for a support chat/contact (e.g., `https://t.me/your_support`).
|
||||||
* `SERVER_STATUS_URL`: (Optional) URL to a server status page (e.g., Uptime Kuma).
|
* `SERVER_STATUS_URL`: (Optional) URL to a server status page (e.g., Uptime Kuma).
|
||||||
|
* `SUBSCRIPTION_MINI_APP_URL`: (Optional) URL of the Telegram mini app for viewing subscription details. If set, the "My Subscription" button will open this mini app and the bot will register it automatically via API.
|
||||||
|
* `START_COMMAND_DESCRIPTION`: (Optional) Description for the `/start` command shown in the bot's menu.
|
||||||
* **YooKassa Settings:**
|
* **YooKassa Settings:**
|
||||||
* `YOOKASSA_SHOP_ID`: Your shop ID from YooKassa.
|
* `YOOKASSA_SHOP_ID`: Your shop ID from YooKassa.
|
||||||
* `YOOKASSA_SECRET_KEY`: Your secret key from YooKassa.
|
* `YOOKASSA_SECRET_KEY`: Your secret key from YooKassa.
|
||||||
@@ -100,7 +102,8 @@ This Telegram bot is designed to automate the sale and management of subscriptio
|
|||||||
* **Panel API Settings:**
|
* **Panel API Settings:**
|
||||||
* `PANEL_API_URL`: Full URL to your Remnawave panel's API (e.g., `http://remnawave:3000/api` or `https://panel.yourdomain.com/api`).
|
* `PANEL_API_URL`: Full URL to your Remnawave panel's API (e.g., `http://remnawave:3000/api` or `https://panel.yourdomain.com/api`).
|
||||||
* `PANEL_API_KEY`: API Key for authenticating with the Remnawave panel.
|
* `PANEL_API_KEY`: API Key for authenticating with the Remnawave panel.
|
||||||
* `PANEL_USER_DEFAULT_INBOUND_UUIDS`: (Optional) Comma-separated list of inbound UUIDs from your panel to assign to users. If empty, `activateAllInbounds: true` (panel default) is used for new users.
|
* `USER_INBOUND_UUIDS`: (Optional) Comma-separated list of inbound UUIDs from your panel to assign to users. If empty, `activateAllInbounds: true` (panel default) is used for new users.
|
||||||
|
* `USER_TRAFFIC_LIMIT_GB` and `USER_TRAFFIC_STRATEGY`: Default traffic limit in gigabytes (0 for unlimited) and the reset strategy applied when updating users on the panel.
|
||||||
* `TRIAL_ENABLED`, `TRIAL_DURATION_DAYS`, `TRIAL_TRAFFIC_LIMIT_GB`: Settings for the trial period.
|
* `TRIAL_ENABLED`, `TRIAL_DURATION_DAYS`, `TRIAL_TRAFFIC_LIMIT_GB`: Settings for the trial period.
|
||||||
* `WEB_SERVER_HOST`, `WEB_SERVER_PORT`: Host and port for the bot's internal webhook server.
|
* `WEB_SERVER_HOST`, `WEB_SERVER_PORT`: Host and port for the bot's internal webhook server.
|
||||||
* `LOGS_PAGE_SIZE`: For admin panel log pagination.
|
* `LOGS_PAGE_SIZE`: For admin panel log pagination.
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from bot.services.panel_api_service import PanelApiService
|
|||||||
from bot.services.yookassa_service import YooKassaService
|
from bot.services.yookassa_service import YooKassaService
|
||||||
from bot.middlewares.i18n import JsonI18n
|
from bot.middlewares.i18n import JsonI18n
|
||||||
from config.settings import Settings
|
from config.settings import Settings
|
||||||
|
from bot.services.notification_service import notify_admin_new_payment
|
||||||
|
|
||||||
payment_processing_lock = asyncio.Lock()
|
payment_processing_lock = asyncio.Lock()
|
||||||
|
|
||||||
@@ -175,6 +176,15 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
|||||||
f"Failed to send final payment success message to user {user_id}: {e_notify}"
|
f"Failed to send final payment success message to user {user_id}: {e_notify}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await notify_admin_new_payment(
|
||||||
|
bot,
|
||||||
|
settings,
|
||||||
|
i18n,
|
||||||
|
user_id,
|
||||||
|
subscription_months,
|
||||||
|
payment_value,
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e_process:
|
except Exception as e_process:
|
||||||
logging.error(
|
logging.error(
|
||||||
f"Error during process_successful_payment main try block for user {user_id}: {e_process}",
|
f"Error during process_successful_payment main try block for user {user_id}: {e_process}",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from datetime import datetime
|
|||||||
from config.settings import Settings
|
from config.settings import Settings
|
||||||
from bot.services.subscription_service import SubscriptionService
|
from bot.services.subscription_service import SubscriptionService
|
||||||
from bot.services.panel_api_service import PanelApiService
|
from bot.services.panel_api_service import PanelApiService
|
||||||
|
from bot.services.notification_service import notify_admin_new_trial
|
||||||
from bot.keyboards.inline.user_keyboards import (
|
from bot.keyboards.inline.user_keyboards import (
|
||||||
get_trial_confirmation_keyboard,
|
get_trial_confirmation_keyboard,
|
||||||
get_main_menu_inline_keyboard,
|
get_main_menu_inline_keyboard,
|
||||||
@@ -189,6 +190,15 @@ async def confirm_activate_trial_handler(
|
|||||||
disable_web_page_preview=True,
|
disable_web_page_preview=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if activation_result and activation_result.get("activated") and end_date_obj:
|
||||||
|
await notify_admin_new_trial(
|
||||||
|
callback.bot,
|
||||||
|
settings,
|
||||||
|
i18n,
|
||||||
|
user_id,
|
||||||
|
end_date_obj,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "main_action:cancel_trial")
|
@router.callback_query(F.data == "main_action:cancel_trial")
|
||||||
async def cancel_trial_activation(
|
async def cancel_trial_activation(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
|
from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
|
||||||
from aiogram.types import InlineKeyboardMarkup
|
from aiogram.types import InlineKeyboardMarkup, WebAppInfo
|
||||||
from typing import Dict, Optional, List
|
from typing import Dict, Optional, List
|
||||||
|
|
||||||
from config.settings import Settings
|
from config.settings import Settings
|
||||||
@@ -21,9 +21,20 @@ def get_main_menu_inline_keyboard(
|
|||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(text=_(key="menu_subscribe_inline"),
|
InlineKeyboardButton(text=_(key="menu_subscribe_inline"),
|
||||||
callback_data="main_action:subscribe"))
|
callback_data="main_action:subscribe"))
|
||||||
|
if settings.SUBSCRIPTION_MINI_APP_URL:
|
||||||
builder.row(
|
builder.row(
|
||||||
InlineKeyboardButton(text=_(key="menu_my_subscription_inline"),
|
InlineKeyboardButton(
|
||||||
callback_data="main_action:my_subscription"))
|
text=_(key="menu_my_subscription_inline"),
|
||||||
|
web_app=WebAppInfo(url=settings.SUBSCRIPTION_MINI_APP_URL),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
builder.row(
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=_(key="menu_my_subscription_inline"),
|
||||||
|
callback_data="main_action:my_subscription",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
referral_button = InlineKeyboardButton(
|
referral_button = InlineKeyboardButton(
|
||||||
text=_(key="menu_referral_inline"),
|
text=_(key="menu_referral_inline"),
|
||||||
|
|||||||
+37
-1
@@ -3,7 +3,13 @@ import asyncio
|
|||||||
from typing import Callable, Dict, Any, Awaitable, Optional
|
from typing import Callable, Dict, Any, Awaitable, Optional
|
||||||
|
|
||||||
from aiogram import Bot, Dispatcher, BaseMiddleware, Router, F
|
from aiogram import Bot, Dispatcher, BaseMiddleware, Router, F
|
||||||
from aiogram.types import Update
|
from aiogram.types import (
|
||||||
|
Update,
|
||||||
|
MenuButtonDefault,
|
||||||
|
MenuButtonWebApp,
|
||||||
|
WebAppInfo,
|
||||||
|
BotCommand,
|
||||||
|
)
|
||||||
from aiogram.enums import ParseMode
|
from aiogram.enums import ParseMode
|
||||||
from aiogram.filters import CommandStart, Command
|
from aiogram.filters import CommandStart, Command
|
||||||
from aiogram.client.default import DefaultBotProperties
|
from aiogram.client.default import DefaultBotProperties
|
||||||
@@ -171,6 +177,36 @@ async def on_startup_configured(dispatcher: Dispatcher):
|
|||||||
)
|
)
|
||||||
await bot.delete_webhook(drop_pending_updates=True)
|
await bot.delete_webhook(drop_pending_updates=True)
|
||||||
|
|
||||||
|
if settings.SUBSCRIPTION_MINI_APP_URL:
|
||||||
|
try:
|
||||||
|
menu_text = i18n_instance.gettext(
|
||||||
|
settings.DEFAULT_LANGUAGE,
|
||||||
|
"menu_my_subscription_inline",
|
||||||
|
)
|
||||||
|
await bot.set_chat_menu_button(
|
||||||
|
menu_button=MenuButtonWebApp(
|
||||||
|
text=menu_text,
|
||||||
|
web_app=WebAppInfo(url=settings.SUBSCRIPTION_MINI_APP_URL),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await bot.set_chat_menu_button(menu_button=MenuButtonDefault())
|
||||||
|
logging.info(
|
||||||
|
"STARTUP: Mini app domain registered and default menu button restored."
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(
|
||||||
|
f"STARTUP: Failed to register mini app domain: {e}", exc_info=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if settings.START_COMMAND_DESCRIPTION:
|
||||||
|
try:
|
||||||
|
await bot.set_my_commands([
|
||||||
|
BotCommand(command="start", description=settings.START_COMMAND_DESCRIPTION)
|
||||||
|
])
|
||||||
|
logging.info("STARTUP: /start command description set.")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"STARTUP: Failed to set bot commands: {e}", exc_info=True)
|
||||||
|
|
||||||
logging.info("STARTUP: Bot on_startup_configured completed.")
|
logging.info("STARTUP: Bot on_startup_configured completed.")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -133,3 +133,46 @@ async def schedule_subscription_notifications(
|
|||||||
logging.info(
|
logging.info(
|
||||||
f"Subscription expiration warning job scheduled daily at {notification_hour:02d}:{notification_minute:02d} UTC."
|
f"Subscription expiration warning job scheduled daily at {notification_hour:02d}:{notification_minute:02d} UTC."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def notify_admins(bot: Bot, settings: Settings, i18n: JsonI18n,
|
||||||
|
message_key: str, parse_mode: str | None = None,
|
||||||
|
**kwargs) -> None:
|
||||||
|
if not settings.ADMIN_IDS:
|
||||||
|
return
|
||||||
|
admin_lang = settings.DEFAULT_LANGUAGE
|
||||||
|
msg = i18n.gettext(admin_lang, message_key, **kwargs)
|
||||||
|
for admin_id in settings.ADMIN_IDS:
|
||||||
|
try:
|
||||||
|
await bot.send_message(admin_id, msg, parse_mode=parse_mode)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to send admin notification to {admin_id}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def notify_admin_new_trial(bot: Bot, settings: Settings, i18n: JsonI18n,
|
||||||
|
user_id: int, end_date: datetime) -> None:
|
||||||
|
end_date_str = end_date.strftime('%Y-%m-%d') if isinstance(end_date, datetime) else str(end_date)
|
||||||
|
await notify_admins(
|
||||||
|
bot,
|
||||||
|
settings,
|
||||||
|
i18n,
|
||||||
|
"admin_new_trial_notification",
|
||||||
|
user_id=user_id,
|
||||||
|
end_date=end_date_str,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def notify_admin_new_payment(bot: Bot, settings: Settings, i18n: JsonI18n,
|
||||||
|
user_id: int, months: int, amount: float,
|
||||||
|
currency: str | None = None) -> None:
|
||||||
|
currency_symbol = currency or settings.DEFAULT_CURRENCY_SYMBOL
|
||||||
|
await notify_admins(
|
||||||
|
bot,
|
||||||
|
settings,
|
||||||
|
i18n,
|
||||||
|
"admin_new_payment_notification",
|
||||||
|
user_id=user_id,
|
||||||
|
months=months,
|
||||||
|
amount=f"{amount:.2f}",
|
||||||
|
currency=currency_symbol,
|
||||||
|
)
|
||||||
|
|||||||
@@ -137,8 +137,7 @@ class ReferralService:
|
|||||||
"status_from_panel":
|
"status_from_panel":
|
||||||
"ACTIVE_BONUS",
|
"ACTIVE_BONUS",
|
||||||
"traffic_limit_bytes":
|
"traffic_limit_bytes":
|
||||||
self.settings.
|
self.settings.user_traffic_limit_bytes,
|
||||||
PANEL_USER_DEFAULT_TRAFFIC_BYTES,
|
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
await subscription_dal.deactivate_other_active_subscriptions(
|
await subscription_dal.deactivate_other_active_subscriptions(
|
||||||
|
|||||||
@@ -99,6 +99,26 @@ class SubscriptionService:
|
|||||||
logging.warning(
|
logging.warning(
|
||||||
f"Local panel_uuid {current_local_panel_uuid} for TG user {user_id} also not found on panel. User might be deleted from panel or UUID desynced."
|
f"Local panel_uuid {current_local_panel_uuid} for TG user {user_id} also not found on panel. User might be deleted from panel or UUID desynced."
|
||||||
)
|
)
|
||||||
|
logging.info(
|
||||||
|
f"Creating new panel user '{panel_username_on_panel_standard}' for TG user {user_id}."
|
||||||
|
)
|
||||||
|
creation_response = await self.panel_service.create_panel_user(
|
||||||
|
username_on_panel=panel_username_on_panel_standard,
|
||||||
|
telegram_id=user_id,
|
||||||
|
specific_inbound_uuids=self.settings.parsed_user_inbound_uuids,
|
||||||
|
default_traffic_limit_bytes=self.settings.user_traffic_limit_bytes,
|
||||||
|
default_traffic_limit_strategy=self.settings.USER_TRAFFIC_STRATEGY,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
creation_response
|
||||||
|
and not creation_response.get("error")
|
||||||
|
and creation_response.get("response")
|
||||||
|
):
|
||||||
|
panel_user_obj_from_api = creation_response.get("response")
|
||||||
|
panel_user_created_or_linked_now = True
|
||||||
|
else:
|
||||||
|
await self._notify_admin_panel_user_creation_failed(user_id)
|
||||||
|
return None, None, None, False
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
@@ -108,7 +128,9 @@ class SubscriptionService:
|
|||||||
creation_response = await self.panel_service.create_panel_user(
|
creation_response = await self.panel_service.create_panel_user(
|
||||||
username_on_panel=panel_username_on_panel_standard,
|
username_on_panel=panel_username_on_panel_standard,
|
||||||
telegram_id=user_id,
|
telegram_id=user_id,
|
||||||
specific_inbound_uuids=self.settings.parsed_default_panel_user_inbound_uuids,
|
specific_inbound_uuids=self.settings.parsed_user_inbound_uuids,
|
||||||
|
default_traffic_limit_bytes=self.settings.user_traffic_limit_bytes,
|
||||||
|
default_traffic_limit_strategy=self.settings.USER_TRAFFIC_STRATEGY,
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
creation_response
|
creation_response
|
||||||
@@ -333,11 +355,11 @@ class SubscriptionService:
|
|||||||
),
|
),
|
||||||
"status": "ACTIVE",
|
"status": "ACTIVE",
|
||||||
"trafficLimitBytes": self.settings.trial_traffic_limit_bytes,
|
"trafficLimitBytes": self.settings.trial_traffic_limit_bytes,
|
||||||
"trafficLimitStrategy": self.settings.PANEL_USER_DEFAULT_TRAFFIC_STRATEGY,
|
"trafficLimitStrategy": self.settings.USER_TRAFFIC_STRATEGY,
|
||||||
}
|
}
|
||||||
if self.settings.parsed_default_panel_user_inbound_uuids:
|
if self.settings.parsed_user_inbound_uuids:
|
||||||
panel_update_payload["activeUserInbounds"] = (
|
panel_update_payload["activeUserInbounds"] = (
|
||||||
self.settings.parsed_default_panel_user_inbound_uuids
|
self.settings.parsed_user_inbound_uuids
|
||||||
)
|
)
|
||||||
elif panel_user_created_now:
|
elif panel_user_created_now:
|
||||||
panel_update_payload["activateAllInbounds"] = True
|
panel_update_payload["activateAllInbounds"] = True
|
||||||
@@ -460,7 +482,7 @@ class SubscriptionService:
|
|||||||
"duration_months": months,
|
"duration_months": months,
|
||||||
"is_active": True,
|
"is_active": True,
|
||||||
"status_from_panel": "ACTIVE",
|
"status_from_panel": "ACTIVE",
|
||||||
"traffic_limit_bytes": self.settings.PANEL_USER_DEFAULT_TRAFFIC_BYTES,
|
"traffic_limit_bytes": self.settings.user_traffic_limit_bytes,
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
"skip_notifications": provider == "tribute",
|
"skip_notifications": provider == "tribute",
|
||||||
}
|
}
|
||||||
@@ -481,12 +503,12 @@ class SubscriptionService:
|
|||||||
"+00:00", "Z"
|
"+00:00", "Z"
|
||||||
),
|
),
|
||||||
"status": "ACTIVE",
|
"status": "ACTIVE",
|
||||||
"trafficLimitBytes": self.settings.PANEL_USER_DEFAULT_TRAFFIC_BYTES,
|
"trafficLimitBytes": self.settings.user_traffic_limit_bytes,
|
||||||
"trafficLimitStrategy": self.settings.PANEL_USER_DEFAULT_TRAFFIC_STRATEGY,
|
"trafficLimitStrategy": self.settings.USER_TRAFFIC_STRATEGY,
|
||||||
}
|
}
|
||||||
if self.settings.parsed_default_panel_user_inbound_uuids:
|
if self.settings.parsed_user_inbound_uuids:
|
||||||
panel_update_payload["activeUserInbounds"] = (
|
panel_update_payload["activeUserInbounds"] = (
|
||||||
self.settings.parsed_default_panel_user_inbound_uuids
|
self.settings.parsed_user_inbound_uuids
|
||||||
)
|
)
|
||||||
elif panel_user_created_now:
|
elif panel_user_created_now:
|
||||||
panel_update_payload["activateAllInbounds"] = True
|
panel_update_payload["activateAllInbounds"] = True
|
||||||
@@ -554,7 +576,7 @@ class SubscriptionService:
|
|||||||
"duration_months": 0,
|
"duration_months": 0,
|
||||||
"is_active": True,
|
"is_active": True,
|
||||||
"status_from_panel": "ACTIVE_BONUS",
|
"status_from_panel": "ACTIVE_BONUS",
|
||||||
"traffic_limit_bytes": self.settings.PANEL_USER_DEFAULT_TRAFFIC_BYTES,
|
"traffic_limit_bytes": self.settings.user_traffic_limit_bytes,
|
||||||
}
|
}
|
||||||
await subscription_dal.deactivate_other_active_subscriptions(
|
await subscription_dal.deactivate_other_active_subscriptions(
|
||||||
session, panel_uuid, panel_sub_uuid
|
session, panel_uuid, panel_sub_uuid
|
||||||
|
|||||||
+17
-7
@@ -86,10 +86,9 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
PANEL_API_URL: Optional[str] = None
|
PANEL_API_URL: Optional[str] = None
|
||||||
PANEL_API_KEY: Optional[str] = None
|
PANEL_API_KEY: Optional[str] = None
|
||||||
PANEL_USER_DEFAULT_EXPIRE_DAYS: int = Field(default=1)
|
USER_TRAFFIC_LIMIT_GB: Optional[float] = Field(default=0.0)
|
||||||
PANEL_USER_DEFAULT_TRAFFIC_BYTES: int = Field(default=0)
|
USER_TRAFFIC_STRATEGY: str = Field(default="NO_RESET")
|
||||||
PANEL_USER_DEFAULT_TRAFFIC_STRATEGY: str = Field(default="NO_RESET")
|
USER_INBOUND_UUIDS: Optional[str] = Field(
|
||||||
PANEL_USER_DEFAULT_INBOUND_UUIDS: Optional[str] = Field(
|
|
||||||
default=None,
|
default=None,
|
||||||
description=
|
description=
|
||||||
"Comma-separated UUIDs of inbounds to activate for new panel users")
|
"Comma-separated UUIDs of inbounds to activate for new panel users")
|
||||||
@@ -102,6 +101,10 @@ class Settings(BaseSettings):
|
|||||||
WEB_SERVER_PORT: int = Field(default=8080)
|
WEB_SERVER_PORT: int = Field(default=8080)
|
||||||
LOGS_PAGE_SIZE: int = Field(default=10)
|
LOGS_PAGE_SIZE: int = Field(default=10)
|
||||||
|
|
||||||
|
SUBSCRIPTION_MINI_APP_URL: Optional[str] = Field(default=None)
|
||||||
|
|
||||||
|
START_COMMAND_DESCRIPTION: Optional[str] = Field(default=None)
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
def DATABASE_URL(self) -> str:
|
def DATABASE_URL(self) -> str:
|
||||||
@@ -139,11 +142,18 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
def parsed_default_panel_user_inbound_uuids(self) -> Optional[List[str]]:
|
def user_traffic_limit_bytes(self) -> int:
|
||||||
if self.PANEL_USER_DEFAULT_INBOUND_UUIDS:
|
if self.USER_TRAFFIC_LIMIT_GB is None or self.USER_TRAFFIC_LIMIT_GB <= 0:
|
||||||
|
return 0
|
||||||
|
return int(self.USER_TRAFFIC_LIMIT_GB * (1024**3))
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def parsed_user_inbound_uuids(self) -> Optional[List[str]]:
|
||||||
|
if self.USER_INBOUND_UUIDS:
|
||||||
return [
|
return [
|
||||||
uuid.strip()
|
uuid.strip()
|
||||||
for uuid in self.PANEL_USER_DEFAULT_INBOUND_UUIDS.split(',')
|
for uuid in self.USER_INBOUND_UUIDS.split(',')
|
||||||
if uuid.strip()
|
if uuid.strip()
|
||||||
]
|
]
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -96,6 +96,19 @@ async def update_payment_status_by_db_id(
|
|||||||
return payment
|
return payment
|
||||||
|
|
||||||
|
|
||||||
|
async def user_has_successful_payment_for_provider(
|
||||||
|
session: AsyncSession, user_id: int, provider: str) -> bool:
|
||||||
|
"""Check if a user has at least one successful payment for the provider."""
|
||||||
|
|
||||||
|
stmt = (select(Payment.payment_id)
|
||||||
|
.where(Payment.user_id == user_id,
|
||||||
|
Payment.provider == provider,
|
||||||
|
Payment.status == 'succeeded')
|
||||||
|
.limit(1))
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
return result.scalar_one_or_none() is not None
|
||||||
|
|
||||||
|
|
||||||
async def update_payment_status_by_yk_id(session: AsyncSession,
|
async def update_payment_status_by_yk_id(session: AsyncSession,
|
||||||
yookassa_payment_id: str,
|
yookassa_payment_id: str,
|
||||||
new_status: str) -> Optional[Payment]:
|
new_status: str) -> Optional[Payment]:
|
||||||
|
|||||||
@@ -231,3 +231,25 @@ async def set_skip_notifications_for_provider(
|
|||||||
Subscription.provider == provider).values(skip_notifications=skip))
|
Subscription.provider == provider).values(skip_notifications=skip))
|
||||||
result = await session.execute(stmt)
|
result = await session.execute(stmt)
|
||||||
return result.rowcount
|
return result.rowcount
|
||||||
|
|
||||||
|
|
||||||
|
async def get_active_subscriptions_for_autorenew(
|
||||||
|
session: AsyncSession, provider: str,
|
||||||
|
days_threshold: int = 1,
|
||||||
|
require_skip_flag: bool = True) -> List[Subscription]:
|
||||||
|
"""Fetch active subscriptions nearing expiration for auto-renew logic."""
|
||||||
|
|
||||||
|
now_utc = datetime.now(timezone.utc)
|
||||||
|
threshold_date = now_utc + timedelta(days=days_threshold)
|
||||||
|
|
||||||
|
conditions = [
|
||||||
|
Subscription.provider == provider,
|
||||||
|
Subscription.is_active == True,
|
||||||
|
Subscription.end_date <= threshold_date,
|
||||||
|
]
|
||||||
|
if require_skip_flag:
|
||||||
|
conditions.append(Subscription.skip_notifications == True)
|
||||||
|
|
||||||
|
stmt = select(Subscription).where(*conditions)
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
return result.scalars().all()
|
||||||
|
|||||||
@@ -214,5 +214,8 @@
|
|||||||
"subscription_ending_soon_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription ends on {end_date} (in {days_left} days).\n\nTo avoid interruption, please renew it in the main menu.",
|
"subscription_ending_soon_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription ends on {end_date} (in {days_left} days).\n\nTo avoid interruption, please renew it in the main menu.",
|
||||||
"subscription_cancelled_notification": "Your recurring subscription was cancelled. You will keep access until the paid period ends.",
|
"subscription_cancelled_notification": "Your recurring subscription was cancelled. You will keep access until the paid period ends.",
|
||||||
|
|
||||||
|
"admin_new_trial_notification": "\ud83c\udf21 User {user_id} activated a free trial until {end_date}.",
|
||||||
|
"admin_new_payment_notification": "\ud83d\udcb3 Payment received from user {user_id}: {months} mo. for {amount} {currency}.",
|
||||||
|
|
||||||
"error_unknown": "An unknown error occurred."
|
"error_unknown": "An unknown error occurred."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,5 +214,8 @@
|
|||||||
"subscription_ending_soon_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекает {end_date} (через {days_left} дн.).\n\nЧтобы не потерять доступ, пожалуйста, продлите ее заранее в главном меню бота.",
|
"subscription_ending_soon_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекает {end_date} (через {days_left} дн.).\n\nЧтобы не потерять доступ, пожалуйста, продлите ее заранее в главном меню бота.",
|
||||||
"subscription_cancelled_notification": "Ваша подписка отменена. Доступ сохранится до конца оплаченного периода.",
|
"subscription_cancelled_notification": "Ваша подписка отменена. Доступ сохранится до конца оплаченного периода.",
|
||||||
|
|
||||||
|
"admin_new_trial_notification": "\ud83c\udf21 Пользователь {user_id} активировал пробный период до {end_date}.",
|
||||||
|
"admin_new_payment_notification": "\ud83d\udcb3 Получен платеж от пользователя {user_id}: {months} мес. за {amount} {currency}.",
|
||||||
|
|
||||||
"error_unknown": "Произошла неизвестная ошибка."
|
"error_unknown": "Произошла неизвестная ошибка."
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user