From 8c2e889f2a8ea547cd6560b610b2bfd07f13a7cf Mon Sep 17 00:00:00 2001 From: Drew Malzahn Date: Sat, 5 Jul 2025 18:10:24 -0400 Subject: [PATCH] Implement /unnag --- commands/calendar/nag/checkin.ts | 74 +++--- commands/calendar/nag/nag.ts | 164 +++++++------ commands/calendar/nag/service.test.ts | 174 ++++++------- commands/calendar/nag/service.ts | 337 +++++++++++++------------- commands/calendar/nag/unnag.ts | 38 +-- index.ts | 190 ++++++++------- 6 files changed, 505 insertions(+), 472 deletions(-) diff --git a/commands/calendar/nag/checkin.ts b/commands/calendar/nag/checkin.ts index a6525a4..07ae6af 100644 --- a/commands/calendar/nag/checkin.ts +++ b/commands/calendar/nag/checkin.ts @@ -1,48 +1,50 @@ import { - type ChatInputCommandInteraction, - SlashCommandBuilder, -} from "discord.js"; + type ChatInputCommandInteraction, + SlashCommandBuilder, +} from 'discord.js'; -import type { Settings } from "./service"; +import type {Settings} from './service'; -import { Nag, CheckIn } from "./service"; +import {Nag, CheckIn} from './service'; 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"), - ); + .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) {} async function execute(interaction: ChatInputCommandInteraction) { - // TODO For now there is only one nag, but in the future this could be different. So let's construct it as a loop for now. - const result = await Nag.findAll({ - where: { - userId: interaction.user.id, - }, - }); - if (!result) { - await interaction.reply( - "Couldn't find any nags for you, what are you checking in for?", - ); - return; - } - for (const nag of result) { - await CheckIn.create({ - nagId: nag.id, - lastCheckIn: new Date(Date.now()), - }); - await interaction.reply("Thanks for checking in!"); - break; - } + // TODO For now there is only one nag, but in the future this could be different. So let's construct it as a loop for now. + const result = await Nag.findAll({ + where: { + userId: interaction.user.id, + }, + }); + if (!result) { + await interaction.reply( + "Couldn't find any nags for you, what are you checking in for?", + ); + return; + } + for (const nag of result) { + await CheckIn.create({ + nagId: nag.id, + lastCheckIn: new Date(Date.now()), + }); + await interaction.reply('Thanks for checking in!'); + break; + } } -export default function () { - return { - data, - }; +export default function (settings: Settings) { + return { + data, + initialize: async () => initialize(settings), + execute: execute, + }; } diff --git a/commands/calendar/nag/nag.ts b/commands/calendar/nag/nag.ts index 6efd722..3ed459a 100644 --- a/commands/calendar/nag/nag.ts +++ b/commands/calendar/nag/nag.ts @@ -1,91 +1,97 @@ import { - type ChatInputCommandInteraction, - SlashCommandBuilder, -} from "discord.js"; + type ChatInputCommandInteraction, + SlashCommandBuilder, +} from 'discord.js'; -import { Nag, CheckIn, type Settings } from "./service"; -import { Chrono } from "chrono-node"; +import {Nag, CheckIn, type Settings} from './service'; +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), - ); + .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') + .setRequired(false), + ) + .addBooleanOption(option => + option + .setName('mentionhere') + .setDescription('Whether to DM you or @ this channel') + .setRequired(false), + ); 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; - } - // Check if we already have an existing nag. In theory, this should be supported entirely, however - // I want to keep things simple for now. - const existingNags = await Nag.findAll({ - where: { userId: interaction.user.id }, - order: [["createdAt", "ASC"]], - }); - if (existingNags && existingNags.length > 0) { - // TODO: Hmm... For now, I guess we can just update the database. - for (const nag of existingNags) { - nag.text = text; - nag.failText = interaction.options.getString("failText") ?? undefined; - await nag.save(); - break; - } - 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.`, - ); - return; - } - // Otherwise, we need to create a new nag. - const nag = await Nag.create({ - userId: interaction.user.id, - guildId: interaction.guild?.id, - channelId: interaction.channel?.id, - messageId: interaction.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.`, - ); + const text = interaction.options.getString('text'); + if (text === null || text === undefined) { + await interaction.reply("Nag can't have a blank `text`, try again."); + return; + } + // Check if we already have an existing nag. In theory, this should be supported entirely, however + // I want to keep things simple for now. + const existingNags = await Nag.findAll({ + where: { + userId: interaction.user.id, + }, + // order: [["createdAt", "ASC"]], + }); + console.log('Successfully looked for checkIns'); + if (existingNags && existingNags.length > 0) { + // TODO: Hmm... For now, I guess we can just update the database. + for (const nag of existingNags) { + nag.text = text; + nag.failText = interaction.options.getString('failtext') ?? undefined; + await nag.save(); + break; + } + 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.`, + ); + return; + } + // Otherwise, we need to create a new nag. + const nag = await Nag.create({ + userId: interaction.user.id, + guildId: interaction.guild?.id, + channelId: interaction.channel?.id, + messageId: interaction.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({ + nag: { + id: nag.id, + }, + lastCheckIn: new Date(Date.now()), + }); + 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), - }; + return { + data, + execute, + initialize: async () => await initialize(settings), + }; } diff --git a/commands/calendar/nag/service.test.ts b/commands/calendar/nag/service.test.ts index 58b44c2..390f722 100644 --- a/commands/calendar/nag/service.test.ts +++ b/commands/calendar/nag/service.test.ts @@ -1,97 +1,97 @@ -import { expect, test, vi, it, describe, beforeEach, afterEach } from "vitest"; +import {expect, test, vi, it, describe, beforeEach, afterEach} from 'vitest'; import { - nextCheckInDate, - initAndSyncTables, - Nag, - CheckIn, - findGuiltyNags, - getCheckIn, -} from "./service"; -import { Sequelize, literal, Op } from "sequelize"; + nextCheckInDate, + initAndSyncTables, + Nag, + CheckIn, + findGuiltyNags, + getCheckIn, +} from './service'; +import {Sequelize, literal, Op} from 'sequelize'; -describe("nextCheckInDate", () => { - beforeEach(() => { - vi.useFakeTimers(); // Tell vitest to use fake timers - }); - afterEach(() => { - vi.useRealTimers(); // Reset date after test runs - }); - it("Returns 9AM if called before 9AM that day", () => { - const now = new Date(Date.now()); - let at9AM = new Date( - now.getFullYear(), - now.getMonth(), - now.getDate(), - 9, - 0, - ); - vi.setSystemTime( - new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0), - ); - expect(nextCheckInDate()).toEqual(at9AM); - }); - it("Returns 9AM tomorrow if called after 9AM", () => { - const dayInMS = 24 * 60 * 60 * 1000; - const now = new Date(Date.now()); - const tomorrow = new Date(Date.now() + dayInMS); - let tomorrow9AM = new Date( - tomorrow.getFullYear(), - tomorrow.getMonth(), - tomorrow.getDate(), - 9, - 0, - ); - vi.setSystemTime( - new Date(now.getFullYear(), now.getMonth(), now.getDate(), 9, 30), - ); - expect(nextCheckInDate()).toEqual(tomorrow9AM); - }); +describe('nextCheckInDate', () => { + beforeEach(() => { + vi.useFakeTimers(); // Tell vitest to use fake timers + }); + afterEach(() => { + vi.useRealTimers(); // Reset date after test runs + }); + it('Returns 9AM if called before 9AM that day', () => { + const now = new Date(Date.now()); + let at9AM = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate(), + 9, + 0, + ); + vi.setSystemTime( + new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0), + ); + expect(nextCheckInDate()).toEqual(at9AM); + }); + it('Returns 9AM tomorrow if called after 9AM', () => { + const dayInMS = 24 * 60 * 60 * 1000; + const now = new Date(Date.now()); + const tomorrow = new Date(Date.now() + dayInMS); + let tomorrow9AM = new Date( + tomorrow.getFullYear(), + tomorrow.getMonth(), + tomorrow.getDate(), + 9, + 0, + ); + vi.setSystemTime( + new Date(now.getFullYear(), now.getMonth(), now.getDate(), 9, 30), + ); + expect(nextCheckInDate()).toEqual(tomorrow9AM); + }); }); -describe("Finding nags without check-ins", async () => { - const sequelize = new Sequelize("sqlite://:memory:"); - const exampleNag = { - userId: "1234", - guildId: "1234", - channelId: "1234", - messageId: "1234", - text: "Example nag 1", - mentionHere: false, - }; +describe('Finding nags without check-ins', async () => { + const sequelize = new Sequelize('sqlite://:memory:'); + const exampleNag = { + userId: '1234', + guildId: '1234', + channelId: '1234', + messageId: '1234', + text: 'Example nag 1', + mentionHere: false, + }; - await initAndSyncTables(sequelize); + await initAndSyncTables(sequelize); - beforeEach(async () => { - vi.useFakeTimers(); - }); + beforeEach(async () => { + vi.useFakeTimers(); + }); - afterEach(async () => { - await Nag.destroy({ where: {} }); - await CheckIn.destroy({ where: {} }); - vi.useRealTimers(); - }); + afterEach(async () => { + await Nag.destroy({where: {}}); + await CheckIn.destroy({where: {}}); + vi.useRealTimers(); + }); - it("Finds nags without any check-ins", async () => { - const now = new Date(); - vi.setSystemTime( - new Date(now.getFullYear(), now.getMonth(), now.getDate(), 9), - ); - await Nag.create(exampleNag); - const results = await findGuiltyNags(); - expect(results.map((nag) => nag.userId)).toEqual(["1234"]); - }); + it('Finds nags without any check-ins', async () => { + const now = new Date(); + vi.setSystemTime( + new Date(now.getFullYear(), now.getMonth(), now.getDate(), 9), + ); + await Nag.create(exampleNag); + const results = await findGuiltyNags(); + expect(results.map(nag => nag.userId)).toEqual(['1234']); + }); - it("Ignores nags with a recent check-in", async () => { - const newNag = await Nag.create(exampleNag); - newNag.save(); - const currentCheckInTime = getCheckIn(9, 0); - const newCheckIn = await CheckIn.create({ - nagId: newNag.id, - // 1 hour previously; i.e. we checked in before the required time - lastCheckIn: new Date(currentCheckInTime.getTime() - 60 * 60 * 1000), - }); - newCheckIn.save(); - const results = await findGuiltyNags(); - expect(results.map((nag) => nag.userId)).toEqual([]); - }); + it('Ignores nags with a recent check-in', async () => { + const newNag = await Nag.create(exampleNag); + newNag.save(); + const currentCheckInTime = getCheckIn(9, 0); + const newCheckIn = await CheckIn.create({ + nagId: newNag.id, + // 1 hour previously; i.e. we checked in before the required time + lastCheckIn: new Date(currentCheckInTime.getTime() - 60 * 60 * 1000), + }); + newCheckIn.save(); + const results = await findGuiltyNags(); + expect(results.map(nag => nag.userId)).toEqual([]); + }); }); diff --git a/commands/calendar/nag/service.ts b/commands/calendar/nag/service.ts index 9e48d1d..60b3e16 100644 --- a/commands/calendar/nag/service.ts +++ b/commands/calendar/nag/service.ts @@ -1,87 +1,83 @@ -import { type Client, TextChannel } from "discord.js"; +import {type Client, TextChannel} from 'discord.js'; import { - type Sequelize, - Model, - INTEGER, - STRING, - BOOLEAN, - DATE, - literal, - Op, -} from "sequelize"; + type Sequelize, + Model, + INTEGER, + STRING, + BOOLEAN, + DATE, + literal, + Op, +} 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 + client: Client; // Main Discord client object + db: Sequelize; // Database access object + publicChannel?: string; // Channel to use if a reminder is public } export class Nag extends Model { - // Primary key - declare id: number; - // User who created this nag - declare userId: string; - // Guild (server) of original nag request - declare guildId: string; - // Channel of original nag request - declare channelId: string; - // Message of original nag request - declare messageId: string; - // Description of what you're supposed to do - declare text: string; - // Custom failure text - declare failText?: string; - // Should we @here? - declare mentionHere?: boolean; + // Primary key + declare id: number; + // User who created this nag + declare userId: string; + // Guild (server) of original nag request + declare guildId: string; + // Channel of original nag request + declare channelId: string; + // Message of original nag request + declare messageId: string; + // 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; + // 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: STRING, - 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 }, - ); - CheckIn.hasOne(Nag, { foreignKey: "nagId" }); - await Nag.sync(); - await CheckIn.sync(); + Nag.init( + { + id: { + primaryKey: true, + type: INTEGER, + autoIncrement: true, + }, + userId: { + type: STRING, + allowNull: false, + }, + text: { + type: STRING, + allowNull: false, + }, + failText: { + type: STRING, + }, + mentionHere: { + type: BOOLEAN, + }, + }, + {sequelize}, + ); + CheckIn.init( + { + lastCheckIn: { + type: DATE, + allowNull: false, + }, + }, + {sequelize}, + ); + Nag.hasMany(CheckIn); + CheckIn.belongsTo(Nag); + await Nag.sync(); + await CheckIn.sync(); } /** @@ -91,38 +87,38 @@ export async function initAndSyncTables(sequelize: Sequelize) { * @returns Date object representing the check-in time */ export function getCheckIn(hour: number, offset: number = 0) { - const nDaysInMS = offset * 24 * 60 * 60 * 1000; - const day = new Date(Date.now() + nDaysInMS); - const checkInDate = new Date( - day.getFullYear(), - day.getMonth(), - day.getDate(), - hour, - ); - return checkInDate; + const nDaysInMS = offset * 24 * 60 * 60 * 1000; + const day = new Date(Date.now() + nDaysInMS); + const checkInDate = new Date( + day.getFullYear(), + day.getMonth(), + day.getDate(), + hour, + ); + return checkInDate; } export async function findGuiltyNags() { - const results = await Nag.findAll({ where: {} }); - const guiltyNags: Nag[] = []; - const prevCheckIn = getCheckIn(9, -1); - const currentCheckIn = getCheckIn(9, 0); + const results = await Nag.findAll(); + const guiltyNags: Nag[] = []; + const prevCheckIn = getCheckIn(9, -1); + const currentCheckIn = getCheckIn(9, 0); - for (const nag of results) { - console.log("Checking nag: ", nag.id); - const checkInResults = await CheckIn.findAll({ - where: { - nagId: nag.id, - lastCheckIn: { - [Op.between]: [prevCheckIn, currentCheckIn], - }, - }, - }); - if (checkInResults.length <= 0) { - guiltyNags.push(nag); - } - } - return guiltyNags; + for (const nag of results) { + console.log('Checking nag: ', nag.id); + const checkInResults = await CheckIn.findAll({ + where: { + nagId: nag.id, + lastCheckIn: { + [Op.between]: [prevCheckIn, currentCheckIn], + }, + }, + }); + if (checkInResults.length <= 0) { + guiltyNags.push(nag); + } + } + return guiltyNags; } /** @@ -132,21 +128,21 @@ export async function findGuiltyNags() { * after the check-in time for today.) */ export function nextCheckInDate() { - const todaysCheckInDate = getCheckIn(9, 0); - if (Date.now() - todaysCheckInDate.getTime() < 0) { - return getCheckIn(9, 0); - } else { - return getCheckIn(9, 1); - } + const todaysCheckInDate = getCheckIn(9, 0); + if (Date.now() - todaysCheckInDate.getTime() < 0) { + return getCheckIn(9, 0); + } else { + return getCheckIn(9, 1); + } } function nextCheckInMs() { - const delayMs = nextCheckInDate().getTime() - Date.now(); - if (delayMs <= 0) { - // The value of nextCheckInDate is guaranteed to be in the future; if not, that's a bug in the program. - throw Error("Invalid value for nextCheckInDate"); - } - return delayMs; + const delayMs = nextCheckInDate().getTime() - Date.now(); + if (delayMs <= 0) { + // The value of nextCheckInDate is guaranteed to be in the future; if not, that's a bug in the program. + throw Error('Invalid value for nextCheckInDate'); + } + return delayMs; } /** @@ -154,69 +150,70 @@ function nextCheckInMs() { * tracking of timers and triggers. */ export class Manager { - // NOTE(DWM): I really hate the word 'Manager' when applied to code, but I - // thought for quite a bit about what to name these classes and I just can't - // come up with anything solid. (Pun intended.) + // NOTE(DWM): I really hate the word 'Manager' when applied to code, but I + // thought for quite a bit about what to name these classes and I just can't + // come up with anything solid. (Pun intended.) - settings: Settings; - interval?: NodeJS.Timeout; + settings: Settings; + interval?: NodeJS.Timeout; - constructor(settings: Settings) { - this.settings = settings; - } + constructor(settings: Settings) { + this.settings = settings; + initAndSyncTables(this.settings.db); + } - start() { - this.interval = setTimeout(this.loop, nextCheckInMs()); - } + start() { + this.interval = setTimeout(this.loop, nextCheckInMs()); + } - async triggerNag(nag: Nag) { - const client = this.settings.client; - // First, try and find the appropriate channel to send on - const guild = client.guilds.cache.get(nag.guildId); - if (!guild) { - console.error( - `No longer have access to Guild ${nag.guildId}; consider deleting this nag manually :(`, - ); - return; - } - const channel = guild.channels.cache.get(nag.channelId); - if (!channel) { - console.error( - `No longer have access to Channel ${nag.channelId}; consider deleting this nag manually :(`, - ); - return; - } - if (!channel?.isSendable() || !(channel instanceof TextChannel)) { - console.error("Somehow, we specified a channel which isn't sendable"); - 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 channel.send(msg); - } catch (error) { - console.log("Error while creating Nag:", error); // TODO - } - } + async triggerNag(nag: Nag) { + const client = this.settings.client; + // First, try and find the appropriate channel to send on + const guild = client.guilds.cache.get(nag.guildId); + if (!guild) { + console.error( + `No longer have access to Guild ${nag.guildId}; consider deleting this nag manually :(`, + ); + return; + } + const channel = guild.channels.cache.get(nag.channelId); + if (!channel) { + console.error( + `No longer have access to Channel ${nag.channelId}; consider deleting this nag manually :(`, + ); + return; + } + if (!channel?.isSendable() || !(channel instanceof TextChannel)) { + console.error("Somehow, we specified a channel which isn't sendable"); + 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 channel.send(msg); + } catch (error) { + console.log('Error while creating Nag:', error); // TODO + } + } - async loop() { - // According to MDN we don't need to do this, but it makes me feel happier - // knowing that we won't accidentally call clearInterval(...) on a timer - // that isn't running anymore. - this.interval = undefined; + async loop() { + // According to MDN we don't need to do this, but it makes me feel happier + // knowing that we won't accidentally call clearInterval(...) on a timer + // that isn't running anymore. + this.interval = undefined; - console.debug("nag.js main loop"); - const guiltyNags = await findGuiltyNags(); - for (const nag of guiltyNags) { - await this.triggerNag(nag); - } - this.interval = setTimeout(this.loop, nextCheckInMs()); - } + console.debug('nag.js main loop'); + const guiltyNags = await findGuiltyNags(); + for (const nag of guiltyNags) { + await this.triggerNag(nag); + } + this.interval = setTimeout(this.loop, nextCheckInMs()); + } - stop() { - clearInterval(this.interval); - } + stop() { + clearInterval(this.interval); + } } diff --git a/commands/calendar/nag/unnag.ts b/commands/calendar/nag/unnag.ts index 5f880df..4431a0f 100644 --- a/commands/calendar/nag/unnag.ts +++ b/commands/calendar/nag/unnag.ts @@ -1,31 +1,31 @@ import { - type ChatInputCommandInteraction, - SlashCommandBuilder, -} from "discord.js"; + type ChatInputCommandInteraction, + SlashCommandBuilder, +} from 'discord.js'; -import { type Settings, Nag } from "./service"; +import {type Settings, Nag} from './service'; const data = new SlashCommandBuilder() - .setName("unnag") - .setDescription("Remove a nag"); + .setName('unnag') + .setDescription('Remove a nag'); async function initialize(settings: Settings) {} async function execute(interaction: ChatInputCommandInteraction) { - // Find all nags for this user - const results = await Nag.findAll({ - where: { userId: interaction.user.id }, - }); - for (const result of results) { - await result.destroy(); - } - return; + // Find all nags for this user + const results = await Nag.findAll({ + where: {userId: interaction.user.id}, + }); + for (const result of results) { + await result.destroy(); + } + return; } export default function (settings: Settings) { - return { - data, - execute, - initialize: async () => await initialize(settings), - }; + return { + data, + execute, + initialize: async () => await initialize(settings), + }; } diff --git a/index.ts b/index.ts index 1c22d06..d9d0bb3 100644 --- a/index.ts +++ b/index.ts @@ -1,11 +1,11 @@ -import type { Interaction } from "discord.js"; -import { Client, Events, GatewayIntentBits, MessageFlags } from "discord.js"; +import type {Interaction} from 'discord.js'; +import {Client, Events, GatewayIntentBits, MessageFlags} from 'discord.js'; import type { - SlashCommandBuilder, - SlashCommandOptionsOnlyBuilder, -} from "discord.js"; + SlashCommandBuilder, + SlashCommandOptionsOnlyBuilder, +} from 'discord.js'; -import { sql, GuildSetting, initDb } from "./database"; +import {sql, GuildSetting, initDb} from './database'; const BLITZCRANK_BANNER = ` ****++++++++++*+++ @@ -63,105 +63,133 @@ const BLITZCRANK_BANNER = ` `; const client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.GuildMembers, - GatewayIntentBits.MessageContent, - ], + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.MessageContent, + ], }); -import { Routes } from "discord.js"; -import { guildId, appId, token, remindersChannelId } from "./config.json"; -import { REST } from "discord.js"; +import {Routes} from 'discord.js'; +import {guildId, appId, token, remindersChannelId} from './config.json'; +import {REST} from 'discord.js'; const rest = new REST(); rest.setToken(token); interface Command { - data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder; - execute: (interaction: any) => Promise; - initialize: (any) => Promise; + data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder; + execute: (interaction: any) => Promise; + initialize: (any) => Promise; } -import { Collection } from "discord.js"; +import {Collection} from 'discord.js'; const commands = new Collection(); -import PingCommand from "./commands/calendar/ping"; -import RemindCommand from "./commands/calendar/remind"; -import QuoteCommand from "./commands/quotes/quote"; -import NagCommand from "./commands/calendar/nag/nag"; -import UnnagCommand from "./commands/calendar/nag/unnag"; -import CheckinCommand from "./commands/calendar/nag/checkin"; +import PingCommand from './commands/calendar/ping'; +import RemindCommand from './commands/calendar/remind'; +import QuoteCommand from './commands/quotes/quote'; +import NagCommand from './commands/calendar/nag/nag'; +import UnnagCommand from './commands/calendar/nag/unnag'; +import CheckinCommand from './commands/calendar/nag/checkin'; + +import {Manager} from './commands/calendar/nag/service'; +const nagManager = new Manager({ + client: client, + db: sql, +}); +nagManager.start(); console.debug(`${remindersChannelId}`); -commands.set("ping", PingCommand({ client: client, db: sql })); +commands.set('ping', PingCommand({client: client, db: sql})); commands.set( - "remind", - RemindCommand({ - client: client, - db: sql, - publicChannel: remindersChannelId, - responseMode: "public", - }), + 'remind', + RemindCommand({ + client: client, + db: sql, + publicChannel: remindersChannelId, + responseMode: 'public', + }), +); +commands.set('quote', QuoteCommand({})); +commands.set( + 'nag', + NagCommand({ + client: client, + db: sql, + }), +); +commands.set( + 'unnag', + UnnagCommand({ + client: client, + db: sql, + }), +); +commands.set( + 'checkin', + CheckinCommand({ + client: client, + db: sql, + }), ); -commands.set("quote", QuoteCommand({})); async function syncCommands() { - try { - console.log(`Started refreshing slash commands`); - const _data = await rest.put( - Routes.applicationGuildCommands(appId, guildId), - { - body: commands.mapValues((cmd) => cmd.data.toJSON()), - }, - ); - console.log(`Successfully reloaded slash commands`); - } catch (error) { - console.error(error); - } + try { + console.log(`Started refreshing slash commands`); + const _data = await rest.put( + Routes.applicationGuildCommands(appId, guildId), + { + body: commands.mapValues(cmd => cmd.data.toJSON()), + }, + ); + console.log(`Successfully reloaded slash commands`); + } catch (error) { + console.error(error); + } } client.on(Events.InteractionCreate, async (interaction: Interaction) => { - if (!interaction.isChatInputCommand()) { - return; - } - try { - const command = commands.get(interaction.commandName); - if (command == null) { - await interaction.followUp(`No such command ${interaction.commandName}`); - return; - } - command.execute(interaction); - } catch (error) { - console.error(error); - if (interaction.replied || interaction.deferred) { - await interaction.followUp({ - content: "There was an error while executing this command", - flags: MessageFlags.Ephemeral, - }); - } - } - // TODO + if (!interaction.isChatInputCommand()) { + return; + } + try { + const command = commands.get(interaction.commandName); + if (command == null) { + await interaction.followUp(`No such command ${interaction.commandName}`); + return; + } + command.execute(interaction); + } catch (error) { + console.error(error); + if (interaction.replied || interaction.deferred) { + await interaction.followUp({ + content: 'There was an error while executing this command', + flags: MessageFlags.Ephemeral, + }); + } + } + // TODO }); -client.once(Events.ClientReady, async (readyClient) => { - await syncCommands(); - initDb(); // TODO - GuildSetting.sync(); // TODO +client.once(Events.ClientReady, async readyClient => { + await syncCommands(); + initDb(); // TODO + GuildSetting.sync(); // TODO - for (const [_name, cmd] of commands) { - await cmd?.initialize({ - client: client, - db: sql, - }); - } - // Print banner - for (const ln of BLITZCRANK_BANNER.split("\n")) { - console.log(ln); - } - console.log(`Logged in as ${readyClient.user.tag}`); + for (const [_name, cmd] of commands) { + await cmd?.initialize({ + client: client, + db: sql, + }); + } + // Print banner + for (const ln of BLITZCRANK_BANNER.split('\n')) { + console.log(ln); + } + console.log(`Logged in as ${readyClient.user.tag}`); }); client.login(token);