Update configuration and README for improved setup and security; disable default API and emoji sync, and enhance Discord permission checks in API routes.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
14
bot/api/__init__.py
Normal file
14
bot/api/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════╗
|
||||
# ║ ║
|
||||
# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║
|
||||
# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║
|
||||
# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║
|
||||
# ║ ║
|
||||
# ║ © 2026 CodeX Devs — All Rights Reserved ║
|
||||
# ║ ║
|
||||
# ║ discord ── https://discord.gg/codexdev ║
|
||||
# ║ youtube ── https://youtube.com/@CodeXDevs ║
|
||||
# ║ github ── https://github.com/RayExo ║
|
||||
# ║ ║
|
||||
# ╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
54
bot/api/db_manager.py
Normal file
54
bot/api/db_manager.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════╗
|
||||
# ║ ║
|
||||
# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║
|
||||
# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║
|
||||
# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║
|
||||
# ║ ║
|
||||
# ║ © 2026 CodeX Devs — All Rights Reserved ║
|
||||
# ║ ║
|
||||
# ║ discord ── https://discord.gg/codexdev ║
|
||||
# ║ youtube ── https://youtube.com/@CodeXDevs ║
|
||||
# ║ github ── https://github.com/RayExo ║
|
||||
# ║ ║
|
||||
# ╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
import aiosqlite
|
||||
import asyncio
|
||||
from typing import Dict
|
||||
|
||||
class DatabaseManager:
|
||||
"""
|
||||
A simple manager for persistent SQLite connections to avoid
|
||||
opening/closing files on every API request.
|
||||
"""
|
||||
def __init__(self):
|
||||
self._connections: Dict[str, aiosqlite.Connection] = {}
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def get_connection(self, db_path: str) -> aiosqlite.Connection:
|
||||
"""
|
||||
Retrieves an existing connection or creates a new one for the given path.
|
||||
"""
|
||||
async with self._lock:
|
||||
if db_path not in self._connections:
|
||||
# We use check_same_thread=False because aiosqlite handles
|
||||
# thread safety by running queries in a dedicated thread.
|
||||
conn = await aiosqlite.connect(db_path, check_same_thread=False)
|
||||
conn.row_factory = aiosqlite.Row
|
||||
self._connections[db_path] = conn
|
||||
return self._connections[db_path]
|
||||
|
||||
async def close_all(self):
|
||||
"""
|
||||
Closes all open connections. Called on API shutdown.
|
||||
"""
|
||||
async with self._lock:
|
||||
for path, conn in self._connections.items():
|
||||
try:
|
||||
await conn.close()
|
||||
except:
|
||||
pass
|
||||
self._connections.clear()
|
||||
|
||||
# Singleton instance
|
||||
db_manager = DatabaseManager()
|
||||
75
bot/api/dependencies.py
Normal file
75
bot/api/dependencies.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════╗
|
||||
# ║ ║
|
||||
# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║
|
||||
# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║
|
||||
# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║
|
||||
# ║ ║
|
||||
# ║ © 2026 CodeX Devs — All Rights Reserved ║
|
||||
# ║ ║
|
||||
# ║ discord ── https://discord.gg/codexdev ║
|
||||
# ║ youtube ── https://youtube.com/@CodeXDevs ║
|
||||
# ║ github ── https://github.com/RayExo ║
|
||||
# ║ ║
|
||||
# ╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
import os
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from fastapi import HTTPException, Depends, Security
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.zyrox import zyrox
|
||||
|
||||
# Initialize rate limiter
|
||||
limiter = Limiter(key_func=get_remote_address, default_limits=["1000 per minute"])
|
||||
|
||||
# Global reference to the bot instance
|
||||
_bot_instance: Optional["zyrox"] = None
|
||||
|
||||
# Security scheme
|
||||
security = HTTPBearer()
|
||||
|
||||
def verify_api_key(credentials: HTTPAuthorizationCredentials = Security(security)):
|
||||
"""
|
||||
Dependency to verify the API key from the Authorization header.
|
||||
Expected: Authorization: Bearer <API_KEY>
|
||||
"""
|
||||
api_key = os.getenv("DASHBOARD_API_KEY")
|
||||
|
||||
# If no key is set in env, we might want to allow for local testing or force it.
|
||||
# The requirement says "Read API key from environment variable", implying it's required.
|
||||
if not api_key:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="DASHBOARD_API_KEY environment variable is not set."
|
||||
)
|
||||
|
||||
if credentials.credentials != api_key:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Invalid or missing API key."
|
||||
)
|
||||
return credentials.credentials
|
||||
|
||||
def set_bot(bot_instance: "zyrox"):
|
||||
"""
|
||||
Sets the global bot instance.
|
||||
This should be called in CodeX.py during startup.
|
||||
"""
|
||||
global _bot_instance
|
||||
_bot_instance = bot_instance
|
||||
|
||||
def get_bot() -> "zyrox":
|
||||
"""
|
||||
FastAPI dependency to retrieve the Discord bot instance.
|
||||
Usage: bot: zyrox = Depends(get_bot)
|
||||
"""
|
||||
if _bot_instance is None:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Discord bot instance is not initialized yet."
|
||||
)
|
||||
return _bot_instance
|
||||
130
bot/api/discord_auth.py
Normal file
130
bot/api/discord_auth.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════╗
|
||||
# ║ Discord OAuth permission checks for dashboard API requests ║
|
||||
# ╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from fastapi import HTTPException, Request
|
||||
|
||||
from utils.config import OWNER_IDS_STR
|
||||
|
||||
MANAGE_GUILD = 0x20
|
||||
ADMINISTRATOR = 0x8
|
||||
|
||||
GUILD_PATH_RE = re.compile(r"^/api/v1/guilds/(\d+)(?:/|$)")
|
||||
|
||||
|
||||
def _parse_admin_ids() -> list[str]:
|
||||
import os
|
||||
|
||||
raw = os.getenv("DASHBOARD_ADMIN_IDS", "").strip()
|
||||
if raw:
|
||||
return [p.strip() for p in raw.split(",") if p.strip().isdigit()]
|
||||
return list(OWNER_IDS_STR)
|
||||
|
||||
|
||||
def can_manage_guild(guild: dict[str, Any]) -> bool:
|
||||
if guild.get("owner"):
|
||||
return True
|
||||
try:
|
||||
perms = int(guild.get("permissions", "0"))
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return bool(perms & ADMINISTRATOR) or bool(perms & MANAGE_GUILD)
|
||||
|
||||
|
||||
async def fetch_user_guilds(access_token: str) -> list[dict[str, Any]]:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
"https://discord.com/api/users/@me/guilds",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Invalid or expired Discord access token.",
|
||||
)
|
||||
data = await resp.json()
|
||||
return data if isinstance(data, list) else []
|
||||
|
||||
|
||||
async def verify_discord_token(access_token: str) -> dict[str, Any]:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
"https://discord.com/api/users/@me",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
) as resp:
|
||||
if resp.status != 200:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Invalid or expired Discord access token.",
|
||||
)
|
||||
return await resp.json()
|
||||
|
||||
|
||||
async def user_can_manage_guild(access_token: str, guild_id: int) -> bool:
|
||||
guilds = await fetch_user_guilds(access_token)
|
||||
for guild in guilds:
|
||||
if str(guild.get("id")) == str(guild_id):
|
||||
return can_manage_guild(guild)
|
||||
return False
|
||||
|
||||
|
||||
async def get_manageable_guild_ids(access_token: str) -> set[str]:
|
||||
guilds = await fetch_user_guilds(access_token)
|
||||
return {str(g["id"]) for g in guilds if can_manage_guild(g)}
|
||||
|
||||
|
||||
def get_discord_headers(request: Request) -> tuple[str | None, str | None]:
|
||||
token = request.headers.get("X-Discord-Access-Token")
|
||||
user_id = request.headers.get("X-Discord-User-Id")
|
||||
return token, user_id
|
||||
|
||||
|
||||
async def require_discord_session(request: Request) -> tuple[str, str]:
|
||||
token, user_id = get_discord_headers(request)
|
||||
if not token:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="X-Discord-Access-Token header is required.",
|
||||
)
|
||||
|
||||
user = await verify_discord_token(token)
|
||||
resolved_id = str(user.get("id", ""))
|
||||
if not resolved_id:
|
||||
raise HTTPException(status_code=403, detail="Could not resolve Discord user.")
|
||||
|
||||
if user_id and user_id != resolved_id:
|
||||
raise HTTPException(status_code=403, detail="Discord user ID mismatch.")
|
||||
|
||||
return token, resolved_id
|
||||
|
||||
|
||||
async def require_guild_access(request: Request, guild_id: int) -> None:
|
||||
token, _ = await require_discord_session(request)
|
||||
if not await user_can_manage_guild(token, guild_id):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You do not have permission to manage this guild.",
|
||||
)
|
||||
|
||||
|
||||
async def require_dashboard_admin(request: Request) -> None:
|
||||
token, user_id = await require_discord_session(request)
|
||||
admin_ids = _parse_admin_ids()
|
||||
if user_id not in admin_ids:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Dashboard admin access required.",
|
||||
)
|
||||
|
||||
|
||||
def extract_guild_id_from_path(path: str) -> int | None:
|
||||
match = GUILD_PATH_RE.match(path)
|
||||
if not match:
|
||||
return None
|
||||
return int(match.group(1))
|
||||
14
bot/api/routes/__init__.py
Normal file
14
bot/api/routes/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════╗
|
||||
# ║ ║
|
||||
# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║
|
||||
# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║
|
||||
# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║
|
||||
# ║ ║
|
||||
# ║ © 2026 CodeX Devs — All Rights Reserved ║
|
||||
# ║ ║
|
||||
# ║ discord ── https://discord.gg/codexdev ║
|
||||
# ║ youtube ── https://youtube.com/@CodeXDevs ║
|
||||
# ║ github ── https://github.com/RayExo ║
|
||||
# ║ ║
|
||||
# ╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
128
bot/api/routes/admin.py
Normal file
128
bot/api/routes/admin.py
Normal file
@@ -0,0 +1,128 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════╗
|
||||
# ║ ║
|
||||
# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║
|
||||
# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║
|
||||
# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║
|
||||
# ║ ║
|
||||
# ║ © 2026 CodeX Devs — All Rights Reserved ║
|
||||
# ║ ║
|
||||
# ║ discord ── https://discord.gg/codexdev ║
|
||||
# ║ youtube ── https://youtube.com/@CodeXDevs ║
|
||||
# ║ github ── https://github.com/RayExo ║
|
||||
# ║ ║
|
||||
# ╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from api.dependencies import get_bot
|
||||
from api.schemas import AdminStats, AdminNodeStatus, AdminConfig, AdminConfigUpdate
|
||||
from typing import TYPE_CHECKING, List
|
||||
import os
|
||||
import aiosqlite
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.zyrox import zyrox
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
CONFIG_DB = "db/admin_config.db"
|
||||
|
||||
async def init_db():
|
||||
async with aiosqlite.connect(CONFIG_DB) as db:
|
||||
await db.execute("CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT)")
|
||||
# Default values
|
||||
await db.execute("INSERT OR IGNORE INTO config (key, value) VALUES ('maintenance_mode', 'false')")
|
||||
await db.execute("INSERT OR IGNORE INTO config (key, value) VALUES ('global_notification', '')")
|
||||
await db.commit()
|
||||
|
||||
import psutil
|
||||
import time
|
||||
|
||||
@router.get("/stats", response_model=AdminStats)
|
||||
async def get_admin_stats(bot: "zyrox" = Depends(get_bot)):
|
||||
# Calculate DB size and shard info
|
||||
total_size: float = 0.0
|
||||
db_count = 0
|
||||
db_dir = "db"
|
||||
if os.path.exists(db_dir):
|
||||
for f in os.listdir(db_dir):
|
||||
if f.endswith(".db"):
|
||||
total_size += float(os.path.getsize(os.path.join(db_dir, f)))
|
||||
db_count += 1
|
||||
|
||||
mb_size = total_size / (1024 * 1024)
|
||||
db_size_str = f"{mb_size:.2f} MB"
|
||||
if mb_size > 1024:
|
||||
db_size_str = f"{(mb_size / 1024):.2f} GB"
|
||||
|
||||
# System Metrics
|
||||
process = psutil.Process(os.getpid())
|
||||
# Use a non-blocking interval check or global state for CPU
|
||||
cpu_usage = psutil.cpu_percent()
|
||||
ram_raw = process.memory_info().rss
|
||||
ram_mb = ram_raw / (1024 * 1024)
|
||||
|
||||
total_commands = len(bot.commands)
|
||||
loaded_cogs = len(bot.cogs or {})
|
||||
|
||||
# Node Healths
|
||||
nodes = [
|
||||
AdminNodeStatus(
|
||||
name="Primary API Cluster",
|
||||
status="Healthy",
|
||||
load=f"CPU: {cpu_usage}% | RAM: {ram_mb:.1f}MB",
|
||||
icon="Globe"
|
||||
),
|
||||
AdminNodeStatus(
|
||||
name="Database Shards",
|
||||
status="Healthy" if db_count > 0 else "Warning",
|
||||
load=f"{db_count} SQLite DBs | {db_size_str}",
|
||||
icon="Database"
|
||||
),
|
||||
AdminNodeStatus(
|
||||
name="Bot Microservices",
|
||||
status="Healthy" if bot.is_ready() else "Booting",
|
||||
load=f"{loaded_cogs} Modules",
|
||||
icon="Cpu"
|
||||
),
|
||||
AdminNodeStatus(
|
||||
name="Auth Sockets",
|
||||
status="Healthy",
|
||||
load=f"Shard: {bot.shard_count} | Latency: {round(bot.latency * 1000)}ms",
|
||||
icon="Lock"
|
||||
)
|
||||
]
|
||||
|
||||
total_members = sum(g.member_count or 0 for g in bot.guilds)
|
||||
|
||||
return AdminStats(
|
||||
total_users=str(total_members),
|
||||
active_servers=str(len(bot.guilds)),
|
||||
api_latency=f"{round(bot.latency * 1000, 2)}ms",
|
||||
db_size=db_size_str,
|
||||
nodes=nodes
|
||||
)
|
||||
|
||||
@router.get("/config", response_model=AdminConfig)
|
||||
async def get_admin_config():
|
||||
await init_db()
|
||||
async with aiosqlite.connect(CONFIG_DB) as db:
|
||||
async with db.execute("SELECT value FROM config WHERE key = 'maintenance_mode'") as cursor:
|
||||
mm = await cursor.fetchone()
|
||||
async with db.execute("SELECT value FROM config WHERE key = 'global_notification'") as cursor:
|
||||
gn = await cursor.fetchone()
|
||||
|
||||
return AdminConfig(
|
||||
maintenance_mode=mm[0].lower() == 'true' if mm else False,
|
||||
global_notification=gn[0] if gn else None
|
||||
)
|
||||
|
||||
@router.patch("/config")
|
||||
async def patch_admin_config(data: AdminConfigUpdate):
|
||||
await init_db()
|
||||
async with aiosqlite.connect(CONFIG_DB) as db:
|
||||
if data.maintenance_mode is not None:
|
||||
await db.execute("UPDATE config SET value = ? WHERE key = 'maintenance_mode'", (str(data.maintenance_mode).lower(),))
|
||||
if data.global_notification is not None:
|
||||
await db.execute("UPDATE config SET value = ? WHERE key = 'global_notification'", (data.global_notification,))
|
||||
await db.commit()
|
||||
return {"status": "success"}
|
||||
53
bot/api/routes/bot.py
Normal file
53
bot/api/routes/bot.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════╗
|
||||
# ║ ║
|
||||
# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║
|
||||
# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║
|
||||
# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║
|
||||
# ║ ║
|
||||
# ║ © 2026 CodeX Devs — All Rights Reserved ║
|
||||
# ║ ║
|
||||
# ║ discord ── https://discord.gg/codexdev ║
|
||||
# ║ youtube ── https://youtube.com/@CodeXDevs ║
|
||||
# ║ github ── https://github.com/RayExo ║
|
||||
# ║ ║
|
||||
# ╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from api.dependencies import get_bot
|
||||
from api.schemas import BotInfo, BotStatus
|
||||
from typing import TYPE_CHECKING
|
||||
from utils.config import *
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.zyrox import zyrox
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/status", response_model=BotStatus, summary="Get bot status", description="Returns real-time health metrics, latency, and scale information.")
|
||||
async def get_status(bot: "zyrox" = Depends(get_bot)):
|
||||
"""
|
||||
Returns the live status of the bot.
|
||||
"""
|
||||
return BotStatus(
|
||||
user=str(bot.user),
|
||||
id=str(bot.user.id) if bot.user else None,
|
||||
latency=bot.latency * 1000,
|
||||
guild_count=len(bot.guilds),
|
||||
user_count=sum(g.member_count or 0 for g in bot.guilds),
|
||||
shards=bot.shard_count
|
||||
)
|
||||
|
||||
@router.get("/info", response_model=BotInfo, summary="Get bot info", description="Returns general information about the bot including command count and user reach.")
|
||||
async def get_bot_info(bot: "zyrox" = Depends(get_bot)):
|
||||
"""
|
||||
Get general information about the Discord bot.
|
||||
"""
|
||||
return BotInfo(
|
||||
name=bot.user.name if bot.user else BRAND_NAME,
|
||||
id=str(bot.user.id) if bot.user else None,
|
||||
guilds=len(bot.guilds),
|
||||
users=sum(g.member_count or 0 for g in bot.guilds),
|
||||
commands=len(bot.commands),
|
||||
latency=f"{round(bot.latency * 1000, 2)}ms"
|
||||
)
|
||||
1391
bot/api/routes/guilds.py
Normal file
1391
bot/api/routes/guilds.py
Normal file
File diff suppressed because it is too large
Load Diff
346
bot/api/schemas.py
Normal file
346
bot/api/schemas.py
Normal file
@@ -0,0 +1,346 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════╗
|
||||
# ║ ║
|
||||
# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║
|
||||
# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║
|
||||
# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║
|
||||
# ║ ║
|
||||
# ║ © 2026 CodeX Devs — All Rights Reserved ║
|
||||
# ║ ║
|
||||
# ║ discord ── https://discord.gg/codexdev ║
|
||||
# ║ youtube ── https://youtube.com/@CodeXDevs ║
|
||||
# ║ github ── https://github.com/RayExo ║
|
||||
# ║ ║
|
||||
# ╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
from pydantic import BaseModel, HttpUrl
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
# --- Bot Schemas ---
|
||||
|
||||
class BotInfo(BaseModel):
|
||||
name: str
|
||||
id: Optional[str]
|
||||
guilds: int
|
||||
users: int
|
||||
commands: int
|
||||
latency: str
|
||||
|
||||
class BotStatus(BaseModel):
|
||||
user: str
|
||||
id: Optional[str]
|
||||
latency: float
|
||||
guild_count: int
|
||||
user_count: int
|
||||
shards: Optional[int]
|
||||
|
||||
# --- Guild Schemas ---
|
||||
|
||||
class GuildSummary(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
icon_url: Optional[str]
|
||||
owner_id: str
|
||||
member_count: int
|
||||
|
||||
class GuildDetails(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
icon: Optional[str]
|
||||
owner_id: str
|
||||
member_count: int
|
||||
role_count: int
|
||||
channel_count: int
|
||||
|
||||
class DiscordChannel(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
type: str
|
||||
|
||||
class DiscordRole(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
color: int
|
||||
position: int # text, voice, category, etc.
|
||||
|
||||
# --- Module Configurations ---
|
||||
|
||||
class PrefixConfig(BaseModel):
|
||||
guild_id: int
|
||||
prefix: str
|
||||
|
||||
class AutomodConfig(BaseModel):
|
||||
guild_id: int
|
||||
enabled: bool
|
||||
punishments: Dict[str, str]
|
||||
ignored_roles: List[int]
|
||||
ignored_channels: List[int]
|
||||
logging_channel: Optional[int]
|
||||
|
||||
class TicketCategory(BaseModel):
|
||||
name: str
|
||||
emoji: Optional[str]
|
||||
staff_roles: List[int] = []
|
||||
button_style: Optional[int] = 2 # Blurple
|
||||
discord_category_id: Optional[int] = None
|
||||
|
||||
class TicketEmbed(BaseModel):
|
||||
title: Optional[str]
|
||||
description: Optional[str]
|
||||
color: Optional[int] = None
|
||||
image_url: Optional[str] = None
|
||||
thumbnail_url: Optional[str] = None
|
||||
|
||||
class TicketConfig(BaseModel):
|
||||
guild_id: int
|
||||
panel_channel: Optional[int]
|
||||
panel_message: Optional[int]
|
||||
logging_channel: Optional[int] = None
|
||||
closed_category: Optional[int] = None
|
||||
panel_type: Optional[str] = "button"
|
||||
embed: TicketEmbed
|
||||
categories: List[TicketCategory]
|
||||
staff_roles: List[int]
|
||||
open_ticket_count: int
|
||||
|
||||
class LevelingEmbedStyle(BaseModel):
|
||||
color: str
|
||||
thumbnail: bool = False
|
||||
image: Optional[str] = None
|
||||
|
||||
class LevelingConfig(BaseModel):
|
||||
guild_id: int
|
||||
enabled: bool
|
||||
xp_per_message: int
|
||||
cooldown: int
|
||||
level_up_channel: Optional[int]
|
||||
embed_style: LevelingEmbedStyle
|
||||
|
||||
class LoggingConfig(BaseModel):
|
||||
guild_id: int
|
||||
log_enabled: Dict[str, bool]
|
||||
log_channels: Dict[str, int]
|
||||
ignore_channels: List[int]
|
||||
ignore_roles: List[int]
|
||||
ignore_users: List[int]
|
||||
auto_delete_duration: Optional[int]
|
||||
|
||||
class WelcomeEmbedData(BaseModel):
|
||||
message: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
footer_text: Optional[str] = None
|
||||
footer_icon: Optional[str] = None
|
||||
author_name: Optional[str] = None
|
||||
author_icon: Optional[str] = None
|
||||
thumbnail: Optional[str] = None
|
||||
image: Optional[str] = None
|
||||
|
||||
class WelcomeConfig(BaseModel):
|
||||
guild_id: int
|
||||
welcome_type: Optional[str] = None
|
||||
welcome_message: Optional[str] = None
|
||||
channel_id: Optional[str] = None
|
||||
embed_data: Optional[WelcomeEmbedData] = None
|
||||
auto_delete_duration: Optional[int] = None
|
||||
|
||||
class AntiNukeConfig(BaseModel):
|
||||
guild_id: int
|
||||
status: bool
|
||||
whitelisted_users: List[str] = []
|
||||
|
||||
class VerificationConfig(BaseModel):
|
||||
guild_id: int
|
||||
verification_channel_id: Optional[str] = None
|
||||
verified_role_id: Optional[str] = None
|
||||
log_channel_id: Optional[str] = None
|
||||
verification_method: Optional[str] = "both"
|
||||
enabled: Optional[bool] = True
|
||||
|
||||
class TrackingConfig(BaseModel):
|
||||
guild_id: int
|
||||
channel_id: Optional[int] = None
|
||||
|
||||
class TrackingUpdate(BaseModel):
|
||||
channel_id: Optional[int] = None
|
||||
|
||||
class J2CConfig(BaseModel):
|
||||
guild_id: str
|
||||
join_channel_id: Optional[str] = None
|
||||
control_channel_id: Optional[str] = None
|
||||
category_id: Optional[str] = None
|
||||
|
||||
class J2CUpdate(BaseModel):
|
||||
join_channel_id: Optional[str] = None
|
||||
control_channel_id: Optional[str] = None
|
||||
category_id: Optional[str] = None
|
||||
|
||||
class JoinDMConfig(BaseModel):
|
||||
guild_id: str
|
||||
message: Optional[str] = None
|
||||
|
||||
class JoinDMUpdate(BaseModel):
|
||||
message: Optional[str] = None
|
||||
|
||||
class CustomRoleConfig(BaseModel):
|
||||
guild_id: str
|
||||
staff: Optional[str] = None
|
||||
girl: Optional[str] = None
|
||||
vip: Optional[str] = None
|
||||
guest: Optional[str] = None
|
||||
frnd: Optional[str] = None
|
||||
reqrole: Optional[str] = None
|
||||
|
||||
class CustomRoleUpdate(BaseModel):
|
||||
staff: Optional[str] = None
|
||||
girl: Optional[str] = None
|
||||
vip: Optional[str] = None
|
||||
guest: Optional[str] = None
|
||||
frnd: Optional[str] = None
|
||||
reqrole: Optional[str] = None
|
||||
|
||||
class AutoReactTrigger(BaseModel):
|
||||
trigger: str
|
||||
emojis: str
|
||||
|
||||
class AutoReactConfig(BaseModel):
|
||||
guild_id: str
|
||||
triggers: List[AutoReactTrigger] = []
|
||||
|
||||
class AutoReactUpdate(BaseModel):
|
||||
triggers: List[AutoReactTrigger]
|
||||
|
||||
class InvcConfig(BaseModel):
|
||||
guild_id: str
|
||||
role_id: Optional[str] = None
|
||||
enabled: bool = False
|
||||
|
||||
class InvcUpdate(BaseModel):
|
||||
role_id: Optional[str] = None
|
||||
enabled: Optional[bool] = None
|
||||
|
||||
class ReactionRoleEntry(BaseModel):
|
||||
message_id: str
|
||||
emoji: str
|
||||
role_id: str
|
||||
|
||||
class RRConfig(BaseModel):
|
||||
guild_id: str
|
||||
dm_enabled: bool
|
||||
roles: List[ReactionRoleEntry] = []
|
||||
|
||||
class RRUpdate(BaseModel):
|
||||
dm_enabled: Optional[bool] = None
|
||||
add_role: Optional[ReactionRoleEntry] = None
|
||||
remove_role_message_id: Optional[str] = None
|
||||
remove_role_emoji: Optional[str] = None
|
||||
|
||||
class InviteStat(BaseModel):
|
||||
user_id: str
|
||||
total: int
|
||||
fake: int
|
||||
left: int
|
||||
rejoin: int
|
||||
|
||||
class InvitesLeaderboard(BaseModel):
|
||||
guild_id: str
|
||||
data: List[InviteStat]
|
||||
|
||||
# --- Update Schemas (Input) ---
|
||||
|
||||
class VerificationUpdate(BaseModel):
|
||||
verification_channel_id: Optional[str] = None
|
||||
verified_role_id: Optional[str] = None
|
||||
log_channel_id: Optional[str] = None
|
||||
verification_method: Optional[str] = None
|
||||
enabled: Optional[bool] = None
|
||||
|
||||
class VanityRoleSetup(BaseModel):
|
||||
vanity: str
|
||||
role_id: str
|
||||
log_channel_id: str
|
||||
|
||||
class AutoRoleConfig(BaseModel):
|
||||
guild_id: str
|
||||
bots: List[str]
|
||||
humans: List[str]
|
||||
|
||||
class AutoRoleUpdate(BaseModel):
|
||||
bots: Optional[List[str]] = None
|
||||
humans: Optional[List[str]] = None
|
||||
|
||||
class AntiNukeUpdate(BaseModel):
|
||||
status: Optional[bool] = None
|
||||
add_whitelist: Optional[str] = None
|
||||
remove_whitelist: Optional[str] = None
|
||||
|
||||
|
||||
class WelcomeUpdate(BaseModel):
|
||||
welcome_type: Optional[str] = None
|
||||
welcome_message: Optional[str] = None
|
||||
channel_id: Optional[str] = None
|
||||
embed_data: Optional[WelcomeEmbedData] = None
|
||||
auto_delete_duration: Optional[int] = None
|
||||
|
||||
class PrefixUpdate(BaseModel):
|
||||
prefix: str
|
||||
|
||||
class TicketUpdate(BaseModel):
|
||||
panel_channel: Optional[int] = None
|
||||
logging_channel: Optional[int] = None
|
||||
closed_category: Optional[int] = None
|
||||
panel_type: Optional[str] = None
|
||||
embed_title: Optional[str] = None
|
||||
embed_description: Optional[str] = None
|
||||
embed_color: Optional[int] = None
|
||||
embed_image_url: Optional[str] = None
|
||||
embed_thumbnail_url: Optional[str] = None
|
||||
categories: Optional[List[TicketCategory]] = None
|
||||
staff_roles: Optional[List[int]] = None
|
||||
|
||||
class AutomodUpdate(BaseModel):
|
||||
enabled: Optional[bool] = None
|
||||
punishments: Optional[Dict[str, str]] = None
|
||||
ignored_roles: Optional[List[int]] = None
|
||||
ignored_channels: Optional[List[int]] = None
|
||||
logging_channel: Optional[int] = None
|
||||
|
||||
class LevelingUpdate(BaseModel):
|
||||
enabled: Optional[bool] = None
|
||||
xp_per_message: Optional[int] = None
|
||||
cooldown: Optional[int] = None
|
||||
level_up_channel: Optional[int] = None
|
||||
embed_color: Optional[str] = None
|
||||
|
||||
class LoggingUpdate(BaseModel):
|
||||
log_enabled: Optional[Dict[str, bool]] = None
|
||||
log_channels: Optional[Dict[str, int]] = None
|
||||
|
||||
class LeaderboardEntry(BaseModel):
|
||||
user_id: str
|
||||
name: str
|
||||
level: int
|
||||
xp: int
|
||||
|
||||
# --- Admin Schemas ---
|
||||
|
||||
class AdminNodeStatus(BaseModel):
|
||||
name: str
|
||||
status: str
|
||||
load: str
|
||||
icon: str # Icon identifier
|
||||
|
||||
class AdminStats(BaseModel):
|
||||
total_users: str
|
||||
active_servers: str
|
||||
api_latency: str
|
||||
db_size: str
|
||||
nodes: List[AdminNodeStatus]
|
||||
|
||||
class AdminConfig(BaseModel):
|
||||
maintenance_mode: bool
|
||||
global_notification: Optional[str] = None
|
||||
|
||||
class AdminConfigUpdate(BaseModel):
|
||||
maintenance_mode: Optional[bool] = None
|
||||
global_notification: Optional[str] = None
|
||||
153
bot/api/server.py
Normal file
153
bot/api/server.py
Normal file
@@ -0,0 +1,153 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════╗
|
||||
# ║ ║
|
||||
# ║ ░█▀▀░█▀█░█▀▄░█▀▀░█░█ ░█▀▄░█▀▀░█░█░█▀▀ ║
|
||||
# ║ ░█░░░█░█░█░█░█▀▀░▄▀▄ ░█░█░█▀▀░▀▄▀░▀▀█ ║
|
||||
# ║ ░▀▀▀░▀▀▀░▀▀░░▀▀▀░▀░▀ ░▀▀░░▀▀▀░░▀░░▀▀▀ ║
|
||||
# ║ ║
|
||||
# ║ © 2026 CodeX Devs — All Rights Reserved ║
|
||||
# ║ ║
|
||||
# ║ discord ── https://discord.gg/codexdev ║
|
||||
# ║ youtube ── https://youtube.com/@CodeXDevs ║
|
||||
# ║ github ── https://github.com/RayExo ║
|
||||
# ║ ║
|
||||
# ╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
from fastapi import FastAPI, Depends, Request, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
import os
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
from slowapi import _rate_limit_exceeded_handler
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from slowapi.middleware import SlowAPIMiddleware
|
||||
from utils.config import *
|
||||
|
||||
|
||||
from api.routes import bot, guilds, admin
|
||||
from api.dependencies import verify_api_key, limiter
|
||||
from api.db_manager import db_manager
|
||||
from api.discord_auth import (
|
||||
require_dashboard_admin,
|
||||
require_discord_session,
|
||||
require_guild_access,
|
||||
extract_guild_id_from_path,
|
||||
)
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
# Configure logging
|
||||
logger = logging.getLogger("api_request_logs")
|
||||
logger.setLevel(logging.INFO)
|
||||
if not logger.handlers:
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(logging.Formatter('%(message)s'))
|
||||
logger.addHandler(handler)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Setup: Nothing special needed for now
|
||||
yield
|
||||
# Shutdown: Close all shared database connections
|
||||
await db_manager.close_all()
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""
|
||||
Initializes the FastAPI application for the CodeX Bot Dashboard.
|
||||
The bot instance will be attached to app.state.bot in CodeX.py at runtime.
|
||||
"""
|
||||
app = FastAPI(
|
||||
title=f"{BRAND_NAME} Bot API",
|
||||
description=f"REST API to manage the {BRAND_NAME} Discord Bot features",
|
||||
version="1.0",
|
||||
dependencies=[Depends(verify_api_key)],
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# Discord permission checks for dashboard API routes
|
||||
@app.middleware("http")
|
||||
async def discord_permission_middleware(request: Request, call_next):
|
||||
if request.method == "OPTIONS":
|
||||
return await call_next(request)
|
||||
|
||||
path = request.url.path
|
||||
try:
|
||||
if path.startswith("/api/v1/admin"):
|
||||
await require_dashboard_admin(request)
|
||||
elif path.startswith("/api/v1/bot"):
|
||||
await require_discord_session(request)
|
||||
elif path.startswith("/api/v1/guilds"):
|
||||
guild_id = extract_guild_id_from_path(path)
|
||||
if guild_id is not None:
|
||||
await require_guild_access(request, guild_id)
|
||||
else:
|
||||
await require_discord_session(request)
|
||||
except HTTPException as exc:
|
||||
return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
|
||||
|
||||
return await call_next(request)
|
||||
|
||||
# Structured Logging Middleware
|
||||
@app.middleware("http")
|
||||
async def log_requests(request: Request, call_next):
|
||||
start_time = time.time()
|
||||
response = await call_next(request)
|
||||
process_time = time.time() - start_time
|
||||
|
||||
log_data = {
|
||||
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
"method": request.method,
|
||||
"path": request.url.path,
|
||||
"status_code": response.status_code,
|
||||
"duration_ms": round(process_time * 1000, 2),
|
||||
"client_ip": request.client.host if request.client else "unknown"
|
||||
}
|
||||
|
||||
logger.info(json.dumps(log_data))
|
||||
return response
|
||||
|
||||
# Attach limiter and handler
|
||||
app.state.limiter = limiter
|
||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||
app.add_middleware(SlowAPIMiddleware)
|
||||
|
||||
# Build allowed origins from env + hardcoded fallbacks
|
||||
_extra_origins = [
|
||||
o.strip()
|
||||
for o in os.getenv("CORS_ORIGINS", "").split(",")
|
||||
if o.strip()
|
||||
]
|
||||
_allowed_origins = list(dict.fromkeys([
|
||||
"http://localhost:3000",
|
||||
"https://localhost:3000",
|
||||
"https://your-vercel-url-here.vercel.app",
|
||||
*_extra_origins,
|
||||
]))
|
||||
|
||||
# Enable CORS for Next.js dashboard
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=_allowed_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Register Routers
|
||||
app.include_router(bot.router, prefix="/api/v1/bot", tags=["Bot"])
|
||||
app.include_router(guilds.router, prefix="/api/v1/guilds", tags=["Guilds"])
|
||||
app.include_router(admin.router, prefix="/api/v1/admin", tags=["Admin"])
|
||||
|
||||
@app.get("/", summary="API Root", description="Returns basic API information and online status.")
|
||||
async def root():
|
||||
return {
|
||||
"status": "online",
|
||||
"bot_name": {BRAND_NAME},
|
||||
"api_version": "1.0"
|
||||
}
|
||||
|
||||
@app.get("/health", summary="Health Check", description="Performs a health check for container orchestration and uptime monitoring.")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
return app
|
||||
Reference in New Issue
Block a user