fix(env): stabilize .env parsing and add feature toggles
- Handle dotenv placeholders like 'KEY= # comment' without validation crashes - Add REQUIRED_CHANNEL_SUBSCRIBE_TO_USE to explicitly enable channel gate - Add REFERRAL_ENABLED to fully disable referral flow and bonuses - Hide referral UI/actions when disabled and ignore ref start params - Update .env.example defaults and README docs for new flags
This commit is contained in:
+19
-17
@@ -17,14 +17,15 @@ DEFAULT_CURRENCY_SYMBOL="RUB" #
|
|||||||
SUPPORT_LINK=https://t.me/your_support_link # Link to the support chat
|
SUPPORT_LINK=https://t.me/your_support_link # Link to the support chat
|
||||||
SERVER_STATUS_URL=https://status.yourdomain.tld/status/your_service # Link to the server status page
|
SERVER_STATUS_URL=https://status.yourdomain.tld/status/your_service # Link to the server status page
|
||||||
TERMS_OF_SERVICE_URL=https://example.com/tos # Link to the terms of service
|
TERMS_OF_SERVICE_URL=https://example.com/tos # Link to the terms of service
|
||||||
SUBSCRIPTION_MINI_APP_URL= # URL of the subscription mini-app
|
SUBSCRIPTION_MINI_APP_URL="" # URL of the subscription mini-app
|
||||||
START_COMMAND_DESCRIPTION= # Description of the /start command
|
START_COMMAND_DESCRIPTION="" # Description of the /start command
|
||||||
DISABLE_WELCOME_MESSAGE= # Disable the welcome message
|
DISABLE_WELCOME_MESSAGE=False # Disable the welcome message
|
||||||
MY_DEVICES_SECTION_ENABLED=False # Enable the My Devices section in the subscription menu
|
MY_DEVICES_SECTION_ENABLED=False # Enable the My Devices section in the subscription menu
|
||||||
USER_HWID_DEVICE_LIMIT=0 # Default HWID/device limit for panel users (0 = unlimited)
|
USER_HWID_DEVICE_LIMIT=0 # Default HWID/device limit for panel users (0 = unlimited)
|
||||||
|
|
||||||
# Required channel subscription
|
# Required channel subscription
|
||||||
REQUIRED_CHANNEL_ID= # Telegram channel ID (e.g. -1001234567890) the user must join
|
REQUIRED_CHANNEL_SUBSCRIBE_TO_USE=False # Enable/disable the required channel gate
|
||||||
|
REQUIRED_CHANNEL_ID="" # Telegram channel ID (e.g. -1001234567890) the user must join
|
||||||
REQUIRED_CHANNEL_LINK=https://t.me/your_channel # Optional: public link/invite button text opens
|
REQUIRED_CHANNEL_LINK=https://t.me/your_channel # Optional: public link/invite button text opens
|
||||||
|
|
||||||
# Webhook Base URL (used for Telegram and payment providers)
|
# Webhook Base URL (used for Telegram and payment providers)
|
||||||
@@ -71,21 +72,21 @@ CRYPTOPAY_ASSET=RUB #
|
|||||||
|
|
||||||
# Platega Payment Gateway Configuration
|
# Platega Payment Gateway Configuration
|
||||||
PLATEGA_BASE_URL=https://app.platega.io # Base API URL
|
PLATEGA_BASE_URL=https://app.platega.io # Base API URL
|
||||||
PLATEGA_MERCHANT_ID= # Your MerchantId from Platega
|
PLATEGA_MERCHANT_ID="" # Your MerchantId from Platega
|
||||||
PLATEGA_SECRET= # API secret from Platega
|
PLATEGA_SECRET="" # API secret from Platega
|
||||||
PLATEGA_PAYMENT_METHOD=2 # Payment method ID (2=SBP QR, 10=RU cards, 12=International, 13=Crypto)
|
PLATEGA_PAYMENT_METHOD=2 # Payment method ID (2=SBP QR, 10=RU cards, 12=International, 13=Crypto)
|
||||||
PLATEGA_RETURN_URL= # Optional: redirect after successful payment (defaults to bot link)
|
PLATEGA_RETURN_URL="" # Optional: redirect after successful payment (defaults to bot link)
|
||||||
PLATEGA_FAILED_URL= # Optional: redirect after failed/cancelled payment (defaults to return URL)
|
PLATEGA_FAILED_URL="" # Optional: redirect after failed/cancelled payment (defaults to return URL)
|
||||||
|
|
||||||
# SeverPay Payment Gateway Configuration
|
# SeverPay Payment Gateway Configuration
|
||||||
SEVERPAY_BASE_URL=https://severpay.io/api/merchant # Base API URL
|
SEVERPAY_BASE_URL=https://severpay.io/api/merchant # Base API URL
|
||||||
SEVERPAY_MID= # Your MID from SeverPay
|
SEVERPAY_MID="" # Your MID from SeverPay
|
||||||
SEVERPAY_TOKEN= # API token/secret for signing requests
|
SEVERPAY_TOKEN="" # API token/secret for signing requests
|
||||||
SEVERPAY_RETURN_URL= # Optional: redirect URL after payment (defaults to bot link)
|
SEVERPAY_RETURN_URL="" # Optional: redirect URL after payment (defaults to bot link)
|
||||||
SEVERPAY_LIFETIME_MINUTES= # Optional: payment link lifetime in minutes (30-4320, leave empty for default)
|
SEVERPAY_LIFETIME_MINUTES="" # Optional: payment link lifetime in minutes (30-4320, leave empty for default)
|
||||||
|
|
||||||
# Tribute Payment Gateway Configuration
|
# Tribute Payment Gateway Configuration
|
||||||
TRIBUTE_API_KEY= # API key for verifying Tribute webhook signatures
|
TRIBUTE_API_KEY="" # API key for verifying Tribute webhook signatures
|
||||||
TRIBUTE_SKIP_NOTIFICATIONS=True # Skip renewal notifications for Tribute payments
|
TRIBUTE_SKIP_NOTIFICATIONS=True # Skip renewal notifications for Tribute payments
|
||||||
TRIBUTE_SKIP_CANCELLATION_NOTIFICATIONS=False # Skip cancellation notifications for Tribute payments
|
TRIBUTE_SKIP_CANCELLATION_NOTIFICATIONS=False # Skip cancellation notifications for Tribute payments
|
||||||
|
|
||||||
@@ -121,6 +122,7 @@ SUBSCRIPTION_NOTIFY_AFTER_EXPIRE=True #
|
|||||||
SUBSCRIPTION_NOTIFY_DAYS_BEFORE=3 # Days before expiration to notify
|
SUBSCRIPTION_NOTIFY_DAYS_BEFORE=3 # Days before expiration to notify
|
||||||
|
|
||||||
|
|
||||||
|
REFERRAL_ENABLED=True # Enable/disable the referral system
|
||||||
REFERRAL_ONE_BONUS_PER_REFEREE=False # Give a bonus only once per referee
|
REFERRAL_ONE_BONUS_PER_REFEREE=False # Give a bonus only once per referee
|
||||||
LEGACY_REFS=true # Allow ref_<tg_id> links. Leave unset/true unless you want to disable old links
|
LEGACY_REFS=true # Allow ref_<tg_id> links. Leave unset/true unless you want to disable old links
|
||||||
# Referral Bonus Days
|
# Referral Bonus Days
|
||||||
@@ -138,7 +140,7 @@ REFEREE_BONUS_DAYS_12_MONTHS=15
|
|||||||
# Panel API Configuration
|
# Panel API Configuration
|
||||||
PANEL_API_URL=http://your_panel_api_url/api # URL of the panel API
|
PANEL_API_URL=http://your_panel_api_url/api # URL of the panel API
|
||||||
PANEL_API_KEY=your_panel_api_key # Panel API key
|
PANEL_API_KEY=your_panel_api_key # Panel API key
|
||||||
PANEL_WEBHOOK_SECRET= # secret used to verify panel webhook signatures
|
PANEL_WEBHOOK_SECRET="" # secret used to verify panel webhook signatures
|
||||||
|
|
||||||
# User traffic limits (applied for all users)
|
# User traffic limits (applied for all users)
|
||||||
# 0 means unlimited
|
# 0 means unlimited
|
||||||
@@ -148,7 +150,7 @@ USER_TRAFFIC_STRATEGY="NO_RESET" #
|
|||||||
# Default Internal Squads for Users (Optional, comma-separated UUIDs)
|
# Default Internal Squads for Users (Optional, comma-separated UUIDs)
|
||||||
USER_SQUAD_UUIDS=uuid1,uuid2,uuid3
|
USER_SQUAD_UUIDS=uuid1,uuid2,uuid3
|
||||||
# Default External Squad for Users (Optional, single UUID)
|
# Default External Squad for Users (Optional, single UUID)
|
||||||
USER_EXTERNAL_SQUAD_UUID= # Optional: UUID from Remnawave External Squads to auto-link new panel users
|
USER_EXTERNAL_SQUAD_UUID="" # Optional: UUID from Remnawave External Squads to auto-link new panel users
|
||||||
|
|
||||||
# Trial Settings
|
# Trial Settings
|
||||||
TRIAL_ENABLED=True # Enable the trial period
|
TRIAL_ENABLED=True # Enable the trial period
|
||||||
@@ -158,7 +160,7 @@ TRIAL_TRAFFIC_STRATEGY="NO_RESET" #
|
|||||||
|
|
||||||
# Connection link handling (happ crypt4)
|
# Connection link handling (happ crypt4)
|
||||||
CRYPT4_ENABLED=False # Enable happ crypt4 encryption for subscription URLs
|
CRYPT4_ENABLED=False # Enable happ crypt4 encryption for subscription URLs
|
||||||
CRYPT4_REDIRECT_URL= # Base redirect to wrap the connect button, e.g. https://redir.example.com?url=
|
CRYPT4_REDIRECT_URL="" # Base redirect to wrap the connect button, e.g. https://redir.example.com?url=
|
||||||
|
|
||||||
# Web Server Settings (for handling webhooks)
|
# Web Server Settings (for handling webhooks)
|
||||||
WEB_SERVER_HOST="0.0.0.0"
|
WEB_SERVER_HOST="0.0.0.0"
|
||||||
@@ -170,7 +172,7 @@ LOG_LEVEL=INFO #
|
|||||||
|
|
||||||
# Admin Logging Configuration
|
# Admin Logging Configuration
|
||||||
LOG_CHAT_ID=-1001234567890 # Telegram chat/group ID for admin notifications
|
LOG_CHAT_ID=-1001234567890 # Telegram chat/group ID for admin notifications
|
||||||
LOG_THREAD_ID= # Optional: Thread ID for supergroup messages
|
LOG_THREAD_ID="" # Optional: Thread ID for supergroup messages
|
||||||
LOG_NEW_USERS=True # Log new user registrations
|
LOG_NEW_USERS=True # Log new user registrations
|
||||||
LOG_PAYMENTS=True # Log payments
|
LOG_PAYMENTS=True # Log payments
|
||||||
LOG_PROMO_ACTIVATIONS=True # Log promo code activations
|
LOG_PROMO_ACTIVATIONS=True # Log promo code activations
|
||||||
|
|||||||
@@ -68,8 +68,10 @@
|
|||||||
| `SUPPORT_LINK` | (Опционально) Ссылка на поддержку. | `https://t.me/your_support` |
|
| `SUPPORT_LINK` | (Опционально) Ссылка на поддержку. | `https://t.me/your_support` |
|
||||||
| `SUBSCRIPTION_MINI_APP_URL` | (Опционально) URL Mini App для показа подписки. | `https://t.me/your_bot/app` |
|
| `SUBSCRIPTION_MINI_APP_URL` | (Опционально) URL Mini App для показа подписки. | `https://t.me/your_bot/app` |
|
||||||
| `MY_DEVICES_SECTION_ENABLED` | Включить раздел «Мои устройства» в меню подписки (`true`/`false`). | `false` |
|
| `MY_DEVICES_SECTION_ENABLED` | Включить раздел «Мои устройства» в меню подписки (`true`/`false`). | `false` |
|
||||||
| `REQUIRED_CHANNEL_ID` | (Опционально) ID канала, на который пользователь должен подписаться перед использованием. Оставьте пустым, если проверка не нужна. | `-1001234567890` |
|
| `REQUIRED_CHANNEL_SUBSCRIBE_TO_USE` | Включить/выключить обязательную проверку подписки на канал (`true`/`false`). | `false` |
|
||||||
|
| `REQUIRED_CHANNEL_ID` | ID канала для проверки подписки. Используется, только если `REQUIRED_CHANNEL_SUBSCRIBE_TO_USE=true`. | `-1001234567890` |
|
||||||
| `REQUIRED_CHANNEL_LINK` | (Опционально) Публичная ссылка или invite на канал для кнопки «Проверить подписку». | `https://t.me/your_channel` |
|
| `REQUIRED_CHANNEL_LINK` | (Опционально) Публичная ссылка или invite на канал для кнопки «Проверить подписку». | `https://t.me/your_channel` |
|
||||||
|
| `REFERRAL_ENABLED` | Включить/выключить реферальную систему полностью (`true`/`false`). | `true` |
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
@@ -175,7 +177,7 @@
|
|||||||
docker compose logs -f remnawave-tg-shop
|
docker compose logs -f remnawave-tg-shop
|
||||||
```
|
```
|
||||||
|
|
||||||
> 💡 Если включена проверка подписки на канал (`REQUIRED_CHANNEL_ID`), добавьте бота администратором в этот канал. Пользователь увидит кнопку «Проверить подписку», и, после первого успешного подтверждения, дальнейшие действия блокироваться не будут.
|
> 💡 Если включена проверка подписки (`REQUIRED_CHANNEL_SUBSCRIBE_TO_USE=true`), добавьте бота администратором в канал из `REQUIRED_CHANNEL_ID`. Пользователь увидит кнопку «Проверить подписку», и после успешного подтверждения доступ продолжится.
|
||||||
|
|
||||||
## Подробная инструкция для развертывания на сервере с панелью Remnawave
|
## Подробная инструкция для развертывания на сервере с панелью Remnawave
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,15 @@ async def referral_command_handler(event: Union[types.Message,
|
|||||||
|
|
||||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||||
|
|
||||||
|
if not settings.REFERRAL_ENABLED:
|
||||||
|
await target_message_obj.answer(
|
||||||
|
_("referral_no_bonuses_configured"),
|
||||||
|
reply_markup=get_back_to_main_menu_markup(current_lang, i18n),
|
||||||
|
)
|
||||||
|
if isinstance(event, types.CallbackQuery):
|
||||||
|
await event.answer()
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bot_info = await bot.get_me()
|
bot_info = await bot.get_me()
|
||||||
bot_username = bot_info.username
|
bot_username = bot_info.username
|
||||||
@@ -136,6 +145,9 @@ async def referral_action_handler(callback: types.CallbackQuery, settings: Setti
|
|||||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||||
|
|
||||||
if action == "share_message":
|
if action == "share_message":
|
||||||
|
if not settings.REFERRAL_ENABLED:
|
||||||
|
await callback.answer(_("referral_no_bonuses_configured"), show_alert=True)
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
bot_info = await bot.get_me()
|
bot_info = await bot.get_me()
|
||||||
bot_username = bot_info.username
|
bot_username = bot_info.username
|
||||||
|
|||||||
@@ -134,8 +134,15 @@ async def ensure_required_channel_subscription(
|
|||||||
Verify that the user is a member of the required channel (if configured).
|
Verify that the user is a member of the required channel (if configured).
|
||||||
Returns True when access can proceed, False when user must subscribe first.
|
Returns True when access can proceed, False when user must subscribe first.
|
||||||
"""
|
"""
|
||||||
|
if not settings.REQUIRED_CHANNEL_SUBSCRIBE_TO_USE:
|
||||||
|
return True
|
||||||
|
|
||||||
required_channel_id = settings.REQUIRED_CHANNEL_ID
|
required_channel_id = settings.REQUIRED_CHANNEL_ID
|
||||||
if not required_channel_id:
|
if not required_channel_id:
|
||||||
|
logging.warning(
|
||||||
|
"REQUIRED_CHANNEL_SUBSCRIBE_TO_USE is enabled but REQUIRED_CHANNEL_ID is not set. "
|
||||||
|
"Channel gate is skipped."
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if isinstance(event, types.CallbackQuery):
|
if isinstance(event, types.CallbackQuery):
|
||||||
@@ -327,7 +334,7 @@ async def start_command_handler(message: types.Message,
|
|||||||
promo_code_to_apply: Optional[str] = None
|
promo_code_to_apply: Optional[str] = None
|
||||||
ad_start_param: Optional[str] = None
|
ad_start_param: Optional[str] = None
|
||||||
|
|
||||||
if ref_match:
|
if ref_match and settings.REFERRAL_ENABLED:
|
||||||
raw_ref_value = ref_match.group(1)
|
raw_ref_value = ref_match.group(1)
|
||||||
if raw_ref_value.isdigit():
|
if raw_ref_value.isdigit():
|
||||||
if settings.LEGACY_REFS:
|
if settings.LEGACY_REFS:
|
||||||
@@ -345,6 +352,11 @@ async def start_command_handler(message: types.Message,
|
|||||||
session, normalized_code)
|
session, normalized_code)
|
||||||
if ref_user and ref_user.user_id != user_id:
|
if ref_user and ref_user.user_id != user_id:
|
||||||
referred_by_user_id = ref_user.user_id
|
referred_by_user_id = ref_user.user_id
|
||||||
|
elif ref_match and not settings.REFERRAL_ENABLED:
|
||||||
|
logging.info(
|
||||||
|
"User %s started with referral parameter while referral system is disabled.",
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
elif promo_match:
|
elif promo_match:
|
||||||
promo_code_to_apply = promo_match.group(1)
|
promo_code_to_apply = promo_match.group(1)
|
||||||
logging.info(f"User {user_id} started with promo code: {promo_code_to_apply}")
|
logging.info(f"User {user_id} started with promo code: {promo_code_to_apply}")
|
||||||
@@ -663,6 +675,10 @@ async def main_action_callback_handler(
|
|||||||
promo_code_service: PromoCodeService, session: AsyncSession):
|
promo_code_service: PromoCodeService, session: AsyncSession):
|
||||||
action = callback.data.split(":")[1]
|
action = callback.data.split(":")[1]
|
||||||
user_id = callback.from_user.id
|
user_id = callback.from_user.id
|
||||||
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||||
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||||
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
|
||||||
|
) if i18n else key
|
||||||
|
|
||||||
from . import subscription as user_subscription_handlers
|
from . import subscription as user_subscription_handlers
|
||||||
from . import referral as user_referral_handlers
|
from . import referral as user_referral_handlers
|
||||||
@@ -685,6 +701,10 @@ async def main_action_callback_handler(
|
|||||||
callback, i18n_data, settings, panel_service, subscription_service,
|
callback, i18n_data, settings, panel_service, subscription_service,
|
||||||
session, bot)
|
session, bot)
|
||||||
elif action == "referral":
|
elif action == "referral":
|
||||||
|
if not settings.REFERRAL_ENABLED:
|
||||||
|
await callback.answer(_("referral_no_bonuses_configured"),
|
||||||
|
show_alert=True)
|
||||||
|
return
|
||||||
await user_referral_handlers.referral_command_handler(
|
await user_referral_handlers.referral_command_handler(
|
||||||
callback, settings, i18n_data, referral_service, bot, session)
|
callback, settings, i18n_data, referral_service, bot, session)
|
||||||
elif action == "apply_promo":
|
elif action == "apply_promo":
|
||||||
@@ -711,7 +731,4 @@ async def main_action_callback_handler(
|
|||||||
session,
|
session,
|
||||||
is_edit=False)
|
is_edit=False)
|
||||||
else:
|
else:
|
||||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
|
||||||
_ = lambda key, **kwargs: i18n.gettext(
|
|
||||||
i18n_data.get("current_language"), key, **kw) if i18n else key
|
|
||||||
await callback.answer(_("main_menu_unknown_action"), show_alert=True)
|
await callback.answer(_("main_menu_unknown_action"), show_alert=True)
|
||||||
|
|||||||
@@ -28,13 +28,16 @@ def get_main_menu_inline_keyboard(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
referral_button = InlineKeyboardButton(
|
|
||||||
text=_(key="menu_referral_inline"),
|
|
||||||
callback_data="main_action:referral")
|
|
||||||
promo_button = InlineKeyboardButton(
|
promo_button = InlineKeyboardButton(
|
||||||
text=_(key="menu_apply_promo_button"),
|
text=_(key="menu_apply_promo_button"),
|
||||||
callback_data="main_action:apply_promo")
|
callback_data="main_action:apply_promo")
|
||||||
|
if settings.REFERRAL_ENABLED:
|
||||||
|
referral_button = InlineKeyboardButton(
|
||||||
|
text=_(key="menu_referral_inline"),
|
||||||
|
callback_data="main_action:referral")
|
||||||
builder.row(referral_button, promo_button)
|
builder.row(referral_button, promo_button)
|
||||||
|
else:
|
||||||
|
builder.row(promo_button)
|
||||||
|
|
||||||
language_button = InlineKeyboardButton(
|
language_button = InlineKeyboardButton(
|
||||||
text=_(key="menu_language_settings_inline"),
|
text=_(key="menu_language_settings_inline"),
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ class ReferralService:
|
|||||||
current_payment_db_id: Optional[int] = None,
|
current_payment_db_id: Optional[int] = None,
|
||||||
skip_if_active_before_payment: bool = True) -> Dict[str, Any]:
|
skip_if_active_before_payment: bool = True) -> Dict[str, Any]:
|
||||||
|
|
||||||
|
if not getattr(self.settings, "REFERRAL_ENABLED", True):
|
||||||
|
return {
|
||||||
|
"referee_bonus_applied_days": None,
|
||||||
|
"referee_new_end_date": None,
|
||||||
|
"inviter_bonus_applied_flag": False,
|
||||||
|
}
|
||||||
|
|
||||||
referee_final_end_date: Optional[datetime] = None
|
referee_final_end_date: Optional[datetime] = None
|
||||||
referee_bonus_applied_days: Optional[int] = None
|
referee_bonus_applied_days: Optional[int] = None
|
||||||
inviter_bonus_successfully_applied = False
|
inviter_bonus_successfully_applied = False
|
||||||
@@ -260,6 +267,9 @@ class ReferralService:
|
|||||||
async def generate_referral_link(self, session: AsyncSession,
|
async def generate_referral_link(self, session: AsyncSession,
|
||||||
bot_username: str,
|
bot_username: str,
|
||||||
inviter_user_id: int) -> Optional[str]:
|
inviter_user_id: int) -> Optional[str]:
|
||||||
|
if not getattr(self.settings, "REFERRAL_ENABLED", True):
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = await user_dal.get_user_by_id(session, inviter_user_id)
|
user = await user_dal.get_user_by_id(session, inviter_user_id)
|
||||||
if not user:
|
if not user:
|
||||||
|
|||||||
+36
-8
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
from pydantic import Field, ValidationError, computed_field, field_validator
|
from pydantic import Field, ValidationError, computed_field, field_validator, model_validator
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
@@ -23,6 +23,10 @@ class Settings(BaseSettings):
|
|||||||
SUPPORT_LINK: Optional[str] = Field(default=None)
|
SUPPORT_LINK: Optional[str] = Field(default=None)
|
||||||
SERVER_STATUS_URL: Optional[str] = Field(default=None)
|
SERVER_STATUS_URL: Optional[str] = Field(default=None)
|
||||||
TERMS_OF_SERVICE_URL: Optional[str] = Field(default=None)
|
TERMS_OF_SERVICE_URL: Optional[str] = Field(default=None)
|
||||||
|
REQUIRED_CHANNEL_SUBSCRIBE_TO_USE: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="Require users to subscribe to REQUIRED_CHANNEL_ID before using the bot",
|
||||||
|
)
|
||||||
REQUIRED_CHANNEL_ID: Optional[int] = Field(
|
REQUIRED_CHANNEL_ID: Optional[int] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Telegram channel ID the user must join to access the bot")
|
description="Telegram channel ID the user must join to access the bot")
|
||||||
@@ -168,6 +172,10 @@ class Settings(BaseSettings):
|
|||||||
default=True,
|
default=True,
|
||||||
description="When true, referral bonuses (for inviter and referee) are applied only once per invited user - on their first successful payment."
|
description="When true, referral bonuses (for inviter and referee) are applied only once per invited user - on their first successful payment."
|
||||||
)
|
)
|
||||||
|
REFERRAL_ENABLED: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="Enable referral links, referral menu and referral bonuses",
|
||||||
|
)
|
||||||
LEGACY_REFS: bool = Field(
|
LEGACY_REFS: bool = Field(
|
||||||
default=True,
|
default=True,
|
||||||
description="Allow legacy referral links like ref_<telegram_id> to continue working. Defaults to True when unset."
|
description="Allow legacy referral links like ref_<telegram_id> to continue working. Defaults to True when unset."
|
||||||
@@ -530,13 +538,24 @@ class Settings(BaseSettings):
|
|||||||
return "INFO"
|
return "INFO"
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@field_validator('LOG_CHAT_ID', 'LOG_THREAD_ID', mode='before')
|
@model_validator(mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_optional_int_fields(cls, v):
|
def drop_comment_placeholder_values(cls, values: Any):
|
||||||
"""Convert empty strings to None for optional integer fields"""
|
"""
|
||||||
if isinstance(v, str) and v.strip() == '':
|
dotenv parses lines like `KEY= # comment` as `"# comment"`.
|
||||||
return None
|
Treat such values as unset so defaults/optionals work as expected.
|
||||||
return v
|
"""
|
||||||
|
if not isinstance(values, dict):
|
||||||
|
return values
|
||||||
|
|
||||||
|
sanitized: Dict[str, Any] = {}
|
||||||
|
for key, value in values.items():
|
||||||
|
if isinstance(value, str):
|
||||||
|
trimmed = value.strip()
|
||||||
|
if trimmed == "#" or trimmed.startswith("# "):
|
||||||
|
continue
|
||||||
|
sanitized[key] = value
|
||||||
|
return sanitized
|
||||||
|
|
||||||
@field_validator(
|
@field_validator(
|
||||||
'REQUIRED_CHANNEL_LINK',
|
'REQUIRED_CHANNEL_LINK',
|
||||||
@@ -552,7 +571,16 @@ class Settings(BaseSettings):
|
|||||||
return None
|
return None
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@field_validator('USER_HWID_DEVICE_LIMIT', 'SEVERPAY_MID', 'SEVERPAY_LIFETIME_MINUTES', mode='before')
|
@field_validator(
|
||||||
|
'REQUIRED_CHANNEL_ID',
|
||||||
|
'FREEKASSA_PAYMENT_METHOD_ID',
|
||||||
|
'USER_HWID_DEVICE_LIMIT',
|
||||||
|
'SEVERPAY_MID',
|
||||||
|
'SEVERPAY_LIFETIME_MINUTES',
|
||||||
|
'LOG_CHAT_ID',
|
||||||
|
'LOG_THREAD_ID',
|
||||||
|
mode='before'
|
||||||
|
)
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_optional_int(cls, v):
|
def validate_optional_int(cls, v):
|
||||||
if isinstance(v, str):
|
if isinstance(v, str):
|
||||||
|
|||||||
Reference in New Issue
Block a user