// import type { ChatInputCommandInteraction } from "discord.js"; import { PermissionsBitField, SlashCommandBuilder, TextChannel, type ChatInputCommandInteraction, type Client, } from "discord.js"; import { type Sequelize, Model, INTEGER, TEXT, DATE, BOOLEAN } from "sequelize"; import * as sequelize from "sequelize"; import * as chrono from "chrono-node"; // const REMINDERS_CHANNEL = "1062196593379520593"; // #bot-test-channel // const REMINDERS_CHANNEL = ""; // #general interface Settings { client: Client; // Main Discord client object db: Sequelize; // Database access object publicChannel: string; // Channel to use if a reminder is public loopIntervalSec?: number; // Loop interval in seconds } const 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), ); class Reminder extends Model { declare id: number; declare userId: string; declare text: string | null; declare trigger: Date; declare isValid: boolean; declare requestChannel: string; declare requestMessage: string; } class Plugin { settings: Settings; publicChannel: TextChannel; constructor(settings: Settings) { this.settings = settings; this.publicChannel = getSendableTextChannel( settings.client, settings.publicChannel, ); } async 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( "(julianday(trigger) - julianday('now')) <= 1.0", ), }); for (const reminder of results) { try { const now = new Date(Date.now()); const wait = reminder.trigger.getTime() - now.getTime(); console.log( `Callback for Reminder ${reminder.get("id")} triggering in ${wait}ms`, ); setTimeout( async () => await triggerReminder(this.publicChannel, reminder), wait, ); } catch (error) { console.error(`Unrecoverable error: ${error}`); console.trace(); } } } } async function initialize(settings: Settings) { console.log("Initializing remind.js"); // Populate Reminder table inside SQLite DB Reminder.init( { id: { type: INTEGER, primaryKey: true, autoIncrement: true, }, userId: { type: TEXT, allowNull: false, }, text: TEXT, trigger: { type: DATE, allowNull: false, }, isValid: BOOLEAN, requestChannel: TEXT, requestMessage: TEXT, }, { sequelize: settings.db }, ); await Reminder.sync(); if ( !(await getSendableTextChannel(settings.client, settings.publicChannel)) ) { throw Error( "Invalid value for publicChannel; specify a channel the bot can access.", ); } const plugin = new Plugin(settings); if (settings.loopIntervalSec == null || settings.loopIntervalSec <= 0) { console.log("Setting plugin loop interval to default of 60 seconds"); settings.loopIntervalSec = 60; } setInterval(plugin.loop, 1000 * settings.loopIntervalSec); } function getSendableTextChannel(client: Client, chanId: string) { console.log(`getSendableTextChannel(${client}, ${chanId})`); const channel = client.channels.cache.get(chanId); if (!client.user) { throw Error("Can't check non-existent client"); } if (!channel) { throw Error("No such channel is visible to Blitz"); } if (!channel?.isSendable()) { throw Error("Channel is not a text channel, or otherwise not Sendable"); } if (!(channel instanceof TextChannel)) { throw Error("Channel is not a guild channel"); } const permissions = channel?.permissionsFor(client.user); if ( !permissions?.has([ PermissionsBitField.Flags.SendMessages, PermissionsBitField.Flags.ViewChannel, ]) ) { throw Error("Missing required permissions: SendMessages, ViewChannel"); } return channel; } async function triggerReminder(channel: TextChannel, reminder: Reminder) { await channel.send({ content: `Reminder for <@${reminder.userId}>: ${reminder.text}`, }); reminder.isValid = false; await reminder.save(); } async function execute(interaction: ChatInputCommandInteraction) { const when = chrono.parseDate(interaction.options.getString("when") ?? "now"); 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}`, ); } export default function (settings: Settings) { return { data, execute, initialize, Reminder, settings, }; }