39 lines
2.9 KiB
TypeScript
39 lines
2.9 KiB
TypeScript
import { db } from "@/lib/db";
|
|
|
|
function allowed(bucket: string, seconds = 3600) {
|
|
const now = Math.floor(Date.now() / 1000); const row = db.prepare("SELECT reset_at FROM rate_limits WHERE bucket=?").get(bucket) as { reset_at: number } | undefined;
|
|
if (row && row.reset_at > now) return false;
|
|
db.prepare("INSERT INTO rate_limits(bucket,count,reset_at) VALUES(?,1,?) ON CONFLICT(bucket) DO UPDATE SET count=count+1,reset_at=excluded.reset_at").run(bucket, now + seconds); return true;
|
|
}
|
|
export function sendAlert(bucket: string, title: string, message: string) {
|
|
if (!process.env.ALERT_WEBHOOK_URL?.trim() || !allowed(`alert:${bucket}`)) return false;
|
|
db.prepare("INSERT INTO alert_deliveries(bucket,title,message) VALUES(?,?,?)").run(bucket, title, message);
|
|
return true;
|
|
}
|
|
|
|
type Alert = { id: number; title: string; message: string; attempts: number };
|
|
|
|
export async function deliverNextAlert() {
|
|
const candidate = db.prepare("SELECT id FROM alert_deliveries WHERE status='queued' AND run_after<=CURRENT_TIMESTAMP ORDER BY id LIMIT 1").get() as { id: number } | undefined;
|
|
if (!candidate) return false;
|
|
const claimed = db.prepare("UPDATE alert_deliveries SET status='running',attempts=attempts+1,started_at=CURRENT_TIMESTAMP WHERE id=? AND status='queued'").run(candidate.id);
|
|
if (!claimed.changes) return false;
|
|
const alert = db.prepare("SELECT id,title,message,attempts FROM alert_deliveries WHERE id=?").get(candidate.id) as Alert;
|
|
const url = process.env.ALERT_WEBHOOK_URL?.trim();
|
|
try {
|
|
if (!url) throw new Error("ALERT_WEBHOOK_URL is not configured");
|
|
const target = new URL(url);
|
|
if (target.protocol !== "https:") throw new Error("Alert webhook must use HTTPS");
|
|
const isNtfy = /(^|\.)ntfy\.sh$/.test(target.hostname);
|
|
const response = await fetch(target, isNtfy ? { method: "POST", headers: { Title: alert.title, Priority: "high" }, body: alert.message, signal: AbortSignal.timeout(10_000) } : { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: `**${alert.title}**\n${alert.message}` }), signal: AbortSignal.timeout(10_000) });
|
|
if (!response.ok) throw new Error(`Alert endpoint returned ${response.status}`);
|
|
db.prepare("UPDATE alert_deliveries SET status='done',finished_at=CURRENT_TIMESTAMP,last_error=NULL WHERE id=?").run(alert.id);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : "Alert delivery failed";
|
|
const exhausted = alert.attempts >= 5;
|
|
const delayMinutes = Math.min(60, 2 ** Math.max(0, alert.attempts - 1));
|
|
db.prepare("UPDATE alert_deliveries SET status=?,last_error=?,finished_at=CASE WHEN ? THEN CURRENT_TIMESTAMP ELSE NULL END,run_after=CASE WHEN ? THEN run_after ELSE datetime('now', ?) END WHERE id=?").run(exhausted ? "failed" : "queued", message, exhausted ? 1 : 0, exhausted ? 1 : 0, `+${delayMinutes} minutes`, alert.id);
|
|
}
|
|
return true;
|
|
}
|