Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions src/commands/general/calreminder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ export default class extends Command {
}

// Retrieve events

const events = await retrieveEvents(
calendar.calendarId,
interaction
Expand Down Expand Up @@ -328,9 +329,10 @@ export default class extends Command {
}

// Build more detailed reminder text
const eventInfo = `${
chosenEvent.summary
}\nStarts at: ${dateObj.toLocaleString()}`;
const eventInfo = formatEventInfo(
chosenEvent,
chosenEvent.start?.timeZone
);

// Create reminder in DB
const EXPIRE_BUFFER_MS = 180 * 24 * 60 * 60 * 1000; // 180 days in ms
Expand All @@ -339,6 +341,7 @@ export default class extends Command {
owner: btnInt.user.id,
content: eventInfo,
mode: 'private',
summary: chosenEvent.summary,
expires: remindDate, // next fire time
repeat: repeatInterval, // "every_event" or null
calendarId: calendar.calendarId, // for fetching next events
Expand Down Expand Up @@ -458,3 +461,19 @@ export default class extends Command {
}

}

function formatEventInfo(
event: calendarV3.Schema$Event,
timeZone = 'America/New_York'
): string {
const dateObj = new Date(event.start?.dateTime || '');
const formattedStart = dateObj.toLocaleString('en-US', {
timeZone,
dateStyle: 'short',
timeStyle: 'short'
});

return `${event.summary || 'Untitled Event'}\nStarts at: ${formattedStart}${
event.location ? `\nLocation: ${event.location}` : ''
}${event.description ? `\nDetails: ${event.description}` : ''}`;
}
35 changes: 25 additions & 10 deletions src/commands/reminders/remind.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,53 @@
import { BOT, DB } from '@root/config';
import { ApplicationCommandOptionData, ApplicationCommandOptionType, ChatInputCommandInteraction, InteractionResponse } from 'discord.js';
import {
ApplicationCommandOptionData,
ApplicationCommandOptionType,
ChatInputCommandInteraction,
InteractionResponse
} from 'discord.js';
import { Reminder } from '@lib/types/Reminder';
import parse from 'parse-duration';
import { reminderTime } from '@root/src/lib/utils/generalUtils';
import { Command } from '@lib/types/Command';

export default class extends Command {


description = `Have ${BOT.NAME} give you a reminder.`;
extendedHelp = 'Reminders can be set to repeat daily or weekly.';
options: ApplicationCommandOptionData[] = [
{
name: 'content',
description: 'What you\'d like to be reminded of',
description: "What you'd like to be reminded of",
type: ApplicationCommandOptionType.String,
required: true
},
{
name: 'duration',
description: 'When you\'d like to be reminded',
description: "When you'd like to be reminded",
type: ApplicationCommandOptionType.String,
required: true
},
{
name: 'repeat',
description: 'How often you want the reminder to repeat',
choices: [{ name: 'Daily', value: 'daily' }, { name: 'Weekly', value: 'weekly' }],
choices: [
{ name: 'Daily', value: 'daily' },
{ name: 'Weekly', value: 'weekly' }
],
type: ApplicationCommandOptionType.String,
required: false
}
]
];

run(interaction: ChatInputCommandInteraction): Promise<InteractionResponse<boolean> | void> {
run(
interaction: ChatInputCommandInteraction
): Promise<InteractionResponse<boolean> | void> {
const content = interaction.options.getString('content');
const rawDuration = interaction.options.getString('duration');
const duration = parse(rawDuration);
const repeat = interaction.options.getString('repeat') as 'daily' | 'weekly' || null;
const repeat
= interaction.options.getString('repeat') as 'daily' | 'weekly'
|| null;

if (!duration) {
return interaction.reply({
Expand All @@ -49,12 +60,16 @@ export default class extends Command {
content,
mode: 'public', // temporary
expires: new Date(duration + Date.now()),
repeat
repeat,
summary: content // safe default
};

interaction.client.mongo.collection(DB.REMINDERS).insertOne(reminder);

return interaction.reply({ content: `I'll remind you about that at ${reminderTime(reminder)}.`, ephemeral: true });
return interaction.reply({
content: `I'll remind you about that at ${reminderTime(reminder)}.`,
ephemeral: true
});
}

}
122 changes: 70 additions & 52 deletions src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,92 +1,110 @@
/* eslint-disable camelcase */
import dotenv from 'dotenv';
dotenv.config();
import { calendar_v3, google } from 'googleapis';
import { JWT } from 'google-auth-library';
import { ChatInputCommandInteraction } from 'discord.js';
import { GaxiosResponse } from 'gaxios';

const SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'];
const KEY_PATH = process.env.MYPATH;
console.log('[DEBUG] MYPATH:', process.env.MYPATH);
console.log('[DEBUG] Resolved KEY_PATH:', KEY_PATH);
console.log('[DEBUG] Working directory:', process.cwd());

/**
* This function will retrive and return the events of the given calendar ID. It will send error messages if it cannot retrive the events
* This function will retrieve and return the events of the given calendar ID.
* If an interaction is provided, it handles user-facing error messages.
* If not, it throws errors for background use (e.g., in checkReminders).
*
* @param {string} calendarId The ID of the calendar you want to retrieve
* @param {ChatInputCommandInteraction} interaction Optional: Current Discord interacton
* @param {boolean} singleEvents Optional: Determines whether to list out each event instead of just the parent events - Default: true
* @returns {Promise<GaxiosResponse<calendar_v3.Schema$Events>>} Return the events of the given calendar ID
* @param {ChatInputCommandInteraction} interaction Optional: Current Discord interaction
* @param {boolean} singleEvents Optional: Whether to list each event separately (default: true)
* @returns {Promise<calendar_v3.Schema$Event[]>}
*/
export async function retrieveEvents(calendarId: string, interaction?: ChatInputCommandInteraction, singleEvents = true): Promise<calendar_v3.Schema$Event[]> {
// Retrieve an authenticaiton token
export async function retrieveEvents(
calendarId: string,
interaction?: ChatInputCommandInteraction,
singleEvents = true
): Promise<calendar_v3.Schema$Event[]> {
if (!KEY_PATH) {
const msg = '❌ Environment variable MYPATH is not set.';
if (interaction) {
await safeReply(interaction, msg);
return [];
} else {
throw new Error(msg);
}
}

// Initialize auth with keyFile
const auth = new JWT({
keyFile: KEY_PATH,
scopes: SCOPES
});

// Authorize access to google calendar and retrieve the calendar
let calendar: calendar_v3.Calendar = null;
let calendar: calendar_v3.Calendar;
try {
calendar = google.calendar({ version: 'v3', auth: auth });
} catch {
const errorMessage = '⚠️ Failed to authenticate with Google Calendar. Please try again later.';
calendar = google.calendar({ version: 'v3', auth });
} catch (err) {
const msg = '⚠️ Failed to authenticate with Google Calendar.';
if (interaction) {
if (interaction.replied) {
await interaction.followUp({
content: errorMessage,
ephemeral: true
});
} else {
await interaction.reply({
content: errorMessage,
ephemeral: true
});
}
await safeReply(interaction, msg);
return [];
} else {
console.log(errorMessage);
throw err;
}
}

// Retrieve the events from the calendar
let events: calendar_v3.Schema$Event[] = null;
try {
const tenDaysMs = 10 * 24 * 60 * 60 * 1000;

let response: GaxiosResponse;
const baseParams = {
calendarId: calendarId,
timeMin: new Date().toISOString(),
timeMax: new Date(Date.now() + tenDaysMs).toISOString(),
singleEvents
};

// This makes sure events are only sorted if single events is true
if (singleEvents) {
response = await calendar.events.list({
calendarId: calendarId,
timeMin: new Date().toISOString(),
timeMax: new Date(Date.now() + (10 * 24 * 60 * 60 * 1000)).toISOString(),
singleEvents: singleEvents,
...baseParams,
orderBy: 'startTime'
});
} else {
response = await calendar.events.list({
calendarId: calendarId,
timeMin: new Date().toISOString(),
timeMax: new Date(Date.now() + (10 * 24 * 60 * 60 * 1000)).toISOString(),
singleEvents: singleEvents
});
response = await calendar.events.list(baseParams);
}

events = response.data.items;
} catch {
const errorMessage = '⚠️ Failed to retrieve calendar events. Please try again later.';
return response.data.items ?? [];
} catch (err) {
const msg = '⚠️ Failed to retrieve calendar events.';
if (interaction) {
if (interaction.replied) {
await interaction.followUp({
content: errorMessage,
ephemeral: true
});
} else {
await interaction.reply({
content: errorMessage,
ephemeral: true
});
}
await safeReply(interaction, msg);
return [];
} else {
console.log(errorMessage);
throw err;
}
}
}

/**
* Helper to safely reply or follow up without throwing if already replied
* @param {ChatInputCommandInteraction} interaction The Discord interaction
* @param {string} message The message to send
*/

return events;
async function safeReply(
interaction: ChatInputCommandInteraction,
message: string
): Promise<void> {
try {
if (interaction.replied || interaction.deferred) {
await interaction.followUp({ content: message, ephemeral: true });
} else {
await interaction.reply({ content: message, ephemeral: true });
}
} catch (err) {
console.warn('⚠️ Failed to send error message to interaction:', err);
}
}
2 changes: 2 additions & 0 deletions src/lib/types/Reminder.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export interface Reminder {
_id?: ObjectId;
owner: string;
expires: Date;
content: string;
repeat: null | 'daily' | 'weekly' | 'every_event';
summary: string;
mode: 'public' | 'private';
calendarId?: string;
offset?: number;
Expand Down
Loading