Files
blitzcrank/plugins/nag/index.ts

150 lines
4.2 KiB
TypeScript

import { TextChannel } from 'discord.js';
import { Op } from 'sequelize';
import { BasePlugin, type Command, type Settings } from '../../plugin'
import CheckinCommand from './checkin'
import checkin from './checkin';
import { CheckIn, initializeModels, Nag } from './models'
import NagCommand from './nag'
import UnnagCommand from './unnag'
/**
*
* @param hour Number between 0-24 representing the hour the check-in is performed.
* @param offset Number of days from today; this can be positive for future days or negative for past days.
* @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;
}
export async function findGuiltyNags() {
const results = await Nag.findAll();
const guiltyNags: Nag[] = [];
const prevCheckIn = getCheckIn(9, -1);
const currentCheckIn = getCheckIn(9, 0);
for (const nag of results) {
const checkInResults = await CheckIn.findAll({
where: {
id: nag.id,
lastCheckIn: {
[Op.between]: [prevCheckIn, currentCheckIn],
},
},
});
if (checkInResults.length <= 0) {
guiltyNags.push(nag);
}
}
return guiltyNags;
}
/**
*
* @returns The Date representing the next check-in, which will be either
* today (if we are asking in the morning) or tomorrow (if we are asking
* 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);
}
}
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;
}
/**
* Handle state for the various nag-related commands, mainly related to
* tracking of timers and triggers.
*/
export class NagPlugin extends BasePlugin {
// 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.)
interval?: NodeJS.Timeout;
constructor(settings: Settings) {
super(settings);
initializeModels(settings.database);
this.commands = [
CheckinCommand(settings) as Command, // TODO Why, TS?...
NagCommand(settings) as Command, // TODO Why, TS?...
UnnagCommand(settings),
]
}
async 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 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());
}
async stop() {
clearInterval(this.interval);
}
}