import * as chrono from 'chrono-node'; import { type ChatInputCommandInteraction, SlashCommandBuilder, TextChannel, } from 'discord.js'; import * as sequelize from 'sequelize'; import {BasePlugin, type Command, type Settings} from '../../plugin'; import {initializeModels, Reminder, syncModels} from './models'; async function triggerReminder(channel: TextChannel, reminder: Reminder) { try { await channel.send({ content: `Reminder for <@${reminder.userId}>: ${reminder.text}`, }); reminder.isValid = false; } catch (error) { console.log(`Error trigggering Reminder ${reminder.id}: ${error}`); } finally { await reminder.save(); } } export class RemindPlugin extends BasePlugin { interval: NodeJS.Timeout; constructor(settings: Settings) { super(settings); initializeModels(settings.database); this.commands = [ { data: new SlashCommandBuilder() .setName('remind') .setDescription('Remind me to do something') .addStringOption(option => option .setName('when') .setDescription('Short description of when you want the reminder') .setRequired(true), ) .addStringOption(option => option .setName('what') .setDescription( 'Short description of what you want to be reminded of', ) .setRequired(true), ), execute: async (interaction: ChatInputCommandInteraction) => { const whenString = interaction.options.getString('when') ?? 'now'; const when = chrono.parseDate(whenString); if (!when) { await interaction.reply( `Sorry, I don't understand '${when}' as a date`, ); return; } const reminder = await Reminder.create({ userId: interaction.user.id, text: interaction.options.getString('what'), trigger: when, // TODO requestMessage: interaction.id, requestChannel: interaction.channelId, isValid: true, }); reminder.save(); await interaction.reply(`Ok, I'll remind you at ${when}`); console.info( `Created Reminder ${reminder.id} to be triggered at ${reminder.trigger}`, ); }, } as Command, // TODO ]; } getChannel(reminder: Reminder) { // TODO I don't really know what else needs to go into this function // I had some permissions stuff but it seemed very out-of-place // I don't know why isSendable() doesn't cover permissions? Or maybe // it does? // Either way this function kind of smells const client = this.settings.client; const channel = client.channels.cache.get(reminder.requestChannel); if (!(channel instanceof TextChannel)) { throw TypeError("Can't send to a non-ext channel"); } if (!channel) { throw Error('Cannot find valid channel to send reminder on'); } if (!channel.isSendable()) { throw Error("Can't send to that channel"); } return channel; } async loop() { console.debug('remind.js main loop'); const results = await Reminder.findAll({ // NOTE: 'trigger' is the time when the reminder should go off. If trigger - // now is positive, trigger is in the future. If trigger - now <= // 1.0, then we have less than one minute before the timer should go // off. where: sequelize.literal( "isValid AND (julianday(trigger) - julianday('now')) <= 1.0", ), }); for (const reminder of results) { try { const now = new Date(Date.now()); const delay = reminder.trigger.getTime() - now.getTime(); console.log( `Callback for Reminder ${reminder.get('id')} triggering in ${delay}ms`, ); const channel = this.getChannel(reminder); setTimeout(async () => await triggerReminder(channel, reminder), delay); } catch (error) { console.error( `Error while processing Reminder ${reminder.id}: ${error}`, ); console.trace(); } } } async start() { await syncModels(); this.interval = setInterval(this.loop.bind(this), 1000 * 60); } async stop() { clearInterval(this.interval); } }