import { WebhookConfig } from '../../types/config' async function getWorkerLocation() { const res = await fetch('https://cloudflare.com/cdn-cgi/trace') const text = await res.text() const colo = /^colo=(.*)$/m.exec(text)?.[1] return colo } const fetchTimeout = ( url: string, ms: number, { signal, ...options }: RequestInit | undefined = {} ): Promise => { const controller = new AbortController() const promise = fetch(url, { signal: controller.signal, ...options }) if (signal) signal.addEventListener('abort', () => controller.abort()) const timeout = setTimeout(() => controller.abort(), ms) return promise.finally(() => clearTimeout(timeout)) } function withTimeout(millis: number, promise: Promise): Promise { const timeout = new Promise((resolve, reject) => setTimeout(() => reject(new Error(`Promise timed out after ${millis}ms`)), millis) ) return Promise.race([promise, timeout]) } function formatStatusChangeNotification( monitor: any, isUp: boolean, timeIncidentStart: number, timeNow: number, reason: string, timeZone: string ) { const dateFormatter = new Intl.DateTimeFormat('en-US', { month: 'numeric', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, timeZone: timeZone, }) let downtimeDuration = Math.round((timeNow - timeIncidentStart) / 60) const timeNowFormatted = dateFormatter.format(new Date(timeNow * 1000)) const timeIncidentStartFormatted = dateFormatter.format(new Date(timeIncidentStart * 1000)) if (isUp) { return `✅ ${monitor.name} is up! \nThe service is up again after being down for ${downtimeDuration} minutes.` } else if (timeNow == timeIncidentStart) { return `🔴 ${ monitor.name } is currently down. \nService is unavailable at ${timeNowFormatted}. \nIssue: ${ reason || 'unspecified' }` } else { return `🔴 ${ monitor.name } is still down. \nService is unavailable since ${timeIncidentStartFormatted} (${downtimeDuration} minutes). \nIssue: ${ reason || 'unspecified' }` } } async function webhookNotify(webhook: WebhookConfig, message: string) { console.log( 'Sending webhook notification: ' + JSON.stringify(message) + ' to webhook ' + webhook.url ) try { let url = webhook.url let method = webhook.method let headers = new Headers(webhook.headers as any) let payloadTemplated = webhook.payload Object.keys(payloadTemplated).forEach((k) => { if (payloadTemplated[k] === '$MSG') { payloadTemplated[k] = message } }) let body = undefined switch (webhook.payloadType) { case 'param': method = method ?? 'GET' const urlTmp = new URL(url) for (const [k, v] of Object.entries(payloadTemplated)) { urlTmp.searchParams.append(k, v.toString()) } url = urlTmp.toString() break case 'json': method = method ?? 'POST' if (headers.get('content-type') === null) { headers.set('content-type', 'application/json') } body = JSON.stringify(payloadTemplated) break case 'x-www-form-urlencoded': method = method ?? 'POST' if (headers.get('content-type') === null) { headers.set('content-type', 'application/x-www-form-urlencoded') } body = new URLSearchParams(payloadTemplated as any).toString() break default: throw 'Unrecognized payload type: ' + webhook.payloadType } console.log( `Webhook finalized parameters: ${method} ${url}, headers ${JSON.stringify( Object.fromEntries(headers.entries()) )}, body ${JSON.stringify(body)}` ) const resp = await fetchTimeout(url, webhook.timeout ?? 5000, { method, headers, body }) if (!resp.ok) { console.log( 'Error calling webhook server, code: ' + resp.status + ', response: ' + (await resp.text()) ) } else { console.log('Webhook notification sent successfully, code: ' + resp.status) } } catch (e) { console.log('Error calling webhook server: ' + e) } } export { getWorkerLocation, fetchTimeout, withTimeout, webhookNotify, formatStatusChangeNotification, }