ui updates and bot updates. new commands and command handler
This commit is contained in:
@@ -182,6 +182,50 @@ app.post('/api/servers/:guildId/welcome-leave-settings', (req, res) => {
|
|||||||
db[guildId].leaveMessage = newSettings.leave.message;
|
db[guildId].leaveMessage = newSettings.leave.message;
|
||||||
db[guildId].leaveCustomMessage = newSettings.leave.customMessage;
|
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);
|
writeDb(db);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
@@ -190,6 +234,34 @@ app.get('/', (req, res) => {
|
|||||||
res.send('Hello from the backend!');
|
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');
|
const bot = require('../discord-bot');
|
||||||
|
|
||||||
bot.login();
|
bot.login();
|
||||||
|
|||||||
33
checklist.md
33
checklist.md
@@ -82,6 +82,8 @@
|
|||||||
- [x] **Bot Integration:**
|
- [x] **Bot Integration:**
|
||||||
- [x] Connect frontend settings to the backend.
|
- [x] Connect frontend settings to the backend.
|
||||||
- [x] Implement bot logic to send welcome/leave messages based on server settings.
|
- [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] **Slash Command Integration:**
|
||||||
- [x] ~~Create a `/config-welcome` slash command.~~
|
- [x] ~~Create a `/config-welcome` slash command.~~
|
||||||
- [x] ~~Add a subcommand to `set-channel` for welcome messages.~~
|
- [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 `set-message` with options for default and custom messages.~~
|
||||||
- [x] ~~Add a subcommand to `disable` leave messages.~~
|
- [x] ~~Add a subcommand to `disable` leave messages.~~
|
||||||
- [x] ~~Create a `/view-config` slash command to display the current welcome and leave channels.~~
|
- [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.
|
- [x] 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.
|
- [x] Refactor `/config-leave` to `/setup-leave` with interactive setup for channel and message.
|
||||||
- [ ] Rename `/view-config` to `/view-welcome-leave`.
|
- [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 slash commands are reflected on the frontend.
|
||||||
- [x] Ensure settings updated via the frontend are reflected in the bot's behavior.
|
- [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.
|
- [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.
|
||||||
@@ -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.');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
25
discord-bot/commands/help.js
Normal file
25
discord-bot/commands/help.js
Normal 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 });
|
||||||
|
},
|
||||||
|
};
|
||||||
112
discord-bot/commands/manage-commands.js
Normal file
112
discord-bot/commands/manage-commands.js
Normal 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 */ }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
63
discord-bot/commands/setup-autorole.js
Normal file
63
discord-bot/commands/setup-autorole.js
Normal 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: [] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
116
discord-bot/commands/setup-leave.js
Normal file
116
discord-bot/commands/setup-leave.js
Normal 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: [] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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 { readDb, writeDb } = require('../../backend/db.js');
|
||||||
|
|
||||||
|
const defaultWelcomeMessages = ["Welcome to the server, {user}!", "Hey {user}, welcome!", "{user} has joined the party!"];
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: 'config-welcome',
|
name: 'setup-welcome',
|
||||||
description: 'Configure the welcome message for this server.',
|
description: 'Interactively set up the welcome message for this server.',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
builder: new SlashCommandBuilder()
|
builder: new SlashCommandBuilder()
|
||||||
.setName('config-welcome')
|
.setName('setup-welcome')
|
||||||
.setDescription('Configure the welcome message for this server.')
|
.setDescription('Interactively set up 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.')
|
|
||||||
),
|
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
const db = readDb();
|
const db = readDb();
|
||||||
const guildId = interaction.guildId;
|
const guildId = interaction.guildId;
|
||||||
const subcommand = interaction.options.getSubcommand();
|
|
||||||
|
|
||||||
if (!db[guildId]) {
|
if (!db[guildId]) {
|
||||||
db[guildId] = {};
|
db[guildId] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'set-channel') {
|
const channelSelect = new ChannelSelectMenuBuilder()
|
||||||
const channel = interaction.options.getChannel('channel');
|
.setCustomId('welcome_channel_select')
|
||||||
db[guildId].welcomeChannel = channel.id;
|
.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;
|
db[guildId].welcomeEnabled = true;
|
||||||
writeDb(db);
|
|
||||||
await interaction.reply(`Welcome channel set to ${channel}.`);
|
const messageOptions = defaultWelcomeMessages.map(msg => ({
|
||||||
} else if (subcommand === 'set-message') {
|
label: msg.length > 100 ? msg.substring(0, 97) + '...' : msg,
|
||||||
const message = interaction.options.getString('message');
|
value: msg,
|
||||||
db[guildId].welcomeMessage = message;
|
}));
|
||||||
db[guildId].welcomeEnabled = true;
|
|
||||||
writeDb(db);
|
messageOptions.push({
|
||||||
await interaction.reply(`Welcome message set to: "${message}"`);
|
label: 'Custom Message',
|
||||||
} else if (subcommand === 'disable') {
|
value: 'custom',
|
||||||
db[guildId].welcomeEnabled = false;
|
});
|
||||||
writeDb(db);
|
|
||||||
await interaction.reply('Welcome messages disabled.');
|
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: [] });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
29
discord-bot/commands/view-autorole.js
Normal file
29
discord-bot/commands/view-autorole.js
Normal 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 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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) {
|
} catch (error) {
|
||||||
console.error(`Error in guildMemberAdd event for guild ${member.guild.id}:`, error);
|
console.error(`Error in guildMemberAdd event for guild ${member.guild.id}:`, error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,31 @@ module.exports = {
|
|||||||
const settings = db[member.guild.id];
|
const settings = db[member.guild.id];
|
||||||
|
|
||||||
if (settings && settings.leaveEnabled && settings.leaveChannel) {
|
if (settings && settings.leaveEnabled && settings.leaveChannel) {
|
||||||
const channel = member.guild.channels.cache.get(settings.leaveChannel);
|
let channel = member.guild.channels.cache.get(settings.leaveChannel);
|
||||||
if (channel) {
|
if (!channel) {
|
||||||
try {
|
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);
|
await channel.send(message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Could not send leave message to channel ${settings.leaveChannel} in guild ${member.guild.id}:`, error);
|
console.error(`Could not send leave message to channel ${settings.leaveChannel} in guild ${member.guild.id}:`, error);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ module.exports = (client) => {
|
|||||||
|
|
||||||
for (const file of commandFiles) {
|
for (const file of commandFiles) {
|
||||||
const filePath = path.join(commandsPath, file);
|
const filePath = path.join(commandsPath, file);
|
||||||
|
// Clear require cache to allow updates during development
|
||||||
|
delete require.cache[require.resolve(filePath)];
|
||||||
const command = require(filePath);
|
const command = require(filePath);
|
||||||
if (command.enabled === false) continue;
|
|
||||||
|
|
||||||
if (command.name) {
|
if (command.name) {
|
||||||
client.commands.set(command.name, command);
|
client.commands.set(command.name, command);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const { Client, GatewayIntentBits, Collection } = require('discord.js');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const deployCommands = require('./deploy-commands');
|
const deployCommands = require('./deploy-commands');
|
||||||
|
const { readDb } = require('../backend/db');
|
||||||
|
|
||||||
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers] });
|
const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers] });
|
||||||
|
|
||||||
@@ -20,11 +21,35 @@ client.on('interactionCreate', async interaction => {
|
|||||||
|
|
||||||
if (!command) return;
|
if (!command) return;
|
||||||
|
|
||||||
|
// Check per-guild toggles
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error('Error checking command toggles:', error);
|
||||||
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
|
await interaction.reply({ content: 'Internal error occurred.', flags: 64 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import { CssBaseline } from '@mui/material';
|
|||||||
import Login from './components/Login';
|
import Login from './components/Login';
|
||||||
import Dashboard from './components/Dashboard';
|
import Dashboard from './components/Dashboard';
|
||||||
import ServerSettings from './components/ServerSettings';
|
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() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@@ -13,10 +17,14 @@ function App() {
|
|||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Router>
|
<Router>
|
||||||
|
<NavBar />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Login />} />
|
<Route path="/" element={<Login />} />
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/server/:guildId" element={<ServerSettings />} />
|
<Route path="/server/:guildId" element={<ServerSettings />} />
|
||||||
|
<Route path="/server/:guildId/help" element={<HelpPage />} />
|
||||||
|
<Route path="/contact" element={<ContactPage />} />
|
||||||
|
<Route path="/discord" element={<DiscordPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
11
frontend/src/components/ContactPage.js
Normal file
11
frontend/src/components/ContactPage.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
const ContactPage = () => (
|
||||||
|
<Box sx={{ padding: 2 }}>
|
||||||
|
<Typography variant="h5">Contact</Typography>
|
||||||
|
<Typography sx={{ mt: 1 }}>For support, contact support@example.com</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ContactPage;
|
||||||
11
frontend/src/components/DiscordPage.js
Normal file
11
frontend/src/components/DiscordPage.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
const DiscordPage = () => (
|
||||||
|
<Box sx={{ padding: 2 }}>
|
||||||
|
<Typography variant="h5">Discord!</Typography>
|
||||||
|
<Typography sx={{ mt: 1 }}>Open the Discord invite or community links here.</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default DiscordPage;
|
||||||
46
frontend/src/components/HelpPage.js
Normal file
46
frontend/src/components/HelpPage.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { Box, IconButton, Typography } from '@mui/material';
|
||||||
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
|
|
||||||
|
const HelpPage = () => {
|
||||||
|
const { guildId } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const [commands, setCommands] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
axios.get(`http://localhost:3002/api/servers/${guildId}/commands`)
|
||||||
|
.then(res => setCommands(res.data || []))
|
||||||
|
.catch(() => setCommands([]));
|
||||||
|
}, [guildId]);
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
// Navigate back to server settings and instruct it to open Commands accordion
|
||||||
|
navigate(`/server/${guildId}`, { state: { openCommands: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 20 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<IconButton onClick={handleBack}><ArrowBackIcon /></IconButton>
|
||||||
|
<Typography variant="h5">Help - Commands for this Server</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ marginTop: 2 }}>
|
||||||
|
{commands.length === 0 && <Typography>No commands available.</Typography>}
|
||||||
|
{commands.map(cmd => (
|
||||||
|
<Box key={cmd.name} sx={{ border: '1px solid #eee', borderRadius: 1, padding: 1, marginTop: 1 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Typography sx={{ fontWeight: 'bold' }}>/{cmd.name}</Typography>
|
||||||
|
<Typography sx={{ color: cmd.locked ? 'primary.main' : (cmd.enabled ? 'success.main' : 'text.secondary') }}>{cmd.locked ? 'Locked' : (cmd.enabled ? 'Enabled' : 'Disabled')}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Typography sx={{ mt: 0.5 }}>{cmd.description}</Typography>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HelpPage;
|
||||||
21
frontend/src/components/NameBar.js
Normal file
21
frontend/src/components/NameBar.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { AppBar, Toolbar, Button, Box } from '@mui/material';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
const NameBar = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppBar position="static" color="transparent" elevation={0} sx={{ mb: 2 }}>
|
||||||
|
<Toolbar>
|
||||||
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<Button onClick={() => navigate('/dashboard')} color="inherit">Dashboard</Button>
|
||||||
|
<Button onClick={() => navigate('/discord')} color="inherit">Discord!</Button>
|
||||||
|
<Button onClick={() => navigate('/contact')} color="inherit">Contact</Button>
|
||||||
|
</Box>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NameBar;
|
||||||
34
frontend/src/components/NavBar.js
Normal file
34
frontend/src/components/NavBar.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { AppBar, Toolbar, Button, Box, IconButton, Collapse } from '@mui/material';
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
|
||||||
|
import { useNavigate, useLocation, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
const NavBar = () => {
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
// Pull guildId from URL if present
|
||||||
|
const guildIdMatch = location.pathname.match(/\/server\/(\d+)/);
|
||||||
|
const guildId = guildIdMatch ? guildIdMatch[1] : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppBar position="static" color="transparent" elevation={0} sx={{ mb: 2 }}>
|
||||||
|
<Toolbar>
|
||||||
|
<IconButton onClick={() => setOpen(prev => !prev)} aria-label="toggle menu"><MenuIcon /></IconButton>
|
||||||
|
<Collapse in={open} orientation="horizontal">
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Button onClick={() => navigate('/dashboard')} color="inherit">Dashboard</Button>
|
||||||
|
<Button onClick={() => navigate('/discord')} color="inherit">Discord!</Button>
|
||||||
|
<Button onClick={() => navigate('/contact')} color="inherit">Contact</Button>
|
||||||
|
{guildId && (
|
||||||
|
<Button startIcon={<HelpOutlineIcon />} onClick={() => navigate(`/server/${guildId}/help`)} color="inherit">Help</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavBar;
|
||||||
@@ -17,6 +17,13 @@ const ServerSettings = () => {
|
|||||||
const [server, setServer] = useState(null);
|
const [server, setServer] = useState(null);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
const [channels, setChannels] = useState([]);
|
const [channels, setChannels] = useState([]);
|
||||||
|
const [roles, setRoles] = useState([]);
|
||||||
|
const [autoroleSettings, setAutoroleSettings] = useState({
|
||||||
|
enabled: false,
|
||||||
|
roleId: '',
|
||||||
|
});
|
||||||
|
const [commandsList, setCommandsList] = useState([]);
|
||||||
|
const [commandsExpanded, setCommandsExpanded] = useState(false);
|
||||||
const [welcomeLeaveSettings, setWelcomeLeaveSettings] = useState({
|
const [welcomeLeaveSettings, setWelcomeLeaveSettings] = useState({
|
||||||
welcome: {
|
welcome: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -79,8 +86,53 @@ const ServerSettings = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fetch roles
|
||||||
|
axios.get(`http://localhost:3002/api/servers/${guildId}/roles`)
|
||||||
|
.then(response => {
|
||||||
|
setRoles(response.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch autorole settings
|
||||||
|
axios.get(`http://localhost:3002/api/servers/${guildId}/autorole-settings`)
|
||||||
|
.then(response => {
|
||||||
|
if (response.data) {
|
||||||
|
setAutoroleSettings(response.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch commands/help list
|
||||||
|
axios.get(`http://localhost:3002/api/servers/${guildId}/commands`)
|
||||||
|
.then(response => {
|
||||||
|
setCommandsList(response.data || []);
|
||||||
|
})
|
||||||
|
.catch(() => setCommandsList([]));
|
||||||
|
|
||||||
|
// Open commands accordion if navigated from Help back button
|
||||||
|
if (location.state && location.state.openCommands) {
|
||||||
|
setCommandsExpanded(true);
|
||||||
|
}
|
||||||
|
|
||||||
}, [guildId, location.state]);
|
}, [guildId, location.state]);
|
||||||
|
|
||||||
|
const handleAutoroleSettingUpdate = (newSettings) => {
|
||||||
|
axios.post(`http://localhost:3002/api/servers/${guildId}/autorole-settings`, newSettings)
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.success) {
|
||||||
|
setAutoroleSettings(newSettings);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAutoroleToggleChange = (event) => {
|
||||||
|
const newSettings = { ...autoroleSettings, enabled: event.target.checked };
|
||||||
|
handleAutoroleSettingUpdate(newSettings);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAutoroleRoleChange = (event) => {
|
||||||
|
const newSettings = { ...autoroleSettings, roleId: event.target.value };
|
||||||
|
handleAutoroleSettingUpdate(newSettings);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSettingUpdate = (newSettings) => {
|
const handleSettingUpdate = (newSettings) => {
|
||||||
axios.post(`http://localhost:3002/api/servers/${guildId}/welcome-leave-settings`, newSettings)
|
axios.post(`http://localhost:3002/api/servers/${guildId}/welcome-leave-settings`, newSettings)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@@ -193,7 +245,7 @@ const ServerSettings = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
<UserSettings />
|
<UserSettings />
|
||||||
</Box>
|
</Box>
|
||||||
<Accordion sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }}>
|
<Accordion sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }} expanded={commandsExpanded} onChange={() => setCommandsExpanded(prev => !prev)}>
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
<Typography variant="h6">Commands</Typography>
|
<Typography variant="h6">Commands</Typography>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
@@ -201,12 +253,18 @@ const ServerSettings = () => {
|
|||||||
{!isBotInServer && <Typography>Invite the bot to enable commands.</Typography>}
|
{!isBotInServer && <Typography>Invite the bot to enable commands.</Typography>}
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '10px' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '10px' }}>
|
||||||
<Typography>Ping Command</Typography>
|
<Typography>Ping Command</Typography>
|
||||||
<Button variant="contained" onClick={togglePingCommand} disabled={!isBotInServer}>
|
<Box>
|
||||||
{settings.pingCommand ? 'Disable' : 'Enable'}
|
<Button variant="contained" onClick={togglePingCommand} disabled={!isBotInServer}>
|
||||||
</Button>
|
{settings.pingCommand ? 'Disable' : 'Enable'}
|
||||||
|
</Button>
|
||||||
|
<Button sx={{ ml: 1 }} variant="outlined" onClick={() => navigate(`/server/${guildId}/help`)}>
|
||||||
|
Open Help Page
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
{/* Help moved to dedicated Help page */}
|
||||||
<Accordion sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }}>
|
<Accordion sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }}>
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
<Typography variant="h6">Welcome/Leave</Typography>
|
<Typography variant="h6">Welcome/Leave</Typography>
|
||||||
@@ -297,6 +355,32 @@ const ServerSettings = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
<Accordion sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }}>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
|
<Typography variant="h6">Autorole</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
{!isBotInServer && <Typography>Invite the bot to enable this feature.</Typography>}
|
||||||
|
<Box sx={{ marginTop: '10px' }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={<Switch checked={autoroleSettings.enabled} onChange={handleAutoroleToggleChange} disabled={!isBotInServer} />}
|
||||||
|
label="Enable Autorole"
|
||||||
|
/>
|
||||||
|
<FormControl fullWidth sx={{ marginTop: '10px' }} disabled={!isBotInServer || !autoroleSettings.enabled}>
|
||||||
|
<Select
|
||||||
|
value={autoroleSettings.roleId}
|
||||||
|
onChange={handleAutoroleRoleChange}
|
||||||
|
displayEmpty
|
||||||
|
>
|
||||||
|
<MenuItem value="" disabled>Select a role</MenuItem>
|
||||||
|
{roles.map(role => (
|
||||||
|
<MenuItem key={role.id} value={role.id}><span style={{ color: role.color }}>{role.name}</span></MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
<Accordion sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }}>
|
<Accordion sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }}>
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
<Typography variant="h6">Admin Commands</Typography>
|
<Typography variant="h6">Admin Commands</Typography>
|
||||||
|
|||||||
Reference in New Issue
Block a user