Migrating to Postgres from sqlite3

This commit is contained in:
machka
2025-05-20 18:41:32 +00:00
parent ac2f83c061
commit a56d804b58
38 changed files with 5017 additions and 4389 deletions
+111 -86
View File
@@ -1,6 +1,6 @@
import logging
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field, field_validator, ValidationError
from pydantic import Field, ValidationError, computed_field
from typing import Optional, List, Dict, Any
@@ -11,64 +11,67 @@ class Settings(BaseSettings):
alias="ADMIN_IDS",
description="Comma-separated list of admin Telegram User IDs")
DB_NAME: str = Field(default="bot_database.sqlite3")
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,
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)")
TERMS_OF_SERVICE_URL: Optional[str] = Field(
default=None, description="Link to Terms of Use page")
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,
description="Default email for YooKassa receipts (REQUIRED for 54-FZ)")
YOOKASSA_VAT_CODE: int = Field(
default=1, description="YooKassa VAT code (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)")
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] = None
PRICE_3_MONTHS: Optional[int] = None
PRICE_6_MONTHS: Optional[int] = None
PRICE_12_MONTHS: Optional[int] = 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_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
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)
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)
@@ -78,45 +81,42 @@ class Settings(BaseSettings):
WEB_SERVER_PORT: int = Field(default=8080)
LOGS_PAGE_SIZE: int = Field(default=10)
_admin_ids_list: Optional[List[int]] = None
@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_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
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]:
admin_ids_list = self.ADMIN_IDS
if admin_ids_list:
return admin_ids_list[0]
return None
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:
@@ -127,49 +127,61 @@ class Settings(BaseSettings):
]
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, 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
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_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
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.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
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',
@@ -188,10 +200,23 @@ def get_settings() -> Settings:
_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."
"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}")
raise SystemExit(
f"CRITICAL SETTINGS ERROR: {e}. Please check your .env file and Settings model."
)
return _settings_instance