first commit

This commit is contained in:
RayExo
2026-07-12 15:16:07 +05:30
commit 2289301f26
915 changed files with 69400 additions and 0 deletions

0
bot/api/__init__.py Normal file
View File

40
bot/api/db_manager.py Normal file
View File

@@ -0,0 +1,40 @@
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()

61
bot/api/dependencies.py Normal file
View File

@@ -0,0 +1,61 @@
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

View File

114
bot/api/routes/admin.py Normal file
View File

@@ -0,0 +1,114 @@
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"}

39
bot/api/routes/bot.py Normal file
View File

@@ -0,0 +1,39 @@
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"
)

1371
bot/api/routes/guilds.py Normal file

File diff suppressed because it is too large Load Diff

332
bot/api/schemas.py Normal file
View File

@@ -0,0 +1,332 @@
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

99
bot/api/server.py Normal file
View File

@@ -0,0 +1,99 @@
from fastapi import FastAPI, Depends, Request
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
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
# 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
)
# 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)
# Enable CORS for Next.js dashboard
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000",
"https://localhost:3000",
# Add production domain here later
],
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