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>
150 lines
5.3 KiB
JavaScript
150 lines
5.3 KiB
JavaScript
// Owns the single discord.js Client instance for this process: lifecycle
|
|
// (start/stop/status) and slash-command registration/dispatch. Command
|
|
// definitions themselves live in ./commands — this file only wires them up.
|
|
const { Client, GatewayIntentBits, REST, Routes } = require('discord.js')
|
|
|
|
const createLogger = require('../utils/logger')
|
|
const commands = require('./commands')
|
|
const messageFilter = require('./messageFilter')
|
|
const scheduler = require('../scheduler/scheduler')
|
|
const roleMenuHandler = require('./roleMenuHandler')
|
|
const { handleGuildMemberAdd } = require('./guildMemberAdd')
|
|
const { handleGuildMemberRemove } = require('./guildMemberRemove')
|
|
const inviteTracker = require('./inviteTracker')
|
|
const tempRoleSweeper = require('../roles/tempRoleSweeper')
|
|
const inviteScheduler = require('../invites/inviteScheduler')
|
|
|
|
const log = createLogger('discord')
|
|
|
|
let client = null
|
|
let guildId = null
|
|
let status = 'disconnected' // disconnected | connecting | connected | error
|
|
let statusDetail = null
|
|
let lastConnectedAt = null
|
|
|
|
async function registerCommands(applicationId, targetGuildId) {
|
|
const rest = new REST({ version: '10' }).setToken(client.token)
|
|
await rest.put(Routes.applicationGuildCommands(applicationId, targetGuildId), {
|
|
body: commands.all.map((c) => c.data),
|
|
})
|
|
log.info('registered guild slash commands', { guildId: targetGuildId, count: commands.all.length })
|
|
}
|
|
|
|
async function stop() {
|
|
if (!client) {
|
|
status = 'disconnected'
|
|
statusDetail = null
|
|
return
|
|
}
|
|
scheduler.stop()
|
|
tempRoleSweeper.stop()
|
|
inviteScheduler.stop()
|
|
try {
|
|
await client.destroy()
|
|
} catch (err) {
|
|
log.warn('error while destroying client', { message: err.message })
|
|
}
|
|
client = null
|
|
status = 'disconnected'
|
|
statusDetail = null
|
|
log.info('discord client disconnected')
|
|
}
|
|
|
|
// Post-login startup: register commands and start the background workers. A
|
|
// failure here leaves the client connected but flags an error status.
|
|
async function onReady() {
|
|
try {
|
|
await registerCommands(client.application.id, guildId)
|
|
await scheduler.start(client)
|
|
tempRoleSweeper.start(client)
|
|
inviteScheduler.start(client, guildId)
|
|
await inviteTracker.prime(client, guildId)
|
|
status = 'connected'
|
|
statusDetail = null
|
|
lastConnectedAt = new Date()
|
|
log.info('discord client ready', { user: client.user?.tag, guildId })
|
|
} catch (err) {
|
|
status = 'error'
|
|
statusDetail = `startup failed: ${err.message}`
|
|
log.error('post-login startup failed (commands/scheduler/temp-roles/invites)', { message: err.message })
|
|
}
|
|
}
|
|
|
|
// Route an interaction: role-menu handler first, then chat-input slash commands.
|
|
async function onInteractionCreate(interaction) {
|
|
if (await roleMenuHandler.handleInteraction(interaction)) return
|
|
if (!interaction.isChatInputCommand()) return
|
|
const command = commands.get(interaction.commandName)
|
|
if (!command) return
|
|
try {
|
|
await command.execute(interaction)
|
|
} catch (err) {
|
|
log.error('command execution failed', { command: interaction.commandName, message: err.message })
|
|
const payload = { content: 'Something went wrong running that command.', ephemeral: true }
|
|
if (interaction.replied || interaction.deferred) await interaction.followUp(payload)
|
|
else await interaction.reply(payload)
|
|
}
|
|
}
|
|
|
|
// start({ token, guildId }) — (re)connects. Always stops any existing client
|
|
// first so re-saving config or toggling Enabled off/on is idempotent.
|
|
async function start({ token, guildId: gid }) {
|
|
await stop()
|
|
guildId = gid
|
|
status = 'connecting'
|
|
statusDetail = null
|
|
|
|
// GuildMessages + MessageContent (Phase 3, filter) and GuildMembers
|
|
// (Phase 5, auto-role + bulk role ops) are all privileged — must be enabled
|
|
// in the Discord Developer Portal, see the Phase 1 setup notes. GuildInvites
|
|
// (Phase 6b, invite-usage attribution) is NOT privileged — no portal toggle.
|
|
client = new Client({
|
|
intents: [
|
|
GatewayIntentBits.Guilds,
|
|
GatewayIntentBits.GuildMessages,
|
|
GatewayIntentBits.MessageContent,
|
|
GatewayIntentBits.GuildMembers,
|
|
GatewayIntentBits.GuildInvites,
|
|
],
|
|
})
|
|
|
|
client.once('ready', onReady)
|
|
client.on('interactionCreate', onInteractionCreate)
|
|
client.on('messageCreate', messageFilter.handleMessageCreate)
|
|
client.on('guildMemberAdd', handleGuildMemberAdd)
|
|
client.on('guildMemberRemove', handleGuildMemberRemove)
|
|
// Keep the invite-use cache fresh so guildMemberAdd can attribute joins.
|
|
client.on('inviteCreate', inviteTracker.onInviteCreate)
|
|
client.on('inviteDelete', inviteTracker.onInviteDelete)
|
|
|
|
client.on('error', (err) => {
|
|
status = 'error'
|
|
statusDetail = err.message
|
|
log.error('discord client error', { message: err.message })
|
|
})
|
|
|
|
try {
|
|
await client.login(token)
|
|
} catch (err) {
|
|
status = 'error'
|
|
statusDetail = err.message
|
|
client = null
|
|
log.error('discord login failed', { message: err.message })
|
|
throw err
|
|
}
|
|
}
|
|
|
|
function getStatus() {
|
|
return { status, statusDetail, guildId, lastConnectedAt }
|
|
}
|
|
|
|
// For code that needs the live client + which guild it's connected to (the
|
|
// /internal/announce handler, slash commands already get both from the
|
|
// interaction itself so they don't need this). Returns null if disconnected.
|
|
function getConnection() {
|
|
if (!client || status !== 'connected') return null
|
|
return { client, guildId }
|
|
}
|
|
|
|
module.exports = { start, stop, getStatus, getConnection }
|