- Save servers_config.json atomically via temp file + fsync + rename - Keep .bak backup and auto-restore when main config is empty/corrupt - Add /servers/recover endpoint for manual recovery - Auto-recover basic server configs on startup when config is empty but bot is in guilds
232 lines
9.5 KiB
Python
232 lines
9.5 KiB
Python
"""Server management routes: CRUD, bedtime, repair."""
|
|
|
|
import os
|
|
import json
|
|
from fastapi import APIRouter
|
|
from fastapi.responses import JSONResponse
|
|
import globals
|
|
from server_manager import server_manager
|
|
from routes.models import ServerConfigRequest
|
|
from utils.logger import get_logger
|
|
|
|
logger = get_logger('api')
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/servers")
|
|
def get_servers():
|
|
"""Get all configured servers"""
|
|
logger.debug("/servers endpoint called")
|
|
logger.debug(f"server_manager.servers keys: {list(server_manager.servers.keys())}")
|
|
logger.debug(f"server_manager.servers count: {len(server_manager.servers)}")
|
|
|
|
config_file = server_manager.config_file
|
|
logger.debug(f"Config file path: {config_file}")
|
|
if os.path.exists(config_file):
|
|
try:
|
|
with open(config_file, "r", encoding="utf-8") as f:
|
|
config_data = json.load(f)
|
|
logger.debug(f"Config file contains: {list(config_data.keys())}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to read config file: {e}")
|
|
else:
|
|
logger.warning("Config file does not exist")
|
|
|
|
servers = []
|
|
for server in server_manager.get_all_servers():
|
|
server_data = server.to_dict()
|
|
server_data['enabled_features'] = list(server_data['enabled_features'])
|
|
server_data['guild_id'] = str(server_data['guild_id'])
|
|
servers.append(server_data)
|
|
logger.debug(f"Adding server to response: {server_data['guild_id']} - {server_data['guild_name']}")
|
|
logger.debug(f"Server data type check - guild_id: {type(server_data['guild_id'])}, value: {server_data['guild_id']}")
|
|
|
|
logger.debug(f"Returning {len(servers)} servers")
|
|
return {"servers": servers}
|
|
|
|
|
|
@router.post("/servers")
|
|
def add_server(data: ServerConfigRequest):
|
|
"""Add a new server configuration"""
|
|
enabled_features = set(data.enabled_features) if data.enabled_features else None
|
|
success = server_manager.add_server(
|
|
guild_id=data.guild_id,
|
|
guild_name=data.guild_name,
|
|
autonomous_channel_id=data.autonomous_channel_id,
|
|
autonomous_channel_name=data.autonomous_channel_name,
|
|
bedtime_channel_ids=data.bedtime_channel_ids,
|
|
enabled_features=enabled_features
|
|
)
|
|
|
|
if success:
|
|
server_manager.stop_all_schedulers()
|
|
server_manager.start_all_schedulers(globals.client)
|
|
return {"status": "ok", "message": f"Server {data.guild_name} added successfully"}
|
|
else:
|
|
return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to add server"})
|
|
|
|
|
|
@router.delete("/servers/{guild_id}")
|
|
def remove_server(guild_id: int):
|
|
"""Remove a server configuration"""
|
|
success = server_manager.remove_server(guild_id)
|
|
if success:
|
|
return {"status": "ok", "message": "Server removed successfully"}
|
|
else:
|
|
return JSONResponse(status_code=404, content={"status": "error", "message": "Failed to remove server"})
|
|
|
|
|
|
@router.put("/servers/{guild_id}")
|
|
def update_server(guild_id: int, data: dict):
|
|
"""Update server configuration"""
|
|
success = server_manager.update_server_config(guild_id, **data)
|
|
if success:
|
|
server_manager.stop_all_schedulers()
|
|
server_manager.start_all_schedulers(globals.client)
|
|
return {"status": "ok", "message": "Server configuration updated"}
|
|
else:
|
|
return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to update server configuration"})
|
|
|
|
|
|
@router.post("/servers/{guild_id}/bedtime-range")
|
|
def update_server_bedtime_range(guild_id: int, data: dict):
|
|
"""Update server bedtime range configuration"""
|
|
logger.debug(f"Updating bedtime range for server {guild_id}: {data}")
|
|
|
|
required_fields = ['bedtime_hour', 'bedtime_minute', 'bedtime_hour_end', 'bedtime_minute_end']
|
|
for field in required_fields:
|
|
if field not in data:
|
|
return JSONResponse(status_code=400, content={"status": "error", "message": f"Missing required field: {field}"})
|
|
|
|
try:
|
|
bedtime_hour = int(data['bedtime_hour'])
|
|
bedtime_minute = int(data['bedtime_minute'])
|
|
bedtime_hour_end = int(data['bedtime_hour_end'])
|
|
bedtime_minute_end = int(data['bedtime_minute_end'])
|
|
|
|
if not (0 <= bedtime_hour <= 23) or not (0 <= bedtime_hour_end <= 23):
|
|
return JSONResponse(status_code=400, content={"status": "error", "message": "Hours must be between 0 and 23"})
|
|
if not (0 <= bedtime_minute <= 59) or not (0 <= bedtime_minute_end <= 59):
|
|
return JSONResponse(status_code=400, content={"status": "error", "message": "Minutes must be between 0 and 59"})
|
|
|
|
except (ValueError, TypeError):
|
|
return JSONResponse(status_code=400, content={"status": "error", "message": "Invalid time values provided"})
|
|
|
|
success = server_manager.update_server_config(guild_id, **data)
|
|
if success:
|
|
job_success = server_manager.update_server_bedtime_job(guild_id, globals.client)
|
|
if job_success:
|
|
logger.info(f"Bedtime range updated for server {guild_id}")
|
|
return {
|
|
"status": "ok",
|
|
"message": f"Bedtime range updated: {bedtime_hour:02d}:{bedtime_minute:02d} - {bedtime_hour_end:02d}:{bedtime_minute_end:02d}"
|
|
}
|
|
else:
|
|
return JSONResponse(status_code=500, content={"status": "error", "message": "Updated config but failed to update scheduler"})
|
|
else:
|
|
return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to update bedtime range"})
|
|
|
|
|
|
@router.post("/servers/repair")
|
|
def repair_server_config():
|
|
"""Repair corrupted server configuration"""
|
|
try:
|
|
server_manager.repair_config()
|
|
return {"status": "ok", "message": "Server configuration repaired and saved"}
|
|
except Exception as e:
|
|
return JSONResponse(status_code=500, content={"status": "error", "message": f"Failed to repair configuration: {e}"})
|
|
|
|
|
|
@router.post("/servers/recover")
|
|
def recover_servers_from_discord():
|
|
"""Auto-discover servers from Discord guilds and create config entries.
|
|
|
|
Use this when servers_config.json is lost/corrupted and you need to
|
|
quickly restore basic server configurations. Each discovered guild gets
|
|
a placeholder config using the first available text channel as the
|
|
autonomous channel. You can then adjust channels via the dashboard.
|
|
"""
|
|
if not globals.client or not globals.client.is_ready():
|
|
return JSONResponse(status_code=503, content={
|
|
"status": "error",
|
|
"message": "Discord client not ready — bot must be connected"
|
|
})
|
|
|
|
if not globals.client.guilds:
|
|
return JSONResponse(status_code=404, content={
|
|
"status": "error",
|
|
"message": "Bot is not in any Discord guilds"
|
|
})
|
|
|
|
recovered = []
|
|
skipped = []
|
|
failed = []
|
|
|
|
for guild in globals.client.guilds:
|
|
guild_id = guild.id
|
|
guild_name = guild.name
|
|
|
|
# Skip if already configured
|
|
if server_manager.get_server_config(guild_id):
|
|
skipped.append({"guild_id": str(guild_id), "guild_name": guild_name, "reason": "Already configured"})
|
|
continue
|
|
|
|
# Find the first text channel (prefer one named "general" or "chat")
|
|
text_channels = [ch for ch in guild.text_channels if ch.permissions_for(guild.me).send_messages]
|
|
if not text_channels:
|
|
# Try any text channel even without send permissions
|
|
text_channels = guild.text_channels
|
|
|
|
if not text_channels:
|
|
failed.append({"guild_id": str(guild_id), "guild_name": guild_name, "reason": "No text channels found"})
|
|
continue
|
|
|
|
# Prefer "general" or "chat" channel, otherwise use the first one
|
|
preferred = None
|
|
for ch in text_channels:
|
|
if ch.name.lower() in ("general", "chat", "main", "lounge", "general-chat"):
|
|
preferred = ch
|
|
break
|
|
channel = preferred or text_channels[0]
|
|
|
|
try:
|
|
success = server_manager.add_server(
|
|
guild_id=guild_id,
|
|
guild_name=guild_name,
|
|
autonomous_channel_id=channel.id,
|
|
autonomous_channel_name=f"#{channel.name}",
|
|
bedtime_channel_ids=[channel.id],
|
|
enabled_features={"autonomous", "bedtime", "monday_video"}
|
|
)
|
|
if success:
|
|
recovered.append({
|
|
"guild_id": str(guild_id),
|
|
"guild_name": guild_name,
|
|
"autonomous_channel": f"#{channel.name} ({channel.id})"
|
|
})
|
|
logger.info(f"Recovered server config: {guild_name} (ID: {guild_id}) → #{channel.name}")
|
|
else:
|
|
failed.append({"guild_id": str(guild_id), "guild_name": guild_name, "reason": "add_server returned False"})
|
|
except Exception as e:
|
|
failed.append({"guild_id": str(guild_id), "guild_name": guild_name, "reason": str(e)})
|
|
logger.error(f"Failed to recover server {guild_name}: {e}")
|
|
|
|
# Restart schedulers if we recovered any servers
|
|
if recovered:
|
|
try:
|
|
server_manager.stop_all_schedulers()
|
|
server_manager.start_all_schedulers(globals.client)
|
|
except Exception as e:
|
|
logger.error(f"Failed to restart schedulers after recovery: {e}")
|
|
|
|
return {
|
|
"status": "ok",
|
|
"recovered": recovered,
|
|
"skipped": skipped,
|
|
"failed": failed,
|
|
"total_guilds": len(globals.client.guilds),
|
|
"note": "Recovered servers use the first text channel as autonomous channel. "
|
|
"Use the Servers tab to adjust channel settings."
|
|
}
|