diff options
-rw-r--r-- | src/commands/utilities/remind.ts | 59 | ||||
-rw-r--r-- | src/commands/utilities/reminders.ts | 33 | ||||
-rw-r--r-- | src/lib/extensions/discord-akairo/BushClient.ts | 24 | ||||
-rw-r--r-- | src/lib/index.ts | 2 | ||||
-rw-r--r-- | src/lib/models/Reminder.ts | 77 | ||||
-rw-r--r-- | src/tasks/handleReminders.ts | 38 |
6 files changed, 223 insertions, 10 deletions
diff --git a/src/commands/utilities/remind.ts b/src/commands/utilities/remind.ts new file mode 100644 index 0000000..8df24c1 --- /dev/null +++ b/src/commands/utilities/remind.ts @@ -0,0 +1,59 @@ +import { ArgType, BushCommand, Reminder, type BushMessage, type BushSlashMessage } from '#lib'; + +export default class RemindCommand extends BushCommand { + public constructor() { + super('remind', { + aliases: ['remind', 'remindme', 'reminder'], + category: 'utilities', + description: 'Create reminders that will be DMed to you when the time expires.', + usage: ['remind <duration> <reason>'], + examples: ['template 1 2'], + args: [ + { + id: 'reason_and_duration', + type: 'contentWithDuration', + match: 'rest', + description: 'The reason to be reminded and the duration to remind the user in.', + prompt: 'What would you like to be reminded about and when?', + retry: '{error} Choose a reason to be reminded about with a duration for when to be notified.', + optional: true, + slashType: 'STRING' + } + ], + slash: true, + clientPermissions: (m) => util.clientSendAndPermCheck(m), + userPermissions: [] + }); + } + + public override async exec( + message: BushMessage | BushSlashMessage, + args: { reason_and_duration: ArgType<'contentWithDuration'> | string } + ) { + const { duration, contentWithoutTime: reason } = + typeof args.reason_and_duration === 'string' + ? await util.arg.cast('contentWithDuration', message, args.reason_and_duration) + : args.reason_and_duration; + + if (!reason?.trim()) return await message.util.reply(`${util.emojis.error} Please enter a reason to be reminded about.`); + if (!duration) return await message.util.reply(`${util.emojis.error} Please enter a duration.`); + + if (duration < 30_000) + return await message.util.reply(`${util.emojis.error} You cannot pick a duration less than 30 seconds.`); + + const created = new Date(); + const expires = new Date(Date.now() + duration); + const delta = util.format.bold(util.dateDelta(expires)); + + const success = await Reminder.create({ + content: reason.trim(), + messageUrl: message.url!, + user: message.author.id, + created, + expires + }).catch(() => false); + + if (!success) return await message.util.reply(`${util.emojis.error} Could not create a reminder.`); + return await message.util.reply(`${util.emojis.success} I will remind you in ${delta} (${util.timestamp(expires, 'T')}).`); + } +} diff --git a/src/commands/utilities/reminders.ts b/src/commands/utilities/reminders.ts new file mode 100644 index 0000000..7180aa9 --- /dev/null +++ b/src/commands/utilities/reminders.ts @@ -0,0 +1,33 @@ +import { BushCommand, ButtonPaginator, Reminder, type BushMessage, type BushSlashMessage } from '#lib'; +import { MessageEmbedOptions } from 'discord.js'; +import { Op } from 'sequelize'; + +export default class RemindersCommand extends BushCommand { + public constructor() { + super('reminders', { + aliases: ['reminders', 'view-reminders', 'list-reminders'], + category: 'utilities', + description: 'List all your current reminders.', + usage: ['reminder'], + examples: ['reminders'], + slash: true, + clientPermissions: (m) => util.clientSendAndPermCheck(m, 'EMBED_LINKS'), + userPermissions: [] + }); + } + + public override async exec(message: BushMessage | BushSlashMessage) { + const reminders = await Reminder.findAll({ where: { user: message.author.id, expires: { [Op.gt]: new Date() } } }); + if (!reminders.length) return message.util.send(`${util.emojis.error} You don't have any reminders set.`); + + const formattedReminders = reminders.map((reminder) => `${util.timestamp(reminder.expires, 't')} - ${reminder.content}`); + + const chunked = util.chunk(formattedReminders, 15); + const embeds: MessageEmbedOptions[] = chunked.map((chunk) => ({ + title: `Reminders`, + description: chunk.join('\n'), + color: util.colors.default + })); + return await ButtonPaginator.send(message, embeds); + } +} diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts index 41ecfaf..7321c17 100644 --- a/src/lib/extensions/discord-akairo/BushClient.ts +++ b/src/lib/extensions/discord-akairo/BushClient.ts @@ -1,3 +1,15 @@ +import { + abbreviatedNumber, + contentWithDuration, + discordEmoji, + duration, + durationSeconds, + globalUser, + messageLink, + permission, + roleWithDuration, + snowflake +} from '#args'; import type { BushApplicationCommand, BushBaseGuildEmojiManager, @@ -35,16 +47,6 @@ import path from 'path'; import readline from 'readline'; import type { Sequelize as SequelizeType } from 'sequelize'; import { fileURLToPath } from 'url'; -import { abbreviatedNumber } from '../../../arguments/abbreviatedNumber.js'; -import { contentWithDuration } from '../../../arguments/contentWithDuration.js'; -import { discordEmoji } from '../../../arguments/discordEmoji.js'; -import { duration } from '../../../arguments/duration.js'; -import { durationSeconds } from '../../../arguments/durationSeconds.js'; -import { globalUser } from '../../../arguments/globalUser.js'; -import { messageLink } from '../../../arguments/messageLink.js'; -import { permission } from '../../../arguments/permission.js'; -import { roleWithDuration } from '../../../arguments/roleWithDuration.js'; -import { snowflake } from '../../../arguments/snowflake.js'; import UpdateCacheTask from '../../../tasks/updateCache.js'; import UpdateStatsTask from '../../../tasks/updateStats.js'; import { ActivePunishment } from '../../models/ActivePunishment.js'; @@ -52,6 +54,7 @@ import { Global } from '../../models/Global.js'; import { Guild as GuildModel } from '../../models/Guild.js'; import { Level } from '../../models/Level.js'; import { ModLog } from '../../models/ModLog.js'; +import { Reminder } from '../../models/Reminder.js'; import { Stat } from '../../models/Stat.js'; import { StickyRole } from '../../models/StickyRole.js'; import { AllowedMentions } from '../../utils/AllowedMentions.js'; @@ -425,6 +428,7 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re Level.initModel(this.db); StickyRole.initModel(this.db); Stat.initModel(this.db); + Reminder.initModel(this.db); await this.db.sync({ alter: true }); // Sync all tables to fix everything if updated await this.console.success('startup', `Successfully connected to <<database>>.`, false); } catch (e) { diff --git a/src/lib/index.ts b/src/lib/index.ts index 65a6e74..94a7dd9 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -24,6 +24,7 @@ export * from './extensions/discord.js/BushApplicationCommand.js'; export type { BushApplicationCommandManager } from './extensions/discord.js/BushApplicationCommandManager.js'; export type { BushApplicationCommandPermissionsManager } from './extensions/discord.js/BushApplicationCommandPermissionsManager.js'; export type { BushBaseGuildEmojiManager } from './extensions/discord.js/BushBaseGuildEmojiManager.js'; +export type { BushBaseGuildVoiceChannel } from './extensions/discord.js/BushBaseGuildVoiceChannel.js'; export * from './extensions/discord.js/BushButtonInteraction.js'; export * from './extensions/discord.js/BushCategoryChannel.js'; export type { BushChannel } from './extensions/discord.js/BushChannel.js'; @@ -68,6 +69,7 @@ export * from './models/Global.js'; export * from './models/Guild.js'; export * from './models/Level.js'; export * from './models/ModLog.js'; +export * from './models/Reminder.js'; export * from './models/Stat.js'; export * from './models/StickyRole.js'; export * from './utils/AllowedMentions.js'; diff --git a/src/lib/models/Reminder.ts b/src/lib/models/Reminder.ts new file mode 100644 index 0000000..b8cd669 --- /dev/null +++ b/src/lib/models/Reminder.ts @@ -0,0 +1,77 @@ +import { Snowflake } from 'discord.js'; +import { nanoid } from 'nanoid'; +import { type Sequelize } from 'sequelize'; +import { BaseModel } from './BaseModel.js'; +const { DataTypes } = (await import('sequelize')).default; + +export interface ReminderModel { + id: string; + user: Snowflake; + messageUrl: string; + content: string; + created: Date; + expires: Date; + notified: boolean; +} + +export interface ReminderModelCreationAttributes { + id?: string; + user: Snowflake; + messageUrl: string; + content: string; + created: Date; + expires: Date; + notified?: boolean; +} + +export class Reminder extends BaseModel<ReminderModel, ReminderModelCreationAttributes> implements ReminderModel { + /** + * The id of the reminder. + */ + public declare id: string; + + /** + * The user that the reminder is for. + */ + public declare user: Snowflake; + + /** + * The url of the message where the reminder was created. + */ + public declare messageUrl: string; + + /** + * The content of the reminder. + */ + public declare content: string; + + /** + * The date the reminder was created. + */ + public declare created: Date; + + /** + * The date when the reminder expires. + */ + public declare expires: Date; + + /** + * Whether the user has been notified about the reminder. + */ + public declare notified: boolean; + + public static initModel(sequelize: Sequelize): void { + Reminder.init( + { + id: { type: DataTypes.STRING, primaryKey: true, defaultValue: nanoid }, + user: { type: DataTypes.STRING, allowNull: false }, + messageUrl: { type: DataTypes.STRING, allowNull: false }, + content: { type: DataTypes.TEXT, allowNull: false }, + created: { type: DataTypes.DATE, allowNull: false }, + expires: { type: DataTypes.DATE, allowNull: false }, + notified: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false } + }, + { sequelize } + ); + } +} diff --git a/src/tasks/handleReminders.ts b/src/tasks/handleReminders.ts new file mode 100644 index 0000000..935a08c --- /dev/null +++ b/src/tasks/handleReminders.ts @@ -0,0 +1,38 @@ +import { BushTask, Reminder } from '#lib'; +const { Op } = (await import('sequelize')).default; + +export default class HandlerRemindersTask extends BushTask { + public constructor() { + super('handlerReminders', { + delay: 30_000, // 30 seconds + runOnStart: true + }); + } + + public override async exec() { + const expiredEntries = await Reminder.findAll({ + where: { + expires: { + [Op.lt]: new Date(Date.now() + 30_000) // Find all rows with an expiry date before 10 seconds from now + }, + notified: false + } + }); + + void client.logger.verbose(`handlerReminders`, `Queried reminders, found <<${expiredEntries.length}>> expired reminders.`); + + for (const entry of expiredEntries) { + setTimeout(() => { + void client.users + .send( + entry.user, + `The reminder you set ${util.dateDelta(entry.created)} ago has expired: ${util.format.bold(entry.content)}\n${ + entry.messageUrl + }` + ) + .catch(() => false); + void entry.update({ notified: true }); + }, entry.expires.getTime() - new Date().getTime()); + } + } +} |