// import type { ChatInputCommandInteraction } from "discord.js"; import { SlashCommandBuilder, type Client, type ChatInputCommandInteraction, GuildChannel, PermissionsBitField, } from "discord.js"; import { type Sequelize, Model, INTEGER, TEXT, DATE, BOOLEAN, BIGINT, } from "sequelize"; import * as sequelize from "sequelize"; import * as chrono from "chrono-node"; // const REMINDERS_CHANNEL = "1062196593379520593"; // #bot-test-channel const REMINDERS_CHANNEL = "395408839110950915"; // #general export default function (settings: { client: Client; db: Sequelize }) { const client = settings.client; const db = settings.db; 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; } 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: db }, ); async function canSendChannel(chanId: string): Promise { 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 GuildChannel)) { throw Error("Channel is not a guild channel"); } const permissions = channel?.permissionsFor(client.user); const flags = new PermissionsBitField([ PermissionsBitField.Flags.SendMessages, PermissionsBitField.Flags.ViewChannel, ]); if (permissions?.has(flags)) { return true; } return false; } async function triggerReminder(reminder: Reminder) { try { const channel = client.channels.cache.get(REMINDERS_CHANNEL); if (!(await canSendChannel(REMINDERS_CHANNEL))) return console.error("We're not permitted to send on that channel!"); if (!channel?.isSendable()) { return console.error( `Can't find channel ${REMINDERS_CHANNEL} or it is not sendable`, ); } await channel.send({ content: `Reminder for <@${reminder.userId}>: ${reminder.text}`, }); console.log("Reminder sent!"); reminder.set('isValid', false); await reminder.save(); } catch (error) { console.log(error); console.trace(); } } async function loop() { const results = await Reminder.findAll({ // TODO: This means that there's less than or equal to one minute left before // the reminder goes off. where: sequelize.literal( "isValid AND (julianday(trigger) - julianday('now')) <= 1.0 AND (julianday(trigger) - julianday('now')) > 0", ), }); for (const rmd of results) { try { const now = new Date(Date.now()); const wait = rmd.trigger.getTime() - now.getTime(); console.log( `Callback for Reminder ${rmd.get("id")} triggering in ${wait}ms`, ); setTimeout(async () => await triggerReminder(rmd), wait); } catch (error) { console.error(`Unrecoverable error: ${error}`) console.trace(); } } } async function execute(interaction: ChatInputCommandInteraction) { try { 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(); console.info( `Created Reminder ${reminder.get("id")} to be triggered at ${reminder.get("trigger")}`, ); await interaction.reply(`Ok, I'll remind you at ${when}`); } catch (_error) { await interaction.reply( "Something went wrong with adding your reminder.", ); } } return { Reminder: Reminder, loop: loop, 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: execute, initialize: async () => { console.log("Initializing remind.js"); await Reminder.sync(); await loop(); setInterval(loop, 1000 * 60); }, }; }