Implement /unnag
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user