diff --git a/Documentation/Spring 2025 Implementations b/Documentation/Spring 2025 Implementations index 28b2d0b5..9312a015 100644 --- a/Documentation/Spring 2025 Implementations +++ b/Documentation/Spring 2025 Implementations @@ -1,2 +1,45 @@ -Add here! -Please commit please! \ No newline at end of file +Here is what we got done: + +1. /calendar: + +2. /importcalendar: + +3. /calreminder: - Google OAuth flow wired up so users can link their calendar + + - Events pulled into bot memory via retrieveEvents() + + - Parsing system extracts summary, start date/time, location, etc. + + - Filter menu lets you pick by course ID, day, date, location (virtual/in-person) + + - UI shows 3 events per page with Next/Prev buttons + + - Offset selector (At event, 10m / 30m / 1h / 1 d before) + + - “Repeat” toggle for per-event reminders (bug documented in code) + + - Builds reminder date, checks against past times + + - Inserts reminder into MongoDB (calreminders collection) + + - Sends ephemeral DM confirmation with the scheduled time + +4. /cancelreminder: + + - “Cancel Reminder” button available after setting a reminder + + - Deletes the reminder document from MongoDB + + - Sends an ephemeral DM follow-up: “Your reminder has been canceled.” + + - Basic try/catch around the delete, logs errors to console + +5. /viewreminders: + + - Fetches all upcoming reminders for the invoking user from MongoDB + + - Displays them in pages of 3 (same paginated UI as /calreminder) + + - Shows event summary, reminder time, and repeat status + + - All output sent as an ephemeral DM for privacy diff --git a/src/commands/general/calreminder.ts b/src/commands/general/calreminder.ts index b7870637..4c399505 100644 --- a/src/commands/general/calreminder.ts +++ b/src/commands/general/calreminder.ts @@ -27,382 +27,434 @@ export default class extends Command { description: 'Course ID', type: ApplicationCommandOptionType.String, required: true + }, + { + name: 'filter', + description: + 'Office-hours name or keyword to narrow results (optional)', + type: ApplicationCommandOptionType.String, + required: false } ]; async run(interaction: ChatInputCommandInteraction): Promise { - let eventMenu: PagifiedSelectMenu; - let offsetMenu: PagifiedSelectMenu; - - function generateMessage( - repeatInterval: 'every_event' | null, - chosenEvent?: calendarV3.Schema$Event, - chosenOffset?: number, - renderMenus = false, - eventCurrentPage = 0, - offsetCurrentPage = 0 - ) { - if (renderMenus) { - eventMenu = new PagifiedSelectMenu(); - eventMenu.createSelectMenu({ - customId: 'select_event', - placeHolder: 'Select an event', - minimumValues: 1, - maximumValues: 1 - }); - let defaultSet = false; + if (!interaction.deferred && !interaction.replied) { + await interaction.deferReply({ ephemeral: true }); + } + try { + let eventMenu: PagifiedSelectMenu; + let offsetMenu: PagifiedSelectMenu; + + const generateMessage = ( + repeatInterval: 'every_event' | null, + chosenEvent?: calendarV3.Schema$Event, + chosenOffset?: number, + renderMenus = false, + eventCurrentPage = 0, + offsetCurrentPage = 0 + ) => { + if (renderMenus) { + eventMenu = new PagifiedSelectMenu(); + eventMenu.createSelectMenu({ + customId: 'select_event', + placeHolder: 'Select an event', + minimumValues: 1, + maximumValues: 1 + }); + let defaultSet = false; + + filteredEvents.forEach((event, index) => { + if (!event.start?.dateTime) return; + + const isDefault + = !defaultSet + && chosenEvent?.start?.dateTime + === event.start?.dateTime; + + if (isDefault) defaultSet = true; + + eventMenu.addOption({ + label: event.summary, + value: `${event.start.dateTime}::${index}`, + description: `Starts at: ${new Date( + event.start.dateTime + ).toLocaleString()}`, + default: isDefault + }); + }); - filteredEvents.forEach((event, index) => { - if (!event.start?.dateTime) return; + eventMenu.currentPage = eventCurrentPage; + + // Create offset select menu + const offsetOptions = [ + { label: 'At event', value: '0' }, + { label: '10 minutes before', value: '10m' }, + { label: '30 minutes before', value: '30m' }, + { label: '1 hour before', value: '1h' }, + { label: '1 day before', value: '1d' } + ]; + + offsetMenu = new PagifiedSelectMenu(); + offsetMenu.createSelectMenu({ + customId: 'select_offset', + placeHolder: 'Select reminder offset', + minimumValues: 1, + maximumValues: 1 + }); - const isDefault - = !defaultSet - && chosenEvent?.start?.dateTime === event.start?.dateTime; + let offsetDefaultSet = false; - if (isDefault) defaultSet = true; + offsetOptions.forEach((option) => { + const isDefault + = !offsetDefaultSet + && chosenOffset === parse(option.value); + if (isDefault) offsetDefaultSet = true; - eventMenu.addOption({ - label: event.summary, - value: `${event.start.dateTime}::${index}`, - description: `Starts at: ${new Date( - event.start.dateTime - ).toLocaleString()}`, - default: isDefault + offsetMenu.addOption({ + label: option.label, + value: option.value, + default: isDefault + }); }); - }); - - eventMenu.currentPage = eventCurrentPage; - - // Create offset select menu - const offsetOptions = [ - { label: 'At event', value: '0' }, - { label: '10 minutes before', value: '10m' }, - { label: '30 minutes before', value: '30m' }, - { label: '1 hour before', value: '1h' }, - { label: '1 day before', value: '1d' } - ]; - offsetMenu = new PagifiedSelectMenu(); - offsetMenu.createSelectMenu({ - customId: 'select_offset', - placeHolder: 'Select reminder offset', - minimumValues: 1, - maximumValues: 1 - }); + offsetMenu.currentPage = offsetCurrentPage; + } - let offsetDefaultSet = false; + // 1) Generate event menu row(s) + const eventMenuRows = eventMenu.generateActionRows(); + + // 2) Generate offset menu row(s) + const offsetMenuRows = offsetMenu.generateActionRows(); + + // 3) Generate repeat button + const toggleRepeatButton = new ButtonBuilder() + .setCustomId('toggle_repeat') + .setLabel( + repeatInterval === 'every_event' + ? 'Repeat: On' + : 'Repeat: Off' + ) + .setStyle(ButtonStyle.Secondary); + + // 4) Generate set reminder button + const setReminder = new ButtonBuilder() + .setCustomId('set_reminder') + .setLabel('Set Reminder') + .setStyle(ButtonStyle.Success); + + const setReminderAndRepeatRow + = new ActionRowBuilder().addComponents( + toggleRepeatButton, + setReminder + ); - offsetOptions.forEach((option) => { - const isDefault - = !offsetDefaultSet - && chosenOffset === parse(option.value); - if (isDefault) offsetDefaultSet = true; + return [ + ...eventMenuRows, + ...offsetMenuRows, + setReminderAndRepeatRow + ]; + }; - offsetMenu.addOption({ - label: option.label, - value: option.value, - default: isDefault - }); + // 2) All your pre-flight checks (courseCode, DB lookup, retrieveEvents, filters, etc.) + const courseCode = interaction.options + .getString('classname') + ?.toUpperCase(); + if (!courseCode) { + await interaction.editReply({ + content: '❗ You must specify a class name.' }); - - offsetMenu.currentPage = offsetCurrentPage; + return; } - // 1) Generate event menu row(s) - const eventMenuRows = eventMenu.generateActionRows(); - - // 2) Generate offset menu row(s) - const offsetMenuRows = offsetMenu.generateActionRows(); - - // 3) Generate repeat button - const toggleRepeatButton = new ButtonBuilder() - .setCustomId('toggle_repeat') - .setLabel( - repeatInterval === 'every_event' - ? 'Repeat: On' - : 'Repeat: Off' - ) - .setStyle(ButtonStyle.Secondary); - - // 4) Generate set reminder button - const setReminder = new ButtonBuilder() - .setCustomId('set_reminder') - .setLabel('Set Reminder') - .setStyle(ButtonStyle.Success); - - const setReminderAndRepeatRow - = new ActionRowBuilder().addComponents( - toggleRepeatButton, - setReminder - ); + // OPTIONAL name filter (e.g. "Phil", "Sophia") + const nameFilter + = interaction.options.getString('filter')?.trim().toLowerCase() + ?? null; - return [ - ...eventMenuRows, - ...offsetMenuRows, - setReminderAndRepeatRow - ]; - } + // Lookup calendar from MongoDB + let calendar: { calendarId: string; calendarName: string }; + try { + const client = new MongoClient(MONGO_URI, { + useUnifiedTopology: true + }); + await client.connect(); - const courseCode = interaction.options - .getString('classname') - ?.toUpperCase(); + const db = client.db('CalendarDatabase'); + const collection = db.collection('calendarIds'); - if (!courseCode) { - await interaction.reply({ - content: '❗ You must specify a class name.', - ephemeral: true - }); - return; - } + const calendarInDB = await collection.findOne({ + calendarName: { $regex: `^${courseCode}$`, $options: 'i' } + }); - // Lookup calendar from MongoDB - let calendar: { calendarId: string; calendarName: string }; - try { - const client = new MongoClient(MONGO_URI, { - useUnifiedTopology: true - }); - await client.connect(); + await client.close(); - const db = client.db('CalendarDatabase'); - const collection = db.collection('calendarIds'); + if (!calendarInDB) { + await interaction.editReply({ + content: `⚠️ There are no matching calendars with course code **${courseCode}**.` + }); + return; + } - const calendarInDB = await collection.findOne({ - calendarName: { $regex: `^${courseCode}$`, $options: 'i' } - }); + calendar = { + calendarId: calendarInDB.calendarId, + calendarName: calendarInDB.calendarName + }; + } catch (error) { + console.error('Calendar lookup failed:', error); + await interaction.editReply({ + content: `❌ Database error while fetching calendar for **${courseCode}**.` + }); + return; + } - await client.close(); + // Retrieve events + const events = await retrieveEvents( + calendar.calendarId, + interaction + ); - if (!calendarInDB) { - await interaction.reply({ - content: `⚠️ There are no matching calendars with course code **${courseCode}**.`, - ephemeral: true + if (!events || events.length === 0) { + await interaction.editReply({ + content: + '⚠️ Failed to fetch calendar events or no events found.' }); return; } - calendar = { - calendarId: calendarInDB.calendarId, - calendarName: calendarInDB.calendarName - }; - } catch (error) { - console.error('Calendar lookup failed:', error); - await interaction.reply({ - content: `❌ Database error while fetching calendar for **${courseCode}**.`, - ephemeral: true - }); - return; - } - - // Retrieve events - const events = await retrieveEvents(calendar.calendarId, interaction); + let filteredEvents = events; // no filtering needed since each calendar is specific to a course + if (nameFilter) { + filteredEvents = events.filter((e) => + e.summary?.toLowerCase().includes(nameFilter) + ); + if (filteredEvents.length === 0) { + await interaction.editReply({ + content: `⚠️ No events found for **${courseCode}** matching **${nameFilter}**.` + }); + return; + } + } - if (!events || events.length === 0) { - await interaction.reply({ - content: - '⚠️ Failed to fetch calendar events or no events found.', - ephemeral: true - }); - return; - } + let chosenEvent: calendarV3.Schema$Event = null; + let chosenOffset: number = null; + let repeatInterval: 'every_event' = null; + let activeReminderId: string = null; - const filteredEvents = events; // no filtering needed since each calendar is specific to a course - - let chosenEvent: calendarV3.Schema$Event = null; - let chosenOffset: number = null; - let repeatInterval: 'every_event' = null; - let activeReminderId: string = null; - - const initialComponents = generateMessage( - repeatInterval, - chosenEvent, - chosenOffset, - true - ); - if (chosenOffset === null) { - chosenOffset = 0; - } + const initialComponents = generateMessage( + repeatInterval, + chosenEvent, + chosenOffset, + true + ); - const replyMessage = await interaction.reply({ - components: initialComponents, - ephemeral: true - }); - - // Main collector for event & offset - const collector = replyMessage.createMessageComponentCollector({ - componentType: ComponentType.StringSelect, - time: 60_000 - }); - - collector.on('collect', async (i) => { - if (i.customId === 'select_event') { - const [, indexStr] = i.values[0].split('::'); - const selectedIndex = parseInt(indexStr); - chosenEvent = filteredEvents[selectedIndex]; - await i.deferUpdate(); - } else if (i.customId === 'select_offset') { - const rawOffsetStr = i.values[0]; - chosenOffset = rawOffsetStr === '0' ? 0 : parse(rawOffsetStr); - await i.deferUpdate(); + if (chosenOffset === null) { + chosenOffset = 0; } - }); - - // Button collector for Cancel and Set Reminder - const buttonCollector = replyMessage.createMessageComponentCollector({ - componentType: ComponentType.Button, - time: 300_000 // 5 minutes - }); - - buttonCollector.on('collect', async (btnInt) => { - if (btnInt.customId === 'toggle_repeat') { - repeatInterval = repeatInterval ? null : 'every_event'; - - const updatedComponents = generateMessage( - repeatInterval, - chosenEvent, - chosenOffset, - true, - eventMenu.currentPage, - offsetMenu.currentPage - ); - - await btnInt.update({ - components: updatedComponents - }); - } else if (btnInt.customId === 'set_reminder') { - // If user hasn’t selected both fields, just silently acknowledge - if (!chosenEvent || chosenOffset === null) { - if (!btnInt.deferred && !btnInt.replied) { - await btnInt.deferUpdate(); // Prevent "interaction failed" - } - return; - } - // Everything is valid, continue with reminder setup - await btnInt.deferUpdate(); + // 2) Send your menus by editing the deferred reply: + const replyMessage = await interaction.editReply({ + components: initialComponents + }); - const dateObj = new Date(chosenEvent.start.dateTime); - const remindDate = new Date(dateObj.getTime() - chosenOffset); + // Main collector for event & offset + const collector = replyMessage.createMessageComponentCollector({ + componentType: ComponentType.StringSelect, + time: 60_000 + }); - // Check if it's already in the past - if (remindDate.getTime() <= Date.now()) { - await btnInt.editReply({ - content: - '⏰ That reminder time is in the past. No reminder was set.', - components: [] - }); - collector.stop(); - buttonCollector.stop(); - return; + collector.on('collect', async (i) => { + if (i.customId === 'select_event') { + const [, indexStr] = i.values[0].split('::'); + const selectedIndex = parseInt(indexStr); + chosenEvent = filteredEvents[selectedIndex]; + await i.deferUpdate(); + } else if (i.customId === 'select_offset') { + const rawOffsetStr = i.values[0]; + chosenOffset + = rawOffsetStr === '0' ? 0 : parse(rawOffsetStr); + await i.deferUpdate(); } + }); - // Build more detailed reminder text - const eventInfo = `${ - chosenEvent.summary - }\nStarts at: ${dateObj.toLocaleString()}`; - - // Create reminder in DB - const EXPIRE_BUFFER_MS = 180 * 24 * 60 * 60 * 1000; // 180 days in ms - - const reminder: Reminder = { - owner: btnInt.user.id, - content: eventInfo, - mode: 'public', - expires: repeatInterval - ? new Date(remindDate.getTime() + EXPIRE_BUFFER_MS) // give repeat reminders more time - : remindDate, // one-time reminders - repeat: repeatInterval - }; + // Button collector for Cancel and Set Reminder + const buttonCollector + = replyMessage.createMessageComponentCollector({ + componentType: ComponentType.Button, + time: 300_000 // 5 minutes + }); - let result; - try { - result = await btnInt.client.mongo - .collection(DB.REMINDERS) - .insertOne(reminder); - activeReminderId = result.insertedId; - } catch (err) { - console.error('Failed to insert reminder:', err); - await btnInt.editReply({ - content: - '❌ Failed to save reminder. Please try again later.', - components: [] + buttonCollector.on('collect', async (btnInt) => { + if (btnInt.customId === 'toggle_repeat') { + repeatInterval = repeatInterval ? null : 'every_event'; + + const updatedComponents = generateMessage( + repeatInterval, + chosenEvent, + chosenOffset, + true, + eventMenu.currentPage, + offsetMenu.currentPage + ); + + await btnInt.update({ + components: updatedComponents }); - buttonCollector.stop(); - return; - } + } else if (btnInt.customId === 'set_reminder') { + // If user hasn’t selected both fields, just silently acknowledge + if (!chosenEvent || chosenOffset === null) { + if (!btnInt.deferred && !btnInt.replied) { + await btnInt.deferUpdate(); // Prevent "interaction failed" + } + return; + } - // Build Cancel button row - const cancelButton = new ButtonBuilder() - .setCustomId('cancel_reminder') - .setLabel('Cancel Reminder') - .setStyle(ButtonStyle.Danger); + // Everything is valid, continue with reminder setup + await btnInt.deferUpdate(); - const buttonRow - = new ActionRowBuilder().addComponents( - cancelButton + const dateObj = new Date(chosenEvent.start.dateTime); + const remindDate = new Date( + dateObj.getTime() - chosenOffset ); - // Update ephemeral message with final reminder text + Cancel button - await btnInt.editReply({ - content: `✅ Your reminder is set!\nI'll remind you at **${remindDate.toLocaleString()}** about:\n\`\`\`\n${ - reminder.content - }\n\`\`\`${ - repeatInterval - ? `\n🔁 Repeats every event (for up to 180 days) -` - : '' - }`, - components: [buttonRow] - }); - } else if (btnInt.customId === 'cancel_reminder') { - try { - // 1) Defer *a new reply* (ephemeral) - if (!btnInt.deferred && !btnInt.replied) { - await btnInt.deferReply({ ephemeral: true }); + // Check if it's already in the past + if (remindDate.getTime() <= Date.now()) { + await btnInt.editReply({ + content: + '⏰ That reminder time is in the past. No reminder was set.', + components: [] + }); + collector.stop(); + buttonCollector.stop(); + return; } - // 2) Delete the reminder from DB if it exists - if (activeReminderId) { - await btnInt.client.mongo + // Build more detailed reminder text + const eventInfo = `${ + chosenEvent.summary + }\nStarts at: ${dateObj.toLocaleString()}`; + + // Create reminder in DB + const EXPIRE_BUFFER_MS = 180 * 24 * 60 * 60 * 1000; // 180 days in ms + + const reminder: Reminder = { + owner: btnInt.user.id, + content: eventInfo, + mode: 'private', + expires: remindDate, // next fire time + repeat: repeatInterval, // "every_event" or null + calendarId: calendar.calendarId, // for fetching next events + offset: chosenOffset, // ms before event + repeatUntil: new Date( + remindDate.getTime() + EXPIRE_BUFFER_MS + ) + }; + + let result; + try { + result = await btnInt.client.mongo .collection(DB.REMINDERS) - .deleteOne({ _id: activeReminderId }); + .insertOne(reminder); + activeReminderId = result.insertedId; + } catch (err) { + console.error('Failed to insert reminder:', err); + await btnInt.editReply({ + content: + '❌ Failed to save reminder. Please try again later.', + components: [] + }); + buttonCollector.stop(); + return; } - // 3) Send brand new ephemeral follow-up - await btnInt.followUp({ - content: '❌ Your reminder has been canceled.', - ephemeral: true - }); + // Build Cancel button row + const cancelButton = new ButtonBuilder() + .setCustomId('cancel_reminder') + .setLabel('Cancel Reminder') + .setStyle(ButtonStyle.Danger); + + const buttonRow + = new ActionRowBuilder().addComponents( + cancelButton + ); - // 4) Stop the collector - buttonCollector.stop(); - } catch (err) { - console.error('Failed to cancel reminder:', err); + // Update ephemeral message with final reminder text + Cancel button + await btnInt.editReply({ + content: `✅ Your reminder is set!\nI'll remind you at **${remindDate.toLocaleString()}** about:\n\`\`\`\n${ + reminder.content + }\n\`\`\`${ + repeatInterval + ? `\n🔁 Repeats every event (for up to 180 days) +` + : '' + }`, + components: [buttonRow] + }); + } else if (btnInt.customId === 'cancel_reminder') { + try { + // 1) Defer *a new reply* (ephemeral) + if (!btnInt.deferred && !btnInt.replied) { + await btnInt.deferReply({ ephemeral: true }); + } + + // 2) Delete the reminder from DB if it exists + if (activeReminderId) { + await btnInt.client.mongo + .collection(DB.REMINDERS) + .deleteOne({ _id: activeReminderId }); + } + + // 3) Send brand new ephemeral follow-up + await btnInt.followUp({ + content: '❌ Your reminder has been canceled.', + ephemeral: true + }); + + // 4) Stop the collector + buttonCollector.stop(); + } catch (err) { + console.error('Failed to cancel reminder:', err); + } } - } - const actions: Record void> = { - 'next_button:select_event': () => eventMenu.currentPage++, - 'prev_button:select_event': () => eventMenu.currentPage--, - 'next_button:select_offset': () => offsetMenu.currentPage++, - 'prev_button:select_offset': () => offsetMenu.currentPage-- - }; - const action = actions[btnInt.customId]; - - if (action) { - await btnInt.deferUpdate(); - action(); - - const newRows = generateMessage( - repeatInterval, - chosenEvent, - chosenOffset, - true, // ← force menus to regenerate - eventMenu.currentPage, // ← keep the event page - offsetMenu.currentPage // ← keep the offset page - ); - await btnInt.editReply({ components: newRows }); + const actions: Record void> = { + 'next_button:select_event': () => eventMenu.currentPage++, + 'prev_button:select_event': () => eventMenu.currentPage--, + 'next_button:select_offset': () => offsetMenu.currentPage++, + 'prev_button:select_offset': () => offsetMenu.currentPage-- + }; + const action = actions[btnInt.customId]; + + if (action) { + await btnInt.deferUpdate(); + action(); + + const newRows = generateMessage( + repeatInterval, + chosenEvent, + chosenOffset, + true, // ← force menus to regenerate + eventMenu.currentPage, // ← keep the event page + offsetMenu.currentPage // ← keep the offset page + ); + await btnInt.editReply({ components: newRows }); + } + }); + } catch (err) { + console.error('calreminder error:', err); + // 5) Error fallback: if we’ve already deferred/replied, use followUp + if (interaction.deferred || interaction.replied) { + await interaction.followUp({ + content: + '❌ An error occurred; the team has been notified.', + ephemeral: true + }); + } else { + await interaction.reply({ + content: + '❌ An error occurred; the team has been notified.', + ephemeral: true + }); } - }); + } } } diff --git a/src/lib/types/Reminder.d.ts b/src/lib/types/Reminder.d.ts index 23535a1c..93eb509f 100644 --- a/src/lib/types/Reminder.d.ts +++ b/src/lib/types/Reminder.d.ts @@ -4,4 +4,7 @@ export interface Reminder { content: string; repeat: null | 'daily' | 'weekly' | 'every_event'; mode: 'public' | 'private'; + calendarId?: string; + offset?: number; + repeatUntil?: Date; } diff --git a/src/pieces/tasks.ts b/src/pieces/tasks.ts index a3850612..2eda4732 100644 --- a/src/pieces/tasks.ts +++ b/src/pieces/tasks.ts @@ -6,8 +6,7 @@ import { Poll, PollResult } from '@lib/types/Poll'; async function register(bot: Client): Promise { schedule('0/30 * * * * *', () => { - handleCron(bot) - .catch(async error => bot.emit('error', error)); + handleCron(bot).catch(async (error) => bot.emit('error', error)); }); } @@ -17,25 +16,32 @@ async function handleCron(bot: Client): Promise { } async function checkPolls(bot: Client): Promise { - const polls: Poll[] = await bot.mongo.collection(DB.POLLS).find({ - expires: { $lte: new Date() } - }).toArray(); + const polls: Poll[] = await bot.mongo + .collection(DB.POLLS) + .find({ + expires: { $lte: new Date() } + }) + .toArray(); const emotes = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟']; - polls.forEach(async poll => { + polls.forEach(async (poll) => { const mdTimestamp = ``; // figure out the winner and also put the results in a map for ease of use const resultMap = new Map(); let winners: PollResult[] = []; - poll.results.forEach(res => { + poll.results.forEach((res) => { resultMap.set(res.option, res.users.length); if (!winners[0]) { winners = [res]; return; } - if (winners[0] && res.users.length > winners[0].users.length) winners = [res]; - else if (res.users.length === winners[0].users.length) winners.push(res); + + if (winners[0] && res.users.length > winners[0].users.length) { + winners = [res]; + } else if (res.users.length === winners[0].users.length) { + winners.push(res); + } }); // build up the win string @@ -44,85 +50,114 @@ async function checkPolls(bot: Client): Promise { if (winCount === 0) { winMessage = `It looks like no one has voted!`; } else if (winners.length === 1) { - winMessage = `**${winners[0].option}** has won the poll with ${winCount} vote${winCount === 1 ? '' : 's'}!`; - } else { winMessage = `**${ - winners.slice(0, -1).map(win => win.option).join(', ') - } and ${ + winners[0].option + }** has won the poll with ${winCount} vote${ + winCount === 1 ? '' : 's' + }!`; + } else { + winMessage = `**${winners + .slice(0, -1) + .map((win) => win.option) + .join(', ')} and ${ winners.slice(-1)[0].option - }** have won the poll with ${winners[0].users.length} vote${winCount === 1 ? '' : 's'} each!`; + }** have won the poll with ${winners[0].users.length} vote${ + winCount === 1 ? '' : 's' + } each!`; } // build up the text that is on the final poll embed let choiceText = ''; let count = 0; resultMap.forEach((value, key) => { - choiceText += `${emotes[count++]} ${key}: ${value} vote${value === 1 ? '' : 's'}\n`; + choiceText += `${emotes[count++]} ${key}: ${value} vote${ + value === 1 ? '' : 's' + }\n`; }); const pollChannel = await bot.channels.fetch(poll.channel); - if (pollChannel.type !== ChannelType.GuildText) throw 'something went wrong fetching the poll\'s channel'; + if (pollChannel.type !== ChannelType.GuildText) { + throw "something went wrong fetching the poll's channel"; + } const pollMsg = await pollChannel.messages.fetch(poll.message); const owner = await pollMsg.guild.members.fetch(poll.owner); const pollEmbed = new EmbedBuilder() .setTitle(poll.question) - .setDescription(`This poll was created by ${owner.displayName} and ended **${mdTimestamp}**`) - .addFields({ name: `Winner${winners.length === 1 ? '' : 's'}`, value: winMessage }) + .setDescription( + `This poll was created by ${owner.displayName} and ended **${mdTimestamp}**` + ) + .addFields({ + name: `Winner${winners.length === 1 ? '' : 's'}`, + value: winMessage + }) .addFields({ name: 'Choices', value: choiceText }) .setColor('Random'); pollMsg.edit({ embeds: [pollEmbed], components: [] }); - - pollMsg.channel.send({ embeds: [new EmbedBuilder() - .setTitle(poll.question) - .setDescription(`${owner}'s poll has ended!`) - .addFields({ name: `Winner${winners.length === 1 ? '' : 's'}`, value: winMessage }) - .addFields({ name: 'Original poll', value: `Click [here](${pollMsg.url}) to see the original poll.` }) - .setColor('Random') - ] }); + pollMsg.channel.send({ + embeds: [ + new EmbedBuilder() + .setTitle(poll.question) + .setDescription(`${owner}'s poll has ended!`) + .addFields({ + name: `Winner${winners.length === 1 ? '' : 's'}`, + value: winMessage + }) + .addFields({ + name: 'Original poll', + value: `Click [here](${pollMsg.url}) to see the original poll.` + }) + .setColor('Random') + ] + }); await bot.mongo.collection(DB.POLLS).findOneAndDelete(poll); }); } async function checkReminders(bot: Client): Promise { - const reminders: Array = await bot.mongo.collection(DB.REMINDERS).find({ - expires: { $lte: new Date() } - }).toArray(); - const pubChan = await bot.channels.fetch(CHANNELS.SAGE) as TextChannel; - - reminders.forEach(reminder => { - const message = `<@${reminder.owner}>, here's the reminder you asked for: **${reminder.content}**`; - - if (reminder.mode === 'public') { - pubChan.send(message); - } else { - bot.users.fetch(reminder.owner).then(user => user.send(message).catch(() => { - pubChan.send(`<@${reminder.owner}>, I tried to send you a DM about your private reminder but it looks like you have -DMs closed. Please enable DMs in the future if you'd like to get private reminders.`); - })); + const now = new Date(); + + // 1) fetch all reminders whose time has come + const reminders: Reminder[] = await bot.mongo + .collection(DB.REMINDERS) + .find({ expires: { $lte: now } }) + .toArray(); + + // 2) send each one as a DM‐embed first, fallback to Sage channel + for (const rem of reminders) { + // build a pretty embed + const embed = new EmbedBuilder() + .setTitle('⏰ Reminder') + .setDescription(rem.content) // your full "content" string + .setColor('Blue') + .setTimestamp(now); + + // only if it's repeating, tack on a “Repeats” field + if (rem.repeat) { + embed.addFields({ + name: '🔁 Repeats', + value: + rem.repeat === 'every_event' ? 'Every event' : rem.repeat, + inline: true + }); } - // copied value by value for several reasons, change it and I take no responsibility for it breaking. - const newReminder: Reminder = { - content: reminder.content, - expires: new Date(reminder.expires), - mode: reminder.mode, - repeat: reminder.repeat, - owner: reminder.owner - }; - - if (reminder.repeat === 'daily') { - newReminder.expires.setDate(reminder.expires.getDate() + 1); - bot.mongo.collection(DB.REMINDERS).findOneAndReplace(reminder, newReminder); - } else if (reminder.repeat === 'weekly') { - newReminder.expires.setDate(reminder.expires.getDate() + 7); - bot.mongo.collection(DB.REMINDERS).findOneAndReplace(reminder, newReminder); - } else { - bot.mongo.collection(DB.REMINDERS).findOneAndDelete(reminder); + try { + const user = await bot.users.fetch(rem.owner); + await user.send({ embeds: [embed] }); + } catch { + const sage = (await bot.channels.fetch( + CHANNELS.SAGE + )) as TextChannel; + await sage.send({ embeds: [embed] }); } - }); -} + } + // 3) clean up the ones we just dispatched + await bot.mongo + .collection(DB.REMINDERS) + .deleteMany({ expires: { $lte: now } }); +} export default register;