import { env } from "cloudflare:workers";
import { all, chunks, first, placeholders, run } from "../db";
import { hmacSha256Hex, randomId, timingSafeEqualStrings } from "../crypto";
import { fromBase64Url, toBase64Url } from "~/lib/base64url";
import { publicUrl } from "../events/emit";
import { CATEGORY_INFO, NOTIFICATION_CATEGORIES } from "~/lib/notification-categories";
import type { DigestMode, NotificationCategory } from "~/lib/notification-categories";
import type { EmailFooter } from "../email";
export type CategoryPref = { web: boolean; email: boolean; digest: DigestMode };
/** Unsubscribe targets: a category, or the legacy participating/watching email switches. */
export type UnsubscribeTarget = NotificationCategory | "participating" | "watching";
const HOUR_MS = 60 * 60 * 1000;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
/** Per-recipient email budget per hour; overflow is folded into the next digest instead of being dropped. */
export const EMAILS_PER_HOUR = 20;
function defaults(category: NotificationCategory): CategoryPref {
const info = CATEGORY_INFO[category];
return { web: info.web, email: info.email, digest: "off" };
}
type PrefRow = { user_id: string; category: string; web: number; email: number; digest: string };
function fromRow(category: NotificationCategory, row: PrefRow | undefined): CategoryPref {
if (!row) return defaults(category);
const digest = row.digest === "daily" && CATEGORY_INFO[category].digestable ? "daily" : "off";
return { web: row.web === 1, email: row.email === 1, digest };
}
export async function loadCategoryPrefs(userId: string): Promise<Record<NotificationCategory, CategoryPref>> {
const rows = await all<PrefRow>("SELECT * FROM notification_category_prefs WHERE user_id = ?", userId);
const byCategory = new Map(rows.map((row) => [row.category, row]));
const out = {} as Record<NotificationCategory, CategoryPref>;
for (const category of NOTIFICATION_CATEGORIES) out[category] = fromRow(category, byCategory.get(category));
return out;
}