From a8da519cd1142b42603a1602b947545e38fcfa01 Mon Sep 17 00:00:00 2001 From: Michael Xie Date: Wed, 1 Oct 2025 11:21:11 -0400 Subject: [PATCH 01/11] timer functionality --- docs/COMMAND-WIKI.md | 8 ++ src/commandDetails/reminder/timer.ts | 150 +++++++++++++++++++++++++++ src/commands/reminder/timer.ts | 16 +++ src/events/ready.ts | 2 +- 4 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 src/commandDetails/reminder/timer.ts create mode 100644 src/commands/reminder/timer.ts diff --git a/docs/COMMAND-WIKI.md b/docs/COMMAND-WIKI.md index c2cc7eca..5b944104 100644 --- a/docs/COMMAND-WIKI.md +++ b/docs/COMMAND-WIKI.md @@ -304,6 +304,14 @@ - ``description``: The description of the customization to be set for the user. - **Subcommands:** None +# REMINDER +## timer +- **Aliases:** None +- **Description:** Set up a timer for anything you want! +- **Examples:**
`/timer value seconds:30`
`/timer value minutes:5`
`/timer value hours:2 minutes:30`
`/timer value minutes:10 message:Take a break!` +- **Options:** None +- **Subcommands:** `value`, `name`, `description`, `executeCommand`, `isCommandResponseEphemeral`, `options`, `name`, `description`, `required`, `type` + # SUGGESTION ## suggestion - **Aliases:** ``suggest`` diff --git a/src/commandDetails/reminder/timer.ts b/src/commandDetails/reminder/timer.ts new file mode 100644 index 00000000..56b20072 --- /dev/null +++ b/src/commandDetails/reminder/timer.ts @@ -0,0 +1,150 @@ +import { container } from '@sapphire/framework'; +import { + CodeyCommandDetails, + CodeyCommandOptionType, + SapphireMessageExecuteType, + SapphireMessageResponse, +} from '../../codeyCommand'; + +const TIME_UNITS = { + seconds: { multiplier: 1, singular: 'second', plural: 'seconds' }, + minutes: { multiplier: 60, singular: 'minute', plural: 'minutes' }, + hours: { multiplier: 3600, singular: 'hour', plural: 'hours' }, + days: { multiplier: 86400, singular: 'day', plural: 'days' }, +} as const; + +type TimeUnit = keyof typeof TIME_UNITS; + +const timerExecuteCommand: SapphireMessageExecuteType = ( + _client, + _messageFromUser, + _args, +): Promise => { + return Promise.resolve('Please use a subcommand: `/timer value`'); // Fixed from 'set' to 'value' +}; + +const timerSetExecuteCommand: SapphireMessageExecuteType = ( + _client, + messageFromUser, + args, +): Promise => { + const providedTimes: Array<{ unit: TimeUnit; value: number }> = []; + let totalSeconds = 0; + + // Check each time unit + for (const [unit, config] of Object.entries(TIME_UNITS)) { + const value = args[unit] as number; + if (value !== undefined && value > 0) { + providedTimes.push({ unit: unit as TimeUnit, value }); + totalSeconds += value * config.multiplier; + } + } + + if (providedTimes.length === 0) { + return Promise.resolve('Please specify at least one time duration!'); + } + + if (totalSeconds > 604800) { + return Promise.resolve('Timer duration cannot exceed 1 week!'); + } + + const reminderMessage = (args['message'] as string) || "Time's up!"; + + const timeDescription = providedTimes + .map(({ unit, value }) => { + const config = TIME_UNITS[unit]; + const label = value === 1 ? config.singular : config.plural; + return `${value} ${label}`; + }) + .join(', '); + + // Handle both Message and ChatInputCommandInteraction + const user = 'author' in messageFromUser ? messageFromUser.author : messageFromUser.user; + const channel = messageFromUser.channel; + + setTimeout(async () => { + try { + // Try to DM first + await user.send(`⏰ **Timer Reminder:** ${reminderMessage}`); + } catch (dmError) { + // If DM fails, send a temporary message in channel + if (channel && 'send' in channel) { + try { + const fallbackMsg = await channel.send( + `<@${user.id}> ⏰ **Timer Reminder:** ${reminderMessage} (couldn't DM you!)` + ); + // delete after 30 seconds to remove spam + setTimeout(() => { + fallbackMsg.delete().catch(() => {}); // Ignore if already deleted + }, 30000); + } catch (channelError) { + console.error('Failed to send timer reminder:', channelError); + } + } + } + }, totalSeconds * 1000); + + const content = `Timer set for ${timeDescription}! I'll dm you with: "${reminderMessage}"`; + return Promise.resolve(content); +}; + +export const timerCommandDetails: CodeyCommandDetails = { + name: 'timer', + aliases: [], + description: 'Set up a timer for anything you want!', + detailedDescription: `**Examples:** + \`/timer value seconds:30\` + \`/timer value minutes:5\` + \`/timer value hours:2 minutes:30\` + \`/timer value minutes:10 message:Take a break!\``, + isCommandResponseEphemeral: true, + messageWhenExecutingCommand: 'Building a timer...', + executeCommand: timerExecuteCommand, + messageIfFailure: 'Failed to set up a timer.', + options: [], + subcommandDetails: { + value: { + name: 'value', + description: 'Set a timer with specified duration', + executeCommand: timerSetExecuteCommand, + isCommandResponseEphemeral: true, + options: [ + { + name: 'seconds', + description: 'Number of seconds', + required: false, + + type: CodeyCommandOptionType.INTEGER, + }, + { + name: 'minutes', + description: 'Number of minutes', + required: false, + type: CodeyCommandOptionType.INTEGER, + }, + { + name: 'hours', + description: 'Number of hours', + required: false, + type: CodeyCommandOptionType.INTEGER, + }, + { + name: 'days', + description: 'Number of days', + required: false, + type: CodeyCommandOptionType.INTEGER, + }, + { + name: 'message', + description: 'Custom reminder message (optional)', + required: false, + type: CodeyCommandOptionType.STRING, + }, + ], + aliases: [], + detailedDescription: + 'Set a timer by specifying seconds, minutes, hours, and/or days with an optional message', + subcommandDetails: {}, + }, + }, +}; diff --git a/src/commands/reminder/timer.ts b/src/commands/reminder/timer.ts new file mode 100644 index 00000000..122b09aa --- /dev/null +++ b/src/commands/reminder/timer.ts @@ -0,0 +1,16 @@ +import { Command } from '@sapphire/framework'; +import { CodeyCommand } from '../../codeyCommand'; +import { timerCommandDetails } from '../../commandDetails/reminder/timer'; + +export class TimerCommand extends CodeyCommand { + details = timerCommandDetails; + + public constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + aliases: timerCommandDetails.aliases, + description: timerCommandDetails.description, + detailedDescription: timerCommandDetails.detailedDescription, + }); + } +} diff --git a/src/events/ready.ts b/src/events/ready.ts index 8fa6e7ff..6391cb20 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -37,7 +37,7 @@ ${line03}${dev ? ` ${pad}${blc('<')}${llc('/')}${blc('>')} ${llc('DEVELOPMENT MO const sendReady = async (client: Client): Promise => { const notif = (await client.channels.fetch(NOTIF_CHANNEL_ID)) as TextChannel; const latestRelease = (await getRepositoryReleases('uwcsc', 'codeybot'))[0]; - notif.send(`Codey is up! App version: ${latestRelease.tag_name}`); +// notif.send(`Codey is up! App version: ${latestRelease.tag_name}`); }; export const initReady = (client: Client): void => { From 1648b87e442e33c58ecd040757fa0d2fb4b307d0 Mon Sep 17 00:00:00 2001 From: Michael Xie Date: Wed, 1 Oct 2025 17:00:11 -0400 Subject: [PATCH 02/11] front end of reminders --- docs/COMMAND-WIKI.md | 10 ++ src/commandDetails/reminder/reminder.ts | 211 ++++++++++++++++++++++++ src/commands/reminder/reminder.ts | 16 ++ 3 files changed, 237 insertions(+) create mode 100644 src/commandDetails/reminder/reminder.ts create mode 100644 src/commands/reminder/reminder.ts diff --git a/docs/COMMAND-WIKI.md b/docs/COMMAND-WIKI.md index 5b944104..e7f33cba 100644 --- a/docs/COMMAND-WIKI.md +++ b/docs/COMMAND-WIKI.md @@ -305,6 +305,16 @@ - **Subcommands:** None # REMINDER +## reminder +- **Aliases:** None +- **Description:** Set up a reminder for anything you want! +- **Examples:**
`/reminder` - Opens an interactive reminder setup form +- **Options:** + - ``date``: A date in the format YYYY-MM-DD + - ``time``: A time in the format HH:DD + - ``message``: Message for your Reminder +- **Subcommands:** None + ## timer - **Aliases:** None - **Description:** Set up a timer for anything you want! diff --git a/src/commandDetails/reminder/reminder.ts b/src/commandDetails/reminder/reminder.ts new file mode 100644 index 00000000..8531ebb0 --- /dev/null +++ b/src/commandDetails/reminder/reminder.ts @@ -0,0 +1,211 @@ +import { container } from '@sapphire/framework'; +import { + CodeyCommandDetails, + CodeyCommandOptionType, + SapphireMessageExecuteType, + SapphireMessageResponse, +} from '../../codeyCommand'; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonInteraction, + ButtonStyle, + ChatInputCommandInteraction, + Colors, + ComponentType, + EmbedBuilder, + Message, + ModalBuilder, + TextInputBuilder, + TextInputStyle, +} from 'discord.js'; + +const reminderExecuteCommand: SapphireMessageExecuteType = async ( + _client, + messageFromUser, + args, +): Promise => { + // Check if messageFromUser is actually a ChatInputCommandInteraction (slash command) + + const interaction = messageFromUser as ChatInputCommandInteraction; + + if (!interaction) { + return 'This command only works with slash commands. Please use `/reminder` instead.'; + } + + // Get option values from the command (if provided) + const dateOption= args['date'] as string; + const timeOption = interaction.options.getString('time') || ''; + const messageOption = interaction.options.getString('message') || ''; + + // Create the modal + const modal = new ModalBuilder().setCustomId('reminder-modal').setTitle('📅 Set Your Reminder'); + + // Create text inputs for date, time, and message with pre-populated values + const dateInput = new TextInputBuilder() + .setCustomId('reminder-date') + .setLabel('Date (YYYY-MM-DD)') + .setPlaceholder('e.g., 2025-12-25') + .setStyle(TextInputStyle.Short) + .setRequired(true) + .setMaxLength(10) + .setMinLength(8); + + // Pre-populate date field if provided + if (dateOption) { + dateInput.setValue(dateOption); + } + + const timeInput = new TextInputBuilder() + .setCustomId('reminder-time') + .setLabel('Time (HH:MM) - 24 hour format') + .setPlaceholder('e.g., 14:30 or 09:15') + .setStyle(TextInputStyle.Short) + .setRequired(true) + .setMaxLength(5) + .setMinLength(4); + + // Pre-populate time field if provided + if (timeOption) { + timeInput.setValue(timeOption); + } + + const messageInput = new TextInputBuilder() + .setCustomId('reminder-message') + .setLabel('Reminder Message') + .setPlaceholder('What would you like to be reminded about?') + .setStyle(TextInputStyle.Paragraph) + .setRequired(true) + .setMaxLength(500) + .setMinLength(1); + + // Pre-populate message field if provided + if (messageOption) { + messageInput.setValue(messageOption); + } + + // Add inputs to action rows (Discord modals support up to 5 action rows with 1 text input each) + const firstActionRow = new ActionRowBuilder().addComponents(dateInput); + const secondActionRow = new ActionRowBuilder().addComponents(timeInput); + const thirdActionRow = new ActionRowBuilder().addComponents(messageInput); + + modal.addComponents(firstActionRow, secondActionRow, thirdActionRow); + + // Show the modal to the user + await interaction.showModal(modal); + + + + try { + const modalSubmit = await interaction.awaitModalSubmit({ + time: 300000, // 5 minutes timeout + filter: (i) => i.customId === 'reminder-modal' && i.user.id === interaction.user.id, + }); + + const date = modalSubmit.fields.getTextInputValue('reminder-date').trim(); + const time = modalSubmit.fields.getTextInputValue('reminder-time').trim(); + const message = modalSubmit.fields.getTextInputValue('reminder-message').trim(); + + // Basic validation + const dateRegex = /^\d{4}-\d{1,2}-\d{1,2}$/; + const timeRegex = /^\d{1,2}:\d{2}$/; + + let validationErrors = []; + + if (!dateRegex.test(date)) { + validationErrors.push('Date must be in YYYY-MM-DD format'); + } + + if (!timeRegex.test(time)) { + validationErrors.push('Time must be in HH:MM format (24-hour)'); + } + + const dateParts = date.split('-'); + const paddedDate = `${dateParts[0]}-${dateParts[1].padStart(2, '0')}-${dateParts[2].padStart(2, '0')}`; + + const timeParts = time.split(':'); + const paddedTime = `${timeParts[0].padStart(2, '0')}:${timeParts[1].padStart(2, '0')}`; + + const inputDateTime = new Date(`${paddedDate}T${paddedTime}:00`); + console.log(inputDateTime) + console.log(inputDateTime) + const now = new Date(); + + if (inputDateTime <= now) { + validationErrors.push('Reminder time must be in the future'); + } + + if (validationErrors.length > 0) { + const errorEmbed = new EmbedBuilder() + .setTitle('❌ Invalid Input') + .setDescription( + `Please fix the following errors:\n${validationErrors + .map((error) => `• ${error}`) + .join('\n')}`, + + + // TODO Add backend reminder functionality here + ) + .setColor(Colors.Red); + await modalSubmit.reply({ embeds: [errorEmbed], ephemeral: true }); + return ''; + } + + // Create success output embed + const outputEmbed = new EmbedBuilder() + .setTitle('📝 Reminder Created Successfully!') + .setDescription("Here's your reminder details:") + .addFields( + { name: '📅 Date', value: `\`${date}\``, inline: true }, + { name: '🕐 Time', value: `\`${time}\``, inline: true }, + { + name: '📍 Scheduled For', + value: ``, + inline: false, + }, + { name: '💬 Message', value: `\`${message}\``, inline: false }, + ) + .setColor(Colors.Green) + .setFooter({ text: 'Your reminder has been set!' }) + .setTimestamp(); + + await modalSubmit.reply({ embeds: [outputEmbed], ephemeral: true }); + } catch (error) { + console.log('Modal submission timed out or was cancelled'); + } + + return ''; +}; + +export const reminderCommandDetails: CodeyCommandDetails = { + name: 'reminder', + aliases: [], + description: 'Set up a reminder for anything you want!', + detailedDescription: `**Examples:** + \`/reminder\` - Opens an interactive reminder setup form`, + isCommandResponseEphemeral: true, + messageWhenExecutingCommand: 'Setting up reminder form...', + executeCommand: reminderExecuteCommand, + messageIfFailure: 'Failed to set up reminder form.', + options: [ + { + name: 'date', + description: 'A date in the format YYYY-MM-DD', + type: CodeyCommandOptionType.STRING, + required: false, + }, + { + name: 'time', + description: 'A time in the format HH:DD', + type: CodeyCommandOptionType.STRING, + required: false, + }, + { + name: 'message', + description: 'Message for your Reminder', + type: CodeyCommandOptionType.STRING, + required: false, + }, + ], + subcommandDetails: {}, +}; diff --git a/src/commands/reminder/reminder.ts b/src/commands/reminder/reminder.ts new file mode 100644 index 00000000..589d6f10 --- /dev/null +++ b/src/commands/reminder/reminder.ts @@ -0,0 +1,16 @@ +import { Command } from '@sapphire/framework'; +import { CodeyCommand } from '../../codeyCommand'; +import { reminderCommandDetails } from '../../commandDetails/reminder/reminder'; + +export class ReminderCommand extends CodeyCommand { + details = reminderCommandDetails; + + public constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + aliases: reminderCommandDetails.aliases, + description: reminderCommandDetails.description, + detailedDescription: reminderCommandDetails.detailedDescription, + }); + } +} From 8dab33c876dd7e860d6dba057146bb297f00b962 Mon Sep 17 00:00:00 2001 From: Michael Xie Date: Wed, 1 Oct 2025 18:55:15 -0400 Subject: [PATCH 03/11] allow user to add and view reminders --- docs/COMMAND-WIKI.md | Bin 11127 -> 19522 bytes src/commandDetails/reminder/reminder.ts | 179 ++++++++++++++++++------ src/components/db.ts | 15 ++ src/components/reminder.ts | 52 +++++++ 4 files changed, 207 insertions(+), 39 deletions(-) create mode 100644 src/components/reminder.ts diff --git a/docs/COMMAND-WIKI.md b/docs/COMMAND-WIKI.md index e7f33cba41c6088eb64d35f9e87a284f31b2d9ed..42ffcfc6a0b565d3c197f96ead7110545db84a91 100644 GIT binary patch literal 19522 zcmeHPS#R9P5ze!IMH_lpK(I!(n5cACp|Sv!qxq<2>bwE{a_4?CkD}Ov)xHQYq?ECJJ+M@1sn` zwd9@=y@+0lsE+v6iL8@K@s?;u?`0!mafVrrX%@4lSgB@VrgbzLMXZUDFle=!o8eusA5L*FB+DR+9Ebe6wMpi+JuDCN0q#fN9L{4S5_B0K8Ik9M(vOYXG zSglr*q-yhIF$01nN)L+a7JT}5Oep$bsgkOwi+K|ux)ScK=+QrnD|f}aj~9fc=b0*# zLghW$=M13fH34b*yHEY-f&T8>H}t^2d4>+sb^_>$G@7iZ)S^U7VhGeP-NSl4+tV#- z8)!0W?6JV@U?`bT6_aT2l~5j-_V2qJFGL91{yrocqu=n zN*;z)C7RHDyqfrfkeCE3Y9HZ{1tFHSuPnNxXKCG@g24jvnh=;-*%q ztZmvNZxlEe3U0R(wb_|8-I@W{4+kuVSNm+X25qS;qR{Ye~()cQm z1h>}ECabdE5tWQ<$Z)lI5G%N2xa3)!=PJKtvC_nk%Jum2l>G=Xd|P6FMn*4_EF&?8 z+v8NBUZvwFh)4)FPCvIeMSjw&dhw+R6HMsrOGW^YtDQa10rZR-MqVvB;PNd_ZEZ_zV|2o?%kpfd$NFp*WHMb#5L=VBs$|~u=6TFR zVoHxTZ}I1EU*(Og?v-50s_tzaFSUtDAc zv@(n-k4ak@CNq-sCqj4;^E^HG>i5#>-WAHwx$k_HKZQ1w-jGA9VsBU%QH$g#5{ip@>wDZdH2 zH&d-yz#H|?%5Ab6bCst(3gzQIk3Nv&5bLeW~L?>5xN?Y zF~Kz^xS&u}ne zo^atuCk%Tcy%L1xy)}EMs#`56{7$rMPuG6A-Ua~Q9QE6Vmz%s0|2KG{3`dqHsxB#c z-@`XXb;Nc!$3k~D5ibyambPlp-?K843)tZzQ{OF2h=#}3w8Q~LBoK2@U$CoEl${g96^)5S{-|Dc!WKdm^`o=vO zPqKwrB3vdRsH2ppjVqDY^mZe3hwi4LaFR;b&Fdk(^7&vgAevaZqg=IFRmI+AhJbnJ>WL(gfl!}REW`V(&9E_o3s_lK*?cc*9U zwuhnyl9UZavGaIVv<;F`p{*N+(+FE3_QYiv5oIQ~)d)QC79{AM5HoM9nDaX*IM6=d z&d7W1=};WpyA9g?%@11W#sYTZ?k@J1DmUa2OE`oqtb^_JCzp}%w|1F!{EX354NjQ;ojcejEu8`!!9V{|P2$Ve4dQv)6fTC+loSE1?JT039 zWjMvs)FoOWZ8VyYjaK0+O;b3qzWq$s-{RQ`ye8`jx0`cV3&hp*{Pf}kcm9ZWkhsP* z#P-JkaTvj8>;^I^xc6%obZ-a*D^_vd1Orj%I$eqJdJ)dQ%TngGi(q5aIzf|d2LU!= zAv$5Y=HMJ&$CE}0?l&90u;%nPEZt)t8NU1?leUq^Iu%1Lsx}XR1vo2eIV~4*DXW+( z8w>rZCUqQ`v3V$Y!AkzCRh7ggJaLtr>((c$V=0e%CO`EU)ZZacs3pqY@IC0d2J*mv z4aS4^`tV1q+#7EfsM4(u-{2}q_j+QI*RK!>)HOn$!<+aHI7H0A{JR(0Rc_;rdC!k< z_S?(K$q3?Pxj|Oq`s4fe)9ahl%L_r0tJ~XK1Re;C>sOoyfLOef+8&(FiC;;=S5mi=z2$dz)d*v;dz0gI=*=1gpoHI zOW-9;s>mNuh*`hXFxSpR&XDw&gG+=~ybXZJ3W`&uGS#b`@+~!z?D-G4+JrHa-&$%R z`)eUX(atiJ@FF0s&A4z2JM$>LS1|}YIj1>n3|u!Tn$uoy0}Svn7ey0qkyE{aSmbi$ z7?Lbdg()*Zq@-eZQOss2o1Yl5c)I7#%~UqALQ)D9Y$y6BInxgJSl(Ti67d}ZsVM?6 zs%DJ%xA8P&?Wlox_ij2B$3Gw4unHx`d@f~AeVo_ogl|S~%qY-?(_ygi)3V6)Mom_- z0NtbcSWkL2n(kG3X+%I>IuB zW#k$C1c1q3bPVOAE?RsKASbR0d=d5}x?%Qqg=HpxjzL&RgaEzkY6t$OA{60{unfVM kmQncd8e!Q8%Qzo2!m<&TWpuYU!ZO0)BP?UYX<^xa0H|Ssq5uE@ delta 665 zcmZ8e&uVJT@mq33YNtk>IY2M!5LB$CtFGUlZu5Tt#DrT-md0?OG4J?h6PkR7&%yJ zW)~U~x%fO)L{nMBZtN0Hw-l-LKE>Y!@ONSn$>rEtVpux2P3julB0E3~JSr=3`3SVf zM=mtE#>Rg20+x2-$d!5TWkLx|mHDJ=Q_Inr9m#{LHUrd$92%l-7T-5ujzFP2J*Ggxwo(MnIsLKecKm#NK;i^~=%uIs2 z;^aG>e5(}!aq>WjYnaD;iT>m7?^jMyP3AcCp3m;gpsYLdq{~b(pm1WcvhF>)`eE)b D2|wVZ diff --git a/src/commandDetails/reminder/reminder.ts b/src/commandDetails/reminder/reminder.ts index 8531ebb0..8eba4c2d 100644 --- a/src/commandDetails/reminder/reminder.ts +++ b/src/commandDetails/reminder/reminder.ts @@ -19,8 +19,9 @@ import { TextInputBuilder, TextInputStyle, } from 'discord.js'; +import * as reminderComponents from '../../components/reminder' -const reminderExecuteCommand: SapphireMessageExecuteType = async ( +const reminderCreateCommand: SapphireMessageExecuteType = async ( _client, messageFromUser, args, @@ -34,7 +35,7 @@ const reminderExecuteCommand: SapphireMessageExecuteType = async ( } // Get option values from the command (if provided) - const dateOption= args['date'] as string; + const dateOption = args['date'] as string; const timeOption = interaction.options.getString('time') || ''; const messageOption = interaction.options.getString('message') || ''; @@ -94,8 +95,6 @@ const reminderExecuteCommand: SapphireMessageExecuteType = async ( // Show the modal to the user await interaction.showModal(modal); - - try { const modalSubmit = await interaction.awaitModalSubmit({ time: 300000, // 5 minutes timeout @@ -121,14 +120,17 @@ const reminderExecuteCommand: SapphireMessageExecuteType = async ( } const dateParts = date.split('-'); - const paddedDate = `${dateParts[0]}-${dateParts[1].padStart(2, '0')}-${dateParts[2].padStart(2, '0')}`; - + const paddedDate = `${dateParts[0]}-${dateParts[1].padStart(2, '0')}-${dateParts[2].padStart( + 2, + '0', + )}`; + const timeParts = time.split(':'); const paddedTime = `${timeParts[0].padStart(2, '0')}:${timeParts[1].padStart(2, '0')}`; const inputDateTime = new Date(`${paddedDate}T${paddedTime}:00`); - console.log(inputDateTime) - console.log(inputDateTime) + console.log(inputDateTime); + console.log(inputDateTime); const now = new Date(); if (inputDateTime <= now) { @@ -143,14 +145,14 @@ const reminderExecuteCommand: SapphireMessageExecuteType = async ( .map((error) => `• ${error}`) .join('\n')}`, - - // TODO Add backend reminder functionality here ) .setColor(Colors.Red); await modalSubmit.reply({ embeds: [errorEmbed], ephemeral: true }); return ''; } + // TODO Add backend reminder functionality here + reminderComponents.addReminder(messageFromUser.client.id!, now.toISOString(), inputDateTime.toISOString(), message) // Create success output embed const outputEmbed = new EmbedBuilder() .setTitle('📝 Reminder Created Successfully!') @@ -177,35 +179,134 @@ const reminderExecuteCommand: SapphireMessageExecuteType = async ( return ''; }; +const reminderExecuteCommand: SapphireMessageExecuteType = async ( + _client, + messageFromUser, + args, +): Promise => { + return 'ooga booga' +} + +const reminderViewCommand: SapphireMessageExecuteType = async ( + _client, + messageFromUser, + args, +): Promise => { + let list = await reminderComponents.getReminders(messageFromUser.client.user.id); + console.log('Fetching reminders for user:', messageFromUser.client.user.id); + + if (!list || list.length === 0) { + return "📭 You have no reminders set."; + } + + let response = "📝 **Your Reminders:**\n\n"; + + list.forEach((reminder, index) => { + // Parse the ISO date string + const reminderDate = new Date(reminder.reminder_at); + const createdDate = new Date(reminder.created_at); + + // Format dates nicely + const reminderFormatted = reminderDate.toLocaleString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZoneName: 'short' + }); + + const createdFormatted = createdDate.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + + // Calculate time until reminder + const now = new Date(); + const timeDiff = reminderDate.getTime() - now.getTime(); + let timeUntil = ''; + + if (timeDiff > 0) { + const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60)); + + if (days > 0) { + timeUntil = `⏳ In ${days}d ${hours}h`; + } else if (hours > 0) { + timeUntil = `⏳ In ${hours}h ${minutes}m`; + } else { + timeUntil = `⏳ In ${minutes}m`; + } + } else { + timeUntil = `🔴 **OVERDUE**`; + } + + response += `**${index + 1}.** ${reminder.message}\n`; + response += `📅 **When:** ${reminderFormatted}\n`; + response += `${timeUntil}\n`; + response += `📝 *Created: ${createdFormatted}*\n\n`; + }); + + return response; +} +// ...existing code... + + export const reminderCommandDetails: CodeyCommandDetails = { - name: 'reminder', - aliases: [], - description: 'Set up a reminder for anything you want!', - detailedDescription: `**Examples:** + name: 'reminder', + aliases: [], + description: 'Set up a reminder for anything you want!', + detailedDescription: `**Examples:** \`/reminder\` - Opens an interactive reminder setup form`, - isCommandResponseEphemeral: true, - messageWhenExecutingCommand: 'Setting up reminder form...', - executeCommand: reminderExecuteCommand, - messageIfFailure: 'Failed to set up reminder form.', - options: [ - { - name: 'date', - description: 'A date in the format YYYY-MM-DD', - type: CodeyCommandOptionType.STRING, - required: false, - }, - { - name: 'time', - description: 'A time in the format HH:DD', - type: CodeyCommandOptionType.STRING, - required: false, - }, - { - name: 'message', - description: 'Message for your Reminder', - type: CodeyCommandOptionType.STRING, - required: false, + isCommandResponseEphemeral: true, + messageWhenExecutingCommand: 'Setting up reminder form...', + executeCommand: reminderExecuteCommand, + messageIfFailure: 'Failed to set up reminder form.', + options: [], + subcommandDetails: { + create: { + name: 'create', + description: 'Set a timer with specified duration', + executeCommand: reminderCreateCommand, + isCommandResponseEphemeral: true, + options: [ + { + name: 'date', + description: 'A date in the format YYYY-MM-DD', + type: CodeyCommandOptionType.STRING, + required: false, + }, + { + name: 'time', + description: 'A time in the format HH:DD', + type: CodeyCommandOptionType.STRING, + required: false, + }, + { + name: 'message', + description: 'Message for your Reminder', + type: CodeyCommandOptionType.STRING, + required: false, + }, + ], + aliases: [], + detailedDescription: '', + subcommandDetails: {} + }, + + view: { + name: 'view', + description: 'View any reminders you set!', + executeCommand: reminderViewCommand, + isCommandResponseEphemeral: true, + options: [], + aliases: [], + detailedDescription: '', + subcommandDetails: {} + }, }, - ], - subcommandDetails: {}, -}; +} diff --git a/src/components/db.ts b/src/components/db.ts index 19dad417..af85544c 100644 --- a/src/components/db.ts +++ b/src/components/db.ts @@ -222,6 +222,20 @@ const initPeopleCompaniesTable = async (db: Database): Promise => { )`); }; +const initRemindersTable = async (db: Database): Promise => { + await db.run(` + CREATE TABLE IF NOT EXISTS reminders ( + id INTEGER PRIMARY KEY NOT NULL, + user_id VARCHAR(255) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + reminder_at TIMESTAMP NOT NULL, + message TEXT NOT NULL, + status INTEGER NOT NULL DEFAULT 0 + ) + `); + // TODO includ indexing if performance needs it? +} + const initTables = async (db: Database): Promise => { //initialize all relevant tables await initCoffeeChatTables(db); @@ -236,6 +250,7 @@ const initTables = async (db: Database): Promise => { await initResumePreview(db); await initCompaniesTable(db); await initPeopleCompaniesTable(db); + await initRemindersTable(db); }; export const openDB = async (): Promise => { diff --git a/src/components/reminder.ts b/src/components/reminder.ts new file mode 100644 index 00000000..e5ea2ff8 --- /dev/null +++ b/src/components/reminder.ts @@ -0,0 +1,52 @@ +import _ from 'lodash'; +import { openDB } from './db'; + + +export enum Status { + Active, + Paused, +} + +export interface Reminder { + userId: string, + created_at: string, + reminder_at: string, + message: string +} + +/* + Returns a list of reminders by userID. +*/ +export const getReminders = async ( + userId: string +): Promise => { + const db = await openDB(); + console.log(`Received request for user_id ${userId}`) + return await db.all('SELECT * FROM reminders WHERE user_id = ?', userId); +}; + + +// Adds a reminder to the DB +export const addReminder = async ( +userId: string, created_at: string, reminder_at: string, message: string): Promise => { + const db = await openDB(); + + console.log('addReminder parameters:'); + console.log('userId:', userId); + console.log('created_at:', created_at); + console.log('reminder_at:', reminder_at); + console.log('message:', message); + + // Save reminder into DB + await db.run( + ` + INSERT INTO reminders (user_id, created_at, reminder_at, message, status) + VALUES(?,?,?,?,?); + `, + userId, + created_at, + reminder_at, + message, + 0 + ); +}; From 96ecf25996025f998ab4ceda516354fdd5c65812 Mon Sep 17 00:00:00 2001 From: Michael Xie Date: Wed, 1 Oct 2025 22:30:51 -0400 Subject: [PATCH 04/11] added observer for reminder functionality --- docs/COMMAND-WIKI.md | Bin 19522 -> 12371 bytes src/commandDetails/fun/rollDice.ts | 11 +- src/commandDetails/reminder/reminder.ts | 245 ++++++++++---------- src/components/{ => reminder}/reminder.ts | 42 ++-- src/components/reminder/reminderObserver.ts | 69 ++++++ src/events/ready.ts | 5 + 6 files changed, 241 insertions(+), 131 deletions(-) rename src/components/{ => reminder}/reminder.ts (50%) create mode 100644 src/components/reminder/reminderObserver.ts diff --git a/docs/COMMAND-WIKI.md b/docs/COMMAND-WIKI.md index 42ffcfc6a0b565d3c197f96ead7110545db84a91..a3b3b1c6fb7208aff4e1f2139e3ef350a715ab4f 100644 GIT binary patch delta 563 zcmYL{L1+_E5Qcf$Cbb&3w(FWT8efuDL%SwvNP<*NjL8-YNehV{L=U@3Hn26>jhj^H ztzJAT$?riBZ>1oDAW!h9)oY=po{KGbQR+bt;>nr~#hzxE`Dgx*VLm=1pOwJ?&B^l3 z%v@GRWI6~g?qos!6LAFsv~21IRV(ugO-yBij}M-( zk#xlVftIBj&8lYMVR!p_#DOp2t;4isVn^_^Kd1LdCpm&$zdNCX%4WTx8jD@fN^e<7 zZECbwkd-SXD|yha>c&AIJ5%G5W>{veW}|gS?ONDP&GB!&XeH#v%X! literal 19522 zcmeHPS#R9P5ze!IMH_lpK(I!(n5cACp|Sv!qxq<2>bwE{a_4?CkD}Ov)xHQYq?ECJJ+M@1sn` zwd9@=y@+0lsE+v6iL8@K@s?;u?`0!mafVrrX%@4lSgB@VrgbzLMXZUDFle=!o8eusA5L*FB+DR+9Ebe6wMpi+JuDCN0q#fN9L{4S5_B0K8Ik9M(vOYXG zSglr*q-yhIF$01nN)L+a7JT}5Oep$bsgkOwi+K|ux)ScK=+QrnD|f}aj~9fc=b0*# zLghW$=M13fH34b*yHEY-f&T8>H}t^2d4>+sb^_>$G@7iZ)S^U7VhGeP-NSl4+tV#- z8)!0W?6JV@U?`bT6_aT2l~5j-_V2qJFGL91{yrocqu=n zN*;z)C7RHDyqfrfkeCE3Y9HZ{1tFHSuPnNxXKCG@g24jvnh=;-*%q ztZmvNZxlEe3U0R(wb_|8-I@W{4+kuVSNm+X25qS;qR{Ye~()cQm z1h>}ECabdE5tWQ<$Z)lI5G%N2xa3)!=PJKtvC_nk%Jum2l>G=Xd|P6FMn*4_EF&?8 z+v8NBUZvwFh)4)FPCvIeMSjw&dhw+R6HMsrOGW^YtDQa10rZR-MqVvB;PNd_ZEZ_zV|2o?%kpfd$NFp*WHMb#5L=VBs$|~u=6TFR zVoHxTZ}I1EU*(Og?v-50s_tzaFSUtDAc zv@(n-k4ak@CNq-sCqj4;^E^HG>i5#>-WAHwx$k_HKZQ1w-jGA9VsBU%QH$g#5{ip@>wDZdH2 zH&d-yz#H|?%5Ab6bCst(3gzQIk3Nv&5bLeW~L?>5xN?Y zF~Kz^xS&u}ne zo^atuCk%Tcy%L1xy)}EMs#`56{7$rMPuG6A-Ua~Q9QE6Vmz%s0|2KG{3`dqHsxB#c z-@`XXb;Nc!$3k~D5ibyambPlp-?K843)tZzQ{OF2h=#}3w8Q~LBoK2@U$CoEl${g96^)5S{-|Dc!WKdm^`o=vO zPqKwrB3vdRsH2ppjVqDY^mZe3hwi4LaFR;b&Fdk(^7&vgAevaZqg=IFRmI+AhJbnJ>WL(gfl!}REW`V(&9E_o3s_lK*?cc*9U zwuhnyl9UZavGaIVv<;F`p{*N+(+FE3_QYiv5oIQ~)d)QC79{AM5HoM9nDaX*IM6=d z&d7W1=};WpyA9g?%@11W#sYTZ?k@J1DmUa2OE`oqtb^_JCzp}%w|1F!{EX354NjQ;ojcejEu8`!!9V{|P2$Ve4dQv)6fTC+loSE1?JT039 zWjMvs)FoOWZ8VyYjaK0+O;b3qzWq$s-{RQ`ye8`jx0`cV3&hp*{Pf}kcm9ZWkhsP* z#P-JkaTvj8>;^I^xc6%obZ-a*D^_vd1Orj%I$eqJdJ)dQ%TngGi(q5aIzf|d2LU!= zAv$5Y=HMJ&$CE}0?l&90u;%nPEZt)t8NU1?leUq^Iu%1Lsx}XR1vo2eIV~4*DXW+( z8w>rZCUqQ`v3V$Y!AkzCRh7ggJaLtr>((c$V=0e%CO`EU)ZZacs3pqY@IC0d2J*mv z4aS4^`tV1q+#7EfsM4(u-{2}q_j+QI*RK!>)HOn$!<+aHI7H0A{JR(0Rc_;rdC!k< z_S?(K$q3?Pxj|Oq`s4fe)9ahl%L_r0tJ~XK1Re;C>sOoyfLOef+8&(FiC;;=S5mi=z2$dz)d*v;dz0gI=*=1gpoHI zOW-9;s>mNuh*`hXFxSpR&XDw&gG+=~ybXZJ3W`&uGS#b`@+~!z?D-G4+JrHa-&$%R z`)eUX(atiJ@FF0s&A4z2JM$>LS1|}YIj1>n3|u!Tn$uoy0}Svn7ey0qkyE{aSmbi$ z7?Lbdg()*Zq@-eZQOss2o1Yl5c)I7#%~UqALQ)D9Y$y6BInxgJSl(Ti67d}ZsVM?6 zs%DJ%xA8P&?Wlox_ij2B$3Gw4unHx`d@f~AeVo_ogl|S~%qY-?(_ygi)3V6)Mom_- z0NtbcSWkL2n(kG3X+%I>IuB zW#k$C1c1q3bPVOAE?RsKASbR0d=d5}x?%Qqg=HpxjzL&RgaEzkY6t$OA{60{unfVM kmQncd8e!Q8%Qzo2!m<&TWpuYU!ZO0)BP?UYX<^xa0H|Ssq5uE@ diff --git a/src/commandDetails/fun/rollDice.ts b/src/commandDetails/fun/rollDice.ts index 953b5312..d66afc7e 100644 --- a/src/commandDetails/fun/rollDice.ts +++ b/src/commandDetails/fun/rollDice.ts @@ -23,7 +23,16 @@ const rollDiceExecuteCommand: SapphireMessageExecuteType = ( return new Promise((resolve, _reject) => resolve("that's too many sides!")); } const diceFace = getRandomIntFrom1(sides); - return new Promise((resolve, _reject) => resolve(`you rolled a ${diceFace}!`)); + let userId: string; + if ('author' in _messageFromUser) { + userId = _messageFromUser.author.id; + } else if ('user' in _messageFromUser) { + console.log("using user") + userId = _messageFromUser.user.id; + } else { + userId = 'unknown'; + } + return new Promise((resolve, _reject) => resolve(`your id is ${userId}!`)); }; export const rollDiceCommandDetails: CodeyCommandDetails = { diff --git a/src/commandDetails/reminder/reminder.ts b/src/commandDetails/reminder/reminder.ts index 8eba4c2d..862249a2 100644 --- a/src/commandDetails/reminder/reminder.ts +++ b/src/commandDetails/reminder/reminder.ts @@ -19,13 +19,20 @@ import { TextInputBuilder, TextInputStyle, } from 'discord.js'; -import * as reminderComponents from '../../components/reminder' +import * as reminderComponents from '../../components/reminder/reminder'; +const getUser = (messageFromUser: Message | ChatInputCommandInteraction) => { + return 'user' in messageFromUser ? messageFromUser.user : messageFromUser.author; +}; + +// Create a new reminder const reminderCreateCommand: SapphireMessageExecuteType = async ( _client, messageFromUser, args, ): Promise => { + const user = getUser(messageFromUser); + // Check if messageFromUser is actually a ChatInputCommandInteraction (slash command) const interaction = messageFromUser as ChatInputCommandInteraction; @@ -144,15 +151,17 @@ const reminderCreateCommand: SapphireMessageExecuteType = async ( `Please fix the following errors:\n${validationErrors .map((error) => `• ${error}`) .join('\n')}`, - ) .setColor(Colors.Red); await modalSubmit.reply({ embeds: [errorEmbed], ephemeral: true }); return ''; } - - // TODO Add backend reminder functionality here - reminderComponents.addReminder(messageFromUser.client.id!, now.toISOString(), inputDateTime.toISOString(), message) + reminderComponents.addReminder( + user.id, + now.toISOString(), + inputDateTime.toISOString(), + message, + ); // Create success output embed const outputEmbed = new EmbedBuilder() .setTitle('📝 Reminder Created Successfully!') @@ -179,134 +188,136 @@ const reminderCreateCommand: SapphireMessageExecuteType = async ( return ''; }; +// Test command (just returns the user ID for now) const reminderExecuteCommand: SapphireMessageExecuteType = async ( _client, messageFromUser, args, ): Promise => { - return 'ooga booga' -} + return `User id is ${messageFromUser.client.id}`; +}; +// View reminders const reminderViewCommand: SapphireMessageExecuteType = async ( _client, messageFromUser, args, ): Promise => { - let list = await reminderComponents.getReminders(messageFromUser.client.user.id); - console.log('Fetching reminders for user:', messageFromUser.client.user.id); - - if (!list || list.length === 0) { - return "📭 You have no reminders set."; - } - - let response = "📝 **Your Reminders:**\n\n"; - - list.forEach((reminder, index) => { - // Parse the ISO date string - const reminderDate = new Date(reminder.reminder_at); - const createdDate = new Date(reminder.created_at); - - // Format dates nicely - const reminderFormatted = reminderDate.toLocaleString('en-US', { - weekday: 'short', - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - timeZoneName: 'short' - }); - - const createdFormatted = createdDate.toLocaleString('en-US', { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - - // Calculate time until reminder - const now = new Date(); - const timeDiff = reminderDate.getTime() - now.getTime(); - let timeUntil = ''; - - if (timeDiff > 0) { - const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24)); - const hours = Math.floor((timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); - const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60)); - - if (days > 0) { - timeUntil = `⏳ In ${days}d ${hours}h`; - } else if (hours > 0) { - timeUntil = `⏳ In ${hours}h ${minutes}m`; - } else { - timeUntil = `⏳ In ${minutes}m`; - } - } else { - timeUntil = `🔴 **OVERDUE**`; - } - - response += `**${index + 1}.** ${reminder.message}\n`; - response += `📅 **When:** ${reminderFormatted}\n`; - response += `${timeUntil}\n`; - response += `📝 *Created: ${createdFormatted}*\n\n`; + const user = getUser(messageFromUser); + let list = await reminderComponents.getReminders(user.id); + console.log('Fetching reminders for user:', user.id); + + if (!list || list.length === 0) { + return '📭 You have no reminders set.'; + } + + let response = '📝 **Your Reminders:**\n\n'; + + list.forEach((reminder, index) => { + // Parse the ISO date string + const reminderDate = new Date(reminder.reminder_at); + const createdDate = new Date(reminder.created_at); + + // Format dates nicely + const reminderFormatted = reminderDate.toLocaleString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZoneName: 'short', }); - - return response; -} -// ...existing code... + const createdFormatted = createdDate.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + + // Calculate time until reminder + const now = new Date(); + const timeDiff = reminderDate.getTime() - now.getTime(); + let timeUntil = ''; + + if (timeDiff > 0) { + const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60)); + + if (days > 0) { + timeUntil = `⏳ In ${days}d ${hours}h`; + } else if (hours > 0) { + timeUntil = `⏳ In ${hours}h ${minutes}m`; + } else { + timeUntil = `⏳ In ${minutes}m`; + } + } else { + timeUntil = `🔴 **OVERDUE**`; + } + + response += `**${index + 1}.** ${reminder.message}\n`; + response += `📅 **When:** ${reminderFormatted}\n`; + response += `${timeUntil}\n`; + response += `📝 *Created: ${createdFormatted}*\n\n`; + }); + + return response; +}; +// ...existing code... export const reminderCommandDetails: CodeyCommandDetails = { - name: 'reminder', - aliases: [], - description: 'Set up a reminder for anything you want!', - detailedDescription: `**Examples:** + name: 'reminder', + aliases: [], + description: 'Set up a reminder for anything you want!', + detailedDescription: `**Examples:** \`/reminder\` - Opens an interactive reminder setup form`, - isCommandResponseEphemeral: true, - messageWhenExecutingCommand: 'Setting up reminder form...', - executeCommand: reminderExecuteCommand, - messageIfFailure: 'Failed to set up reminder form.', - options: [], - subcommandDetails: { - create: { - name: 'create', - description: 'Set a timer with specified duration', - executeCommand: reminderCreateCommand, - isCommandResponseEphemeral: true, - options: [ - { - name: 'date', - description: 'A date in the format YYYY-MM-DD', - type: CodeyCommandOptionType.STRING, - required: false, - }, - { - name: 'time', - description: 'A time in the format HH:DD', - type: CodeyCommandOptionType.STRING, - required: false, - }, - { - name: 'message', - description: 'Message for your Reminder', - type: CodeyCommandOptionType.STRING, - required: false, - }, - ], - aliases: [], - detailedDescription: '', - subcommandDetails: {} + isCommandResponseEphemeral: true, + messageWhenExecutingCommand: 'Setting up reminder form...', + executeCommand: reminderExecuteCommand, + messageIfFailure: 'Failed to set up reminder form.', + options: [], + subcommandDetails: { + create: { + name: 'create', + description: 'Set a timer with specified duration', + executeCommand: reminderCreateCommand, + isCommandResponseEphemeral: true, + options: [ + { + name: 'date', + description: 'A date in the format YYYY-MM-DD', + type: CodeyCommandOptionType.STRING, + required: false, }, - - view: { - name: 'view', - description: 'View any reminders you set!', - executeCommand: reminderViewCommand, - isCommandResponseEphemeral: true, - options: [], - aliases: [], - detailedDescription: '', - subcommandDetails: {} + { + name: 'time', + description: 'A time in the format HH:DD', + type: CodeyCommandOptionType.STRING, + required: false, + }, + { + name: 'message', + description: 'Message for your Reminder', + type: CodeyCommandOptionType.STRING, + required: false, }, + ], + aliases: [], + detailedDescription: '', + subcommandDetails: {}, }, -} + + view: { + name: 'view', + description: 'View any reminders you set!', + executeCommand: reminderViewCommand, + isCommandResponseEphemeral: true, + options: [], + aliases: [], + detailedDescription: '', + subcommandDetails: {}, + }, + }, +}; diff --git a/src/components/reminder.ts b/src/components/reminder/reminder.ts similarity index 50% rename from src/components/reminder.ts rename to src/components/reminder/reminder.ts index e5ea2ff8..8451b4bc 100644 --- a/src/components/reminder.ts +++ b/src/components/reminder/reminder.ts @@ -1,6 +1,5 @@ import _ from 'lodash'; -import { openDB } from './db'; - +import { openDB } from '../db'; export enum Status { Active, @@ -8,29 +7,46 @@ export enum Status { } export interface Reminder { - userId: string, - created_at: string, - reminder_at: string, - message: string + userId: string; + created_at: string; + reminder_at: string; + message: string; } +// Returns a list of reminders that are due now + +export const getDueReminders = async (): Promise => { + const db = await openDB(); + const now = new Date().toISOString(); + return await db.all( + 'SELECT * FROM reminders WHERE reminder_at <= ? AND status = 0', + now + ); +}; /* Returns a list of reminders by userID. */ -export const getReminders = async ( - userId: string -): Promise => { +export const getReminders = async (userId: string): Promise => { const db = await openDB(); - console.log(`Received request for user_id ${userId}`) + console.log(`Received request for user_id ${userId}`); return await db.all('SELECT * FROM reminders WHERE user_id = ?', userId); }; +// Mark reminder as sent +export const markReminderAsSent = async (reminderId: number): Promise => { + const db = await openDB(); + await db.run('UPDATE reminders SET status = 1 WHERE id = ?', reminderId); +}; // Adds a reminder to the DB export const addReminder = async ( -userId: string, created_at: string, reminder_at: string, message: string): Promise => { + userId: string, + created_at: string, + reminder_at: string, + message: string, +): Promise => { const db = await openDB(); - + console.log('addReminder parameters:'); console.log('userId:', userId); console.log('created_at:', created_at); @@ -47,6 +63,6 @@ userId: string, created_at: string, reminder_at: string, message: string): Promi created_at, reminder_at, message, - 0 + 0, ); }; diff --git a/src/components/reminder/reminderObserver.ts b/src/components/reminder/reminderObserver.ts new file mode 100644 index 00000000..b0bf0ec5 --- /dev/null +++ b/src/components/reminder/reminderObserver.ts @@ -0,0 +1,69 @@ +import { EventEmitter } from 'events'; +import { Client } from 'discord.js'; +import * as reminderComponents from './reminder'; + +export class ReminderObserver extends EventEmitter { + private client: Client; + private scheduledTimeouts: Map = new Map(); + private globalCheckInterval: NodeJS.Timeout | null = null; + + constructor(client: Client) { + super(); + this.client = client; + this.setupEventListeners(); + } + + private setupEventListeners() { + this.on('reminderDue', async (reminder) => { + console.log(`🔔 Processing due reminder: ${reminder.message}`); + await this.sendReminder(reminder); + await reminderComponents.markReminderAsSent(reminder.id); + }); + } + + async start() { + console.log('🔔 Reminder Observer started'); + this.globalCheckInterval = setInterval(async () => { + await this.checkForDueReminders(); + }, 60 * 1000); + await this.checkForDueReminders(); + } + + private async checkForDueReminders() { + try { + const dueReminders = await reminderComponents.getDueReminders(); + if (dueReminders.length > 0) { + console.log(`📬 Found ${dueReminders.length} due reminder(s)`); + dueReminders.forEach((reminder: any) => { + this.emit('reminderDue', reminder); // emits event for observers + }); + } + } catch (error) { + console.error('Error checking due reminders:', error); + } + } + + private async sendReminder(reminder: any) { + try { + const user = await this.client.users.fetch(reminder.user_id); + if (user) { + await user.send({ + embeds: [ + { + title: '🔔 Reminder!', + description: reminder.message, + color: 0x00ff00, + timestamp: new Date().toISOString(), + footer: { + text: `Set on ${new Date(reminder.created_at).toLocaleDateString()}`, + }, + }, + ], + }); + console.log(`Sent reminder to ${user.username}: ${reminder.message}`); + } + } catch (error) { + console.error(`Failed to send reminder to user ${reminder.user_id}:`, error); + } + } +} diff --git a/src/events/ready.ts b/src/events/ready.ts index 6391cb20..988aaee7 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -6,6 +6,7 @@ import { vars } from '../config'; import { logger } from '../logger/default'; import { getRepositoryReleases } from '../utils/github'; import { updateWiki } from '../utils/updateWiki'; +import { ReminderObserver } from '../components/reminder/reminderObserver'; const dev = process.env.NODE_ENV === 'dev'; @@ -46,5 +47,9 @@ export const initReady = (client: Client): void => { sendReady(client); initCrons(client); initEmojis(client); + + let reminderObserver = new ReminderObserver(client); + reminderObserver.start(); + if (dev) updateWiki(); // will not run in staging/prod }; From 21419f45fe74be7a07bafde3fc5ccd62d008e842 Mon Sep 17 00:00:00 2001 From: Michael Xie Date: Wed, 1 Oct 2025 23:00:44 -0400 Subject: [PATCH 05/11] switched timer to use reminder system --- docs/COMMAND-WIKI.md | 198 +++++++++++---------------- src/commandDetails/reminder/timer.ts | 64 ++++----- 2 files changed, 106 insertions(+), 156 deletions(-) diff --git a/docs/COMMAND-WIKI.md b/docs/COMMAND-WIKI.md index a3b3b1c6..87d311b2 100644 --- a/docs/COMMAND-WIKI.md +++ b/docs/COMMAND-WIKI.md @@ -1,4 +1,15 @@ -# LEETCODE +# ADMIN +## ban +- **Aliases:** None +- **Description:** Ban a user. +- **Examples:**
`.ban @jeff spam` +- **Options:** + - ``user``: The user to ban. + - ``reason``: The reason why we are banning the user. +- **Subcommands:** None + +# COIN +## coin - **Aliases:** None - **Description:** Handle coin functions. - **Examples:**
`.coin adjust @Codey 100`
`.coin adjust @Codey -100 Codey broke.`
`.coin`
`.coin check @Codey`
`.coin c @Codey`
`.coin info`
`.coin i`
`.coin update @Codey 100`
`.coin update @Codey 0 Reset Codey's balance.`
`.coin transfer @Codey 10`
`.coin transfer @Codey 15 Lost a bet to Codey ` @@ -12,16 +23,12 @@ - **Options:** - ``user``: The user to adjust the balance of. - ``amount``: The amount to adjust the balance of the specified user by. -## leetcode -- **Aliases:** None -- **Description:** Handle LeetCode functions. -- -- **Options:** None -- **Subcommands:** `random`, `specific` + - ``reason``: The reason why we are adjusting the balance. +- **Subcommands:** None -## leetcode random -- **Aliases:** `r` - the balance of. +## coin check +- **Aliases:** `c`, `b`, `balance`, `bal` +- **Description:** The user to check the balance of. - **Examples:**
`.coin check @Codey`
`.coin c @Codey` - **Options:** - ``user``: The user to check the balance of. @@ -42,15 +49,11 @@ - **Subcommands:** None ## coin transfer -- **Description:** Get a random LeetCode problem. -- **Examples:**
`.leetcode`n
`.leetcode random` +- **Aliases:** `t` +- **Description:** Transfer coins from your balance to another user. +- **Examples:**
`.coin transfer @Codey 10`
`.coin transfer @Codey 10 Lost a bet to @Codey` - **Options:** - - ``difficulty``: The difficulty of the problem. -- **Subcommands:** None - -## leetcode specific -- **Aliases:** `spec`, `s` - to. + - ``user``: The user to transfer coins to. - ``amount``: The amount to transfer to the specified user. - ``reason``: The reason for transferring. - **Subcommands:** None @@ -73,14 +76,10 @@ - **Options:** None - **Subcommands:** `enroll`, `add`, `remove`, `find`, `profile` -- **Description:** Get a LeetCode problem with specified problem ID. ## company add -- **Examples:**
`.leetcode specific 1` -- **Options:** - - ``problem-id``: The problem ID. -- **Subcommands:** None - -chbase.com/organization/microsoft`
`.company a microsoft ` +- **Aliases:** `a` +- **Description:** Add a company to your profile +- **Examples:**
`.company add https://www.crunchbase.com/organization/microsoft`
`.company a microsoft ` - **Options:** - **Subcommands:** None @@ -94,10 +93,13 @@ chbase.com/organization/microsoft`
`.company a microsoft ` ## company find - **Aliases:** `f` - **Description:** Find all individuals that work at the company. -# MISCELLANEOUS -## help -- **Aliases:** `wiki` - the companies you are associated with +- **Examples:**
`.company find https://www.crunchbase.com/organization/microsoft`
`.company f microsoft` +- **Options:** +- **Subcommands:** None + +## company profile +- **Aliases:** `p` +- **Description:** List all the companies you are associated with - **Examples:**
`.company profile`
`.company p` - **Options:** None - **Subcommands:** None @@ -111,15 +113,13 @@ chbase.com/organization/microsoft`
`.company a microsoft ` # FUN ## flipcoin -- **Description:** Get the URL to the wiki page. -- **Examples:**
`.help`
`.wiki` +- **Aliases:** `fc`, `flip`, `flip-coin`, `coin-flip`, `coinflip` +- **Description:** None +- **Examples:**
`.flip-coin`
`.fc`
`.flip`
`.coin-flip`
`.coinflip`
`.flipcoin` - **Options:** None - **Subcommands:** None -## info -- **Aliases:** None -- **Description:** Get Codey information - app version, repository link and issue templates. -ice +## rolldice - **Aliases:** `rd`, `roll`, `roll-dice`, `dice-roll`, `diceroll`, `dice` - **Description:** Roll a dice! :game_die: - **Examples:**
`.roll-dice 6`
`.dice-roll 30`
`.roll 100`
`.rd 4`
`.diceroll 2`
`.dice 1`
`.rolldice 10` @@ -132,21 +132,20 @@ ice - **Aliases:** `blj`, `blackjack`, `21` - **Description:** Play a Blackjack game to win some Codey coins! - **Examples:**
`.bj 100`
`.blj 100` -- **Examples:**
`.info` -- **Options:** None +- **Options:** + - ``bet``: A valid bet amount - **Subcommands:** None -## member +## connect4 - **Aliases:** None -- **Description:** Get CSC membership information of a user. -- **Examples:**
`.member [id]` -- **Options:** - - ``uwid``: The Quest ID of the user. +- **Description:** Play Connect 4! +- **Examples:**
`.connect4`
`.connect 4 @user` +- **Options:** None - **Subcommands:** None -## ping -- **Aliases:** `pong` -per, Scissors! +## rps +- **Aliases:** None +- **Description:** Play Rock, Paper, Scissors! - **Examples:**
`.rps`
`.rps 10` - **Options:** - ``bet``: How much to bet - default is 10. @@ -181,14 +180,12 @@ per, Scissors! - **Examples:**
`.interviewer list`
`.interviewer list backend` - **Options:** - ``domain``: The domain to be examined -- **Description:** Ping the bot to see if it is alive. :ping_pong: -- **Examples:**
`.ping`
`.pong` -- **Options:** None - **Subcommands:** None -## uptime -- **Aliases:** `up`, `timeup` -e` +## interviewer pause +- **Aliases:** `ps` +- **Description:** Put your interviewer profile on pause +- **Examples:**
`.interviewer pause` - **Options:** None - **Subcommands:** None @@ -213,18 +210,21 @@ e` - **Options:** - ``calendar_url``: A valid calendly.com or x.ai calendar link - **Subcommands:** None -- **Examples:**
`.uptime`
`.up`
`.timeup` -- **Options:** None -- **Subcommands:** None -on:** Handle LeetCode functions. +# LEETCODE +## leetcode +- **Aliases:** None +- **Description:** Handle LeetCode functions. - - **Options:** None - **Subcommands:** `random`, `specific` -# PROFILE -## profile - The difficulty of the problem. +## leetcode random +- **Aliases:** `r` +- **Description:** Get a random LeetCode problem. +- **Examples:**
`.leetcode`n
`.leetcode random` +- **Options:** + - ``difficulty``: The difficulty of the problem. - **Subcommands:** None ## leetcode specific @@ -236,13 +236,15 @@ on:** Handle LeetCode functions. - **Subcommands:** None # MISCELLANEOUS -- **Aliases:** `userprofile`, `aboutme` -- **Description:** Handle user profile functions. -- **Examples:**
`.profile @Codey` +## help +- **Aliases:** `wiki` +- **Description:** Get the URL to the wiki page. +- **Examples:**
`.help`
`.wiki` - **Options:** None -- **Subcommands:** `about`, `grad`, `set` +- **Subcommands:** None -** None +## info +- **Aliases:** None - **Description:** Get Codey information - app version, repository link and issue templates. - **Examples:**
`.info` - **Options:** None @@ -259,19 +261,21 @@ on:** Handle LeetCode functions. ## ping - **Aliases:** `pong` - **Description:** Ping the bot to see if it is alive. :ping_pong: -## profile about -- **Aliases:** `a` -- **Description:** Display user profile. -- **Examples:**
`.profile about @Codey`
`.profile a @Codey` -- **Options:** - - ``user``: The user to give profile of. +- **Examples:**
`.ping`
`.pong` +- **Options:** None - **Subcommands:** None -## profile grad -- **Aliases:** `g` -- **Description:** Update Grad Roles. -- **Examples:**
`.profile grad`
`.profile g` -functions. +## uptime +- **Aliases:** `up`, `timeup` +- **Description:** None +- **Examples:**
`.uptime`
`.up`
`.timeup` +- **Options:** None +- **Subcommands:** None + +# PROFILE +## profile +- **Aliases:** `userprofile`, `aboutme` +- **Description:** Handle user profile functions. - **Examples:**
`.profile @Codey` - **Options:** None - **Subcommands:** `about`, `grad`, `set` @@ -300,48 +304,6 @@ functions. - ``description``: The description of the customization to be set for the user. - **Subcommands:** None -- **Options:** None -# REMINDER -- **Subcommands:** None - -## reminder -- **Aliases:** None -- **Description:** Set up a reminder for anything you want! -- **Examples:**
`/reminder` - Opens an interactive reminder setup form -## profile set -- **Aliases:** `s` -- **Description:** Set parameters of user profile. -- **Examples:**
`.profile set @Codey`
`.profile a @Codey` -- **Options:** - - ``customization``: The customization to be set for the user. - - ``description``: The description of the customization to be set for the user. -value minutes:5`
`/timer value hours:2 minutes:30`
`/timer value minutes:10 message:Take a break!` -- **Options:** None -- **Subcommands:** `value`, `name`, `description`, `executeCommand`, `isCommandResponseEphemeral`, `options`, `name`, `description`, `required`, `type` - -# SUGGESTION -## suggestion -- **Aliases:** ``suggest`` -- **Description:** Handle suggestion functions. -- This command will forward a suggestion to the CSC Discord Mods. Please note that your suggestion is not anonymous, your Discord username and ID will be recorded. If you don't want to make a suggestion in public, you could use this command via a DM to Codey instead. - **Examples:** - ``.suggestion I want a new Discord channel named #hobbies.`` -- **Options:** - - ``details``: Details of your suggestion -- **Subcommands:** ``list``, ``update``, ``create`` - -# COFFEE CHAT -## coffee -- **Aliases:** None -- **Description:** Handle coffee chat functions. -- **Examples:** - ``.coffee match`` - ``.coffee test 10`` -- **Options:** None -- **Subcommands:** ``match``, ``test`` - -- **Subcommands:** None - # REMINDER ## reminder - **Aliases:** None @@ -353,9 +315,9 @@ value minutes:5`
`/timer value hours:2 minutes:30`
`/timer value minut ## timer - **Aliases:** None - **Description:** Set up a timer for anything you want! -- **Examples:**
`/timer value seconds:30`
`/timer value minutes:5`
`/timer value hours:2 minutes:30`
`/timer value minutes:10 message:Take a break!` +- **Examples:**
`/timer value minutes:5`
`/timer value hours:2 minutes:30`
`/timer value minutes:10 message:Take a break!` - **Options:** None -- **Subcommands:** `value`, `name`, `description`, `executeCommand`, `isCommandResponseEphemeral`, `options`, `name`, `description`, `required`, `type` +- **Subcommands:** `create`, `name`, `description`, `executeCommand`, `isCommandResponseEphemeral`, `options`, `name`, `description`, `required`, `type` # SUGGESTION ## suggestion diff --git a/src/commandDetails/reminder/timer.ts b/src/commandDetails/reminder/timer.ts index 56b20072..2dbf1ceb 100644 --- a/src/commandDetails/reminder/timer.ts +++ b/src/commandDetails/reminder/timer.ts @@ -5,9 +5,9 @@ import { SapphireMessageExecuteType, SapphireMessageResponse, } from '../../codeyCommand'; +import * as reminderComponents from '../../components/reminder/reminder'; const TIME_UNITS = { - seconds: { multiplier: 1, singular: 'second', plural: 'seconds' }, minutes: { multiplier: 60, singular: 'minute', plural: 'minutes' }, hours: { multiplier: 3600, singular: 'hour', plural: 'hours' }, days: { multiplier: 86400, singular: 'day', plural: 'days' }, @@ -23,7 +23,7 @@ const timerExecuteCommand: SapphireMessageExecuteType = ( return Promise.resolve('Please use a subcommand: `/timer value`'); // Fixed from 'set' to 'value' }; -const timerSetExecuteCommand: SapphireMessageExecuteType = ( +const timerSetExecuteCommand: SapphireMessageExecuteType = async ( _client, messageFromUser, args, @@ -58,34 +58,30 @@ const timerSetExecuteCommand: SapphireMessageExecuteType = ( }) .join(', '); - // Handle both Message and ChatInputCommandInteraction const user = 'author' in messageFromUser ? messageFromUser.author : messageFromUser.user; - const channel = messageFromUser.channel; - - setTimeout(async () => { - try { - // Try to DM first - await user.send(`⏰ **Timer Reminder:** ${reminderMessage}`); - } catch (dmError) { - // If DM fails, send a temporary message in channel - if (channel && 'send' in channel) { - try { - const fallbackMsg = await channel.send( - `<@${user.id}> ⏰ **Timer Reminder:** ${reminderMessage} (couldn't DM you!)` - ); - // delete after 30 seconds to remove spam - setTimeout(() => { - fallbackMsg.delete().catch(() => {}); // Ignore if already deleted - }, 30000); - } catch (channelError) { - console.error('Failed to send timer reminder:', channelError); - } - } - } - }, totalSeconds * 1000); - const content = `Timer set for ${timeDescription}! I'll dm you with: "${reminderMessage}"`; - return Promise.resolve(content); + // Calculate the future date/time when the timer should trigger + const now = new Date(); + const futureDateTime = new Date(now.getTime() + totalSeconds * 1000); + + try { + // Save the timer as a reminder in the database + await reminderComponents.addReminder( + user.id, + now.toISOString(), + futureDateTime.toISOString(), + `⏰ **Timer:** ${reminderMessage}`, + ); + + const content = `⏰ Timer set for ${timeDescription}! I'll DM you with: "${reminderMessage}" + +📅 **Scheduled for:** `; + + return Promise.resolve(content); + } catch (error) { + console.error('Failed to save timer reminder:', error); + return Promise.resolve('Failed to set timer. Please try again.'); + } }; export const timerCommandDetails: CodeyCommandDetails = { @@ -93,7 +89,6 @@ export const timerCommandDetails: CodeyCommandDetails = { aliases: [], description: 'Set up a timer for anything you want!', detailedDescription: `**Examples:** - \`/timer value seconds:30\` \`/timer value minutes:5\` \`/timer value hours:2 minutes:30\` \`/timer value minutes:10 message:Take a break!\``, @@ -103,19 +98,12 @@ export const timerCommandDetails: CodeyCommandDetails = { messageIfFailure: 'Failed to set up a timer.', options: [], subcommandDetails: { - value: { - name: 'value', + create: { + name: 'create', description: 'Set a timer with specified duration', executeCommand: timerSetExecuteCommand, isCommandResponseEphemeral: true, options: [ - { - name: 'seconds', - description: 'Number of seconds', - required: false, - - type: CodeyCommandOptionType.INTEGER, - }, { name: 'minutes', description: 'Number of minutes', From 47a028ad5dd77d7381d4c05bdefe98446dec2443 Mon Sep 17 00:00:00 2001 From: Michael Xie Date: Thu, 2 Oct 2025 11:04:42 -0400 Subject: [PATCH 06/11] shared view between reminder and timer --- src/commandDetails/reminder/reminder.ts | 66 +--------------- src/commandDetails/reminder/sharedViews.ts | 84 +++++++++++++++++++++ src/commandDetails/reminder/timer.ts | 34 +++++++-- src/components/db.ts | 1 + src/components/reminder/reminder.ts | 34 ++++++--- src/components/reminder/reminderObserver.ts | 2 +- 6 files changed, 142 insertions(+), 79 deletions(-) create mode 100644 src/commandDetails/reminder/sharedViews.ts diff --git a/src/commandDetails/reminder/reminder.ts b/src/commandDetails/reminder/reminder.ts index 862249a2..c1ca4350 100644 --- a/src/commandDetails/reminder/reminder.ts +++ b/src/commandDetails/reminder/reminder.ts @@ -20,6 +20,7 @@ import { TextInputStyle, } from 'discord.js'; import * as reminderComponents from '../../components/reminder/reminder'; +import { genericViewResponse } from './sharedViews'; const getUser = (messageFromUser: Message | ChatInputCommandInteraction) => { return 'user' in messageFromUser ? messageFromUser.user : messageFromUser.author; @@ -158,6 +159,7 @@ const reminderCreateCommand: SapphireMessageExecuteType = async ( } reminderComponents.addReminder( user.id, + true, now.toISOString(), inputDateTime.toISOString(), message, @@ -203,69 +205,9 @@ const reminderViewCommand: SapphireMessageExecuteType = async ( messageFromUser, args, ): Promise => { - const user = getUser(messageFromUser); - let list = await reminderComponents.getReminders(user.id); - console.log('Fetching reminders for user:', user.id); - - if (!list || list.length === 0) { - return '📭 You have no reminders set.'; - } - - let response = '📝 **Your Reminders:**\n\n'; - - list.forEach((reminder, index) => { - // Parse the ISO date string - const reminderDate = new Date(reminder.reminder_at); - const createdDate = new Date(reminder.created_at); - - // Format dates nicely - const reminderFormatted = reminderDate.toLocaleString('en-US', { - weekday: 'short', - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - timeZoneName: 'short', - }); - - const createdFormatted = createdDate.toLocaleString('en-US', { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); - - // Calculate time until reminder - const now = new Date(); - const timeDiff = reminderDate.getTime() - now.getTime(); - let timeUntil = ''; - - if (timeDiff > 0) { - const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24)); - const hours = Math.floor((timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); - const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60)); - - if (days > 0) { - timeUntil = `⏳ In ${days}d ${hours}h`; - } else if (hours > 0) { - timeUntil = `⏳ In ${hours}h ${minutes}m`; - } else { - timeUntil = `⏳ In ${minutes}m`; - } - } else { - timeUntil = `🔴 **OVERDUE**`; - } - - response += `**${index + 1}.** ${reminder.message}\n`; - response += `📅 **When:** ${reminderFormatted}\n`; - response += `${timeUntil}\n`; - response += `📝 *Created: ${createdFormatted}*\n\n`; - }); - - return response; + let ret = await genericViewResponse(_client, messageFromUser, args) + return ret as SapphireMessageResponse }; -// ...existing code... export const reminderCommandDetails: CodeyCommandDetails = { name: 'reminder', diff --git a/src/commandDetails/reminder/sharedViews.ts b/src/commandDetails/reminder/sharedViews.ts new file mode 100644 index 00000000..1f4257cd --- /dev/null +++ b/src/commandDetails/reminder/sharedViews.ts @@ -0,0 +1,84 @@ +import { SapphireMessageExecuteType, SapphireMessageResponse } from '../../codeyCommand'; +import * as reminderComponents from '../../components/reminder/reminder'; +import { ChatInputCommandInteraction, Client, Message } from 'discord.js'; + +const getUser = (messageFromUser: Message | ChatInputCommandInteraction) => { + return 'user' in messageFromUser ? messageFromUser.user : messageFromUser.author; +}; + +export const genericViewResponse = async ( + _client: Client, + messageFromUser: any, + args: any, + is_reminder: boolean = true, +): Promise => { + const user = getUser(messageFromUser); + + let list = is_reminder + ? await reminderComponents.getReminders(user.id) + : await reminderComponents.getTimers(user.id); + + const itemType = is_reminder ? 'reminders' : 'timers'; + const itemTypeCapitalized = is_reminder ? 'Reminders' : 'Timers'; + const emoji = is_reminder ? '📝' : '⏰'; + + console.log(`Fetching ${itemType} for user:`, user.id); + + if (!list || list.length === 0) { + return `📭 You have no ${itemType} set.`; + } + + let response = `${emoji} **Your ${itemTypeCapitalized}:**\n\n`; + + list.forEach((item, index) => { + // Parse the ISO date string + const reminderDate = new Date(item.reminder_at); + const createdDate = new Date(item.created_at); + + // Format dates nicely + const reminderFormatted = reminderDate.toLocaleString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + timeZoneName: 'short', + }); + + const createdFormatted = createdDate.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); + + // Calculate time until reminder/timer + const now = new Date(); + const timeDiff = reminderDate.getTime() - now.getTime(); + let timeUntil = ''; + + if (timeDiff > 0) { + const days = Math.floor(timeDiff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((timeDiff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60)); + + if (days > 0) { + timeUntil = `⏳ In ${days}d ${hours}h`; + } else if (hours > 0) { + timeUntil = `⏳ In ${hours}h ${minutes}m`; + } else { + timeUntil = `⏳ In ${minutes}m`; + } + } else { + timeUntil = `🔴 **OVERDUE**`; + } + + response += `**${index + 1}.** ${item.message}\n`; + response += `📅 **When:** ${reminderFormatted}\n`; + response += `${timeUntil}\n`; + response += `📝 *Created: ${createdFormatted}*\n\n`; + }); + + return response; +}; diff --git a/src/commandDetails/reminder/timer.ts b/src/commandDetails/reminder/timer.ts index 2dbf1ceb..c061b59a 100644 --- a/src/commandDetails/reminder/timer.ts +++ b/src/commandDetails/reminder/timer.ts @@ -6,6 +6,8 @@ import { SapphireMessageResponse, } from '../../codeyCommand'; import * as reminderComponents from '../../components/reminder/reminder'; +import { ChatInputCommandInteraction, Message } from 'discord.js'; +import { genericViewResponse } from './sharedViews'; const TIME_UNITS = { minutes: { multiplier: 60, singular: 'minute', plural: 'minutes' }, @@ -15,12 +17,12 @@ const TIME_UNITS = { type TimeUnit = keyof typeof TIME_UNITS; -const timerExecuteCommand: SapphireMessageExecuteType = ( +const timerExecuteCommand: SapphireMessageExecuteType = async ( _client, - _messageFromUser, - _args, + messageFromUser, + args, ): Promise => { - return Promise.resolve('Please use a subcommand: `/timer value`'); // Fixed from 'set' to 'value' + return Promise.resolve('Please use a subcommand: `/timer create`'); }; const timerSetExecuteCommand: SapphireMessageExecuteType = async ( @@ -68,9 +70,10 @@ const timerSetExecuteCommand: SapphireMessageExecuteType = async ( // Save the timer as a reminder in the database await reminderComponents.addReminder( user.id, + false, now.toISOString(), futureDateTime.toISOString(), - `⏰ **Timer:** ${reminderMessage}`, + `${reminderMessage}`, ); const content = `⏰ Timer set for ${timeDescription}! I'll DM you with: "${reminderMessage}" @@ -84,6 +87,15 @@ const timerSetExecuteCommand: SapphireMessageExecuteType = async ( } }; +const timerViewCommand: SapphireMessageExecuteType = async ( + _client, + messageFromUser, + args, +): Promise => { + let ret = await genericViewResponse(_client, messageFromUser, args, false); + return ret as SapphireMessageResponse; +}; + export const timerCommandDetails: CodeyCommandDetails = { name: 'timer', aliases: [], @@ -131,7 +143,17 @@ export const timerCommandDetails: CodeyCommandDetails = { ], aliases: [], detailedDescription: - 'Set a timer by specifying seconds, minutes, hours, and/or days with an optional message', + 'Set a timer by specifying minutes, hours, and/or days with an optional message', + subcommandDetails: {}, + }, + view: { + name: 'view', + description: 'View any timers you set!', + executeCommand: timerViewCommand, + isCommandResponseEphemeral: true, + options: [], + aliases: [], + detailedDescription: '', subcommandDetails: {}, }, }, diff --git a/src/components/db.ts b/src/components/db.ts index af85544c..33b24754 100644 --- a/src/components/db.ts +++ b/src/components/db.ts @@ -226,6 +226,7 @@ const initRemindersTable = async (db: Database): Promise => { await db.run(` CREATE TABLE IF NOT EXISTS reminders ( id INTEGER PRIMARY KEY NOT NULL, + is_reminder BOOLEAN NOT NULL, user_id VARCHAR(255) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, reminder_at TIMESTAMP NOT NULL, diff --git a/src/components/reminder/reminder.ts b/src/components/reminder/reminder.ts index 8451b4bc..fd976dbc 100644 --- a/src/components/reminder/reminder.ts +++ b/src/components/reminder/reminder.ts @@ -7,7 +7,7 @@ export enum Status { } export interface Reminder { - userId: string; + user_id: string; created_at: string; reminder_at: string; message: string; @@ -23,13 +23,20 @@ export const getDueReminders = async (): Promise => { now ); }; -/* - Returns a list of reminders by userID. -*/ + +// Returns a list of reminders that have yet to be shown export const getReminders = async (userId: string): Promise => { const db = await openDB(); console.log(`Received request for user_id ${userId}`); - return await db.all('SELECT * FROM reminders WHERE user_id = ?', userId); + return await db.all('SELECT * FROM reminders WHERE user_id = ? AND status = 0 AND is_reminder = 1', userId); +}; + + +// Returns a list of timers that have yet to be shown +export const getTimers = async (userId: string): Promise => { + const db = await openDB(); + console.log(`Received request for user_id ${userId}`); + return await db.all('SELECT * FROM reminders WHERE user_id = ? AND status = 0 AND is_reminder = 0', userId); }; // Mark reminder as sent @@ -40,7 +47,8 @@ export const markReminderAsSent = async (reminderId: number): Promise => { // Adds a reminder to the DB export const addReminder = async ( - userId: string, + user_id: string, + is_reminder: boolean, created_at: string, reminder_at: string, message: string, @@ -48,18 +56,24 @@ export const addReminder = async ( const db = await openDB(); console.log('addReminder parameters:'); - console.log('userId:', userId); + console.log('userId:', user_id); console.log('created_at:', created_at); console.log('reminder_at:', reminder_at); console.log('message:', message); // Save reminder into DB + // TODO Include type for announcements(user_id can be ignored, since message will need to be pinged in #announcements) + // Status metrics: + // 0: Not pinged + // 1: Pinged + // -1: Error await db.run( ` - INSERT INTO reminders (user_id, created_at, reminder_at, message, status) - VALUES(?,?,?,?,?); + INSERT INTO reminders (user_id, is_reminder, created_at, reminder_at, message, status) + VALUES(?,?,?,?,?,?); `, - userId, + user_id, + is_reminder, created_at, reminder_at, message, diff --git a/src/components/reminder/reminderObserver.ts b/src/components/reminder/reminderObserver.ts index b0bf0ec5..327bd80a 100644 --- a/src/components/reminder/reminderObserver.ts +++ b/src/components/reminder/reminderObserver.ts @@ -50,7 +50,7 @@ export class ReminderObserver extends EventEmitter { await user.send({ embeds: [ { - title: '🔔 Reminder!', + title: reminder.is_reminder? 'Reminder!' : 'Timer', description: reminder.message, color: 0x00ff00, timestamp: new Date().toISOString(), From 5fef1b6ca5d5808e4d80b705c4fbb725a59f06bf Mon Sep 17 00:00:00 2001 From: Michael Xie Date: Sun, 5 Oct 2025 15:43:05 -0400 Subject: [PATCH 07/11] add delete options --- src/codeyCommand.ts | 2 - src/commandDetails/reminder/reminder.ts | 30 +++++- src/commandDetails/reminder/sharedViews.ts | 115 +++++++++++++++++++++ src/commandDetails/reminder/timer.ts | 31 +++++- src/components/reminder/reminder.ts | 18 ++-- 5 files changed, 175 insertions(+), 21 deletions(-) diff --git a/src/codeyCommand.ts b/src/codeyCommand.ts index 3058704a..36edc2c1 100644 --- a/src/codeyCommand.ts +++ b/src/codeyCommand.ts @@ -322,7 +322,6 @@ export class CodeyCommand extends SapphireCommand { interaction: SapphireCommand.ChatInputCommandInteraction, ): Promise | undefined> { const { client } = container; - // Get subcommand name let subcommandName = ''; try { @@ -373,7 +372,6 @@ export class CodeyCommand extends SapphireCommand { ) { successResponse.response = { content: successResponse.response }; } - // cannot double reply to a slash command (in case command replies on its own), runtime error if (!interaction.replied) { await interaction.reply( diff --git a/src/commandDetails/reminder/reminder.ts b/src/commandDetails/reminder/reminder.ts index c1ca4350..5c6a979a 100644 --- a/src/commandDetails/reminder/reminder.ts +++ b/src/commandDetails/reminder/reminder.ts @@ -16,11 +16,12 @@ import { EmbedBuilder, Message, ModalBuilder, + StringSelectMenuBuilder, TextInputBuilder, TextInputStyle, } from 'discord.js'; import * as reminderComponents from '../../components/reminder/reminder'; -import { genericViewResponse } from './sharedViews'; +import { genericDeleteResponse, genericViewResponse } from './sharedViews'; const getUser = (messageFromUser: Message | ChatInputCommandInteraction) => { return 'user' in messageFromUser ? messageFromUser.user : messageFromUser.author; @@ -190,12 +191,14 @@ const reminderCreateCommand: SapphireMessageExecuteType = async ( return ''; }; + // Test command (just returns the user ID for now) const reminderExecuteCommand: SapphireMessageExecuteType = async ( _client, messageFromUser, args, ): Promise => { + console.log('executing here!'); return `User id is ${messageFromUser.client.id}`; }; @@ -205,8 +208,18 @@ const reminderViewCommand: SapphireMessageExecuteType = async ( messageFromUser, args, ): Promise => { - let ret = await genericViewResponse(_client, messageFromUser, args) - return ret as SapphireMessageResponse + let ret = await genericViewResponse(_client, messageFromUser, args); + return ret as SapphireMessageResponse; +}; + +// Delete Reminders +const reminderDeleteCommand: SapphireMessageExecuteType = async ( + _client, + messageFromUser, + args, +): Promise => { + let ret = await genericDeleteResponse(_client, messageFromUser, args); + return ret as SapphireMessageResponse; }; export const reminderCommandDetails: CodeyCommandDetails = { @@ -251,6 +264,17 @@ export const reminderCommandDetails: CodeyCommandDetails = { subcommandDetails: {}, }, + delete: { + name: 'delete', + description: 'Delete one of your active reminders.', + executeCommand: reminderDeleteCommand, + isCommandResponseEphemeral: true, + options: [], + aliases: [], + detailedDescription: 'Choose a reminder to permanently delete it.', + subcommandDetails: {}, + }, + view: { name: 'view', description: 'View any reminders you set!', diff --git a/src/commandDetails/reminder/sharedViews.ts b/src/commandDetails/reminder/sharedViews.ts index 1f4257cd..60c43256 100644 --- a/src/commandDetails/reminder/sharedViews.ts +++ b/src/commandDetails/reminder/sharedViews.ts @@ -1,6 +1,13 @@ import { SapphireMessageExecuteType, SapphireMessageResponse } from '../../codeyCommand'; import * as reminderComponents from '../../components/reminder/reminder'; import { ChatInputCommandInteraction, Client, Message } from 'discord.js'; +import { + ActionRowBuilder, + Colors, + ComponentType, + EmbedBuilder, + StringSelectMenuBuilder, +} from 'discord.js'; const getUser = (messageFromUser: Message | ChatInputCommandInteraction) => { return 'user' in messageFromUser ? messageFromUser.user : messageFromUser.author; @@ -82,3 +89,111 @@ export const genericViewResponse = async ( return response; }; + +// Delete a reminder +export const genericDeleteResponse = async ( + _client: Client, + messageFromUser: any, + args: any, + is_reminder: boolean = true, +): Promise => { + const interaction = messageFromUser as ChatInputCommandInteraction; + const user = getUser(messageFromUser); + console.log(`clicked delete ${is_reminder ? 'reminders' : 'timers'}!`); + + // Immediately defer the reply to prevent timeout + await interaction.deferReply({ ephemeral: true }); + + // Fetch active items for the user (reminders or timers) + const items = is_reminder + ? await reminderComponents.getReminders(user.id) + : await reminderComponents.getTimers(user.id); + + const itemType = is_reminder ? 'reminder' : 'timer'; + const itemTypeCapitalized = is_reminder ? 'Reminder' : 'Timer'; + + console.log(`${itemType}s:`, items); + + if (!items || items.length === 0) { + const errorEmbed = new EmbedBuilder() + .setTitle(`❌ No ${itemTypeCapitalized}s`) + .setDescription(`You do not have any ${itemType}s to delete.`) + .setColor(Colors.Red); + await interaction.editReply({ embeds: [errorEmbed] }); + return ''; + } + + const embed = new EmbedBuilder() + .setTitle(`🗑️ Delete a ${itemTypeCapitalized}`) + .setDescription(`Select a ${itemType} to delete from the dropdown menu below.`) + .setColor(Colors.Red); + + // Create dropdown menu options + const options = items.map((item) => ({ + label: `ID: ${item.id} - "${ + item.message.length > 80 ? item.message.substring(0, 77) + '...' : item.message + }"`, + description: `Due: ${new Date(item.reminder_at).toLocaleString()}`, + value: item.id.toString(), + })); + + const customId = `delete-${itemType}-select`; + const selectMenu = new StringSelectMenuBuilder() + .setCustomId(customId) + .setPlaceholder(`Choose a ${itemType} to delete...`) + .addOptions(options); + + const row = new ActionRowBuilder().addComponents(selectMenu); + + // Send the message with the select menu + const response = await interaction.editReply({ + embeds: [embed], + components: [row], + }); + + try { + const selection = await response.awaitMessageComponent({ + componentType: ComponentType.StringSelect, + filter: (i) => i.user.id === user.id && i.customId === customId, + time: 60000, // 60 seconds timeout + }); + + const itemIdToDelete = parseInt(selection.values[0], 10); + await reminderComponents.deleteReminder(itemIdToDelete, user.id, is_reminder); + + // Confirm deletion + const successEmbed = new EmbedBuilder() + .setTitle(`✅ ${itemTypeCapitalized} Deleted`) + .setDescription(`Successfully deleted ${itemType} with ID \`${itemIdToDelete}\`.`) + .setColor(Colors.Green); + + await selection.update({ embeds: [successEmbed], components: [] }); + } catch (e) { + const timeoutEmbed = new EmbedBuilder() + .setTitle('⏰ Timed Out') + .setDescription('You did not make a selection in time.') + .setColor(Colors.Yellow); + await interaction.editReply({ embeds: [timeoutEmbed], components: [] }); + } + return ''; +}; + +// Test command (just returns the user ID for now) +const reminderExecuteCommand: SapphireMessageExecuteType = async ( + _client, + messageFromUser, + args, +): Promise => { + console.log('executing here!'); + return `User id is ${messageFromUser.client.id}`; +}; + +// View reminders +const reminderViewCommand: SapphireMessageExecuteType = async ( + _client, + messageFromUser, + args, +): Promise => { + let ret = await genericViewResponse(_client, messageFromUser, args); + return ret as SapphireMessageResponse; +}; diff --git a/src/commandDetails/reminder/timer.ts b/src/commandDetails/reminder/timer.ts index c061b59a..60b64188 100644 --- a/src/commandDetails/reminder/timer.ts +++ b/src/commandDetails/reminder/timer.ts @@ -7,7 +7,7 @@ import { } from '../../codeyCommand'; import * as reminderComponents from '../../components/reminder/reminder'; import { ChatInputCommandInteraction, Message } from 'discord.js'; -import { genericViewResponse } from './sharedViews'; +import { genericDeleteResponse, genericViewResponse } from './sharedViews'; const TIME_UNITS = { minutes: { multiplier: 60, singular: 'minute', plural: 'minutes' }, @@ -33,7 +33,6 @@ const timerSetExecuteCommand: SapphireMessageExecuteType = async ( const providedTimes: Array<{ unit: TimeUnit; value: number }> = []; let totalSeconds = 0; - // Check each time unit for (const [unit, config] of Object.entries(TIME_UNITS)) { const value = args[unit] as number; if (value !== undefined && value > 0) { @@ -41,7 +40,7 @@ const timerSetExecuteCommand: SapphireMessageExecuteType = async ( totalSeconds += value * config.multiplier; } } - + console.log('finished checking'); if (providedTimes.length === 0) { return Promise.resolve('Please specify at least one time duration!'); } @@ -66,8 +65,11 @@ const timerSetExecuteCommand: SapphireMessageExecuteType = async ( const now = new Date(); const futureDateTime = new Date(now.getTime() + totalSeconds * 1000); + console.log('finished calculations'); try { // Save the timer as a reminder in the database + + console.log('awaiting reminder'); await reminderComponents.addReminder( user.id, false, @@ -79,7 +81,7 @@ const timerSetExecuteCommand: SapphireMessageExecuteType = async ( const content = `⏰ Timer set for ${timeDescription}! I'll DM you with: "${reminderMessage}" 📅 **Scheduled for:** `; - + console.log('returning!'); return Promise.resolve(content); } catch (error) { console.error('Failed to save timer reminder:', error); @@ -87,6 +89,17 @@ const timerSetExecuteCommand: SapphireMessageExecuteType = async ( } }; +// Delete timers +// Delete Reminders +const timerDeleteCommand: SapphireMessageExecuteType = async ( + _client, + messageFromUser, + args, +): Promise => { + let ret = await genericDeleteResponse(_client, messageFromUser, args, false); + return ret as SapphireMessageResponse; +}; + const timerViewCommand: SapphireMessageExecuteType = async ( _client, messageFromUser, @@ -156,5 +169,15 @@ export const timerCommandDetails: CodeyCommandDetails = { detailedDescription: '', subcommandDetails: {}, }, + delete:{ + name: 'delete', + description: 'Delete any timers you set!', + executeCommand: timerDeleteCommand, + isCommandResponseEphemeral: true, + options: [], + aliases: [], + detailedDescription: '', + subcommandDetails: {}, + } }, }; diff --git a/src/components/reminder/reminder.ts b/src/components/reminder/reminder.ts index fd976dbc..314e6431 100644 --- a/src/components/reminder/reminder.ts +++ b/src/components/reminder/reminder.ts @@ -1,12 +1,7 @@ import _ from 'lodash'; import { openDB } from '../db'; - -export enum Status { - Active, - Paused, -} - export interface Reminder { + id: any; user_id: string; created_at: string; reminder_at: string; @@ -45,6 +40,11 @@ export const markReminderAsSent = async (reminderId: number): Promise => { await db.run('UPDATE reminders SET status = 1 WHERE id = ?', reminderId); }; +export const deleteReminder = async (reminderId: number, userId: string, is_reminder: boolean): Promise => { + const db = await openDB(); + await db.run('DELETE FROM reminders WHERE id = ? AND user_id = ? and is_reminder = ?', reminderId, userId, is_reminder); +}; + // Adds a reminder to the DB export const addReminder = async ( user_id: string, @@ -61,12 +61,6 @@ export const addReminder = async ( console.log('reminder_at:', reminder_at); console.log('message:', message); - // Save reminder into DB - // TODO Include type for announcements(user_id can be ignored, since message will need to be pinged in #announcements) - // Status metrics: - // 0: Not pinged - // 1: Pinged - // -1: Error await db.run( ` INSERT INTO reminders (user_id, is_reminder, created_at, reminder_at, message, status) From bed1fec153b6aaa9d464ce23134937ccce296d99 Mon Sep 17 00:00:00 2001 From: Michael Xie Date: Mon, 6 Oct 2025 00:07:12 -0400 Subject: [PATCH 08/11] announcement activities and observer --- docs/COMMAND-WIKI.md | 7 + src/commandDetails/reminder/announcements.ts | 504 +++++++++++++++++++ src/commandDetails/reminder/timer.ts | 7 +- src/commands/reminder/announcements.ts | 16 + src/components/db.ts | 17 +- src/components/reminder/announcement.ts | 89 ++++ src/components/reminder/reminderObserver.ts | 56 ++- 7 files changed, 687 insertions(+), 9 deletions(-) create mode 100644 src/commandDetails/reminder/announcements.ts create mode 100644 src/commands/reminder/announcements.ts create mode 100644 src/components/reminder/announcement.ts diff --git a/docs/COMMAND-WIKI.md b/docs/COMMAND-WIKI.md index 87d311b2..e4c5e84d 100644 --- a/docs/COMMAND-WIKI.md +++ b/docs/COMMAND-WIKI.md @@ -305,6 +305,13 @@ - **Subcommands:** None # REMINDER +## announcement +- **Aliases:** None +- **Description:** Set up an announcement! +- **Examples:**
`/announcement` - Opens an setup form +- **Options:** None +- **Subcommands:** `create`, `name`, `description`, `executeCommand`, `isCommandResponseEphemeral`, `options`, `name`, `description`, `type`, `required` + ## reminder - **Aliases:** None - **Description:** Set up a reminder for anything you want! diff --git a/src/commandDetails/reminder/announcements.ts b/src/commandDetails/reminder/announcements.ts new file mode 100644 index 00000000..5cb6698f --- /dev/null +++ b/src/commandDetails/reminder/announcements.ts @@ -0,0 +1,504 @@ +import { container } from '@sapphire/framework'; +import { + CodeyCommandDetails, + CodeyCommandOptionType, + SapphireMessageExecuteType, + SapphireMessageResponse, +} from '../../codeyCommand'; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonInteraction, + ButtonStyle, + ChatInputCommandInteraction, + Colors, + ComponentType, + EmbedBuilder, + Message, + ModalBuilder, + StringSelectMenuBuilder, + TextInputBuilder, + TextInputStyle, +} from 'discord.js'; +import * as reminderComponents from '../../components/reminder/reminder'; // TODO Delete +import * as announcementComponents from '../../components/reminder/announcement'; +import { genericDeleteResponse, genericViewResponse } from './sharedViews'; + +const getUser = (messageFromUser: Message | ChatInputCommandInteraction) => { + return 'user' in messageFromUser ? messageFromUser.user : messageFromUser.author; +}; + +// Create a new announcement +const announcementCreateCommand: SapphireMessageExecuteType = async ( + _client, + messageFromUser, + args, +): Promise => { + const user = getUser(messageFromUser); + const interaction = messageFromUser as ChatInputCommandInteraction; + + // Get option values from the command (if provided) + const dateOption = args['date'] as string; + const timeOption = interaction.options.getString('time') || ''; + const messageOption = interaction.options.getString('message') || ''; + + // Create the modal + const modal = new ModalBuilder() + .setCustomId('announcement-modal') + .setTitle('📅 Set Your Announcement'); + + const titleInput = new TextInputBuilder() + .setCustomId('announcement-title') // Changed ID for consistency + .setLabel('Title') + .setPlaceholder('e.g., Codey eats π') + .setStyle(TextInputStyle.Short) + .setRequired(true) + .setMaxLength(100) // Increased max length to be more reasonable + .setMinLength(3); // Decreased min length + + const dateInput = new TextInputBuilder() + .setCustomId('announcement-date') + .setLabel('Date (YYYY-MM-DD)') + .setPlaceholder('e.g., 2025-12-25') + .setStyle(TextInputStyle.Short) + .setRequired(true) + .setMaxLength(10) + .setMinLength(8); + + // Pre-populate date field if provided + if (dateOption) { + dateInput.setValue(dateOption); + } + + const timeInput = new TextInputBuilder() + .setCustomId('announcement-time') + .setLabel('Time (HH:MM) - 24 hour format') + .setPlaceholder('e.g., 14:30 or 09:15') + .setStyle(TextInputStyle.Short) + .setRequired(true) + .setMaxLength(5) + .setMinLength(4); + + // Pre-populate time field if provided + if (timeOption) { + timeInput.setValue(timeOption); + } + + const messageInput = new TextInputBuilder() + .setCustomId('announcement-message') + .setLabel('Announcement Message') + .setPlaceholder('What would you like to announce?') + .setStyle(TextInputStyle.Paragraph) + .setRequired(true) + .setMinLength(1); + + // Pre-populate message field if provided + if (messageOption) { + messageInput.setValue(messageOption); + } + + const photoInput = new TextInputBuilder() + .setCustomId('announcement-photo') // Changed ID for consistency + .setLabel('Photo Embed') + .setPlaceholder('Provide an Embedded Photo Link if you have one.') + .setStyle(TextInputStyle.Paragraph) + .setRequired(false) + .setMaxLength(500); + + // Add title to modal (it was missing) + const titleActionRow = new ActionRowBuilder().addComponents(titleInput); + const dateActionRow = new ActionRowBuilder().addComponents(dateInput); + const timeActionRow = new ActionRowBuilder().addComponents(timeInput); + const messageActionRow = new ActionRowBuilder().addComponents(messageInput); + const photoActionRow = new ActionRowBuilder().addComponents(photoInput); + + modal.addComponents( + titleActionRow, + dateActionRow, + timeActionRow, + messageActionRow, + photoActionRow, + ); + await interaction.showModal(modal); + + try { + const modalSubmit = await interaction.awaitModalSubmit({ + time: 300000, // 5 minutes timeout + filter: (i) => i.customId === 'announcement-modal' && i.user.id === interaction.user.id, + }); + console.log('submitted modal!'); + + // Match these field IDs with the customIds set on the input fields + const title = modalSubmit.fields.getTextInputValue('announcement-title').trim(); + const date = modalSubmit.fields.getTextInputValue('announcement-date').trim(); + const time = modalSubmit.fields.getTextInputValue('announcement-time').trim(); + const message = modalSubmit.fields.getTextInputValue('announcement-message').trim(); + const image_url = modalSubmit.fields.getTextInputValue('announcement-photo').trim(); + + // Basic validation + const dateRegex = /^\d{4}-\d{1,2}-\d{1,2}$/; + const timeRegex = /^\d{1,2}:\d{2}$/; + + let validationErrors = []; + + if (!dateRegex.test(date)) { + validationErrors.push('Date must be in YYYY-MM-DD format'); + } + + if (!timeRegex.test(time)) { + validationErrors.push('Time must be in HH:MM format (24-hour)'); + } + + const dateParts = date.split('-'); + const paddedDate = `${dateParts[0]}-${dateParts[1].padStart(2, '0')}-${dateParts[2].padStart( + 2, + '0', + )}`; + + const timeParts = time.split(':'); + const paddedTime = `${timeParts[0].padStart(2, '0')}:${timeParts[1].padStart(2, '0')}`; + + const inputDateTime = new Date(`${paddedDate}T${paddedTime}:00`); + console.log(inputDateTime); + const now = new Date(); + + if (inputDateTime <= now) { + validationErrors.push('Announcement time must be in the future'); + } + + if (validationErrors.length > 0) { + const errorEmbed = new EmbedBuilder() + .setTitle('❌ Invalid Input') + .setDescription( + `Please fix the following errors:\n${validationErrors + .map((error) => `• ${error}`) + .join('\n')}`, + ) + .setColor(Colors.Red); + + await modalSubmit.reply({ embeds: [errorEmbed], ephemeral: true }); + return ''; + } + + await announcementComponents.addAnnouncement( + user.id, + title, + now.toISOString(), + inputDateTime.toISOString(), + message, + image_url.length > 0 ? image_url : undefined, // Only pass image_url if it's not empty + ); + const formattedTimestamp = ``; + + const previewMessage = [ + `*Scheduled for: ${formattedTimestamp}*`, + "**Here's how the announcement will appear:**", + '', + message, + ].join('\n'); + + await modalSubmit.reply({ + content: `✅ **Announcement created and scheduled!**\n\n${previewMessage}`, + ephemeral: true, + files: image_url.length > 0 ? [image_url] : [], // Attach the image if provided + allowedMentions: { parse: ['users', 'roles'] }, + }); + } catch (error) { + console.log('Modal submission timed out or was cancelled'); + } + return ''; +}; + +const announcementExecuteCommand: SapphireMessageExecuteType = async ( + _client, + messageFromUser, + args, +): Promise => { + return `User id is ${messageFromUser.client.id}`; +}; + +const announcementViewCommand: SapphireMessageExecuteType = async ( + _client, + messageFromUser, + args, +): Promise => { + const interaction = messageFromUser as ChatInputCommandInteraction; + const user = getUser(messageFromUser); + + // Immediately defer the reply to prevent timeout + await interaction.deferReply({ ephemeral: true }); + + // Fetch all announcements for the user + const announcements = await announcementComponents.getAnnouncements(); + + if (!announcements || announcements.length === 0) { + const noAnnouncementsEmbed = new EmbedBuilder() + .setTitle('No Announcements') + .setDescription("You don't have any scheduled announcements.") + .setColor(Colors.Red); + + await interaction.editReply({ embeds: [noAnnouncementsEmbed] }); + return ''; + } + + // Create dropdown options for each announcement + const options = announcements.map((announcement) => ({ + label: + announcement.title.length > 25 + ? announcement.title.substring(0, 22) + '...' + : announcement.title, + description: `Scheduled for ${new Date(announcement.reminder_at).toLocaleString()}`, + value: announcement.id.toString(), + })); + + // Create the select menu + const selectMenu = new StringSelectMenuBuilder() + .setCustomId('announcement-select') + .setPlaceholder('Select an announcement to preview') + .addOptions(options); + + const row = new ActionRowBuilder().addComponents(selectMenu); + + const listEmbed = new EmbedBuilder() + .setTitle('📢 Scheduled Announcements') + .setDescription('Select an announcement from the dropdown to preview how it will look') + .setColor(Colors.Blue) + .addFields({ + name: 'Total Announcements', + value: announcements.length.toString(), + inline: true, + }) + .setTimestamp(); + + const response = await interaction.editReply({ + embeds: [listEmbed], + components: [row], + }); + + // Create collector to wait for selection + const collector = response.createMessageComponentCollector({ + componentType: ComponentType.StringSelect, + filter: (i) => i.user.id === user.id && i.customId === 'announcement-select', + time: 60000, // 1 minute timeout + }); + + collector.on('collect', async (selectInteraction) => { + const announcementId = parseInt(selectInteraction.values[0], 10); + + // Find the selected announcement + const selectedAnnouncement = announcements.find((a) => a.id === announcementId); + + if (!selectedAnnouncement) { + await selectInteraction.update({ + content: 'Error: Could not find the selected announcement', + embeds: [], + components: [], + }); + return; + } + + const formattedTimestamp = ``; + + // Create the preview content similar to how it will be displayed + const previewMessage = [ + selectedAnnouncement.message, + '', + `*(Scheduled to be announced on ${formattedTimestamp})*`, + ].join('\n'); + + // Create button to return to list + const backButton = new ButtonBuilder() + .setCustomId('back-to-list') + .setLabel('Back to List') + .setStyle(ButtonStyle.Secondary); + + const buttonRow = new ActionRowBuilder().addComponents(backButton); + + // Show the announcement preview + await selectInteraction.update({ + content: previewMessage, + embeds: [], + components: [buttonRow], + files: selectedAnnouncement.image_url ? [selectedAnnouncement.image_url] : [], + }); + + // Create collector for the back button + const buttonCollector = response.createMessageComponentCollector({ + componentType: ComponentType.Button, + filter: (i) => i.user.id === user.id && i.customId === 'back-to-list', + time: 60000, // 1 minute timeout + }); + + buttonCollector.on('collect', async (buttonInteraction) => { + await buttonInteraction.update({ + content: null, + embeds: [listEmbed], + components: [row], + files: [], + }); + }); + }); + + collector.on('end', async (collected) => { + if (collected.size === 0) { + // Timeout - remove components + await interaction.editReply({ + content: 'Selection timed out.', + components: [], + }); + } + }); + + return ''; +}; + +const announcementDeleteCommand: SapphireMessageExecuteType = async ( + _client, + messageFromUser, + args, +): Promise => { + const interaction = messageFromUser as ChatInputCommandInteraction; + const user = getUser(messageFromUser); + console.log(`clicked delete announcements!`); + + // Immediately defer the reply to prevent timeout + await interaction.deferReply({ ephemeral: true }); + + // Fetch active announcements for the user + const announcements = await announcementComponents.getAnnouncements(); + + console.log(`announcements:`, announcements); + + if (!announcements || announcements.length === 0) { + const errorEmbed = new EmbedBuilder() + .setTitle(`❌ No Announcements`) + .setDescription(`You do not have any announcements to delete.`) + .setColor(Colors.Red); + await interaction.editReply({ embeds: [errorEmbed] }); + return ''; + } + + const embed = new EmbedBuilder() + .setTitle(`🗑️ Delete an Announcement`) + .setDescription(`Select an announcement to delete from the dropdown menu below.`) + .setColor(Colors.Red); + + // Create dropdown menu options + const options = announcements.map((announcement) => ({ + label: `${ + announcement.title.length > 50 + ? announcement.title.substring(0, 47) + '...' + : announcement.title + }`, + description: `Scheduled for: ${new Date(announcement.reminder_at).toLocaleString()}`, + value: announcement.id.toString(), + })); + + const customId = `delete-announcement-select`; + const selectMenu = new StringSelectMenuBuilder() + .setCustomId(customId) + .setPlaceholder(`Choose an announcement to delete...`) + .addOptions(options); + + const row = new ActionRowBuilder().addComponents(selectMenu); + + // Send the message with the select menu + const response = await interaction.editReply({ + embeds: [embed], + components: [row], + }); + + try { + const selection = await response.awaitMessageComponent({ + componentType: ComponentType.StringSelect, + filter: (i) => i.user.id === user.id && i.customId === customId, + time: 60000, // 60 seconds timeout + }); + + const announcementIdToDelete = parseInt(selection.values[0], 10); + await announcementComponents.deleteAnnouncement(announcementIdToDelete); + + // Confirm deletion + const successEmbed = new EmbedBuilder() + .setTitle(`✅ Announcement Deleted`) + .setDescription(`Successfully deleted announcement.`) + .setColor(Colors.Green); + + await selection.update({ embeds: [successEmbed], components: [] }); + } catch (e) { + const timeoutEmbed = new EmbedBuilder() + .setTitle('⏰ Timed Out') + .setDescription('You did not make a selection in time.') + .setColor(Colors.Yellow); + await interaction.editReply({ embeds: [timeoutEmbed], components: [] }); + } + return ''; +}; + +export const announcementCommandDetails: CodeyCommandDetails = { + name: 'announcement', + aliases: [], + description: 'Set up an announcement!', + detailedDescription: `**Examples:** + \`/announcement\` - Opens an setup form`, + isCommandResponseEphemeral: true, + messageWhenExecutingCommand: 'Setting up the announcement...', + executeCommand: announcementExecuteCommand, + messageIfFailure: 'Failed to set up the announcement', + options: [], + subcommandDetails: { + create: { + name: 'create', + description: 'Set up an announcement with specified duration', + executeCommand: announcementCreateCommand, + isCommandResponseEphemeral: true, + options: [ + { + name: 'date', + description: 'A date in the format YYYY-MM-DD', + type: CodeyCommandOptionType.STRING, + required: false, + }, + { + name: 'time', + description: 'A time in the format HH:DD', + type: CodeyCommandOptionType.STRING, + required: false, + }, + { + name: 'message', + description: 'What you want to announce.', + type: CodeyCommandOptionType.STRING, + required: false, + }, + ], + aliases: [], + detailedDescription: '', + subcommandDetails: {}, + }, + + delete: { + name: 'delete', + description: 'Delete one of the planned announcements.', + executeCommand: announcementDeleteCommand, + isCommandResponseEphemeral: true, + options: [], + aliases: [], + detailedDescription: 'Choose an announccement to permanently delete it.', + subcommandDetails: {}, + }, + + view: { + name: 'view', + description: 'View any announcements that are planned!', + executeCommand: announcementViewCommand, + isCommandResponseEphemeral: true, + options: [], + aliases: [], + detailedDescription: '', + subcommandDetails: {}, + }, + }, +}; diff --git a/src/commandDetails/reminder/timer.ts b/src/commandDetails/reminder/timer.ts index 60b64188..51e12d39 100644 --- a/src/commandDetails/reminder/timer.ts +++ b/src/commandDetails/reminder/timer.ts @@ -81,7 +81,6 @@ const timerSetExecuteCommand: SapphireMessageExecuteType = async ( const content = `⏰ Timer set for ${timeDescription}! I'll DM you with: "${reminderMessage}" 📅 **Scheduled for:** `; - console.log('returning!'); return Promise.resolve(content); } catch (error) { console.error('Failed to save timer reminder:', error); @@ -89,8 +88,6 @@ const timerSetExecuteCommand: SapphireMessageExecuteType = async ( } }; -// Delete timers -// Delete Reminders const timerDeleteCommand: SapphireMessageExecuteType = async ( _client, messageFromUser, @@ -169,7 +166,7 @@ export const timerCommandDetails: CodeyCommandDetails = { detailedDescription: '', subcommandDetails: {}, }, - delete:{ + delete: { name: 'delete', description: 'Delete any timers you set!', executeCommand: timerDeleteCommand, @@ -178,6 +175,6 @@ export const timerCommandDetails: CodeyCommandDetails = { aliases: [], detailedDescription: '', subcommandDetails: {}, - } + }, }, }; diff --git a/src/commands/reminder/announcements.ts b/src/commands/reminder/announcements.ts new file mode 100644 index 00000000..28c8c63c --- /dev/null +++ b/src/commands/reminder/announcements.ts @@ -0,0 +1,16 @@ +import { Command } from '@sapphire/framework'; +import { CodeyCommand } from '../../codeyCommand' +import { announcementCommandDetails } from '../../commandDetails/reminder/announcements'; + +export class AnnouncementCommand extends CodeyCommand { + details = announcementCommandDetails; + + public constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + aliases: announcementCommandDetails.aliases, + description: announcementCommandDetails.description, + detailedDescription: announcementCommandDetails.detailedDescription, + }); + } +} diff --git a/src/components/db.ts b/src/components/db.ts index 33b24754..6c6994cc 100644 --- a/src/components/db.ts +++ b/src/components/db.ts @@ -234,7 +234,21 @@ const initRemindersTable = async (db: Database): Promise => { status INTEGER NOT NULL DEFAULT 0 ) `); - // TODO includ indexing if performance needs it? +} + +const initAnnouncementsTable = async (db: Database): Promise => { + await db.run(` + CREATE TABLE IF NOT EXISTS announcements( + id INTEGER PRIMARY KEY NOT NULL, + user_id VARCHAR(255) NOT NULL, + title TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + reminder_at TIMESTAMP NOT NULL, + message TEXT NOT NULL, + image_url TEXT, + status INTEGER NOT NULL DEFAULT 0 + ) + `); } const initTables = async (db: Database): Promise => { @@ -252,6 +266,7 @@ const initTables = async (db: Database): Promise => { await initCompaniesTable(db); await initPeopleCompaniesTable(db); await initRemindersTable(db); + await initAnnouncementsTable(db); }; export const openDB = async (): Promise => { diff --git a/src/components/reminder/announcement.ts b/src/components/reminder/announcement.ts new file mode 100644 index 00000000..a71c6835 --- /dev/null +++ b/src/components/reminder/announcement.ts @@ -0,0 +1,89 @@ +import _ from 'lodash'; +import { openDB } from '../db'; + +export interface Announcement { + id: any; + user_id: string; + title: string; // Added required title field + created_at: string; + reminder_at: string; + message: string; + image_url?: string; + status: number; +} + +// Returns a list of announcements that are due now +export const getDueAnnouncements = async (): Promise => { + const db = await openDB(); + const now = new Date().toISOString(); + return await db.all('SELECT * FROM announcements WHERE reminder_at <= ? AND status = 0', now); +}; + +// Returns a list of all announcements +export const getAnnouncements = async (): Promise => { + const db = await openDB(); + return await db.all('SELECT * FROM announcements WHERE status = 0'); +}; + +// Mark announcement as sent +export const markAnnouncementAsSent = async (announcementId: number): Promise => { + const db = await openDB(); + await db.run('UPDATE announcements SET status = 1 WHERE id = ?', announcementId); +}; + +// Delete an announcement +export const deleteAnnouncement = async (announcementId: number): Promise => { + const db = await openDB(); + await db.run('DELETE FROM announcements WHERE id = ?', announcementId); +}; + +// Adds an announcement to the DB (with optional image URL) +export const addAnnouncement = async ( + user_id: string, + title: string, + created_at: string, + reminder_at: string, + message: string, + image_url?: string, // Optional parameter +): Promise => { + const db = await openDB(); + if (image_url) { + // With image URL + await db.run( + ` + INSERT INTO announcements (user_id, title, created_at, reminder_at, message, image_url, status) + VALUES(?,?,?,?,?,?,?); + `, + user_id, + title, + created_at, + reminder_at, + message, + image_url, + 0, + ); + } else { + // Without image URL + await db.run( + ` + INSERT INTO announcements (user_id, title, created_at, reminder_at, message, status) + VALUES(?,?,?,?,?,?); + `, + user_id, + title, + created_at, + reminder_at, + message, + 0, + ); + } + console.log("finished announcements") +}; + +// Get an announcement with image data +export const getAnnouncementWithImage = async ( + announcementId: number, +): Promise => { + const db = await openDB(); + return await db.get('SELECT * FROM announcements WHERE id = ?', announcementId); +}; diff --git a/src/components/reminder/reminderObserver.ts b/src/components/reminder/reminderObserver.ts index 327bd80a..bfabb5b7 100644 --- a/src/components/reminder/reminderObserver.ts +++ b/src/components/reminder/reminderObserver.ts @@ -1,10 +1,11 @@ import { EventEmitter } from 'events'; -import { Client } from 'discord.js'; +import { Client, TextChannel } from 'discord.js'; import * as reminderComponents from './reminder'; +import * as announcementComponents from './announcement'; +import config from '../../../config/staging/vars.json'; export class ReminderObserver extends EventEmitter { private client: Client; - private scheduledTimeouts: Map = new Map(); private globalCheckInterval: NodeJS.Timeout | null = null; constructor(client: Client) { @@ -19,14 +20,22 @@ export class ReminderObserver extends EventEmitter { await this.sendReminder(reminder); await reminderComponents.markReminderAsSent(reminder.id); }); + + this.on('announcementDue', async (announcement) => { + console.log(`📢 Processing due announcement: ${announcement.title}`); + await this.sendAnnouncement(announcement); + await announcementComponents.markAnnouncementAsSent(announcement.id); + }); } async start() { console.log('🔔 Reminder Observer started'); this.globalCheckInterval = setInterval(async () => { await this.checkForDueReminders(); + await this.checkForDueAnnouncements(); }, 60 * 1000); await this.checkForDueReminders(); + await this.checkForDueAnnouncements(); } private async checkForDueReminders() { @@ -42,6 +51,19 @@ export class ReminderObserver extends EventEmitter { console.error('Error checking due reminders:', error); } } + private async checkForDueAnnouncements() { + try { + const dueReminders = await announcementComponents.getDueAnnouncements(); + if (dueReminders.length > 0) { + console.log(`📬 Found ${dueReminders.length} due announcement(s)`); + dueReminders.forEach((reminder: any) => { + this.emit('announcementDue', reminder); // emits event for observers + }); + } + } catch (error) { + console.error('Error checking due announcements:', error); + } + } private async sendReminder(reminder: any) { try { @@ -50,7 +72,7 @@ export class ReminderObserver extends EventEmitter { await user.send({ embeds: [ { - title: reminder.is_reminder? 'Reminder!' : 'Timer', + title: reminder.is_reminder ? 'Reminder!' : 'Timer', description: reminder.message, color: 0x00ff00, timestamp: new Date().toISOString(), @@ -66,4 +88,32 @@ export class ReminderObserver extends EventEmitter { console.error(`Failed to send reminder to user ${reminder.user_id}:`, error); } } + + private async sendAnnouncement(announcement: any) { + try { + const guild = this.client.guilds.cache.get(config.TARGET_GUILD_ID); + if (!guild) { + console.error(`Could not find guild with ID ${config.TARGET_GUILD_ID}`); + return; + } + + // Get the announcements channel using its ID from config + const channel = guild.channels.cache.get(config.ANNOUNCEMENTS_CHANNEL_ID) as TextChannel; + if (!channel) { + console.error( + `Could not find announcements channel with ID ${config.ANNOUNCEMENTS_CHANNEL_ID}`, + ); + return; + } + // Send the announcement with proper formatting + await channel.send({ + content: announcement.message, + files: announcement.image_url ? [announcement.image_url] : [], + }); + + console.log(`Sent announcement "${announcement.title}" to #${channel.name}`); + } catch (error) { + console.error(`Failed to send announcement:`, error); + } + } } From a5bd89c51452e066ff8482840f522bb98ab158a9 Mon Sep 17 00:00:00 2001 From: Michael Xie Date: Mon, 6 Oct 2025 22:08:56 -0400 Subject: [PATCH 09/11] cleaned up code --- src/commandDetails/reminder/announcements.ts | 36 +++++++++----------- src/commandDetails/reminder/reminder.ts | 27 +-------------- src/commandDetails/reminder/timer.ts | 1 - src/components/reminder/announcement.ts | 12 ++----- src/components/reminder/reminderObserver.ts | 4 +-- 5 files changed, 21 insertions(+), 59 deletions(-) diff --git a/src/commandDetails/reminder/announcements.ts b/src/commandDetails/reminder/announcements.ts index 5cb6698f..f443ffeb 100644 --- a/src/commandDetails/reminder/announcements.ts +++ b/src/commandDetails/reminder/announcements.ts @@ -8,7 +8,6 @@ import { import { ActionRowBuilder, ButtonBuilder, - ButtonInteraction, ButtonStyle, ChatInputCommandInteraction, Colors, @@ -20,9 +19,8 @@ import { TextInputBuilder, TextInputStyle, } from 'discord.js'; -import * as reminderComponents from '../../components/reminder/reminder'; // TODO Delete + import * as announcementComponents from '../../components/reminder/announcement'; -import { genericDeleteResponse, genericViewResponse } from './sharedViews'; const getUser = (messageFromUser: Message | ChatInputCommandInteraction) => { return 'user' in messageFromUser ? messageFromUser.user : messageFromUser.author; @@ -37,24 +35,23 @@ const announcementCreateCommand: SapphireMessageExecuteType = async ( const user = getUser(messageFromUser); const interaction = messageFromUser as ChatInputCommandInteraction; - // Get option values from the command (if provided) const dateOption = args['date'] as string; const timeOption = interaction.options.getString('time') || ''; const messageOption = interaction.options.getString('message') || ''; + const photoOption = interaction.options.getString('photo') || ''; // Add this line - // Create the modal const modal = new ModalBuilder() .setCustomId('announcement-modal') .setTitle('📅 Set Your Announcement'); const titleInput = new TextInputBuilder() - .setCustomId('announcement-title') // Changed ID for consistency + .setCustomId('announcement-title') .setLabel('Title') .setPlaceholder('e.g., Codey eats π') .setStyle(TextInputStyle.Short) .setRequired(true) - .setMaxLength(100) // Increased max length to be more reasonable - .setMinLength(3); // Decreased min length + .setMaxLength(100) + .setMinLength(3); const dateInput = new TextInputBuilder() .setCustomId('announcement-date') @@ -65,7 +62,6 @@ const announcementCreateCommand: SapphireMessageExecuteType = async ( .setMaxLength(10) .setMinLength(8); - // Pre-populate date field if provided if (dateOption) { dateInput.setValue(dateOption); } @@ -105,7 +101,10 @@ const announcementCreateCommand: SapphireMessageExecuteType = async ( .setRequired(false) .setMaxLength(500); - // Add title to modal (it was missing) + if (photoOption) { + photoInput.setValue(photoOption); + } + const titleActionRow = new ActionRowBuilder().addComponents(titleInput); const dateActionRow = new ActionRowBuilder().addComponents(dateInput); const timeActionRow = new ActionRowBuilder().addComponents(timeInput); @@ -135,7 +134,6 @@ const announcementCreateCommand: SapphireMessageExecuteType = async ( const message = modalSubmit.fields.getTextInputValue('announcement-message').trim(); const image_url = modalSubmit.fields.getTextInputValue('announcement-photo').trim(); - // Basic validation const dateRegex = /^\d{4}-\d{1,2}-\d{1,2}$/; const timeRegex = /^\d{1,2}:\d{2}$/; @@ -251,7 +249,6 @@ const announcementViewCommand: SapphireMessageExecuteType = async ( value: announcement.id.toString(), })); - // Create the select menu const selectMenu = new StringSelectMenuBuilder() .setCustomId('announcement-select') .setPlaceholder('Select an announcement to preview') @@ -275,7 +272,6 @@ const announcementViewCommand: SapphireMessageExecuteType = async ( components: [row], }); - // Create collector to wait for selection const collector = response.createMessageComponentCollector({ componentType: ComponentType.StringSelect, filter: (i) => i.user.id === user.id && i.customId === 'announcement-select', @@ -285,7 +281,6 @@ const announcementViewCommand: SapphireMessageExecuteType = async ( collector.on('collect', async (selectInteraction) => { const announcementId = parseInt(selectInteraction.values[0], 10); - // Find the selected announcement const selectedAnnouncement = announcements.find((a) => a.id === announcementId); if (!selectedAnnouncement) { @@ -308,7 +303,6 @@ const announcementViewCommand: SapphireMessageExecuteType = async ( `*(Scheduled to be announced on ${formattedTimestamp})*`, ].join('\n'); - // Create button to return to list const backButton = new ButtonBuilder() .setCustomId('back-to-list') .setLabel('Back to List') @@ -324,7 +318,6 @@ const announcementViewCommand: SapphireMessageExecuteType = async ( files: selectedAnnouncement.image_url ? [selectedAnnouncement.image_url] : [], }); - // Create collector for the back button const buttonCollector = response.createMessageComponentCollector({ componentType: ComponentType.Button, filter: (i) => i.user.id === user.id && i.customId === 'back-to-list', @@ -363,10 +356,9 @@ const announcementDeleteCommand: SapphireMessageExecuteType = async ( const user = getUser(messageFromUser); console.log(`clicked delete announcements!`); - // Immediately defer the reply to prevent timeout + // Defering to avoid timeout await interaction.deferReply({ ephemeral: true }); - // Fetch active announcements for the user const announcements = await announcementComponents.getAnnouncements(); console.log(`announcements:`, announcements); @@ -385,7 +377,6 @@ const announcementDeleteCommand: SapphireMessageExecuteType = async ( .setDescription(`Select an announcement to delete from the dropdown menu below.`) .setColor(Colors.Red); - // Create dropdown menu options const options = announcements.map((announcement) => ({ label: `${ announcement.title.length > 50 @@ -404,7 +395,6 @@ const announcementDeleteCommand: SapphireMessageExecuteType = async ( const row = new ActionRowBuilder().addComponents(selectMenu); - // Send the message with the select menu const response = await interaction.editReply({ embeds: [embed], components: [row], @@ -473,6 +463,12 @@ export const announcementCommandDetails: CodeyCommandDetails = { type: CodeyCommandOptionType.STRING, required: false, }, + { + name: 'photo', + description: 'URL of an image to include with the announcement', + type: CodeyCommandOptionType.STRING, + required: false, + }, ], aliases: [], detailedDescription: '', diff --git a/src/commandDetails/reminder/reminder.ts b/src/commandDetails/reminder/reminder.ts index 5c6a979a..bd013585 100644 --- a/src/commandDetails/reminder/reminder.ts +++ b/src/commandDetails/reminder/reminder.ts @@ -7,16 +7,11 @@ import { } from '../../codeyCommand'; import { ActionRowBuilder, - ButtonBuilder, - ButtonInteraction, - ButtonStyle, ChatInputCommandInteraction, Colors, - ComponentType, EmbedBuilder, Message, ModalBuilder, - StringSelectMenuBuilder, TextInputBuilder, TextInputStyle, } from 'discord.js'; @@ -35,14 +30,8 @@ const reminderCreateCommand: SapphireMessageExecuteType = async ( ): Promise => { const user = getUser(messageFromUser); - // Check if messageFromUser is actually a ChatInputCommandInteraction (slash command) - const interaction = messageFromUser as ChatInputCommandInteraction; - if (!interaction) { - return 'This command only works with slash commands. Please use `/reminder` instead.'; - } - // Get option values from the command (if provided) const dateOption = args['date'] as string; const timeOption = interaction.options.getString('time') || ''; @@ -94,19 +83,17 @@ const reminderCreateCommand: SapphireMessageExecuteType = async ( messageInput.setValue(messageOption); } - // Add inputs to action rows (Discord modals support up to 5 action rows with 1 text input each) const firstActionRow = new ActionRowBuilder().addComponents(dateInput); const secondActionRow = new ActionRowBuilder().addComponents(timeInput); const thirdActionRow = new ActionRowBuilder().addComponents(messageInput); modal.addComponents(firstActionRow, secondActionRow, thirdActionRow); - // Show the modal to the user await interaction.showModal(modal); try { const modalSubmit = await interaction.awaitModalSubmit({ - time: 300000, // 5 minutes timeout + time: 300000, filter: (i) => i.customId === 'reminder-modal' && i.user.id === interaction.user.id, }); @@ -191,17 +178,6 @@ const reminderCreateCommand: SapphireMessageExecuteType = async ( return ''; }; - -// Test command (just returns the user ID for now) -const reminderExecuteCommand: SapphireMessageExecuteType = async ( - _client, - messageFromUser, - args, -): Promise => { - console.log('executing here!'); - return `User id is ${messageFromUser.client.id}`; -}; - // View reminders const reminderViewCommand: SapphireMessageExecuteType = async ( _client, @@ -230,7 +206,6 @@ export const reminderCommandDetails: CodeyCommandDetails = { \`/reminder\` - Opens an interactive reminder setup form`, isCommandResponseEphemeral: true, messageWhenExecutingCommand: 'Setting up reminder form...', - executeCommand: reminderExecuteCommand, messageIfFailure: 'Failed to set up reminder form.', options: [], subcommandDetails: { diff --git a/src/commandDetails/reminder/timer.ts b/src/commandDetails/reminder/timer.ts index 51e12d39..67154d6a 100644 --- a/src/commandDetails/reminder/timer.ts +++ b/src/commandDetails/reminder/timer.ts @@ -6,7 +6,6 @@ import { SapphireMessageResponse, } from '../../codeyCommand'; import * as reminderComponents from '../../components/reminder/reminder'; -import { ChatInputCommandInteraction, Message } from 'discord.js'; import { genericDeleteResponse, genericViewResponse } from './sharedViews'; const TIME_UNITS = { diff --git a/src/components/reminder/announcement.ts b/src/components/reminder/announcement.ts index a71c6835..05247551 100644 --- a/src/components/reminder/announcement.ts +++ b/src/components/reminder/announcement.ts @@ -4,7 +4,7 @@ import { openDB } from '../db'; export interface Announcement { id: any; user_id: string; - title: string; // Added required title field + title: string; created_at: string; reminder_at: string; message: string; @@ -44,7 +44,7 @@ export const addAnnouncement = async ( created_at: string, reminder_at: string, message: string, - image_url?: string, // Optional parameter + image_url?: string, ): Promise => { const db = await openDB(); if (image_url) { @@ -79,11 +79,3 @@ export const addAnnouncement = async ( } console.log("finished announcements") }; - -// Get an announcement with image data -export const getAnnouncementWithImage = async ( - announcementId: number, -): Promise => { - const db = await openDB(); - return await db.get('SELECT * FROM announcements WHERE id = ?', announcementId); -}; diff --git a/src/components/reminder/reminderObserver.ts b/src/components/reminder/reminderObserver.ts index bfabb5b7..cd29c1a0 100644 --- a/src/components/reminder/reminderObserver.ts +++ b/src/components/reminder/reminderObserver.ts @@ -57,7 +57,7 @@ export class ReminderObserver extends EventEmitter { if (dueReminders.length > 0) { console.log(`📬 Found ${dueReminders.length} due announcement(s)`); dueReminders.forEach((reminder: any) => { - this.emit('announcementDue', reminder); // emits event for observers + this.emit('announcementDue', reminder); }); } } catch (error) { @@ -105,7 +105,7 @@ export class ReminderObserver extends EventEmitter { ); return; } - // Send the announcement with proper formatting + await channel.send({ content: announcement.message, files: announcement.image_url ? [announcement.image_url] : [], From 8c9aabf775b075c47e6f78bab0a12a5021c9f4fc Mon Sep 17 00:00:00 2001 From: Michael Xie Date: Tue, 7 Oct 2025 22:21:43 -0400 Subject: [PATCH 10/11] first draft --- src/commandDetails/reminder/timer.ts | 2 -- src/components/reminder/reminderObserver.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/commandDetails/reminder/timer.ts b/src/commandDetails/reminder/timer.ts index 67154d6a..98d40c24 100644 --- a/src/commandDetails/reminder/timer.ts +++ b/src/commandDetails/reminder/timer.ts @@ -66,8 +66,6 @@ const timerSetExecuteCommand: SapphireMessageExecuteType = async ( console.log('finished calculations'); try { - // Save the timer as a reminder in the database - console.log('awaiting reminder'); await reminderComponents.addReminder( user.id, diff --git a/src/components/reminder/reminderObserver.ts b/src/components/reminder/reminderObserver.ts index cd29c1a0..5e5334cf 100644 --- a/src/components/reminder/reminderObserver.ts +++ b/src/components/reminder/reminderObserver.ts @@ -44,7 +44,7 @@ export class ReminderObserver extends EventEmitter { if (dueReminders.length > 0) { console.log(`📬 Found ${dueReminders.length} due reminder(s)`); dueReminders.forEach((reminder: any) => { - this.emit('reminderDue', reminder); // emits event for observers + this.emit('reminderDue', reminder); }); } } catch (error) { From e299d87d31350a5833b21773f4a10f7f08897220 Mon Sep 17 00:00:00 2001 From: Michael Xie Date: Tue, 7 Oct 2025 22:54:22 -0400 Subject: [PATCH 11/11] fixed linting issues --- src/commandDetails/fun/rollDice.ts | 5 +-- src/commandDetails/reminder/announcements.ts | 16 +++---- src/commandDetails/reminder/reminder.ts | 11 ++--- src/commandDetails/reminder/sharedViews.ts | 44 +++++--------------- src/commandDetails/reminder/timer.ts | 13 ++---- src/commands/reminder/announcements.ts | 2 +- src/components/db.ts | 4 +- src/components/reminder/announcement.ts | 4 +- src/components/reminder/reminder.ts | 41 +++++++++--------- src/components/reminder/reminderObserver.ts | 31 ++++---------- src/events/ready.ts | 4 +- 11 files changed, 61 insertions(+), 114 deletions(-) diff --git a/src/commandDetails/fun/rollDice.ts b/src/commandDetails/fun/rollDice.ts index d66afc7e..73b03630 100644 --- a/src/commandDetails/fun/rollDice.ts +++ b/src/commandDetails/fun/rollDice.ts @@ -5,7 +5,7 @@ import { SapphireMessageExecuteType, SapphireMessageResponse, } from '../../codeyCommand'; -import { getRandomIntFrom1 } from '../../utils/num'; +// import { getRandomIntFrom1 } from '../../utils/num'; const rollDiceExecuteCommand: SapphireMessageExecuteType = ( _client, @@ -22,12 +22,11 @@ const rollDiceExecuteCommand: SapphireMessageExecuteType = ( if (sides > SIDES_UPPER_BOUND) { return new Promise((resolve, _reject) => resolve("that's too many sides!")); } - const diceFace = getRandomIntFrom1(sides); + // const diceFace = getRandomIntFrom1(sides); let userId: string; if ('author' in _messageFromUser) { userId = _messageFromUser.author.id; } else if ('user' in _messageFromUser) { - console.log("using user") userId = _messageFromUser.user.id; } else { userId = 'unknown'; diff --git a/src/commandDetails/reminder/announcements.ts b/src/commandDetails/reminder/announcements.ts index f443ffeb..7c7103ba 100644 --- a/src/commandDetails/reminder/announcements.ts +++ b/src/commandDetails/reminder/announcements.ts @@ -1,4 +1,3 @@ -import { container } from '@sapphire/framework'; import { CodeyCommandDetails, CodeyCommandOptionType, @@ -125,7 +124,6 @@ const announcementCreateCommand: SapphireMessageExecuteType = async ( time: 300000, // 5 minutes timeout filter: (i) => i.customId === 'announcement-modal' && i.user.id === interaction.user.id, }); - console.log('submitted modal!'); // Match these field IDs with the customIds set on the input fields const title = modalSubmit.fields.getTextInputValue('announcement-title').trim(); @@ -137,7 +135,7 @@ const announcementCreateCommand: SapphireMessageExecuteType = async ( const dateRegex = /^\d{4}-\d{1,2}-\d{1,2}$/; const timeRegex = /^\d{1,2}:\d{2}$/; - let validationErrors = []; + const validationErrors = []; if (!dateRegex.test(date)) { validationErrors.push('Date must be in YYYY-MM-DD format'); @@ -157,7 +155,6 @@ const announcementCreateCommand: SapphireMessageExecuteType = async ( const paddedTime = `${timeParts[0].padStart(2, '0')}:${timeParts[1].padStart(2, '0')}`; const inputDateTime = new Date(`${paddedDate}T${paddedTime}:00`); - console.log(inputDateTime); const now = new Date(); if (inputDateTime <= now) { @@ -202,7 +199,7 @@ const announcementCreateCommand: SapphireMessageExecuteType = async ( allowedMentions: { parse: ['users', 'roles'] }, }); } catch (error) { - console.log('Modal submission timed out or was cancelled'); + return 'Error'; } return ''; }; @@ -210,7 +207,7 @@ const announcementCreateCommand: SapphireMessageExecuteType = async ( const announcementExecuteCommand: SapphireMessageExecuteType = async ( _client, messageFromUser, - args, + _args, ): Promise => { return `User id is ${messageFromUser.client.id}`; }; @@ -218,7 +215,7 @@ const announcementExecuteCommand: SapphireMessageExecuteType = async ( const announcementViewCommand: SapphireMessageExecuteType = async ( _client, messageFromUser, - args, + _args, ): Promise => { const interaction = messageFromUser as ChatInputCommandInteraction; const user = getUser(messageFromUser); @@ -350,19 +347,16 @@ const announcementViewCommand: SapphireMessageExecuteType = async ( const announcementDeleteCommand: SapphireMessageExecuteType = async ( _client, messageFromUser, - args, + _args, ): Promise => { const interaction = messageFromUser as ChatInputCommandInteraction; const user = getUser(messageFromUser); - console.log(`clicked delete announcements!`); // Defering to avoid timeout await interaction.deferReply({ ephemeral: true }); const announcements = await announcementComponents.getAnnouncements(); - console.log(`announcements:`, announcements); - if (!announcements || announcements.length === 0) { const errorEmbed = new EmbedBuilder() .setTitle(`❌ No Announcements`) diff --git a/src/commandDetails/reminder/reminder.ts b/src/commandDetails/reminder/reminder.ts index bd013585..377d72b0 100644 --- a/src/commandDetails/reminder/reminder.ts +++ b/src/commandDetails/reminder/reminder.ts @@ -1,4 +1,3 @@ -import { container } from '@sapphire/framework'; import { CodeyCommandDetails, CodeyCommandOptionType, @@ -105,7 +104,7 @@ const reminderCreateCommand: SapphireMessageExecuteType = async ( const dateRegex = /^\d{4}-\d{1,2}-\d{1,2}$/; const timeRegex = /^\d{1,2}:\d{2}$/; - let validationErrors = []; + const validationErrors = []; if (!dateRegex.test(date)) { validationErrors.push('Date must be in YYYY-MM-DD format'); @@ -125,8 +124,6 @@ const reminderCreateCommand: SapphireMessageExecuteType = async ( const paddedTime = `${timeParts[0].padStart(2, '0')}:${timeParts[1].padStart(2, '0')}`; const inputDateTime = new Date(`${paddedDate}T${paddedTime}:00`); - console.log(inputDateTime); - console.log(inputDateTime); const now = new Date(); if (inputDateTime <= now) { @@ -172,7 +169,7 @@ const reminderCreateCommand: SapphireMessageExecuteType = async ( await modalSubmit.reply({ embeds: [outputEmbed], ephemeral: true }); } catch (error) { - console.log('Modal submission timed out or was cancelled'); + // Modal submission timed out or was cancelled } return ''; @@ -184,7 +181,7 @@ const reminderViewCommand: SapphireMessageExecuteType = async ( messageFromUser, args, ): Promise => { - let ret = await genericViewResponse(_client, messageFromUser, args); + const ret = await genericViewResponse(_client, messageFromUser, args); return ret as SapphireMessageResponse; }; @@ -194,7 +191,7 @@ const reminderDeleteCommand: SapphireMessageExecuteType = async ( messageFromUser, args, ): Promise => { - let ret = await genericDeleteResponse(_client, messageFromUser, args); + const ret = await genericDeleteResponse(_client, messageFromUser, args); return ret as SapphireMessageResponse; }; diff --git a/src/commandDetails/reminder/sharedViews.ts b/src/commandDetails/reminder/sharedViews.ts index 60c43256..b91f9642 100644 --- a/src/commandDetails/reminder/sharedViews.ts +++ b/src/commandDetails/reminder/sharedViews.ts @@ -1,6 +1,7 @@ -import { SapphireMessageExecuteType, SapphireMessageResponse } from '../../codeyCommand'; +import { Command } from '@sapphire/framework'; +import { CodeyCommandArguments, SapphireMessageResponse } from '../../codeyCommand'; import * as reminderComponents from '../../components/reminder/reminder'; -import { ChatInputCommandInteraction, Client, Message } from 'discord.js'; +import { CacheType, ChatInputCommandInteraction, Client, Message } from 'discord.js'; import { ActionRowBuilder, Colors, @@ -15,13 +16,13 @@ const getUser = (messageFromUser: Message | ChatInputCommandInteraction) => { export const genericViewResponse = async ( _client: Client, - messageFromUser: any, - args: any, - is_reminder: boolean = true, + messageFromUser: Message | Command.ChatInputCommandInteraction, + _args: CodeyCommandArguments, + is_reminder = true, ): Promise => { const user = getUser(messageFromUser); - let list = is_reminder + const list = is_reminder ? await reminderComponents.getReminders(user.id) : await reminderComponents.getTimers(user.id); @@ -29,8 +30,6 @@ export const genericViewResponse = async ( const itemTypeCapitalized = is_reminder ? 'Reminders' : 'Timers'; const emoji = is_reminder ? '📝' : '⏰'; - console.log(`Fetching ${itemType} for user:`, user.id); - if (!list || list.length === 0) { return `📭 You have no ${itemType} set.`; } @@ -93,13 +92,12 @@ export const genericViewResponse = async ( // Delete a reminder export const genericDeleteResponse = async ( _client: Client, - messageFromUser: any, - args: any, - is_reminder: boolean = true, + messageFromUser: Message | Command.ChatInputCommandInteraction, + _args: CodeyCommandArguments, + is_reminder = true, ): Promise => { const interaction = messageFromUser as ChatInputCommandInteraction; const user = getUser(messageFromUser); - console.log(`clicked delete ${is_reminder ? 'reminders' : 'timers'}!`); // Immediately defer the reply to prevent timeout await interaction.deferReply({ ephemeral: true }); @@ -112,8 +110,6 @@ export const genericDeleteResponse = async ( const itemType = is_reminder ? 'reminder' : 'timer'; const itemTypeCapitalized = is_reminder ? 'Reminder' : 'Timer'; - console.log(`${itemType}s:`, items); - if (!items || items.length === 0) { const errorEmbed = new EmbedBuilder() .setTitle(`❌ No ${itemTypeCapitalized}s`) @@ -177,23 +173,3 @@ export const genericDeleteResponse = async ( } return ''; }; - -// Test command (just returns the user ID for now) -const reminderExecuteCommand: SapphireMessageExecuteType = async ( - _client, - messageFromUser, - args, -): Promise => { - console.log('executing here!'); - return `User id is ${messageFromUser.client.id}`; -}; - -// View reminders -const reminderViewCommand: SapphireMessageExecuteType = async ( - _client, - messageFromUser, - args, -): Promise => { - let ret = await genericViewResponse(_client, messageFromUser, args); - return ret as SapphireMessageResponse; -}; diff --git a/src/commandDetails/reminder/timer.ts b/src/commandDetails/reminder/timer.ts index 98d40c24..e277d3ba 100644 --- a/src/commandDetails/reminder/timer.ts +++ b/src/commandDetails/reminder/timer.ts @@ -1,4 +1,3 @@ -import { container } from '@sapphire/framework'; import { CodeyCommandDetails, CodeyCommandOptionType, @@ -18,8 +17,8 @@ type TimeUnit = keyof typeof TIME_UNITS; const timerExecuteCommand: SapphireMessageExecuteType = async ( _client, - messageFromUser, - args, + _messageFromUser, + _args, ): Promise => { return Promise.resolve('Please use a subcommand: `/timer create`'); }; @@ -39,7 +38,6 @@ const timerSetExecuteCommand: SapphireMessageExecuteType = async ( totalSeconds += value * config.multiplier; } } - console.log('finished checking'); if (providedTimes.length === 0) { return Promise.resolve('Please specify at least one time duration!'); } @@ -64,9 +62,7 @@ const timerSetExecuteCommand: SapphireMessageExecuteType = async ( const now = new Date(); const futureDateTime = new Date(now.getTime() + totalSeconds * 1000); - console.log('finished calculations'); try { - console.log('awaiting reminder'); await reminderComponents.addReminder( user.id, false, @@ -80,7 +76,6 @@ const timerSetExecuteCommand: SapphireMessageExecuteType = async ( 📅 **Scheduled for:** `; return Promise.resolve(content); } catch (error) { - console.error('Failed to save timer reminder:', error); return Promise.resolve('Failed to set timer. Please try again.'); } }; @@ -90,7 +85,7 @@ const timerDeleteCommand: SapphireMessageExecuteType = async ( messageFromUser, args, ): Promise => { - let ret = await genericDeleteResponse(_client, messageFromUser, args, false); + const ret = await genericDeleteResponse(_client, messageFromUser, args, false); return ret as SapphireMessageResponse; }; @@ -99,7 +94,7 @@ const timerViewCommand: SapphireMessageExecuteType = async ( messageFromUser, args, ): Promise => { - let ret = await genericViewResponse(_client, messageFromUser, args, false); + const ret = await genericViewResponse(_client, messageFromUser, args, false); return ret as SapphireMessageResponse; }; diff --git a/src/commands/reminder/announcements.ts b/src/commands/reminder/announcements.ts index 28c8c63c..c9963eae 100644 --- a/src/commands/reminder/announcements.ts +++ b/src/commands/reminder/announcements.ts @@ -1,5 +1,5 @@ import { Command } from '@sapphire/framework'; -import { CodeyCommand } from '../../codeyCommand' +import { CodeyCommand } from '../../codeyCommand'; import { announcementCommandDetails } from '../../commandDetails/reminder/announcements'; export class AnnouncementCommand extends CodeyCommand { diff --git a/src/components/db.ts b/src/components/db.ts index 6c6994cc..0cd7fb5a 100644 --- a/src/components/db.ts +++ b/src/components/db.ts @@ -234,7 +234,7 @@ const initRemindersTable = async (db: Database): Promise => { status INTEGER NOT NULL DEFAULT 0 ) `); -} +}; const initAnnouncementsTable = async (db: Database): Promise => { await db.run(` @@ -249,7 +249,7 @@ const initAnnouncementsTable = async (db: Database): Promise => { status INTEGER NOT NULL DEFAULT 0 ) `); -} +}; const initTables = async (db: Database): Promise => { //initialize all relevant tables diff --git a/src/components/reminder/announcement.ts b/src/components/reminder/announcement.ts index 05247551..03d359e1 100644 --- a/src/components/reminder/announcement.ts +++ b/src/components/reminder/announcement.ts @@ -1,8 +1,7 @@ -import _ from 'lodash'; import { openDB } from '../db'; export interface Announcement { - id: any; + id: number; user_id: string; title: string; created_at: string; @@ -77,5 +76,4 @@ export const addAnnouncement = async ( 0, ); } - console.log("finished announcements") }; diff --git a/src/components/reminder/reminder.ts b/src/components/reminder/reminder.ts index 314e6431..1f27f644 100644 --- a/src/components/reminder/reminder.ts +++ b/src/components/reminder/reminder.ts @@ -1,7 +1,6 @@ -import _ from 'lodash'; import { openDB } from '../db'; export interface Reminder { - id: any; + id: number; user_id: string; created_at: string; reminder_at: string; @@ -13,25 +12,25 @@ export interface Reminder { export const getDueReminders = async (): Promise => { const db = await openDB(); const now = new Date().toISOString(); - return await db.all( - 'SELECT * FROM reminders WHERE reminder_at <= ? AND status = 0', - now - ); + return await db.all('SELECT * FROM reminders WHERE reminder_at <= ? AND status = 0', now); }; // Returns a list of reminders that have yet to be shown export const getReminders = async (userId: string): Promise => { const db = await openDB(); - console.log(`Received request for user_id ${userId}`); - return await db.all('SELECT * FROM reminders WHERE user_id = ? AND status = 0 AND is_reminder = 1', userId); + return await db.all( + 'SELECT * FROM reminders WHERE user_id = ? AND status = 0 AND is_reminder = 1', + userId, + ); }; - // Returns a list of timers that have yet to be shown export const getTimers = async (userId: string): Promise => { const db = await openDB(); - console.log(`Received request for user_id ${userId}`); - return await db.all('SELECT * FROM reminders WHERE user_id = ? AND status = 0 AND is_reminder = 0', userId); + return await db.all( + 'SELECT * FROM reminders WHERE user_id = ? AND status = 0 AND is_reminder = 0', + userId, + ); }; // Mark reminder as sent @@ -40,9 +39,18 @@ export const markReminderAsSent = async (reminderId: number): Promise => { await db.run('UPDATE reminders SET status = 1 WHERE id = ?', reminderId); }; -export const deleteReminder = async (reminderId: number, userId: string, is_reminder: boolean): Promise => { +export const deleteReminder = async ( + reminderId: number, + userId: string, + is_reminder: boolean, +): Promise => { const db = await openDB(); - await db.run('DELETE FROM reminders WHERE id = ? AND user_id = ? and is_reminder = ?', reminderId, userId, is_reminder); + await db.run( + 'DELETE FROM reminders WHERE id = ? AND user_id = ? and is_reminder = ?', + reminderId, + userId, + is_reminder, + ); }; // Adds a reminder to the DB @@ -54,13 +62,6 @@ export const addReminder = async ( message: string, ): Promise => { const db = await openDB(); - - console.log('addReminder parameters:'); - console.log('userId:', user_id); - console.log('created_at:', created_at); - console.log('reminder_at:', reminder_at); - console.log('message:', message); - await db.run( ` INSERT INTO reminders (user_id, is_reminder, created_at, reminder_at, message, status) diff --git a/src/components/reminder/reminderObserver.ts b/src/components/reminder/reminderObserver.ts index 5e5334cf..8ff35719 100644 --- a/src/components/reminder/reminderObserver.ts +++ b/src/components/reminder/reminderObserver.ts @@ -16,20 +16,17 @@ export class ReminderObserver extends EventEmitter { private setupEventListeners() { this.on('reminderDue', async (reminder) => { - console.log(`🔔 Processing due reminder: ${reminder.message}`); await this.sendReminder(reminder); await reminderComponents.markReminderAsSent(reminder.id); }); this.on('announcementDue', async (announcement) => { - console.log(`📢 Processing due announcement: ${announcement.title}`); await this.sendAnnouncement(announcement); await announcementComponents.markAnnouncementAsSent(announcement.id); }); } - async start() { - console.log('🔔 Reminder Observer started'); + async start(): Promise { this.globalCheckInterval = setInterval(async () => { await this.checkForDueReminders(); await this.checkForDueAnnouncements(); @@ -42,29 +39,28 @@ export class ReminderObserver extends EventEmitter { try { const dueReminders = await reminderComponents.getDueReminders(); if (dueReminders.length > 0) { - console.log(`📬 Found ${dueReminders.length} due reminder(s)`); - dueReminders.forEach((reminder: any) => { + dueReminders.forEach((reminder: reminderComponents.Reminder) => { this.emit('reminderDue', reminder); }); } } catch (error) { - console.error('Error checking due reminders:', error); + // Error in checking for due reminders } } private async checkForDueAnnouncements() { try { const dueReminders = await announcementComponents.getDueAnnouncements(); if (dueReminders.length > 0) { - console.log(`📬 Found ${dueReminders.length} due announcement(s)`); - dueReminders.forEach((reminder: any) => { + dueReminders.forEach((reminder: reminderComponents.Reminder) => { this.emit('announcementDue', reminder); }); } } catch (error) { - console.error('Error checking due announcements:', error); + // Error finding announcement } } + // eslint-disable-next-line @typescript-eslint/no-explicit-any private async sendReminder(reminder: any) { try { const user = await this.client.users.fetch(reminder.user_id); @@ -82,27 +78,22 @@ export class ReminderObserver extends EventEmitter { }, ], }); - console.log(`Sent reminder to ${user.username}: ${reminder.message}`); } } catch (error) { - console.error(`Failed to send reminder to user ${reminder.user_id}:`, error); + // Error in send reminder } } - private async sendAnnouncement(announcement: any) { + private async sendAnnouncement(announcement: announcementComponents.Announcement) { try { const guild = this.client.guilds.cache.get(config.TARGET_GUILD_ID); if (!guild) { - console.error(`Could not find guild with ID ${config.TARGET_GUILD_ID}`); return; } // Get the announcements channel using its ID from config const channel = guild.channels.cache.get(config.ANNOUNCEMENTS_CHANNEL_ID) as TextChannel; if (!channel) { - console.error( - `Could not find announcements channel with ID ${config.ANNOUNCEMENTS_CHANNEL_ID}`, - ); return; } @@ -110,10 +101,6 @@ export class ReminderObserver extends EventEmitter { content: announcement.message, files: announcement.image_url ? [announcement.image_url] : [], }); - - console.log(`Sent announcement "${announcement.title}" to #${channel.name}`); - } catch (error) { - console.error(`Failed to send announcement:`, error); - } + } catch (error) {} } } diff --git a/src/events/ready.ts b/src/events/ready.ts index 988aaee7..becd6f58 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -38,7 +38,7 @@ ${line03}${dev ? ` ${pad}${blc('<')}${llc('/')}${blc('>')} ${llc('DEVELOPMENT MO const sendReady = async (client: Client): Promise => { const notif = (await client.channels.fetch(NOTIF_CHANNEL_ID)) as TextChannel; const latestRelease = (await getRepositoryReleases('uwcsc', 'codeybot'))[0]; -// notif.send(`Codey is up! App version: ${latestRelease.tag_name}`); + notif.send(`Codey is up! App version: ${latestRelease.tag_name}`); }; export const initReady = (client: Client): void => { @@ -48,7 +48,7 @@ export const initReady = (client: Client): void => { initCrons(client); initEmojis(client); - let reminderObserver = new ReminderObserver(client); + const reminderObserver = new ReminderObserver(client); reminderObserver.start(); if (dev) updateWiki(); // will not run in staging/prod