From e53d38e0ad84215dd02b455392a7852194a0a866 Mon Sep 17 00:00:00 2001 From: Drew Malzahn Date: Fri, 4 Jul 2025 10:36:40 -0400 Subject: [PATCH] WIP: Implementing /nag, /unnag, /checkin --- commands/calendar/nag/checkin.ts | 26 ++++++ commands/calendar/nag/common.ts | 133 +++++++++++++++++++++++++++++++ commands/calendar/nag/nag.ts | 79 ++++++++++++++++++ commands/calendar/nag/unnag.ts | 29 +++++++ database.ts | 7 +- 5 files changed, 269 insertions(+), 5 deletions(-) create mode 100644 commands/calendar/nag/checkin.ts create mode 100644 commands/calendar/nag/common.ts create mode 100644 commands/calendar/nag/nag.ts create mode 100644 commands/calendar/nag/unnag.ts diff --git a/commands/calendar/nag/checkin.ts b/commands/calendar/nag/checkin.ts new file mode 100644 index 0000000..7a35429 --- /dev/null +++ b/commands/calendar/nag/checkin.ts @@ -0,0 +1,26 @@ +import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; + +import { Settings } from "./common"; + +import { Nag, CheckIn } from './common'; + +const data = new SlashCommandBuilder() + .setName("checkin") + .setDescription("Check-in for your daily nag") + .addStringOption((option) => + option + .setName("text") + .setDescription("Optional description of what you have achieved"), + ); + +async function initialize(settings: Settings) {} + +function execute(interaction: ChatInputCommandInteraction) { + +} + +export default function () { + return { + data, + }; +} diff --git a/commands/calendar/nag/common.ts b/commands/calendar/nag/common.ts new file mode 100644 index 0000000..2e5d3ab --- /dev/null +++ b/commands/calendar/nag/common.ts @@ -0,0 +1,133 @@ +import { + ChatInputCommandInteraction, + Client, + SlashCommandBuilder, + TextChannel, +} from "discord.js"; +import { + Sequelize, + Model, + INTEGER, + STRING, + BOOLEAN, + DATE, + literal, +} from "sequelize"; + +export 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 +} + +export class Nag extends Model { + // Primary key + declare id: number; + // User who created this nag + declare userId: number; + // Description of what you're supposed to do + declare text: string; + // Custom failure text + declare failText?: string; + // Should we @here? + declare mentionHere?: boolean; +} + +export class CheckIn extends Model { + declare nagId: string; + // Date of the last time user ran /checkin + declare lastCheckIn: Date; +} + +export async function initAndSyncTables(sequelize: Sequelize) { + Nag.init( + { + id: { + primaryKey: true, + type: INTEGER, + autoIncrement: true, + }, + userId: { + type: INTEGER, + allowNull: false, + }, + text: { + type: STRING, + allowNull: false, + }, + failText: { + type: STRING, + }, + mentionHere: { + type: BOOLEAN, + }, + }, + { sequelize }, + ); + CheckIn.init( + { + nagId: { + type: INTEGER, + allowNull: false, + }, + lastCheckIn: { + type: DATE, + allowNull: false, + }, + }, + { sequelize }, + ); + Nag.hasOne(CheckIn, { foreignKey: "nagId" }); + await Nag.sync(); + await CheckIn.sync(); +} + +import { Guild, Channel } from "discord.js"; + +export class Plugin { + settings: Settings; + publicChannel?: Channel; + interval: NodeJS.Timeout; + + constructor(settings: Settings) { + this.settings = settings; + } + + start() { + if (!this.settings.loopIntervalSec) { + this.settings.loopIntervalSec = 60; // 1 minute + } + this.interval = setInterval( + this.loop, + this.settings.loopIntervalSec * 1000, + ); + } + + async triggerNag(nag: Nag) { + const client = this.settings.client; + const chan = client.channels.cache.get("1234"); // TODO + if (!(chan instanceof TextChannel)) { + return; // TODO + } + try { + const failText = + nag.failText ?? + `<@${nag.userId}> didn't complete "${nag.text}". Shame shame!`; + const mentionHere = nag.mentionHere ? "<@here> " : ""; + const msg = `${mentionHere}${failText}`; + await chan.send(msg); + } catch (error) { + console.log("Error while creating Nag:", error); // TODO + } + } + + async loop() { + console.debug("nag.js main loop"); + // Find all nags where the last check-in was before (next check in) - (24 hours) + } + + stop() { + clearInterval(this.interval); + } +} diff --git a/commands/calendar/nag/nag.ts b/commands/calendar/nag/nag.ts new file mode 100644 index 0000000..768dd84 --- /dev/null +++ b/commands/calendar/nag/nag.ts @@ -0,0 +1,79 @@ +import { + ChatInputCommandInteraction, + Client, + SlashCommandBuilder, +} from "discord.js"; +import { Sequelize, literal } from "sequelize"; + +import { Nag, CheckIn, Settings } from "./common"; +import { Chrono } from "chrono-node"; + +const data = new SlashCommandBuilder() + .setName("nag") + .setDescription("Let Blitzcrank nag you every day about something") + .addStringOption((option) => + option + .setRequired(true) + .setName("text") + .setDescription("What you have to do every day"), + ) + .addStringOption((option) => + option + .setName("failText") + .setDescription("Custom message to be broadcast on failure"), + ) + .addBooleanOption((option) => + option + .setName("mentionHere") + .setDescription("Whether to DM you or @ a channel") + .setRequired(false), + ); + +function lateCheckedInUsers() { + return Nag.findAll({ + include: [CheckIn], + where: literal( + "checkInTime <= datetime('now', '-1 day', 'start of day', '+9 hours')", + ), + }); +} + +async function initialize(settings: Settings) {} + +async function execute(interaction: ChatInputCommandInteraction) { + const text = interaction.options.getString("text"); + if (text === null || text === undefined) { + await interaction.reply("Nag can't have a blank `text`, try again."); + return; + } + const nag = await Nag.create({ + userId: interaction.user.id, + text: text, + failText: interaction.options.getString("failText"), + mentionHere: interaction.options.getBoolean("mentionHere") ?? false, + }); + await nag.save(); + const chrono = new Chrono(); + const checkIn = chrono.parseDate("today at 9AM"); + if (!checkIn) { + await interaction.reply( + "Internal error while saving your nag. Tell Drew the bot is broken!!!", + ); + return; + } + await CheckIn.create({ + nagId: nag.id, + checkIn: checkIn, + }); + await interaction.reply( + `I'll check every day at 9AM if you've completed '${text}'. If not, I'll nag you! Use /checkin to prevent a shameful callout, and /unnag to cancel.`, + ); +} + +export default function (settings: Settings) { + return { + data, + execute, + initialize: async () => await initialize(settings), + }; +} diff --git a/commands/calendar/nag/unnag.ts b/commands/calendar/nag/unnag.ts new file mode 100644 index 0000000..ceaa84e --- /dev/null +++ b/commands/calendar/nag/unnag.ts @@ -0,0 +1,29 @@ +import { + ChatInputCommandInteraction, + Client, + SlashCommandBuilder, +} from "discord.js"; +import { Sequelize } from "sequelize"; + +import { Settings } from './common' + + +const data = new SlashCommandBuilder() + .setName("unnag") + .setDescription("Remove a nag"); + + +async function initialize(settings: Settings) { +} + +async function execute(interaction: ChatInputCommandInteraction) { + return; +} + +export default function (settings: Settings) { + return { + data, + execute, + initialize: async() => await initialize(settings), + }; +} diff --git a/database.ts b/database.ts index c74a829..d2be0b4 100644 --- a/database.ts +++ b/database.ts @@ -13,11 +13,8 @@ export class GuildSetting extends Model { GuildSetting.init( { - guildId: { - type: STRING, - primaryKey: true, - }, - key: { type: STRING, primaryKey: true }, + guildId: { type: STRING }, + key: { type: STRING }, value: { type: STRING }, }, { sequelize: sql },