Skip to content

Birthday notifications #118

Merged
XeIris merged 3 commits intomasterfrom
ei-will-kill-me
Mar 30, 2026
Merged

Birthday notifications #118
XeIris merged 3 commits intomasterfrom
ei-will-kill-me

Conversation

@XeIris
Copy link
Copy Markdown
Collaborator

@XeIris XeIris commented Mar 30, 2026

blah blah blah you can now be notified about someones birthday in advance

Summary by CodeRabbit

  • New Features

    • Birthday reminder system: set, remove, and test reminders for other users.
    • Daily automated UTC reminders sent via DM.
    • Slash subcommands: notify (1/7/14 days), unnotify, and testreminder for dev testing.
  • Improvements

    • Improved birthday set flow with timezone parsing, validation, clearer error messages, and a deletion option.
  • Chores

    • Added persistent reminder storage and related DB support.

@XeIris XeIris self-assigned this Mar 30, 2026
@XeIris XeIris added the enhancement New feature or request label Mar 30, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

Adds a birthday reminder subsystem: new DB table/model/queries, commands to create/remove/test reminders, scheduler logic to send daily DM reminders at 00:00 UTC, and updates to birthday-set behavior and user attribute handling.

Changes

Cohort / File(s) Summary
Database schema & model
database/tables/birthdayReminderTable.js, database/queries/birthdayReminderQueries.js, database/models/BirthdayReminderModel.js, database/models/index.js, database/tables/index.js, database/Database.js
Introduce BirthdayReminder table, SQL queries (upsert/delete/get/get pending/update year), and BirthdayReminderModel with upsert/delete/get/getPending/markReminderSent methods; expose model via Database getter.
Scheduler
classes/birthdayScheduler.js
Add daily cron job (00:00 UTC) that fetches pending reminders, computes next occurrence and daysUntil, filters by daysBefore, fetches users, sends DM reminders, logs outcomes, and marks reminders sent.
Commands: notify / unnotify / testreminder
commands/birthday_notify.js, commands/birthday_unnotify.js, commands/birthday_testreminder.js, commands/commandgroups/birthday.js
Add birthday notify (create/update reminder), birthday unnotify (delete reminder), and birthday testreminder (dev-only preview) subcommands; register subcommands in birthday group; handle DB interactions and DM sending.
Birthday set & user model
commands/birthday_set.js, database/models/UserModel.js
Add timezone parsing and deletion branch (day/month/year = 0) in birthday_set; change validation to reply-based errors and formatted confirmation; allow birthdays attribute to be set/cleared in UserModel.setUserAttr.

Sequence Diagram

sequenceDiagram
    participant Scheduler as Birthday Scheduler (Daily 00:00 UTC)
    participant DB as Database / BirthdayReminderModel
    participant Discord as Discord API
    participant Notifier as Notifier User

    Scheduler->>DB: getPendingReminders(currentUTCYear)
    DB-->>Scheduler: pending reminder rows

    loop per reminder
        Scheduler->>Scheduler: compute next occurrence & daysUntil
        alt daysUntil == entry.daysBefore
            Scheduler->>Discord: fetch tracked user & notifier
            Discord-->>Scheduler: user objects (or null)
            Scheduler->>Notifier: send DM (embed)
            alt DM success
                Notifier-->>Scheduler: OK
                Scheduler->>DB: markReminderSent(notifierId, trackedUserId, year)
                DB-->>Scheduler: updated
            else DM failed
                Notifier-->>Scheduler: Error
                Scheduler->>Scheduler: log failure
            end
        else
            Scheduler->>Scheduler: skip
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hop and I code with a thump and a twitch,
I add little nudges so birthdays don't glitch,
Fourteen, seven, or one—I'll softly remind,
DMs delivered, no birthday left behind,
Hooray for confetti and carrot-cake time! 🎂✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Birthday notifications' directly and clearly summarizes the main feature being added: a birthday notification system. The PR objectives confirm this is the core purpose.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ei-will-kill-me

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
commands/birthday_testreminder.js (1)

16-18: Filter reminders in SQL instead of loading all pending rows.
getPendingReminders(currentYear) + in-memory filter pulls unnecessary rows. Add a notifier-scoped query (e.g., WHERE notifier_id = ?) to reduce load and data exposure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@commands/birthday_testreminder.js` around lines 16 - 18, The current code
calls this.client.db.birthdayReminder.getPendingReminders(currentYear) and then
does an in-memory filter (pending.filter((r) => r.notifierId === notifierId)),
which loads unnecessary rows; change the DB call to perform the filter in SQL
instead. Add or update a method on this.client.db.birthdayReminder (e.g.,
getPendingRemindersForNotifier or extend getPendingReminders to accept
notifierId) to query pending reminders with WHERE notifier_id = ? AND year = ?
(or equivalent), then replace the pending + mine in-memory filtering with a
single call to that new/updated method and remove the pending.filter usage.
Ensure you pass notifierId into the new DB method and update call sites
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@classes/birthdayScheduler.js`:
- Around line 102-109: The reminder is marked sent regardless of send success;
move the await
this.client.db.birthdayReminder.markReminderSent(entry.notifierId,
entry.trackedUserId, currentYear) into the try block immediately after await
notifier.send({ embeds: [reminderEmbed] }) so it only runs when notifier.send
succeeds, and leave the catch to log the dmError via logError (do not call
markReminderSent in the catch).
- Around line 71-79: The nextBirthday calculation currently rebuilds a date at
00:00:00Z which discards the stored UTC time offset (birthdays were saved via
birthday.toISOString() in commands/birthday_set.js), so change the logic in
classes/birthdayScheduler.js to preserve the time component from the parsed
birthday Date (the variable birthday) when deriving the candidate for the
current year: construct thisYear by cloning birthday but setting its year to
currentYear (keeping month, day, and time), compare that to now, and if it's in
the past increment the year by one to form nextBirthday; ensure you use
UTC-getters/setters (e.g., getUTCFullYear/setUTCFullYear) so the original stored
UTC time is preserved when computing daysUntil.
- Line 61: The cron job scheduled in cron.schedule('0 0 * * *', async () => {
... } ) relies on node-cron defaulting to system timezone; update the call to
pass an options object with timezone: 'UTC' (e.g., cron.schedule('0 0 * * *',
async () => { ... }, { timezone: 'UTC' })) so the "midnight UTC" behavior is
enforced; locate the call to cron.schedule in birthdayScheduler.js and add the {
timezone: 'UTC' } option.

In `@commands/birthday_set.js`:
- Around line 129-131: In the catch block that currently calls logError(...) and
await interaction.editReply(...), keep the detailed error only in the log
(logError) and replace the user-facing text passed to interaction.editReply in
the birthday_set command with a generic message (e.g., "An unexpected error
occurred while setting your birthday. Please try again later."); do not include
error.message or any internal details in the reply. Ensure the catch block still
logs the full error via logError and only sends the sanitized generic message to
the user.
- Around line 107-117: The review flags that you are logging sensitive full
birth date information via the variables day, month, year, timezone and the
constructed birthday ISO; update the code in the birthday_set.js block where
log(...) prints "Received inputs..." and "Constructed birthday..." so it no
longer emits raw PII: remove or redact those log calls and instead log only
non-identifying status messages (e.g., "Received birthday inputs for user"
without the date) and a success message that omits the ISO timestamp; keep the
existing validation (Number.isNaN(birthday.getTime())), user feedback
(interaction.editReply), and persistence call
(this.client.db.user.setUserAttr(userId, 'birthdays', birthday.toISOString()))
but ensure logs around these actions reference only userId or operation status,
not the birthday, day, month, year, or timezone variables.

In `@commands/birthday_testreminder.js`:
- Around line 10-49: The execute override in this command bypasses the
DevCommand access check (isDev) so non-devs can run it; fix by routing through
the DevCommand guard: at the top of this command's execute method call await
super.execute(interaction) (or explicitly call the DevCommand isDev guard and
return when it denies access) before running the test DM logic; reference the
execute method on this class, the base DevCommand class and its isDev guard to
ensure the original access-control path is invoked and non-dev users are
blocked.

In `@database/models/BirthdayReminderModel.js`:
- Around line 9-35: The upsertReminder, deleteReminder and markReminderSent
functions currently log success unconditionally even when this.db.executeQuery
can swallow failures; update each to detect failures from executeQuery (either
by checking the returned result object for an error/ok flag or by letting the
underlying error propagate) and rethrow or return an explicit error when
persistence failed, only calling log(...) after a confirmed successful result;
reference the methods upsertReminder, deleteReminder, markReminderSent and the
database executeQuery behavior (see Database.js executeQuery) to implement
either: (a) throw when result indicates failure, or (b) remove internal
swallowing in executeQuery and let exceptions bubble, and ensure logging happens
after success-only confirmation.

---

Nitpick comments:
In `@commands/birthday_testreminder.js`:
- Around line 16-18: The current code calls
this.client.db.birthdayReminder.getPendingReminders(currentYear) and then does
an in-memory filter (pending.filter((r) => r.notifierId === notifierId)), which
loads unnecessary rows; change the DB call to perform the filter in SQL instead.
Add or update a method on this.client.db.birthdayReminder (e.g.,
getPendingRemindersForNotifier or extend getPendingReminders to accept
notifierId) to query pending reminders with WHERE notifier_id = ? AND year = ?
(or equivalent), then replace the pending + mine in-memory filtering with a
single call to that new/updated method and remove the pending.filter usage.
Ensure you pass notifierId into the new DB method and update call sites
accordingly.
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 57332bad-e19e-4386-908a-61791e81d541

📥 Commits

Reviewing files that changed from the base of the PR and between 6f1b443 and 3274bd6.

📒 Files selected for processing (13)
  • classes/birthdayScheduler.js
  • commands/birthday_notify.js
  • commands/birthday_set.js
  • commands/birthday_testreminder.js
  • commands/birthday_unnotify.js
  • commands/commandgroups/birthday.js
  • database/Database.js
  • database/models/BirthdayReminderModel.js
  • database/models/UserModel.js
  • database/models/index.js
  • database/queries/birthdayReminderQueries.js
  • database/tables/birthdayReminderTable.js
  • database/tables/index.js

Comment thread classes/birthdayScheduler.js
Comment thread classes/birthdayScheduler.js
Comment thread classes/birthdayScheduler.js Outdated
Comment thread commands/birthday_set.js Outdated
Comment thread commands/birthday_set.js Outdated
Comment thread commands/birthday_testreminder.js
Comment thread database/models/BirthdayReminderModel.js
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 30, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Autofix skipped. No unresolved CodeRabbit review comments with fix instructions found.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
database/models/BirthdayReminderModel.js (1)

18-26: Inconsistent return value.

deleteReminder returns result while upsertReminder and markReminderSent return nothing. If the return value isn't needed by callers, consider removing it for consistency.

♻️ Suggested fix
   async deleteReminder(notifierId, trackedUserId) {
     const query = birthdayReminderQueries.DELETE_REMINDER;
     const result = await this.db.executeQuery(query, [notifierId, trackedUserId]);
     if (!result.changes) {
       throw new Error(`Failed to delete birthday reminder: notifier=${notifierId}, tracked=${trackedUserId}`);
     }
     log(`Deleted birthday reminder: notifier=${notifierId}, tracked=${trackedUserId}`);
-    return result;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@database/models/BirthdayReminderModel.js` around lines 18 - 26,
deleteReminder currently returns the execution result while upsertReminder and
markReminderSent return nothing; remove the inconsistent return by deleting the
final "return result;" in the deleteReminder method (in
BirthdayReminderModel.deleteReminder) so the method behaves consistently (still
throw on !result.changes and still log the deletion); ensure no callers rely on
the returned value and adjust callers if any do.
commands/birthday_set.js (1)

88-90: Extract duplicated monthNames into a single constant.

The same month array is declared twice (Line 88 and Line 118). Pull it into one module-level constant to avoid drift and simplify maintenance.

Also applies to: 118-120

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@commands/birthday_set.js` around lines 88 - 90, Extract the duplicated
monthNames array into a single module-level constant (e.g., const MONTH_NAMES)
and replace both local declarations with references to that constant; update any
uses in the file (such as in the birthday validation logic where monthNames is
referenced) to use MONTH_NAMES and remove the second array declaration to avoid
drift and duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@commands/birthday_set.js`:
- Around line 48-49: The command accepts minute-offset timezones (e.g., +5.30)
but downstream matching in database/queries/userQueries.js uses
strftime('%m-%dT%H', birthdays) (hour-only), so users with non-:00 minute
offsets get wrong notifications; either reject minute offsets at input or make
matching minute-accurate. Fix by updating commands/birthday_set.js timezone
argument validation (the timezone option and the code that builds the ISO
datetime stored) to reject or normalize any timezone strings with non-00 minutes
(e.g., allow only offsets whose minutes are 00) and return a clear validation
error to the user, or alternatively update the matching logic in
database/queries/userQueries.js to use strftime('%m-%dT%H:%M', birthdays) (and
corresponding scheduler/job logic) so minutes are matched end-to-end; choose one
approach and make the change consistently where timezone ISO is constructed and
where strftime is used.

---

Nitpick comments:
In `@commands/birthday_set.js`:
- Around line 88-90: Extract the duplicated monthNames array into a single
module-level constant (e.g., const MONTH_NAMES) and replace both local
declarations with references to that constant; update any uses in the file (such
as in the birthday validation logic where monthNames is referenced) to use
MONTH_NAMES and remove the second array declaration to avoid drift and
duplication.

In `@database/models/BirthdayReminderModel.js`:
- Around line 18-26: deleteReminder currently returns the execution result while
upsertReminder and markReminderSent return nothing; remove the inconsistent
return by deleting the final "return result;" in the deleteReminder method (in
BirthdayReminderModel.deleteReminder) so the method behaves consistently (still
throw on !result.changes and still log the deletion); ensure no callers rely on
the returned value and adjust callers if any do.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cdff4ca5-b3fd-410e-b99d-e7c36c719347

📥 Commits

Reviewing files that changed from the base of the PR and between 3274bd6 and 7561a50.

📒 Files selected for processing (4)
  • classes/birthdayScheduler.js
  • commands/birthday_set.js
  • commands/birthday_testreminder.js
  • database/models/BirthdayReminderModel.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • classes/birthdayScheduler.js
  • commands/birthday_testreminder.js

Comment thread commands/birthday_set.js
Comment on lines +48 to 49
description: 'Timezone offset (e.g. +8, -5, +5.30, 10.00). Defaults to UTC.',
type: 3,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Minute-offset timezones are accepted, but downstream matching is hour-only.

Line 48 and Line 102 advertise/support minute offsets (e.g., +5.30), and Line 115 stores them in ISO form, but database/queries/userQueries.js (Lines 128-133) matches birthdays with strftime('%m-%dT%H', birthdays) (no minutes). Users in half/quarter-hour offsets can be notified at the wrong minute.

Consider either (a) rejecting non-00 minutes here for now, or (b) updating downstream query/scheduler matching to include minutes end-to-end.

Also applies to: 99-103, 115-116

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@commands/birthday_set.js` around lines 48 - 49, The command accepts
minute-offset timezones (e.g., +5.30) but downstream matching in
database/queries/userQueries.js uses strftime('%m-%dT%H', birthdays)
(hour-only), so users with non-:00 minute offsets get wrong notifications;
either reject minute offsets at input or make matching minute-accurate. Fix by
updating commands/birthday_set.js timezone argument validation (the timezone
option and the code that builds the ISO datetime stored) to reject or normalize
any timezone strings with non-00 minutes (e.g., allow only offsets whose minutes
are 00) and return a clear validation error to the user, or alternatively update
the matching logic in database/queries/userQueries.js to use
strftime('%m-%dT%H:%M', birthdays) (and corresponding scheduler/job logic) so
minutes are matched end-to-end; choose one approach and make the change
consistently where timezone ISO is constructed and where strftime is used.

@XeIris XeIris merged commit a975a2c into master Mar 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant