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>
122 lines
5.2 KiB
JavaScript
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)
|
|
},
|
|
}
|