From 6244251c0d2e25a80e3acc108e7a7bcf29d3eaa4 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:03:16 -0400 Subject: [PATCH 1/5] feat(cron): add birthday cron @cataladev @cata --- apps/cron/src/crons/birthday.ts | 55 +++++++++++++++++++++++++++++++++ apps/cron/src/env.ts | 2 ++ apps/cron/src/index.ts | 3 ++ 3 files changed, 60 insertions(+) create mode 100644 apps/cron/src/crons/birthday.ts diff --git a/apps/cron/src/crons/birthday.ts b/apps/cron/src/crons/birthday.ts new file mode 100644 index 000000000..862d3cfa4 --- /dev/null +++ b/apps/cron/src/crons/birthday.ts @@ -0,0 +1,55 @@ +import { WebhookClient } from "discord.js"; +import { and, eq, exists, sql } from "drizzle-orm"; + +import { db } from "@forge/db/client"; +import { Permissions, User } from "@forge/db/schemas/auth"; +import { Member } from "@forge/db/schemas/knight-hacks"; +import { logger } from "@forge/utils"; + +import { env } from "../env"; +import { CronBuilder } from "../structs/CronBuilder"; + +const BIRTHDAY_WEBHOOK = new WebhookClient({ + url: env.DISCORD_WEBHOOK_BIRTHDAY, +}); + +export const birthday = new CronBuilder({ + name: "birthday", + color: 7, +}).addCron( + "* * * * *", // every day at 12 (noon!) + async () => { + const today = new Date(); + const month = today.getMonth() + 1; + const day = today.getDate(); + + const members = await db + .select({ + firstName: Member.firstName, + lastName: Member.lastName, + guildProfileVisible: Member.guildProfileVisible, + discordId: User.discordUserId, + }) + .from(Member) + .leftJoin(User, eq(User.id, Member.userId)) + .where( + and( + exists( + // can be removed if we want to open to full member list + db + .select() + .from(Permissions) + .where(eq(Permissions.userId, Member.userId)), + ), + eq(Member.guildProfileVisible, true), + eq(sql`EXTRACT(MONTH FROM ${Member.dob})`, month), + eq(sql`EXTRACT(DAY FROM ${Member.dob})`, day), + ), + ); + + for (const u of members) { + logger.log(`${u.firstName} ${u.lastName}'s birthday today!`); + await BIRTHDAY_WEBHOOK.send(`Happy Birthday, <@${u.discordId}>`); + } + }, +); diff --git a/apps/cron/src/env.ts b/apps/cron/src/env.ts index d8864e6d3..ea1ac1dfb 100644 --- a/apps/cron/src/env.ts +++ b/apps/cron/src/env.ts @@ -6,6 +6,7 @@ export const env = createEnv({ DISCORD_BOT_TOKEN: z.string(), DISCORD_WEBHOOK_ANIMAL: z.string(), DISCORD_WEBHOOK_LEETCODE: z.string(), + DISCORD_WEBHOOK_BIRTHDAY: z.string(), DISCORD_WEBHOOK_REMINDERS: z.string(), DISCORD_WEBHOOK_REMINDERS_PRE: z.string(), DISCORD_WEBHOOK_REMINDERS_HACK: z.string(), @@ -13,6 +14,7 @@ export const env = createEnv({ runtimeEnvStrict: { DISCORD_BOT_TOKEN: process.env.DISCORD_BOT_TOKEN, DISCORD_WEBHOOK_ANIMAL: process.env.DISCORD_WEBHOOK_ANIMAL, + DISCORD_WEBHOOK_BIRTHDAY: process.env.DISCORD_WEBHOOK_BIRTHDAY, DISCORD_WEBHOOK_LEETCODE: process.env.DISCORD_WEBHOOK_LEETCODE, DISCORD_WEBHOOK_REMINDERS: process.env.DISCORD_WEBHOOK_REMINDERS, DISCORD_WEBHOOK_REMINDERS_PRE: process.env.DISCORD_WEBHOOK_REMINDERS_PRE, diff --git a/apps/cron/src/index.ts b/apps/cron/src/index.ts index 90a8c9275..6d255d90f 100644 --- a/apps/cron/src/index.ts +++ b/apps/cron/src/index.ts @@ -1,6 +1,7 @@ import { alumniAssign } from "./crons/alumni-assign"; import { capybara, cat, duck, goat } from "./crons/animals"; import { backupFilteredDb } from "./crons/backup-filtered-db"; +import { birthday } from "./crons/birthday"; import { leetcode } from "./crons/leetcode"; import { preReminders, reminders } from "./crons/reminder"; import { roleSync } from "./crons/role-sync"; @@ -22,4 +23,6 @@ reminders.schedule(); // Silencing for now, needs to be manually re-enabled for hacks @WHOEVER_IS_DEV_LEAD_RN // hackReminders.schedule(); +birthday.schedule(); + roleSync.schedule(); From c73f6a4cd97664fbc041f9d5683214724f0e4b87 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Mon, 9 Mar 2026 21:04:05 -0400 Subject: [PATCH 2/5] fix(cron): made a mistake! --- apps/cron/src/crons/birthday.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cron/src/crons/birthday.ts b/apps/cron/src/crons/birthday.ts index 862d3cfa4..3055f3370 100644 --- a/apps/cron/src/crons/birthday.ts +++ b/apps/cron/src/crons/birthday.ts @@ -17,7 +17,7 @@ export const birthday = new CronBuilder({ name: "birthday", color: 7, }).addCron( - "* * * * *", // every day at 12 (noon!) + "0 12 * * *", // every day at 12 (noon!) async () => { const today = new Date(); const month = today.getMonth() + 1; From f9391a0024491bdd8ee67d93c3b576b4eab3ea69 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:01:35 -0400 Subject: [PATCH 3/5] chore(*): update example env --- .env.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index bb272bc96..4c1575268 100644 --- a/.env.example +++ b/.env.example @@ -18,7 +18,8 @@ DISCORD_BOT_TOKEN="discord-bot-token" DISCORD_CLIENT_ID="discord-client-id" DISCORD_CLIENT_SECRET="discord-client-secret" DISCORD_WEATHER_API_KEY="discord-weather-api-key" -DISCORD_WEBHOOK_ANIMAL="" +DISCORD_WEBHOOK_ANIMAL="discord-webhook-animal" +DISCORD_WEBHOOK_BIRTHDAY="discord-webhook-birthday" DISCORD_WEBHOOK_LEETCODE="discord-webhook-leetcode" DISCORD_WEBHOOK_REMINDERS="discord-webhook-reminders" DISCORD_WEBHOOK_REMINDERS_HACK="discord-webhook-reminders-hack" From da1f58649f915f3e24d9181e9a7d7d09292ba74c Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:02:01 -0400 Subject: [PATCH 4/5] feat(cron/birthday): change set of opted in users --- apps/cron/src/crons/birthday.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/apps/cron/src/crons/birthday.ts b/apps/cron/src/crons/birthday.ts index 3055f3370..74e795854 100644 --- a/apps/cron/src/crons/birthday.ts +++ b/apps/cron/src/crons/birthday.ts @@ -1,9 +1,9 @@ import { WebhookClient } from "discord.js"; -import { and, eq, exists, sql } from "drizzle-orm"; +import { and, eq, exists, or, sql } from "drizzle-orm"; import { db } from "@forge/db/client"; import { Permissions, User } from "@forge/db/schemas/auth"; -import { Member } from "@forge/db/schemas/knight-hacks"; +import { DuesPayment, Member } from "@forge/db/schemas/knight-hacks"; import { logger } from "@forge/utils"; import { env } from "../env"; @@ -34,12 +34,20 @@ export const birthday = new CronBuilder({ .leftJoin(User, eq(User.id, Member.userId)) .where( and( - exists( - // can be removed if we want to open to full member list - db - .select() - .from(Permissions) - .where(eq(Permissions.userId, Member.userId)), + // check that they are dues paying or have some permissions + or( + exists( + db + .select() + .from(Permissions) + .where(eq(Permissions.userId, Member.userId)), + ), + exists( + db + .select() + .from(DuesPayment) + .where(eq(DuesPayment.memberId, Member.id)), + ), ), eq(Member.guildProfileVisible, true), eq(sql`EXTRACT(MONTH FROM ${Member.dob})`, month), From e80b0499e67d7b621081f6cbd5482cbd1ce332a7 Mon Sep 17 00:00:00 2001 From: Alexander Paolini <30964205+alexanderpaolini@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:54:16 -0400 Subject: [PATCH 5/5] fjx(cron/birthday): remove dues paying requirement --- apps/cron/src/crons/birthday.ts | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/apps/cron/src/crons/birthday.ts b/apps/cron/src/crons/birthday.ts index 74e795854..4912e247a 100644 --- a/apps/cron/src/crons/birthday.ts +++ b/apps/cron/src/crons/birthday.ts @@ -34,20 +34,11 @@ export const birthday = new CronBuilder({ .leftJoin(User, eq(User.id, Member.userId)) .where( and( - // check that they are dues paying or have some permissions - or( - exists( - db - .select() - .from(Permissions) - .where(eq(Permissions.userId, Member.userId)), - ), - exists( - db - .select() - .from(DuesPayment) - .where(eq(DuesPayment.memberId, Member.id)), - ), + exists( + db + .select() + .from(Permissions) + .where(eq(Permissions.userId, Member.userId)), ), eq(Member.guildProfileVisible, true), eq(sql`EXTRACT(MONTH FROM ${Member.dob})`, month),