import { serve } from "https://deno.land/std@0.192.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; declare const Deno: { env: { get(key: string): string | undefined; }; }; const RESEND_API_KEY = Deno.env.get("RESEND_API_KEY") ?? ""; const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? ""; const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""; function buildDigestHtml(params: { userName: string; userEmail: string; replies: Array<{ thread_title: string; reply_body: string; link: string; created_at: string }>; upvotes: Array<{ target_type: string; title: string; link: string; count: number }>; mentions: Array<{ thread_title: string; actor_name: string; link: string; created_at: string }>; weekLabel: string; }): string { const { userName, replies, upvotes, mentions, weekLabel } = params; const hasContent = replies.length > 0 || upvotes.length > 0 || mentions.length > 0; const repliesHtml = replies.length > 0 ? ` ${replies.map(r => ` `).join("")}
Thread Replies
${r.thread_title}

${r.reply_body.length > 120 ? r.reply_body.substring(0, 120) + "…" : r.reply_body}

${new Date(r.created_at).toLocaleDateString("en-US", { month: "short", day: "numeric" })}
` : ""; const upvotesHtml = upvotes.length > 0 ? ` ${upvotes.map(u => ` `).join("")}
Upvotes Received
${u.title}

${u.target_type}

+${u.count}

upvotes

` : ""; const mentionsHtml = mentions.length > 0 ? ` ${mentions.map(m => ` `).join("")}
Mentions
${m.thread_title}

${m.actor_name} mentioned you

${new Date(m.created_at).toLocaleDateString("en-US", { month: "short", day: "numeric" })}
` : ""; const emptyHtml = !hasContent ? `

No activity this week. Head to the forum to start a discussion!

` : ""; return ` Your Weekly BroadcastBeat Digest
${hasContent ? ` ` : ""}
BROADCASTBEAT Weekly Digest

Your Week in Review

${weekLabel}

Hi ${userName}, here's a summary of your forum activity this week.

${replies.length}

Replies

${upvotes.reduce((s, u) => s + u.count, 0)}

Upvotes

${mentions.length}

Mentions

${repliesHtml} ${upvotesHtml} ${mentionsHtml} ${emptyHtml}
Visit the Forum

You're receiving this because you opted in to the weekly digest.

Manage email preferences · BroadcastBeat
`; } serve(async (req) => { if (req.method === "OPTIONS") { return new Response("ok", { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Headers": "*", }, }); } try { const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { db: { schema: 'bb' }, }); // Parse optional target user_id from body (for single-user send) let targetUserId: string | null = null; try { const body = await req.json(); targetUserId = body?.user_id ?? null; } catch (_) { /* no body */ } // Fetch opted-in users let profileQuery = supabase .from("user_profiles") .select("id, email, full_name") .eq("weekly_digest_enabled", true); if (targetUserId) { profileQuery = profileQuery.eq("id", targetUserId); } const { data: users, error: usersError } = await profileQuery; if (usersError) throw usersError; if (!users || users.length === 0) { return new Response(JSON.stringify({ sent: 0, message: "No opted-in users" }), { headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }, }); } // Week range: last 7 days const now = new Date(); const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); const weekLabel = `${weekAgo.toLocaleDateString("en-US", { month: "long", day: "numeric" })} – ${now.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}`; let sent = 0; const errors: string[] = []; for (const user of users) { try { // 1. Replies: threads where user replied this week const { data: repliesRaw } = await supabase .from("forum_replies") .select("id, body, thread_id, created_at, forum_threads(title)") .eq("author_id", user.id) .eq("is_ai_response", false) .gte("created_at", weekAgo.toISOString()) .order("created_at", { ascending: false }) .limit(5); const replies = (repliesRaw ?? []).map((r: any) => ({ thread_title: r.forum_threads?.title ?? "Thread", reply_body: r.body ?? "", link: `https://broadcastb5322.builtwithrocket.new/forum/thread/${r.thread_id}`, created_at: r.created_at, })); // 2. Upvotes received this week on user's threads/replies const { data: threadUpvotes } = await supabase .from("forum_votes") .select("target_id, target_type, forum_threads!inner(title, author_id)") .eq("vote_value", 1) .eq("target_type", "thread") .eq("forum_threads.author_id", user.id) .gte("created_at", weekAgo.toISOString()); const { data: replyUpvotes } = await supabase .from("forum_votes") .select("target_id, target_type, forum_replies!inner(author_id, thread_id, forum_threads(title))") .eq("vote_value", 1) .eq("target_type", "reply") .eq("forum_replies.author_id", user.id) .gte("created_at", weekAgo.toISOString()); // Aggregate upvotes by target const upvoteMap = new Map(); for (const v of (threadUpvotes ?? [])) { const key = v.target_id; const title = (v as any).forum_threads?.title ?? "Thread"; if (!upvoteMap.has(key)) { upvoteMap.set(key, { target_type: "thread", title, link: `https://broadcastb5322.builtwithrocket.new/forum/thread/${v.target_id}`, count: 0 }); } upvoteMap.get(key)!.count++; } for (const v of (replyUpvotes ?? [])) { const key = v.target_id; const threadId = (v as any).forum_replies?.thread_id; const title = (v as any).forum_replies?.forum_threads?.title ?? "Reply"; if (!upvoteMap.has(key)) { upvoteMap.set(key, { target_type: "reply", title, link: `https://broadcastb5322.builtwithrocket.new/forum/thread/${threadId}`, count: 0 }); } upvoteMap.get(key)!.count++; } const upvotes = Array.from(upvoteMap.values()).sort((a, b) => b.count - a.count).slice(0, 5); // 3. Mentions from notifications this week const { data: mentionsRaw } = await supabase .from("notifications") .select("title, body, link, actor_name, created_at") .eq("user_id", user.id) .eq("type", "mention") .gte("created_at", weekAgo.toISOString()) .order("created_at", { ascending: false }) .limit(5); const mentions = (mentionsRaw ?? []).map((m: any) => ({ thread_title: m.body ?? m.title ?? "Thread", actor_name: m.actor_name ?? "Someone", link: m.link ?? "https://broadcastb5322.builtwithrocket.new/forum", created_at: m.created_at, })); // Build email HTML const html = buildDigestHtml({ userName: user.full_name || user.email.split("@")[0], userEmail: user.email, replies, upvotes, mentions, weekLabel, }); // Send via Resend const resendRes = await fetch("https://api.resend.com/emails", { method: "POST", headers: { "Authorization": `Bearer ${RESEND_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ from: "onboarding@resend.dev", to: [user.email], subject: `Your BroadcastBeat Weekly Digest — ${weekLabel}`, html, }), }); if (!resendRes.ok) { const errText = await resendRes.text(); errors.push(`${user.email}: ${errText}`); } else { sent++; } } catch (userErr: any) { errors.push(`${user.email}: ${userErr.message}`); } } return new Response(JSON.stringify({ sent, errors: errors.length > 0 ? errors : undefined }), { headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }, }); } catch (error: any) { return new Response(JSON.stringify({ error: error.message }), { status: 500, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }, }); } });