Skip to content

Commit

Permalink
[recnet-api] Update slack message template (#359)
Browse files Browse the repository at this point in the history
## Description

<!--- Describe your changes in detail -->
Beautify slack weekly digest template

## Related Issue

<!--- This project only accepts pull requests related to open issues -->
<!--- If suggesting a new feature or change, please discuss it in an
issue first -->
<!--- If fixing a bug, there should be an issue describing it with steps
to reproduce -->
<!--- Please link to the issue here: -->

- #261 
- #64 

## Notes

<!-- Other thing to say -->
Misc: I think we should add email and slack testing API endpoints back
to ease the development, but I am not sure where to add it, maybe create
`subscription.controller.ts` again? @joannechen1223

## Test

<!--- Please describe in detail how you tested your changes locally. -->

You will need to add back the `POST /subsriptions/slack/test` endpoint
to test. I will provide example code below in the comment to show how I
use mock data to test it.

## Screenshots (if appropriate):

<!--- Add screenshots of your changes here -->
The data in the screenshot were generated by `generateMock()`.

![Screenshot 2024-11-16 at 11 43
14 PM](https://github.com/user-attachments/assets/0077fe9e-03bf-462f-98e0-e1126c35d462)

## TODO

- [ ] Clear `console.log` or `console.error` for debug usage
- [ ] Update the documentation `recnet-docs` if needed
  • Loading branch information
swh00tw authored Nov 18, 2024
2 parents 08d6bb1 + 7fa7314 commit ac328f3
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 16 deletions.
8 changes: 6 additions & 2 deletions apps/recnet-api/src/modules/slack/slack.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@ export class SlackService {
): Promise<SendSlackResult> {
let result;
try {
const slackMessage = weeklyDigestSlackTemplate(
const weeklyDigest = weeklyDigestSlackTemplate(
cutoff,
content,
this.appConfig.nodeEnv
);
result = await this.transporter.sendDirectMessage(user, slackMessage);
result = await this.transporter.sendDirectMessage(
user,
weeklyDigest.messageBlocks,
weeklyDigest.notificationText
);
} catch (e) {
return { success: false, userId: user.id };
}
Expand Down
4 changes: 4 additions & 0 deletions apps/recnet-api/src/modules/slack/slack.type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { SlackBlockDto } from "slack-block-builder";

export type SendSlackResult = {
success: boolean;
skip?: boolean;
userId?: string;
};

export type SlackMessageBlocks = Readonly<SlackBlockDto>[];
Original file line number Diff line number Diff line change
@@ -1,19 +1,79 @@
import groupBy from "lodash.groupby";
import { BlockCollection, Md, Blocks, BlockBuilder } from "slack-block-builder";

import { WeeklyDigestContent } from "@recnet-api/modules/subscription/subscription.type";

import { formatDate } from "@recnet/recnet-date-fns";

import type { SlackMessageBlocks } from "../slack.type";

export type WeeklyDigestDto = {
notificationText?: string;
messageBlocks: SlackMessageBlocks;
};

export const weeklyDigestSlackTemplate = (
cutoff: Date,
content: WeeklyDigestContent,
nodeEnv: string
): string => {
const subject = `${nodeEnv !== "production" && "[DEV] "}📬 Your Weekly Digest for ${formatDate(cutoff)}`;
const unusedInviteCodes = `You have ${content.numUnusedInviteCodes} unused invite codes! Share the love ❤️`;
const latestAnnouncement = content.latestAnnouncement
? `📢 ${content.latestAnnouncement.title} \n ${content.latestAnnouncement.content}`
: "";
const recsUrls = content.recs.map(
(rec) => `[${rec.article.title}](https://recnet.io/rec/${rec.id})`
): WeeklyDigestDto => {
const { recs, numUnusedInviteCodes, latestAnnouncement } = content;

const recsGroupByTitle = groupBy(recs, (rec) => {
const titleLowercase = rec.article.title.toLowerCase();
const words = titleLowercase.split(" ").filter((w) => w.length > 0);
return words.join("");
});
const recSection = Object.values(recsGroupByTitle).map((recs) => {
const article = recs[0].article;
return [
Blocks.Section({
text: `${Md.bold(Md.link(article.link, article.title))}\n${Md.italic(article.author)} - ${article.year}`,
}),
...recs.map((rec) =>
Blocks.Section({
text: `${Md.link(`https://recnet.io/${rec.user.handle}`, rec.user.displayName)}${rec.isSelfRec ? Md.italic("(Self-Rec)") : ""}: ${rec.description} (${Md.link(`https://recnet.io/rec/${rec.id}`, "view")})`,
})
),
Blocks.Divider(),
];
});

const footer: BlockBuilder[] = [];
if (numUnusedInviteCodes > 0) {
footer.push(
Blocks.Section({
text: `❤️ You have ${Md.bold(`${numUnusedInviteCodes}`)} unused invite codes. Share the love!`,
})
);
}
if (latestAnnouncement) {
footer.push(
Blocks.Section({
text: `📢 Announcement - ${latestAnnouncement.title}: ${latestAnnouncement.content}`,
})
);
}

const messageBlocks = BlockCollection(
Blocks.Header({
text: `${nodeEnv !== "production" && "[DEV] "}📬 Your Weekly Digest for ${formatDate(cutoff)}`,
}),
Blocks.Section({
text: `You have ${Md.bold(`${recs.length}`)} recommendations this week!`,
}),
Blocks.Section({
text: "Check out these rec'd paper for you from your network!",
}),
Blocks.Divider(),
...recSection.flat(),
...footer,
Blocks.Section({
text: `👀 Any interesting read this week? ${Md.link("https://recnet.io", "Share with your network!")}`,
})
);
return `${subject}\nYou have ${content.recs.length} recommendations this week!\nCheck out these rec'd paper for you from your network!\n${unusedInviteCodes}\n${latestAnnouncement}\n${recsUrls.join("\n")} \n\nAny interesting read this week? 👀\nShare with your network: https://recnet.io/`;
return {
notificationText: `📬 Your RecNet weekly digest has arrived!`,
messageBlocks,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
SLACK_RETRY_DURATION_MS,
SLACK_RETRY_LIMIT,
} from "../slack.const";
import { SendSlackResult } from "../slack.type";
import { SendSlackResult, SlackMessageBlocks } from "../slack.type";

@Injectable()
export class SlackTransporter {
Expand All @@ -31,7 +31,8 @@ export class SlackTransporter {

public async sendDirectMessage(
user: DbUser,
message: string
message: SlackMessageBlocks,
notificationText?: string
): Promise<SendSlackResult> {
if (
this.appConfig.nodeEnv !== "production" &&
Expand All @@ -45,7 +46,7 @@ export class SlackTransporter {
while (retryCount < SLACK_RETRY_LIMIT) {
try {
const slackId = await this.getUserSlackId(user);
await this.postDirectMessage(slackId, message);
await this.postDirectMessage(slackId, message, notificationText);
return { success: true };
} catch (error) {
retryCount++;
Expand Down Expand Up @@ -82,7 +83,8 @@ export class SlackTransporter {

private async postDirectMessage(
userSlackId: string,
message: string
message: SlackMessageBlocks,
notificationText?: string
): Promise<void> {
// Open a direct message conversation
const conversationResp = await this.client.conversations.open({
Expand All @@ -100,7 +102,8 @@ export class SlackTransporter {
// Send the message
await this.client.chat.postMessage({
channel: conversationId,
text: message,
text: notificationText,
blocks: message,
});
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.0",
"server-only": "^0.0.1",
"slack-block-builder": "^2.8.0",
"sonner": "^1.4.32",
"tailwind-merge": "^2.2.1",
"tailwindcss-radix": "^3.0.3",
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

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

0 comments on commit ac328f3

Please sign in to comment.