From 5b503400783efa1a9e6216d15ec4e35113634047 Mon Sep 17 00:00:00 2001 From: The Doom Lab Date: Mon, 2 Dec 2024 21:17:29 -0500 Subject: [PATCH 1/2] forgot to add the emails --- integrations/emails.tsx | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/integrations/emails.tsx b/integrations/emails.tsx index 3b38ba8a..9152306b 100644 --- a/integrations/emails.tsx +++ b/integrations/emails.tsx @@ -264,3 +264,33 @@ staple.helpdesk@gmail.com `, } } + +export function createDailyNotification(email, notificationContent) { + const html_message = ` + + +
+ STAPLE Logo +
+ +

STAPLE Daily Notifications

+ +

+ This email is to notify you about recent updates to your project. + Here are new announcements, tasks, and other project updates: +

+ + ${notificationContent} + + + ` + + return { + from: "STAPLE ", + to: email, + subject: "STAPLE Daily Notifications", + replyTo: "STAPLE Help ", + html: html_message, + } +} From bfdd0a9e06a527e27099b329b971f410efd7ff5a Mon Sep 17 00:00:00 2001 From: The Doom Lab Date: Tue, 3 Dec 2024 15:00:44 -0500 Subject: [PATCH 2/2] emailer is now working --- integrations/emails.tsx | 30 ------ mailers/cronJob.js | 21 ---- mailers/cronJob.mjs | 169 ++++++++++++++++++++++++++++++++ mailers/notificationsHandler.js | 59 ----------- package-lock.json | 36 ++++++- package.json | 2 + 6 files changed, 204 insertions(+), 113 deletions(-) delete mode 100644 mailers/cronJob.js create mode 100644 mailers/cronJob.mjs delete mode 100644 mailers/notificationsHandler.js diff --git a/integrations/emails.tsx b/integrations/emails.tsx index 9152306b..3b38ba8a 100644 --- a/integrations/emails.tsx +++ b/integrations/emails.tsx @@ -264,33 +264,3 @@ staple.helpdesk@gmail.com `, } } - -export function createDailyNotification(email, notificationContent) { - const html_message = ` - - -
- STAPLE Logo -
- -

STAPLE Daily Notifications

- -

- This email is to notify you about recent updates to your project. - Here are new announcements, tasks, and other project updates: -

- - ${notificationContent} - - - ` - - return { - from: "STAPLE ", - to: email, - subject: "STAPLE Daily Notifications", - replyTo: "STAPLE Help ", - html: html_message, - } -} diff --git a/mailers/cronJob.js b/mailers/cronJob.js deleted file mode 100644 index af14acd4..00000000 --- a/mailers/cronJob.js +++ /dev/null @@ -1,21 +0,0 @@ -import cron from "node-cron" -import { fetchAndGroupNotifications, sendGroupedNotifications } from "notificationsHandler" - -async function sendDailyNotifications() { - try { - const groupedNotifications = await fetchAndGroupNotifications() - await sendGroupedNotifications(groupedNotifications) - } catch (error) { - console.error("Error in sendDailyNotifications:", error) - } -} - -cron.schedule("0 0 * * *", async () => { - console.log(`[${new Date().toISOString()}] Running daily notifications job...`) - try { - await sendDailyNotifications() - console.log(`[${new Date().toISOString()}] Daily notifications job completed successfully.`) - } catch (error) { - console.error(`[${new Date().toISOString()}] Error in daily notifications job:`, error) - } -}) diff --git a/mailers/cronJob.mjs b/mailers/cronJob.mjs new file mode 100644 index 00000000..dad352eb --- /dev/null +++ b/mailers/cronJob.mjs @@ -0,0 +1,169 @@ +import dotenv from "dotenv" +dotenv.config({ path: "../.env.local" }) +import moment from "moment" +import { PrismaClient } from "@prisma/client" +import fetch from "node-fetch" +import { resolver } from "@blitzjs/rpc" + +const db = new PrismaClient() // Create Prisma client instance + +// Helper function to create email content +function createDailyNotification(email, notificationContent) { + const html_message = ` + + +
+ STAPLE Logo +
+ +

STAPLE Daily Notifications

+ +

+ This email is to notify you about recent updates to your project. + Here are new announcements, tasks, and other project updates: +

+ + ${notificationContent} + + + ` + + return { + from: "STAPLE ", + to: email, + subject: "STAPLE Daily Notifications", + replyTo: "STAPLE Help ", + html: html_message, + } +} + +// Function to fetch notifications from the database +const getNotifications = resolver.pipe( + async ({ where, include }) => + await db.notification.findMany({ + where, // Pass `where` directly here + include, + orderBy: { createdAt: "desc" }, // Sort by creation date if needed + }) +) + +// Function to fetch and group notifications by email and project +export async function fetchAndGroupNotifications() { + const last24Hours = moment().subtract(24, "hours").toDate() + + const notifications = await getNotifications({ + where: { + createdAt: { + gte: last24Hours, + }, + }, + include: { + recipients: { select: { email: true } }, + project: { select: { name: true } }, + }, + }) + + return notifications.reduce((acc, notification) => { + // Ensure recipients is defined as an array + const recipients = notification.recipients || [] + + recipients.forEach(({ email }) => { + if (!acc[email]) acc[email] = {} + const projectName = notification.project?.name || "No Project" + if (!acc[email][projectName]) acc[email][projectName] = [] + acc[email][projectName].push(notification.message) + }) + + return acc + }, {}) +} + +// Function to introduce a delay (in milliseconds) +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) + +const maxEmailsPerMinute = 100 // Max number of emails to send per minute +let emailCount = 0 // Keep track of the number of emails sent +let startTime = Date.now() // Track the time when email sending starts + +// Function to check if the rate limit is exceeded +const checkRateLimit = async () => { + // Check if we have sent maxEmailsPerMinute emails + if (emailCount >= maxEmailsPerMinute) { + const elapsedTime = Date.now() - startTime // Get time elapsed in milliseconds + const timeLeft = 60000 - elapsedTime // Calculate remaining time for the current minute + + if (timeLeft > 0) { + console.log(`Rate limit reached. Waiting for ${timeLeft / 1000}s...`) + // Wait for the remaining time before sending the next batch of emails + await delay(timeLeft) + } else { + // If a minute has passed, reset the counter and start time + emailCount = 0 + startTime = Date.now() + } + } +} +// Function to send grouped notifications +export async function sendGroupedNotifications(groupedNotifications) { + const delayTime = 500 // Delay time between each email in milliseconds (e.g., 1 second) + + for (const [email, projects] of Object.entries(groupedNotifications)) { + const notificationContent = Object.entries(projects) + .map(([projectName, messages]) => { + const projectHeader = `

Project: ${projectName}

` + const messagesList = messages.map((message) => `
  • ${message}
  • `).join("") + return projectHeader + `
      ${messagesList}
    ` + }) + .join("") + + const emailContent = createDailyNotification(email, notificationContent) + + // Check rate limit before sending email + await checkRateLimit() + + // Send email + try { + const response = await fetch("https://app.staple.science/api/send-email", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(emailContent), + }) + + if (!response.ok) { + console.error(`Failed to send email to ${email}:`, response.statusText) + } else { + console.log(`Email sent successfully to ${email}`) + } + + emailCount++ // Increment the email count after sending each email + + // Add delay between emails to avoid too many requests in a short time + await delay(delayTime) + } catch (error) { + console.error(`Error sending email to ${email}:`, error) + } + } +} + +// Function to fetch and send daily notifications +async function sendDailyNotifications() { + try { + const groupedNotifications = await fetchAndGroupNotifications() + await sendGroupedNotifications(groupedNotifications) + } catch (error) { + console.error("Error in sendDailyNotifications:", error) + } +} + +// Run the daily notifications job +sendDailyNotifications() + .then(() => { + console.log(`[${new Date().toISOString()}] Daily notifications job completed successfully.`) + }) + .catch((error) => { + console.error(`[${new Date().toISOString()}] Error in daily notifications job:`, error) + }) + .finally(async () => { + await db.$disconnect() // Disconnect from the database when done + }) diff --git a/mailers/notificationsHandler.js b/mailers/notificationsHandler.js deleted file mode 100644 index 6b5a9b14..00000000 --- a/mailers/notificationsHandler.js +++ /dev/null @@ -1,59 +0,0 @@ -import moment from "moment" -import getNotifications from "../src/notifications/queries/getNotifications" -import { createDailyNotification } from "integrations/emails" - -export async function fetchAndGroupNotifications() { - const last24Hours = moment().subtract(24, "hours").toDate() - - const { notifications } = await getNotifications({ - where: { - createdAt: { - gte: last24Hours, - }, - }, - include: { - recipients: { select: { email: true } }, - project: { select: { name: true } }, - }, - }) - - return notifications.reduce((acc, notification) => { - notification.recipients.forEach(({ email }) => { - if (!acc[email]) acc[email] = {} - const projectName = notification.project?.name || "No Project" - if (!acc[email][projectName]) acc[email][projectName] = [] - acc[email][projectName].push(notification.message) - }) - return acc - }, {}) -} - -export async function sendGroupedNotifications(groupedNotifications) { - for (const [email, projects] of Object.entries(groupedNotifications)) { - const notificationContent = Object.entries(projects) - .map(([projectName, messages]) => { - const projectHeader = `

    Project: ${projectName}

    ` - const messagesList = messages.map((message) => `
  • ${message}
  • `).join("") - return projectHeader + `
      ${messagesList}
    ` - }) - .join("") - - const emailContent = createDailyNotification(email, notificationContent) - - try { - const response = await fetch("/api/send-email", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(emailContent), - }) - - if (!response.ok) { - console.error(`Failed to send email to ${email}:`, response.statusText) - } else { - console.log(`Email sent successfully to ${email}`) - } - } catch (error) { - console.error(`Error sending email to ${email}:`, error) - } - } -} diff --git a/package-lock.json b/package-lock.json index 960f72cb..e4e5f1c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "classnames": "2.3.2", "clsx": "2.1.1", "dompurify": "3.1.0", + "dotenv": "16.4.7", "file-saver": "2.0.5", "final-form": "4.20.9", "focus-trap-react": "10.2.3", @@ -47,6 +48,7 @@ "moment": "2.30.1", "next": "13.4.5", "node-cron": "3.0.3", + "node-fetch": "3.3.2", "nodemailer": "6.9.14", "passport-orcid": "0.0.4", "postcss": "8.4.27", @@ -6890,6 +6892,23 @@ "node": ">=10" } }, + "node_modules/blitz/node_modules/node-fetch": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.3.tgz", + "integrity": "sha512-AXP18u4pidSZ1xYXRDPY/8jdv3RAozIt/WLNR/MBGZAz+xjtlr90RvCnsvHQRiXyWliZF/CpytExp32UU67/SA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/blitz/node_modules/semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", @@ -8537,6 +8556,17 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dotenv-expand": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-8.0.3.tgz", @@ -14343,9 +14373,9 @@ } }, "node_modules/node-fetch": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.3.tgz", - "integrity": "sha512-AXP18u4pidSZ1xYXRDPY/8jdv3RAozIt/WLNR/MBGZAz+xjtlr90RvCnsvHQRiXyWliZF/CpytExp32UU67/SA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", diff --git a/package.json b/package.json index 288d4c2e..3fb3780c 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "classnames": "2.3.2", "clsx": "2.1.1", "dompurify": "3.1.0", + "dotenv": "16.4.7", "file-saver": "2.0.5", "final-form": "4.20.9", "focus-trap-react": "10.2.3", @@ -63,6 +64,7 @@ "moment": "2.30.1", "next": "13.4.5", "node-cron": "3.0.3", + "node-fetch": "3.3.2", "nodemailer": "6.9.14", "passport-orcid": "0.0.4", "postcss": "8.4.27",