Refactor to more consistent structure (e.g. "plugins")
This commit is contained in:
44
plugins/nag/checkin.ts
Normal file
44
plugins/nag/checkin.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
type ChatInputCommandInteraction,
|
||||
SlashCommandBuilder,
|
||||
} from 'discord.js';
|
||||
|
||||
import type {Settings} from '../../plugin';
|
||||
import {CheckIn, Nag} from './models';
|
||||
|
||||
export default function (_settings: Settings) {
|
||||
return {
|
||||
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'),
|
||||
),
|
||||
|
||||
execute: async (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;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
99
plugins/nag/index.test.ts
Normal file
99
plugins/nag/index.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import {Sequelize } from 'sequelize';
|
||||
import {afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
findGuiltyNags,
|
||||
getCheckIn,
|
||||
nextCheckInDate,
|
||||
} from '.';
|
||||
import { CheckIn, initializeModels, Nag } from './models'
|
||||
|
||||
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());
|
||||
const 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);
|
||||
const 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,
|
||||
};
|
||||
|
||||
await initializeModels(sequelize);
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.useFakeTimers();
|
||||
await Nag.sync();
|
||||
await CheckIn.sync();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Nag.drop();
|
||||
await CheckIn.drop();
|
||||
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('Ignores nags with a recent check-in', async () => {
|
||||
const currentCheckInTime = getCheckIn(9, 0);
|
||||
vi.setSystemTime(currentCheckInTime);
|
||||
const newNag = await Nag.create(exampleNag);
|
||||
newNag.save();
|
||||
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),
|
||||
});
|
||||
console.log(newCheckIn.lastCheckIn);
|
||||
newCheckIn.save();
|
||||
const results = await findGuiltyNags();
|
||||
expect(results.map(nag => nag.userId)).toEqual([]);
|
||||
});
|
||||
});
|
||||
149
plugins/nag/index.ts
Normal file
149
plugins/nag/index.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
72
plugins/nag/models.ts
Normal file
72
plugins/nag/models.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
BOOLEAN,
|
||||
DATE,
|
||||
INTEGER,
|
||||
Model,
|
||||
type Sequelize,
|
||||
STRING,
|
||||
} from 'sequelize';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export class CheckIn extends Model {
|
||||
// Date of the last time user ran /checkin
|
||||
declare lastCheckIn: Date;
|
||||
}
|
||||
|
||||
export async function initializeModels(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(
|
||||
{
|
||||
lastCheckIn: {
|
||||
type: DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
},
|
||||
{sequelize},
|
||||
);
|
||||
Nag.hasMany(CheckIn);
|
||||
CheckIn.belongsTo(Nag);
|
||||
await Nag.sync();
|
||||
await CheckIn.sync();
|
||||
}
|
||||
92
plugins/nag/nag.ts
Normal file
92
plugins/nag/nag.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import {Chrono} from 'chrono-node';
|
||||
import {
|
||||
type ChatInputCommandInteraction,
|
||||
SlashCommandBuilder,
|
||||
} from 'discord.js';
|
||||
import type { Settings } from '../../plugin'
|
||||
import {CheckIn, Nag} from './models';
|
||||
|
||||
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"]],
|
||||
});
|
||||
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: 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')
|
||||
.setRequired(false),
|
||||
)
|
||||
.addBooleanOption(option =>
|
||||
option
|
||||
.setName('mentionhere')
|
||||
.setDescription('Whether to DM you or @ this channel')
|
||||
.setRequired(false),
|
||||
),
|
||||
execute,
|
||||
};
|
||||
}
|
||||
25
plugins/nag/unnag.ts
Normal file
25
plugins/nag/unnag.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
type ChatInputCommandInteraction,
|
||||
SlashCommandBuilder,
|
||||
} from 'discord.js';
|
||||
import type {Settings} from '../../plugin';
|
||||
import {Nag} from './models';
|
||||
|
||||
export default function (_settings: Settings) {
|
||||
return {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('unnag')
|
||||
.setDescription('Remove a nag'),
|
||||
execute: async (interaction: ChatInputCommandInteraction) => {
|
||||
// Find all nags for this user and delete them.
|
||||
// TODO In the future, we should support having multiple nags
|
||||
const results = await Nag.findAll({
|
||||
where: {userId: interaction.user.id},
|
||||
});
|
||||
for (const result of results) {
|
||||
await result.destroy();
|
||||
}
|
||||
return;
|
||||
},
|
||||
};
|
||||
}
|
||||
22
plugins/ping.ts
Normal file
22
plugins/ping.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import {SlashCommandBuilder} from 'discord.js';
|
||||
|
||||
import {BasePlugin, type Settings} from '../plugin';
|
||||
|
||||
export class PingPlugin extends BasePlugin {
|
||||
constructor(settings: Settings) {
|
||||
super(settings);
|
||||
this.commands = [
|
||||
{
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('ping')
|
||||
.setDescription('Send a ping to the bot'),
|
||||
|
||||
execute: async interaction => {
|
||||
await interaction.reply(
|
||||
`Pong! This command was run by ${interaction.user.username}, who joined on ${interaction.member?.joinedAt}.`,
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
29
plugins/quote/index.ts
Normal file
29
plugins/quote/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type {ChatInputCommandInteraction} from 'discord.js';
|
||||
import {SlashCommandBuilder} from 'discord.js';
|
||||
import {BasePlugin, type Settings } from '../../plugin';
|
||||
import {quotes} from './quotes.json';
|
||||
|
||||
export class QuotePlugin extends BasePlugin {
|
||||
constructor(settings: Settings) {
|
||||
super(settings);
|
||||
this.commands = [
|
||||
{
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('quote')
|
||||
.setDescription('Print a quote from League of Legends.'),
|
||||
|
||||
execute: async (interaction: ChatInputCommandInteraction) => {
|
||||
try {
|
||||
const index = Math.floor(Math.random() * quotes.length);
|
||||
await interaction.reply?.({
|
||||
content: `> *${quotes[index].quote}*
|
||||
> — ${quotes[index].author}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Problem sending to channel: ${error}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
519
plugins/quote/quotes.json
Normal file
519
plugins/quote/quotes.json
Normal file
@@ -0,0 +1,519 @@
|
||||
{
|
||||
"quotes": [
|
||||
{ "author": "Blitzcrank", "quote": "A rolling golem gathers no rust." },
|
||||
{ "author": "Blitzcrank", "quote": "Fired up and ready to serve." },
|
||||
{ "author": "Blitzcrank", "quote": "Metal is harder than flesh." },
|
||||
{
|
||||
"author": "Camille",
|
||||
"quote": "[Speaking to Warwick] We are all monsters. Now, you are just one on the outside."
|
||||
},
|
||||
{ "author": "Camille", "quote": "Efficiency is paramount to success." },
|
||||
{ "author": "Camille", "quote": "Elegance never goes out of fashion." },
|
||||
{
|
||||
"author": "Camille",
|
||||
"quote": "Extremes are easy, it's the balance that is difficult."
|
||||
},
|
||||
{
|
||||
"author": "Camille",
|
||||
"quote": "I don't play the game, I make the rules."
|
||||
},
|
||||
{
|
||||
"author": "Camille",
|
||||
"quote": "I'm what you would call a 'deniable asset'."
|
||||
},
|
||||
{
|
||||
"author": "Camille",
|
||||
"quote": "It is not the weapon that defines you, but how you wield it."
|
||||
},
|
||||
{
|
||||
"author": "Camille",
|
||||
"quote": "It's not lies that cut, but the sharpness of the truth."
|
||||
},
|
||||
{ "author": "Camille", "quote": "Mediocrity is the root of all evil." },
|
||||
{
|
||||
"author": "Camille",
|
||||
"quote": "Morality is a beautiful servant and a dangerous master."
|
||||
},
|
||||
{
|
||||
"author": "Camille",
|
||||
"quote": "Precision is the difference between a butcher and a surgeon."
|
||||
},
|
||||
{
|
||||
"author": "Camille",
|
||||
"quote": "Privilege must be preserved at all costs."
|
||||
},
|
||||
{ "author": "Camille", "quote": "Progress is honed on necessary death." },
|
||||
{
|
||||
"author": "Camille",
|
||||
"quote": "Progress is served by technology, not controlled by it."
|
||||
},
|
||||
{
|
||||
"author": "Camille",
|
||||
"quote": "Regret is what tempers the steel of our soul."
|
||||
},
|
||||
{ "author": "Camille", "quote": "Results are all that matters." },
|
||||
{
|
||||
"author": "Camille",
|
||||
"quote": "Self-made women need to be more prevalent."
|
||||
},
|
||||
{
|
||||
"author": "Camille",
|
||||
"quote": "Sometimes scars are the most refined attire one can wear."
|
||||
},
|
||||
{
|
||||
"author": "Camille",
|
||||
"quote": "The right word cuts more deeply than a knife."
|
||||
},
|
||||
{
|
||||
"author": "Camille",
|
||||
"quote": "The task at hand is the only one that matters."
|
||||
},
|
||||
{
|
||||
"author": "Camille",
|
||||
"quote": "The world is not black or white, but a delicious shade of grey."
|
||||
},
|
||||
{ "author": "Camille", "quote": "Violence is a means to an end." },
|
||||
{
|
||||
"author": "Ekko",
|
||||
"quote": "A second chance? I thought I was on my fifth!"
|
||||
},
|
||||
{ "author": "Ekko", "quote": "Good a time as any to act reckless." },
|
||||
{
|
||||
"author": "Ekko",
|
||||
"quote": "It's not how much time you have, it's how you use it."
|
||||
},
|
||||
{ "author": "Ekko", "quote": "Never had luck. Never needed it." },
|
||||
{ "author": "Ekko", "quote": "Time doesn't heal all wounds." },
|
||||
{
|
||||
"author": "Heimerdinger",
|
||||
"quote": "42... there's just something about that number."
|
||||
},
|
||||
{
|
||||
"author": "Heimerdinger",
|
||||
"quote": "I prefer a battle of wits, but you're unarmed!"
|
||||
},
|
||||
{
|
||||
"author": "Heimerdinger",
|
||||
"quote": "Why do chemists call helium, curium, and barium 'the medical elements'? Because, if you can't 'helium' or 'curium', you 'barium'! Hm hm!"
|
||||
},
|
||||
{ "author": "Heimerdinger", "quote": "ヽ༼ຈل͜ຈ༽ノ raise your dongers" },
|
||||
{ "author": "Jhin", "quote": "Art must exist beyond reason." },
|
||||
{ "author": "Jhin", "quote": "Four!" },
|
||||
{ "author": "Jhin", "quote": "I cannot be good. I must be perfection." },
|
||||
{
|
||||
"author": "Jhin",
|
||||
"quote": "I swear each performance is the last, but I lie every time."
|
||||
},
|
||||
{
|
||||
"author": "Jhin",
|
||||
"quote": "In carnage, I bloom, like a flower in the dawn."
|
||||
},
|
||||
{
|
||||
"author": "Jhin",
|
||||
"quote": "It is by my will alone I set my mind in motion."
|
||||
},
|
||||
{
|
||||
"author": "Jhin",
|
||||
"quote": "It's fun to kill a man, to take all that he had, and could ever have."
|
||||
},
|
||||
{ "author": "Jhin", "quote": "You will learn what beauty truly is." },
|
||||
{
|
||||
"author": "Jinx",
|
||||
"quote": "Fishbones, you know what we oughta do? 'Do the laundry, wash dishes and pay some bills.' Stupid dumb rocket launcher..."
|
||||
},
|
||||
{ "author": "Jinx", "quote": "I'm crazy! Got a doctor's note." },
|
||||
{ "author": "Jinx", "quote": "I'm trying to care! But I just... can't!" },
|
||||
{
|
||||
"author": "Jinx",
|
||||
"quote": "Rules are made to be broken... like buildings! Or people!"
|
||||
},
|
||||
{
|
||||
"author": "Joseph Miklos",
|
||||
"quote": "'It's only a short way'? Is that a short joke?!"
|
||||
},
|
||||
{ "author": "Joseph Miklos", "quote": "I am evil! Stop laughing!" },
|
||||
{
|
||||
"author": "Joseph Miklos",
|
||||
"quote": "Know that if the tables were turned, I would show you no mercy!"
|
||||
},
|
||||
{ "author": "Joseph Miklos", "quote": "You will die by my hand!" },
|
||||
{ "author": "Mordekaiser", "quote": "Ah, life is a bitter shame." },
|
||||
{
|
||||
"author": "Mordekaiser",
|
||||
"quote": "All mortals reek with the stench of decaying flesh."
|
||||
},
|
||||
{
|
||||
"author": "Mordekaiser",
|
||||
"quote": "Fools fear death, the strong wield it."
|
||||
},
|
||||
{
|
||||
"author": "Mordekaiser",
|
||||
"quote": "I alone am the bastion between eternal existence and oblivion."
|
||||
},
|
||||
{
|
||||
"author": "Mordekaiser",
|
||||
"quote": "I carve my kingdom beyond, from the ashes of nothing, no mortals, not even gods, will stop me from claiming what is mine."
|
||||
},
|
||||
{
|
||||
"author": "Mordekaiser",
|
||||
"quote": "I have bent the realm of the dead to my will, this world shall be next."
|
||||
},
|
||||
{
|
||||
"author": "Mordekaiser",
|
||||
"quote": "I raise my iron fist to subjugate the living."
|
||||
},
|
||||
{
|
||||
"author": "Mordekaiser",
|
||||
"quote": "I will grind their petty souls into mortar."
|
||||
},
|
||||
{
|
||||
"author": "Mordekaiser",
|
||||
"quote": "I will silence the incessant thrum of mortal hearts."
|
||||
},
|
||||
{
|
||||
"author": "Mordekaiser",
|
||||
"quote": "In the world beyond, blackened ichor filled a crumbling sky, as souls withered to nothing. But I refused to fade."
|
||||
},
|
||||
{
|
||||
"author": "Mordekaiser",
|
||||
"quote": "Mortals plan in fear for tomorrow, I build for eternity."
|
||||
},
|
||||
{
|
||||
"author": "Mordekaiser",
|
||||
"quote": "Naive men pray to the gods; they will learn to pray to me."
|
||||
},
|
||||
{
|
||||
"author": "Mordekaiser",
|
||||
"quote": "Only the worthy receive the gift of Nightfall's kiss."
|
||||
},
|
||||
{
|
||||
"author": "Mordekaiser",
|
||||
"quote": "Shed the frailty of flesh, embrace the cold edge of iron."
|
||||
},
|
||||
{
|
||||
"author": "Mordekaiser",
|
||||
"quote": "The dead belong to me, the living shall be next."
|
||||
},
|
||||
{
|
||||
"author": "Mordekaiser",
|
||||
"quote": "The world has tried to forget my existence, time to remind them why they fear."
|
||||
},
|
||||
{ "author": "Mordekaiser", "quote": "Twice slain, thrice born." },
|
||||
{
|
||||
"author": "Mordekaiser",
|
||||
"quote": "Weaklings cower in the light, I bring eternal darkness."
|
||||
},
|
||||
{
|
||||
"author": "Poppy",
|
||||
"quote": "I'm in, one-hundred-percent! That's everything, right?"
|
||||
},
|
||||
{ "author": "Poppy", "quote": "I'm no hero—just a Yordle with a hammer." },
|
||||
{ "author": "Poppy", "quote": "Just had a thought—three pigtails!" },
|
||||
{
|
||||
"author": "Poppy",
|
||||
"quote": "The hammer does most of the work, I just swing it."
|
||||
},
|
||||
{ "author": "Rammus", "quote": "OK." },
|
||||
{ "author": "Rammus", "quote": "🆗" },
|
||||
{ "author": "Senna", "quote": "I forgive. No one else has to." },
|
||||
{
|
||||
"author": "Senna",
|
||||
"quote": "I remember my nightmares. Wish I could remember to dream."
|
||||
},
|
||||
{ "author": "Sion", "quote": "A black eye for the earth!" },
|
||||
{ "author": "Sion", "quote": "Death had its chance." },
|
||||
{ "author": "Sion", "quote": "Noxus suffers no cowards." },
|
||||
{ "author": "Sion", "quote": "The quiet... eats at me." },
|
||||
{ "author": "Swain", "quote": "A calculated risk is no risk at all." },
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "A new vantage, is all the advantage I need."
|
||||
},
|
||||
{ "author": "Swain", "quote": "And to think, they called me a 'cripple'." },
|
||||
{ "author": "Swain", "quote": "Destiny marches—like any man." },
|
||||
{ "author": "Swain", "quote": "Diplomacy is a subtle art." },
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "Hmph... I do so enjoy explaining things to idiots."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "I cannot lead if I allow fools to stumble about before me."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "I could kill them all. But it would be far crueler to show them that I am right."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "I have killed more men with words than by my own hand. Not for lack of trying."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "I suppose I should be grateful they have the decency to fear me."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "I've heard what they call me. What a waste of their final words."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "If they already call me a villain, what will they call me when I succeed?"
|
||||
},
|
||||
{ "author": "Swain", "quote": "Is it not enough for Noxus to be strong?" },
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "It is not the visions that haunt me—but what I do not see."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "Never make a bargain with a demon... that you intend to keep."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "One can read the future in battle lines, assuming one can read."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "People often ask for a hero, when a villain is what they truly need."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "Pity stays the hand of the merciful, but not mine."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "Tell me again all the crimes I've committed, and I'll tell you the price of victory."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "The more they try to kill me, the more they reveal I am on the right path."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "The outcome was decided when they brought an army; and I brought a demon."
|
||||
},
|
||||
{ "author": "Swain", "quote": "The right to rule, held, in my hand." },
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "There is always a choice. The truth is no exception."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "They are blind to the cold logic of this world."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "They are five steps from realizing: I am ten steps ahead."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "They expect me to play fairly... We aren't even playing the same game."
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "What is one more demon, when I already have so many?"
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "Would they even struggle to survive, if they knew what was to come?"
|
||||
},
|
||||
{
|
||||
"author": "Swain",
|
||||
"quote": "You can sit on a throne, that doesn't make you a ruler. It only means you have an arse."
|
||||
},
|
||||
{
|
||||
"author": "Tahm Kench",
|
||||
"quote": "All creation is born famished and starving."
|
||||
},
|
||||
{
|
||||
"author": "Tahm Kench",
|
||||
"quote": "Child, you're a couple cows short of a steak!"
|
||||
},
|
||||
{
|
||||
"author": "Tahm Kench",
|
||||
"quote": "The only real sin is to deny a craving."
|
||||
},
|
||||
{
|
||||
"author": "Tahm Kench",
|
||||
"quote": "We all gourmandize from time to time."
|
||||
},
|
||||
{ "author": "Teemo", "quote": "Size doesn't mean everything." },
|
||||
{ "author": "Thresh", "quote": "I am the thing under the bed." },
|
||||
{ "author": "Thresh", "quote": "Me, mad? Haha... quite likely." },
|
||||
{
|
||||
"author": "Urgot",
|
||||
"quote": "Cast into a pit of despair, I climbed out on the corpses."
|
||||
},
|
||||
{
|
||||
"author": "Urgot",
|
||||
"quote": "I am stronger than man, stronger than machine, I am an idea."
|
||||
},
|
||||
{
|
||||
"author": "Urgot",
|
||||
"quote": "I am the very definition of a self-made man."
|
||||
},
|
||||
{
|
||||
"author": "Urgot",
|
||||
"quote": "If they do not stop me, they will die. It is just that simple."
|
||||
},
|
||||
{ "author": "Urgot", "quote": "Pain is the act of becoming." },
|
||||
{
|
||||
"author": "Urgot",
|
||||
"quote": "We will rise from the rubble, stronger than before."
|
||||
},
|
||||
{
|
||||
"author": "Urgot",
|
||||
"quote": "You cannot know strength... Until you are broken."
|
||||
},
|
||||
{
|
||||
"author": "Veigar",
|
||||
"quote": "'It's only a short way'? Is that a short joke?!"
|
||||
},
|
||||
{ "author": "Veigar", "quote": "I am evil! Stop laughing!" },
|
||||
{
|
||||
"author": "Veigar",
|
||||
"quote": "Know that if the tables were turned, I would show you no mercy!"
|
||||
},
|
||||
{
|
||||
"author": "Vex",
|
||||
"quote": "'Death is the true meaning of life.' Whoa! That's deep."
|
||||
},
|
||||
{
|
||||
"author": "Vex",
|
||||
"quote": "[Speaking to Lux] Oh, no. Happiness and rainbows? I'm gonna barf twice."
|
||||
},
|
||||
{
|
||||
"author": "Vex",
|
||||
"quote": "And then I told her, 'Get outta my room!' And she said, 'This is my house, young lady and'... Oh, hang on, Shadow. I'll finish this later."
|
||||
},
|
||||
{ "author": "Vex", "quote": "Calm down, Shadow. I'm trying to sulk." },
|
||||
{
|
||||
"author": "Vex",
|
||||
"quote": "I am not cute. I am dark and forlorn and hopelessly morbid!"
|
||||
},
|
||||
{
|
||||
"author": "Vex",
|
||||
"quote": "I could start a club for people who hate people! Ehh, but no one would show up."
|
||||
},
|
||||
{
|
||||
"author": "Vex",
|
||||
"quote": "This is going to be... awful, in a very good way. A good, awful way. You know what I mean!"
|
||||
},
|
||||
{
|
||||
"author": "Vex",
|
||||
"quote": "Welcome to Sad Town. Population: Me. Everyone else get out."
|
||||
},
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "All that is logical is true, absolute, irrefutable."
|
||||
},
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "Choice is false. It is how we clothe and forgive the baser instincts that spur us to division."
|
||||
},
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "Emotion and logic cannot coexist. One must be shed to gain the other."
|
||||
},
|
||||
{ "author": "Viktor", "quote": "Emotion... clashes with reason." },
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "Governed by instinct, humans are no more than flawed and flailing animals."
|
||||
},
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "Hexcorization requires no justification. What purpose is there in explaining a horseshoe to the horse?"
|
||||
},
|
||||
{ "author": "Viktor", "quote": "Humanity... is self-corrupting." },
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "I am not the man I was, but who I wished to be."
|
||||
},
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "I am the only one with the means to cure suffering. But it is a lonely path."
|
||||
},
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "I chose to become this. Difficulty had no bearing nor did danger. It was... necessary."
|
||||
},
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "I did not know true... beauty, until the Arcane."
|
||||
},
|
||||
{ "author": "Viktor", "quote": "I offer no choice, for there is none." },
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "I sense... trepidation. But one cannot grow if left unchallenged."
|
||||
},
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "Mankind clings to its past. Glorifies its present. And lives in dread of tomorrow."
|
||||
},
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "Passion double-crosses, subverts, divides."
|
||||
},
|
||||
{ "author": "Viktor", "quote": "Sentiment is incompatible with control." },
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "So much of what we value is inconsequential."
|
||||
},
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "They think humanity can survive with emotion? Survive as what? Creatures blinded by impulse?"
|
||||
},
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "To live with flaws, is to be subject to them."
|
||||
},
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "True change must be imposed, not offered."
|
||||
},
|
||||
{
|
||||
"author": "Viktor",
|
||||
"quote": "What I am doing is not torment. Torment is allowing the mind to corrupt the soul."
|
||||
},
|
||||
{ "author": "Volibear", "quote": "A thousand scars, what is one more?" },
|
||||
{
|
||||
"author": "Volibear",
|
||||
"quote": "The creations of mortals fail. The wild remains."
|
||||
},
|
||||
{
|
||||
"author": "Volibear",
|
||||
"quote": "The land slumbers, but it is not dead. With my roar, I wake it. With my thunder, I call it."
|
||||
},
|
||||
{
|
||||
"author": "Volibear",
|
||||
"quote": "They have forgotten the old ways. The old ways have not forgotten them."
|
||||
},
|
||||
{
|
||||
"author": "Volibear",
|
||||
"quote": "Warm-bloods rose on two legs... and forgot how to run."
|
||||
},
|
||||
{
|
||||
"author": "Xerath",
|
||||
"quote": "I am power incarnate! Who dares oppose me?"
|
||||
},
|
||||
{ "author": "Xerath", "quote": "I am the will of man, unbound by flesh." },
|
||||
{
|
||||
"author": "Xerath",
|
||||
"quote": "I see the forces that hold the universe together."
|
||||
},
|
||||
{ "author": "Xerath", "quote": "The secrets of magic are mine alone." },
|
||||
{
|
||||
"author": "Yone",
|
||||
"quote": "Long before blades and sorcery are needed, words... can save a soul."
|
||||
},
|
||||
{
|
||||
"author": "Yone",
|
||||
"quote": "Sleep is not for the weak, but for the blessed."
|
||||
},
|
||||
{
|
||||
"author": "Yone",
|
||||
"quote": "Sometimes, to save someone, you must fight them."
|
||||
}
|
||||
]
|
||||
}
|
||||
136
plugins/remind/index.ts
Normal file
136
plugins/remind/index.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import * as chrono from 'chrono-node';
|
||||
import {
|
||||
type ChatInputCommandInteraction,
|
||||
SlashCommandBuilder,
|
||||
TextChannel,
|
||||
} from 'discord.js';
|
||||
import * as sequelize from 'sequelize';
|
||||
|
||||
import {BasePlugin, type Command, type Settings} from '../../plugin';
|
||||
|
||||
import {initializeModels, Reminder, syncModels} from './models';
|
||||
|
||||
async function triggerReminder(channel: TextChannel, reminder: Reminder) {
|
||||
try {
|
||||
await channel.send({
|
||||
content: `Reminder for <@${reminder.userId}>: ${reminder.text}`,
|
||||
});
|
||||
reminder.isValid = false;
|
||||
} catch (error) {
|
||||
console.log(`Error trigggering Reminder ${reminder.id}: ${error}`);
|
||||
} finally {
|
||||
await reminder.save();
|
||||
}
|
||||
}
|
||||
|
||||
export class RemindPlugin extends BasePlugin {
|
||||
interval: NodeJS.Timeout;
|
||||
|
||||
constructor(settings: Settings) {
|
||||
super(settings);
|
||||
initializeModels(settings.database);
|
||||
this.commands = [
|
||||
{
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('remind')
|
||||
.setDescription('Remind me to do something')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('when')
|
||||
.setDescription('Short description of when you want the reminder')
|
||||
.setRequired(true),
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('what')
|
||||
.setDescription(
|
||||
'Short description of what you want to be reminded of',
|
||||
)
|
||||
.setRequired(true),
|
||||
),
|
||||
|
||||
execute: async (interaction: ChatInputCommandInteraction) => {
|
||||
const whenString = interaction.options.getString('when') ?? 'now';
|
||||
const when = chrono.parseDate(whenString);
|
||||
if (!when) {
|
||||
await interaction.reply(
|
||||
`Sorry, I don't understand '${when}' as a date`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const reminder = await Reminder.create({
|
||||
userId: interaction.user.id,
|
||||
text: interaction.options.getString('what'),
|
||||
trigger: when, // TODO
|
||||
requestMessage: interaction.id,
|
||||
requestChannel: interaction.channelId,
|
||||
isValid: true,
|
||||
});
|
||||
reminder.save();
|
||||
await interaction.reply(`Ok, I'll remind you at ${when}`);
|
||||
console.info(
|
||||
`Created Reminder ${reminder.id} to be triggered at ${reminder.trigger}`,
|
||||
);
|
||||
},
|
||||
} as Command, // TODO
|
||||
];
|
||||
}
|
||||
|
||||
getChannel(reminder: Reminder) {
|
||||
// TODO I don't really know what else needs to go into this function
|
||||
// I had some permissions stuff but it seemed very out-of-place
|
||||
// I don't know why isSendable() doesn't cover permissions? Or maybe
|
||||
// it does?
|
||||
// Either way this function kind of smells
|
||||
const client = this.settings.client;
|
||||
const channel = client.channels.cache.get(reminder.requestChannel);
|
||||
if (!(channel instanceof TextChannel)) {
|
||||
throw TypeError("Can't send to a non-ext channel");
|
||||
}
|
||||
if (!channel) {
|
||||
throw Error('Cannot find valid channel to send reminder on');
|
||||
}
|
||||
if (!channel.isSendable()) {
|
||||
throw Error("Can't send to that channel");
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
async loop() {
|
||||
console.debug('remind.js main loop');
|
||||
const results = await Reminder.findAll({
|
||||
// NOTE: 'trigger' is the time when the reminder should go off. If trigger -
|
||||
// now is positive, trigger is in the future. If trigger - now <=
|
||||
// 1.0, then we have less than one minute before the timer should go
|
||||
// off.
|
||||
where: sequelize.literal(
|
||||
"isValid AND (julianday(trigger) - julianday('now')) <= 1.0",
|
||||
),
|
||||
});
|
||||
for (const reminder of results) {
|
||||
try {
|
||||
const now = new Date(Date.now());
|
||||
const delay = reminder.trigger.getTime() - now.getTime();
|
||||
console.log(
|
||||
`Callback for Reminder ${reminder.get('id')} triggering in ${delay}ms`,
|
||||
);
|
||||
const channel = this.getChannel(reminder);
|
||||
setTimeout(async () => await triggerReminder(channel, reminder), delay);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error while processing Reminder ${reminder.id}: ${error}`,
|
||||
);
|
||||
console.trace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
await syncModels();
|
||||
this.interval = setInterval(this.loop.bind(this), 1000 * 60);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
}
|
||||
41
plugins/remind/models.ts
Normal file
41
plugins/remind/models.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { BOOLEAN, DATE, INTEGER, Model, Sequelize, TEXT } from "sequelize";
|
||||
|
||||
export class Reminder extends Model {
|
||||
declare id: number;
|
||||
declare userId: string;
|
||||
declare text: string | null;
|
||||
declare trigger: Date;
|
||||
declare isValid: boolean;
|
||||
declare requestChannel: string;
|
||||
declare requestMessage: string;
|
||||
}
|
||||
|
||||
export function initializeModels(sequelize: Sequelize) {
|
||||
Reminder.init(
|
||||
{
|
||||
id: {
|
||||
type: INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
},
|
||||
userId: {
|
||||
type: TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
text: TEXT,
|
||||
trigger: {
|
||||
type: DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
isValid: BOOLEAN,
|
||||
requestChannel: TEXT,
|
||||
requestMessage: TEXT,
|
||||
},
|
||||
{sequelize},
|
||||
);
|
||||
// await Reminder.sync(); // TODO
|
||||
}
|
||||
|
||||
export async function syncModels() {
|
||||
await Reminder.sync();
|
||||
}
|
||||
Reference in New Issue
Block a user