diff --git a/backend/index.js b/backend/index.js
index 6b62fb1..23746fb 100644
--- a/backend/index.js
+++ b/backend/index.js
@@ -182,6 +182,50 @@ app.post('/api/servers/:guildId/welcome-leave-settings', (req, res) => {
db[guildId].leaveMessage = newSettings.leave.message;
db[guildId].leaveCustomMessage = newSettings.leave.customMessage;
+ writeDb(db);
+
+ res.json({ success: true });
+});
+
+app.get('/api/servers/:guildId/roles', async (req, res) => {
+ const { guildId } = req.params;
+ const guild = bot.client.guilds.cache.get(guildId);
+ if (!guild) {
+ return res.json([]);
+ }
+ try {
+ const rolesCollection = await guild.roles.fetch();
+ // Exclude @everyone (role.id === guild.id), exclude managed roles, and only include roles below the bot's highest role
+ const botHighest = guild.members.me.roles.highest.position;
+ const manageable = rolesCollection
+ .filter(role => role.id !== guild.id && !role.managed && role.position < botHighest)
+ .sort((a, b) => b.position - a.position)
+ .map(role => ({ id: role.id, name: role.name, color: role.hexColor }));
+ res.json(manageable);
+ } catch (error) {
+ console.error('Error fetching roles:', error);
+ res.status(500).json({ success: false, message: 'Internal Server Error' });
+ }
+});
+
+app.get('/api/servers/:guildId/autorole-settings', (req, res) => {
+ const { guildId } = req.params;
+ const db = readDb();
+ const settings = db[guildId] || {};
+ const autoroleSettings = settings.autorole || { enabled: false, roleId: '' };
+ res.json(autoroleSettings);
+});
+
+app.post('/api/servers/:guildId/autorole-settings', (req, res) => {
+ const { guildId } = req.params;
+ const { enabled, roleId } = req.body;
+ const db = readDb();
+
+ if (!db[guildId]) {
+ db[guildId] = {};
+ }
+
+ db[guildId].autorole = { enabled, roleId };
writeDb(db);
res.json({ success: true });
});
@@ -190,6 +234,34 @@ app.get('/', (req, res) => {
res.send('Hello from the backend!');
});
+// Return list of bot commands and per-guild enabled/disabled status
+app.get('/api/servers/:guildId/commands', (req, res) => {
+ try {
+ const { guildId } = req.params;
+ const db = readDb();
+ const guildSettings = db[guildId] || {};
+ const toggles = guildSettings.commandToggles || {};
+ const protectedCommands = ['manage-commands', 'help'];
+
+ const commands = Array.from(bot.client.commands.values()).map(cmd => {
+ const isLocked = protectedCommands.includes(cmd.name);
+ const isEnabled = isLocked ? true : (toggles[cmd.name] !== false && cmd.enabled !== false);
+ return {
+ name: cmd.name,
+ description: cmd.description || 'No description.',
+ enabled: isEnabled,
+ locked: isLocked,
+ hasSlashBuilder: !!cmd.builder,
+ };
+ });
+
+ res.json(commands);
+ } catch (error) {
+ console.error('Error returning commands:', error);
+ res.status(500).json({ success: false, message: 'Internal Server Error' });
+ }
+});
+
const bot = require('../discord-bot');
bot.login();
diff --git a/checklist.md b/checklist.md
index b2a4a73..aeed8f6 100644
--- a/checklist.md
+++ b/checklist.md
@@ -82,6 +82,8 @@
- [x] **Bot Integration:**
- [x] Connect frontend settings to the backend.
- [x] Implement bot logic to send welcome/leave messages based on server settings.
+ - [x] Fix: Leave messages now fetch channel reliably, ensure bot has permissions (ViewChannel/SendMessages), and use mention-friendly user formatting. Added debug logging.
+ - [x] Fix: Removed verbose console logging of incoming settings and messages in backend and bot (no sensitive or noisy payloads logged).
- [x] **Slash Command Integration:**
- [x] ~~Create a `/config-welcome` slash command.~~
- [x] ~~Add a subcommand to `set-channel` for welcome messages.~~
@@ -92,9 +94,32 @@
- [x] ~~Add a subcommand to `set-message` with options for default and custom messages.~~
- [x] ~~Add a subcommand to `disable` leave messages.~~
- [x] ~~Create a `/view-config` slash command to display the current welcome and leave channels.~~
- - [ ] Refactor `/config-welcome` to `/setup-welcome` with interactive setup for channel and message.
- - [ ] Refactor `/config-leave` to `/setup-leave` with interactive setup for channel and message.
- - [ ] Rename `/view-config` to `/view-welcome-leave`.
+ - [x] Refactor `/config-welcome` to `/setup-welcome` with interactive setup for channel and message.
+ - [x] Refactor `/config-leave` to `/setup-leave` with interactive setup for channel and message.
+ - [x] Rename `/view-config` to `/view-welcome-leave`.
- [x] Ensure settings updated via slash commands are reflected on the frontend.
- [x] Ensure settings updated via the frontend are reflected in the bot's behavior.
- - [x] Persist the selected message option (default or custom) for welcome and leave messages.
\ No newline at end of file
+ - [x] **New:** Interactive setup should prompt for channel, then for message (default or custom, matching frontend options).
+ - [x] Persist the selected message option (default or custom) for welcome and leave messages.
+ - [x] Added `/view-autorole` slash command to report autorole status and selected role.
+ - [x] Added `/manage-commands` admin slash command to list and toggle commands per-server (persists toggles to backend DB).
+ - [x] Refactor: `/manage-commands` now renders a single message with toggle buttons reflecting each command's current state and updates in-place.
+ - [x] Ensure `/manage-commands` lists all loaded commands (including non-slash/simple commands like `ping`) and will include future commands automatically.
+ - [x] Ensure the `/help` command is locked (protected) and cannot be disabled via `/manage-commands`.
+ - [x] Add a Help tab in the frontend Server Settings that lists all bot commands and their descriptions per-server.
+ - [x] Move Help to a dedicated page within the server dashboard and add a top NavBar (collapsible) with Dashboard, Discord!, Contact, and Help (when on a server) buttons. Ensure Help page has a back arrow to return to the Commands section.
+ - [x] Added `/help` slash command that lists commands and their descriptions and shows per-server enable/disable status.
+- [x] **Autorole**
+ - [x] Add "Autorole" section to server settings.
+ - [x] **Backend:**
+ - [x] Create API endpoint to get/set autorole settings.
+ - [x] Create API endpoint to fetch server roles.
+ - [x] **Bot Integration:**
+ - [x] Create a `/setup-autorole` slash command to enable/disable and select a role.
+ - [x] Update `guildMemberAdd` event to assign the selected role on join.
+ - [x] **Frontend:**
+ - [x] Add toggle to enable/disable autorole.
+ - [x] Add dropdown to select a role for autorole.
+ - [x] Ensure settings updated via slash commands are reflected on the frontend.
+ - [x] Ensure settings updated via the frontend are reflected in the bot's behavior.
+ - [x] Fix: Autorole dropdown excludes @everyone and shows only roles the bot can manage. Assignment is validated at join.
\ No newline at end of file
diff --git a/discord-bot/commands/config-leave.js b/discord-bot/commands/config-leave.js
deleted file mode 100644
index 87af3d1..0000000
--- a/discord-bot/commands/config-leave.js
+++ /dev/null
@@ -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.');
- }
- },
-};
diff --git a/discord-bot/commands/help.js b/discord-bot/commands/help.js
new file mode 100644
index 0000000..3d2e0b5
--- /dev/null
+++ b/discord-bot/commands/help.js
@@ -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 });
+ },
+};
diff --git a/discord-bot/commands/manage-commands.js b/discord-bot/commands/manage-commands.js
new file mode 100644
index 0000000..84bb95c
--- /dev/null
+++ b/discord-bot/commands/manage-commands.js
@@ -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 */ }
+ });
+ },
+};
diff --git a/discord-bot/commands/setup-autorole.js b/discord-bot/commands/setup-autorole.js
new file mode 100644
index 0000000..264e9aa
--- /dev/null
+++ b/discord-bot/commands/setup-autorole.js
@@ -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: [] });
+ }
+ },
+};
diff --git a/discord-bot/commands/setup-leave.js b/discord-bot/commands/setup-leave.js
new file mode 100644
index 0000000..dacd8ce
--- /dev/null
+++ b/discord-bot/commands/setup-leave.js
@@ -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: [] });
+ }
+ },
+};
diff --git a/discord-bot/commands/setup-welcome.js b/discord-bot/commands/setup-welcome.js
index bb57918..8787981 100644
--- a/discord-bot/commands/setup-welcome.js
+++ b/discord-bot/commands/setup-welcome.js
@@ -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: [] });
}
},
};
diff --git a/discord-bot/commands/view-autorole.js b/discord-bot/commands/view-autorole.js
new file mode 100644
index 0000000..1320cb8
--- /dev/null
+++ b/discord-bot/commands/view-autorole.js
@@ -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 });
+ }
+ },
+};
diff --git a/discord-bot/events/guildMemberAdd.js b/discord-bot/events/guildMemberAdd.js
index 487b65d..d12dd39 100644
--- a/discord-bot/events/guildMemberAdd.js
+++ b/discord-bot/events/guildMemberAdd.js
@@ -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);
}
diff --git a/discord-bot/events/guildMemberRemove.js b/discord-bot/events/guildMemberRemove.js
index c6f1d3d..f67df42 100644
--- a/discord-bot/events/guildMemberRemove.js
+++ b/discord-bot/events/guildMemberRemove.js
@@ -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) {
diff --git a/discord-bot/handlers/command-handler.js b/discord-bot/handlers/command-handler.js
index 9cf6d1c..5f2bc5b 100644
--- a/discord-bot/handlers/command-handler.js
+++ b/discord-bot/handlers/command-handler.js
@@ -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);
}
diff --git a/discord-bot/index.js b/discord-bot/index.js
index e51ca7a..c798443 100644
--- a/discord-bot/index.js
+++ b/discord-bot/index.js
@@ -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 });
}
});
diff --git a/frontend/src/App.js b/frontend/src/App.js
index 658474a..ec99ff8 100644
--- a/frontend/src/App.js
+++ b/frontend/src/App.js
@@ -6,6 +6,10 @@ import { CssBaseline } from '@mui/material';
import Login from './components/Login';
import Dashboard from './components/Dashboard';
import ServerSettings from './components/ServerSettings';
+import NavBar from './components/NavBar';
+import HelpPage from './components/HelpPage';
+import ContactPage from './components/ContactPage';
+import DiscordPage from './components/DiscordPage';
function App() {
return (
@@ -13,10 +17,14 @@ function App() {