diff --git a/.gitignore b/.gitignore index 15e369f..7209125 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,19 @@ node_modules +# Local database, runtime data, and transcripts +Data/*.db +Data/*.db-shm +Data/*.db-wal +Data/Transcripts/* +Data/dashboard-groups.json +Data/dashboard-users.json +Data/transcript-settings.json + +# Local log files +Logs/Output/* +Logs/Errors/* +Logs/Warns/* + # Dashboard UI source at repo root only (do not ignore public/dashboard/) /dashboard/ diff --git a/API/server.js b/API/server.js index 3c87429..49205e1 100644 --- a/API/server.js +++ b/API/server.js @@ -242,7 +242,7 @@ class APIServer { requirePermission(permission) { return (req, res, next) => { if (req.dashboardService) return next(); - if (req.dashboardIsOwner || req.dashboardImpersonatingGroup) return next(); + if (req.dashboardIsOwner && !req.dashboardImpersonatingGroup) return next(); if (hasPermission(req.dashboardPermissions, permission)) return next(); return forbid(res); }; @@ -251,7 +251,7 @@ class APIServer { requireConfigAccess(mode = 'view') { return (req, res, next) => { if (req.dashboardService) return next(); - if (req.dashboardIsOwner || req.dashboardImpersonatingGroup) return next(); + if (req.dashboardIsOwner && !req.dashboardImpersonatingGroup) return next(); const file = req.params?.file || req.body?.filename; if (!file) return next(); if (!canAccessConfig(req.dashboardPermissions, file, mode)) { @@ -637,6 +637,68 @@ h1{font-size:1.25rem}a{color:#a78bfa}
} }); + app.post('/system/tickets/create', p('tickets'), async (req, res) => { + try { + const { department, subject, reason, userId } = req.body; + if (!this.client?.user) { + return res.status(503).json({ success: false, error: 'Bot is not connected to Discord.' }); + } + + const guild = this.client.guilds.cache.first(); + if (!guild) { + return res.status(404).json({ success: false, error: 'No connected Discord server found.' }); + } + + const deptKey = department || supportbot.Ticket?.DepartmentSystem?.DefaultDepartment || 'general'; + const deptConfig = supportbot.Ticket?.DepartmentSystem?.Departments?.[deptKey]; + if (deptConfig && deptConfig.Enabled === false) { + return res.status(400).json({ success: false, error: `Ticket department '${deptKey}' is currently disabled.` }); + } + + const targetUserId = userId || req.dashboardUser?.id || guild.ownerId; + let targetUser = await this.client.users.fetch(targetUserId).catch(() => null); + if (!targetUser) { + targetUser = this.client.user; + } + + const openCmd = this.client.commands.get(cmdconfig.OpenTicket?.Command || "new"); + if (!openCmd) { + return res.status(500).json({ success: false, error: 'Open ticket command not found in bot.' }); + } + + const fakeInteraction = { + guild, + user: targetUser, + member: await guild.members.fetch(targetUser.id).catch(() => null), + channel: guild.channels.cache.first(), + department: deptKey, + reason: subject || reason || "Opened via Web Dashboard", + priority: deptConfig?.DefaultPriority || supportbot.Ticket?.PrioritySystem?.DefaultPriority || "medium", + isCommand: () => false, + isChatInputCommand: () => false, + isButton: () => false, + isStringSelectMenu: () => false, + isModalSubmit: () => false, + deferred: false, + replied: false, + deferReply: async () => {}, + reply: async (payload) => { fakeInteraction.repliedPayload = payload; }, + followUp: async (payload) => { fakeInteraction.followUpPayload = payload; }, + editReply: async (payload) => { fakeInteraction.editReplyPayload = payload; } + }; + + await openCmd.run(fakeInteraction); + + res.json({ + success: true, + message: `Ticket successfully created for department: ${deptConfig?.Name || deptKey}!`, + }); + } catch (err) { + console.error('[API] Error creating ticket:', err); + res.status(500).json({ success: false, error: err.message || 'Failed to create ticket' }); + } + }); + app.get('/system/tickets/:ticketId/messages', p('tickets'), async (req, res) => { const { ticketId } = req.params; try { @@ -1051,7 +1113,7 @@ h1{font-size:1.25rem}a{color:#a78bfa} res.json({ success: true, data: { - users: listUsers(oauth), + users: listUsers(oauth, this.client), roles: ROLES, configFiles: CONFIG_FILES, }, @@ -1320,6 +1382,9 @@ h1{font-size:1.25rem}a{color:#a78bfa} }); app.put('/configs/raw', cv('edit'), async (req, res) => { + if (!hasPermission(req.dashboardUser?.permissions, 'configs.raw_yaml')) { + return res.status(403).json({ success: false, error: 'Forbidden: Raw YAML editing requires the Raw YAML Editor permission.' }); + } const validFiles = ['supportbot', 'ticket-panel', 'commands', 'messages', 'supportbot-ai', 'api']; const file = req.body.filename; let content = req.body.content; diff --git a/Commands/Tickets/close.js b/Commands/Tickets/close.js index 5eeaf48..3f3299d 100644 --- a/Commands/Tickets/close.js +++ b/Commands/Tickets/close.js @@ -33,9 +33,10 @@ function buildContainer(title, lines = []) { container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true)); - if (lines.length > 0) { + const textContent = lines.filter(Boolean).join("\n").trim(); + if (textContent.length > 0) { container.addTextDisplayComponents( - new Discord.TextDisplayBuilder().setContent(lines.join("\n")), + new Discord.TextDisplayBuilder().setContent(textContent), ); } diff --git a/Commands/Tickets/ticket.js b/Commands/Tickets/ticket.js index 2256e30..69ae6b3 100644 --- a/Commands/Tickets/ticket.js +++ b/Commands/Tickets/ticket.js @@ -28,22 +28,7 @@ const Command = require("../../Structures/Command.js"); const TicketNumberID = require("../../Structures/TicketID.js"); async function isBlacklisted(userId) { - let blacklistData; - try { - blacklistData = JSON.parse( - fs.readFileSync("./Data/BlacklistedUsers.json", "utf8"), - ); - } catch (error) { - console.error("Error reading BlacklistedUsers.json:", error); - return false; - } - - if (!blacklistData || !Array.isArray(blacklistData.blacklistedUsers)) { - console.error("blacklistedUsers is not defined or is not an array"); - return false; - } - - return blacklistData.blacklistedUsers.includes(userId); + return typeof db.isUserBlacklisted === "function" ? db.isUserBlacklisted(userId) : false; } async function getClockedInUsers(guild) { diff --git a/Commands/Tickets/ticketpanel.js b/Commands/Tickets/ticketpanel.js index 0862d12..cb82e7e 100644 --- a/Commands/Tickets/ticketpanel.js +++ b/Commands/Tickets/ticketpanel.js @@ -116,6 +116,7 @@ module.exports = new Command({ } const departmentOptions = Object.entries(departments) + .filter(([_, dept]) => dept && dept.Enabled !== false) .slice(0, 25) .map(([key, dept]) => ({ label: dept.Name || key, diff --git a/Commands/Utils/mod.js b/Commands/Utils/mod.js index c4e0692..545049f 100644 --- a/Commands/Utils/mod.js +++ b/Commands/Utils/mod.js @@ -1,6 +1,7 @@ const fs = require("fs"); const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, MessageFlags } = require("discord.js"); const yaml = require("js-yaml"); +const db = require("../../Structures/Database.js"); const supportbot = require("../../Structures/ConfigStore").supportbot; const cmdconfig = require("../../Structures/ConfigStore").commands; const msgconfig = require("../../Structures/ConfigStore").messages; @@ -18,28 +19,28 @@ function chunkArray(arr, size) { module.exports = new Command({ name: cmdconfig.Mod.Command, - description: cmdconfig.Mod.Command, + description: cmdconfig.Mod.Description || "Moderation commands", type: ApplicationCommandType.ChatInput, options: [ { name: cmdconfig.Mod.TicketBlacklist.Command, - description: cmdconfig.Mod.TicketBlacklist.Description, + description: cmdconfig.Mod.TicketBlacklist.Description || "Ticket Blacklist Commands", type: ApplicationCommandOptionType.SubcommandGroup, options: [ { name: cmdconfig.Mod.TicketBlacklist.Add.Command, - description: msgconfig.Mod.TicketBlacklist.Add.Description || "Add a user to the ticket blacklist", + description: msgconfig.Mod.TicketBlacklist.Add.Description || "Add a user to the blacklist", type: ApplicationCommandOptionType.Subcommand, options: [ { name: 'user', - description: msgconfig.Mod.TicketBlacklist.Add.UserDescription || "User to blacklist", + description: 'The user to blacklist', type: ApplicationCommandOptionType.User, required: true }, { - name: 'reason', - description: msgconfig.Mod.TicketBlacklist.Add.ReasonDescription || "Reason for blacklisting", + name: 'reason', + description: 'Reason for blacklisting', type: ApplicationCommandOptionType.String, required: false } @@ -47,18 +48,18 @@ module.exports = new Command({ }, { name: cmdconfig.Mod.TicketBlacklist.Remove.Command, - description: msgconfig.Mod.TicketBlacklist.Remove.Description || "Remove a user from the ticket blacklist", + description: cmdconfig.Mod.TicketBlacklist.Remove.Description || "Remove a user from the blacklist", type: ApplicationCommandOptionType.Subcommand, options: [ { name: 'user', - description: msgconfig.Mod.TicketBlacklist.Remove.UserDescription || "User to remove from blacklist", + description: 'The user to remove from the blacklist', type: ApplicationCommandOptionType.User, required: true }, { name: 'reason', - description: msgconfig.Mod.TicketBlacklist.Remove.ReasonDescription || "Reason for removing from blacklist", + description: 'Reason for unblacklisting', type: ApplicationCommandOptionType.String, required: false } @@ -67,60 +68,32 @@ module.exports = new Command({ { name: "view", description: msgconfig.Mod.TicketBlacklist.View.Description || "View all blacklisted users", - type: ApplicationCommandOptionType.Subcommand, + type: ApplicationCommandOptionType.Subcommand } ] } ], - permissions: cmdconfig.Mod.Permission, - async run(interaction) { - const { getRole } = interaction.client; - - let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); - let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); - let Moderator = await getRole(supportbot.Roles.StaffMember.Moderator, interaction.guild); - - if (!SupportStaff || !Admin || !Moderator) { - const missingRolesEmbed = new EmbedBuilder() - .setDescription(msgconfig.Error.InvalidChannel || "Required roles are missing!") - .setColor(supportbot.Embed.Colours.Warn); - return interaction.reply({ embeds: [missingRolesEmbed] }); - } - - const NoPerms = new EmbedBuilder() - .setDescription(msgconfig.Error.IncorrectPerms || "You do not have the correct permissions!") - .setColor(supportbot.Embed.Colours.Warn); - - if (!interaction.member.roles.cache.has(Admin.id) && !interaction.member.roles.cache.has(Moderator.id) && (!supportbot.Mod.AllowSupportStaff || !interaction.member.roles.cache.has(SupportStaff.id))) { - return interaction.reply({ embeds: [NoPerms] }); - } - - const subcommand = interaction.options.getSubcommand(false); + run: async ({ interaction }) => { + const subcommandGroup = interaction.options.getSubcommandGroup(); + const subcommand = interaction.options.getSubcommand(); const user = interaction.options.getUser('user'); const reason = interaction.options.getString('reason') || "No reason provided"; try { - let blacklistedUsers; - try { - blacklistedUsers = JSON.parse(fs.readFileSync("./Data/BlacklistedUsers.json", "utf8")).blacklistedUsers; - if (!Array.isArray(blacklistedUsers)) { - blacklistedUsers = []; - } - } catch (error) { - blacklistedUsers = []; - } + const blacklistedUsers = typeof db.getBlacklistedUsers === "function" ? db.getBlacklistedUsers() : []; if (subcommand === cmdconfig.Mod.TicketBlacklist.Add.Command) { - if (blacklistedUsers.includes(user.id)) { + if (db.isUserBlacklisted && db.isUserBlacklisted(user.id)) { const alreadyBlacklistedEmbed = new EmbedBuilder() .setDescription(msgconfig.Mod.TicketBlacklist.Add.AlreadyBlacklisted.replace("{userTag}", user.tag) || ":x: User is already blacklisted.") .setColor(supportbot.Embed.Colours.Warn); return interaction.reply({ embeds: [alreadyBlacklistedEmbed], flags: MessageFlags.Ephemeral }); } - blacklistedUsers.push(user.id); - fs.writeFileSync("./Data/BlacklistedUsers.json", JSON.stringify({ "blacklistedUsers": blacklistedUsers }, null, 4)); + if (typeof db.addBlacklistedUser === "function") { + db.addBlacklistedUser(user.id, reason, interaction.user.tag); + } const successEmbed = new EmbedBuilder() .setDescription(msgconfig.Mod.TicketBlacklist.Add.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been blacklisted.") @@ -133,7 +106,7 @@ module.exports = new Command({ .setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success) .setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() }) .setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`) - .addFields({ name: "Action", value: "\`\`\`Removed from blacklist\`\`\`", inline: false }) + .addFields({ name: "Action", value: "\`\`\`Added to blacklist\`\`\`", inline: false }) .addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false }); blacklistChannel.send({ embeds: [blacklistLogEmbed] }); @@ -145,15 +118,16 @@ module.exports = new Command({ }); } else if (subcommand === cmdconfig.Mod.TicketBlacklist.Remove.Command) { - if (!blacklistedUsers.includes(user.id)) { + if (db.isUserBlacklisted && !db.isUserBlacklisted(user.id)) { const notBlacklistedEmbed = new EmbedBuilder() .setDescription(msgconfig.Mod.TicketBlacklist.Remove.NotBlacklisted.replace("{userTag}", user.tag) || ":x: User is not blacklisted.") .setColor(supportbot.Embed.Colours.Warn); return interaction.reply({ embeds: [notBlacklistedEmbed], flags: MessageFlags.Ephemeral }); } - blacklistedUsers = blacklistedUsers.filter(id => id !== user.id); - fs.writeFileSync("./Data/BlacklistedUsers.json", JSON.stringify({ "blacklistedUsers": blacklistedUsers }, null, 4)); + if (typeof db.removeBlacklistedUser === "function") { + db.removeBlacklistedUser(user.id); + } const removedEmbed = new EmbedBuilder() .setDescription(msgconfig.Mod.TicketBlacklist.Remove.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been removed from the blacklist.") diff --git a/Configs/messages.yml b/Configs/messages.yml index a6ff5e9..5ee6d6c 100644 --- a/Configs/messages.yml +++ b/Configs/messages.yml @@ -8,7 +8,8 @@ # Installed with MIT License # # Discord Support: https://discord.gg/emeraldsrv -# Official Website & Marketplace: https://emeraldsrv.com +# Official Documentation: https://docs.emeraldsrv.com +# Addons Marketplace: https://community.emeraldsrv.com # # Messages Configuration (messages.yml) diff --git a/Configs/supportbot-ai.yml b/Configs/supportbot-ai.yml index 4f47c42..e8f9f89 100644 --- a/Configs/supportbot-ai.yml +++ b/Configs/supportbot-ai.yml @@ -1,15 +1,3 @@ -# ___ _ ___ _ -# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_ -# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | | -# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_| -# |_| |_| -# -# SupportBot created by Emerald Services -# Installed with MIT License -# -# Discord Support: https://discord.gg/emeraldsrv -# Official Website & Marketplace: https://emeraldsrv.com - # ------------------------- # SupportBot AI # @@ -20,8 +8,8 @@ Enabled: true General: - Provider: "groq" - Model: "MODEL_NAME" + Provider: "openai" + Model: "MODEL" Model_API_Key: "MODEL_API_KEY" # Providers Available: # "openai" - Use OpenAI's GPT models. Example: "gpt-5.4" "gpt-5-mini" @@ -33,7 +21,7 @@ General: Temperature: 0.4 Name: "SupportBot AI" - PastebinAPI_Key: "PASTEBIN_API_KEY" + PastebinAPI_Key: "YOUR_PASTEBIN_API_KEY" PastebinAPI_URL: "https://pastebin.com/api/api_post.php" Style: diff --git a/Configs/supportbot.yml b/Configs/supportbot.yml index b7ee41f..e68a47a 100644 --- a/Configs/supportbot.yml +++ b/Configs/supportbot.yml @@ -8,11 +8,11 @@ # Installed with MIT License # # Discord Support: https://discord.gg/emeraldsrv -# Official Website & Marketplace: https://emeraldsrv.com +# Website: https://emeraldsrv.dev # # SupportBot Configuration (supportbot.yml) -SupportBot_Version: "v26.2-pre1" +SupportBot_Version: "v26.2-pre2" # ------------------------- # General @@ -20,12 +20,32 @@ SupportBot_Version: "v26.2-pre1" General: Name: "SupportBot" - Token: "DISCORD_BOT_TOKEN" + Token: "YOUR_BOT_TOKEN_HERE" Addons: Enabled: true - GuildId: "GUILD_ID" + GuildId: "YOUR_GUILD_ID_HERE" # ------------------------- +# Database Configuration +# ------------------------- + +Database: + # Driver options: "mysql" (works for MariaDB & MySQL) or "sqlite" + Driver: "mysql" + + # Connection Settings for MariaDB / MySQL (used when Driver is "mysql" or "mariadb") + MySQL: + Host: "localhost" + Port: 3306 + User: "root" + Password: "password" + Database: "supportbot" + ConnectionLimit: 10 + + # Connection Settings for SQLite (used when Driver is "sqlite") + SQLite: + File: "./Data/supportbot.db" + # Activity # ------------------------- @@ -33,12 +53,12 @@ Activity: Status: "SupportBot" Type: "Watching" StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME" - # Available Activity Types: - # - Playing - # - Watching - # - Listening - # - Competing - # - Streaming (URL required) + # Available Types: + # Playing + # Watching + # Listening + # Competing + # Streaming (URL required) # You can also update the activity via Discord using: /settings @@ -54,6 +74,7 @@ Embed: Warn: "#fca117" Footer: "SupportBot | Developed by Emerald Development" + # Use 0x