99"""
1010
1111import asyncio
12+ import os
1213import discord
1314from discord .ext import commands
1415from typing import Optional
1516
1617from src .core .config import config , validate_config
18+
19+
20+ # =============================================================================
21+ # Guild Protection
22+ # =============================================================================
23+
24+ def _get_authorized_guilds () -> set :
25+ """Get authorized guild IDs from environment."""
26+ guilds = set ()
27+ syria_id = os .getenv ("GUILD_ID" )
28+ mods_id = os .getenv ("MODS_GUILD_ID" )
29+ if syria_id :
30+ guilds .add (int (syria_id ))
31+ if mods_id :
32+ guilds .add (int (mods_id ))
33+ return guilds
34+
35+ AUTHORIZED_GUILD_IDS = _get_authorized_guilds ()
36+
1737from src .core .logger import log
1838from src .services .tempvoice import TempVoiceService
1939from src .services .sync_profile import ProfileSyncService
2040from src .services .xp import XPService
2141from src .services .xp import card as rank_card
2242from src .services .stats_api import SyriaAPI
43+ from src .core .health import HealthCheckServer
2344from src .services .status_webhook import get_status_service
45+ from src .services .backup import BackupScheduler
2446from src .services .afk import AFKService
2547from src .services .gallery import GalleryService
2648from src .services .presence import PresenceHandler
@@ -62,6 +84,7 @@ def __init__(self) -> None:
6284 self .profile_sync : Optional [ProfileSyncService ] = None
6385 self .xp_service : Optional [XPService ] = None
6486 self .stats_api : Optional [SyriaAPI ] = None
87+ self .health_server : Optional [HealthCheckServer ] = None
6588 self .status_webhook = None
6689 self .afk_service : Optional [AFKService ] = None
6790 self .gallery_service : Optional [GalleryService ] = None
@@ -73,6 +96,7 @@ def __init__(self) -> None:
7396 self .city_game_service : Optional [CityGameService ] = None
7497 self .guide_service : Optional [GuideService ] = None
7598 self .social_monitor : Optional [SocialMonitorService ] = None
99+ self .backup_scheduler : Optional [BackupScheduler ] = None
76100
77101 async def setup_hook (self ) -> None :
78102 """Called when the bot is starting up."""
@@ -177,6 +201,58 @@ async def _on_app_command_error(
177201 ("ID" , str (interaction .user .id )),
178202 ])
179203
204+ # =========================================================================
205+ # Guild Protection
206+ # =========================================================================
207+
208+ async def on_guild_join (self , guild : discord .Guild ) -> None :
209+ """Leave immediately if guild is not authorized."""
210+ # Safety: Don't leave if authorized set is empty (misconfigured env)
211+ if not AUTHORIZED_GUILD_IDS :
212+ return
213+ if guild .id not in AUTHORIZED_GUILD_IDS :
214+ log .warning ("Added To Unauthorized Guild - Leaving" , [
215+ ("Guild" , guild .name ),
216+ ("ID" , str (guild .id )),
217+ ])
218+ try :
219+ await guild .leave ()
220+ except Exception as e :
221+ log .error ("Failed To Leave Unauthorized Guild" , [
222+ ("Guild" , guild .name ),
223+ ("Error" , str (e )),
224+ ])
225+
226+ async def _leave_unauthorized_guilds (self ) -> None :
227+ """Leave any guilds not in AUTHORIZED_GUILD_IDS."""
228+ # Safety: Don't leave any guilds if authorized set is empty (misconfigured env)
229+ if not AUTHORIZED_GUILD_IDS :
230+ log .warning ("Guild Protection Skipped" , [
231+ ("Reason" , "AUTHORIZED_GUILD_IDS is empty" ),
232+ ("Action" , "Check GUILD_ID and MODS_GUILD_ID in .env" ),
233+ ])
234+ return
235+ unauthorized = [g for g in self .guilds if g .id not in AUTHORIZED_GUILD_IDS ]
236+ if not unauthorized :
237+ return
238+
239+ log .tree ("Leaving Unauthorized Guilds" , [
240+ ("Count" , str (len (unauthorized ))),
241+ ], emoji = "⚠️" )
242+
243+ for guild in unauthorized :
244+ try :
245+ log .warning ("Leaving Unauthorized Guild" , [
246+ ("Guild" , guild .name ),
247+ ("ID" , str (guild .id )),
248+ ])
249+ await guild .leave ()
250+ except Exception as e :
251+ log .error ("Failed To Leave Guild" , [
252+ ("Guild" , guild .name ),
253+ ("Error" , str (e )),
254+ ])
255+
180256 async def _init_services (self ) -> None :
181257 """Initialize bot services."""
182258 log .tree ("Services Init" , [
@@ -190,6 +266,9 @@ async def _init_services(self) -> None:
190266 ("Action" , "Check environment variables" ),
191267 ], emoji = "🚨" )
192268
269+ # Leave unauthorized guilds before initializing services
270+ await self ._leave_unauthorized_guilds ()
271+
193272 # Check database health first - critical for most services
194273 if not db .is_healthy :
195274 log .tree ("CRITICAL: Database Unhealthy" , [
@@ -227,6 +306,47 @@ async def _init_services(self) -> None:
227306 except Exception as e :
228307 log .error_tree ("XP Service Init Failed" , e )
229308
309+ # Health Check Server (unified)
310+ try :
311+ self .health_server = HealthCheckServer (self )
312+
313+ # Register database health callback
314+ async def db_health () -> dict :
315+ try :
316+ connected = db .is_healthy
317+ error = db .corruption_reason if not connected else None
318+ return {"connected" : connected , "error" : error }
319+ except Exception as e :
320+ return {"connected" : False , "error" : str (e )}
321+
322+ self .health_server .register_db_health (db_health )
323+
324+ # Register system health callback
325+ import psutil
326+ _psutil_process = psutil .Process ()
327+
328+ async def system_health () -> dict :
329+ cpu_percent = psutil .cpu_percent (interval = None )
330+ memory = psutil .virtual_memory ()
331+ disk = psutil .disk_usage ("/" )
332+ bot_memory_mb = _psutil_process .memory_info ().rss / (1024 * 1024 )
333+ return {
334+ "cpu_percent" : cpu_percent ,
335+ "memory_percent" : memory .percent ,
336+ "disk_percent" : disk .percent ,
337+ "disk_total_gb" : round (disk .total / (1024 ** 3 ), 1 ),
338+ "disk_used_gb" : round (disk .used / (1024 ** 3 ), 1 ),
339+ "bot_memory_mb" : round (bot_memory_mb , 1 ),
340+ "threads" : _psutil_process .num_threads (),
341+ "open_files" : len (_psutil_process .open_files ()),
342+ }
343+
344+ self .health_server .register_system (system_health )
345+ await self .health_server .start ()
346+ initialized .append ("HealthServer" )
347+ except Exception as e :
348+ log .error_tree ("Health Server Init Failed" , e )
349+
230350 # Stats API
231351 try :
232352 self .stats_api = SyriaAPI (self )
@@ -238,14 +358,22 @@ async def _init_services(self) -> None:
238358 # Status Webhook
239359 if config .STATUS_WEBHOOK_URL :
240360 try :
241- self .status_webhook = get_status_service (config .STATUS_WEBHOOK_URL )
361+ self .status_webhook = get_status_service (config .STATUS_WEBHOOK_URL , bot_name = "SyriaBot" )
242362 self .status_webhook .set_bot (self )
243363 await self .status_webhook .send_startup_alert ()
244364 await self .status_webhook .start_hourly_alerts ()
245365 initialized .append ("StatusWebhook" )
246366 except Exception as e :
247367 log .error_tree ("Status Webhook Init Failed" , e )
248368
369+ # Backup Scheduler
370+ try :
371+ self .backup_scheduler = BackupScheduler ()
372+ await self .backup_scheduler .start ()
373+ initialized .append ("Backup" )
374+ except Exception as e :
375+ log .error_tree ("Backup Scheduler Init Failed" , e )
376+
249377 # AFK Service
250378 try :
251379 self .afk_service = AFKService (self )
@@ -359,6 +487,13 @@ async def close(self) -> None:
359487 except Exception as e :
360488 log .error_tree ("Status Webhook Stop Error" , e )
361489
490+ if self .backup_scheduler :
491+ try :
492+ await self .backup_scheduler .stop ()
493+ stopped .append ("Backup" )
494+ except Exception as e :
495+ log .error_tree ("Backup Scheduler Stop Error" , e )
496+
362497 if self .stats_api :
363498 try :
364499 await self .stats_api .stop ()
0 commit comments