diff --git a/docs/COMMAND-WIKI.md b/docs/COMMAND-WIKI.md
index c2cc7eca..e4c5e84d 100644
--- a/docs/COMMAND-WIKI.md
+++ b/docs/COMMAND-WIKI.md
@@ -304,6 +304,28 @@
- ``description``: The description of the customization to be set for the user.
- **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!
+- **Examples:**
`/reminder` - Opens an interactive reminder setup form
+- **Options:** None
+- **Subcommands:** `create`, `name`, `description`, `executeCommand`, `isCommandResponseEphemeral`, `options`, `name`, `description`, `type`, `required`
+
+## timer
+- **Aliases:** None
+- **Description:** Set up a timer for anything you want!
+- **Examples:**
`/timer value minutes:5`
`/timer value hours:2 minutes:30`
`/timer value minutes:10 message:Take a break!`
+- **Options:** None
+- **Subcommands:** `create`, `name`, `description`, `executeCommand`, `isCommandResponseEphemeral`, `options`, `name`, `description`, `required`, `type`
+
# SUGGESTION
## suggestion
- **Aliases:** ``suggest``
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/fun/rollDice.ts b/src/commandDetails/fun/rollDice.ts
index 953b5312..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,8 +22,16 @@ const rollDiceExecuteCommand: SapphireMessageExecuteType = (
if (sides > SIDES_UPPER_BOUND) {
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}!`));
+ // const diceFace = getRandomIntFrom1(sides);
+ let userId: string;
+ if ('author' in _messageFromUser) {
+ userId = _messageFromUser.author.id;
+ } else if ('user' in _messageFromUser) {
+ 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/announcements.ts b/src/commandDetails/reminder/announcements.ts
new file mode 100644
index 00000000..7c7103ba
--- /dev/null
+++ b/src/commandDetails/reminder/announcements.ts
@@ -0,0 +1,494 @@
+import {
+ CodeyCommandDetails,
+ CodeyCommandOptionType,
+ SapphireMessageExecuteType,
+ SapphireMessageResponse,
+} from '../../codeyCommand';
+import {
+ ActionRowBuilder,
+ ButtonBuilder,
+ ButtonStyle,
+ ChatInputCommandInteraction,
+ Colors,
+ ComponentType,
+ EmbedBuilder,
+ Message,
+ ModalBuilder,
+ StringSelectMenuBuilder,
+ TextInputBuilder,
+ TextInputStyle,
+} from 'discord.js';
+
+import * as announcementComponents from '../../components/reminder/announcement';
+
+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;
+
+ 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
+
+ const modal = new ModalBuilder()
+ .setCustomId('announcement-modal')
+ .setTitle('📅 Set Your Announcement');
+
+ const titleInput = new TextInputBuilder()
+ .setCustomId('announcement-title')
+ .setLabel('Title')
+ .setPlaceholder('e.g., Codey eats π')
+ .setStyle(TextInputStyle.Short)
+ .setRequired(true)
+ .setMaxLength(100)
+ .setMinLength(3);
+
+ 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);
+
+ 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);
+
+ if (photoOption) {
+ photoInput.setValue(photoOption);
+ }
+
+ 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,
+ });
+
+ // 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();
+
+ const dateRegex = /^\d{4}-\d{1,2}-\d{1,2}$/;
+ const timeRegex = /^\d{1,2}:\d{2}$/;
+
+ const 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`);
+ 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) {
+ return 'Error';
+ }
+ 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(),
+ }));
+
+ 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],
+ });
+
+ 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);
+
+ 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');
+
+ 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] : [],
+ });
+
+ 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);
+
+ // Defering to avoid timeout
+ await interaction.deferReply({ ephemeral: true });
+
+ const announcements = await announcementComponents.getAnnouncements();
+
+ 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);
+
+ 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);
+
+ 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,
+ },
+ {
+ name: 'photo',
+ description: 'URL of an image to include with the announcement',
+ 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/reminder.ts b/src/commandDetails/reminder/reminder.ts
new file mode 100644
index 00000000..377d72b0
--- /dev/null
+++ b/src/commandDetails/reminder/reminder.ts
@@ -0,0 +1,261 @@
+import {
+ CodeyCommandDetails,
+ CodeyCommandOptionType,
+ SapphireMessageExecuteType,
+ SapphireMessageResponse,
+} from '../../codeyCommand';
+import {
+ ActionRowBuilder,
+ ChatInputCommandInteraction,
+ Colors,
+ EmbedBuilder,
+ Message,
+ ModalBuilder,
+ TextInputBuilder,
+ TextInputStyle,
+} from 'discord.js';
+import * as reminderComponents from '../../components/reminder/reminder';
+import { genericDeleteResponse, genericViewResponse } from './sharedViews';
+
+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);
+
+ 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('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);
+ }
+
+ const firstActionRow = new ActionRowBuilder().addComponents(dateInput);
+ const secondActionRow = new ActionRowBuilder().addComponents(timeInput);
+ const thirdActionRow = new ActionRowBuilder().addComponents(messageInput);
+
+ modal.addComponents(firstActionRow, secondActionRow, thirdActionRow);
+
+ await interaction.showModal(modal);
+
+ try {
+ const modalSubmit = await interaction.awaitModalSubmit({
+ time: 300000,
+ 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}$/;
+
+ const 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`);
+ 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')}`,
+ )
+ .setColor(Colors.Red);
+ await modalSubmit.reply({ embeds: [errorEmbed], ephemeral: true });
+ return '';
+ }
+ reminderComponents.addReminder(
+ user.id,
+ true,
+ now.toISOString(),
+ inputDateTime.toISOString(),
+ message,
+ );
+ // 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) {
+ // Modal submission timed out or was cancelled
+ }
+
+ return '';
+};
+
+// View reminders
+const reminderViewCommand: SapphireMessageExecuteType = async (
+ _client,
+ messageFromUser,
+ args,
+): Promise => {
+ const ret = await genericViewResponse(_client, messageFromUser, args);
+ return ret as SapphireMessageResponse;
+};
+
+// Delete Reminders
+const reminderDeleteCommand: SapphireMessageExecuteType = async (
+ _client,
+ messageFromUser,
+ args,
+): Promise => {
+ const ret = await genericDeleteResponse(_client, messageFromUser, args);
+ return ret as SapphireMessageResponse;
+};
+
+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...',
+ 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: {},
+ },
+
+ 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!',
+ executeCommand: reminderViewCommand,
+ isCommandResponseEphemeral: true,
+ options: [],
+ aliases: [],
+ detailedDescription: '',
+ subcommandDetails: {},
+ },
+ },
+};
diff --git a/src/commandDetails/reminder/sharedViews.ts b/src/commandDetails/reminder/sharedViews.ts
new file mode 100644
index 00000000..b91f9642
--- /dev/null
+++ b/src/commandDetails/reminder/sharedViews.ts
@@ -0,0 +1,175 @@
+import { Command } from '@sapphire/framework';
+import { CodeyCommandArguments, SapphireMessageResponse } from '../../codeyCommand';
+import * as reminderComponents from '../../components/reminder/reminder';
+import { CacheType, 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;
+};
+
+export const genericViewResponse = async (
+ _client: Client,
+ messageFromUser: Message | Command.ChatInputCommandInteraction,
+ _args: CodeyCommandArguments,
+ is_reminder = true,
+): Promise => {
+ const user = getUser(messageFromUser);
+
+ const 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 ? '📝' : '⏰';
+
+ 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;
+};
+
+// Delete a reminder
+export const genericDeleteResponse = async (
+ _client: Client,
+ messageFromUser: Message | Command.ChatInputCommandInteraction,
+ _args: CodeyCommandArguments,
+ is_reminder = true,
+): Promise => {
+ const interaction = messageFromUser as ChatInputCommandInteraction;
+ const user = getUser(messageFromUser);
+
+ // 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';
+
+ 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 '';
+};
diff --git a/src/commandDetails/reminder/timer.ts b/src/commandDetails/reminder/timer.ts
new file mode 100644
index 00000000..e277d3ba
--- /dev/null
+++ b/src/commandDetails/reminder/timer.ts
@@ -0,0 +1,172 @@
+import {
+ CodeyCommandDetails,
+ CodeyCommandOptionType,
+ SapphireMessageExecuteType,
+ SapphireMessageResponse,
+} from '../../codeyCommand';
+import * as reminderComponents from '../../components/reminder/reminder';
+import { genericDeleteResponse, genericViewResponse } from './sharedViews';
+
+const TIME_UNITS = {
+ 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 = async (
+ _client,
+ _messageFromUser,
+ _args,
+): Promise => {
+ return Promise.resolve('Please use a subcommand: `/timer create`');
+};
+
+const timerSetExecuteCommand: SapphireMessageExecuteType = async (
+ _client,
+ messageFromUser,
+ args,
+): Promise => {
+ const providedTimes: Array<{ unit: TimeUnit; value: number }> = [];
+ let totalSeconds = 0;
+
+ 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(', ');
+
+ const user = 'author' in messageFromUser ? messageFromUser.author : messageFromUser.user;
+
+ // Calculate the future date/time when the timer should trigger
+ const now = new Date();
+ const futureDateTime = new Date(now.getTime() + totalSeconds * 1000);
+
+ try {
+ await reminderComponents.addReminder(
+ user.id,
+ false,
+ now.toISOString(),
+ futureDateTime.toISOString(),
+ `${reminderMessage}`,
+ );
+
+ const content = `⏰ Timer set for ${timeDescription}! I'll DM you with: "${reminderMessage}"
+
+📅 **Scheduled for:** `;
+ return Promise.resolve(content);
+ } catch (error) {
+ return Promise.resolve('Failed to set timer. Please try again.');
+ }
+};
+
+const timerDeleteCommand: SapphireMessageExecuteType = async (
+ _client,
+ messageFromUser,
+ args,
+): Promise => {
+ const ret = await genericDeleteResponse(_client, messageFromUser, args, false);
+ return ret as SapphireMessageResponse;
+};
+
+const timerViewCommand: SapphireMessageExecuteType = async (
+ _client,
+ messageFromUser,
+ args,
+): Promise => {
+ const ret = await genericViewResponse(_client, messageFromUser, args, false);
+ return ret as SapphireMessageResponse;
+};
+
+export const timerCommandDetails: CodeyCommandDetails = {
+ name: 'timer',
+ aliases: [],
+ description: 'Set up a timer for anything you want!',
+ detailedDescription: `**Examples:**
+ \`/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: {
+ create: {
+ name: 'create',
+ description: 'Set a timer with specified duration',
+ executeCommand: timerSetExecuteCommand,
+ isCommandResponseEphemeral: true,
+ options: [
+ {
+ 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 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: {},
+ },
+ delete: {
+ name: 'delete',
+ description: 'Delete any timers you set!',
+ executeCommand: timerDeleteCommand,
+ isCommandResponseEphemeral: true,
+ options: [],
+ aliases: [],
+ detailedDescription: '',
+ subcommandDetails: {},
+ },
+ },
+};
diff --git a/src/commands/reminder/announcements.ts b/src/commands/reminder/announcements.ts
new file mode 100644
index 00000000..c9963eae
--- /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/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,
+ });
+ }
+}
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/components/db.ts b/src/components/db.ts
index 19dad417..0cd7fb5a 100644
--- a/src/components/db.ts
+++ b/src/components/db.ts
@@ -222,6 +222,35 @@ 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,
+ is_reminder BOOLEAN 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
+ )
+ `);
+};
+
+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 => {
//initialize all relevant tables
await initCoffeeChatTables(db);
@@ -236,6 +265,8 @@ const initTables = async (db: Database): Promise => {
await initResumePreview(db);
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..03d359e1
--- /dev/null
+++ b/src/components/reminder/announcement.ts
@@ -0,0 +1,79 @@
+import { openDB } from '../db';
+
+export interface Announcement {
+ id: number;
+ user_id: string;
+ title: string;
+ 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,
+): 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,
+ );
+ }
+};
diff --git a/src/components/reminder/reminder.ts b/src/components/reminder/reminder.ts
new file mode 100644
index 00000000..1f27f644
--- /dev/null
+++ b/src/components/reminder/reminder.ts
@@ -0,0 +1,77 @@
+import { openDB } from '../db';
+export interface Reminder {
+ id: number;
+ user_id: 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 that have yet to be shown
+export const getReminders = async (userId: string): Promise => {
+ const db = await openDB();
+ 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();
+ return await db.all(
+ 'SELECT * FROM reminders WHERE user_id = ? AND status = 0 AND is_reminder = 0',
+ 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);
+};
+
+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,
+ is_reminder: boolean,
+ created_at: string,
+ reminder_at: string,
+ message: string,
+): Promise => {
+ const db = await openDB();
+ await db.run(
+ `
+ INSERT INTO reminders (user_id, is_reminder, created_at, reminder_at, message, status)
+ VALUES(?,?,?,?,?,?);
+ `,
+ user_id,
+ is_reminder,
+ created_at,
+ reminder_at,
+ message,
+ 0,
+ );
+};
diff --git a/src/components/reminder/reminderObserver.ts b/src/components/reminder/reminderObserver.ts
new file mode 100644
index 00000000..8ff35719
--- /dev/null
+++ b/src/components/reminder/reminderObserver.ts
@@ -0,0 +1,106 @@
+import { EventEmitter } from 'events';
+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 globalCheckInterval: NodeJS.Timeout | null = null;
+
+ constructor(client: Client) {
+ super();
+ this.client = client;
+ this.setupEventListeners();
+ }
+
+ private setupEventListeners() {
+ this.on('reminderDue', async (reminder) => {
+ await this.sendReminder(reminder);
+ await reminderComponents.markReminderAsSent(reminder.id);
+ });
+
+ this.on('announcementDue', async (announcement) => {
+ await this.sendAnnouncement(announcement);
+ await announcementComponents.markAnnouncementAsSent(announcement.id);
+ });
+ }
+
+ async start(): Promise {
+ this.globalCheckInterval = setInterval(async () => {
+ await this.checkForDueReminders();
+ await this.checkForDueAnnouncements();
+ }, 60 * 1000);
+ await this.checkForDueReminders();
+ await this.checkForDueAnnouncements();
+ }
+
+ private async checkForDueReminders() {
+ try {
+ const dueReminders = await reminderComponents.getDueReminders();
+ if (dueReminders.length > 0) {
+ dueReminders.forEach((reminder: reminderComponents.Reminder) => {
+ this.emit('reminderDue', reminder);
+ });
+ }
+ } catch (error) {
+ // Error in checking for due reminders
+ }
+ }
+ private async checkForDueAnnouncements() {
+ try {
+ const dueReminders = await announcementComponents.getDueAnnouncements();
+ if (dueReminders.length > 0) {
+ dueReminders.forEach((reminder: reminderComponents.Reminder) => {
+ this.emit('announcementDue', reminder);
+ });
+ }
+ } catch (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);
+ if (user) {
+ await user.send({
+ embeds: [
+ {
+ title: reminder.is_reminder ? 'Reminder!' : 'Timer',
+ description: reminder.message,
+ color: 0x00ff00,
+ timestamp: new Date().toISOString(),
+ footer: {
+ text: `Set on ${new Date(reminder.created_at).toLocaleDateString()}`,
+ },
+ },
+ ],
+ });
+ }
+ } catch (error) {
+ // Error in send reminder
+ }
+ }
+
+ private async sendAnnouncement(announcement: announcementComponents.Announcement) {
+ try {
+ const guild = this.client.guilds.cache.get(config.TARGET_GUILD_ID);
+ if (!guild) {
+ return;
+ }
+
+ // Get the announcements channel using its ID from config
+ const channel = guild.channels.cache.get(config.ANNOUNCEMENTS_CHANNEL_ID) as TextChannel;
+ if (!channel) {
+ return;
+ }
+
+ await channel.send({
+ content: announcement.message,
+ files: announcement.image_url ? [announcement.image_url] : [],
+ });
+ } catch (error) {}
+ }
+}
diff --git a/src/events/ready.ts b/src/events/ready.ts
index 8fa6e7ff..becd6f58 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);
+
+ const reminderObserver = new ReminderObserver(client);
+ reminderObserver.start();
+
if (dev) updateWiki(); // will not run in staging/prod
};