diff --git a/.env.example b/.env.example index 99636771a9..a00064207f 100644 --- a/.env.example +++ b/.env.example @@ -32,3 +32,5 @@ EMAIL_SERVER_PORT=587 EMAIL_SERVER_USER='' # Keep in mind that if you have 2FA enabled, you will need to provision an App Password. EMAIL_SERVER_PASSWORD='' +# ApiKey for cronjobs +CRON_API_KEY='0cc0e6c35519bba620c9360cfe3e68d0' diff --git a/lib/emails/EventOrganizerRequestReminderMail.ts b/lib/emails/EventOrganizerRequestReminderMail.ts new file mode 100644 index 0000000000..1f935943d2 --- /dev/null +++ b/lib/emails/EventOrganizerRequestReminderMail.ts @@ -0,0 +1,25 @@ +import dayjs, { Dayjs } from "dayjs"; + +import utc from "dayjs/plugin/utc"; +import timezone from "dayjs/plugin/timezone"; +import toArray from "dayjs/plugin/toArray"; +import localizedFormat from "dayjs/plugin/localizedFormat"; +import EventOrganizerRequestMail from "@lib/emails/EventOrganizerRequestMail"; + +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.extend(toArray); +dayjs.extend(localizedFormat); + +export default class EventOrganizerRequestReminderMail extends EventOrganizerRequestMail { + protected getBodyHeader(): string { + return "An event is still waiting for your approval."; + } + + protected getSubject(): string { + const organizerStart: Dayjs = dayjs(this.calEvent.startTime).tz(this.calEvent.organizer.timeZone); + return `Event request is still waiting: ${this.calEvent.attendees[0].name} - ${organizerStart.format( + "LT dddd, LL" + )} - ${this.calEvent.type}`; + } +} diff --git a/pages/api/cron/bookingReminder.ts b/pages/api/cron/bookingReminder.ts new file mode 100644 index 0000000000..1bf9da47e0 --- /dev/null +++ b/pages/api/cron/bookingReminder.ts @@ -0,0 +1,74 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import prisma from "@lib/prisma"; +import dayjs from "dayjs"; +import { ReminderType } from "@prisma/client"; +import EventOrganizerRequestReminderMail from "@lib/emails/EventOrganizerRequestReminderMail"; +import { CalendarEvent } from "@lib/calendarClient"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const apiKey = req.query.apiKey; + if (process.env.CRON_API_KEY != apiKey) { + return res.status(401).json({ message: "Not authenticated" }); + } + + if (req.method == "POST") { + const reminderIntervalMinutes = [48 * 60, 24 * 60, 3 * 60]; + let notificationsSent = 0; + for (const interval of reminderIntervalMinutes) { + const bookings = await prisma.booking.findMany({ + where: { + confirmed: false, + rejected: false, + createdAt: { + lte: dayjs().add(-interval, "minutes").toDate(), + }, + }, + select: { + title: true, + description: true, + startTime: true, + endTime: true, + attendees: true, + user: true, + id: true, + uid: true, + }, + }); + + const reminders = await prisma.reminderMail.findMany({ + where: { + reminderType: ReminderType.PENDING_BOOKING_CONFIRMATION, + referenceId: { + in: bookings.map((b) => b.id), + }, + elapsedMinutes: { + gte: interval, + }, + }, + }); + + for (const booking of bookings.filter((b) => !reminders.some((r) => r.referenceId == b.id))) { + const evt: CalendarEvent = { + type: booking.title, + title: booking.title, + description: booking.description, + startTime: booking.startTime.toISOString(), + endTime: booking.endTime.toISOString(), + organizer: { email: booking.user.email, name: booking.user.name, timeZone: booking.user.timeZone }, + attendees: booking.attendees, + }; + + await new EventOrganizerRequestReminderMail(evt, booking.uid).sendEmail(); + await prisma.reminderMail.create({ + data: { + referenceId: booking.id, + reminderType: ReminderType.PENDING_BOOKING_CONFIRMATION, + elapsedMinutes: interval, + }, + }); + notificationsSent++; + } + } + res.status(200).json({ notificationsSent }); + } +} diff --git a/prisma/migrations/20210718184017_reminder_mails/migration.sql b/prisma/migrations/20210718184017_reminder_mails/migration.sql new file mode 100644 index 0000000000..7386d848ac --- /dev/null +++ b/prisma/migrations/20210718184017_reminder_mails/migration.sql @@ -0,0 +1,13 @@ +-- CreateEnum +CREATE TYPE "ReminderType" AS ENUM ('PENDING_BOOKING_CONFIRMATION'); + +-- CreateTable +CREATE TABLE "ReminderMail" ( + "id" SERIAL NOT NULL, + "referenceId" INTEGER NOT NULL, + "reminderType" "ReminderType" NOT NULL, + "elapsedMinutes" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + PRIMARY KEY ("id") +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5cc676d1aa..83aed7249a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -176,3 +176,15 @@ model ResetPasswordRequest { email String expires DateTime } + +enum ReminderType { + PENDING_BOOKING_CONFIRMATION +} + +model ReminderMail { + id Int @id @default(autoincrement()) + referenceId Int + reminderType ReminderType + elapsedMinutes Int + createdAt DateTime @default(now()) +}