export async function sendNotification(webhookUrl: string | null, message: string, imageUrl?: string) { if (!webhookUrl) return; try { const payload: any = { content: message, text: message } // For Discord: use embed if image exists if (imageUrl && webhookUrl.includes("discord")) { payload.embeds = [{ image: { url: imageUrl } }] } // For generic webhooks, just append URL to text if not Discord, or leave as is? // Let's create a simpler payload if image exists but not discord? // Actually, if we just send JSON, most won't render it unless specific format. // Let's just append the image URL to the message if it's not a Discord webhook, so it's clickable. if (imageUrl && !webhookUrl.includes("discord")) { payload.content += `\n${imageUrl}`; payload.text += `\n${imageUrl}`; } const response = await fetch(webhookUrl, { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": "CatSittingPlanner/1.0" }, body: JSON.stringify(payload), }); if (!response.ok) { console.error(`[Notification] Webhook failed with status ${response.status}`); } } catch (error) { console.error("Failed to send notification:", error); } } import { sendPushNotification } from "./push"; import prisma from "@/lib/prisma"; export async function sendPlanNotification(planId: string, message: string, webhookUrl?: string | null, imageUrl?: string) { // Parallelize sending const promises: Promise[] = []; if (webhookUrl) { promises.push(sendNotification(webhookUrl, message, imageUrl)); } try { const subscriptions = await prisma.pushSubscription.findMany({ where: { planId } }); if (subscriptions.length > 0) { console.log(`[Push] Found ${subscriptions.length} subscriptions for plan ${planId}`); const payload: any = { title: "Cat Sitting Planner", body: message, url: `/`, image: imageUrl }; // Refine URL in payload // We need 'lang'. We don't have it here easily unless passed. // But we can guess or just link to root and let redirect handle it? // Or just link to context. // Let's rely on SW opening `/`. subscriptions.forEach(sub => { promises.push((async () => { try { console.log(`[Push] Sending to endpoint ${sub.endpoint.slice(0, 20)}...`); const res = await sendPushNotification(sub, payload); console.log(`[Push] Result for ${sub.endpoint.slice(0, 20)}...:`, res); if (!res.success) { console.error(`[Push] Failed for endpoint: ${res.statusCode}`); if (res.statusCode === 410 || res.statusCode === 404) { console.log(`[Push] Deleting stale subscription`); await prisma.pushSubscription.delete({ where: { id: sub.id } }); } } } catch (err) { console.error(`[Push] Error inside loop:`, err); } })()); }); } else { console.log(`[Push] No subscriptions found for plan ${planId}`); } } catch (e) { console.error("Failed to fetch/send push subscriptions", e); } await Promise.allSettled(promises); }