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" diff --git a/apps/cron/src/crons/birthday.ts b/apps/cron/src/crons/birthday.ts new file mode 100644 index 000000000..0694ee9d7 --- /dev/null +++ b/apps/cron/src/crons/birthday.ts @@ -0,0 +1,116 @@ +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, +}); + +const birthdayStrs = [ + `Happy Birthday, {{USERS}} +It's {{USERS}}'s birthday{{PLURAL}} today!`, + + `Happy Birthday, {{USERS}} +Today is all about {{USERS}}'s birthday{{PLURAL}}.`, + + `Happy Birthday, {{USERS}} +Wishing you a great {{USERS}}'s birthday{{PLURAL}}.`, + + `Happy Birthday, {{USERS}} +Hope {{USERS}}'s birthday{{PLURAL}} is full of fun.`, + + `Happy Birthday, {{USERS}} +Celebrating {{USERS}}'s birthday{{PLURAL}} today.`, + + `Happy Birthday, {{USERS}} +Another year, another {{USERS}}'s birthday{{PLURAL}}.`, + + `Happy Birthday, {{USERS}} +Yes, it's {{USERS}}'s birthday{{PLURAL}} again. It keeps happening.`, + + `Happy Birthday, {{USERS}} +Breaking news: it's {{USERS}}'s birthday{{PLURAL}}.`, + + `Happy Birthday, {{USERS}} +Hope {{USERS}}'s birthday{{PLURAL}} is a good one.`, + + `Happy Birthday, {{USERS}} +Make the most of {{USERS}}'s birthday{{PLURAL}}.`, + + `Happy Birthday, {{USERS}} +We checked. It is indeed {{USERS}}'s birthday{{PLURAL}}.`, + + `Happy Birthday, {{USERS}} +Sending good wishes for {{USERS}}'s birthday{{PLURAL}}.`, +]; + +export const birthday = new CronBuilder({ + name: "birthday", + color: 7, +}).addCron( + "0 12 * * *", // 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( + 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), + ), + ); + + const birthdays = members.reduce<{ names: string[]; ids: string[] }>( + (a, c) => { + if (!c.discordId) return a; + a.names.push(c.firstName + " " + c.lastName); + a.ids.push(`<@{c.discordId}>`); + return a; + }, + { names: [], ids: [] }, + ); + if (!birthdays.ids.length) return; + + logger.log(`It is ${birthdays.names.join(" ")}'s birthdays today`); + if (birthdays.ids.length > 1) + birthdays.ids[birthdays.ids.length - 2] = + birthdays.ids[birthdays.ids.length - 2] + + (birthdays.ids.length >= 3 ? ", and" : " and"); + const usersStr = birthdays.ids.join(" "); + const msg = birthdayStrs[Math.floor(Math.random() * birthdayStrs.length)] + ?.replaceAll("{{USERS}}", usersStr) + .replace("{{PLURAL}}", birthdays.ids.length > 1 ? "s" : ""); + + if (!msg) { + logger.log("Birthday message is empty for some reason!"); + logger.log(birthdays); + return; + } + + await BIRTHDAY_WEBHOOK.send(msg); + }, +); diff --git a/apps/cron/src/env.ts b/apps/cron/src/env.ts index 4084a7e85..3eb4eea81 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(), @@ -14,6 +15,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 3677a7809..cdbf64baf 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 { issueReminders } from "./crons/issue-reminders"; import { leetcode } from "./crons/leetcode"; import { preReminders, reminders } from "./crons/reminder"; @@ -23,6 +24,8 @@ reminders.schedule(); // Silencing for now, needs to be manually re-enabled for hacks @WHOEVER_IS_DEV_LEAD_RN // hackReminders.schedule(); +birthday.schedule(); + roleSync.schedule(); issueReminders.schedule();