Refactor database path resolution in cogs and improve cog loading error handling. Update database connections in Giveaway, Nightmode, and Owner cogs to use a centralized path resolver. Enhance cog loading feedback with success and failure messages.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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<string, CacheEntry>();
|
||||
const inflight = new Map<string, Promise<DiscordGuild[]>>();
|
||||
|
||||
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<DiscordGuild[]> {
|
||||
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<DiscordGuild[]> {
|
||||
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<DiscordGuild[]> {
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user