import { BOT_SECRET_SETUP_URL, DISCORD_API_BASE_URL, DISCORD_USER_AGENT, throwDiscordApiError, } from './errors.ts' import { secretName, type InputRecord } from './validation.ts' export type DiscordRequestInit = { method?: string headers?: HeadersInit body?: BodyInit | null query?: Record } function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } export function botAuthorizationHeader(name: string): string { return 'Bot {{secret:' + name + '|scope=user}}' } export async function discordBotRequest( path: string, input: InputRecord, init: DiscordRequestInit = {}, ): Promise { const name = secretName(input) const url = new URL(DISCORD_API_BASE_URL + path) for (const [key, value] of Object.entries(init.query ?? {})) { if (value === undefined) continue url.searchParams.set(key, String(value)) } const requestPath = url.pathname + url.search for (let attempt = 0; ; attempt += 1) { const headers = new Headers(init.headers || {}) headers.set('Authorization', botAuthorizationHeader(name)) headers.set('User-Agent', DISCORD_USER_AGENT) if (init.body && !headers.has('Content-Type') && typeof init.body === 'string') { headers.set('Content-Type', 'application/json') } let response: Response try { response = await fetch(url.toString(), { method: init.method ?? 'GET', headers, body: init.body ?? undefined, }) } catch (error) { const message = error instanceof Error ? error.message : String(error) throw new Error( 'Discord bot fetch failed before a response: ' + message + '. Approve host discord.com and save `' + name + '` at ' + BOT_SECRET_SETUP_URL, ) } const text = await response.text() let json: unknown = null if (text) { try { json = JSON.parse(text) } catch { json = text } } if (response.status === 429 && attempt === 0) { const retryAfter = typeof json === 'object' && json && 'retry_after' in json ? Number((json as { retry_after?: unknown }).retry_after) : Number(response.headers.get('retry-after')) const waitSeconds = Math.min(Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : 1, 5) await sleep(waitSeconds * 1000) continue } if (!response.ok) { throwDiscordApiError({ status: response.status, path: requestPath, lane: 'bot', details: json ?? text, secretName: name, }) } return json } }