Skip to content

Commit 613cbce

Browse files
committed
Add scheduled dm for urgent messages
1 parent d4fc547 commit 613cbce

4 files changed

Lines changed: 210 additions & 6 deletions

File tree

src/infrastructure/discord/bot.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,13 @@ import {
77
assigneeSelectInteraction,
88
issueButtonInteraction,
99
} from "./interactions";
10+
import { schedule } from "./scheduler";
1011

1112
config();
1213

1314
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
1415

1516
const commands = new Map<string, any>();
16-
const tempIssueData: Record<
17-
string,
18-
{ title: string; description: string; dueDate: string }
19-
> = {};
2017

2118
// Load slash commands
2219
const commandFiles = fs.readdirSync(path.join(__dirname, "commands"));
@@ -27,8 +24,9 @@ const commandFiles = fs.readdirSync(path.join(__dirname, "commands"));
2724
}
2825
})();
2926

30-
client.once(Events.ClientReady, (c) => {
31-
console.log(`✅ Logged in as ${c.user.tag}`);
27+
client.once(Events.ClientReady, (client) => {
28+
console.log(`✅ Logged in as ${client.user.tag}`);
29+
schedule(client);
3230
});
3331

3432
client.on(Events.InteractionCreate, async (interaction) => {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import cron from "node-cron";
2+
import { Client } from "discord.js";
3+
import { urgentItemsDirectMessage } from "./tasks/urgentItemsDirectMessage";
4+
5+
export function schedule(client: Client) {
6+
cron.schedule("30 8 * * *", async () => {
7+
await urgentItemsDirectMessage(client);
8+
});
9+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { GithubAPI } from "@infrastructure/github";
2+
import logger from "@src/config/logger";
3+
import { filterOutStatus, filterForUrgentItems } from "@src/items";
4+
import { formatDiscordDate } from "../webhookMessages";
5+
6+
type GithubDiscordMapping = {
7+
[githubUsername: string]: {
8+
githubUsername: string;
9+
githubId: string;
10+
discordId: string;
11+
};
12+
};
13+
import githubDiscordMapJson from "../../../../data/githubDiscordMap.json";
14+
const githubDiscordMap = githubDiscordMapJson as GithubDiscordMapping;
15+
16+
const urgencyEmojis = ["⏰", "🚨", "⚠️", "❗", "🧨", "💥"];
17+
18+
function getRandomUrgencyEmoji() {
19+
return urgencyEmojis[Math.floor(Math.random() * urgencyEmojis.length)];
20+
}
21+
22+
export const urgentItemsDirectMessage = async (client: any) => {
23+
logger.info({
24+
event: "dailyTasksReminder.start",
25+
body: "Running daily task reminder job.",
26+
});
27+
28+
const githubItemsResult = await GithubAPI.fetchProjectItems();
29+
if (githubItemsResult.err) {
30+
logger.error({
31+
event: "dailyTasksReminder.fetchError",
32+
body: githubItemsResult.val.message,
33+
});
34+
return;
35+
}
36+
37+
const nonBacklogItems = filterOutStatus(githubItemsResult.val, "Backlog");
38+
const urgentItems = filterForUrgentItems(nonBacklogItems);
39+
40+
const groupedByDiscordId = new Map<
41+
string,
42+
{ title: string; dueDate?: Date; url?: string; status?: string }[]
43+
>();
44+
45+
for (const item of urgentItems) {
46+
for (const githubUrl of item.assignedUsers) {
47+
const githubUsername = githubUrl.split("/").pop();
48+
if (!githubUsername) continue;
49+
50+
const mapping = githubDiscordMap[githubUsername];
51+
if (!mapping || !mapping.discordId) {
52+
logger.warn({
53+
event: "dailyTasksReminder.missingMapping",
54+
body: `No Discord ID found for GitHub user: ${githubUsername}`,
55+
});
56+
continue;
57+
}
58+
59+
const discordId = mapping.discordId;
60+
const list = groupedByDiscordId.get(discordId) || [];
61+
list.push({
62+
title: item.title,
63+
dueDate: item.dueDate,
64+
url: item.url,
65+
status: item.status,
66+
});
67+
groupedByDiscordId.set(discordId, list);
68+
}
69+
}
70+
71+
for (const [discordId, issues] of groupedByDiscordId.entries()) {
72+
try {
73+
const user = await client.users.fetch(discordId);
74+
const emoji = getRandomUrgencyEmoji();
75+
76+
const list = issues
77+
.map((item) => {
78+
const titleWithLink = item.url
79+
? `[${item.title}](<${item.url}>)`
80+
: item.title;
81+
const due = item.dueDate ? formatDiscordDate(item.dueDate) : "";
82+
const status = item.status ?? "";
83+
84+
return `\ - ${titleWithLink}${
85+
due ? ` - ${due}` : ""
86+
}${status ? ` - ${status}` : ""}`;
87+
})
88+
.join("\n");
89+
90+
const message = `## ${emoji} You have ${issues.length} urgent issue(s): \n\n${list}\n\n*If you can't meet the deadlines or think they should move, please post in the internal server.*`;
91+
92+
await user.send(message);
93+
94+
logger.info({
95+
event: "dailyTasksReminder.dmSuccess",
96+
body: `Sent DM to ${user.tag} (${discordId})`,
97+
});
98+
} catch (err) {
99+
logger.error({
100+
event: "dailyTasksReminder.dmError",
101+
body: `Failed to DM user ${discordId}: ${err}`,
102+
});
103+
}
104+
}
105+
106+
logger.info({
107+
event: "dailyTasksReminder.success",
108+
body: "Daily tasks reminder sent successfully.",
109+
});
110+
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { urgentItemsDirectMessage } from "@infrastructure/discord/tasks/urgentItemsDirectMessage";
2+
import { GithubAPI } from "@infrastructure/github";
3+
4+
jest.mock("@infrastructure/github", () => ({
5+
GithubAPI: {
6+
fetchProjectItems: jest.fn(),
7+
},
8+
}));
9+
10+
jest.mock("@infrastructure/discord/webhookMessages", () => ({
11+
formatDiscordDate: jest.fn((date) => date.toISOString().split("T")[0]),
12+
}));
13+
14+
jest.mock(
15+
"../../../../data/githubDiscordMap.json",
16+
() => ({
17+
MathyouMB: {
18+
githubUsername: "MathyouMB",
19+
githubId: "mock-github-id",
20+
discordId: "147881865548791808",
21+
},
22+
}),
23+
{ virtual: true },
24+
);
25+
26+
describe("urgentItemsDirectMessage", () => {
27+
let mockSend: jest.Mock;
28+
let mockClient: any;
29+
30+
beforeEach(() => {
31+
jest.resetModules();
32+
mockSend = jest.fn();
33+
mockClient = {
34+
users: {
35+
fetch: jest.fn().mockResolvedValue({ tag: "TestUser", send: mockSend }),
36+
},
37+
};
38+
});
39+
40+
it("will send a DM to mapped user with urgent issues", async () => {
41+
(GithubAPI.fetchProjectItems as jest.Mock).mockResolvedValue({
42+
err: false,
43+
val: [
44+
{
45+
title: "Fix bug",
46+
assignedUsers: ["https://github.com/MathyouMB"],
47+
dueDate: new Date("2025-05-20"),
48+
url: "https://github.com/test/test/issues/1",
49+
status: "In Progress",
50+
},
51+
],
52+
});
53+
54+
await urgentItemsDirectMessage(mockClient);
55+
56+
expect(mockClient.users.fetch).toHaveBeenCalledWith("147881865548791808");
57+
expect(mockSend).toHaveBeenCalledWith(expect.stringContaining("Fix bug"));
58+
});
59+
60+
it("will return early when GitHub fetch fails", async () => {
61+
(GithubAPI.fetchProjectItems as jest.Mock).mockResolvedValue({
62+
err: true,
63+
val: { message: "GitHub error" },
64+
});
65+
66+
await urgentItemsDirectMessage(mockClient);
67+
68+
expect(mockClient.users.fetch).not.toHaveBeenCalled();
69+
});
70+
71+
it("will skip unmapped GitHub usernames", async () => {
72+
(GithubAPI.fetchProjectItems as jest.Mock).mockResolvedValue({
73+
err: false,
74+
val: [
75+
{
76+
title: "Unknown user task",
77+
assignedUsers: ["https://github.com/UnknownUser"],
78+
dueDate: new Date("2025-05-21"),
79+
},
80+
],
81+
});
82+
83+
await urgentItemsDirectMessage(mockClient);
84+
85+
expect(mockSend).not.toHaveBeenCalled();
86+
});
87+
});

0 commit comments

Comments
 (0)