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:
k.eaven committed 2026-10-30 10:37:13 +00:00
1 parent e68b248033
commit bec06bd909
10 files changed
+802 -408

No files matched your search

+33 -18
View File
@@ -14,6 +14,7 @@ import pytest
import asyncio
import json
import os
from collections import defaultdict
from unittest.mock import Mock, AsyncMock, patch, MagicMock
from datetime import datetime
import configparser
@@ -52,7 +53,8 @@ def mock_config():
config.client_api_key = 'ptlc_test_client_key_123'
config.application_api_key = 'ptla_test_app_key_456'
config.discord_token = 'test_discord_token'
config.allowed_guild_id = 123456789
config.allowed_guild_ids = [123456789, 987654321]
config.allowed_guild_id = 123456789 # Backward compat
config.required_role = "Game Server User"
config.update_interval = 10
config.embed_storage_path = Path("./embed/embed_locations.json")
@@ -177,8 +179,8 @@ def mock_bot(mock_config, mock_panel_api):
bot = PterodactylBot(mock_config, command_prefix="!", intents=intents)
bot.api = mock_panel_api
bot.server_cache = {}
bot.embed_manager.embed_locations = {}
bot.previous_states = {}
# Don't override embed_locations - let it use the defaultdict from EmbedManager
bot.previous_states = defaultdict(dict)
return bot
@@ -561,13 +563,15 @@ class TestServerStatusView:
server_id='abc123',
server_name='Test Server',
api=mock_panel_api,
server_data=sample_server_data
server_data=sample_server_data,
guild_id=123456789
)
assert view.server_id == 'abc123'
assert view.server_name == 'Test Server'
assert view.api is mock_panel_api
assert view.timeout is None # Persistent view
assert view.guild_id == 123456789
@pytest.mark.asyncio
async def test_interaction_check_authorized(self, mock_config, mock_panel_api, sample_server_data, mock_discord_interaction):
@@ -576,7 +580,8 @@ class TestServerStatusView:
server_id='abc123',
server_name='Test Server',
api=mock_panel_api,
server_data=sample_server_data
server_data=sample_server_data,
guild_id=123456789
)
result = await view.interaction_check(mock_discord_interaction)
@@ -590,7 +595,8 @@ class TestServerStatusView:
server_id='abc123',
server_name='Test Server',
api=mock_panel_api,
server_data=sample_server_data
server_data=sample_server_data,
guild_id=123456789
)
mock_discord_interaction.guild_id = 999999999
@@ -607,7 +613,8 @@ class TestServerStatusView:
server_id='abc123',
server_name='Test Server',
api=mock_panel_api,
server_data=sample_server_data
server_data=sample_server_data,
guild_id=123456789
)
# Remove required role
@@ -638,7 +645,8 @@ class TestPterodactylBot:
assert bot.metrics_manager is not None
assert bot.embed_manager is not None
assert bot.server_cache == {}
assert bot.previous_states == {}
# previous_states is now a defaultdict
assert isinstance(bot.previous_states, dict)
@pytest.mark.asyncio
async def test_track_new_embed(self, mock_bot, mock_discord_interaction):
@@ -647,25 +655,29 @@ class TestPterodactylBot:
mock_message.channel.id = 987654321
mock_message.id = 123456789
await mock_bot.embed_manager.track_embed('abc123', mock_message)
await mock_bot.embed_manager.track_embed(123456789, 'abc123', mock_message)
assert 'abc123' in mock_bot.embed_manager.embed_locations
assert mock_bot.embed_manager.embed_locations['abc123']['channel_id'] == '987654321'
assert mock_bot.embed_manager.embed_locations['abc123']['message_id'] == '123456789'
assert 123456789 in mock_bot.embed_manager.embed_locations
assert 'abc123' in mock_bot.embed_manager.embed_locations[123456789]
assert mock_bot.embed_manager.embed_locations[123456789]['abc123']['channel_id'] == '987654321'
assert mock_bot.embed_manager.embed_locations[123456789]['abc123']['message_id'] == '123456789'
@pytest.mark.asyncio
async def test_load_embed_locations(self, mock_bot, tmp_path):
"""Test loading embed locations from file."""
# Create a temporary embed locations file
# Create a temporary embed locations file with new multi-guild format
embed_file = tmp_path / "embed_locations.json"
embed_file.write_text(json.dumps({
"server1": {"channel_id": "123", "message_id": "456"}
"123456789": {
"server1": {"channel_id": "123", "message_id": "456"}
}
}))
mock_bot.embed_manager.storage_path = embed_file
await mock_bot.embed_manager.load_locations()
assert "server1" in mock_bot.embed_manager.embed_locations
assert 123456789 in mock_bot.embed_manager.embed_locations
assert "server1" in mock_bot.embed_manager.embed_locations[123456789]
@pytest.mark.asyncio
async def test_save_embed_locations(self, mock_bot, tmp_path):
@@ -674,13 +686,16 @@ class TestPterodactylBot:
mock_bot.embed_manager.storage_path = embed_file
mock_bot.embed_manager.embed_locations = {
"server1": {"channel_id": "123", "message_id": "456"}
123456789: {
"server1": {"channel_id": "123", "message_id": "456"}
}
}
await mock_bot.embed_manager.save_locations()
saved_data = json.loads(embed_file.read_text())
assert "server1" in saved_data
assert "123456789" in saved_data
assert "server1" in saved_data["123456789"]
# ==========================================
@@ -715,7 +730,7 @@ class TestIntegration:
server_obj = ServerData.model_validate(sample_server_data)
resources_obj = ServerResources.model_validate(sample_resources_data)
embed, view = await mock_bot.embed_manager.create_server_embed(server_obj, resources_obj)
embed, view = await mock_bot.embed_manager.create_server_embed(server_obj, resources_obj, 123456789)
assert embed is not None
assert view is not None