Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"jsonwebtoken": "^9.0.2",
"mongodb-memory-server": "^10.1.0",
"mongoose": "^8.3.2",
"node-cron": "^4.2.1",
"nodemailer": "^7.0.5"
}
}
3 changes: 3 additions & 0 deletions server/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ const config = {
EMAIL_PASSWORD: getEnvVariable("EMAIL_PASSWORD", null),
// Google OAuth configuration
CLIENT_ID: getEnvVariable("CLIENT_ID", null),

IS_TEST_MODE: getEnvVariable("IS_TEST_MODE", "false"),
TEST_EMAIL: getEnvVariable("TEST_EMAIL", null),
};

export default config;
58 changes: 58 additions & 0 deletions server/src/cron/weeklyDigestCron.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import cron from "node-cron";
import { generateWeeklyDigest } from "../services/weeklyDigest.js";
import { makeDigestHtml } from "../util/makeDigestHtml.js";
import { sendWeeklyEmail } from "../util/sendWeeklyEmail.js";
import config from "../config.js";

const { IS_TEST_MODE, TEST_EMAIL } = config;

// Sleep function
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

// cron.schedule("0 10 * * 5", async () => { //every Friday at 10:00 AM (server time).
cron.schedule("*/2 * * * *", async () => {
//will run the task every 2nd minute.
try {
const digests = await generateWeeklyDigest();
const maxEmailsPerRun = 10;
let sentCount = 0;

if (IS_TEST_MODE === "true") {
// TEST MODE
if (digests.length > 0) {
const digest = digests[0];
const html = makeDigestHtml(digest.topPosts);
const subject = "Your Weekly Digest — TEST";

await sendWeeklyEmail({
to: TEST_EMAIL,
subject,
text: "Test: check out the top posts of the week on our website.",
html,
});
}
} else {
// PROD MODE
for (const digest of digests) {
if (sentCount >= maxEmailsPerRun) {
break;
}
const html = makeDigestHtml(digest.topPosts);
const subject = "Your Weekly Digest — Top 5 Posts";

await sendWeeklyEmail({
to: digest.email,
subject,
text: "Check out the top posts of the week on our website.",
html,
});
sentCount++;
await sleep(10000);
}
}
} catch (e) {
console.error("Weekly digest cron error:", e);

Check warning on line 56 in server/src/cron/weeklyDigestCron.js

View workflow job for this annotation

GitHub Actions / build (24.x)

Unexpected console statement
}
});
2 changes: 1 addition & 1 deletion server/src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// index.js
import express from "express";
import app from "./app.js";
import { logInfo, logError } from "./util/logging.js";
Expand All @@ -7,6 +6,7 @@ import testRouter from "./testRouter.js";
import config from "./config.js";
import path from "path";
import { fileURLToPath } from "url";
import "./cron/weeklyDigestCron.js";

const { PORT, NODE_ENV } = config;

Expand Down
76 changes: 76 additions & 0 deletions server/src/services/weeklyDigest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Post from "../models/Post.js";
import User from "../models/User.js";
import { calculatePostScore } from "../util/score.js";

export async function generateWeeklyDigest(limit = 50, skip = 0) {
const users = await User.find().skip(skip).limit(limit);
const oneWeekAgo = new Date();
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);

const recentPosts = await Post.aggregate([
{
$match: {
published_at: { $gte: oneWeekAgo },
tags: { $exists: true, $ne: [] },
},
},
{
$lookup: {
from: "likes",
localField: "_id",
foreignField: "post",
as: "likes",
},
},
{
$addFields: {
likeCount: { $size: "$likes" },
},
},
{
$project: {
_id: 1,
title: 1,
content: 1,
tags: 1,
likeCount: 1,
published_at: 1,
author: 1,
},
},
]);

const digestByUser = [];

for (const user of users) {
const userPosts = recentPosts.filter(
(post) => post.author?.toString() === user._id.toString(),
);
const tagFrequency = {};

userPosts.forEach((post) => {
if (Array.isArray(post.tags)) {
post.tags.forEach((tag) => {
tagFrequency[tag] = (tagFrequency[tag] || 0) + 1;
});
}
});

const scoredPosts = recentPosts.map((post) => ({
title: post.title,
content: post.content,
tags: post.tags,
score: calculatePostScore(post.likeCount, post.tags, tagFrequency),
}));

scoredPosts.sort((a, b) => b.score - a.score);

digestByUser.push({
userId: user._id,
email: user.email,
topPosts: scoredPosts.slice(0, 5),
});
}

return digestByUser;
}
24 changes: 24 additions & 0 deletions server/src/util/makeDigestHtml.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export function makeDigestHtml(posts) {
return `
<div style="background:#fafafa; border-radius:8px; padding:32px 24px; font-family:sans-serif; color:#222; max-width:520px; margin:auto;">
<h2 style="color: #fe4a22; text-align:center; margin-bottom: 16px;">Top 5 posts of the week</h2>
<ol style="padding-left:20px;">
${posts
.map(
(p) => `
<li style="margin-bottom: 18px;">
<strong style="font-size:1.1em;">${p.title}</strong><br>
<span style="color:#444;">${p.content.slice(0, 150)}...</span><br>
<em style="color:#fe4a22;">Tags: ${p.tags.join(", ")}</em>
</li>`,
)
.join("")}
</ol>
<div style="text-align:center; margin:32px 0;">
<a href="https://c52a.hyf.dev/" style="background:#fe4a22; color:white; padding:12px 28px; border-radius:5px; text-decoration:none; font-weight:bold; font-size:16px;">See more on our website</a>
</div>
<hr style="border:none; border-top:1px solid #eee; margin:24px 0;">
<p style="color:#aaa; font-size:12px; text-align:center; margin:0;">This is an automated email, please do not reply.</p>
</div>
`;
}
32 changes: 32 additions & 0 deletions server/src/util/sendWeeklyEmail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import nodemailer from "nodemailer";
import { logError } from "./logging.js";
import config from "../config.js";

const { EMAIL, EMAIL_PASSWORD, EMAIL_PROVIDER } = config;

const transporter = nodemailer.createTransport({
service: EMAIL_PROVIDER,
auth: {
user: EMAIL,
pass: EMAIL_PASSWORD,
},
});

async function sendWeeklyEmail({ to, subject, text, html }) {
const mailOptions = {
from: `'MySite' <${EMAIL}>`,
to,
subject,
text,
html,
};

try {
return await transporter.sendMail(mailOptions);
} catch (error) {
logError(`Error sending weekly email to ${to}:`, error);
throw error;
}
}

export { sendWeeklyEmail };