Files
website/bot/src/discord/commands/schedule.command.js
wtclaude 12d50fd615
All checks were successful
PR Checks / bot-install (pull_request) Successful in 13s
PR Checks / client-build (pull_request) Successful in 22s
PR Checks / server-tests (pull_request) Successful in 11m13s
chore(quality): resolve SonarQube code smells across website
Clears the 124 CODE_SMELL findings from the SonarQube scan (server, client,
and bot). All changes are behaviour-preserving refactors — no route, protocol,
schema, or config changes — verified against the full server (381) and client
(43) test suites plus a clean client build.

By rule:
- S3776 (20, cognitive complexity): extract helpers/handlers so each function
  drops under the threshold — shard model upsert builders, page/wiki update,
  block validation, notification stream mapping (dispatch table), SSO mobile
  login, shard ingest deps, uo-link socket backfill/connect, the bot slash-
  command dispatchers + discord manager, and the Shard/UserDetail/HeroEditor/
  CharacterStats React components.
- S4624 (34, nested template literals): pull inner templates into locals /
  a withQs() helper; rewrite shardEvents.describe() as a formatter table.
- S3358 (35, nested ternaries): lift to if/else vars, lookup maps, small
  components, or guarded JSX expressions.
- S6479 (12, array-index React keys): key by stable content instead of index
  (two in-editor lists left as-is; index matches their by-index edit model).
- S6353 (6): [0-9]/[^0-9] -> \d/\D.  S125 (5): reword state-shape comments that
  parsed as code.  S3800/S3782 (botScore): JSDoc-type PATH_WEIGHTS tuples.
- S6481 (2): memoize Auth/Site context values (and SiteContext brand).
- S4144: dedupe HeroEditor upload handler into useImageUpload().
- S1126 (2), S6035, S5869 (redundant A-Z under /i), S5843 (town-name regex ->
  prefix list): assorted one-liners.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 04:35:39 -05:00

122 lines
5.2 KiB
JavaScript

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')
// Per-subcommand handlers, split out of execute() so the dispatch stays flat.
async function handleRecurring(interaction) {
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 })
}
async function handleOnce(interaction) {
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 })
}
async function handleRemove(interaction) {
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 })
}
async function handleList(interaction) {
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) => {
let kind
if (r.cron_expression) kind = `cron \`${r.cron_expression}\``
else if (r.sent_at) kind = `sent ${new Date(r.sent_at).toLocaleString()}`
else kind = `due ${new Date(r.run_at).toLocaleString()}`
const suffix = r.enabled ? '' : ' (disabled)'
return `**#${r.id}** <#${r.channel_id}> — ${kind}${suffix}`
})
await interaction.reply({ content: lines.join('\n'), ephemeral: true })
}
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') return handleRecurring(interaction)
if (sub === 'once') return handleOnce(interaction)
if (sub === 'remove') return handleRemove(interaction)
if (sub === 'list') return handleList(interaction)
},
}