feat: Multi-guild support implementation
- Added multi-guild support with comma-separated AllowedGuildIDs config - Per-guild embed tracking with isolated state - Per-server action locking to prevent race conditions across guilds - Cross-guild embed updates after power actions - Debug logging for embed update triggers - Fixed resource usage field formatting (brackets now properly closed) - Updated config.ini.sample, generate_config.py for backward compatibility - Updated README.md with multi-guild documentation - Updated tests for multi-guild support
This commit is contained in:
1 parent
e68b248033
commit
bec06bd909
10 files changed
+802
-408
No files matched your search
@@ -9,7 +9,7 @@ import configparser
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from exceptions import ConfigValidationError
|
||||
|
||||
@@ -30,7 +30,8 @@ class Config:
|
||||
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
|
||||
allowed_guild_ids: List of Discord server IDs where bot operates
|
||||
allowed_guild_id: First guild ID (backward compatibility)
|
||||
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
|
||||
@@ -60,7 +61,9 @@ class Config:
|
||||
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.allowed_guild_ids: List[int] = self._parse_allowed_guild_ids()
|
||||
# Backward compatibility: first guild ID
|
||||
self.allowed_guild_id: int = self.allowed_guild_ids[0] if self.allowed_guild_ids else 0
|
||||
self.required_role: str = "Game Server User"
|
||||
self.update_interval: int = 10
|
||||
self.embed_storage_path: Path = Path("./embed/embed_locations.json")
|
||||
@@ -96,18 +99,29 @@ class Config:
|
||||
if not self._config.has_section("Discord"):
|
||||
errors.append("Missing [Discord] section in config.ini")
|
||||
else:
|
||||
required_discord = ["Token", "AllowedGuildID"]
|
||||
required_discord = ["Token"] # Guild ID validated separately below
|
||||
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 AllowedGuildIDs (comma-separated list)
|
||||
allowed_guild_ids_str = self._config.get("Discord", "AllowedGuildIDs", fallback="")
|
||||
# Backward compatibility: also check for singular AllowedGuildID
|
||||
if not allowed_guild_ids_str:
|
||||
allowed_guild_ids_str = self._config.get("Discord", "AllowedGuildID", fallback="")
|
||||
|
||||
if not allowed_guild_ids_str:
|
||||
errors.append("Missing required Discord config value: AllowedGuildIDs (or AllowedGuildID)")
|
||||
else:
|
||||
try:
|
||||
guild_ids = [int(gid.strip()) for gid in allowed_guild_ids_str.split(",") if gid.strip()]
|
||||
if not guild_ids:
|
||||
errors.append("AllowedGuildIDs must contain at least one guild ID")
|
||||
for gid in guild_ids:
|
||||
if gid <= 0:
|
||||
errors.append(f"Invalid guild ID: {gid} (must be positive integer)")
|
||||
except ValueError:
|
||||
errors.append("AllowedGuildIDs must be comma-separated integers")
|
||||
|
||||
# Validate API keys have correct prefixes (Pterodactyl and Pelican Panel)
|
||||
client_key = self._config.get("Pterodactyl", "ClientAPIKey", fallback="")
|
||||
@@ -138,6 +152,24 @@ class Config:
|
||||
or self.application_api_key.startswith("papp_")
|
||||
)
|
||||
|
||||
def _parse_allowed_guild_ids(self) -> List[int]:
|
||||
"""Parse comma-separated guild IDs from config.
|
||||
|
||||
Supports both new 'AllowedGuildIDs' and legacy 'AllowedGuildID' keys.
|
||||
|
||||
Returns:
|
||||
List of guild IDs as integers
|
||||
"""
|
||||
allowed_guild_ids_str = self._config.get("Discord", "AllowedGuildIDs", fallback="")
|
||||
if not allowed_guild_ids_str:
|
||||
# Backward compatibility
|
||||
allowed_guild_ids_str = self._config.get("Discord", "AllowedGuildID", fallback="")
|
||||
|
||||
if not allowed_guild_ids_str:
|
||||
return []
|
||||
|
||||
return [int(gid.strip()) for gid in allowed_guild_ids_str.split(",") if gid.strip()]
|
||||
|
||||
@property
|
||||
def panel_type(self) -> str:
|
||||
"""Return the detected panel type."""
|
||||
@@ -182,7 +214,7 @@ def generate_config_from_env(output_path: str = "config.ini") -> None:
|
||||
Useful for Docker/container deployments where config comes from env vars.
|
||||
|
||||
Required environment variables:
|
||||
DISCORD_TOKEN, ALLOWED_GUILD_ID, PANEL_URL,
|
||||
DISCORD_TOKEN, ALLOWED_GUILD_IDS (or ALLOWED_GUILD_ID), PANEL_URL,
|
||||
CLIENT_API_KEY, APPLICATION_API_KEY
|
||||
|
||||
Args:
|
||||
@@ -196,9 +228,14 @@ def generate_config_from_env(output_path: str = "config.ini") -> None:
|
||||
"ApplicationAPIKey": os.environ.get("APPLICATION_API_KEY", ""),
|
||||
}
|
||||
|
||||
# Support both new ALLOWED_GUILD_IDS and legacy ALLOWED_GUILD_ID
|
||||
allowed_guild_ids = os.environ.get("ALLOWED_GUILD_IDS", "")
|
||||
if not allowed_guild_ids:
|
||||
allowed_guild_ids = os.environ.get("ALLOWED_GUILD_ID", "")
|
||||
|
||||
config["Discord"] = {
|
||||
"Token": os.environ.get("DISCORD_TOKEN", ""),
|
||||
"AllowedGuildID": os.environ.get("ALLOWED_GUILD_ID", ""),
|
||||
"AllowedGuildIDs": allowed_guild_ids,
|
||||
}
|
||||
|
||||
with open(output_path, "w") as f:
|
||||
|
||||
Reference in new issue
Block a user