import { log } from "../deps.ts"; import config from "./config.ts"; import { sleep } from "./util.ts"; export async function sendWebhook( id: string, token: string, headers: HeadersInit, body: string, ): Promise { const url = `https://discord.com/api/webhooks/${id}/${token}/github?wait=1`; let res: Response; let retries = 0; do { const req = new Request(url, { method: "POST", headers: headers, body: body, }); log.info(`sending webhook request to ${url}`); res = await fetch(req); // return response if everything's fine if (res.status !== 429) break; const reset = res.headers.get("retry-after"); // should always exist, even for cf bans, but checking anyway if (reset === null) break; // parse retry delay let resetms = parseFloat(reset); if (!res.headers.has("via")) { // if there's no `via` header, this is likely a cf ban, which uses seconds instead of milliseconds resetms *= 1000; } // if we'd wait longer than the configured limit, just return the 429 if (resetms > config.maxWebhookRetryMs) { log.warning( `ratelimited for ${resetms}ms (> ${config.maxWebhookRetryMs}ms), not retrying`, ); break; } // maybe wait and retry if (retries >= config.maxWebhookRetries) { log.warning(`reached maximum number of retries (${retries})`); break; } retries++; log.warning(`retrying after ${resetms}ms (retry ${retries})`); await sleep(resetms); } while (true); // clone response to make headers mutable res = new Response(res.body, res); // set metadata headers const meta: Record = { "deploy": config.deployId, }; if (retries) meta["retries"] = retries.toString(); setMetadata(res, meta); return res; } function setMetadata(res: Response, meta: Record): void { for (const [key, value] of Object.entries(meta)) { res.headers.set(`x-webhook-filter-${key}`, value); } }