From 9bc7a5e6b8db3feee38201bae07d74f6a90339fa Mon Sep 17 00:00:00 2001 From: chad Date: Fri, 3 Oct 2025 08:39:32 -0400 Subject: [PATCH] ui changes --- backend/db.js | 38 +++ backend/index.js | 140 ++++++---- backend/package-lock.json | 304 +--------------------- backend/package.json | 3 +- checklist.md | 27 ++ discord-bot/commands/ping.js | 23 +- discord-bot/deploy-commands.js | 17 +- discord-bot/events/guildMemberAdd.js | 26 ++ discord-bot/events/guildMemberRemove.js | 26 ++ discord-bot/events/ready.js | 2 +- discord-bot/index.js | 7 +- discord-bot/package-lock.json | 13 +- discord-bot/package.json | 5 +- errors.md | 68 +++++ frontend/src/components/ConfirmDialog.js | 28 ++ frontend/src/components/Dashboard.js | 65 ++++- frontend/src/components/Login.js | 26 +- frontend/src/components/ServerSettings.js | 230 +++++++++++++++- 18 files changed, 629 insertions(+), 419 deletions(-) create mode 100644 backend/db.js create mode 100644 discord-bot/events/guildMemberAdd.js create mode 100644 discord-bot/events/guildMemberRemove.js create mode 100644 errors.md create mode 100644 frontend/src/components/ConfirmDialog.js diff --git a/backend/db.js b/backend/db.js new file mode 100644 index 0000000..233b5a7 --- /dev/null +++ b/backend/db.js @@ -0,0 +1,38 @@ +const fs = require('fs'); +const path = require('path'); +const CryptoJS = require('crypto-js'); + +const encryptionKey = process.env.ENCRYPTION_KEY; +const dbPath = path.join(__dirname, 'db.json'); + +const readDb = () => { + try { + const data = fs.readFileSync(dbPath, 'utf8'); + if (data) { + const bytes = CryptoJS.AES.decrypt(data, encryptionKey); + const decryptedData = bytes.toString(CryptoJS.enc.Utf8); + if (decryptedData) { + return JSON.parse(decryptedData); + } + } + return {}; + } catch (error) { + if (error.code === 'ENOENT') { + return {}; + } + console.error('Error reading or decrypting db.json:', error); + return {}; + } +}; + +const writeDb = (db) => { + try { + const data = JSON.stringify(db, null, 2); + const encryptedData = CryptoJS.AES.encrypt(data, encryptionKey).toString(); + fs.writeFileSync(dbPath, encryptedData, 'utf8'); + } catch (error) { + console.error('Error encrypting or writing to db.json:', error); + } +}; + +module.exports = { readDb, writeDb, dbPath }; diff --git a/backend/index.js b/backend/index.js index 505ca6d..196b477 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,4 +1,4 @@ -require('dotenv').config(); +require('dotenv').config({ path: __dirname + '/.env' }); const express = require('express'); const cors = require('cors'); @@ -52,56 +52,32 @@ app.get('/auth/discord/callback', async (req, res) => { const adminGuilds = guildsResponse.data.filter(guild => (guild.permissions & 0x8) === 0x8); const user = userResponse.data; - fs.readFile('db.json', 'utf8', (err, data) => { - if (err) { - console.error(err); - user.theme = 'light'; - } else { - const db = JSON.parse(data); - user.theme = db.users && db.users[user.id] ? db.users[user.id].theme : 'light'; - } - const guilds = adminGuilds; - res.redirect(`http://localhost:3000/dashboard?user=${encodeURIComponent(JSON.stringify(user))}&guilds=${encodeURIComponent(JSON.stringify(guilds))}`); - }); + const db = readDb(); + user.theme = db.users && db.users[user.id] ? db.users[user.id].theme : 'light'; + const guilds = adminGuilds; + res.redirect(`http://localhost:3000/dashboard?user=${encodeURIComponent(JSON.stringify(user))}&guilds=${encodeURIComponent(JSON.stringify(guilds))}`); } catch (error) { console.error('Error during Discord OAuth2 callback:', error); res.status(500).send('Internal Server Error'); } }); -const fs = require('fs'); +const { readDb, writeDb } = require('./db'); app.get('/api/servers/:guildId/settings', (req, res) => { const { guildId } = req.params; - fs.readFile('db.json', 'utf8', (err, data) => { - if (err) { - console.error(err); - return res.status(500).send('Internal Server Error'); - } - const db = JSON.parse(data); - const settings = db[guildId] || { pingCommand: false }; - res.json(settings); - }); + const db = readDb(); + const settings = db[guildId] || { pingCommand: false }; + res.json(settings); }); app.post('/api/servers/:guildId/settings', (req, res) => { const { guildId } = req.params; const { pingCommand } = req.body; - fs.readFile('db.json', 'utf8', (err, data) => { - if (err) { - console.error(err); - return res.status(500).send('Internal Server Error'); - } - const db = JSON.parse(data); - db[guildId] = { pingCommand }; - fs.writeFile('db.json', JSON.stringify(db, null, 2), (err) => { - if (err) { - console.error(err); - return res.status(500).send('Internal Server Error'); - } - res.json({ success: true }); - }); - }); + const db = readDb(); + db[guildId] = { pingCommand }; + writeDb(db); + res.json({ success: true }); }); app.get('/api/servers/:guildId/bot-status', (req, res) => { @@ -120,27 +96,77 @@ app.get('/api/client-id', (req, res) => { app.post('/api/user/theme', (req, res) => { const { userId, theme } = req.body; - fs.readFile('db.json', 'utf8', (err, data) => { - if (err) { - console.error(err); - return res.status(500).send('Internal Server Error'); - } - const db = JSON.parse(data); - if (!db.users) { - db.users = {}; - } - if (!db.users[userId]) { - db.users[userId] = {}; - } - db.users[userId].theme = theme; - fs.writeFile('db.json', JSON.stringify(db, null, 2), (err) => { - if (err) { - console.error(err); - return res.status(500).send('Internal Server Error'); - } + const db = readDb(); + if (!db.users) { + db.users = {}; + } + if (!db.users[userId]) { + db.users[userId] = {}; + } + db.users[userId].theme = theme; + writeDb(db); + res.json({ success: true }); +}); + +app.post('/api/servers/:guildId/leave', async (req, res) => { + const { guildId } = req.params; + try { + const guild = await bot.client.guilds.fetch(guildId); + if (guild) { + await guild.leave(); res.json({ success: true }); - }); - }); + } else { + res.status(404).json({ success: false, message: 'Bot is not in the specified server' }); + } + } catch (error) { + console.error('Error leaving server:', error); + res.status(500).json({ success: false, message: 'Internal Server Error' }); + } +}); + +app.get('/api/servers/:guildId/channels', async (req, res) => { + const { guildId } = req.params; + const guild = bot.client.guilds.cache.get(guildId); + if (!guild) { + return res.json([]); + } + try { + const channels = await guild.channels.fetch(); + const textChannels = channels.filter(channel => channel.type === 0).map(channel => ({ id: channel.id, name: channel.name })); + res.json(textChannels); + } catch (error) { + console.error('Error fetching channels:', error); + res.status(500).json({ success: false, message: 'Internal Server Error' }); + } +}); + +app.get('/api/servers/:guildId/welcome-leave-settings', (req, res) => { + const { guildId } = req.params; + const db = readDb(); + const settings = db[`${guildId}_welcome_leave`] || { + welcome: { + enabled: false, + channel: '', + message: 'Welcome to the server, {user}!', + customMessage: '', + }, + leave: { + enabled: false, + channel: '', + message: '{user} has left the server.', + customMessage: '', + }, + }; + res.json(settings); +}); + +app.post('/api/servers/:guildId/welcome-leave-settings', (req, res) => { + const { guildId } = req.params; + const newSettings = req.body; + const db = readDb(); + db[`${guildId}_welcome_leave`] = newSettings; + writeDb(db); + res.json({ success: true }); }); app.get('/', (req, res) => { diff --git a/backend/package-lock.json b/backend/package-lock.json index e2de9bd..cf1f9b5 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "axios": "^1.7.2", "cors": "^2.8.5", - "discord.js": "^14.15.3", + "crypto-js": "^4.2.0", "dotenv": "^16.4.5", "express": "^4.19.2" }, @@ -19,194 +19,6 @@ "nodemon": "^3.1.3" } }, - "node_modules/@discordjs/builders": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.11.3.tgz", - "integrity": "sha512-p3kf5eV49CJiRTfhtutUCeivSyQ/l2JlKodW1ZquRwwvlOWmG9+6jFShX6x8rUiYhnP6wKI96rgN/SXMy5e5aw==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/formatters": "^0.6.1", - "@discordjs/util": "^1.1.1", - "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "^0.38.16", - "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.4", - "tslib": "^2.6.3" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/collection": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", - "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.11.0" - } - }, - "node_modules/@discordjs/formatters": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", - "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", - "license": "Apache-2.0", - "dependencies": { - "discord-api-types": "^0.38.1" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", - "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^2.1.1", - "@discordjs/util": "^1.1.1", - "@sapphire/async-queue": "^1.5.3", - "@sapphire/snowflake": "^3.5.3", - "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "^0.38.16", - "magic-bytes.js": "^1.10.0", - "tslib": "^2.6.3", - "undici": "6.21.3" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", - "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/util": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", - "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/ws": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", - "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^2.1.0", - "@discordjs/rest": "^2.5.1", - "@discordjs/util": "^1.1.0", - "@sapphire/async-queue": "^1.5.2", - "@types/ws": "^8.5.10", - "@vladfrangu/async_event_emitter": "^2.2.4", - "discord-api-types": "^0.38.1", - "tslib": "^2.6.2", - "ws": "^8.17.0" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", - "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@sapphire/async-queue": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", - "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sapphire/shapeshift": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", - "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=v16" - } - }, - "node_modules/@sapphire/snowflake": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", - "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@types/node": { - "version": "24.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz", - "integrity": "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.13.0" - } - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@vladfrangu/async_event_emitter": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", - "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -456,6 +268,12 @@ "node": ">= 0.10" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -493,42 +311,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/discord-api-types": { - "version": "0.38.26", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.26.tgz", - "integrity": "sha512-xpmPviHjIJ6dFu1eNwNDIGQ3N6qmPUUYFVAx/YZ64h7ZgPkTcKjnciD8bZe8Vbeji7yS5uYljyciunpq0J5NSw==", - "license": "MIT", - "workspaces": [ - "scripts/actions/documentation" - ] - }, - "node_modules/discord.js": { - "version": "14.22.1", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.22.1.tgz", - "integrity": "sha512-3k+Kisd/v570Jr68A1kNs7qVhNehDwDJAPe4DZ2Syt+/zobf9zEcuYFvsfIaAOgCa0BiHMfOOKQY4eYINl0z7w==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/builders": "^1.11.2", - "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.6.1", - "@discordjs/rest": "^2.6.0", - "@discordjs/util": "^1.1.1", - "@discordjs/ws": "^1.2.3", - "@sapphire/snowflake": "3.5.3", - "discord-api-types": "^0.38.16", - "fast-deep-equal": "3.1.3", - "lodash.snakecase": "4.1.1", - "magic-bytes.js": "^1.10.0", - "tslib": "^2.6.3", - "undici": "6.21.3" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -676,12 +458,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -998,24 +774,6 @@ "node": ">=0.12.0" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", - "license": "MIT" - }, - "node_modules/magic-bytes.js": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", - "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", - "license": "MIT" - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1554,18 +1312,6 @@ "nodetouch": "bin/nodetouch.js" } }, - "node_modules/ts-mixer": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", - "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1586,21 +1332,6 @@ "dev": true, "license": "MIT" }, - "node_modules/undici": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", - "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/undici-types": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", - "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==", - "license": "MIT" - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1627,27 +1358,6 @@ "engines": { "node": ">= 0.8" } - }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } } } } diff --git a/backend/package.json b/backend/package.json index c726952..846d4a6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,10 +14,11 @@ "dependencies": { "axios": "^1.7.2", "cors": "^2.8.5", + "crypto-js": "^4.2.0", "dotenv": "^16.4.5", "express": "^4.19.2" }, "devDependencies": { "nodemon": "^3.1.3" } -} \ No newline at end of file +} diff --git a/checklist.md b/checklist.md index cd32e76..5f02e4c 100644 --- a/checklist.md +++ b/checklist.md @@ -8,6 +8,8 @@ - [x] Set up Discord OAuth2 - [x] Create API endpoint to get user's servers - [x] Store user theme preference on the backend +- [x] Create API endpoint to make bot leave a server +- [x] Encrypt user information in `db.json`. ## Frontend - [x] Create login page @@ -43,6 +45,13 @@ - Acceptance criteria: On the server-specific settings page, the "Invite Bot" button (or the "Bot is already in this server" text) should be vertically and horizontally aligned with the main server name title for a cleaner look. - [x] Fix incorrect invite link on dashboard cards - Acceptance criteria: The invite link generated for the dashboard cards should correctly redirect to the Discord OAuth2 authorization page with the proper client ID, permissions, and scope, matching the functionality of the invite button in the server settings panel. +- [x] ~~by default hide or gray out bot options in each server dashboard panel if the bot is not in the server. Only allow to edit the features if the bot is in the discord server.~~ (User changed their mind) +- [x] Allow dashboard cards to be clickable even if the bot is not in the server. Inside the server settings, all commands and categories should be greyed out and not touchable. Only allow the invite button to be clicked within. +- [x] Add a button to the server cards that allows the user to make the bot leave the server. The button should only be visible if the bot is in the server. +- [x] In the server settings, replace the text "The bot is already in this server" with a "Leave" button. +- [x] Add a confirmation dialog to the "Leave" buttons on both the dashboard cards and the server settings page. +- [x] Redesign the login page to be more bubbly, centered, and eye-catching, with bigger text and responsive design. +- [x] Make server settings panels collapsible for a cleaner mobile UI. ## Discord Bot - [x] Create a basic Discord bot @@ -52,3 +61,21 @@ - [x] Implement command handler - [x] Implement event handler - [x] Set bot status on ready event +- [x] Automatically register slash commands on server join. + +## Features +- [x] **Welcome/Leave Messages** + - [x] Add "Welcome/Leave" section to server settings. + - [x] **Welcome Messages:** + - [x] Add toggle to enable/disable welcome messages. + - [x] Add dropdown to select a channel for welcome messages. + - [x] Add 3 default welcome message options. + - [x] Add a custom welcome message option with a text input and apply button. + - [x] **Leave Messages:** + - [x] Add toggle to enable/disable leave messages. + - [x] Add dropdown to select a channel for leave messages. + - [x] Add 3 default leave message options. + - [x] Add a custom leave message option with a text input and apply button. + - [x] **Bot Integration:** + - [x] Connect frontend settings to the backend. + - [x] Implement bot logic to send welcome/leave messages based on server settings. diff --git a/discord-bot/commands/ping.js b/discord-bot/commands/ping.js index 91d98bf..b1cfe34 100644 --- a/discord-bot/commands/ping.js +++ b/discord-bot/commands/ping.js @@ -1,23 +1,16 @@ -const fs = require('fs'); -const path = require('path'); +const { readDb } = require('../../backend/db.js'); module.exports = { name: 'ping', description: 'Replies with Pong!', execute(interaction) { - fs.readFile(path.join(__dirname, '../../backend/db.json'), 'utf8', (err, data) => { - if (err) { - console.error(err); - return interaction.reply('An error occurred.'); - } - const db = JSON.parse(data); - const settings = db[interaction.guildId] || { pingCommand: false }; + const db = readDb(); + const settings = db[interaction.guildId] || { pingCommand: false }; - if (settings.pingCommand) { - interaction.reply('Pong!'); - } else { - interaction.reply('The ping command is disabled on this server.'); - } - }); + if (settings.pingCommand) { + interaction.reply('Pong!'); + } else { + interaction.reply('The ping command is disabled on this server.'); + } }, }; diff --git a/discord-bot/deploy-commands.js b/discord-bot/deploy-commands.js index 07d6ebc..d571e2b 100644 --- a/discord-bot/deploy-commands.js +++ b/discord-bot/deploy-commands.js @@ -1,25 +1,26 @@ require('dotenv').config({ path: '../backend/.env' }); -const { REST } = require('@discordjs/rest'); -const { Routes } = require('discord-api-types/v9'); +const { REST, Routes } = require('discord.js'); const commands = [{ name: 'ping', description: 'Replies with Pong!', }]; -const rest = new REST({ version: '9' }).setToken(process.env.DISCORD_BOT_TOKEN); +const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_BOT_TOKEN); -(async () => { +const deployCommands = async (guildId) => { try { - console.log('Started refreshing application (/) commands.'); + console.log(`Started refreshing application (/) commands for guild ${guildId}.`); await rest.put( - Routes.applicationGuildCommands(process.env.DISCORD_CLIENT_ID, process.env.GUILD_ID), + Routes.applicationGuildCommands(process.env.DISCORD_CLIENT_ID, guildId), { body: commands }, ); - console.log('Successfully reloaded application (/) commands.'); + console.log(`Successfully reloaded application (/) commands for guild ${guildId}.`); } catch (error) { console.error(error); } -})(); +}; + +module.exports = deployCommands; diff --git a/discord-bot/events/guildMemberAdd.js b/discord-bot/events/guildMemberAdd.js new file mode 100644 index 0000000..d869744 --- /dev/null +++ b/discord-bot/events/guildMemberAdd.js @@ -0,0 +1,26 @@ +const { Events } = require('discord.js'); +const { readDb } = require('../../backend/db.js'); + +module.exports = { + name: Events.GuildMemberAdd, + async execute(member) { + try { + const db = readDb(); + const settings = db[`${member.guild.id}_welcome_leave`]; + + if (settings && settings.welcome && settings.welcome.enabled && settings.welcome.channel) { + const channel = member.guild.channels.cache.get(settings.welcome.channel); + if (channel) { + try { + const message = settings.welcome.message.replace('{user}', member.user.toString()); + await channel.send(message); + } catch (error) { + console.error(`Could not send welcome message to channel ${settings.welcome.channel} in guild ${member.guild.id}:`, error); + } + } + } + } catch (error) { + console.error(`Error in guildMemberAdd event for guild ${member.guild.id}:`, error); + } + }, +}; \ No newline at end of file diff --git a/discord-bot/events/guildMemberRemove.js b/discord-bot/events/guildMemberRemove.js new file mode 100644 index 0000000..fde56ec --- /dev/null +++ b/discord-bot/events/guildMemberRemove.js @@ -0,0 +1,26 @@ +const { Events } = require('discord.js'); +const { readDb } = require('../../backend/db.js'); + +module.exports = { + name: Events.GuildMemberRemove, + async execute(member) { + try { + const db = readDb(); + const settings = db[`${member.guild.id}_welcome_leave`]; + + if (settings && settings.leave && settings.leave.enabled && settings.leave.channel) { + const channel = member.guild.channels.cache.get(settings.leave.channel); + if (channel) { + try { + const message = settings.leave.message.replace('{user}', member.user.tag); + await channel.send(message); + } catch (error) { + console.error(`Could not send leave message to channel ${settings.leave.channel} in guild ${member.guild.id}:`, error); + } + } + } + } catch (error) { + console.error(`Error in guildMemberRemove event for guild ${member.guild.id}:`, error); + } + }, +}; \ No newline at end of file diff --git a/discord-bot/events/ready.js b/discord-bot/events/ready.js index eff4247..70a5842 100644 --- a/discord-bot/events/ready.js +++ b/discord-bot/events/ready.js @@ -1,7 +1,7 @@ const { ActivityType } = require('discord.js'); module.exports = { - name: 'ready', + name: 'clientReady', once: true, execute(client) { console.log('ECS - Full Stack Bot Online!'); diff --git a/discord-bot/index.js b/discord-bot/index.js index 599c83f..e51ca7a 100644 --- a/discord-bot/index.js +++ b/discord-bot/index.js @@ -1,8 +1,9 @@ const { Client, GatewayIntentBits, Collection } = require('discord.js'); const fs = require('fs'); const path = require('path'); +const deployCommands = require('./deploy-commands'); -const client = new Client({ intents: [GatewayIntentBits.Guilds] }); +const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers] }); client.commands = new Collection(); @@ -27,6 +28,10 @@ client.on('interactionCreate', async interaction => { } }); +client.on('guildCreate', guild => { + deployCommands(guild.id); +}); + const login = () => { client.login(process.env.DISCORD_BOT_TOKEN); } diff --git a/discord-bot/package-lock.json b/discord-bot/package-lock.json index 96f2ccc..32afbdf 100644 --- a/discord-bot/package-lock.json +++ b/discord-bot/package-lock.json @@ -9,9 +9,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@discordjs/rest": "^2.3.0", - "discord-api-types": "^0.37.92", - "discord.js": "^14.15.3", + "crypto-js": "^4.2.0", + "discord.js": "^14.22.1", "dotenv": "^16.4.5" } }, @@ -218,10 +217,10 @@ "npm": ">=7.0.0" } }, - "node_modules/discord-api-types": { - "version": "0.37.120", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.120.tgz", - "integrity": "sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw==", + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", "license": "MIT" }, "node_modules/discord.js": { diff --git a/discord-bot/package.json b/discord-bot/package.json index 1efabcc..b290fec 100644 --- a/discord-bot/package.json +++ b/discord-bot/package.json @@ -10,9 +10,8 @@ "author": "", "license": "ISC", "dependencies": { - "discord.js": "^14.15.3", - "@discordjs/rest": "^2.3.0", - "discord-api-types": "^0.37.92", + "crypto-js": "^4.2.0", + "discord.js": "^14.22.1", "dotenv": "^16.4.5" } } diff --git a/errors.md b/errors.md new file mode 100644 index 0000000..8a58e9d --- /dev/null +++ b/errors.md @@ -0,0 +1,68 @@ +# Error Log + +## ENOENT: no such file or directory, open 'F:\Projects\Github\ECS-Discordweb\discord-bot\events\...ackenddb.json' + +**Context:** This error occurs in the `guildMemberAdd` and `guildMemberRemove` event handlers in the Discord bot when trying to read the `db.json` file. + +**Initial Fix Attempts:** + +1. **`path.join` with relative path:** Initially, the path was constructed using `path.join(__dirname, '..\..\backend\db.json')`. This was incorrect as it didn't go up enough directories. +2. **`path.join` with corrected relative path:** The path was corrected to `path.join(__dirname, '..\..\..\backend\db.json')`. This seemed logically correct, but still resulted in the same error, with a strange `......` in the error path. +3. **`path.resolve`:** The path construction was changed to use `path.resolve(__dirname, '..\..\..\backend\db.json')` to ensure an absolute path was resolved. This also failed with the same error. + +**Root Cause Analysis:** + +The exact cause of the path resolution failure is unclear, but it seems to be related to how `__dirname` and relative paths are handled when the bot module is required by the backend server. The inconsistent working directory might be the source of the problem. + +**Solution:** + +To provide a more robust and centralized solution, the database path will be managed within the `backend/db.js` file. + +1. The `dbPath` will be exported from `backend/db.js`. +2. The `guildMemberAdd.js` and `guildMemberRemove.js` event handlers will import the `dbPath` directly from `backend/db.js`, eliminating the need for path resolution in the event handlers themselves. + +## SyntaxError: Unexpected token 'U', "U2FsdGVkX1"... is not valid JSON + +**Context:** This error occurs in the `guildMemberAdd` and `guildMemberRemove` event handlers when trying to parse the content of `db.json`. + +**Root Cause Analysis:** + +The `db.json` file is encrypted, and the event handlers were reading the file content directly and trying to parse it as JSON. The encrypted string starts with "U2FsdGVkX1", which is not a valid JSON token, causing the `JSON.parse` to fail. + +**Solution:** + +The `backend/db.js` module already provides a `readDb` function that handles reading the file and decrypting its content. The event handlers must be updated to use this function instead of reading the file directly. This ensures that the encrypted data is correctly decrypted before being parsed as JSON. + +## Outdated Discord.js Practices + +**Context:** The `discord-bot` is using a slightly outdated version of `discord.js` (`^14.15.3` instead of the latest `^14.22.1`). Additionally, the `package.json` includes `@discordjs/rest` and `discord-api-types` as explicit dependencies, which are now bundled with `discord.js` v14 and can cause conflicts. + +**Solution:** + +1. Update the `discord.js` dependency to the latest stable version (`^14.22.1`). +2. Remove the `@discordjs/rest` and `discord-api-types` dependencies from `package.json`. +3. Run `npm install` to apply the changes and ensure all packages are up-to-date. + +## Error: Cannot find module 'discord-api-types/v9' + +**Context:** After removing the `discord-api-types` package, the application fails to start due to a missing module error in `discord-bot/deploy-commands.js`. + +**Root Cause Analysis:** + +The `deploy-commands.js` file was still referencing `discord-api-types/v9` for API-related enums or types. Since this package is no longer an explicit dependency (as it is bundled with `discord.js` v14), the import fails. + +**Solution:** + +The `deploy-commands.js` file needs to be updated to import the necessary modules, such as `Routes`, directly from `discord.js` instead of the now-removed `discord-api-types` package. + +## DiscordAPIError[10004]: Unknown Guild + +**Context:** The backend throws an "Unknown Guild" error when the frontend tries to fetch the channels for a server that the bot is not a member of. + +**Root Cause Analysis:** + +The `/api/servers/:guildId/channels` endpoint was attempting to fetch guild information regardless of whether the bot was actually in that guild. This caused a Discord API error when the guild was not found. + +**Solution:** + +The endpoint will be updated to first check if the guild exists in the bot's cache using `bot.client.guilds.cache.get(guildId)`. If the guild is not found, it will return an empty array, preventing the API error. diff --git a/frontend/src/components/ConfirmDialog.js b/frontend/src/components/ConfirmDialog.js new file mode 100644 index 0000000..96ac504 --- /dev/null +++ b/frontend/src/components/ConfirmDialog.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button } from '@mui/material'; + +const ConfirmDialog = ({ open, onClose, onConfirm, title, message }) => { + return ( + + {title} + + + {message} + + + + + + + + ); +}; + +export default ConfirmDialog; diff --git a/frontend/src/components/Dashboard.js b/frontend/src/components/Dashboard.js index b30257a..1623762 100644 --- a/frontend/src/components/Dashboard.js +++ b/frontend/src/components/Dashboard.js @@ -4,14 +4,19 @@ import { Grid, Card, CardContent, Typography, Box, CardMedia, IconButton, Snackb import { UserContext } from '../contexts/UserContext'; import UserSettings from './UserSettings'; import PersonAddIcon from '@mui/icons-material/PersonAdd'; +import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline'; import axios from 'axios'; +import ConfirmDialog from './ConfirmDialog'; + const Dashboard = () => { const { user, setUser } = useContext(UserContext); const [guilds, setGuilds] = useState([]); const [botStatus, setBotStatus] = useState({}); const [snackbarOpen, setSnackbarOpen] = useState(false); const [snackbarMessage, setSnackbarMessage] = useState(''); + const [dialogOpen, setDialogOpen] = useState(false); + const [selectedGuild, setSelectedGuild] = useState(null); const navigate = useNavigate(); useEffect(() => { @@ -77,18 +82,34 @@ const Dashboard = () => { const handleInviteBot = async (e, guild) => { e.stopPropagation(); - if (botStatus[guild.id]) { - setSnackbarMessage('Bot already added to this server.'); - setSnackbarOpen(true); - return; - } - const clientId = '1423377662055026840'; // Hardcoded client ID from user request const permissions = 8; // Administrator const inviteUrl = `https://discord.com/api/oauth2/authorize?client_id=${clientId}&permissions=${permissions}&scope=bot%20applications.commands&guild_id=${guild.id}&disable_guild_select=true`; window.open(inviteUrl, '_blank', 'noopener,noreferrer'); }; + const handleLeaveBot = (e, guild) => { + e.stopPropagation(); + setSelectedGuild(guild); + setDialogOpen(true); + }; + + const handleConfirmLeave = async () => { + if (!selectedGuild) return; + try { + await axios.post(`http://localhost:3002/api/servers/${selectedGuild.id}/leave`); + setBotStatus(prevStatus => ({ ...prevStatus, [selectedGuild.id]: false })); + setSnackbarMessage('Bot has left the server.'); + setSnackbarOpen(true); + } catch (error) { + console.error('Error leaving server:', error); + setSnackbarMessage('Failed to make the bot leave the server.'); + setSnackbarOpen(true); + } + setDialogOpen(false); + setSelectedGuild(null); + }; + const handleSnackbarClose = (event, reason) => { if (reason === 'clickaway') { return; @@ -158,14 +179,23 @@ const Dashboard = () => { {guild.name} - handleInviteBot(e, guild)} - disabled={botStatus[guild.id]} - > - - + {botStatus[guild.id] ? ( + handleLeaveBot(e, guild)} + > + + + ) : ( + handleInviteBot(e, guild)} + > + + + )} @@ -177,6 +207,13 @@ const Dashboard = () => { {snackbarMessage} + setDialogOpen(false)} + onConfirm={handleConfirmLeave} + title="Confirm Leave" + message={`Are you sure you want the bot to leave ${selectedGuild?.name}?`} + /> ); }; diff --git a/frontend/src/components/Login.js b/frontend/src/components/Login.js index 7a3dc4c..da07ed5 100644 --- a/frontend/src/components/Login.js +++ b/frontend/src/components/Login.js @@ -1,5 +1,6 @@ import React, { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; +import { Button, Container, Paper, Typography, Box } from '@mui/material'; const Login = () => { const navigate = useNavigate(); @@ -16,10 +17,27 @@ const Login = () => { }; return ( -
-

Login

- -
+ + + + Welcome! + + + Login with your Discord account to continue + + + + + + ); }; diff --git a/frontend/src/components/ServerSettings.js b/frontend/src/components/ServerSettings.js index 8e67206..e939317 100644 --- a/frontend/src/components/ServerSettings.js +++ b/frontend/src/components/ServerSettings.js @@ -1,9 +1,11 @@ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate, useLocation } from 'react-router-dom'; import axios from 'axios'; -import { Button, Typography, Card, CardContent, Box, IconButton } from '@mui/material'; +import { Button, Typography, Box, IconButton, Switch, Select, MenuItem, FormControl, FormControlLabel, Radio, RadioGroup, TextField, Accordion, AccordionSummary, AccordionDetails } from '@mui/material'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import UserSettings from './UserSettings'; +import ConfirmDialog from './ConfirmDialog'; const ServerSettings = () => { const { guildId } = useParams(); @@ -13,6 +15,25 @@ const ServerSettings = () => { const [isBotInServer, setIsBotInServer] = useState(false); const [clientId, setClientId] = useState(null); const [server, setServer] = useState(null); + const [dialogOpen, setDialogOpen] = useState(false); + const [channels, setChannels] = useState([]); + const [welcomeLeaveSettings, setWelcomeLeaveSettings] = useState({ + welcome: { + enabled: false, + channel: '', + message: 'Welcome to the server, {user}!', + customMessage: '', + }, + leave: { + enabled: false, + channel: '', + message: '{user} has left the server.', + customMessage: '', + }, + }); + + const defaultWelcomeMessages = ["Welcome to the server, {user}!", "Hey {user}, welcome!", "{user} has joined the party!"]; + const defaultLeaveMessages = ["{user} has left the server.", "Goodbye, {user}.", "We'll miss you, {user}."]; useEffect(() => { if (location.state && location.state.guild) { @@ -43,9 +64,78 @@ const ServerSettings = () => { .then(response => { setClientId(response.data.clientId); }); + + // Fetch channels + axios.get(`http://localhost:3002/api/servers/${guildId}/channels`) + .then(response => { + setChannels(response.data); + }); + + // Fetch welcome/leave settings + axios.get(`http://localhost:3002/api/servers/${guildId}/welcome-leave-settings`) + .then(response => { + if (response.data) { + setWelcomeLeaveSettings(response.data); + } + }); }, [guildId, location.state]); + const handleSettingUpdate = (newSettings) => { + axios.post(`http://localhost:3002/api/servers/${guildId}/welcome-leave-settings`, newSettings) + .then(response => { + if (response.data.success) { + setWelcomeLeaveSettings(newSettings); + } + }); + } + + const handleToggleChange = (type) => (event) => { + const newSettings = { ...welcomeLeaveSettings }; + newSettings[type].enabled = event.target.checked; + handleSettingUpdate(newSettings); + }; + + const handleChannelChange = (type) => (event) => { + const newSettings = { ...welcomeLeaveSettings }; + newSettings[type].channel = event.target.value; + handleSettingUpdate(newSettings); + }; + + const handleMessageOptionChange = (type) => (event) => { + const newSettings = { ...welcomeLeaveSettings }; + if (event.target.value !== 'custom') { + newSettings[type].message = event.target.value; + handleSettingUpdate(newSettings); + } else { + const tempSettings = { ...welcomeLeaveSettings }; + // Set message to custom message to get the radio button to select custom + tempSettings[type].message = tempSettings[type].customMessage; + setWelcomeLeaveSettings(tempSettings); + } + }; + + const handleCustomMessageChange = (type) => (event) => { + const newSettings = { ...welcomeLeaveSettings }; + newSettings[type].customMessage = event.target.value; + setWelcomeLeaveSettings(newSettings); + }; + + const handleApplyCustomMessage = (type) => () => { + const newSettings = { ...welcomeLeaveSettings }; + newSettings[type].message = newSettings[type].customMessage; + handleSettingUpdate(newSettings); + }; + + const getMessageValue = (type) => { + const currentMessage = welcomeLeaveSettings[type].message; + const defaultMessages = type === 'welcome' ? defaultWelcomeMessages : defaultLeaveMessages; + if (defaultMessages.includes(currentMessage)) { + return currentMessage; + } + return 'custom'; + } + const togglePingCommand = () => { const newSettings = { ...settings, pingCommand: !settings.pingCommand }; axios.post(`http://localhost:3002/api/servers/${guildId}/settings`, newSettings) @@ -63,6 +153,20 @@ const ServerSettings = () => { window.open(url, '_blank'); }; + const handleLeaveBot = () => { + setDialogOpen(true); + }; + + const handleConfirmLeave = async () => { + try { + await axios.post(`http://localhost:3002/api/servers/${guildId}/leave`); + setIsBotInServer(false); + } catch (error) { + console.error('Error leaving server:', error); + } + setDialogOpen(false); + }; + const handleBack = () => { navigate('/dashboard'); } @@ -78,7 +182,9 @@ const ServerSettings = () => { {server ? `Server Settings for ${server.name}` : 'Loading...'} {isBotInServer ? ( - The bot is already in this server. + ) : ( - - - - + + + + }> + Welcome/Leave + + + {!isBotInServer && Invite the bot to enable this feature.} + + Welcome Messages + } + label="Enable Welcome Messages" + /> + + + + + + {defaultWelcomeMessages.map(msg => ( + } label={msg} /> + ))} + } label="Custom" /> + + + + + + + + + Leave Messages + } + label="Enable Leave Messages" + /> + + + + + + {defaultLeaveMessages.map(msg => ( + } label={msg} /> + ))} + } label="Custom" /> + + + + + + + + + + + }> Admin Commands + + Coming soon... - - + + + setDialogOpen(false)} + onConfirm={handleConfirmLeave} + title="Confirm Leave" + message={`Are you sure you want the bot to leave ${server?.name}?`} + /> ); };