diff --git a/bot/cogs/__init__.py b/bot/cogs/__init__.py index 2a84f44..e974cb5 100644 --- a/bot/cogs/__init__.py +++ b/bot/cogs/__init__.py @@ -192,8 +192,17 @@ COG_CLASSES: tuple[type, ...] = ( async def setup(bot: Axiom) -> None: + loaded = 0 for cog_cls in COG_CLASSES: - await bot.add_cog(cog_cls(bot)) - print(Fore.RED + Style.BRIGHT + f"Loaded cog: {cog_cls.__name__}") + try: + await bot.add_cog(cog_cls(bot)) + loaded += 1 + print(Fore.GREEN + Style.BRIGHT + f"Loaded cog: {cog_cls.__name__}") + except Exception as e: + print( + Fore.RED + + Style.BRIGHT + + f"Failed cog {getattr(cog_cls, '__name__', cog_cls)}: {type(e).__name__}: {e}" + ) - print(Fore.RED + Style.BRIGHT + f"All {BotName} Cogs loaded successfully.") + print(Fore.GREEN + Style.BRIGHT + f"Loaded {loaded}/{len(COG_CLASSES)} {BotName} cogs.") diff --git a/bot/cogs/commands/giveaway.py b/bot/cogs/commands/giveaway.py index f48bf00..8038565 100644 --- a/bot/cogs/commands/giveaway.py +++ b/bot/cogs/commands/giveaway.py @@ -25,10 +25,11 @@ import os import aiohttp from utils.cv2 import CV2 -db_folder = 'db' +from utils.db_paths import db_path as resolve_db_path + db_file = 'giveaways.db' -db_path = os.path.join(db_folder, db_file) -connection = sqlite3.connect(db_path) +giveaways_db_path = resolve_db_path(db_file) +connection = sqlite3.connect(giveaways_db_path) cursor = connection.cursor() @@ -74,7 +75,7 @@ class Giveaway(commands.Cog): self.bot = bot async def cog_load(self) -> None: - self.connection = await aiosqlite.connect(db_path) + self.connection = await aiosqlite.connect(giveaways_db_path) self.cursor = await self.connection.cursor() await self.check_for_ended_giveaways() self.GiveawayEnd.start() diff --git a/bot/cogs/commands/nightmode.py b/bot/cogs/commands/nightmode.py index dcda7ba..46705b3 100644 --- a/bot/cogs/commands/nightmode.py +++ b/bot/cogs/commands/nightmode.py @@ -19,11 +19,10 @@ from utils.Tools import * from utils.cv2 import CV2 from discord.ui import TextDisplay, Separator, ActionRow, LayoutView, Container from utils.config import OWNER_IDS_STR +from utils.db_paths import db_path as resolve_db_path # Database setup -db_folder = "db" -db_file = "anti.db" -db_path = os.path.join(db_folder, db_file) +nightmode_db_path = resolve_db_path("anti.db") class Nightmode(commands.Cog): @@ -34,7 +33,7 @@ class Nightmode(commands.Cog): self.color = 0xFF0000 async def initialize_db(self): - self.db = await aiosqlite.connect(db_path) + self.db = await aiosqlite.connect(nightmode_db_path) await self.db.execute(""" CREATE TABLE IF NOT EXISTS Nightmode ( guildId TEXT, diff --git a/bot/cogs/commands/owner.py b/bot/cogs/commands/owner.py index 58dfb0f..61d4dbf 100644 --- a/bot/cogs/commands/owner.py +++ b/bot/cogs/commands/owner.py @@ -12,7 +12,7 @@ # ╚══════════════════════════════════════════════════════════════════╝ from __future__ import annotations -from utils.db_paths import db_path +from utils.db_paths import db_path as resolve_db_path from discord.ext import commands from discord import * from PIL import Image, ImageDraw, ImageFont, ImageFilter @@ -71,12 +71,12 @@ BACK_COMPAT_BADGE_DICT = DISCORD_BADGE_EMOJIS db_folder = 'db' db_file = 'badges.db' -db_path = os.path.join(db_folder, db_file) +badges_db_path = resolve_db_path(db_file) FONT_PATH = os.path.join('utils', 'arial.ttf') # --- Database Setup --- -os.makedirs(db_folder, exist_ok=True) -conn = sqlite3.connect(db_path) +os.makedirs(os.path.dirname(badges_db_path) or 'db', exist_ok=True) +conn = sqlite3.connect(badges_db_path) c = conn.cursor() c.execute('CREATE TABLE IF NOT EXISTS badges (user_id INTEGER PRIMARY KEY)') @@ -136,7 +136,7 @@ class Owner(commands.Cog): self.client = client self.staff = set() self.np_cache = [] - self.db_path = db_path('np.db') + self.db_path = resolve_db_path('np.db') self.stop_tour = False self.bot_owner_ids = BOT_OWNER_IDS self.client.loop.create_task(self.setup_database()) diff --git a/dashboard/app/dashboard/guilds/page.tsx b/dashboard/app/dashboard/guilds/page.tsx index df0c847..de8bcf1 100644 --- a/dashboard/app/dashboard/guilds/page.tsx +++ b/dashboard/app/dashboard/guilds/page.tsx @@ -64,26 +64,11 @@ export default async function GuildsPage() { } try { - const res = await fetch("https://discord.com/api/users/@me/guilds", { - headers: { - Authorization: `Bearer ${session.accessToken}`, - }, - cache: "no-store", - }); - - if (res.ok) { - userGuilds = await res.json(); - } else { - const body = await res.text().catch(() => ""); - console.error("Discord guilds fetch failed:", res.status, body); - userDiscordError = - res.status === 401 - ? "Discord session expired — please sign in again." - : "Failed to fetch your Discord servers."; - } - } catch (err) { + const { fetchUserGuilds } = await import("@/lib/guild-auth"); + userGuilds = await fetchUserGuilds(session.accessToken); + } catch (err: any) { console.error("Discord API Error:", err); - userDiscordError = "Error connecting to Discord."; + userDiscordError = err?.message || "Error connecting to Discord."; } const MANAGE_GUILD = BigInt(0x20); diff --git a/dashboard/lib/guild-auth.ts b/dashboard/lib/guild-auth.ts index cdea21a..6f61151 100644 --- a/dashboard/lib/guild-auth.ts +++ b/dashboard/lib/guild-auth.ts @@ -1,9 +1,12 @@ /** * Discord guild permission helpers for dashboard access control. + * Includes short-lived cache + 429 retry to avoid Discord rate limits. */ export interface DiscordGuild { id: string; + name?: string; + icon?: string | null; permissions: string; owner?: boolean; } @@ -11,6 +14,14 @@ export interface DiscordGuild { const MANAGE_GUILD = BigInt(0x20); const ADMINISTRATOR = BigInt(0x8); +const CACHE_TTL_MS = 60_000; +const MAX_RETRIES = 3; + +type CacheEntry = { guilds: DiscordGuild[]; expiresAt: number }; + +const guildCache = new Map(); +const inflight = new Map>(); + export function canManageGuild(guild: DiscordGuild): boolean { if (guild.owner) return true; try { @@ -24,15 +35,80 @@ export function canManageGuild(guild: DiscordGuild): boolean { } } -export async function fetchUserGuilds( - accessToken: string -): Promise { - const res = await fetch("https://discord.com/api/users/@me/guilds", { - headers: { Authorization: `Bearer ${accessToken}` }, - cache: "no-store", - }); - if (!res.ok) return []; - return res.json(); +function cacheKey(accessToken: string): string { + // Avoid storing raw token as object key longer than needed — short fingerprint is enough for process cache + return `tok:${accessToken.slice(0, 12)}:${accessToken.length}:${accessToken.slice(-8)}`; +} + +export function invalidateUserGuildsCache(accessToken?: string) { + if (!accessToken) { + guildCache.clear(); + return; + } + guildCache.delete(cacheKey(accessToken)); +} + +async function fetchUserGuildsUncached(accessToken: string): Promise { + let attempt = 0; + while (attempt < MAX_RETRIES) { + attempt += 1; + const res = await fetch("https://discord.com/api/users/@me/guilds", { + headers: { Authorization: `Bearer ${accessToken}` }, + cache: "no-store", + }); + + if (res.ok) { + return (await res.json()) as DiscordGuild[]; + } + + if (res.status === 429) { + let retryAfter = 1; + try { + const body = await res.json(); + retryAfter = Number(body.retry_after ?? 1); + } catch { + const header = res.headers.get("retry-after"); + retryAfter = header ? Number(header) : 1; + } + const waitMs = Math.min(Math.max(retryAfter * 1000, 300), 5000); + console.warn(`Discord guilds rate-limited — retry in ${waitMs}ms (attempt ${attempt}/${MAX_RETRIES})`); + await new Promise((r) => setTimeout(r, waitMs)); + continue; + } + + const body = await res.text().catch(() => ""); + console.error("Discord guilds fetch failed:", res.status, body); + throw new Error( + res.status === 401 + ? "Discord session expired — please sign in again." + : `Failed to fetch your Discord servers. (${res.status})` + ); + } + + throw new Error("Failed to fetch your Discord servers. (rate limited)"); +} + +export async function fetchUserGuilds(accessToken: string): Promise { + const key = cacheKey(accessToken); + const cached = guildCache.get(key); + if (cached && cached.expiresAt > Date.now()) { + return cached.guilds; + } + + const existing = inflight.get(key); + if (existing) return existing; + + const promise = fetchUserGuildsUncached(accessToken) + .then((guilds) => { + guildCache.set(key, { guilds, expiresAt: Date.now() + CACHE_TTL_MS }); + return guilds; + }) + .finally(() => { + inflight.delete(key); + }); + + inflight.set(key, promise); + return promise; } export async function userCanManageGuild(