Skip to content

Commit

Permalink
feat: Implement cron job for comment and reply email notification
Browse files Browse the repository at this point in the history
GitOrigin-RevId: a378140f813856bd01c03bf8786fc26c785cba44
  • Loading branch information
abbas-nazar authored and actions-user committed Dec 12, 2024
1 parent 2cc17f7 commit 3ebfc6b
Show file tree
Hide file tree
Showing 5 changed files with 526 additions and 44 deletions.
6 changes: 6 additions & 0 deletions platform/wab/src/wab/server/AppServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ import {
getWorkspaces,
updateWorkspace,
} from "@/wab/server/routes/workspaces";
import { sendCommentsNotificationEmails } from "@/wab/server/scripts/send-comments-notifications";
import { logError } from "@/wab/server/server-util";
import { ASYNC_TIMING } from "@/wab/server/timing-util";
import { TypeormStore } from "@/wab/server/util/TypeormSessionStore";
Expand Down Expand Up @@ -2119,6 +2120,11 @@ export async function createApp(
pruneCache();
});

// runs every 10 minutes
cron.schedule("*/10 * * * *", async () => {
await sendCommentsNotificationEmails(config);
});

// Don't leak infra info
app.disable("x-powered-by");

Expand Down
35 changes: 35 additions & 0 deletions platform/wab/src/wab/server/db/DbMgr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9584,6 +9584,41 @@ export class DbMgr implements MigrationDbMgr {
});
}

async getCommentsForThread(threadId: CommentThreadId): Promise<Comment[]> {
return await this.comments().find({
where: {
threadId,
...excludeDeleted(),
},
order: {
createdAt: "ASC", // Sort by createdAt in ascending order
},
relations: ["createdBy"],
});
}

async getUnnotifiedComments(): Promise<Comment[]> {
this.checkSuperUser();
return await this.comments().find({
where: {
isEmailNotificationSent: false,
...excludeDeleted(),
},
order: {
createdAt: "ASC", // Sort by createdAt in ascending order
},
relations: ["createdBy"],
});
}

async markCommentsAsNotified(commentIds: string[]): Promise<void> {
this.checkSuperUser();
await this.comments().update(
{ id: In(commentIds) }, // Match comments by their IDs
{ isEmailNotificationSent: true } // Set the notification status to true
);
}

async postCommentInProject(
{ projectId, branchId }: ProjectAndBranchId,
data: { location: CommentLocation; body: string; threadId: string }
Expand Down
138 changes: 115 additions & 23 deletions platform/wab/src/wab/server/emails/comment-notification-email.spec.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,127 @@
import { sendCommentNotificationEmail } from "@/wab/server/emails/comment-notification-email";
import { sendUserNotificationEmail } from "@/wab/server/emails/comment-notification-email";
import { setupEmailTest } from "@/wab/server/emails/test/email-test-util";
import { Project, User } from "@/wab/server/entities/Entities";
import { createProjectUrl } from "@/wab/shared/urls";

describe("sendCommentNotificationEmail", () => {
it("sends an email", async () => {
// Utility function to normalize HTML by removing extra whitespace
const normalizeHtml = (html) => html.replace(/\s+/g, " ");

describe("sendUserNotificationEmail", () => {
it("sends an email with notifications grouped by project", async () => {
const { req, config, mailer } = setupEmailTest();
await sendCommentNotificationEmail(
req,
{
name: "My Project",
id: "proj-id",
} as Project,
{
email: "[email protected]",
firstName: "Author",
lastName: "Person",
} as User,
"[email protected]",
"This is a comment"

// Mock input
const notifications = new Map([
[
"proj-1",
{
projectName: "Project Alpha",
threads: new Map([
[
"thread1",
[
{
author: "John Doe",
body: "What's this supposed to mean?",
},
{
author: "Zoro",
body: "This is a navigation system that I have developed",
},
],
],
[
"thread2",
[
{
author: "John Doe",
body: "When can we expect to deliver this?",
},
{
author: "Zoro",
body: "In a week may be",
},
],
],
]),
},
],
[
"proj-2",
{
projectName: "Project Beta",
threads: new Map([
[
"thread1",
[
{
author: "John Doe",
body: "Comment",
},
{
author: "Sanji",
body: "I can reply",
},
{
author: "Nami",
body: "I can aswell",
},
],
],
[
"thread2",
[
{
author: "John Doe",
body: "this is a comment",
},
{
author: "Nami",
body: "this is a reply",
},
],
],
]),
},
],
]);

const expectedEmailBody = normalizeHtml(
`<p>
You have new activity in your projects:</p> <div><h2>New comments in project: <a href="${createProjectUrl(
req.config.host,
"proj-1"
)}">${
notifications.get("proj-1")?.projectName
}</a></h2><hr><p>What's this supposed to mean? by <strong>John Doe</strong></p><ul> <li><p>This is a navigation system that I have developed by <strong>Zoro</strong></p></li> </ul><hr><p>When can we expect to deliver this? by <strong>John Doe</strong></p><ul> <li><p>In a week may be by <strong>Zoro</strong></p></li> </ul></div><div><h2>New comments in project: <a href="${createProjectUrl(
req.config.host,
"proj-2"
)}">${
notifications.get("proj-2")?.projectName
}</a></h2><hr><p>Comment by <strong>John Doe</strong></p><ul> <li><p>I can reply by <strong>Sanji</strong></p></li> <li><p>I can aswell by <strong>Nami</strong></p></li> </ul><hr><p>this is a comment by <strong>John Doe</strong></p><ul> <li><p>this is a reply by <strong>Nami</strong></p></li> </ul></div> <p>If you wish to modify your notification settings, please visit the appropriate section in Plasmic Studio.</p>`
);

await sendUserNotificationEmail(
mailer,
"[email protected]", // User's email
notifications,
req.config.host,
config.mailFrom,
req.config.mailBcc // Optional BCC
);

// Get the actual email body sent
const receivedHtml = normalizeHtml(mailer.sendMail.mock.calls[0][0].html);

// Assert that the normalized HTML matches
expect(receivedHtml).toBe(expectedEmailBody);

// Assert other email properties
expect(mailer.sendMail).toHaveBeenCalledWith({
from: config.mailFrom,
to: "[email protected]",
bcc: req.config.mailBcc,
subject: `New comments from Author Person on My Project`,
html: `<p><strong>Author Person</strong> replied to a comment on <strong>My Project</strong>:</p>
<pre style="font: inherit;">This is a comment</pre>
<p><a href="https://studio.plasmic.app/projects/proj-id">Open project in Plasmic Studio</a> to reply or change notification settings</p>`,
subject: "New Activity in Your Projects",
html: expect.any(String), // Already tested above
});
});
});
82 changes: 61 additions & 21 deletions platform/wab/src/wab/server/emails/comment-notification-email.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,71 @@
import { Project, User } from "@/wab/server/entities/Entities";
import { fullName } from "@/wab/shared/ApiSchemaUtil";
import { Mailer } from "@/wab/server/emails/Mailer";
import {
ProjectThreads,
UserComment,
} from "@/wab/server/scripts/send-comments-notifications";
import { createProjectUrl } from "@/wab/shared/urls";
import { Request } from "express-serve-static-core";

export async function sendCommentNotificationEmail(
req: Request,
project: Project,
author: User,
function getComment(comment: UserComment) {
return `<p>${comment.body} ${
comment.author ? `by <strong>${comment.author}</strong>` : ""
}</p>`;
}

/**
* Sends a user notification email with detailed project, thread, and comment breakdowns.
*/
export async function sendUserNotificationEmail(
mailer: Mailer,
email: string,
commentBody: string
projects: Map<string, ProjectThreads>,
host: string,
mailFrom: string,
mailBcc?: string
) {
const commentNotificationBody = `<p><strong>${fullName(
author
)}</strong> replied to a comment on <strong>${project.name}</strong>:</p>
let commentsBody = ``;

// Process each project in the Map
for (const [projectId, { projectName, threads }] of projects) {
const projectUrl = createProjectUrl(host, projectId);

commentsBody += `<div><h2>New comments in project: <a href="${projectUrl}">${projectName}</a></h2>`;

// Process each thread in the project (threads is a Map)
for (const [threadId, comments] of threads) {
if (comments.length === 0) {
return;
} // Skip empty threads

commentsBody += `<hr>${getComment(comments[0])}`;

if (comments.length > 1) {
commentsBody += `<ul>`;

// Add remaining comments
comments.slice(1).forEach((comment) => {
commentsBody += `
<li>${getComment(comment)}</li>
`;
});

commentsBody += `</ul>`;
}
}

<pre style="font: inherit;">${commentBody}</pre>
commentsBody += `</div>`;
}

<p><a href="${createProjectUrl(
req.config.host,
project.id
)}">Open project in Plasmic Studio</a> to reply or change notification settings</p>`;
const emailBody = `<p>
You have new activity in your projects:</p>
${commentsBody}
<p>If you wish to modify your notification settings, please visit the appropriate section in Plasmic Studio.</p>`;

await req.mailer.sendMail({
from: req.config.mailFrom,
// Send the email
await mailer.sendMail({
from: mailFrom,
to: email,
bcc: req.config.mailBcc,
subject: `New comments from ${fullName(author)} on ${project.name}`,
html: commentNotificationBody,
bcc: mailBcc, // Optional BCC
subject: "New Activity in Your Projects",
html: emailBody,
});
}
Loading

0 comments on commit 3ebfc6b

Please sign in to comment.