Share member list refreshes, fall back to stale cache and retry on Discord rate limits

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
leanderandClaude Opus 5.5 committed 2026-10-31 01:41:54 +01:00
1 parent 7c37c4d6d5
commit 6a3487e6da
2 files changed
+75 -87

No files matched your search

+35 -35
View File
@@ -1,50 +1,50 @@
import type { DiscordGuildMember } from "@/types"; import type { DiscordGuildMember } from "@/types";
export async function verifyUserRole(userId: string): Promise<boolean> { const DISCORD_API_URL = "https://discord.com/api/v10";
const guildId = process.env.DISCORD_GUILD_ID; const MAX_ATTEMPTS = 3;
const requiredRoleId = process.env.DISCORD_REQUIRED_ROLE_ID; const MAX_RETRY_DELAY_SECONDS = 10;
if (!guildId || !requiredRoleId) { export function requireEnv(name: string): string {
console.error("Missing DISCORD_GUILD_ID or DISCORD_REQUIRED_ROLE_ID environment variables"); const value = process.env[name];
return false; if (!value) {
throw new Error(`Missing ${name} environment variable`);
} }
return value;
}
try { export async function discordFetch(path: string): Promise<Response> {
// Get the bot token from the Discord application const botToken = requireEnv("DISCORD_BOT_TOKEN");
const botToken = process.env.DISCORD_BOT_TOKEN;
if (!botToken) {
console.error("Missing DISCORD_BOT_TOKEN environment variable");
return false;
}
// Fetch the user's guild member information for (let attempt = 1; ; attempt++) {
const response = await fetch(`https://discord.com/api/v10/guilds/${guildId}/members/${userId}`, { const response = await fetch(`${DISCORD_API_URL}${path}`, {
headers: { headers: { Authorization: `Bot ${botToken}` },
Authorization: `Bot ${botToken}`,
"Content-Type": "application/json",
},
}); });
if (response.status !== 429 || attempt === MAX_ATTEMPTS) {
return response;
}
const retryAfter = Math.min(Number(response.headers.get("retry-after")) || 1, MAX_RETRY_DELAY_SECONDS);
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
}
}
export async function verifyUserRole(userId: string): Promise<boolean> {
try {
const guildId = requireEnv("DISCORD_GUILD_ID");
const requiredRoleId = requireEnv("DISCORD_REQUIRED_ROLE_ID");
const response = await discordFetch(`/guilds/${guildId}/members/${userId}`);
if (response.status === 404) {
return false;
}
if (!response.ok) { if (!response.ok) {
if (response.status === 404) { throw new Error(`Failed to fetch guild member ${userId}: ${response.status} ${response.statusText}`);
// User is not a member of the guild
console.log(`User ${userId} is not a member of guild ${guildId}`);
return false;
}
if (response.status === 403) {
console.error("Bot does not have permission to view guild members");
return false;
}
throw new Error(`Discord API error: ${response.status} ${response.statusText}`);
} }
const member: DiscordGuildMember = await response.json(); const member: DiscordGuildMember = await response.json();
return member.roles.includes(requiredRoleId);
// Check if the user has the required role
const hasRole = member.roles.includes(requiredRoleId);
console.log(`User ${userId} has role ${requiredRoleId}: ${hasRole}`);
return hasRole;
} catch (error) { } catch (error) {
console.error("Error verifying user role:", error); console.error("Error verifying user role:", error);
return false; return false;
+40 -52
View File
@@ -1,68 +1,56 @@
import { discordFetch, requireEnv } from "@/lib/discord";
import type { DiscordGuildMember, Match } from "@/types"; import type { DiscordGuildMember, Match } from "@/types";
interface CacheEntry { const CACHE_TTL = 5 * 60 * 1000;
data: DiscordGuildMember[]; const PAGE_SIZE = 1000;
timestamp: number;
}
// In-memory cache for Discord API responses let membersCache: { members: DiscordGuildMember[]; fetchedAt: number } | null = null;
let discordMembersCache: CacheEntry | null = null; let pendingFetch: Promise<DiscordGuildMember[]> | null = null;
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes in milliseconds
async function getDiscordMembers(): Promise<DiscordGuildMember[]> { async function fetchAllMembers(): Promise<DiscordGuildMember[]> {
const guildId = process.env.DISCORD_GUILD_ID; const guildId = requireEnv("DISCORD_GUILD_ID");
const botToken = process.env.DISCORD_BOT_TOKEN; const members: DiscordGuildMember[] = [];
let after = "0";
if (!guildId || !botToken) {
throw new Error("Missing required Discord environment variables");
}
// Check if cache is valid
if (discordMembersCache && Date.now() - discordMembersCache.timestamp < CACHE_TTL) {
return discordMembersCache.data;
}
// Fetch fresh data from Discord API with pagination
const allMembers: DiscordGuildMember[] = [];
let after: string | undefined = undefined;
const limit = 1000; // Maximum allowed by Discord API
do {
const url = new URL(`https://discord.com/api/v10/guilds/${guildId}/members`);
url.searchParams.set("limit", limit.toString());
if (after) {
url.searchParams.set("after", after);
}
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bot ${botToken}`,
"Content-Type": "application/json",
},
});
for (;;) {
const response = await discordFetch(`/guilds/${guildId}/members?limit=${PAGE_SIZE}&after=${after}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch guild members: ${response.status} ${response.statusText}`); throw new Error(`Failed to fetch guild members: ${response.status} ${response.statusText}`);
} }
const members: DiscordGuildMember[] = await response.json(); const page: DiscordGuildMember[] = await response.json();
allMembers.push(...members); members.push(...page);
// Update 'after' for next page (use the last member's user ID) if (page.length < PAGE_SIZE) {
if (members.length === limit) { return members;
after = members[members.length - 1].user.id;
} else {
after = undefined; // No more pages
} }
} while (after); after = page[page.length - 1].user.id;
}
}
// Update cache async function getDiscordMembers(): Promise<DiscordGuildMember[]> {
discordMembersCache = { if (membersCache && Date.now() - membersCache.fetchedAt < CACHE_TTL) {
data: allMembers, return membersCache.members;
timestamp: Date.now(), }
};
return allMembers; pendingFetch ??= fetchAllMembers()
.then((members) => {
membersCache = { members, fetchedAt: Date.now() };
return members;
})
.finally(() => {
pendingFetch = null;
});
try {
return await pendingFetch;
} catch (error) {
if (!membersCache) {
throw error;
}
console.error("Failed to refresh guild members, using stale cache:", error);
return membersCache.members;
}
} }
function getDisplayName(member: DiscordGuildMember): string { function getDisplayName(member: DiscordGuildMember): string {