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
? `
|
Thread Replies
|
${replies.map(r => `
|
${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" })}
|
`).join("")}
|
`
: "";
const upvotesHtml = upvotes.length > 0
? `
|
Upvotes Received
|
${upvotes.map(u => `
|
|
`).join("")}
|
`
: "";
const mentionsHtml = mentions.length > 0
? `
|
Mentions
|
${mentions.map(m => `
|
${m.thread_title}
${m.actor_name} mentioned you
${new Date(m.created_at).toLocaleDateString("en-US", { month: "short", day: "numeric" })}
|
`).join("")}
|
`
: "";
const emptyHtml = !hasContent
? `
|
No activity this week. Head to the forum to start a discussion!
|
`
: "";
return `
Your Weekly BroadcastBeat Digest
|
BROADCASTBEAT
|
Weekly Digest
|
|
Your Week in Review
${weekLabel}
Hi ${userName}, here's a summary of your forum activity this week.
|
${hasContent ? `
|
${replies.length}
Replies
|
${upvotes.reduce((s, u) => s + u.count, 0)}
Upvotes
|
${mentions.length}
Mentions
|
|
` : ""}
${repliesHtml}
${upvotesHtml}
${mentionsHtml}
${emptyHtml}
|
|
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": "*" },
});
}
});