223 lines
8.6 KiB
Python
223 lines
8.6 KiB
Python
import logging
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
from pydantic import Field, ValidationError, computed_field
|
|
from typing import Optional, List, Dict, Any
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
BOT_TOKEN: str
|
|
ADMIN_IDS_STR: str = Field(
|
|
default="",
|
|
alias="ADMIN_IDS",
|
|
description="Comma-separated list of admin Telegram User IDs")
|
|
|
|
POSTGRES_USER: str = Field(default="user")
|
|
POSTGRES_PASSWORD: str = Field(default="password")
|
|
POSTGRES_HOST: str = Field(default="localhost")
|
|
POSTGRES_PORT: int = Field(default=5432)
|
|
POSTGRES_DB: str = Field(default="vpn_shop_db")
|
|
|
|
DEFAULT_LANGUAGE: str = Field(default="ru")
|
|
DEFAULT_CURRENCY_SYMBOL: str = Field(default="RUB")
|
|
|
|
SUPPORT_LINK: Optional[str] = Field(default=None)
|
|
SERVER_STATUS_URL: Optional[str] = Field(default=None)
|
|
TERMS_OF_SERVICE_URL: Optional[str] = Field(default=None)
|
|
|
|
YOOKASSA_SHOP_ID: Optional[str] = None
|
|
YOOKASSA_SECRET_KEY: Optional[str] = None
|
|
YOOKASSA_WEBHOOK_BASE_URL: Optional[str] = None
|
|
YOOKASSA_RETURN_URL: Optional[str] = None
|
|
|
|
YOOKASSA_DEFAULT_RECEIPT_EMAIL: Optional[str] = Field(default=None)
|
|
YOOKASSA_VAT_CODE: int = Field(default=1)
|
|
YOOKASSA_PAYMENT_MODE: str = Field(default="full_prepayment")
|
|
YOOKASSA_PAYMENT_SUBJECT: str = Field(default="service")
|
|
|
|
TELEGRAM_WEBHOOK_BASE_URL: Optional[str] = None
|
|
|
|
PRICE_1_MONTH: Optional[int] = Field(default=None)
|
|
PRICE_3_MONTHS: Optional[int] = Field(default=None)
|
|
PRICE_6_MONTHS: Optional[int] = Field(default=None)
|
|
PRICE_12_MONTHS: Optional[int] = Field(default=None)
|
|
|
|
SUBSCRIPTION_EXPIRATION_NOTIFICATION_DAYS: int = Field(default=7)
|
|
SUBSCRIPTION_NOTIFICATION_HOUR_UTC: int = Field(default=9)
|
|
SUBSCRIPTION_NOTIFICATION_MINUTE_UTC: int = Field(default=0)
|
|
|
|
REFERRAL_BONUS_DAYS_INVITER_1_MONTH: Optional[int] = Field(
|
|
default=3, alias="REFERRAL_BONUS_DAYS_1_MONTH")
|
|
REFERRAL_BONUS_DAYS_INVITER_3_MONTHS: Optional[int] = Field(
|
|
default=7, alias="REFERRAL_BONUS_DAYS_3_MONTHS")
|
|
REFERRAL_BONUS_DAYS_INVITER_6_MONTHS: Optional[int] = Field(
|
|
default=15, alias="REFERRAL_BONUS_DAYS_6_MONTHS")
|
|
REFERRAL_BONUS_DAYS_INVITER_12_MONTHS: Optional[int] = Field(
|
|
default=30, alias="REFERRAL_BONUS_DAYS_12_MONTHS")
|
|
|
|
REFERRAL_BONUS_DAYS_REFEREE_1_MONTH: Optional[int] = Field(
|
|
default=1, alias="REFEREE_BONUS_DAYS_1_MONTH")
|
|
REFERRAL_BONUS_DAYS_REFEREE_3_MONTHS: Optional[int] = Field(
|
|
default=3, alias="REFEREE_BONUS_DAYS_3_MONTHS")
|
|
REFERRAL_BONUS_DAYS_REFEREE_6_MONTHS: Optional[int] = Field(
|
|
default=7, alias="REFEREE_BONUS_DAYS_6_MONTHS")
|
|
REFERRAL_BONUS_DAYS_REFEREE_12_MONTHS: Optional[int] = Field(
|
|
default=15, alias="REFEREE_BONUS_DAYS_12_MONTHS")
|
|
|
|
PANEL_API_URL: Optional[str] = None
|
|
PANEL_API_KEY: Optional[str] = None
|
|
PANEL_USER_DEFAULT_EXPIRE_DAYS: int = Field(default=1)
|
|
PANEL_USER_DEFAULT_TRAFFIC_BYTES: int = Field(default=0)
|
|
PANEL_USER_DEFAULT_TRAFFIC_STRATEGY: str = Field(default="NO_RESET")
|
|
PANEL_USER_DEFAULT_INBOUND_UUIDS: Optional[str] = Field(
|
|
default=None,
|
|
description=
|
|
"Comma-separated UUIDs of inbounds to activate for new panel users")
|
|
|
|
TRIAL_ENABLED: bool = Field(default=True)
|
|
TRIAL_DURATION_DAYS: int = Field(default=3)
|
|
TRIAL_TRAFFIC_LIMIT_GB: Optional[float] = Field(default=5.0)
|
|
|
|
WEB_SERVER_HOST: str = Field(default="0.0.0.0")
|
|
WEB_SERVER_PORT: int = Field(default=8080)
|
|
LOGS_PAGE_SIZE: int = Field(default=10)
|
|
|
|
@computed_field
|
|
@property
|
|
def DATABASE_URL(self) -> str:
|
|
return f"postgresql+asyncpg://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
|
|
|
|
@computed_field
|
|
@property
|
|
def ADMIN_IDS(self) -> List[int]:
|
|
if self.ADMIN_IDS_STR:
|
|
try:
|
|
return [
|
|
int(admin_id.strip())
|
|
for admin_id in self.ADMIN_IDS_STR.split(',')
|
|
if admin_id.strip().isdigit()
|
|
]
|
|
except ValueError:
|
|
logging.error(
|
|
f"Invalid ADMIN_IDS_STR format: '{self.ADMIN_IDS_STR}'. Expected comma-separated integers."
|
|
)
|
|
return []
|
|
return []
|
|
|
|
@computed_field
|
|
@property
|
|
def PRIMARY_ADMIN_ID(self) -> Optional[int]:
|
|
ids = self.ADMIN_IDS
|
|
return ids[0] if ids else None
|
|
|
|
@computed_field
|
|
@property
|
|
def trial_traffic_limit_bytes(self) -> int:
|
|
if self.TRIAL_TRAFFIC_LIMIT_GB is None or self.TRIAL_TRAFFIC_LIMIT_GB <= 0:
|
|
return 0
|
|
return int(self.TRIAL_TRAFFIC_LIMIT_GB * (1024**3))
|
|
|
|
@computed_field
|
|
@property
|
|
def parsed_default_panel_user_inbound_uuids(self) -> Optional[List[str]]:
|
|
if self.PANEL_USER_DEFAULT_INBOUND_UUIDS:
|
|
return [
|
|
uuid.strip()
|
|
for uuid in self.PANEL_USER_DEFAULT_INBOUND_UUIDS.split(',')
|
|
if uuid.strip()
|
|
]
|
|
return None
|
|
|
|
@computed_field
|
|
@property
|
|
def yookassa_webhook_path(self) -> str:
|
|
|
|
return "/webhook/yookassa"
|
|
|
|
@computed_field
|
|
@property
|
|
def yookassa_full_webhook_url(self) -> Optional[str]:
|
|
if self.YOOKASSA_WEBHOOK_BASE_URL:
|
|
|
|
return f"{self.YOOKASSA_WEBHOOK_BASE_URL.rstrip('/')}{self.yookassa_webhook_path}"
|
|
return None
|
|
|
|
@computed_field
|
|
@property
|
|
def subscription_options(self) -> Dict[int, float]:
|
|
options: Dict[int, float] = {}
|
|
|
|
if self.PRICE_1_MONTH is not None:
|
|
options[1] = float(self.PRICE_1_MONTH / 100.0)
|
|
if self.PRICE_3_MONTHS is not None:
|
|
options[3] = float(self.PRICE_3_MONTHS / 100.0)
|
|
if self.PRICE_6_MONTHS is not None:
|
|
options[6] = float(self.PRICE_6_MONTHS / 100.0)
|
|
if self.PRICE_12_MONTHS is not None:
|
|
options[12] = float(self.PRICE_12_MONTHS / 100.0)
|
|
return options
|
|
|
|
@computed_field
|
|
@property
|
|
def referral_bonus_inviter(self) -> Dict[int, int]:
|
|
bonuses: Dict[int, int] = {}
|
|
if self.REFERRAL_BONUS_DAYS_INVITER_1_MONTH is not None:
|
|
bonuses[1] = self.REFERRAL_BONUS_DAYS_INVITER_1_MONTH
|
|
if self.REFERRAL_BONUS_DAYS_INVITER_3_MONTHS is not None:
|
|
bonuses[3] = self.REFERRAL_BONUS_DAYS_INVITER_3_MONTHS
|
|
if self.REFERRAL_BONUS_DAYS_INVITER_6_MONTHS is not None:
|
|
bonuses[6] = self.REFERRAL_BONUS_DAYS_INVITER_6_MONTHS
|
|
if self.REFERRAL_BONUS_DAYS_INVITER_12_MONTHS is not None:
|
|
bonuses[12] = self.REFERRAL_BONUS_DAYS_INVITER_12_MONTHS
|
|
return bonuses
|
|
|
|
@computed_field
|
|
@property
|
|
def referral_bonus_referee(self) -> Dict[int, int]:
|
|
bonuses: Dict[int, int] = {}
|
|
if self.REFERRAL_BONUS_DAYS_REFEREE_1_MONTH is not None:
|
|
bonuses[1] = self.REFERRAL_BONUS_DAYS_REFEREE_1_MONTH
|
|
if self.REFERRAL_BONUS_DAYS_REFEREE_3_MONTHS is not None:
|
|
bonuses[3] = self.REFERRAL_BONUS_DAYS_REFEREE_3_MONTHS
|
|
if self.REFERRAL_BONUS_DAYS_REFEREE_6_MONTHS is not None:
|
|
bonuses[6] = self.REFERRAL_BONUS_DAYS_REFEREE_6_MONTHS
|
|
if self.REFERRAL_BONUS_DAYS_REFEREE_12_MONTHS is not None:
|
|
bonuses[12] = self.REFERRAL_BONUS_DAYS_REFEREE_12_MONTHS
|
|
return bonuses
|
|
|
|
model_config = SettingsConfigDict(env_file='.env',
|
|
env_file_encoding='utf-8',
|
|
extra='ignore',
|
|
populate_by_name=True)
|
|
|
|
|
|
_settings_instance: Optional[Settings] = None
|
|
|
|
|
|
def get_settings() -> Settings:
|
|
global _settings_instance
|
|
if _settings_instance is None:
|
|
try:
|
|
_settings_instance = Settings()
|
|
if not _settings_instance.ADMIN_IDS:
|
|
logging.warning(
|
|
"CRITICAL: ADMIN_IDS not set or contains no valid integer IDs in .env. "
|
|
"Admin functionality will be restricted.")
|
|
|
|
if not _settings_instance.PANEL_API_URL:
|
|
logging.warning(
|
|
"CRITICAL: PANEL_API_URL is not set. Panel integration will not work."
|
|
)
|
|
if not _settings_instance.YOOKASSA_SHOP_ID or not _settings_instance.YOOKASSA_SECRET_KEY:
|
|
logging.warning(
|
|
"CRITICAL: YooKassa credentials (SHOP_ID or SECRET_KEY) are not set. Payments will not work."
|
|
)
|
|
|
|
except ValidationError as e:
|
|
logging.critical(
|
|
f"Pydantic validation error while loading settings: {e}")
|
|
|
|
raise SystemExit(
|
|
f"CRITICAL SETTINGS ERROR: {e}. Please check your .env file and Settings model."
|
|
)
|
|
return _settings_instance
|