diff --git a/app/api/import/route.ts b/app/api/import/route.ts new file mode 100644 index 0000000..67d8d47 --- /dev/null +++ b/app/api/import/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from 'next/server'; +import prisma from '../../../prisma/prisma'; +import { NewsDatabaseSchema } from '../../../utils/schemas'; +import { singleNews, topNews } from '../../../utils/urls'; + +export async function GET(request: Request) { + if ( + request.headers.get('Authorization') !== `Bearer ${process.env.CRON_SECRET}` + ) { + return new Response('Unauthorized', { status: 401 }); + } + + try { + const topstories: number[] = await fetch(topNews).then(res => res.json()); + + const newsPromises = topstories + .splice(0, Number(process.env.NEWS_LIMIT)) + .map(async id => { + const sourceNews = await fetch(singleNews(id)).then(res => res.json()); + const validation = NewsDatabaseSchema.safeParse(sourceNews); + + if (validation.success) { + const result = await prisma.news.upsert({ + create: { + ...validation.data, + id + }, + update: { + ...validation.data + }, + where: { + id + } + }); + + return result; + } + }); + + await Promise.all(newsPromises); + + return new NextResponse(`Imported ${newsPromises.length} news.`, { + status: 200 + }); + } catch { + return new NextResponse(`Import failed.`, { + status: 500 + }); + } +} diff --git a/app/api/cron/route.ts b/app/api/mailing/route.ts similarity index 60% rename from app/api/cron/route.ts rename to app/api/mailing/route.ts index 79ec22f..15b0a28 100644 --- a/app/api/cron/route.ts +++ b/app/api/mailing/route.ts @@ -2,9 +2,8 @@ import { NextResponse } from 'next/server'; import { z } from 'zod'; import NewsletterTemplate from '../../../components/emails/newsletter'; import prisma from '../../../prisma/prisma'; -import { NewsDatabaseSchema, NewsSchema } from '../../../utils/schemas'; +import { NewsSchema } from '../../../utils/schemas'; import { sender } from '../../../utils/sender'; -import { singleNews, topNews } from '../../../utils/urls'; export async function GET(request: Request) { if ( @@ -13,40 +12,19 @@ export async function GET(request: Request) { return new Response('Unauthorized', { status: 401 }); } - const topstories: number[] = await fetch(topNews).then(res => res.json()); - - const newsPromises = topstories - .splice(0, Number(process.env.NEWS_LIMIT)) - .map(async id => { - const sourceNews = await fetch(singleNews(id)).then(res => res.json()); - const validation = NewsDatabaseSchema.safeParse(sourceNews); - - if (validation.success) { - const result = await prisma.news.upsert({ - create: { - ...validation.data, - id - }, - update: { - ...validation.data - }, - where: { - id - } - }); - - return result; - } - }); - - await Promise.all(newsPromises); - + // send newsletter to users who didn't get it in the last 23h 50m, assuming a cron job every 10 minutes + // this is to avoid sending the newsletter to the same users multiple times + // this is not a perfect solution, but it's good enough for now const users = await prisma.user.findMany({ where: { confirmed: true, - deleted: false + deleted: false, + lastMail: { + lt: new Date(Date.now() - 1000 * 60 * 60 * 24 + 1000 * 10 * 60) // 24h - 10m + } }, select: { + id: true, email: true } }); @@ -80,6 +58,18 @@ export async function GET(request: Request) { }); } + // update users so they don't get the newsletter again + await prisma.user.updateMany({ + where: { + id: { + in: users.map(user => user.id) + } + }, + data: { + lastMail: new Date() + } + }); + return new NextResponse(`Newsletter sent to ${users.length} addresses.`, { status: 200 }); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4931387..3fa8a51 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -15,6 +15,7 @@ model User { confirmed Boolean @default(false) deleted Boolean @default(false) createdAt DateTime @default(now()) @map(name: "created_at") + lastMail DateTime @default(now()) @updatedAt @map(name: "updated_at") @@map(name: "users") } diff --git a/utils/apiResponse.ts b/utils/apiResponse.ts index 6594516..f1893e0 100644 --- a/utils/apiResponse.ts +++ b/utils/apiResponse.ts @@ -1,5 +1,7 @@ +import { NextResponse } from 'next/server'; + export function ApiResponse(status: number, message: string) { - const response = new Response(message, { status }); + const response = new NextResponse(message, { status }); response.headers.set('Access-Control-Allow-Origin', process.env.HOME_URL!); return response; diff --git a/utils/sender.ts b/utils/sender.ts index 53e99c6..5238a0c 100644 --- a/utils/sender.ts +++ b/utils/sender.ts @@ -5,10 +5,14 @@ type EmailTemplate = { template: JSX.Element; }; -async function sendEmail(to: string[], { subject, template }: EmailTemplate) { +export async function sender( + to: string[], + { subject, template }: EmailTemplate +) { const resend = new Resend(process.env.RESEND_KEY); try { + // TODO: adjust code once Resend supports batch sending const { error } = await resend.emails.send({ from: process.env.RESEND_FROM!, to, @@ -34,22 +38,3 @@ async function sendEmail(to: string[], { subject, template }: EmailTemplate) { return true; } - -export async function sender( - to: string[], - { subject, template }: EmailTemplate -) { - let success = false; - let i = 5; - - while (i < 5) { - const sent = await sendEmail(to, { subject, template }); - if (sent) { - success = true; - break; - } - i++; - } - - return success; -} diff --git a/vercel.json b/vercel.json index 01288e2..6789338 100644 --- a/vercel.json +++ b/vercel.json @@ -1,7 +1,11 @@ { "crons": [ { - "path": "/api/cron", + "path": "/api/import", + "schedule": "0 3 * * *" + }, + { + "path": "/api/mailing", "schedule": "0 5 * * *" } ],