Skip to content

Multi Ai personas, birthday ping#98

Merged
XeIris merged 7 commits intomasterfrom
ei-will-kill-me
Aug 25, 2025
Merged

Multi Ai personas, birthday ping#98
XeIris merged 7 commits intomasterfrom
ei-will-kill-me

Conversation

@XeIris
Copy link
Copy Markdown
Collaborator

@XeIris XeIris commented Aug 25, 2025

More ai personas added (e.g gpt, grok, deepseek)

  • add a new persona by defining a trigger word
  • followed by adding a new persona
  • requires openrouter

Enhancements:

Birthday:

  • corrected setdate feedback
  • pings user when birthday comes and uses username

Fixes

/f1-standings:

  • fix the undefined error

/claim

  • fixed frieren gifs not appearing by updating links

Summary by CodeRabbit

  • New Features

    • AI personas for replies with persona-specific name/avatar and additional triggers (@GPT, @deepseek, @imgen).
    • Image-generation support and smarter message delivery with reply and previous navigation buttons.
  • Enhancements

    • Birthday announcements now show the user’s avatar and ping them; birthday confirmation uses a clearer DD-MonthName-YYYY format.
    • More accurate F1 driver names in standings.
    • Claim command adds new GIF responses and updates existing ones for better reliability.
  • Chores

    • Added libraries to support GIF handling, MIME types, HTTP requests, and AI integrations.

XeIris added 7 commits August 17, 2025 23:41
Ai chatbots personas handled in aiPersonas.json
Names & avartarUrl: shows to the client what model theyre using
Sys prompt : self explainatory
trigger: how to call a specific ai model
Provider: currently openRouter and gemini supported
@XeIris XeIris self-assigned this Aug 25, 2025
@XeIris XeIris added the enhancement New feature or request label Aug 25, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 25, 2025

Walkthrough

Adds AI persona configuration and routing with OpenRouter integration in keywords handler, updates keyword triggers, and introduces a new data file for personas. Adjusts birthday scheduler to fetch user details, update embed content, and ping users. Refines F1 driver-name parsing. Expands claim response GIFs. Adds several new npm dependencies.

Changes

Cohort / File(s) Summary
AI personas and keyword routing
data/aiPersonas.json, data/keywords.json
Adds persona config with provider/model/systemPrompt/avatar fields; expands grok triggers to include @GPT, @deepseek, @imgen.
AI content generation handler
classes/handlers/keywordsBehaviorHandler.js
Integrates OpenRouter client; adds persona resolution and unified generateContent supporting OpenRouter and Gemini; updates grok flow to use webhooks, chunking, navigation buttons, and error handling.
Birthday features
classes/birthdayScheduler.js, commands/birthday_set.js
Scheduler now fetches Discord user, uses username, avatar image, and pings user; birthday_set formats confirmation date as DD-MonthName-YYYY.
F1 standings parsing
commands/f1Standings.js
Rewrites driver name extraction to handle varied span structures with fallbacks; output shape unchanged.
Claim responses
commands/claim.js
Updates several Tenor GIF URLs, appends two new responses; selection logic unchanged.
Dependencies
package.json
Adds dependencies: gif-frames, gifencoder, gifuct-js, mime, node-fetch@2, openai.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Discord as Discord Message
  participant Handler as keywordsBehaviorHandler.grok
  participant Personas as resolvePersona
  participant Gen as generateContent
  participant OR as OpenRouter
  participant Gemini as Gemini API
  participant WH as Channel Webhook

  User->>Discord: Message with trigger (e.g., @gpt, @grok)
  Discord->>Handler: Invoke grok(message)
  Handler->>Personas: resolvePersona(trigger, aiPersonas.json)
  Personas-->>Handler: {provider, model, systemPrompt, avatarURL, name}
  alt Provider = openrouter
    Handler->>Gen: generateContent(openrouter, prompt, persona)
    Gen->>OR: chat.completions
    OR-->>Gen: text (and/or images)
  else Provider = gemini
    Handler->>Gen: generateContent(gemini, prompt, persona)
    Gen->>Gemini: stream/generate (text or image)
    Gemini-->>Gen: text and/or image bytes
  end
  Gen-->>Handler: {textChunks[], attachments?}
  Handler->>WH: Get/create webhook (persona name/avatar)
  loop For each chunk
    Handler->>WH: Send chunk (+attachments on first) with nav buttons
    WH-->>Handler: Message ref
  end
Loading
sequenceDiagram
  autonumber
  participant Cron as Cron (0 * * * *)
  participant Scheduler as birthdayScheduler
  participant Discord as Discord API
  participant Channel as Target Channel

  Cron->>Scheduler: Tick
  Scheduler->>Discord: users.fetch(user.id)
  alt User found
    Scheduler->>Channel: Send embed (username, avatar) + content mention
  else Not found
    Scheduler->>Channel: Send embed (Unknown User (id)) + content mention
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • fix keywords #94 — Also modifies keyword handling and data/keywords.json triggers, likely related to routing messages to the grok handler.

Suggested labels

silverwolf is very hot, unfucking fucked code

Poem

A rabbit taps keys with a thump-thump cheer,
Personas assembled, new voices appear.
Grok hums, GPT grins, Deepseek peers in,
Birthdays get cakes with avatars to spin.
GIFs twirl, F1 names shine bright—
I twitch my whiskers: ship it tonight! 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • 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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
commands/f1Standings.js (1)

110-115: Return after invalid year to prevent a second editReply and a pointless fetch

Without a return, the code continues and calls axios.get even after telling the user the year is invalid.

Apply this minimal fix:

     if (year > currentYear || year < minYear) {
       interaction.editReply({
         content: `Invalid year for ${type} standings. Must be between ${minYear} and ${currentYear}.`,
         ephemeral: true,
       });
+      return;
     }
classes/birthdayScheduler.js (2)

26-33: Missing env guard and cache-only channel lookup.

  • If BIRTHDAY_CHANNELS is undefined, .split(',') will throw.
  • Cache misses are common after restarts; fetch from API as fallback.
-          const channelIds = process.env.BIRTHDAY_CHANNELS.split(','); // Get all channel IDs from .env
-          channelIds.forEach(async (channelId) => {
-            const channel = this.client.channels.cache.get(channelId.trim()); // Trim spaces and get the channel
+          const channelsEnv = process.env.BIRTHDAY_CHANNELS || '';
+          const channelIds = channelsEnv.split(',').map(s => s.trim()).filter(Boolean);
+          if (channelIds.length === 0) {
+            logError('BIRTHDAY_CHANNELS is not configured.');
+            return;
+          }
+          for (const channelId of channelIds) {
+            const channel =
+              this.client.channels.cache.get(channelId)
+              || await this.client.channels.fetch(channelId).catch(() => null); // fetch if not cached
             if (!channel) {
               logError(`Channel ID ${channelId} not found or invalid.`);
-              return;
+              continue;
             }
-          });
+          }

27-49: Avoid async .forEach; switch to for..of to ensure ordering and error handling.

The nested forEach(async ...) calls spawn unawaited promises; errors won’t be caught by the outer try/catch and rate limits can spike.

Refactor both loops:

-          channelIds.forEach(async (channelId) => {
+          for (const channelId of channelIds) {
             const channel = this.client.channels.cache.get(channelId.trim()); // Trim spaces and get the channel
             if (!channel) {
               logError(`Channel ID ${channelId} not found or invalid.`);
-              return;
+              continue;
             }
-
-            birthdays.forEach(async (user) => {
+            for (const user of birthdays) {
               const discordUser = await this.client.users.fetch(user.id).catch(() => null);
               const username = discordUser ? discordUser.username : `Unknown User (${user.id})`;
 
               const birthdayEmbed = new EmbedBuilder()
                 .setTitle('🎉 Birthday Alert! 🎉')
                 .setDescription(`Today is ${username}'s birthday! Let's all wish them a great day! 🥳`)
-                .setImage(discordUser.displayAvatarURL({ dynamic: true, format: 'png', size: 4096 }))
                 .setColor(0x00FF00);
 
               log(`Sending birthday message for ${user.id} to channel ${channelId}`);
               await channel.send({
                 content: `<@${user.id}>`,
                 embeds: [birthdayEmbed],
+                allowedMentions: { parse: ['users'] },
               });
-            });
-          });
+            }
+          }
🧹 Nitpick comments (17)
package.json (1)

24-26: Minimize cold-start and reduce RAM: lazy-load GIF tooling

gif-frames, gifencoder, and gifuct-js are heavy and rarely used along the hot path. Consider dynamic importing them only where needed to keep memory footprint down for the bot’s common commands.

Example pattern inside the handler that actually processes GIFs:

// on-demand only
const { default: GIFEncoder } = await import('gifencoder');
const gifFrames = (await import('gif-frames')).default;

Also applies to: 31-32

commands/claim.js (1)

65-74: Round the “hours left” to avoid awkward decimals

Users will often see long decimals. Round up to the next hour or show one decimal for better UX.

Apply this focused change:

-              .setDescription(`You can claim your next nuggie in ${cooldown - diff / HOUR_LENGTH} hours.`)
+              .setDescription(() => {
+                const hoursLeft = Math.max(0, Math.ceil((cooldown * HOUR_LENGTH - diff) / HOUR_LENGTH));
+                return `You can claim your next nuggie in ${hoursLeft} hour${hoursLeft === 1 ? '' : 's'}.`;
+              })()
commands/birthday_set.js (1)

63-70: Formatting bug: don’t pad month names with zeros; show a clean “DD-Month-YYYY”

padStart on monthName is a no-op and misleading. Also, consider validating timezone format and clamping day/month ranges before constructing the Date for clearer error messages.

Apply this diff:

-      const monthName = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
-      const formattedDate = `${String(day).padStart(2, '0')}-${String(monthName[month - 1]).padStart(2, '0')}-${year}`;
+      const monthName = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
+      const formattedDate = `${String(day).padStart(2, '0')}-${monthName[month - 1]}-${year}`;

Optional guardrails:

const tzOk = /^[+-](0[0-9]|1[0-2]):[0-5][0-9]$/.test(timezone);
if (!tzOk) throw new Error('Invalid timezone. Use ±HH:MM (e.g., +05:30)');
if (month < 1 || month > 12) throw new Error('Month must be 1–12');
if (day < 1 || day > 31) throw new Error('Day must be 1–31');
commands/f1Standings.js (3)

9-59: Driver-name parsing is much more robust now—nice work

  • Handles presence/absence of nested span containers and falls back cleanly.
  • CSS class escaping for max-lg:hidden and max-md:hidden is correct.
  • Good use of filter(Boolean) to drop null rows.

One suggestion: guard parseInt results (position/points) with Number.isFinite to avoid NaN in embeds when the table layout changes mid-season.

Example defensive parse:

const toInt = (s) => {
  const n = parseInt(String(s).trim(), 10);
  return Number.isFinite(n) ? n : 0;
};

126-130: ephemeral on editReply is ignored by Discord API

ephemeral applies to the initial interaction reply or followUps, not editReply. Remove it here to avoid confusion.

-      await interaction.editReply({
-        content: 'Failed to fetch the F1 standings. Please try again later.',
-        ephemeral: true,
-      });
+      await interaction.editReply({
+        content: 'Failed to fetch the F1 standings. Please try again later.',
+      });

117-123: Validation Successful – Endpoints Confirmed

  • The endpoints for both drivers and team (e.g. https://www.formula1.com/en/results/2024/drivers and …/team) return HTTP 200 and include the expected f1-table-with-data element. No .html suffix is required.
  • No 403 errors were observed when fetching these URLs without custom headers.
  • Optional Refactor: To future-proof against potential blocking, you may add browser-style headers to the axios request:
    const response = await axios.get(apiUrl, {
      headers: {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
        'Accept-Language': 'en-US,en;q=0.9',
      },
    });
data/aiPersonas.json (5)

36-41: Set response modalities for Imgen persona; optional system prompt.

You’re already toggling Gemini’s image-generation path by model name, but making the intent explicit helps future maintenance.

Apply this diff to add the persona-level modalities (harmless if unused today, self-documenting for future logic):

   {
     "name": "Imgen",
     "triggers": ["@imgen"],
     "provider": "gemini",
     "model": "gemini-2.0-flash-preview-image-generation",
-    "systemPrompt": "", 
+    "systemPrompt": "",
+    "responseModalities": ["IMAGE", "TEXT"],
     "avatarURL": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/19/Tung_tung_tung_sahur.webp/500px-Tung_tung_tung_sahur.webp.png"
   }

13-16: Reduce false positives in trigger matching; prefer mention-style or start-of-message anchors.

Bare triggers like "jarvis", "gpt", and "deepseek" will match inside normal sentences (contentLower.includes). Consider standardizing on mention-style tokens (e.g., "@jarvis") or anchoring to the start-of-message.

If you want to keep both, use word-boundary regexes or a "first token" check to cut accidental matches.

Also applies to: 20-23, 28-31


9-9: Hotlinking external avatars risks broken images and licensing issues.

ArtStation, random blogs, and logo sites can 404 or change content. Also, licensing for these images may not permit redistribution.

  • Host static persona avatars in your own bucket/CDN or in the repo.
  • Add a safe default avatar URL in defaults if per-persona is missing.

Would you like a quick script to scan for 4xx avatar URLs at deploy time and fall back to defaults?

Also applies to: 17-17, 25-25, 33-33, 41-41


5-6: Typo or intentional? "@gork" trigger.

If "@gork" is intentional to catch common misspells, ignore. Otherwise, correct to "@grok".


21-26: Verify model slug and add fallback for GPT persona

  • The slug "openai/gpt-oss-20b:free" is indeed a valid free‐tier variant on OpenRouter, as shown in the OpenRouter model page (openrouter.ai).
  • To guard against transient unavailability or mistyped slugs, implement a runtime fallback:
    • On receiving a 404/“unsupported model” error when calling OpenRouter with openai/gpt-oss-20b:free, retry with the "openrouter/auto" slug, which automatically selects the best free model available (supersharpai.com).

Locations to update:

  • In your API handler (where the GPT persona’s model is used), wrap the OpenRouter call in a try/catch or promise chain that:
    1. Attempts "openai/gpt-oss-20b:free".
    2. On 404, retries once with "openrouter/auto".

No change is needed in data/aiPersonas.json; this is purely a runtime safeguard.

classes/birthdayScheduler.js (1)

21-24: Limit PII in logs; prefer counts over raw objects.

log('Users with birthdays this hour:', birthdays) will dump IDs/user objects. For noisy channels, this can clutter logs and leak data. Log counts instead.

-        log('Users with birthdays this hour:', birthdays);
+        log(`Users with birthdays this hour: ${birthdays.length}`);

I can wire a debug flag to toggle verbose lists if needed.

Also applies to: 52-55

classes/handlers/keywordsBehaviorHandler.js (5)

22-42: Reduce false-positive persona selection; match tokens or start-of-message.

Current includes() can trigger on substrings (e.g., "sWeeTgptastic"). Consider token or prefix-based matching:

-function resolvePersona(messageContent = '') {
-  const contentLower = messageContent.toLowerCase();
-  const personas = personasConfig.personas || [];
-  const foundPersona = personas.find(
-    (p) => Array.isArray(p.triggers)
-      && p.triggers.some((t) => contentLower.includes(String(t).toLowerCase())),
-  );
+function resolvePersona(messageContent = '') {
+  const contentLower = messageContent.toLowerCase();
+  const firstToken = contentLower.trim().split(/\s+/)[0] || '';
+  const personas = personasConfig.personas || [];
+  const foundPersona = personas.find((p) => {
+    if (!Array.isArray(p.triggers)) return false;
+    return p.triggers.some((raw) => {
+      const t = String(raw).toLowerCase();
+      // Exact first-token or word-boundary match
+      return firstToken === t || new RegExp(`\\b${t}\\b`, 'i').test(contentLower);
+    });
+  });

242-250: Guard for missing OpenRouter API key before calling; provide user-facing fallback.

If OPENROUTER_API_KEY is unset, the OpenAI client will 401. Fail fast with a helpful message and switch to defaults (e.g., Gemini).

Example pattern inside the try:

-      const { text, images } = await generateContent({
+      if (persona.provider === 'openrouter' && !process.env.OPENROUTER_API_KEY) {
+        throw new Error('OPENROUTER_API_KEY is not configured');
+      }
+      const { text, images } = await generateContent({
         provider: persona.provider,
         model: persona.model,
         systemPrompt: persona.systemPrompt,
         prompt,
         responseModalities: persona.responseModalities, // Pass the new property from persona
       });

193-197: UI nit: don’t lowercase usernames in UI elements.

Lowercasing looks odd in buttons. Use the original casing for better UX.

-    const username = message.author?.username
-      ? message.author.username.toLowerCase()
-      : 'user';
+    const username = message.author?.username ?? 'user';
@@
-          .setLabel(`↩ Replying to: ${username}`)
+          .setLabel(`↩ Replying to: ${username}`)

Also applies to: 270-275


218-231: Product check: special-case censorship block for Deepseek.

This hard-coded gag responses block specific topics only for Deepseek. If intentional, add a short comment and a config flag to disable per guild/env. Otherwise, remove to avoid surprising users.


55-68: OpenRouter token cap and safety: consider lower max_tokens and temperature control.

max_tokens: 4000 can hit provider caps or accumulate costs. Add temperature top-level config and lower max_tokens (e.g., 1200–2000), exposing both in persona/defaults.

I can wire persona-level generationConfig (maxTokens, temperature, topP) if you want.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e808d6e and d3a0384.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • classes/birthdayScheduler.js (2 hunks)
  • classes/handlers/keywordsBehaviorHandler.js (3 hunks)
  • commands/birthday_set.js (1 hunks)
  • commands/claim.js (1 hunks)
  • commands/f1Standings.js (1 hunks)
  • data/aiPersonas.json (1 hunks)
  • data/keywords.json (1 hunks)
  • package.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
classes/handlers/keywordsBehaviorHandler.js (3)
commands/f1Standings.js (3)
  • require (3-3)
  • require (4-4)
  • require (5-5)
classes/birthdayScheduler.js (2)
  • require (2-2)
  • require (3-3)
commands/askSilverwolfAI.js (6)
  • require (1-1)
  • require (2-2)
  • require (3-3)
  • require (5-5)
  • require (6-6)
  • genAI (8-8)
classes/birthdayScheduler.js (1)
classes/handlers/keywordsBehaviorHandler.js (2)
  • username (141-141)
  • username (194-196)
commands/birthday_set.js (1)
classes/handlers/keywordsBehaviorHandler.js (1)
  • embed (349-353)
🔇 Additional comments (3)
data/keywords.json (1)

44-46: Verify persona routing and safe mentions

Duplicate triggers check passed—no overlapping entries found via the provided script.

• Confirm that keywordsBehaviorHandler resolves “@GPT”, “@deepseek”, and “@imgen” to their intended personas and does not fall back to the “grok” script.
• Ensure any outgoing messages disable Discord-style pings by using allowedMentions: { parse: [] }.
• Implement or verify word-boundary logic (e.g. \btrigger\b) so casual mentions (like “jarvis” inside another word) don’t fire unexpectedly.

Please review these points manually to guarantee correct routing, safe mentions, and precise trigger matching.

commands/claim.js (1)

35-44: Validation Complete: All Tenor URLs Return 200 with Correct Content-Type

All of the Tenor CDN endpoints used in commands/claim.js (both the block at lines 35–44 and the block at lines 51–60) have been verified to respond with HTTP 200 and a Content-Type: image/gif.

• Health-check results for all seven URLs tested:
– 200 OK, image/gif → https://c.tenor.com/i6sOwD66MAEAAAAC/tenor.gif
– 200 OK, image/gif → https://c.tenor.com/h6XlgMwYBnkAAAAd/tenor.gif
– 200 OK, image/gif → https://c.tenor.com/TYW-RNzp6hEAAAAC/tenor.gif
– 200 OK, image/gif → https://media.tenor.com/RYGLfSXNIRIAAAAi/frieren.gif
– 200 OK, image/gif → https://c.tenor.com/kU_EwdsrkLkAAAAC/tenor.gif
– 200 OK, image/gif → https://media.tenor.com/WKPXrrxUvEgAAAAi/frieren-kuru-kuru.gif
– 200 OK, image/gif → https://c.tenor.com/_33fqJ2mxQUAAAAd/tenor.gif

No failures or unexpected media-types were observed. The swap to c.tenor.com and media.tenor.com URLs is functioning as intended. Changes are approved.

commands/birthday_set.js (1)

61-62: No action needed: DB attribute name is consistently ‘birthdays’

I’ve verified that throughout the codebase the user-birthday attribute is always called “birthdays”:

  • commands/birthday_set.js writes to setUserAttr(userId, 'birthdays', …)
  • commands/birthday_get.js reads via getUserAttr(userId, 'birthdays')
  • classes/database.js’s (commented) stub for getUsersWithBirthday also references the birthdays field
  • classes/birthdayScheduler.js invokes getUsersWithBirthday(todayHour) which internally (per the stub) queries the birthdays column

There are no stray references to singular “birthday”. The setter and reader align, so there’s no mismatch risk.

// Start the scheduler to run every hour
start() {
cron.schedule('0 * * * *', async () => { // This runs at the start of every hour
cron.schedule('0 * * * *', async () => { // * * * * * every minute for testing, change to '0 * * * *' for every hour
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Cron comment mismatch and timezone drift; pin to UTC or make configurable.

Expression is hourly ("0 * * * *") but the comment mentions minute-level testing. Also, node-cron uses server local time by default while your date math is UTC.

Apply one (or both):

  • Add timezone option to align the trigger with your UTC-based matching.
  • Make the cron string configurable for testing.
-    cron.schedule('0 * * * *', async () => { // * * * * * every minute for testing, change to '0 * * * *' for every hour
+    // Use env override for testing: BIRTHDAY_CRON="* * * * *"
+    const cronExpr = process.env.BIRTHDAY_CRON || '0 * * * *';
+    cron.schedule(cronExpr, async () => { // runs hourly by default
       const now = new Date();
-    });
+    }, { timezone: 'UTC' });

Cron comment and timezone configuration

Based on the provided snippet and node-cron’s behavior (it defaults to the server’s local timezone and your date math is likely using UTC), the original review suggestions are valid and should be applied:

• The inline comment is out of sync (the code is already using an hourly expression, not minute-level).
• Without a timezone option, a local-time trigger will drift relative to your UTC-based checks.
• Making the cron expression configurable is essential for safe testing and easy overrides.

Please update classes/birthdayScheduler.js as follows:

-    cron.schedule('0 * * * *', async () => { // * * * * * every minute for testing, change to '0 * * * *' for every hour
+    // Allow overriding schedule via BIRTHDAY_CRON; default runs at minute 0 of every hour (UTC)
+    const cronExpr = process.env.BIRTHDAY_CRON || '0 * * * *';
+    cron.schedule(
+      cronExpr,
+      async () => {
         const now = new Date();
         // …rest of birthday matching logic…
-    });
+      },
+      { timezone: 'UTC' }
+    );

Key changes:

  • Introduce BIRTHDAY_CRON env var for easy testing/override.
  • Default to '0 * * * *' for hourly runs.
  • Pass { timezone: 'UTC' } so the scheduler aligns with your UTC-based date math.
  • Update the comment to reflect these changes.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cron.schedule('0 * * * *', async () => { // * * * * * every minute for testing, change to '0 * * * *' for every hour
// Allow overriding schedule via BIRTHDAY_CRON; default runs at minute 0 of every hour (UTC)
const cronExpr = process.env.BIRTHDAY_CRON || '0 * * * *';
cron.schedule(
cronExpr,
async () => {
const now = new Date();
// …rest of birthday matching logic…
},
{ timezone: 'UTC' }
);
🤖 Prompt for AI Agents
In classes/birthdayScheduler.js around line 13, the cron invocation comment is
incorrect and the schedule lacks timezone/configurability; update the cron call
to read its expression from a new BIRTHDAY_CRON env var with a default of '0 * *
* *' (hourly) for testing/override, pass { timezone: 'UTC' } into cron.schedule
so it aligns with UTC-based date math, and update the inline comment to
accurately describe the default and how to override via BIRTHDAY_CRON.

Comment on lines 34 to 42
birthdays.forEach(async (user) => {
const discordUser = await this.client.users.fetch(user.id).catch(() => null);
const username = discordUser ? discordUser.username : `Unknown User (${user.id})`;

const birthdayEmbed = new EmbedBuilder()
.setTitle('🎉 Birthday Alert! 🎉')
.setDescription(`Today is <@${user.id}>'s birthday! Let's all wish them a great day! 🥳`)
.setDescription(`Today is ${username}'s birthday! Let's all wish them a great day! 🥳`)
.setImage(discordUser.displayAvatarURL({ dynamic: true, format: 'png', size: 4096 }))
.setColor(0x00FF00);
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

Null dereference on discordUser when setting image; also option names may differ by discord.js version.

discordUser can be null; discordUser.displayAvatarURL(...) will throw. Gate it and prefer version-safe options.

-              const discordUser = await this.client.users.fetch(user.id).catch(() => null);
-              const username = discordUser ? discordUser.username : `Unknown User (${user.id})`;
+              const discordUser = await this.client.users.fetch(user.id).catch(() => null);
+              const username = discordUser?.username ?? `Unknown User (${user.id})`;
               const birthdayEmbed = new EmbedBuilder()
                 .setTitle('🎉 Birthday Alert! 🎉')
-                .setDescription(`Today is ${username}'s birthday! Let's all wish them a great day! 🥳`)
-                .setImage(discordUser.displayAvatarURL({ dynamic: true, format: 'png', size: 4096 }))
+                .setDescription(`Today is ${username}'s birthday! Let's all wish them a great day! 🥳`);
+
+              if (discordUser) {
+                // For discord.js v14+, prefer { size: 4096, extension: 'png', forceStatic: false }
+                birthdayEmbed.setImage(
+                  discordUser.displayAvatarURL({ size: 4096 }) // keep minimal/options-safe
+                );
+              }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In classes/birthdayScheduler.js around lines 34 to 42, the code calls
discordUser.displayAvatarURL(...) without guarding for a null discordUser and
uses displayAvatarURL options that may differ by discord.js version; update the
block to check whether discordUser exists before calling any avatar method and
provide a safe fallback (e.g., a default avatar URL or empty string) when
discordUser is null, and use the version-safe avatar method/options (or
feature-detect which option names are supported) so you never dereference null
and the avatar call is resilient across discord.js versions.

Comment thread classes/handlers/keywordsBehaviorHandler.js
Comment thread classes/handlers/keywordsBehaviorHandler.js
Comment thread package.json
Comment on lines +24 to 33
"gif-frames": "^1.0.1",
"gifencoder": "^2.0.1",
"gifuct-js": "^2.1.2",
"jsdom": "^13.2.0",
"mime": "^4.0.7",
"node_characterai": "^1.2.7",
"node-cron": "^3.0.3",
"node-fetch": "^2.7.0",
"openai": "^5.12.2",
"sqlite3": "^5.1.7",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

OpenAI and mime are very likely ESM-only; confirm import style or you’ll hit ERR_REQUIRE_ESM at runtime

Given the project is CommonJS (uses require()), the newly added deps raise two red flags:

  • openai@^5.x has moved to ESM-first and cannot be required via require('openai') in CJS without dynamic import.
  • mime@^4 is ESM-only; require('mime') will fail in CJS.

Action options:

  • Preferred: keep the project CJS and use dynamic import in the specific modules that need these packages:
    const OpenAI = (await import('openai')).default;
    const mime = (await import('mime')).default;
  • Or migrate the project to ESM ("type": "module") and update all imports.

Also consider:

  • node-fetch@2.7.0 is redundant on Node 18+ (global fetch is available). If you keep it, ensure you’re not mixing globals and node-fetch across the codebase.
  • Add an engines field so production/runtime is explicit:
    "engines": { "node": ">=18.18" }

I recommend verifying actual import usage now to catch crashes before deploy. Script below.

Run this from repo root:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "— Check OpenAI import style —"
rg -nP -C2 --type=js "(require\\(['\"]openai['\"]\\)|from\\s+['\"]openai['\"])"

echo
echo "— Check mime import style —"
rg -nP -C2 --type=js "(require\\(['\"]mime['\"]\\)|from\\s+['\"]mime['\"])"

echo
echo "— engines field in package.json —"
jq '.engines // {}' package.json

Length of output: 772


Confirm and handle ESM-only packages in CommonJS modules

The import of openai@^5.12.2 via require('openai') will throw ERR_REQUIRE_ESM at runtime, and mime@^4.0.7 is likewise ESM-only. You’ll need to either migrate these modules to ESM or switch to dynamic imports in your CJS code:

• classes/handlers/keywordsBehaviorHandler.js (line 4)
Replace

const { OpenAI } = require('openai');

with a dynamic import inside an async context, for example:

// inside an async function
const { default: OpenAI } = await import('openai');
const client = new OpenAI({ /* ... */ });

• Anywhere you use mime (once it’s imported), switch from

const mime = require('mime');

to

const { default: mime } = await import('mime');

• Optional cleanup: Node 18+ provides a global fetch—consider removing node-fetch@2.7.0 or ensure you consistently use one API.

• Declare your supported Node version explicitly by adding to package.json:

"engines": {
  "node": ">=18.18"
}

If you prefer a project-wide migration, add "type": "module" to package.json and convert all requireimport accordingly.

🤖 Prompt for AI Agents
package.json lines 24-33: the project lists ESM-only packages (openai@^5.12.2
and mime@^4.0.7) which will throw ERR_REQUIRE_ESM when imported via require in
CommonJS files; update the affected CommonJS modules (notably
classes/handlers/keywordsBehaviorHandler.js around line 4 and any files that
require mime) to use dynamic import() inside an async context to load these
modules, or migrate the project to ESM by adding "type":"module" and converting
require → import across the codebase; additionally add an engines field (e.g.
"engines":{"node":">=18.18"}) to package.json to declare Node version
compatibility; choose one approach (dynamic imports in specific files or full
ESM migration) and apply it consistently, and remove or consolidate node-fetch
usage if relying on Node 18+ global fetch.

@XeIris XeIris merged commit 23f5640 into master Aug 25, 2025
1 check passed
@coderabbitai coderabbitai Bot mentioned this pull request Dec 23, 2025
This was referenced Mar 29, 2026
This was referenced May 2, 2026
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