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<string, string | number | undefined>
}
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<unknown> {
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
}
}