const { PermissionFlagsBits, ApplicationCommandOptionType, ChannelType } = require('discord.js') const cron = require('node-cron') const scheduledMessages = require('../../model/scheduledMessages') const scheduler = require('../../scheduler/scheduler') const { parseDuration } = require('../../utils/duration') module.exports = { data: { name: 'schedule', description: 'Manage recurring and one-off scheduled channel messages.', default_member_permissions: PermissionFlagsBits.ManageGuild.toString(), options: [ { name: 'recurring', description: 'Schedule a recurring message on a cron schedule.', type: ApplicationCommandOptionType.Subcommand, options: [ { name: 'channel', description: 'Channel to post in', type: ApplicationCommandOptionType.Channel, channel_types: [ChannelType.GuildText], required: true }, { name: 'cron', description: 'Cron expression, e.g. "0 9 * * 5" (Fridays 9am)', type: ApplicationCommandOptionType.String, required: true }, { name: 'message', description: 'Message content to post', type: ApplicationCommandOptionType.String, required: true }, ], }, { name: 'once', description: 'Schedule a one-off message for a future time.', type: ApplicationCommandOptionType.Subcommand, options: [ { name: 'channel', description: 'Channel to post in', type: ApplicationCommandOptionType.Channel, channel_types: [ChannelType.GuildText], required: true }, { name: 'in', description: 'When to post, e.g. 30m, 2h, 1d', type: ApplicationCommandOptionType.String, required: true }, { name: 'message', description: 'Message content to post', type: ApplicationCommandOptionType.String, required: true }, ], }, { name: 'remove', description: 'Remove a scheduled message by id.', type: ApplicationCommandOptionType.Subcommand, options: [{ name: 'id', description: 'Scheduled message id (see /schedule list)', type: ApplicationCommandOptionType.Integer, required: true }], }, { name: 'list', description: 'List all scheduled messages.', type: ApplicationCommandOptionType.Subcommand, options: [], }, ], }, async execute(interaction) { const sub = interaction.options.getSubcommand() if (sub === 'recurring') { const channel = interaction.options.getChannel('channel', true) const cronExpr = interaction.options.getString('cron', true) const message = interaction.options.getString('message', true) if (!cron.validate(cronExpr)) { await interaction.reply({ content: `"${cronExpr}" isn't a valid cron expression.`, ephemeral: true }) return } const id = await scheduledMessages.addRecurring({ guildId: interaction.guildId, channelId: channel.id, content: message, cronExpression: cronExpr, createdBy: interaction.user.id, createdByTag: interaction.user.tag, }) await scheduler.refresh() await interaction.reply({ content: `Scheduled recurring message #${id} in ${channel} on \`${cronExpr}\`.`, ephemeral: true }) return } if (sub === 'once') { const channel = interaction.options.getChannel('channel', true) const inInput = interaction.options.getString('in', true) const message = interaction.options.getString('message', true) const ms = parseDuration(inInput) if (!ms) { await interaction.reply({ content: 'Invalid time — use a number plus s/m/h/d, e.g. `30m`, `2h`, `1d`.', ephemeral: true }) return } const runAt = new Date(Date.now() + ms) const id = await scheduledMessages.addOnce({ guildId: interaction.guildId, channelId: channel.id, content: message, runAt, createdBy: interaction.user.id, createdByTag: interaction.user.tag, }) await interaction.reply({ content: `Scheduled one-off message #${id} in ${channel} for ${runAt.toLocaleString()}.`, ephemeral: true }) return } if (sub === 'remove') { const id = interaction.options.getInteger('id', true) const removed = await scheduledMessages.remove(interaction.guildId, id) await scheduler.refresh() await interaction.reply({ content: removed ? `Removed scheduled message #${id}.` : `No scheduled message #${id} found.`, ephemeral: true }) return } if (sub === 'list') { const rows = await scheduledMessages.list(interaction.guildId) if (rows.length === 0) { await interaction.reply({ content: 'No scheduled messages.', ephemeral: true }) return } const lines = rows.map((r) => { const kind = r.cron_expression ? `cron \`${r.cron_expression}\`` : r.sent_at ? `sent ${new Date(r.sent_at).toLocaleString()}` : `due ${new Date(r.run_at).toLocaleString()}` return `**#${r.id}** <#${r.channel_id}> — ${kind}${r.enabled ? '' : ' (disabled)'}` }) await interaction.reply({ content: lines.join('\n'), ephemeral: true }) } }, }