Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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;
72 changes: 72 additions & 0 deletions server/src/cron/weeklyDigestCron.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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;
console.log(">>> CRON FILE LOADED", new Date());

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

View workflow job for this annotation

GitHub Actions / build (24.x)

Unexpected console statement

// 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.
console.log(">>> CRON TASK TRIGGERED", new Date());

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

View workflow job for this annotation

GitHub Actions / build (24.x)

Unexpected console statement
try {
const digests = await generateWeeklyDigest();
const maxEmailsPerRun = 10;
let sentCount = 0;

console.log(

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

View workflow job for this annotation

GitHub Actions / build (24.x)

Unexpected console statement
"IS_TEST_MODE:",
IS_TEST_MODE,
"typeof:",
typeof IS_TEST_MODE,
"TEST_EMAIL:",
TEST_EMAIL,
);

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,
});
console.log("Test digest sent to", TEST_EMAIL);

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

View workflow job for this annotation

GitHub Actions / build (24.x)

Unexpected console statement
}
} else {
// PROD MODE
for (const digest of digests) {
if (sentCount >= maxEmailsPerRun) {
console.log("Antiflood: Email send limit reached for this run.");

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

View workflow job for this annotation

GitHub Actions / build (24.x)

Unexpected console statement
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);
}
console.log("Weekly digest sent to all users!");

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

View workflow job for this annotation

GitHub Actions / build (24.x)

Unexpected console statement
}
} catch (e) {
console.error("Weekly digest cron error:", e);

Check warning on line 70 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>
`;
}
40 changes: 40 additions & 0 deletions server/src/util/sendWeeklyEmail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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,
},
});

transporter.verify(function (error) {
if (error) {
console.log("SMTP Error:", error);

Check warning on line 17 in server/src/util/sendWeeklyEmail.js

View workflow job for this annotation

GitHub Actions / build (24.x)

Unexpected console statement
} else {
console.log("SMTP Server is ready to take messages");

Check warning on line 19 in server/src/util/sendWeeklyEmail.js

View workflow job for this annotation

GitHub Actions / build (24.x)

Unexpected console statement
}
});

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 };