import { db, notifications, users, recipes, eq } from "@epicure/db"; import { randomUUID } from "crypto"; import { sendPushNotification } from "./push"; import { sendEmail, notificationEmailHtml } from "./email"; import { getMessages, formatMessage } from "./i18n/server"; import { isNotificationCategoryEnabled, isEmailCategoryEnabled } from "./notification-prefs"; type NotificationType = "follow" | "comment" | "reply" | "reaction" | "rating" | "mention"; type CreateNotificationOpts = { userId: string; type: NotificationType; actorId: string; recipeId?: string; commentId?: string; /** Rating score (1-5), only relevant for type "rating" — included in the push/email copy. */ score?: number; }; export async function createNotification(opts: CreateNotificationOpts): Promise { if (opts.userId === opts.actorId) return; // never notify yourself await db.insert(notifications).values({ id: randomUUID(), userId: opts.userId, type: opts.type, actorId: opts.actorId, recipeId: opts.recipeId, commentId: opts.commentId, }); // Push + email are best-effort side effects — never let a slow/failing SMTP // or push provider delay or break the caller (which already does `void // createNotification(...)`). Fire-and-forget from here too. void dispatchAlerts(opts).catch((err) => { console.error("[notifications] failed to dispatch push/email", err); }); } async function dispatchAlerts(opts: CreateNotificationOpts): Promise { const [actor, recipient, recipe] = await Promise.all([ db.query.users.findFirst({ where: eq(users.id, opts.actorId), columns: { name: true, username: true }, }), db.query.users.findFirst({ where: eq(users.id, opts.userId), columns: { email: true, locale: true }, }), opts.recipeId ? db.query.recipes.findFirst({ where: eq(recipes.id, opts.recipeId), columns: { title: true }, }) : Promise.resolve(undefined), ]); if (!actor || !recipient) return; const messages = getMessages(recipient.locale); const n = messages.notifications as Record; const detail = (n["detail"] ?? {}) as Record; const pushTitle = (n["pushTitle"] ?? {}) as Record; const template = detail[opts.type] ?? (n[opts.type] as string | undefined); if (!template) return; const body = formatMessage(template, { name: actor.name, title: recipe?.title ?? "", stars: opts.score != null ? String(opts.score) : "", }); const title = pushTitle[opts.type] ?? "Epicure"; const url = opts.type === "follow" ? (actor.username ? `/u/${actor.username}` : "/") : opts.recipeId ? `/recipes/${opts.recipeId}` : "/"; if (await isNotificationCategoryEnabled(opts.userId, opts.type)) { void sendPushNotification(opts.userId, { title, body, url }).catch((err) => { console.error("[notifications] push failed", err); }); } if (recipient.email && (await isEmailCategoryEnabled(opts.userId, opts.type))) { const baseUrl = process.env["BETTER_AUTH_URL"] ?? "http://localhost:3000"; void sendEmail({ to: recipient.email, subject: title, html: notificationEmailHtml(title, body, `${baseUrl}${url}`), }).catch((err) => { console.error("[notifications] email failed", err); }); } }