Implement /unnag

This commit is contained in:
2025-07-05 18:10:24 -04:00
parent 88679d2eda
commit 8c2e889f2a
6 changed files with 505 additions and 472 deletions

View File

@@ -1,48 +1,50 @@
import { import {
type ChatInputCommandInteraction, type ChatInputCommandInteraction,
SlashCommandBuilder, SlashCommandBuilder,
} from "discord.js"; } 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() const data = new SlashCommandBuilder()
.setName("checkin") .setName('checkin')
.setDescription("Check-in for your daily nag") .setDescription('Check-in for your daily nag')
.addStringOption((option) => .addStringOption(option =>
option option
.setName("text") .setName('text')
.setDescription("Optional description of what you have achieved"), .setDescription('Optional description of what you have achieved'),
); );
async function initialize(settings: Settings) {} async function initialize(settings: Settings) {}
async function execute(interaction: ChatInputCommandInteraction) { 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. // 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({ const result = await Nag.findAll({
where: { where: {
userId: interaction.user.id, userId: interaction.user.id,
}, },
}); });
if (!result) { if (!result) {
await interaction.reply( await interaction.reply(
"Couldn't find any nags for you, what are you checking in for?", "Couldn't find any nags for you, what are you checking in for?",
); );
return; return;
} }
for (const nag of result) { for (const nag of result) {
await CheckIn.create({ await CheckIn.create({
nagId: nag.id, nagId: nag.id,
lastCheckIn: new Date(Date.now()), lastCheckIn: new Date(Date.now()),
}); });
await interaction.reply("Thanks for checking in!"); await interaction.reply('Thanks for checking in!');
break; break;
} }
} }
export default function () { export default function (settings: Settings) {
return { return {
data, data,
}; initialize: async () => initialize(settings),
execute: execute,
};
} }

View File

@@ -1,91 +1,97 @@
import { import {
type ChatInputCommandInteraction, type ChatInputCommandInteraction,
SlashCommandBuilder, SlashCommandBuilder,
} from "discord.js"; } from 'discord.js';
import { Nag, CheckIn, type Settings } from "./service"; import {Nag, CheckIn, type Settings} from './service';
import { Chrono } from "chrono-node"; import {Chrono} from 'chrono-node';
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("nag") .setName('nag')
.setDescription("Let Blitzcrank nag you every day about something") .setDescription('Let Blitzcrank nag you every day about something')
.addStringOption((option) => .addStringOption(option =>
option option
.setRequired(true) .setRequired(true)
.setName("text") .setName('text')
.setDescription("What you have to do every day"), .setDescription('What you have to do every day'),
) )
.addStringOption((option) => .addStringOption(option =>
option option
.setName("failText") .setName('failtext')
.setDescription("Custom message to be broadcast on failure"), .setDescription('Custom message to be broadcast on failure')
) .setRequired(false),
.addBooleanOption((option) => )
option .addBooleanOption(option =>
.setName("mentionHere") option
.setDescription("Whether to DM you or @ a channel") .setName('mentionhere')
.setRequired(false), .setDescription('Whether to DM you or @ this channel')
); .setRequired(false),
);
async function initialize(settings: Settings) {} async function initialize(settings: Settings) {}
async function execute(interaction: ChatInputCommandInteraction) { async function execute(interaction: ChatInputCommandInteraction) {
const text = interaction.options.getString("text"); const text = interaction.options.getString('text');
if (text === null || text === undefined) { if (text === null || text === undefined) {
await interaction.reply("Nag can't have a blank `text`, try again."); await interaction.reply("Nag can't have a blank `text`, try again.");
return; return;
} }
// Check if we already have an existing nag. In theory, this should be supported entirely, however // Check if we already have an existing nag. In theory, this should be supported entirely, however
// I want to keep things simple for now. // I want to keep things simple for now.
const existingNags = await Nag.findAll({ const existingNags = await Nag.findAll({
where: { userId: interaction.user.id }, where: {
order: [["createdAt", "ASC"]], userId: interaction.user.id,
}); },
if (existingNags && existingNags.length > 0) { // order: [["createdAt", "ASC"]],
// TODO: Hmm... For now, I guess we can just update the database. });
for (const nag of existingNags) { console.log('Successfully looked for checkIns');
nag.text = text; if (existingNags && existingNags.length > 0) {
nag.failText = interaction.options.getString("failText") ?? undefined; // TODO: Hmm... For now, I guess we can just update the database.
await nag.save(); for (const nag of existingNags) {
break; nag.text = text;
} nag.failText = interaction.options.getString('failtext') ?? undefined;
await interaction.reply( await nag.save();
`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.`, break;
); }
return; 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.`,
// Otherwise, we need to create a new nag. );
const nag = await Nag.create({ return;
userId: interaction.user.id, }
guildId: interaction.guild?.id, // Otherwise, we need to create a new nag.
channelId: interaction.channel?.id, const nag = await Nag.create({
messageId: interaction.id, userId: interaction.user.id,
text: text, guildId: interaction.guild?.id,
failText: interaction.options.getString("failText"), channelId: interaction.channel?.id,
mentionHere: interaction.options.getBoolean("mentionHere") ?? false, messageId: interaction.id,
}); text: text,
await nag.save(); failText: interaction.options.getString('failtext'),
const chrono = new Chrono(); mentionHere: interaction.options.getBoolean('mentionhere') ?? false,
const checkIn = chrono.parseDate("today at 9AM"); });
if (!checkIn) { await nag.save();
await interaction.reply( const chrono = new Chrono();
"Internal error while saving your nag. Tell Drew the bot is broken!!!", const checkIn = chrono.parseDate('today at 9AM');
); if (!checkIn) {
return; await interaction.reply(
} 'Internal error while saving your nag. Tell Drew the bot is broken!!!',
await CheckIn.create({ );
nagId: nag.id, return;
checkIn: checkIn, }
}); await CheckIn.create({
await interaction.reply( nag: {
`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.`, 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) { export default function (settings: Settings) {
return { return {
data, data,
execute, execute,
initialize: async () => await initialize(settings), initialize: async () => await initialize(settings),
}; };
} }

View File

@@ -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 { import {
nextCheckInDate, nextCheckInDate,
initAndSyncTables, initAndSyncTables,
Nag, Nag,
CheckIn, CheckIn,
findGuiltyNags, findGuiltyNags,
getCheckIn, getCheckIn,
} from "./service"; } from './service';
import { Sequelize, literal, Op } from "sequelize"; import {Sequelize, literal, Op} from 'sequelize';
describe("nextCheckInDate", () => { describe('nextCheckInDate', () => {
beforeEach(() => { beforeEach(() => {
vi.useFakeTimers(); // Tell vitest to use fake timers vi.useFakeTimers(); // Tell vitest to use fake timers
}); });
afterEach(() => { afterEach(() => {
vi.useRealTimers(); // Reset date after test runs vi.useRealTimers(); // Reset date after test runs
}); });
it("Returns 9AM if called before 9AM that day", () => { it('Returns 9AM if called before 9AM that day', () => {
const now = new Date(Date.now()); const now = new Date(Date.now());
let at9AM = new Date( let at9AM = new Date(
now.getFullYear(), now.getFullYear(),
now.getMonth(), now.getMonth(),
now.getDate(), now.getDate(),
9, 9,
0, 0,
); );
vi.setSystemTime( vi.setSystemTime(
new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0), new Date(now.getFullYear(), now.getMonth(), now.getDate(), 8, 0),
); );
expect(nextCheckInDate()).toEqual(at9AM); expect(nextCheckInDate()).toEqual(at9AM);
}); });
it("Returns 9AM tomorrow if called after 9AM", () => { it('Returns 9AM tomorrow if called after 9AM', () => {
const dayInMS = 24 * 60 * 60 * 1000; const dayInMS = 24 * 60 * 60 * 1000;
const now = new Date(Date.now()); const now = new Date(Date.now());
const tomorrow = new Date(Date.now() + dayInMS); const tomorrow = new Date(Date.now() + dayInMS);
let tomorrow9AM = new Date( let tomorrow9AM = new Date(
tomorrow.getFullYear(), tomorrow.getFullYear(),
tomorrow.getMonth(), tomorrow.getMonth(),
tomorrow.getDate(), tomorrow.getDate(),
9, 9,
0, 0,
); );
vi.setSystemTime( vi.setSystemTime(
new Date(now.getFullYear(), now.getMonth(), now.getDate(), 9, 30), new Date(now.getFullYear(), now.getMonth(), now.getDate(), 9, 30),
); );
expect(nextCheckInDate()).toEqual(tomorrow9AM); expect(nextCheckInDate()).toEqual(tomorrow9AM);
}); });
}); });
describe("Finding nags without check-ins", async () => { describe('Finding nags without check-ins', async () => {
const sequelize = new Sequelize("sqlite://:memory:"); const sequelize = new Sequelize('sqlite://:memory:');
const exampleNag = { const exampleNag = {
userId: "1234", userId: '1234',
guildId: "1234", guildId: '1234',
channelId: "1234", channelId: '1234',
messageId: "1234", messageId: '1234',
text: "Example nag 1", text: 'Example nag 1',
mentionHere: false, mentionHere: false,
}; };
await initAndSyncTables(sequelize); await initAndSyncTables(sequelize);
beforeEach(async () => { beforeEach(async () => {
vi.useFakeTimers(); vi.useFakeTimers();
}); });
afterEach(async () => { afterEach(async () => {
await Nag.destroy({ where: {} }); await Nag.destroy({where: {}});
await CheckIn.destroy({ where: {} }); await CheckIn.destroy({where: {}});
vi.useRealTimers(); vi.useRealTimers();
}); });
it("Finds nags without any check-ins", async () => { it('Finds nags without any check-ins', async () => {
const now = new Date(); const now = new Date();
vi.setSystemTime( vi.setSystemTime(
new Date(now.getFullYear(), now.getMonth(), now.getDate(), 9), new Date(now.getFullYear(), now.getMonth(), now.getDate(), 9),
); );
await Nag.create(exampleNag); await Nag.create(exampleNag);
const results = await findGuiltyNags(); const results = await findGuiltyNags();
expect(results.map((nag) => nag.userId)).toEqual(["1234"]); expect(results.map(nag => nag.userId)).toEqual(['1234']);
}); });
it("Ignores nags with a recent check-in", async () => { it('Ignores nags with a recent check-in', async () => {
const newNag = await Nag.create(exampleNag); const newNag = await Nag.create(exampleNag);
newNag.save(); newNag.save();
const currentCheckInTime = getCheckIn(9, 0); const currentCheckInTime = getCheckIn(9, 0);
const newCheckIn = await CheckIn.create({ const newCheckIn = await CheckIn.create({
nagId: newNag.id, nagId: newNag.id,
// 1 hour previously; i.e. we checked in before the required time // 1 hour previously; i.e. we checked in before the required time
lastCheckIn: new Date(currentCheckInTime.getTime() - 60 * 60 * 1000), lastCheckIn: new Date(currentCheckInTime.getTime() - 60 * 60 * 1000),
}); });
newCheckIn.save(); newCheckIn.save();
const results = await findGuiltyNags(); const results = await findGuiltyNags();
expect(results.map((nag) => nag.userId)).toEqual([]); expect(results.map(nag => nag.userId)).toEqual([]);
}); });
}); });

View File

@@ -1,87 +1,83 @@
import { type Client, TextChannel } from "discord.js"; import {type Client, TextChannel} from 'discord.js';
import { import {
type Sequelize, type Sequelize,
Model, Model,
INTEGER, INTEGER,
STRING, STRING,
BOOLEAN, BOOLEAN,
DATE, DATE,
literal, literal,
Op, Op,
} from "sequelize"; } from 'sequelize';
export interface Settings { export interface Settings {
client: Client; // Main Discord client object client: Client; // Main Discord client object
db: Sequelize; // Database access object db: Sequelize; // Database access object
publicChannel?: string; // Channel to use if a reminder is public publicChannel?: string; // Channel to use if a reminder is public
} }
export class Nag extends Model { export class Nag extends Model {
// Primary key // Primary key
declare id: number; declare id: number;
// User who created this nag // User who created this nag
declare userId: string; declare userId: string;
// Guild (server) of original nag request // Guild (server) of original nag request
declare guildId: string; declare guildId: string;
// Channel of original nag request // Channel of original nag request
declare channelId: string; declare channelId: string;
// Message of original nag request // Message of original nag request
declare messageId: string; declare messageId: string;
// Description of what you're supposed to do // Description of what you're supposed to do
declare text: string; declare text: string;
// Custom failure text // Custom failure text
declare failText?: string; declare failText?: string;
// Should we @here? // Should we @here?
declare mentionHere?: boolean; declare mentionHere?: boolean;
} }
export class CheckIn extends Model { export class CheckIn extends Model {
declare nagId: string; // Date of the last time user ran /checkin
// Date of the last time user ran /checkin declare lastCheckIn: Date;
declare lastCheckIn: Date;
} }
export async function initAndSyncTables(sequelize: Sequelize) { export async function initAndSyncTables(sequelize: Sequelize) {
Nag.init( Nag.init(
{ {
id: { id: {
primaryKey: true, primaryKey: true,
type: INTEGER, type: INTEGER,
autoIncrement: true, autoIncrement: true,
}, },
userId: { userId: {
type: STRING, type: STRING,
allowNull: false, allowNull: false,
}, },
text: { text: {
type: STRING, type: STRING,
allowNull: false, allowNull: false,
}, },
failText: { failText: {
type: STRING, type: STRING,
}, },
mentionHere: { mentionHere: {
type: BOOLEAN, type: BOOLEAN,
}, },
}, },
{ sequelize }, {sequelize},
); );
CheckIn.init( CheckIn.init(
{ {
nagId: { lastCheckIn: {
type: INTEGER, type: DATE,
allowNull: false, allowNull: false,
}, },
lastCheckIn: { },
type: DATE, {sequelize},
allowNull: false, );
}, Nag.hasMany(CheckIn);
}, CheckIn.belongsTo(Nag);
{ sequelize }, await Nag.sync();
); await CheckIn.sync();
CheckIn.hasOne(Nag, { foreignKey: "nagId" });
await Nag.sync();
await CheckIn.sync();
} }
/** /**
@@ -91,38 +87,38 @@ export async function initAndSyncTables(sequelize: Sequelize) {
* @returns Date object representing the check-in time * @returns Date object representing the check-in time
*/ */
export function getCheckIn(hour: number, offset: number = 0) { export function getCheckIn(hour: number, offset: number = 0) {
const nDaysInMS = offset * 24 * 60 * 60 * 1000; const nDaysInMS = offset * 24 * 60 * 60 * 1000;
const day = new Date(Date.now() + nDaysInMS); const day = new Date(Date.now() + nDaysInMS);
const checkInDate = new Date( const checkInDate = new Date(
day.getFullYear(), day.getFullYear(),
day.getMonth(), day.getMonth(),
day.getDate(), day.getDate(),
hour, hour,
); );
return checkInDate; return checkInDate;
} }
export async function findGuiltyNags() { export async function findGuiltyNags() {
const results = await Nag.findAll({ where: {} }); const results = await Nag.findAll();
const guiltyNags: Nag[] = []; const guiltyNags: Nag[] = [];
const prevCheckIn = getCheckIn(9, -1); const prevCheckIn = getCheckIn(9, -1);
const currentCheckIn = getCheckIn(9, 0); const currentCheckIn = getCheckIn(9, 0);
for (const nag of results) { for (const nag of results) {
console.log("Checking nag: ", nag.id); console.log('Checking nag: ', nag.id);
const checkInResults = await CheckIn.findAll({ const checkInResults = await CheckIn.findAll({
where: { where: {
nagId: nag.id, nagId: nag.id,
lastCheckIn: { lastCheckIn: {
[Op.between]: [prevCheckIn, currentCheckIn], [Op.between]: [prevCheckIn, currentCheckIn],
}, },
}, },
}); });
if (checkInResults.length <= 0) { if (checkInResults.length <= 0) {
guiltyNags.push(nag); guiltyNags.push(nag);
} }
} }
return guiltyNags; return guiltyNags;
} }
/** /**
@@ -132,21 +128,21 @@ export async function findGuiltyNags() {
* after the check-in time for today.) * after the check-in time for today.)
*/ */
export function nextCheckInDate() { export function nextCheckInDate() {
const todaysCheckInDate = getCheckIn(9, 0); const todaysCheckInDate = getCheckIn(9, 0);
if (Date.now() - todaysCheckInDate.getTime() < 0) { if (Date.now() - todaysCheckInDate.getTime() < 0) {
return getCheckIn(9, 0); return getCheckIn(9, 0);
} else { } else {
return getCheckIn(9, 1); return getCheckIn(9, 1);
} }
} }
function nextCheckInMs() { function nextCheckInMs() {
const delayMs = nextCheckInDate().getTime() - Date.now(); const delayMs = nextCheckInDate().getTime() - Date.now();
if (delayMs <= 0) { if (delayMs <= 0) {
// The value of nextCheckInDate is guaranteed to be in the future; if not, that's a bug in the program. // 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"); throw Error('Invalid value for nextCheckInDate');
} }
return delayMs; return delayMs;
} }
/** /**
@@ -154,69 +150,70 @@ function nextCheckInMs() {
* tracking of timers and triggers. * tracking of timers and triggers.
*/ */
export class Manager { export class Manager {
// NOTE(DWM): I really hate the word 'Manager' when applied to code, but I // 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 // thought for quite a bit about what to name these classes and I just can't
// come up with anything solid. (Pun intended.) // come up with anything solid. (Pun intended.)
settings: Settings; settings: Settings;
interval?: NodeJS.Timeout; interval?: NodeJS.Timeout;
constructor(settings: Settings) { constructor(settings: Settings) {
this.settings = settings; this.settings = settings;
} initAndSyncTables(this.settings.db);
}
start() { start() {
this.interval = setTimeout(this.loop, nextCheckInMs()); this.interval = setTimeout(this.loop, nextCheckInMs());
} }
async triggerNag(nag: Nag) { async triggerNag(nag: Nag) {
const client = this.settings.client; const client = this.settings.client;
// First, try and find the appropriate channel to send on // First, try and find the appropriate channel to send on
const guild = client.guilds.cache.get(nag.guildId); const guild = client.guilds.cache.get(nag.guildId);
if (!guild) { if (!guild) {
console.error( console.error(
`No longer have access to Guild ${nag.guildId}; consider deleting this nag manually :(`, `No longer have access to Guild ${nag.guildId}; consider deleting this nag manually :(`,
); );
return; return;
} }
const channel = guild.channels.cache.get(nag.channelId); const channel = guild.channels.cache.get(nag.channelId);
if (!channel) { if (!channel) {
console.error( console.error(
`No longer have access to Channel ${nag.channelId}; consider deleting this nag manually :(`, `No longer have access to Channel ${nag.channelId}; consider deleting this nag manually :(`,
); );
return; return;
} }
if (!channel?.isSendable() || !(channel instanceof TextChannel)) { if (!channel?.isSendable() || !(channel instanceof TextChannel)) {
console.error("Somehow, we specified a channel which isn't sendable"); console.error("Somehow, we specified a channel which isn't sendable");
return; // TODO return; // TODO
} }
try { try {
const failText = const failText =
nag.failText ?? nag.failText ??
`<@${nag.userId}> didn't complete "${nag.text}". Shame shame!`; `<@${nag.userId}> didn't complete "${nag.text}". Shame shame!`;
const mentionHere = nag.mentionHere ? "<@here> " : ""; const mentionHere = nag.mentionHere ? '<@here> ' : '';
const msg = `${mentionHere}${failText}`; const msg = `${mentionHere}${failText}`;
await channel.send(msg); await channel.send(msg);
} catch (error) { } catch (error) {
console.log("Error while creating Nag:", error); // TODO console.log('Error while creating Nag:', error); // TODO
} }
} }
async loop() { async loop() {
// According to MDN we don't need to do this, but it makes me feel happier // 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 // knowing that we won't accidentally call clearInterval(...) on a timer
// that isn't running anymore. // that isn't running anymore.
this.interval = undefined; this.interval = undefined;
console.debug("nag.js main loop"); console.debug('nag.js main loop');
const guiltyNags = await findGuiltyNags(); const guiltyNags = await findGuiltyNags();
for (const nag of guiltyNags) { for (const nag of guiltyNags) {
await this.triggerNag(nag); await this.triggerNag(nag);
} }
this.interval = setTimeout(this.loop, nextCheckInMs()); this.interval = setTimeout(this.loop, nextCheckInMs());
} }
stop() { stop() {
clearInterval(this.interval); clearInterval(this.interval);
} }
} }

View File

@@ -1,31 +1,31 @@
import { import {
type ChatInputCommandInteraction, type ChatInputCommandInteraction,
SlashCommandBuilder, SlashCommandBuilder,
} from "discord.js"; } from 'discord.js';
import { type Settings, Nag } from "./service"; import {type Settings, Nag} from './service';
const data = new SlashCommandBuilder() const data = new SlashCommandBuilder()
.setName("unnag") .setName('unnag')
.setDescription("Remove a nag"); .setDescription('Remove a nag');
async function initialize(settings: Settings) {} async function initialize(settings: Settings) {}
async function execute(interaction: ChatInputCommandInteraction) { async function execute(interaction: ChatInputCommandInteraction) {
// Find all nags for this user // Find all nags for this user
const results = await Nag.findAll({ const results = await Nag.findAll({
where: { userId: interaction.user.id }, where: {userId: interaction.user.id},
}); });
for (const result of results) { for (const result of results) {
await result.destroy(); await result.destroy();
} }
return; return;
} }
export default function (settings: Settings) { export default function (settings: Settings) {
return { return {
data, data,
execute, execute,
initialize: async () => await initialize(settings), initialize: async () => await initialize(settings),
}; };
} }

190
index.ts
View File

@@ -1,11 +1,11 @@
import type { Interaction } from "discord.js"; import type {Interaction} from 'discord.js';
import { Client, Events, GatewayIntentBits, MessageFlags } from "discord.js"; import {Client, Events, GatewayIntentBits, MessageFlags} from 'discord.js';
import type { import type {
SlashCommandBuilder, SlashCommandBuilder,
SlashCommandOptionsOnlyBuilder, SlashCommandOptionsOnlyBuilder,
} from "discord.js"; } from 'discord.js';
import { sql, GuildSetting, initDb } from "./database"; import {sql, GuildSetting, initDb} from './database';
const BLITZCRANK_BANNER = ` const BLITZCRANK_BANNER = `
****++++++++++*+++ ****++++++++++*+++
@@ -63,105 +63,133 @@ const BLITZCRANK_BANNER = `
`; `;
const client = new Client({ const client = new Client({
intents: [ intents: [
GatewayIntentBits.Guilds, GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMembers,
GatewayIntentBits.MessageContent, GatewayIntentBits.MessageContent,
], ],
}); });
import { Routes } from "discord.js"; import {Routes} from 'discord.js';
import { guildId, appId, token, remindersChannelId } from "./config.json"; import {guildId, appId, token, remindersChannelId} from './config.json';
import { REST } from "discord.js"; import {REST} from 'discord.js';
const rest = new REST(); const rest = new REST();
rest.setToken(token); rest.setToken(token);
interface Command { interface Command {
data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder; data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder;
execute: (interaction: any) => Promise<void>; execute: (interaction: any) => Promise<void>;
initialize: (any) => Promise<void>; initialize: (any) => Promise<void>;
} }
import { Collection } from "discord.js"; import {Collection} from 'discord.js';
const commands = new Collection<string, Command>(); const commands = new Collection<string, Command>();
import PingCommand from "./commands/calendar/ping"; import PingCommand from './commands/calendar/ping';
import RemindCommand from "./commands/calendar/remind"; import RemindCommand from './commands/calendar/remind';
import QuoteCommand from "./commands/quotes/quote"; import QuoteCommand from './commands/quotes/quote';
import NagCommand from "./commands/calendar/nag/nag"; import NagCommand from './commands/calendar/nag/nag';
import UnnagCommand from "./commands/calendar/nag/unnag"; import UnnagCommand from './commands/calendar/nag/unnag';
import CheckinCommand from "./commands/calendar/nag/checkin"; 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}`); console.debug(`${remindersChannelId}`);
commands.set("ping", PingCommand({ client: client, db: sql })); commands.set('ping', PingCommand({client: client, db: sql}));
commands.set( commands.set(
"remind", 'remind',
RemindCommand({ RemindCommand({
client: client, client: client,
db: sql, db: sql,
publicChannel: remindersChannelId, publicChannel: remindersChannelId,
responseMode: "public", 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() { async function syncCommands() {
try { try {
console.log(`Started refreshing slash commands`); console.log(`Started refreshing slash commands`);
const _data = await rest.put( const _data = await rest.put(
Routes.applicationGuildCommands(appId, guildId), Routes.applicationGuildCommands(appId, guildId),
{ {
body: commands.mapValues((cmd) => cmd.data.toJSON()), body: commands.mapValues(cmd => cmd.data.toJSON()),
}, },
); );
console.log(`Successfully reloaded slash commands`); console.log(`Successfully reloaded slash commands`);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
} }
client.on(Events.InteractionCreate, async (interaction: Interaction) => { client.on(Events.InteractionCreate, async (interaction: Interaction) => {
if (!interaction.isChatInputCommand()) { if (!interaction.isChatInputCommand()) {
return; return;
} }
try { try {
const command = commands.get(interaction.commandName); const command = commands.get(interaction.commandName);
if (command == null) { if (command == null) {
await interaction.followUp(`No such command ${interaction.commandName}`); await interaction.followUp(`No such command ${interaction.commandName}`);
return; return;
} }
command.execute(interaction); command.execute(interaction);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
if (interaction.replied || interaction.deferred) { if (interaction.replied || interaction.deferred) {
await interaction.followUp({ await interaction.followUp({
content: "There was an error while executing this command", content: 'There was an error while executing this command',
flags: MessageFlags.Ephemeral, flags: MessageFlags.Ephemeral,
}); });
} }
} }
// TODO // TODO
}); });
client.once(Events.ClientReady, async (readyClient) => { client.once(Events.ClientReady, async readyClient => {
await syncCommands(); await syncCommands();
initDb(); // TODO initDb(); // TODO
GuildSetting.sync(); // TODO GuildSetting.sync(); // TODO
for (const [_name, cmd] of commands) { for (const [_name, cmd] of commands) {
await cmd?.initialize({ await cmd?.initialize({
client: client, client: client,
db: sql, db: sql,
}); });
} }
// Print banner // Print banner
for (const ln of BLITZCRANK_BANNER.split("\n")) { for (const ln of BLITZCRANK_BANNER.split('\n')) {
console.log(ln); console.log(ln);
} }
console.log(`Logged in as ${readyClient.user.tag}`); console.log(`Logged in as ${readyClient.user.tag}`);
}); });
client.login(token); client.login(token);