Update: Major code refactor for modular extensibility
CI/CD Pipeline / Unit Tests (Python 3.11) (push) Successful in 29s
CI/CD Pipeline / Unit Tests (Python 3.12) (push) Successful in 30s
CI/CD Pipeline / Code Quality & Linting (push) Successful in 23s
CI/CD Pipeline / Unit Tests (Python 3.13) (push) Successful in 27s
CI/CD Pipeline / Integration Tests (push) Successful in 21s
CI/CD Pipeline / Security Scanning (push) Successful in 2m16s
CI/CD Pipeline / Generate Test Report (push) Successful in 4s
CI/CD Pipeline / Build Docker Image (push) Successful in 44s
CI/CD Pipeline / CI/CD Pipeline Status (push) Successful in 2s

This commit is contained in:
k.eaven committed 2026-10-24 10:58:01 +00:00
1 parent d891b21ffd
commit 346e6d9604
13 files changed
+3266 -443

No files matched your search

+207
View File
@@ -0,0 +1,207 @@
"""
Configuration Management Module
Handles loading, validation, and access to application configuration.
Supports both config.ini files and environment variable overrides.
"""
import configparser
import logging
import os
from pathlib import Path
from typing import Optional
from exceptions import ConfigValidationError
logger = logging.getLogger("pterodisbot")
class Config:
"""
Application configuration manager.
Loads configuration from config.ini file, validates all required values,
and provides typed access to configuration parameters.
Supports both Pterodactyl (ptlc_/ptla_) and Pelican Panel (pacc_/papp_)
API key formats.
Attributes:
pterodactyl_url: URL of the panel (must include http:// or https://)
client_api_key: Client API key for server operations
application_api_key: Application API key for admin operations
discord_token: Discord bot token
allowed_guild_id: Discord server ID where bot operates
required_role: Role name required for server control permissions
update_interval: Background task update interval in seconds
embed_storage_path: Path to embed locations JSON file
"""
def __init__(self, config_path: str = "config.ini") -> None:
"""
Initialize configuration from file.
Args:
config_path: Path to config.ini file
Raises:
ConfigValidationError: If required values are missing or invalid
"""
self._config = configparser.ConfigParser()
self._config_path = Path(config_path)
if not self._config_path.exists():
raise ConfigValidationError(f"Configuration file not found: {config_path}")
self._config.read(self._config_path)
self._validate()
# Validated configuration values
self.pterodactyl_url: str = self._config.get("Pterodactyl", "PanelURL").rstrip("/")
self.client_api_key: str = self._config.get("Pterodactyl", "ClientAPIKey")
self.application_api_key: str = self._config.get("Pterodactyl", "ApplicationAPIKey")
self.discord_token: str = self._config.get("Discord", "Token")
self.allowed_guild_id: int = self._config.getint("Discord", "AllowedGuildID")
self.required_role: str = "Game Server User"
self.update_interval: int = 10
self.embed_storage_path: Path = Path("./embed/embed_locations.json")
logger.info("Configuration loaded and validated successfully")
def _validate(self) -> None:
"""
Validate all required configuration values.
Checks:
- Required sections exist
- All required keys are present
- AllowedGuildID is a positive integer
- API keys have correct prefixes (ptlc_/pacc_ and ptla_/papp_)
- PanelURL has valid protocol
Raises:
ConfigValidationError: If any validation fails
"""
errors = []
# Validate Pterodactyl section
if not self._config.has_section("Pterodactyl"):
errors.append("Missing [Pterodactyl] section in config.ini")
else:
required_ptero = ["PanelURL", "ClientAPIKey", "ApplicationAPIKey"]
for key in required_ptero:
if not self._config.get("Pterodactyl", key, fallback=None):
errors.append(f"Missing required Pterodactyl config value: {key}")
# Validate Discord section
if not self._config.has_section("Discord"):
errors.append("Missing [Discord] section in config.ini")
else:
required_discord = ["Token", "AllowedGuildID"]
for key in required_discord:
if not self._config.get("Discord", key, fallback=None):
errors.append(f"Missing required Discord config value: {key}")
# Validate AllowedGuildID is a valid integer
try:
guild_id = self._config.getint("Discord", "AllowedGuildID", fallback=0)
if guild_id <= 0:
errors.append("AllowedGuildID must be a positive integer")
except ValueError:
errors.append("AllowedGuildID must be a valid integer")
# Validate API keys have correct prefixes (Pterodactyl and Pelican Panel)
client_key = self._config.get("Pterodactyl", "ClientAPIKey", fallback="")
if client_key and not (client_key.startswith("ptlc_") or client_key.startswith("pacc_")):
errors.append("ClientAPIKey should start with 'ptlc_' (Pterodactyl) or 'pacc_' (Pelican Panel)")
app_key = self._config.get("Pterodactyl", "ApplicationAPIKey", fallback="")
if app_key and not (app_key.startswith("ptla_") or app_key.startswith("papp_")):
errors.append("ApplicationAPIKey should start with 'ptla_' (Pterodactyl) or 'papp_' (Pelican Panel)")
# Validate PanelURL is a valid URL
panel_url = self._config.get("Pterodactyl", "PanelURL", fallback="")
if panel_url and not (panel_url.startswith("http://") or panel_url.startswith("https://")):
errors.append("PanelURL must start with http:// or https://")
if errors:
error_msg = "Configuration validation failed:\n- " + "\n- ".join(errors)
logger.error(error_msg)
raise ConfigValidationError(error_msg)
logger.info("Configuration validation passed")
@property
def is_pelican_panel(self) -> bool:
"""Check if using Pelican Panel API keys (pacc_/papp_ prefixes)."""
return (
self.client_api_key.startswith("pacc_")
or self.application_api_key.startswith("papp_")
)
@property
def panel_type(self) -> str:
"""Return the detected panel type."""
return "Pelican Panel" if self.is_pelican_panel else "Pterodactyl"
def get_panel_display_name(self) -> str:
"""Get display name for the panel type."""
return "Pelican Panel" if self.is_pelican_panel else "Pterodactyl"
@classmethod
def get_instance(cls) -> Optional["Config"]:
"""Get the singleton config instance."""
return cls._instance
# Singleton instance for access from views
Config._instance = None
def load_config(config_path: str = "config.ini") -> Config:
"""
Load and validate configuration from file.
Args:
config_path: Path to config.ini file
Returns:
Config: Validated configuration instance
Raises:
ConfigValidationError: If configuration is invalid
"""
config = Config(config_path)
Config._instance = config
return config
def generate_config_from_env(output_path: str = "config.ini") -> None:
"""
Generate config.ini from environment variables.
Useful for Docker/container deployments where config comes from env vars.
Required environment variables:
DISCORD_TOKEN, ALLOWED_GUILD_ID, PANEL_URL,
CLIENT_API_KEY, APPLICATION_API_KEY
Args:
output_path: Path to write generated config.ini
"""
config = configparser.ConfigParser()
config["Pterodactyl"] = {
"PanelURL": os.environ.get("PANEL_URL", ""),
"ClientAPIKey": os.environ.get("CLIENT_API_KEY", ""),
"ApplicationAPIKey": os.environ.get("APPLICATION_API_KEY", ""),
}
config["Discord"] = {
"Token": os.environ.get("DISCORD_TOKEN", ""),
"AllowedGuildID": os.environ.get("ALLOWED_GUILD_ID", ""),
}
with open(output_path, "w") as f:
config.write(f)
logger.info(f"Generated config.ini from environment variables at {output_path}")