import logging from pydantic_settings import BaseSettings ,SettingsConfigDict from pydantic import Field ,field_validator ,ValidationError 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") DB_NAME :str =Field (default ="bot_database.sqlite3") DEFAULT_LANGUAGE :str =Field (default ="ru") DEFAULT_CURRENCY_SYMBOL :str =Field (default ="RUB") SUPPORT_LINK :Optional [str ]=Field (default =None ,description ="Link to support contact (e.g., t.me/your_support_contact)") SERVER_STATUS_URL :Optional [str ]=Field (default =None ,description ="Link to server status page (e.g., Uptime Kuma)") 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 ,description ="Default email for YooKassa receipts (REQUIRED for 54-FZ)") YOOKASSA_VAT_CODE :int =Field (default =6 ,description ="YooKassa VAT code (e.g., 1=20%, 2=10%, 4=0%, 6=No VAT - check YooKassa docs!)") YOOKASSA_PAYMENT_MODE :str =Field (default ="full_prepayment",description ="YooKassa payment mode (e.g., full_prepayment, full_payment)") YOOKASSA_PAYMENT_SUBJECT :str =Field (default ="service",description ="YooKassa payment subject (e.g., service, commodity)") TELEGRAM_WEBHOOK_BASE_URL :Optional [str ]=None PRICE_1_MONTH :Optional [int ]=None PRICE_3_MONTHS :Optional [int ]=None PRICE_6_MONTHS :Optional [int ]=None PRICE_12_MONTHS :Optional [int ]=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_1_MONTH :Optional [int ]=3 REFERRAL_BONUS_DAYS_3_MONTHS :Optional [int ]=7 REFERRAL_BONUS_DAYS_6_MONTHS :Optional [int ]=15 REFERRAL_BONUS_DAYS_12_MONTHS :Optional [int ]=30 REFEREE_BONUS_DAYS_1_MONTH :Optional [int ]=1 REFEREE_BONUS_DAYS_3_MONTHS :Optional [int ]=3 REFEREE_BONUS_DAYS_6_MONTHS :Optional [int ]=7 REFEREE_BONUS_DAYS_12_MONTHS :Optional [int ]=15 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 ) 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 ) _admin_ids_list :Optional [List [int ]]=None @property def ADMIN_IDS (self )->List [int ]: if self ._admin_ids_list is None : if self .ADMIN_IDS_STR : try : self ._admin_ids_list =[int (admin_id .strip ())for admin_id in self .ADMIN_IDS_STR .split (',')if admin_id .strip ().isdigit ()] if not self ._admin_ids_list and self .ADMIN_IDS_STR : logging .error (f"ADMIN_IDS_STR ('{self.ADMIN_IDS_STR}') contains non-integer values or is malformed. No admin IDs loaded from string.") self ._admin_ids_list =[] except ValueError : logging .error (f"Invalid ADMIN_IDS_STR format: '{self.ADMIN_IDS_STR}'. Expected comma-separated integers.") self ._admin_ids_list =[] else : self ._admin_ids_list =[] return self ._admin_ids_list @property def PRIMARY_ADMIN_ID (self )->Optional [int ]: admin_ids_list =self .ADMIN_IDS if admin_ids_list : return admin_ids_list [0 ] return None @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 )) @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 @property def yookassa_webhook_path (self )->str :return "/webhook/yookassa" @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 @property def subscription_options (self )->Dict [int ,int ]: options :Dict [int ,int ]={}; if self .PRICE_1_MONTH is not None :options [1 ]=self .PRICE_1_MONTH if self .PRICE_3_MONTHS is not None :options [3 ]=self .PRICE_3_MONTHS if self .PRICE_6_MONTHS is not None :options [6 ]=self .PRICE_6_MONTHS if self .PRICE_12_MONTHS is not None :options [12 ]=self .PRICE_12_MONTHS return options @property def referral_bonus_inviter (self )->Dict [int ,int ]: bonuses :Dict [int ,int ]={}; if self .REFERRAL_BONUS_DAYS_1_MONTH is not None :bonuses [1 ]=self .REFERRAL_BONUS_DAYS_1_MONTH if self .REFERRAL_BONUS_DAYS_3_MONTHS is not None :bonuses [3 ]=self .REFERRAL_BONUS_DAYS_3_MONTHS if self .REFERRAL_BONUS_DAYS_6_MONTHS is not None :bonuses [6 ]=self .REFERRAL_BONUS_DAYS_6_MONTHS if self .REFERRAL_BONUS_DAYS_12_MONTHS is not None :bonuses [12 ]=self .REFERRAL_BONUS_DAYS_12_MONTHS return bonuses @property def referral_bonus_referee (self )->Dict [int ,int ]: bonuses :Dict [int ,int ]={}; if self .REFEREE_BONUS_DAYS_1_MONTH is not None :bonuses [1 ]=self .REFEREE_BONUS_DAYS_1_MONTH if self .REFEREE_BONUS_DAYS_3_MONTHS is not None :bonuses [3 ]=self .REFEREE_BONUS_DAYS_3_MONTHS if self .REFEREE_BONUS_DAYS_6_MONTHS is not None :bonuses [6 ]=self .REFEREE_BONUS_DAYS_6_MONTHS if self .REFEREE_BONUS_DAYS_12_MONTHS is not None :bonuses [12 ]=self .REFEREE_BONUS_DAYS_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.") except ValidationError as e : logging .critical (f"Pydantic validation error while loading settings: {e}") raise SystemExit (f"CRITICAL SETTINGS ERROR: {e}") return _settings_instance