ui updates and bot updates. new commands and command handler

This commit is contained in:
2025-10-03 19:53:23 -04:00
parent 524a6cc633
commit f63fca3f1b
20 changed files with 831 additions and 125 deletions

View File

@@ -1,63 +0,0 @@
const { SlashCommandBuilder } = require('discord.js');
const { readDb, writeDb } = require('../../backend/db.js');
module.exports = {
name: 'config-leave',
description: 'Configure the leave message for this server.',
enabled: true,
builder: new SlashCommandBuilder()
.setName('config-leave')
.setDescription('Configure the leave message for this server.')
.addSubcommand(subcommand =>
subcommand
.setName('set-channel')
.setDescription('Set the channel for leave messages.')
.addChannelOption(option =>
option.setName('channel')
.setDescription('The channel to send leave messages to.')
.setRequired(true)
)
)
.addSubcommand(subcommand =>
subcommand
.setName('set-message')
.setDescription('Set the leave message.')
.addStringOption(option =>
option.setName('message')
.setDescription('The leave message. Use {user} for username and {server} for server name.')
.setRequired(true)
)
)
.addSubcommand(subcommand =>
subcommand
.setName('disable')
.setDescription('Disable leave messages.')
),
async execute(interaction) {
const db = readDb();
const guildId = interaction.guildId;
const subcommand = interaction.options.getSubcommand();
if (!db[guildId]) {
db[guildId] = {};
}
if (subcommand === 'set-channel') {
const channel = interaction.options.getChannel('channel');
db[guildId].leaveChannel = channel.id;
db[guildId].leaveEnabled = true;
writeDb(db);
await interaction.reply(`Leave channel set to ${channel}.`);
} else if (subcommand === 'set-message') {
const message = interaction.options.getString('message');
db[guildId].leaveMessage = message;
db[guildId].leaveEnabled = true;
writeDb(db);
await interaction.reply(`Leave message set to: "${message}"`);
} else if (subcommand === 'disable') {
db[guildId].leaveEnabled = false;
writeDb(db);
await interaction.reply('Leave messages disabled.');
}
},
};

View File

@@ -0,0 +1,25 @@
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
name: 'help',
description: 'List available bot commands and what they do.',
enabled: true,
builder: new SlashCommandBuilder()
.setName('help')
.setDescription('List available bot commands and what they do.'),
async execute(interaction) {
const commands = Array.from(interaction.client.commands.values()).filter(cmd => !!cmd.builder);
let text = '**Available Commands:**\n\n';
const db = require('../../backend/db').readDb();
const guildSettings = db[interaction.guildId] || {};
const toggles = guildSettings.commandToggles || {};
const protectedCommands = ['manage-commands', 'help'];
for (const cmd of commands) {
const isEnabled = protectedCommands.includes(cmd.name) ? true : (toggles[cmd.name] !== false && cmd.enabled !== false);
text += `/${cmd.name}${cmd.description || 'No description.'}${isEnabled ? 'Enabled' : 'Disabled'}${protectedCommands.includes(cmd.name) ? ' (locked)' : ''}\n`;
}
await interaction.reply({ content: text, flags: 64 });
},
};

View File

@@ -0,0 +1,112 @@
const { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, PermissionsBitField } = require('discord.js');
const { readDb, writeDb } = require('../../backend/db.js');
module.exports = {
name: 'manage-commands',
description: 'Admin: List bot commands and toggle them Enabled/Disabled for this server.',
enabled: true,
builder: new SlashCommandBuilder()
.setName('manage-commands')
.setDescription('Admin: List bot commands and toggle them Enabled/Disabled for this server.'),
async execute(interaction) {
// Only allow administrators
if (!interaction.member.permissions.has(PermissionsBitField.Flags.Administrator)) {
await interaction.reply({ content: 'You must be a server administrator to use this command.', flags: 64 });
return;
}
const db = readDb();
if (!db[interaction.guildId]) db[interaction.guildId] = {};
if (!db[interaction.guildId].commandToggles) db[interaction.guildId].commandToggles = {};
const toggles = db[interaction.guildId].commandToggles;
// Include all loaded commands so simple command modules (no SlashCommandBuilder) like
// `ping` are also listed. Filter for objects with a name for safety.
const commands = Array.from(interaction.client.commands.values()).filter(cmd => cmd && cmd.name);
// Build button components (max 5 rows, 5 buttons per row)
const actionRows = [];
let currentRow = new ActionRowBuilder();
let buttonsInRow = 0;
const protectedCommands = ['manage-commands', 'help'];
const buildButton = (cmd) => {
if (protectedCommands.includes(cmd.name)) return null;
const isEnabled = toggles[cmd.name] !== false && cmd.enabled !== false;
return new ButtonBuilder()
.setCustomId(`toggle_cmd_${cmd.name}`)
.setLabel(`${cmd.name} : ${isEnabled ? 'ENABLED' : 'DISABLED'}`)
.setStyle(isEnabled ? ButtonStyle.Success : ButtonStyle.Secondary);
};
for (const cmd of commands) {
const btn = buildButton(cmd);
if (btn) {
currentRow.addComponents(btn);
buttonsInRow++;
if (buttonsInRow === 5) {
actionRows.push(currentRow);
currentRow = new ActionRowBuilder();
buttonsInRow = 0;
}
}
}
if (buttonsInRow > 0) actionRows.push(currentRow);
const description = commands.map(cmd => `${cmd.name}${cmd.description || 'No description.'}${protectedCommands.includes(cmd.name) ? ' (locked)' : ''}`).join('\n');
await interaction.reply({ content: `Manage Commands for this server:\n\n${description}`, components: actionRows, flags: 64 });
const message = await interaction.fetchReply();
// Collector to handle button presses for 5 minutes
const filter = i => i.user.id === interaction.user.id && i.customId.startsWith('toggle_cmd_');
const collector = message.createMessageComponentCollector({ filter, time: 5 * 60 * 1000 });
collector.on('collect', async i => {
const cmdName = i.customId.replace('toggle_cmd_', '');
toggles[cmdName] = !(toggles[cmdName] !== false);
writeDb(db);
// rebuild buttons to reflect new state
const updatedRows = [];
let r = new ActionRowBuilder();
let count = 0;
for (const cmd of commands) {
if (protectedCommands.includes(cmd.name)) continue;
const btn = new ButtonBuilder()
.setCustomId(`toggle_cmd_${cmd.name}`)
.setLabel(`${cmd.name} : ${(toggles[cmd.name] !== false && cmd.enabled !== false) ? 'ENABLED' : 'DISABLED'}`)
.setStyle((toggles[cmd.name] !== false && cmd.enabled !== false) ? ButtonStyle.Success : ButtonStyle.Secondary);
r.addComponents(btn);
count++;
if (count === 5) { updatedRows.push(r); r = new ActionRowBuilder(); count = 0; }
}
if (count > 0) updatedRows.push(r);
await i.update({ content: `Manage Commands for this server:\n\n${description}`, components: updatedRows });
});
collector.on('end', async () => {
// disable buttons after collector ends
const disabledRows = [];
let rr = new ActionRowBuilder();
let ccount = 0;
for (const cmd of commands) {
if (protectedCommands.includes(cmd.name)) continue;
const btn = new ButtonBuilder()
.setCustomId(`toggle_cmd_${cmd.name}`)
.setLabel(`${cmd.name} : ${(toggles[cmd.name] !== false && cmd.enabled !== false) ? 'ENABLED' : 'DISABLED'}`)
.setStyle((toggles[cmd.name] !== false && cmd.enabled !== false) ? ButtonStyle.Success : ButtonStyle.Secondary)
.setDisabled(true);
rr.addComponents(btn);
ccount++;
if (ccount === 5) { disabledRows.push(rr); rr = new ActionRowBuilder(); ccount = 0; }
}
if (ccount > 0) disabledRows.push(rr);
try { await message.edit({ components: disabledRows }); } catch (e) { /* ignore */ }
});
},
};

View File

@@ -0,0 +1,63 @@
const { SlashCommandBuilder, ActionRowBuilder, RoleSelectMenuBuilder, ComponentType } = require('discord.js');
const { readDb, writeDb } = require('../../backend/db.js');
module.exports = {
name: 'setup-autorole',
description: 'Interactively set up the autorole for this server.',
enabled: true,
builder: new SlashCommandBuilder()
.setName('setup-autorole')
.setDescription('Interactively set up the autorole for this server.'),
async execute(interaction) {
const db = readDb();
const guildId = interaction.guildId;
if (!db[guildId]) {
db[guildId] = {};
}
const roleSelect = new RoleSelectMenuBuilder()
.setCustomId('autorole_role_select')
.setPlaceholder('Select the role to assign on join.');
const row = new ActionRowBuilder().addComponents(roleSelect);
const roleReply = await interaction.reply({
content: 'Please select the role you want to automatically assign to new members.',
components: [row],
flags: 64,
});
try {
const roleConfirmation = await roleReply.awaitMessageComponent({
componentType: ComponentType.RoleSelect,
time: 60000,
});
const roleId = roleConfirmation.values[0];
const role = interaction.guild.roles.cache.get(roleId);
if (role.managed || role.position >= interaction.guild.members.me.roles.highest.position) {
await roleConfirmation.update({
content: `I cannot assign the role **${role.name}** because it is managed by an integration or is higher than my highest role. Please select another role.`,
components: [],
});
return;
}
db[guildId].autorole = {
enabled: true,
roleId: roleId,
};
writeDb(db);
await roleConfirmation.update({
content: `Autorole setup complete! New members will be assigned the **${role.name}** role.`,
components: [],
});
} catch (error) {
console.error('Error during autorole setup:', error);
await interaction.editReply({ content: 'Configuration timed out or an error occurred.', components: [] });
}
},
};

View File

@@ -0,0 +1,116 @@
const { SlashCommandBuilder, ActionRowBuilder, ChannelSelectMenuBuilder, StringSelectMenuBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ComponentType } = require('discord.js');
const { readDb, writeDb } = require('../../backend/db.js');
const defaultLeaveMessages = ["{user} has left the server.", "Goodbye, {user}.", "We'll miss you, {user}."];
module.exports = {
name: 'setup-leave',
description: 'Interactively set up the leave message for this server.',
enabled: true,
builder: new SlashCommandBuilder()
.setName('setup-leave')
.setDescription('Interactively set up the leave message for this server.'),
async execute(interaction) {
const db = readDb();
const guildId = interaction.guildId;
if (!db[guildId]) {
db[guildId] = {};
}
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId('leave_channel_select')
.setPlaceholder('Select the channel for leave messages.')
.setChannelTypes([0]); // Text channels
const row1 = new ActionRowBuilder().addComponents(channelSelect);
const channelReply = await interaction.reply({
content: 'Please select the channel where you want leave messages to be sent.',
components: [row1],
flags: 64,
});
try {
const channelConfirmation = await channelReply.awaitMessageComponent({
componentType: ComponentType.ChannelSelect,
time: 60000,
});
const channelId = channelConfirmation.values[0];
db[guildId].leaveChannel = channelId;
db[guildId].leaveEnabled = true;
const messageOptions = defaultLeaveMessages.map(msg => ({
label: msg.length > 100 ? msg.substring(0, 97) + '...' : msg,
value: msg,
}));
messageOptions.push({
label: 'Custom Message',
value: 'custom',
});
const messageSelect = new StringSelectMenuBuilder()
.setCustomId('leave_message_select')
.setPlaceholder('Select a leave message or create your own.')
.addOptions(messageOptions);
const row2 = new ActionRowBuilder().addComponents(messageSelect);
await channelConfirmation.update({
content: `Channel <#${channelId}> selected. Now, please select a leave message.`,
components: [row2],
});
const messageConfirmation = await channelReply.awaitMessageComponent({
componentType: ComponentType.StringSelect,
time: 60000,
});
const selectedMessage = messageConfirmation.values[0];
if (selectedMessage === 'custom') {
const modal = new ModalBuilder()
.setCustomId('leave_custom_message_modal')
.setTitle('Custom Leave Message');
const messageInput = new TextInputBuilder()
.setCustomId('custom_message_input')
.setLabel("Your custom message")
.setPlaceholder('Use {user} for username and {server} for server name.')
.setStyle(TextInputStyle.Paragraph)
.setRequired(true);
const firstActionRow = new ActionRowBuilder().addComponents(messageInput);
modal.addComponents(firstActionRow);
await messageConfirmation.showModal(modal);
const modalSubmit = await interaction.awaitModalSubmit({
time: 300000,
});
const customMessage = modalSubmit.fields.getTextInputValue('custom_message_input');
db[guildId].leaveMessage = customMessage;
writeDb(db);
await modalSubmit.reply({
content: `Leave message setup complete! Channel: <#${channelId}>, Message: "${customMessage}"`,
flags: 64,
});
} else {
db[guildId].leaveMessage = selectedMessage;
writeDb(db);
await messageConfirmation.update({
content: `Leave message setup complete! Channel: <#${channelId}>, Message: "${selectedMessage}"`,
components: [],
});
}
} catch (error) {
console.error('Error during leave setup:', error);
await interaction.editReply({ content: 'Configuration timed out or an error occurred.', components: [] });
}
},
};

View File

@@ -1,63 +1,116 @@
const { SlashCommandBuilder } = require('discord.js');
const { SlashCommandBuilder, ActionRowBuilder, ChannelSelectMenuBuilder, StringSelectMenuBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ComponentType } = require('discord.js');
const { readDb, writeDb } = require('../../backend/db.js');
const defaultWelcomeMessages = ["Welcome to the server, {user}!", "Hey {user}, welcome!", "{user} has joined the party!"];
module.exports = {
name: 'config-welcome',
description: 'Configure the welcome message for this server.',
name: 'setup-welcome',
description: 'Interactively set up the welcome message for this server.',
enabled: true,
builder: new SlashCommandBuilder()
.setName('config-welcome')
.setDescription('Configure the welcome message for this server.')
.addSubcommand(subcommand =>
subcommand
.setName('set-channel')
.setDescription('Set the channel for welcome messages.')
.addChannelOption(option =>
option.setName('channel')
.setDescription('The channel to send welcome messages to.')
.setRequired(true)
)
)
.addSubcommand(subcommand =>
subcommand
.setName('set-message')
.setDescription('Set the welcome message.')
.addStringOption(option =>
option.setName('message')
.setDescription('The welcome message. Use {user} for username mention and {server} for server name.')
.setRequired(true)
)
)
.addSubcommand(subcommand =>
subcommand
.setName('disable')
.setDescription('Disable welcome messages.')
),
.setName('setup-welcome')
.setDescription('Interactively set up the welcome message for this server.'),
async execute(interaction) {
const db = readDb();
const guildId = interaction.guildId;
const subcommand = interaction.options.getSubcommand();
if (!db[guildId]) {
db[guildId] = {};
}
if (subcommand === 'set-channel') {
const channel = interaction.options.getChannel('channel');
db[guildId].welcomeChannel = channel.id;
const channelSelect = new ChannelSelectMenuBuilder()
.setCustomId('welcome_channel_select')
.setPlaceholder('Select the channel for welcome messages.')
.setChannelTypes([0]); // Text channels
const row1 = new ActionRowBuilder().addComponents(channelSelect);
const channelReply = await interaction.reply({
content: 'Please select the channel where you want welcome messages to be sent.',
components: [row1],
flags: 64,
});
try {
const channelConfirmation = await channelReply.awaitMessageComponent({
componentType: ComponentType.ChannelSelect,
time: 60000,
});
const channelId = channelConfirmation.values[0];
db[guildId].welcomeChannel = channelId;
db[guildId].welcomeEnabled = true;
writeDb(db);
await interaction.reply(`Welcome channel set to ${channel}.`);
} else if (subcommand === 'set-message') {
const message = interaction.options.getString('message');
db[guildId].welcomeMessage = message;
db[guildId].welcomeEnabled = true;
writeDb(db);
await interaction.reply(`Welcome message set to: "${message}"`);
} else if (subcommand === 'disable') {
db[guildId].welcomeEnabled = false;
writeDb(db);
await interaction.reply('Welcome messages disabled.');
const messageOptions = defaultWelcomeMessages.map(msg => ({
label: msg.length > 100 ? msg.substring(0, 97) + '...' : msg,
value: msg,
}));
messageOptions.push({
label: 'Custom Message',
value: 'custom',
});
const messageSelect = new StringSelectMenuBuilder()
.setCustomId('welcome_message_select')
.setPlaceholder('Select a welcome message or create your own.')
.addOptions(messageOptions);
const row2 = new ActionRowBuilder().addComponents(messageSelect);
await channelConfirmation.update({
content: `Channel <#${channelId}> selected. Now, please select a welcome message.`,
components: [row2],
});
const messageConfirmation = await channelReply.awaitMessageComponent({
componentType: ComponentType.StringSelect,
time: 60000,
});
const selectedMessage = messageConfirmation.values[0];
if (selectedMessage === 'custom') {
const modal = new ModalBuilder()
.setCustomId('welcome_custom_message_modal')
.setTitle('Custom Welcome Message');
const messageInput = new TextInputBuilder()
.setCustomId('custom_message_input')
.setLabel("Your custom message")
.setPlaceholder('Use {user} for username and {server} for server name.')
.setStyle(TextInputStyle.Paragraph)
.setRequired(true);
const firstActionRow = new ActionRowBuilder().addComponents(messageInput);
modal.addComponents(firstActionRow);
await messageConfirmation.showModal(modal);
const modalSubmit = await interaction.awaitModalSubmit({
time: 300000,
});
const customMessage = modalSubmit.fields.getTextInputValue('custom_message_input');
db[guildId].welcomeMessage = customMessage;
writeDb(db);
await modalSubmit.reply({
content: `Welcome message setup complete! Channel: <#${channelId}>, Message: "${customMessage}"`,
flags: 64,
});
} else {
db[guildId].welcomeMessage = selectedMessage;
writeDb(db);
await messageConfirmation.update({
content: `Welcome message setup complete! Channel: <#${channelId}>, Message: "${selectedMessage}"`,
components: [],
});
}
} catch (error) {
console.error('Error during welcome setup:', error);
await interaction.editReply({ content: 'Configuration timed out or an error occurred.', components: [] });
}
},
};

View File

@@ -0,0 +1,29 @@
const { SlashCommandBuilder } = require('discord.js');
const { readDb } = require('../../backend/db.js');
module.exports = {
name: 'view-autorole',
description: 'View the current autorole configuration for this server.',
enabled: true,
builder: new SlashCommandBuilder()
.setName('view-autorole')
.setDescription('View the current autorole configuration for this server.'),
async execute(interaction) {
const db = readDb();
const guildId = interaction.guildId;
const settings = db[guildId] || {};
const autorole = settings.autorole || { enabled: false, roleId: '' };
if (!autorole.enabled) {
await interaction.reply({ content: 'Autorole is currently disabled for this server.', flags: 64 });
return;
}
const role = interaction.guild.roles.cache.get(autorole.roleId);
if (role) {
await interaction.reply({ content: `Autorole is enabled. Selected role: ${role.toString()} (${role.name}).`, flags: 64 });
} else {
await interaction.reply({ content: `Autorole is enabled but the selected role (ID: ${autorole.roleId}) was not found on this server.`, flags: 64 });
}
},
};

View File

@@ -19,6 +19,23 @@ module.exports = {
}
}
}
if (settings && settings.autorole && settings.autorole.enabled && settings.autorole.roleId) {
const role = member.guild.roles.cache.get(settings.autorole.roleId);
if (role) {
try {
// Re-check that role is assignable
const botHighest = member.guild.members.me.roles.highest.position;
if (role.id === member.guild.id || role.managed || role.position >= botHighest) {
console.warn(`Autorole ${role.id} in guild ${member.guild.id} is not assignable (everyone/managed/too high). Skipping.`);
return;
}
await member.roles.add(role);
} catch (error) {
console.error(`Could not assign autorole in guild ${member.guild.id}:`, error);
}
}
}
} catch (error) {
console.error(`Error in guildMemberAdd event for guild ${member.guild.id}:`, error);
}

View File

@@ -9,14 +9,31 @@ module.exports = {
const settings = db[member.guild.id];
if (settings && settings.leaveEnabled && settings.leaveChannel) {
const channel = member.guild.channels.cache.get(settings.leaveChannel);
if (channel) {
let channel = member.guild.channels.cache.get(settings.leaveChannel);
if (!channel) {
try {
const message = (settings.leaveMessage || '{user} has left the server.').replace('{user}', member.user.tag).replace('{server}', member.guild.name);
channel = await member.guild.channels.fetch(settings.leaveChannel);
} catch (err) {
return;
}
}
if (channel && channel.isTextBased && channel.isTextBased()) {
try {
const me = member.guild.members.me;
const perms = channel.permissionsFor(me);
if (!perms || !perms.has('ViewChannel') || !perms.has('SendMessages')) {
return;
}
const userMention = member.user ? (member.user.toString ? member.user.toString() : member.user.tag) : 'A user';
const message = (settings.leaveMessage || '{user} has left the server.').replace('{user}', userMention).replace('{server}', member.guild.name);
await channel.send(message);
} catch (error) {
console.error(`Could not send leave message to channel ${settings.leaveChannel} in guild ${member.guild.id}:`, error);
}
} else {
return;
}
}
} catch (error) {

View File

@@ -7,9 +7,9 @@ module.exports = (client) => {
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
// Clear require cache to allow updates during development
delete require.cache[require.resolve(filePath)];
const command = require(filePath);
if (command.enabled === false) continue;
if (command.name) {
client.commands.set(command.name, command);
}

View File

@@ -2,6 +2,7 @@ const { Client, GatewayIntentBits, Collection } = require('discord.js');
const fs = require('fs');
const path = require('path');
const deployCommands = require('./deploy-commands');
const { readDb } = require('../backend/db');
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers] });
@@ -20,11 +21,35 @@ client.on('interactionCreate', async interaction => {
if (!command) return;
// Check per-guild toggles
try {
await command.execute(interaction);
const db = readDb();
const guildSettings = db[interaction.guildId] || {};
const toggles = guildSettings.commandToggles || {};
const protectedCommands = ['manage-commands', 'help'];
// If command is protected, always allow
if (!protectedCommands.includes(command.name)) {
if (toggles[command.name] === false) {
await interaction.reply({ content: 'This command has been disabled on this server.', flags: 64 });
return;
}
// If the module-level enabled flag is false, treat as disabled too
if (command.enabled === false) {
await interaction.reply({ content: 'This command is currently disabled globally.', flags: 64 });
return;
}
}
try {
await command.execute(interaction);
} catch (error) {
console.error(error);
await interaction.reply({ content: 'There was an error while executing this command!', flags: 64 });
}
} catch (error) {
console.error(error);
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
console.error('Error checking command toggles:', error);
await interaction.reply({ content: 'Internal error occurred.', flags: 64 });
}
});