diff --git a/pages/api/team-cache.ts b/pages/api/team-cache.ts new file mode 100644 index 000000000..703f09c7f --- /dev/null +++ b/pages/api/team-cache.ts @@ -0,0 +1,96 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import teamMembers from '../../public/team.json' + +// Cache duration in seconds (12 hours) +const CACHE_DURATION = 12 * 60 * 60 + +// In-memory cache for user data +// Note: This will be reset whenever the serverless function cold starts +let userDataCache: Record = {} +let cacheTimestamp = 0 + +async function fetchUserData(slackId: string) { + try { + const response = await fetch(`https://cachet.dunkirk.sh/users/${slackId}`, { + method: 'GET', + headers: { + 'Cache-Control': 'no-cache' + } + }) + + return await response.json() + } catch (error) { + console.error(`Error fetching data for ${slackId}:`, error) + return { message: 'Error fetching user data' } + } +} + +async function refreshCache() { + // Skip refresh if cache is still fresh + const now = Date.now() + if (now - cacheTimestamp < CACHE_DURATION * 1000 && Object.keys(userDataCache).length > 0) { + return userDataCache + } + + console.log('Refreshing team data cache...') + const newCache: Record = {} + + // Process members in batches to avoid rate limiting + const batchSize = 5 + const slackIds = teamMembers + .filter(member => member.slackId) + .map(member => member.slackId) + + for (let i = 0; i < slackIds.length; i += batchSize) { + const batch = slackIds.slice(i, i + batchSize) + const promises = batch.map(async slackId => { + const data = await fetchUserData(slackId) + if (!data.message) { + newCache[slackId] = { + pronouns: data.pronouns, + displayName: data.displayName, + user: data.user + } + } + }) + + await Promise.all(promises) + + // Small delay between batches + if (i + batchSize < slackIds.length) { + await new Promise(resolve => setTimeout(resolve, 500)) + } + } + + userDataCache = newCache + cacheTimestamp = now + return newCache +} + +// Export for internal use by other API routes +export async function getUserCache(forceRefresh = false) { + if (forceRefresh || Object.keys(userDataCache).length === 0 || Date.now() - cacheTimestamp > CACHE_DURATION * 1000) { + await refreshCache() + } + + return userDataCache +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + // Force refresh if requested + const forceRefresh = req.query.refresh === 'true' + + // Get the cache (refreshing if needed) + await getUserCache(forceRefresh) + + // Set cache control headers + res.setHeader('Cache-Control', `s-maxage=${CACHE_DURATION}, stale-while-revalidate`) + res.status(200).json({ + cache: userDataCache, + timestamp: cacheTimestamp, + expires: new Date(cacheTimestamp + CACHE_DURATION * 1000).toISOString() + }) +} \ No newline at end of file diff --git a/pages/api/team.ts b/pages/api/team.ts index b06adb82b..f01ba7dce 100644 --- a/pages/api/team.ts +++ b/pages/api/team.ts @@ -12,43 +12,55 @@ interface TeamMember { overrideAvatar: string email: string website: string - pronouns: string + pronouns?: string avatar: string } +// Cache duration in seconds (5 minutes) +const CACHE_DURATION = 5 * 60 + export async function fetchTeam() { const current: TeamMember[] = [] const acknowledged: TeamMember[] = [] - for (const member of teamMembers as TeamMember[]) { - if (process.env.SLACK_API_TOKEN) { - const formData = new FormData() - formData.append('token', process.env.SLACK_API_TOKEN) - formData.append('user', member.slackId) - - const slackData = await fetch( - `https://hackclub.slack.com/api/users.profile.get?user=${member.slackId}`, - { - method: 'POST', - headers: { - 'content-type': 'multipart/form-data', - cookie: process.env.SLACK_API_COOKIE || '' - }, - body: formData + try { + // Get user cache directly from the team-cache module + // This is more reliable in serverless environments than making HTTP requests + const { getUserCache } = await import('./team-cache') + const userDataCache = await getUserCache() + + for (const member of teamMembers as TeamMember[]) { + // Always use cachet.dunkirk.sh for avatar images + if (member.slackId) { + member.avatar = `https://cachet.dunkirk.sh/users/${member.slackId}/r` + + // Apply cached pronouns if available + if (userDataCache[member.slackId]?.pronouns) { + member.pronouns = userDataCache[member.slackId].pronouns } - ).then(r => r.json()) + } - if (slackData.ok) { - member.pronouns = slackData.profile.pronouns + if (member.acknowledged) { + acknowledged.push(member) } else { - console.warn('Not found:', member.slackId) + current.push(member) } } + } catch (error) { + console.error('Error fetching cached team data:', error) + + // Fallback if cache fails + for (const member of teamMembers as TeamMember[]) { + // Always use cachet.dunkirk.sh for avatar images + if (member.slackId) { + member.avatar = `https://cachet.dunkirk.sh/users/${member.slackId}/r` + } - if (member.acknowledged) { - acknowledged.push(member) - } else { - current.push(member) + if (member.acknowledged) { + acknowledged.push(member) + } else { + current.push(member) + } } } @@ -59,5 +71,10 @@ export default async function handler( _req: NextApiRequest, res: NextApiResponse ) { + // Set cache headers + res.setHeader( + 'Cache-Control', + `s-maxage=${CACHE_DURATION}, stale-while-revalidate` + ) res.status(200).json(await fetchTeam()) }