diff --git a/.gitignore b/.gitignore index 2bad43b..3616767 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ yarn-error.log .DS_Store dist/ .tmp/ +start.log +*-backup.* +*-old.* diff --git a/index.js b/index.js index e515450..78f1196 100644 --- a/index.js +++ b/index.js @@ -26,7 +26,6 @@ let adminIds = (localConfig.adminIds || '') .map(id => id.trim()) .filter(id => id.length > 0); -// Generate a strong session secret if not configured if (!localConfig.sessionSecret || localConfig.sessionSecret === 'temp-secret') { localConfig.sessionSecret = crypto.randomBytes(32).toString('hex'); } @@ -95,7 +94,6 @@ const getDiscordUser = async accessToken => { }; const configureOAuth = config => { - // OAuth configuration is validated at runtime if (!config?.clientId || !config?.clientSecret) return false; return true; }; @@ -104,7 +102,6 @@ if (configured) configureOAuth(localConfig); const app = express(); -// Security middleware app.use(helmet({ contentSecurityPolicy: { directives: { @@ -118,28 +115,25 @@ app.use(helmet({ crossOriginEmbedderPolicy: false })); -// Rate limiting for authentication routes const authLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 10, // Limit each IP to 10 requests per windowMs + windowMs: 15 * 60 * 1000, + max: 10, message: 'Too many authentication attempts, please try again later.', standardHeaders: true, legacyHeaders: false }); -// Rate limiting for setup route const setupLimiter = rateLimit({ - windowMs: 60 * 60 * 1000, // 1 hour - max: 5, // Limit each IP to 5 setup attempts per hour + windowMs: 60 * 60 * 1000, + max: 5, message: 'Too many setup attempts, please try again later.', standardHeaders: true, legacyHeaders: false }); -// General rate limiting const generalLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100, // Limit each IP to 100 requests per windowMs + windowMs: 15 * 60 * 1000, + max: 100, standardHeaders: true, legacyHeaders: false }); @@ -153,23 +147,22 @@ app.use(express.urlencoded({ extended: false })); app.use(express.json()); app.use(cookieParser(localConfig.sessionSecret)); -// Configure session store - use MongoStore if MongoDB is available, otherwise MemoryStore with warning suppression const sessionConfig = { secret: localConfig.sessionSecret || 'temp-secret', resave: false, saveUninitialized: false, cookie: { - maxAge: 24 * 60 * 60 * 1000, // 24 hours - secure: process.env.NODE_ENV === 'production', // Use secure cookies in production - httpOnly: true + maxAge: 24 * 60 * 60 * 1000, + secure: process.env.NODE_ENV === 'production', + httpOnly: true, + sameSite: 'lax' } }; -// Use MongoStore for production-ready session storage if MongoDB is configured if (localConfig.mongoUri) { sessionConfig.store = MongoStore.create({ mongoUrl: localConfig.mongoUri, - touchAfter: 24 * 3600, // Lazy session update (in seconds) + touchAfter: 24 * 3600, crypto: { secret: localConfig.sessionSecret } @@ -178,10 +171,8 @@ if (localConfig.mongoUri) { app.use(session(sessionConfig)); -// Apply CSRF protection to all routes that write data app.use(doubleCsrfProtection); -// Middleware to expose CSRF token to views app.use((req, res, next) => { res.locals.csrfToken = generateCsrfToken(req, res); next(); @@ -210,10 +201,8 @@ const connectMongo = async () => { if (mongoose.connection.readyState === 0) { try { await mongoose.connect(localConfig.mongoUri); - // eslint-disable-next-line no-console console.log('✅ Dashboard MongoDB connected'); } catch (err) { - // eslint-disable-next-line no-console console.error('❌ Dashboard MongoDB connection failed:', err.message); throw err; } @@ -257,12 +246,10 @@ app.get('/setup', (req, res) => { app.post('/setup', setupLimiter, async (req, res) => { try { - // Validate required fields (MongoDB is now optional) if (!req.body.botToken || !req.body.clientId || !req.body.clientSecret || !req.body.sessionSecret) { return res.status(400).send('Missing required fields. Please fill in: Bot Token, Client ID, Client Secret, and Session Secret.'); } - // Validate and sanitize inputs const botToken = String(req.body.botToken).trim(); const clientId = String(req.body.clientId).trim(); const clientSecret = String(req.body.clientSecret).trim(); @@ -270,14 +257,13 @@ app.post('/setup', setupLimiter, async (req, res) => { const callbackUrl = String(req.body.callbackUrl || 'http://localhost:8080/auth/discord/callback').trim(); const guildId = String(req.body.guildId || '').trim(); const mongoUri = String(req.body.mongoUri || '').trim(); - const adminIds = String(req.body.adminIds || '').trim(); + const inputAdminIds = String(req.body.adminIds || '').trim(); const port = Number.parseInt(req.body.port, 10); const presenceText = String(req.body.presenceText || 'Ready to serve').trim(); const presenceType = Number.parseInt(req.body.presenceType, 10); const commandScope = String(req.body.commandScope || 'guild').trim(); const invitePermissions = String(req.body.invitePermissions || '8').trim(); - // Validate field lengths and formats if (botToken.length < 50) { return res.status(400).send('Bot Token appears to be invalid (too short).'); } @@ -308,7 +294,7 @@ app.post('/setup', setupLimiter, async (req, res) => { guildId, mongoUri, sessionSecret, - adminIds, + adminIds: inputAdminIds, port: port || 8080, autoStart: req.body.autoStart === 'on', presenceText, @@ -320,7 +306,7 @@ app.post('/setup', setupLimiter, async (req, res) => { await saveLocalConfig(newConfig); localConfig = newConfig; configured = isConfigured(localConfig); - adminIds = (localConfig.adminIds || '') + adminIds = (newConfig.adminIds || '') .split(',') .map(id => id.trim()) .filter(id => id.length > 0); @@ -342,7 +328,6 @@ app.post('/setup', setupLimiter, async (req, res) => { await connectMongo(); await getConfig(); } catch (err) { - // eslint-disable-next-line no-console console.warn('MongoDB connection failed, continuing without database:', err.message); } } @@ -369,7 +354,6 @@ app.post('/setup', setupLimiter, async (req, res) => { `); } catch (err) { - // eslint-disable-next-line no-console console.error(err); res.status(500).send(`Setup failed: ${err.message}`); } @@ -390,12 +374,10 @@ app.get('/selector', ensureAuth, async (req, res) => { app.get('/auth/discord', authLimiter, (req, res) => { const config = localConfig; - // Setup mode requires configuration first if (req.query.setup) { return res.redirect('/setup?message=Please complete setup first, then you can login to get your Discord User ID.'); } - // Normal login requires configuration if (!config?.clientId) { return res.status(500).send('OAuth not configured. Please complete setup first.'); } @@ -432,7 +414,6 @@ app.get('/auth/discord/callback', authLimiter, async (req, res) => { const userInfo = await getDiscordUser(tokenData.access_token); - // Fetch user guilds const guildsResponse = await fetch('https://discord.com/api/users/@me/guilds', { headers: { Authorization: `Bearer ${tokenData.access_token}` } }); @@ -443,11 +424,9 @@ app.get('/auth/discord/callback', authLimiter, async (req, res) => { const guilds = await guildsResponse.json(); - // Filter guilds where user is admin (has ManageGuild permission) const adminGuilds = guilds.filter(guild => { if (!guild.permissions) return false; const permissions = BigInt(guild.permissions); - // Check for ManageGuild permission (0x00000020 = 32) return (permissions & BigInt(32)) === BigInt(32); }); @@ -477,7 +456,6 @@ app.get('/logout', (req, res) => { app.get('/manage/:guildId', ensureAuth, async (req, res) => { const { guildId } = req.params; - // Check if user has admin access to this guild const guild = req.session.user.displayedGuilds?.find(g => g.id === guildId); if (!guild) { return res.status(403).render('error', { @@ -504,7 +482,6 @@ app.post('/control/start', ensureAdmin, async (req, res) => { await botManager.start(); res.redirect('/'); } catch (err) { - // eslint-disable-next-line no-console console.error(err); res.status(500).send('Failed to start bot'); } @@ -515,7 +492,6 @@ app.post('/control/stop', ensureAdmin, async (req, res) => { await botManager.stop(); res.redirect('/'); } catch (err) { - // eslint-disable-next-line no-console console.error(err); res.status(500).send('Failed to stop bot'); } @@ -526,7 +502,6 @@ app.post('/control/restart', ensureAdmin, async (req, res) => { await botManager.restart(); res.redirect('/'); } catch (err) { - // eslint-disable-next-line no-console console.error(err); res.status(500).send('Failed to restart bot'); } @@ -543,7 +518,6 @@ app.post('/control/status', ensureAdmin, async (req, res) => { await botManager.setActivity({ type: activityType, text: statusText }); res.redirect('/'); } catch (err) { - // eslint-disable-next-line no-console console.error(err); res.status(500).send('Failed to update status'); } @@ -559,6 +533,7 @@ app.get('/control/config', ensureAdmin, async (req, res) => { const config = cachedConfig || await getConfig(); res.render('dashboard', { user: req.session.user, + guild: null, inviteLink: botManager.getInviteLink({ permissions: config.invitePermissions }), botStatus: botManager.client ? 'online' : 'offline', config: configToView(config) @@ -567,7 +542,6 @@ app.get('/control/config', ensureAdmin, async (req, res) => { app.post('/control/config', ensureAdmin, async (req, res) => { try { - // Get current config or use local config if MongoDB is not available const config = localConfig.mongoUri ? (cachedConfig || await getConfig()) : null; const presenceType = Number.parseInt(req.body.presenceType, 10); const commandScope = ['global', 'guild'].includes(req.body.commandScope) @@ -578,7 +552,6 @@ app.post('/control/config', ensureAdmin, async (req, res) => { const previousClientId = config?.clientId || localConfig.clientId; const previousScope = config?.commandScope || localConfig.commandScope; - // Update MongoDB config if available if (config) { config.autoStart = req.body.autoStart === 'on'; config.presenceText = req.body.presenceText || config.presenceText; @@ -596,7 +569,6 @@ app.post('/control/config', ensureAdmin, async (req, res) => { cachedConfig = config; } - // Always update local config const updatedLocalConfig = { ...localConfig, autoStart: req.body.autoStart === 'on', @@ -615,7 +587,6 @@ app.post('/control/config', ensureAdmin, async (req, res) => { await saveLocalConfig(updatedLocalConfig); localConfig = updatedLocalConfig; - // Apply the config to bot manager - prefer MongoDB config if available, otherwise use local const configToApply = config ? { botToken: config.botToken || updatedLocalConfig.botToken, clientId: config.clientId || updatedLocalConfig.clientId, @@ -647,7 +618,6 @@ app.post('/control/config', ensureAdmin, async (req, res) => { res.redirect('/'); } catch (err) { - // eslint-disable-next-line no-console console.error(err); res.status(500).send('Failed to update configuration'); } @@ -661,7 +631,6 @@ app.post('/control/profile', ensureAdmin, async (req, res) => { } res.redirect('/'); } catch (err) { - // eslint-disable-next-line no-console console.error(err); res.status(500).send('Failed to update bot profile'); } @@ -675,17 +644,14 @@ const bootstrap = async () => { await connectMongo(); await getConfig(); } catch (err) { - // eslint-disable-next-line no-console console.warn('Mongo connection failed, using local config only:', err.message); } } } const server = app.listen(localConfig.port || 8080, () => { - // eslint-disable-next-line no-console console.log(`Dashboard running on port ${localConfig.port || 8080}`); if (!configured) { - // eslint-disable-next-line no-console console.log(`Setup required: http://localhost:${localConfig.port || 8080}/setup`); } }); @@ -696,7 +662,6 @@ const bootstrap = async () => { .catch(err => console.error('Bot failed to start:', err)); } - // Graceful shutdown const shutdown = async (signal) => { console.log(`\n${signal} received. Shutting down gracefully...`); @@ -722,7 +687,6 @@ const bootstrap = async () => { }; bootstrap().catch(err => { - // eslint-disable-next-line no-console console.error('Failed to bootstrap app:', err); process.exit(1); }); diff --git a/package-lock.json b/package-lock.json index 79decf8..9ef677f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -783,6 +783,7 @@ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.19.0.tgz", "integrity": "sha512-0csaMkGq+vaiZTmSMMGkfdCOabYv192VbytFypcvI0MANrp+4i/7yEkJ0sbAEhycQjntaKGzYfjfXQyVb7BHMA==", "license": "MIT", + "peer": true, "dependencies": { "cookie": "~0.7.2", "cookie-signature": "~1.0.7", @@ -1311,6 +1312,7 @@ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^7.0.0", diff --git a/src/BotManager.js b/src/BotManager.js index 231ec07..651b2b9 100644 --- a/src/BotManager.js +++ b/src/BotManager.js @@ -41,7 +41,6 @@ export default class BotManager { async start() { if (this.client) return this.client; - // Validate configuration before starting if (!this.token) { throw new Error('Missing DISCORD_TOKEN. Please configure the bot token in setup.'); } @@ -73,7 +72,6 @@ export default class BotManager { async setActivity({ type, text }) { if (!this.client) return; - // Replace variables in presence text const processedText = this.processPresenceText(text); this.client.user.setPresence({ @@ -88,7 +86,6 @@ export default class BotManager { let processed = text; const guild = this.client.guilds.cache.first(); - // Replace variables processed = processed .replace(/{members}/gi, this.client.users.cache.size.toString()) .replace(/{guilds}/gi, this.client.guilds.cache.size.toString()) @@ -103,7 +100,6 @@ export default class BotManager { async updateBotProfile({ username, avatar, banner, bio }) { if (!this.client?.user) { - // eslint-disable-next-line no-console console.warn('⚠️ Cannot update profile: bot is not running'); return; } @@ -116,34 +112,27 @@ export default class BotManager { } if (bio && bio.trim()) { - // Process variables in bio const processedBio = this.processPresenceText(bio.trim()); updateData.bio = processedBio; } if (avatar && avatar.trim()) { - // Validate URL format using regex for better performance const urlPattern = /^https?:\/\/.+\.(png|jpg|jpeg|gif|webp)(\?.*)?$/i; if (urlPattern.test(avatar.trim())) { updateData.avatar = avatar.trim(); } else { - // eslint-disable-next-line no-console console.error('❌ Invalid avatar URL format. Must be a valid image URL (PNG/JPG/JPEG/GIF/WEBP)'); } } - // Banner is not supported by Discord API for bots - ignore silently if (Object.keys(updateData).length > 0) { await this.client.user.edit(updateData); - // eslint-disable-next-line no-console console.log('✅ Bot profile updated successfully'); } else { - // eslint-disable-next-line no-console console.log('ℹ️ No profile changes to apply'); } } catch (err) { - // eslint-disable-next-line no-console console.error('❌ Failed to update bot profile:', err.message); throw err; } @@ -165,6 +154,7 @@ export default class BotManager { GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages, + GatewayIntentBits.GuildModeration, GatewayIntentBits.MessageContent ], partials: [Partials.Channel] @@ -177,23 +167,19 @@ export default class BotManager { this.client.once('ready', () => { this.client.user.setPresence(this.defaultPresence); this.autoRestartAttempts = 0; // Reset counter on successful connection - // eslint-disable-next-line no-console console.log(`✅ Bot logged in as ${this.client.user.tag}`); }); this.client.on('disconnect', () => { - // eslint-disable-next-line no-console console.warn('⚠️ Bot disconnected, attempting auto-restart...'); this.autoRestart(); }); this.client.on('error', err => { - // eslint-disable-next-line no-console console.error('❌ Client error:', err); }); this.client.on('shardError', err => { - // eslint-disable-next-line no-console console.error('❌ Shard error:', err); }); @@ -204,7 +190,6 @@ export default class BotManager { try { await command.execute(interaction); } catch (err) { - // eslint-disable-next-line no-console console.error('Command error:', err); const errorMessage = { content: 'An error occurred while executing this command.', ephemeral: true }; try { @@ -214,7 +199,6 @@ export default class BotManager { await interaction.reply(errorMessage); } } catch (replyErr) { - // eslint-disable-next-line no-console console.error('Failed to send error message:', replyErr); } } @@ -225,7 +209,6 @@ export default class BotManager { try { await this.client.login(this.token); } catch (err) { - // eslint-disable-next-line no-console console.error('❌ Failed to login to Discord:', err.message); throw new Error(`Discord login failed: ${err.message}. Please check your bot token.`); } @@ -233,7 +216,6 @@ export default class BotManager { async autoRestart() { if (this.autoRestartAttempts >= this.maxAutoRestartAttempts) { - // eslint-disable-next-line no-console console.error(`Max auto-restart attempts (${this.maxAutoRestartAttempts}) reached. Manual restart required.`); return; } @@ -241,14 +223,12 @@ export default class BotManager { this.autoRestartAttempts++; const delay = Math.pow(2, this.autoRestartAttempts) * 1000; // Exponential backoff - // eslint-disable-next-line no-console console.log(`Auto-restart attempt ${this.autoRestartAttempts}/${this.maxAutoRestartAttempts} in ${delay}ms...`); setTimeout(async () => { try { await this.restart(); } catch (err) { - // eslint-disable-next-line no-console console.error('Auto-restart failed:', err); this.autoRestart(); } @@ -261,10 +241,8 @@ export default class BotManager { try { await mongoose.connect(this.mongoUri); - // eslint-disable-next-line no-console console.log('✅ MongoDB connected'); } catch (err) { - // eslint-disable-next-line no-console console.error('❌ MongoDB connection failed:', err.message); throw err; } @@ -283,14 +261,12 @@ export default class BotManager { this.commands.set(command.data.name, command); } } catch (err) { - // eslint-disable-next-line no-console console.warn('No commands directory found, skipping command load:', err.message); } } async registerCommands() { if (!this.clientId || !this.token) { - // eslint-disable-next-line no-console console.warn('⚠️ Skipping command registration: missing clientId or token'); return; } @@ -300,7 +276,6 @@ export default class BotManager { const body = Array.from(this.commands.values()).map(cmd => cmd.data.toJSON()); if (body.length === 0) { - // eslint-disable-next-line no-console console.log('ℹ️ No commands to register'); return; } @@ -308,15 +283,12 @@ export default class BotManager { try { if (this.commandScope === 'guild' && this.guildId) { await rest.put(Routes.applicationGuildCommands(this.clientId, this.guildId), { body }); - // eslint-disable-next-line no-console console.log(`✅ Registered ${body.length} guild commands for guild ${this.guildId}`); } else { await rest.put(Routes.applicationCommands(this.clientId), { body }); - // eslint-disable-next-line no-console console.log(`✅ Registered ${body.length} global commands`); } } catch (err) { - // eslint-disable-next-line no-console console.error('❌ Failed to register commands:', err.message); throw err; } @@ -342,6 +314,10 @@ export default class BotManager { } async ensureConfig() { + if (!this.mongoUri || mongoose.connection.readyState !== 1) { + this.config = null; + return null; + } let config = await Config.findOne(); if (!config) { config = await Config.create({ diff --git a/src/commands/fun/8ball.js b/src/commands/fun/8ball.js new file mode 100644 index 0000000..56e50ed --- /dev/null +++ b/src/commands/fun/8ball.js @@ -0,0 +1,28 @@ +import { SlashCommandBuilder } from 'discord.js'; + +const responses = [ + 'It is certain.', 'It is decidedly so.', 'Without a doubt.', + 'Yes — definitely.', 'You may rely on it.', 'As I see it, yes.', + 'Most likely.', 'Outlook good.', 'Yes.', 'Signs point to yes.', + 'Reply hazy, try again.', 'Ask again later.', 'Better not tell you now.', + 'Cannot predict now.', 'Concentrate and ask again.', + 'Don\'t count on it.', 'My reply is no.', 'My sources say no.', + 'Outlook not so good.', 'Very doubtful.' +]; + +export default { + data: new SlashCommandBuilder() + .setName('8ball') + .setDescription('Ask the magic 8-ball a question') + .addStringOption(option => + option.setName('question').setDescription('Your question').setRequired(true) + ), + async execute(interaction) { + const question = interaction.options.getString('question'); + const answer = responses[Math.floor(Math.random() * responses.length)]; + return interaction.reply({ + content: `🎱 **${question}**\n${answer}`, + ephemeral: true + }); + } +}; diff --git a/src/commands/moderation/kick.js b/src/commands/moderation/kick.js index 145fd8b..69cbe20 100644 --- a/src/commands/moderation/kick.js +++ b/src/commands/moderation/kick.js @@ -41,12 +41,10 @@ export default { } try { - await target.kick(reason); try { - await target.send(`You were kicked from **${interaction.guild.name}** | Reason: ${reason}`); - } catch (err) { - // ignore DM failures - } + await target.user.send(`You were kicked from **${interaction.guild.name}** | Reason: ${reason}`); + } catch (_) {} + await target.kick(reason); return interaction.reply({ content: `Kicked ${target.user.tag} | Reason: ${reason}`, ephemeral: true diff --git a/src/commands/moderation/purge.js b/src/commands/moderation/purge.js index 040908d..7214f97 100644 --- a/src/commands/moderation/purge.js +++ b/src/commands/moderation/purge.js @@ -5,11 +5,11 @@ export default { .setName('purge') .setDescription('Bulk delete messages') .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) - .addNumberOption(option => + .addIntegerOption(option => option.setName('amount').setDescription('Number of messages to delete').setRequired(true).setMinValue(1).setMaxValue(100) ), async execute(interaction) { - const amount = interaction.options.getNumber('amount'); + const amount = interaction.options.getInteger('amount'); if (!interaction.guild.members.me?.permissions.has(PermissionFlagsBits.ManageMessages)) { return interaction.reply({ content: 'I need message management permissions to do that.', ephemeral: true }); } diff --git a/src/commands/moderation/timeout.js b/src/commands/moderation/timeout.js index e9587d1..f92e13e 100644 --- a/src/commands/moderation/timeout.js +++ b/src/commands/moderation/timeout.js @@ -8,7 +8,7 @@ export default { .addUserOption(option => option.setName('target').setDescription('Member to timeout').setRequired(true) ) - .addNumberOption(option => + .addIntegerOption(option => option.setName('duration').setDescription('Duration in minutes').setRequired(true).setMinValue(1).setMaxValue(40320) ) .addStringOption(option => @@ -16,7 +16,7 @@ export default { ), async execute(interaction) { const target = interaction.options.getMember('target'); - const duration = interaction.options.getNumber('duration') * 60 * 1000; + const duration = interaction.options.getInteger('duration') * 60 * 1000; const reason = interaction.options.getString('reason') || 'No reason provided'; if (!target) { @@ -47,7 +47,7 @@ export default { try { await target.timeout(duration, reason); return interaction.reply({ - content: `Timed out ${target.user.tag} for ${interaction.options.getNumber('duration')} minutes | Reason: ${reason}`, + content: `Timed out ${target.user.tag} for ${interaction.options.getInteger('duration')} minutes | Reason: ${reason}`, ephemeral: true }); } catch (err) { diff --git a/src/commands/moderation/warn.js b/src/commands/moderation/warn.js new file mode 100644 index 0000000..80b4c84 --- /dev/null +++ b/src/commands/moderation/warn.js @@ -0,0 +1,54 @@ +import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js'; + +export default { + data: new SlashCommandBuilder() + .setName('warn') + .setDescription('Send a formal warning to a member') + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) + .addUserOption(option => + option.setName('target').setDescription('Member to warn').setRequired(true) + ) + .addStringOption(option => + option.setName('reason').setDescription('Reason for warning').setRequired(true) + ), + async execute(interaction) { + const target = interaction.options.getMember('target'); + const reason = interaction.options.getString('reason'); + + if (!target) { + return interaction.reply({ content: 'Unable to find that member.', ephemeral: true }); + } + + if (target.id === interaction.user.id) { + return interaction.reply({ content: 'You cannot warn yourself.', ephemeral: true }); + } + + if (target.id === interaction.client.user.id) { + return interaction.reply({ content: 'I cannot warn myself.', ephemeral: true }); + } + + const embed = new EmbedBuilder() + .setTitle('⚠️ Warning Issued') + .setColor('#f59e0b') + .addFields( + { name: 'Member', value: `${target.user.tag} (${target.id})`, inline: true }, + { name: 'Moderator', value: interaction.user.tag, inline: true }, + { name: 'Reason', value: reason } + ) + .setTimestamp(new Date()); + + try { + await target.user.send({ + embeds: [ + new EmbedBuilder() + .setTitle(`⚠️ Warning from ${interaction.guild.name}`) + .setColor('#f59e0b') + .addFields({ name: 'Reason', value: reason }) + .setTimestamp(new Date()) + ] + }); + } catch (_) {} + + return interaction.reply({ embeds: [embed], ephemeral: true }); + } +}; diff --git a/src/commands/utility/channelinfo.js b/src/commands/utility/channelinfo.js new file mode 100644 index 0000000..6ec8cad --- /dev/null +++ b/src/commands/utility/channelinfo.js @@ -0,0 +1,49 @@ +import { SlashCommandBuilder, EmbedBuilder, ChannelType } from 'discord.js'; + +const channelTypeNames = { + [ChannelType.GuildText]: 'Text', + [ChannelType.GuildVoice]: 'Voice', + [ChannelType.GuildCategory]: 'Category', + [ChannelType.GuildAnnouncement]: 'Announcement', + [ChannelType.GuildStageVoice]: 'Stage', + [ChannelType.GuildForum]: 'Forum' +}; + +export default { + data: new SlashCommandBuilder() + .setName('channelinfo') + .setDescription('View details about a channel') + .addChannelOption(option => + option.setName('channel').setDescription('Channel to inspect').setRequired(false) + ), + async execute(interaction) { + const channel = interaction.options.getChannel('channel') || interaction.channel; + + const embed = new EmbedBuilder() + .setTitle(`#${channel.name}`) + .setColor('#5865f2') + .addFields( + { name: 'ID', value: channel.id, inline: true }, + { name: 'Type', value: channelTypeNames[channel.type] || 'Unknown', inline: true }, + { name: 'Created', value: ``, inline: true } + ); + + if (channel.topic) { + embed.addFields({ name: 'Topic', value: channel.topic }); + } + + if (channel.rateLimitPerUser) { + embed.addFields({ name: 'Slowmode', value: `${channel.rateLimitPerUser}s`, inline: true }); + } + + if (channel.nsfw !== undefined) { + embed.addFields({ name: 'NSFW', value: channel.nsfw ? 'Yes' : 'No', inline: true }); + } + + if (channel.parentId) { + embed.addFields({ name: 'Category', value: `<#${channel.parentId}>`, inline: true }); + } + + return interaction.reply({ embeds: [embed], ephemeral: true }); + } +}; diff --git a/src/commands/utility/embed.js b/src/commands/utility/embed.js new file mode 100644 index 0000000..f9cbaa5 --- /dev/null +++ b/src/commands/utility/embed.js @@ -0,0 +1,47 @@ +import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from 'discord.js'; + +export default { + data: new SlashCommandBuilder() + .setName('embed') + .setDescription('Send a custom embed message') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) + .addStringOption(option => + option.setName('title').setDescription('Embed title').setRequired(true) + ) + .addStringOption(option => + option.setName('description').setDescription('Embed description').setRequired(true) + ) + .addStringOption(option => + option.setName('color').setDescription('Hex color (e.g. #ff0000)').setRequired(false) + ) + .addStringOption(option => + option.setName('footer').setDescription('Footer text').setRequired(false) + ) + .addStringOption(option => + option.setName('image').setDescription('Image URL').setRequired(false) + ) + .addStringOption(option => + option.setName('thumbnail').setDescription('Thumbnail URL').setRequired(false) + ), + async execute(interaction) { + const title = interaction.options.getString('title'); + const description = interaction.options.getString('description'); + const color = interaction.options.getString('color'); + const footer = interaction.options.getString('footer'); + const image = interaction.options.getString('image'); + const thumbnail = interaction.options.getString('thumbnail'); + + const embed = new EmbedBuilder() + .setTitle(title) + .setDescription(description) + .setColor(color && /^#[0-9a-fA-F]{6}$/.test(color) ? color : '#5865f2') + .setTimestamp(new Date()); + + if (footer) embed.setFooter({ text: footer }); + if (image) embed.setImage(image); + if (thumbnail) embed.setThumbnail(thumbnail); + + await interaction.channel.send({ embeds: [embed] }); + return interaction.reply({ content: 'Embed sent.', ephemeral: true }); + } +}; diff --git a/src/commands/utility/roleinfo.js b/src/commands/utility/roleinfo.js new file mode 100644 index 0000000..56e5a94 --- /dev/null +++ b/src/commands/utility/roleinfo.js @@ -0,0 +1,28 @@ +import { SlashCommandBuilder, EmbedBuilder } from 'discord.js'; + +export default { + data: new SlashCommandBuilder() + .setName('roleinfo') + .setDescription('View details about a role') + .addRoleOption(option => + option.setName('role').setDescription('Role to inspect').setRequired(true) + ), + async execute(interaction) { + const role = interaction.options.getRole('role'); + + const embed = new EmbedBuilder() + .setTitle(role.name) + .setColor(role.hexColor || '#5865f2') + .addFields( + { name: 'ID', value: role.id, inline: true }, + { name: 'Color', value: role.hexColor || 'None', inline: true }, + { name: 'Members', value: role.members.size.toString(), inline: true }, + { name: 'Mentionable', value: role.mentionable ? 'Yes' : 'No', inline: true }, + { name: 'Hoisted', value: role.hoist ? 'Yes' : 'No', inline: true }, + { name: 'Position', value: role.position.toString(), inline: true }, + { name: 'Created', value: ``, inline: true } + ); + + return interaction.reply({ embeds: [embed], ephemeral: true }); + } +}; diff --git a/src/dashboard/views/dashboard-backup.ejs b/src/dashboard/views/dashboard-backup.ejs deleted file mode 100644 index f3ba9f6..0000000 --- a/src/dashboard/views/dashboard-backup.ejs +++ /dev/null @@ -1,660 +0,0 @@ - - - - - - Turbo Gravity | Dashboard - - - -
-
-
🚀
-
-

Turbo Gravity

-

Control Panel

-
-
-
- <% if (guild) { %> - ← Back - <% } %> - - Invite - Logout -
- <%= user.username %> - Admin -
-
-
- -
- -
- - - - -
- - -
-
-
-
-
-

Current Status

-

Bot Health

-
-
-
- - - <%= botStatus.toUpperCase() %> - -
-

Status: <%= botStatus === 'online' ? 'Bot is connected and ready' : 'Bot is not connected' %>

-
- -
-
-
-

Quick Actions

-

Bot Control

-
-
-
-
- -
-
- -
-
- -
-
-
-
-
- - -
-
-
-
-

Profile Information

-

Bot Profile ?

-
-
-
- - - - - - - - - -
-
-
- - -
-
-
-
-

Bot Presence

-

Status & Activity ?

-
-
-
-
- - - -
- -
- 💡 Supported Variables: Use these in your status text and they'll be replaced with live data -
-
{members} - Total members
-
{guilds} - Total servers
-
{users} - Total users
-
{botname} - Bot name
-
{servername} - Server name
-
{prefix} - Prefix (/)
-
{timestamp} - Current time
-
-
- - -
-
-
- - -
-
-
-
-
-

Bot Settings

-

Configuration ?

-
-
-
- - Automatically start the bot when dashboard launches - - - - - - - - -
-
- -
-
-
-

Invite Link

-

Add Bot to Server ?

-
-
-

Share this link to invite the bot to your servers:

- - Open Invite Link -
-
-
-
- - - - diff --git a/src/dashboard/views/dashboard-new.ejs b/src/dashboard/views/dashboard-new.ejs deleted file mode 100644 index f3ba9f6..0000000 --- a/src/dashboard/views/dashboard-new.ejs +++ /dev/null @@ -1,660 +0,0 @@ - - - - - - Turbo Gravity | Dashboard - - - -
-
-
🚀
-
-

Turbo Gravity

-

Control Panel

-
-
-
- <% if (guild) { %> - ← Back - <% } %> - - Invite - Logout -
- <%= user.username %> - Admin -
-
-
- -
- -
- - - - -
- - -
-
-
-
-
-

Current Status

-

Bot Health

-
-
-
- - - <%= botStatus.toUpperCase() %> - -
-

Status: <%= botStatus === 'online' ? 'Bot is connected and ready' : 'Bot is not connected' %>

-
- -
-
-
-

Quick Actions

-

Bot Control

-
-
-
-
- -
-
- -
-
- -
-
-
-
-
- - -
-
-
-
-

Profile Information

-

Bot Profile ?

-
-
-
- - - - - - - - - -
-
-
- - -
-
-
-
-

Bot Presence

-

Status & Activity ?

-
-
-
-
- - - -
- -
- 💡 Supported Variables: Use these in your status text and they'll be replaced with live data -
-
{members} - Total members
-
{guilds} - Total servers
-
{users} - Total users
-
{botname} - Bot name
-
{servername} - Server name
-
{prefix} - Prefix (/)
-
{timestamp} - Current time
-
-
- - -
-
-
- - -
-
-
-
-
-

Bot Settings

-

Configuration ?

-
-
-
- - Automatically start the bot when dashboard launches - - - - - - - - -
-
- -
-
-
-

Invite Link

-

Add Bot to Server ?

-
-
-

Share this link to invite the bot to your servers:

- - Open Invite Link -
-
-
-
- - - - diff --git a/src/dashboard/views/dashboard-old.ejs b/src/dashboard/views/dashboard-old.ejs deleted file mode 100644 index 47501fe..0000000 --- a/src/dashboard/views/dashboard-old.ejs +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - Turbo Gravity | Dashboard - - - -
-
-
-
-
-

Turbo Gravity

-

Control Panel

-
-
-
- <% if (guild) { %> - ← Back to Servers - <% } %> - - Invite - Logout -
- <%= user.username %> - Admin -
-
-
- -
- <% if (guild) { %> -
-
-

Guild Management

-

<%= guild.name %>

-

Manage this server's bot settings and commands.

-
-
- <% if (guild.icon) { %> - <%= guild.name %> - <% } %> -

Guild ID: <%= guild.id %>

-

Members: <%= guild.member_count || '?' %>

-
-
- <% } else { %> -
-
-

Bot Status

-

Discord Control Center

-

Manage lifecycle, presence, and runtime settings without restarting the dashboard.

-
- <%= botStatus %> - Add to Server -
-
-
-

Quick Actions

-
-
-
-
-
-
-
- <% } %> - -
-
-
-
-

Presence

-

Update Status

-
-
-
- - - -
-
- -
-
-
-

Runtime

-

Configuration

-
-
-
- - -
- - -
- - - - - - - -
- - -
- -
- - -
- - -
-
-
-
- - - - diff --git a/src/dashboard/views/dashboard.ejs b/src/dashboard/views/dashboard.ejs index d1997b0..3a47e5a 100644 --- a/src/dashboard/views/dashboard.ejs +++ b/src/dashboard/views/dashboard.ejs @@ -432,7 +432,6 @@ padding: 6px 10px; font-size: 12px; } - } .user-chip { font-size: 12px; diff --git a/src/localConfig.js b/src/localConfig.js index adf7f4f..c8cbd06 100644 --- a/src/localConfig.js +++ b/src/localConfig.js @@ -31,7 +31,6 @@ export async function loadLocalConfig() { const data = await readFile(CONFIG_FILE, 'utf-8'); return { ...defaultConfig, ...JSON.parse(data) }; } catch (err) { - // eslint-disable-next-line no-console console.warn('Failed to load local config, using defaults:', err.message); return { ...defaultConfig }; } @@ -45,7 +44,6 @@ export async function saveLocalConfig(config) { } export function isConfigured(config) { - // MongoDB is optional, so we don't require mongoUri return !!( config.botToken && config.clientId && diff --git a/start.log b/start.log deleted file mode 100644 index 1949920..0000000 --- a/start.log +++ /dev/null @@ -1,7 +0,0 @@ - -> turbo-gravity-bot@1.0.0 start -> node index.js - -[dotenv@17.2.3] injecting env (0) from .env -- tip: ⚙️ override existing env vars with { override: true } -Dashboard running on port 8080 -Setup required: http://localhost:8080/setup