diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b98d0d5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +Configs/supportbot.yml filter=supportbot-token diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b73f8d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules + +# Dashboard UI source (local only — publish public/dashboard/ instead) +dashboard/ + +.githooks +scripts diff --git a/API/server.js b/API/server.js index edb4b1d..db5ae76 100644 --- a/API/server.js +++ b/API/server.js @@ -1,10 +1,81 @@ const express = require('express'); const cors = require('cors'); +const cookieParser = require('cookie-parser'); const fs = require('fs'); const os = require('os'); +const path = require('path'); const yaml = require('js-yaml'); const YAML = require('yaml'); const db = require('../Structures/Database.js'); +const configStore = require('../Structures/ConfigStore.js'); +const { reloadBot, restartDiscordBot } = require('../Structures/BotReload.js'); +const { LOG_TYPES, fetchLogs, getLogMeta } = require('../Structures/LogReader.js'); +const { evaluateAllModules } = require('../Structures/ModuleStatus.js'); +const { getBotInviteUrl } = require('../Structures/BotInvite.js'); +const notificationStore = require('../Structures/NotificationStore.js'); +const { + notifyBotRestart, + notifyUpdateInstalled, + runUpdateCheck, +} = require('../Structures/NotificationService.js'); +const { + SESSION_COOKIE, + signSession, + verifySession, + createSessionPayload, + sessionCookieOptions, +} = require('../Structures/DashboardSession.js'); +const { + OAUTH_STATE_COOKIE, + buildAuthorizeUrl, + exchangeCode, + fetchDiscordUser, + createOAuthState, + oauthStateCookieOptions, + isOAuthConfigured, + discordAvatarUrl, +} = require('../Structures/DiscordOAuth.js'); +const { + bootstrapFromOAuth, + getAccess, + canLogin, + formatPublicUser, + syncDiscordProfile, + listUsers, + addUser, + updateUser, + removeUser, + hasPermission, + canAccessConfig, + ROLES, + CONFIG_FILES, +} = require('../Structures/DashboardUserStore.js'); +const { + grantServiceAccess, + forbid, + assertConfigFilesEditable, + fullPermissions, +} = require('../Structures/DashboardAuth.js'); +const { + syncGuildHealth, + leaveGuild, + fetchGuildResources, + clearGuildResourcesCache, +} = require('../Structures/GuildManager.js'); +const { withTimeout } = require('../Structures/asyncTimeout.js'); +const { + listCatalogAddons, + listInstalledAddons, + installAddonFromCatalog, + listLocalAddonConfigs, + readAddonConfig, + writeAddonConfig, +} = require('../Structures/AddonCatalog.js'); + +const DASHBOARD_DIR = path.join(__dirname, '../public/dashboard'); +const UPDATE_REPO = 'C-h-a-r/SupportBot-Dashboard'; +const UPDATE_BRANCH = 'release'; +const UPDATE_ZIP_URL = `https://github.com/${UPDATE_REPO}/archive/refs/heads/${UPDATE_BRANCH}.zip`; class APIServer { constructor(client) { @@ -21,30 +92,405 @@ class APIServer { if (!this.config.Enabled) return; - this.app.use(cors()); + this.oauth = this.config.OAuth || {}; + bootstrapFromOAuth(this.oauth); + + this.app.use(cors({ origin: true, credentials: true })); + this.app.use(cookieParser()); this.app.use(express.json()); - this.app.use((req, res, next) => { - const authHeader = req.headers.authorization; - if (!authHeader || authHeader !== `Bearer ${this.config.SecretKey}`) { - return res.status(401).json({ error: 'Unauthorized. Invalid Secret Key.' }); - } - next(); + this.app.get('/api/health', (req, res) => { + const guild = + this.client?.user ? syncGuildHealth(this.client) : null; + res.json({ + success: true, + dashboard: fs.existsSync(DASHBOARD_DIR), + botReady: Boolean(this.client?.user) && !this.client?.__restarting, + botRestarting: Boolean(this.client?.__restarting), + oauthEnabled: isOAuthConfigured(this.oauth), + guild, + }); }); + this.setupAuthRoutes(); + + const api = express.Router(); + api.use((req, res, next) => this.authenticate(req, res, next)); + this.apiRouter = api; this.setupRoutes(); + this.app.use('/api', api); + + this.setupDashboard(); + + this.app.use((err, req, res, next) => { + if (res.headersSent) return next(err); + console.error('[API] Unhandled error:', err); + res.status(500).json({ + success: false, + error: err.message || 'Internal server error', + }); + }); + } + + authenticate(req, res, next) { + const secret = this.config.SecretKey; + const session = verifySession(req.cookies?.[SESSION_COOKIE], secret); + + if (session?.userId) { + const access = getAccess(session.userId, this.oauth); + if (access) { + req.dashboardUser = session; + req.dashboardPermissions = access.permissions; + req.dashboardRole = access.role; + req.dashboardIsOwner = access.isOwner; + return next(); + } + } + + const authHeader = req.headers.authorization; + if (authHeader === `Bearer ${secret}`) { + grantServiceAccess(req); + return next(); + } + + return res.status(401).json({ + error: 'Unauthorized. Sign in with Discord.', + }); + } + + requirePermission(permission) { + return (req, res, next) => { + if (req.dashboardService) return next(); + if (hasPermission(req.dashboardPermissions, permission)) return next(); + return forbid(res); + }; + } + + requireConfigAccess(mode = 'view') { + return (req, res, next) => { + if (req.dashboardService) return next(); + const file = req.params?.file || req.body?.filename; + if (!file) return next(); + if (!canAccessConfig(req.dashboardPermissions, file, mode)) { + return forbid(res); + } + return next(); + }; + } + + respondAuthUser(req, res, session) { + const access = getAccess(session.userId, this.oauth); + if (!access) { + return res.status(401).json({ success: false, error: 'Not signed in' }); + } + + return res.json({ + success: true, + data: formatPublicUser( + access.entry, + access, + { + id: session.userId, + username: session.username, + global_name: session.globalName, + avatar: session.avatar, + }, + ), + }); + } + + setupAuthRoutes() { + const secret = this.config.SecretKey; + const oauth = this.oauth; + + this.app.get('/api/auth/discord', (req, res) => { + if (!isOAuthConfigured(oauth)) { + return res.redirect('/login?error=oauth_not_configured'); + } + + const state = createOAuthState(); + res.cookie(OAUTH_STATE_COOKIE, state, oauthStateCookieOptions()); + res.redirect(buildAuthorizeUrl(oauth, state)); + }); + + this.app.get('/api/auth/discord/callback', async (req, res) => { + if (!isOAuthConfigured(oauth)) { + return res.redirect('/login?error=oauth_not_configured'); + } + + const { code, state, error } = req.query; + if (error) { + return res.redirect(`/login?error=${encodeURIComponent(String(error))}`); + } + if (!code || !state) { + return res.redirect('/login?error=missing_code'); + } + + const savedState = req.cookies?.[OAUTH_STATE_COOKIE]; + res.clearCookie(OAUTH_STATE_COOKIE, { path: '/' }); + + if (!savedState || savedState !== state) { + return res.redirect('/login?error=invalid_state'); + } + + try { + const accessToken = await exchangeCode(oauth, String(code)); + const user = await fetchDiscordUser(accessToken); + + if (!canLogin(user.id, oauth)) { + return res.redirect('/login?error=not_allowed'); + } + + syncDiscordProfile(user.id, { + username: user.username, + globalName: user.global_name || null, + avatar: user.avatar || null, + }); + + const payload = createSessionPayload(user); + const token = signSession(payload, secret); + res.cookie(SESSION_COOKIE, token, sessionCookieOptions()); + res.redirect('/'); + } catch (err) { + console.error('[API] Discord OAuth callback failed:', err.message); + res.redirect('/login?error=oauth_failed'); + } + }); + + this.app.get('/api/auth/me', (req, res) => { + const session = verifySession(req.cookies?.[SESSION_COOKIE], secret); + if (!session?.userId) { + return res.status(401).json({ success: false, error: 'Not signed in' }); + } + return this.respondAuthUser(req, res, session); + }); + + this.app.post('/api/auth/logout', (req, res) => { + res.clearCookie(SESSION_COOKIE, { path: '/' }); + res.json({ success: true }); + }); + } + + setupDashboard() { + if (!fs.existsSync(DASHBOARD_DIR)) { + console.warn('[API] Dashboard UI missing. Ensure public/dashboard/ is present in your install.'); + return; + } + + this.app.use(express.static(DASHBOARD_DIR)); + + this.app.get(/^(?!\/api).*/, (req, res, next) => { + if (req.method !== 'GET') return next(); + res.sendFile(path.join(DASHBOARD_DIR, 'index.html'), (err) => { + if (err) next(err); + }); + }); + + console.log('[API] Dashboard available at http://localhost:' + (this.config.Port || 3000)); } start(port) { + if (this._httpServer) { + return this._httpServer; + } + const listenPort = port || this.config.Port || 3000; - this.app.listen(listenPort, () => { + this._httpServer = this.app.listen(listenPort, () => { console.log(`[API] Server running on port ${listenPort}`); + this.startNotificationJobs(); }); + + this._httpServer.on('error', (err) => { + if (err.code === 'EADDRINUSE') { + console.error( + `[API] Port ${listenPort} is already in use. Change Port in Configs/api.yml.`, + ); + } else { + console.error('[API] Server error:', err); + } + }); + + return this._httpServer; + } + + async restartBotAfterConfigSave() { + if (!this.client) { + return { success: false, error: 'Bot client not available' }; + } + if (this.client.__restarting) { + return { + success: false, + error: 'Bot restart already in progress. Try again in a moment.', + }; + } + + try { + const result = await withTimeout( + restartDiscordBot(this.client), + 90_000, + 'Bot restart timed out after 90 seconds', + ); + clearGuildResourcesCache(); + return { + success: true, + commands: result.commands?.count ?? 0, + }; + } catch (err) { + console.error('[API] Bot restart failed:', err); + return { success: false, error: err.message }; + } + } + + startNotificationJobs() { + const FIRST_MS = 60 * 1000; + const INTERVAL_MS = 6 * 60 * 60 * 1000; + + const run = async () => { + try { + await runUpdateCheck(); + } catch (err) { + console.warn('[Notifications] Update check failed:', err.message); + } + }; + + this._notificationTimers = [ + setTimeout(run, FIRST_MS), + setInterval(run, INTERVAL_MS), + ]; } setupRoutes() { - this.app.get('/api/stats', async (req, res) => { + const app = this.apiRouter; + const oauth = this.oauth; + const p = (permission) => this.requirePermission(permission); + const cv = (mode) => this.requireConfigAccess(mode); + + app.get('/dashboard-users', p('users.view'), (req, res) => { try { + res.json({ + success: true, + data: { + users: listUsers(oauth), + roles: ROLES, + configFiles: CONFIG_FILES, + }, + }); + } catch (err) { + res.status(500).json({ success: false, error: err.message }); + } + }); + + app.post('/dashboard-users', p('users.manage'), (req, res) => { + try { + const actorId = req.dashboardUser?.userId || 'service'; + const user = addUser(req.body, actorId, oauth); + res.json({ success: true, data: user }); + } catch (err) { + res.status(400).json({ success: false, error: err.message }); + } + }); + + app.patch('/dashboard-users/:id', p('users.manage'), (req, res) => { + try { + const actorId = req.dashboardUser?.userId || 'service'; + const user = updateUser(req.params.id, req.body, actorId, oauth); + res.json({ success: true, data: user }); + } catch (err) { + res.status(400).json({ success: false, error: err.message }); + } + }); + + app.delete('/dashboard-users/:id', p('users.manage'), (req, res) => { + try { + removeUser(req.params.id, oauth); + res.json({ success: true }); + } catch (err) { + res.status(400).json({ success: false, error: err.message }); + } + }); + + app.get('/guild/status', p('overview'), (req, res) => { + try { + if (!this.client?.user) { + return res.json({ + success: true, + data: { + status: 'offline', + configuredGuildId: null, + message: 'Bot is not connected to Discord.', + configuredGuild: null, + extraGuilds: [], + }, + }); + } + res.json({ + success: true, + data: syncGuildHealth(this.client), + }); + } catch (err) { + res.status(500).json({ success: false, error: err.message }); + } + }); + + app.get('/guild/resources', (req, res, next) => { + if (req.dashboardService) return next(); + const canAny = CONFIG_FILES.some((f) => + canAccessConfig(req.dashboardPermissions, f, 'view'), + ); + if (!canAny && !hasPermission(req.dashboardPermissions, 'overview')) { + return forbid(res); + } + return next(); + }, async (req, res) => { + try { + if (this.client?.__restarting) { + return res.status(503).json({ + success: false, + error: 'Bot is restarting. Channel and role lists will be back shortly.', + }); + } + if (!this.client?.user) { + return res.status(503).json({ + success: false, + error: 'Bot is not connected.', + }); + } + const data = await fetchGuildResources(this.client); + if (!data) { + return res.status(400).json({ + success: false, + error: 'Bot is not in the configured server. Set General.GuildId and invite the bot.', + }); + } + res.json({ success: true, data }); + } catch (err) { + res.status(500).json({ success: false, error: err.message }); + } + }); + + app.post('/guild/:guildId/leave', p('settings.update'), async (req, res) => { + try { + if (!this.client?.user) { + return res.status(503).json({ + success: false, + error: 'Bot is not connected.', + }); + } + const health = await leaveGuild(this.client, req.params.guildId); + res.json({ success: true, data: health }); + } catch (err) { + res.status(400).json({ success: false, error: err.message }); + } + }); + + app.get('/stats', p('overview'), async (req, res) => { + try { + if (this.client.__restarting) { + return res.json({ + success: false, + error: 'Bot is restarting after a config change. Try again shortly.', + }); + } if (!this.client.user) { return res.json({ success: false, error: 'Bot is still starting up.' }); } @@ -55,8 +501,20 @@ class APIServer { }); const allTickets = db.getAllTickets() || []; - const openTickets = allTickets.filter(t => t.open).length; - const closedTickets = allTickets.filter(t => !t.open).length; + const openTickets = allTickets.filter(t => t.status === 'open').length; + const closedTickets = allTickets.filter(t => t.status !== 'open').length; + + const transcriptDir = path.join(__dirname, '../Data/Transcripts'); + let transcriptCount = 0; + if (fs.existsSync(transcriptDir)) { + transcriptCount = fs + .readdirSync(transcriptDir) + .filter((f) => f.endsWith('-transcript.html')).length; + } + + const modules = evaluateAllModules(configStore, { + transcriptCount, + }); const totalMem = os.totalmem(); const freeMem = os.freemem(); @@ -71,6 +529,7 @@ class APIServer { username: this.client.user.username, id: this.client.user.id, avatar: this.client.user.displayAvatarURL(), + inviteUrl: getBotInviteUrl(this.client.user.id), version: require('../package.json').version, ping: this.client.ws.ping, uptime: process.uptime(), @@ -82,6 +541,7 @@ class APIServer { open: openTickets, closed: closedTickets }, + modules, hosting: { ram_used: (usedMem / 1024 / 1024 / 1024).toFixed(2) + ' GB', ram_total: (totalMem / 1024 / 1024 / 1024).toFixed(2) + ' GB', @@ -97,7 +557,7 @@ class APIServer { } }); - this.app.get('/api/configs/:file', (req, res) => { + app.get('/configs/:file', cv('view'), (req, res) => { const validFiles = ['supportbot', 'ticket-panel', 'commands', 'messages', 'supportbot-ai']; const file = req.params.file; if (!validFiles.includes(file)) return res.status(400).json({ success: false, error: 'Invalid file' }); @@ -109,20 +569,47 @@ class APIServer { } }); - this.app.put('/api/configs/raw', (req, res) => { + app.put('/configs/raw', cv('edit'), async (req, res) => { const validFiles = ['supportbot', 'ticket-panel', 'commands', 'messages', 'supportbot-ai']; const file = req.body.filename; - const content = req.body.content; + let content = req.body.content; if (!validFiles.includes(file)) return res.status(400).json({ success: false, error: 'Invalid file' }); try { + if (file === 'supportbot' && /^\s*Token:\s*["']BOT_TOKEN["']/m.test(content)) { + const currentPath = `./Configs/${file}.yml`; + if (fs.existsSync(currentPath)) { + const current = fs.readFileSync(currentPath, 'utf8'); + const tokenMatch = current.match(/^(\s*Token:\s*)["']([^"']+)["']/m); + if (tokenMatch && tokenMatch[2] !== 'BOT_TOKEN') { + content = content.replace( + /^(\s*Token:\s*)["']BOT_TOKEN["']/m, + `$1"${tokenMatch[2]}"`, + ); + } + } + } fs.writeFileSync(`./Configs/${file}.yml`, content); - res.json({ success: true }); + configStore.reloadFile(file); + + let botRestart = { success: false, error: 'Bot client not available' }; + if (this.client) { + botRestart = await this.restartBotAfterConfigSave(); + notifyBotRestart(botRestart.success, botRestart.error); + } + + res.json({ + success: true, + botRestart, + message: botRestart.success + ? 'Config saved and bot restarted. API and dashboard stayed online.' + : 'Config saved but the bot could not restart. Check server logs.', + }); } catch (err) { res.status(500).json({ success: false, error: 'Failed to write config' }); } }); - this.app.get('/api/configs/json/:file', (req, res) => { + app.get('/configs/json/:file', cv('view'), (req, res) => { const validFiles = ['supportbot', 'ticket-panel', 'commands', 'messages', 'supportbot-ai']; const file = req.params.file; if (!validFiles.includes(file)) return res.status(400).json({ success: false, error: 'Invalid file' }); @@ -134,82 +621,299 @@ class APIServer { } }); - this.app.post('/api/configs/update-fields/multi', (req, res) => { + app.post('/configs/update-fields/multi', async (req, res) => { const updates = req.body; if (!updates || typeof updates !== 'object') return res.status(400).json({ success: false, error: 'Invalid updates' }); + + const touchedFiles = new Set(); + for (const keyPath of Object.keys(updates)) { + touchedFiles.add(keyPath.split(':')[0]); + } + if (!req.dashboardService) { + const denied = assertConfigFilesEditable( + req.dashboardPermissions, + [...touchedFiles], + res, + ); + if (denied) return; + } + try { + const touched = new Set(); for (const [keyPath, value] of Object.entries(updates)) { - const [fileName, path] = keyPath.split(':'); + const [fileName, fieldPath] = keyPath.split(':'); const fileContent = fs.readFileSync(`./Configs/${fileName}.yml`, 'utf8'); const doc = YAML.parseDocument(fileContent); - const keys = path.split('.'); + const keys = fieldPath.split('.'); doc.setIn(keys, value); fs.writeFileSync(`./Configs/${fileName}.yml`, doc.toString()); + touched.add(fileName); } - res.json({ success: true }); + + const reloadFiles = [...touched].filter((f) => f !== 'api'); + + let botRestart = { success: false, error: 'Bot client not available' }; + if (reloadFiles.length && this.client) { + botRestart = await this.restartBotAfterConfigSave(); + notifyBotRestart(botRestart.success, botRestart.error); + } + + res.json({ + success: true, + reloaded: reloadFiles, + botRestart, + message: botRestart.success + ? 'Config saved and bot restarted. API and dashboard stayed online.' + : 'Config saved but the bot could not restart. Check server logs.', + }); } catch (err) { console.error('[API] Update failed:', err); res.status(500).json({ success: false, error: err.message }); } }); - this.app.post('/api/addons/install', async (req, res) => { - const { url, filename } = req.body; - if (!url || !filename) return res.status(400).json({ success: false, error: 'Missing url or filename' }); + app.get('/notifications', p('overview'), (req, res) => { + res.json({ + success: true, + data: { + items: notificationStore.list(), + unreadCount: notificationStore.unreadCount(), + }, + }); + }); + app.post('/notifications/read', p('overview'), (req, res) => { + const { ids } = req.body || {}; + if (ids === 'all') { + notificationStore.markRead('all'); + } else if (Array.isArray(ids)) { + notificationStore.markRead(ids); + } else if (typeof ids === 'string') { + notificationStore.markRead([ids]); + } + res.json({ + success: true, + data: { unreadCount: notificationStore.unreadCount() }, + }); + }); + + app.delete('/notifications/:id', p('overview'), (req, res) => { + notificationStore.dismiss(req.params.id); + res.json({ + success: true, + data: { unreadCount: notificationStore.unreadCount() }, + }); + }); + + app.post('/system/reload-configs', p('settings.update'), async (req, res) => { try { - const axios = require('axios'); - const response = await axios({ - method: 'get', - url: url, - responseType: 'stream' - }); - - // Ensure Addons directory exists - if (!fs.existsSync('./Addons')) { - fs.mkdirSync('./Addons'); + const file = req.body?.file; + const valid = ['supportbot', 'ticket-panel', 'commands', 'messages', 'supportbot-ai']; + if (file && !valid.includes(file)) { + return res.status(400).json({ success: false, error: 'Invalid config file' }); } - - const writer = fs.createWriteStream(`./Addons/${filename}`); - response.data.pipe(writer); - - writer.on('finish', () => { - res.json({ success: true, message: `Addon ${filename} installed successfully.` }); + const result = await reloadBot(this.client, { + fileKey: file || null, + refreshCommands: !file || file === 'commands', }); - - writer.on('error', (err) => { - console.error('[API] Download failed:', err); - res.status(500).json({ success: false, error: 'Failed to write addon file' }); + res.json({ + success: true, + reloaded: result.reloaded, + commands: result.commands, }); - } catch (err) { - console.error('[API] Addon install failed:', err); - res.status(500).json({ success: false, error: 'Failed to download addon' }); + console.error('[API] Reload failed:', err); + res.status(500).json({ success: false, error: err.message }); } }); - this.app.post('/api/addons/push', (req, res) => { - const { filename, content } = req.body; - if (!filename || !content) return res.status(400).json({ success: false, error: 'Missing filename or content' }); - - const sanitizedFilename = filename.replace(/[^a-z0-9_.-]/gi, '_'); - + app.get('/system/logs/meta', p('logs'), (req, res) => { try { - if (!fs.existsSync('./Addons')) { - fs.mkdirSync('./Addons'); - } - fs.writeFileSync(`./Addons/${sanitizedFilename}`, content); - res.json({ success: true, message: `Addon ${sanitizedFilename} deployed successfully.` }); + res.json({ success: true, data: { types: LOG_TYPES, files: getLogMeta() } }); } catch (err) { - console.error('[API] Push failed:', err); - res.status(500).json({ success: false, error: 'Failed to write addon file' }); + console.error('[API] Log meta failed:', err); + res.status(500).json({ success: false, error: err.message }); + } + }); + + app.get('/system/logs', p('logs'), (req, res) => { + try { + const typesParam = req.query.types; + const types = typesParam + ? String(typesParam).split(',').map((t) => t.trim()) + : LOG_TYPES; + + let cursor = {}; + if (req.query.cursor) { + try { + cursor = JSON.parse(String(req.query.cursor)); + } catch { + return res.status(400).json({ success: false, error: 'Invalid cursor' }); + } + } + + const hasCursor = Object.keys(cursor).length > 0; + const tailParam = req.query.tail; + const initialLines = hasCursor + ? 0 + : Math.min( + 1000, + Math.max(0, parseInt(tailParam, 10) || 400), + ); + + const { entries, cursor: nextCursor } = fetchLogs(types, cursor, initialLines); + + res.json({ + success: true, + data: { + entries, + cursor: nextCursor, + }, + }); + } catch (err) { + console.error('[API] Logs fetch failed:', err); + res.status(500).json({ success: false, error: err.message }); + } + }); + + app.get('/system/update-check', p('settings.view'), async (req, res) => { + try { + const info = await runUpdateCheck(); + res.json({ success: true, data: info }); + } catch (err) { + console.error('[API] Update check failed:', err); + res.status(500).json({ success: false, error: 'Could not check for updates. Try again later.' }); + } + }); + + app.get('/addons/catalog', p('settings.view'), async (req, res) => { + try { + const catalog = await listCatalogAddons(); + const installed = await listInstalledAddons(catalog); + const installedIds = new Set(installed.map((a) => a.id)); + res.json({ + success: true, + data: { + repository: `https://github.com/Emerald-Services/Addons`, + addons: catalog.map((addon) => ({ + ...addon, + installed: installedIds.has(addon.id), + })), + }, + }); + } catch (err) { + console.error('[API] Addon catalog failed:', err); + res.status(500).json({ + success: false, + error: err.message || 'Could not load addon catalog from GitHub', + }); + } + }); + + app.get('/addons/installed', p('settings.view'), async (req, res) => { + try { + let catalog = []; + try { + catalog = await listCatalogAddons(); + } catch { + catalog = []; + } + const installed = await listInstalledAddons(catalog); + const configs = listLocalAddonConfigs(); + res.json({ success: true, data: { installed, configs } }); + } catch (err) { + res.status(500).json({ success: false, error: err.message }); + } + }); + + app.post('/addons/install', p('settings.update'), async (req, res) => { + const name = req.body?.name || req.body?.id; + if (!name) { + return res.status(400).json({ success: false, error: 'Missing addon name' }); + } + + try { + const result = await installAddonFromCatalog(name); + let botRestart = { success: false, error: 'Bot client not available' }; + if (this.client) { + botRestart = await this.restartBotAfterConfigSave(); + notifyBotRestart(botRestart.success, botRestart.error); + } + + res.json({ + success: true, + data: result, + botRestart, + message: botRestart.success + ? `${result.addon.name} installed and bot restarted. Enable addons in Bot config if needed.` + : `${result.addon.name} installed but the bot could not restart. Check server logs.`, + }); + } catch (err) { + console.error('[API] Addon install failed:', err); + res.status(500).json({ success: false, error: err.message }); + } + }); + + app.get('/addons/configs', p('settings.view'), (req, res) => { + try { + const files = listLocalAddonConfigs(); + res.json({ success: true, data: files }); + } catch (err) { + res.status(500).json({ success: false, error: err.message }); + } + }); + + app.get('/addons/configs/json/:file', (req, res, next) => { + if (req.dashboardService) return next(); + if (!canAccessConfig(req.dashboardPermissions, 'supportbot', 'view')) { + return forbid(res); + } + return next(); + }, (req, res) => { + try { + const parsed = readAddonConfig(req.params.file); + res.json({ success: true, data: parsed.data, filename: parsed.filename }); + } catch (err) { + res.status(404).json({ success: false, error: err.message }); + } + }); + + app.put('/addons/configs', (req, res, next) => { + if (req.dashboardService) return next(); + if (!canAccessConfig(req.dashboardPermissions, 'supportbot', 'edit')) { + return forbid(res); + } + return next(); + }, async (req, res) => { + const { filename, data } = req.body || {}; + if (!filename || !data || typeof data !== 'object') { + return res.status(400).json({ success: false, error: 'Missing filename or data' }); + } + + try { + writeAddonConfig(filename, data); + let botRestart = { success: false, error: 'Bot client not available' }; + if (this.client) { + botRestart = await this.restartBotAfterConfigSave(); + notifyBotRestart(botRestart.success, botRestart.error); + } + res.json({ + success: true, + botRestart, + message: botRestart.success + ? 'Addon config saved and bot restarted.' + : 'Addon config saved but the bot could not restart.', + }); + } catch (err) { + console.error('[API] Addon config save failed:', err); + res.status(500).json({ success: false, error: err.message }); } }); // --- Nexus Updater API --- - this.app.post('/api/system/update', async (req, res) => { + app.post('/system/update', p('settings.update'), async (req, res) => { const { url, version } = req.body; - if (!url) return res.status(400).json({ success: false, error: 'Missing update URL' }); + const downloadUrl = url || UPDATE_ZIP_URL; try { const axios = require('axios'); @@ -220,7 +924,7 @@ class APIServer { // 1. Download const tempZip = `./update_${Date.now()}.zip`; - const response = await axios({ method: 'get', url: url, responseType: 'stream' }); + const response = await axios({ method: 'get', url: downloadUrl, responseType: 'stream' }); const writer = fs.createWriteStream(tempZip); response.data.pipe(writer); @@ -276,8 +980,8 @@ class APIServer { } }); - // 4. Overwrite Core Folders - const coreFolders = ['API', 'Commands', 'Events', 'Structures']; + // 4. Overwrite core folders (keep Configs + Data) + const coreFolders = ['API', 'Commands', 'Events', 'Structures', 'public', 'scripts']; coreFolders.forEach(folder => { const src = path.join(sourcePath, folder); if (fs.existsSync(src)) { @@ -305,19 +1009,39 @@ class APIServer { execSync(`rm -rf "${tempDir}"`); } - res.json({ success: true, message: 'Update installed successfully. Bot is restarting...' }); - - // Reboot - setTimeout(() => process.exit(0), 1000); + if (fs.existsSync(path.join(sourcePath, 'index.js'))) { + fs.copyFileSync(path.join(sourcePath, 'index.js'), './index.js'); + } + + configStore.reloadBotConfigs(); + if (this.client) { + await reloadBot(this.client, { refreshCommands: true }); + } + + const updateMessage = + 'Update installed. Please restart your server (stop and run npm start again) to apply all file changes.'; + notifyUpdateInstalled(updateMessage); + + res.json({ + success: true, + message: updateMessage, + restartRequired: true, + }); } catch (err) { console.error('[Updater] Update failed:', err); + notificationStore.add({ + type: 'error', + title: 'Update failed', + message: err.message, + href: '/settings', + }); res.status(500).json({ success: false, error: err.message }); } }); // --- Transcript API --- - this.app.get('/api/system/transcripts', (req, res) => { + app.get('/system/transcripts', p('transcripts'), (req, res) => { const transcriptDir = './Data/Transcripts'; try { if (!fs.existsSync(transcriptDir)) { @@ -341,7 +1065,7 @@ class APIServer { } }); - this.app.get('/api/system/transcripts/:id', (req, res) => { + app.get('/system/transcripts/:id', p('transcripts'), (req, res) => { const id = req.params.id.replace(/[^0-9]/g, ''); // Sanitize ID const filePath = `./Data/Transcripts/${id}-transcript.html`; try { diff --git a/Addons/Configs/discord-stats.yml b/Addons/Configs/discord-stats.yml new file mode 100644 index 0000000..f295c0e --- /dev/null +++ b/Addons/Configs/discord-stats.yml @@ -0,0 +1,11 @@ + +# SupportBot | Emerald Services +# Discord Stats addon | Created by Xiled Emerald + +Guild_ID: "GUILD_ID_HERE" + +StatsCategory: "Server Statistics" + +UserCount_Name: "Total Members:" +ChannelCount_Name: "Total Channels:" +RoleCount_Name: "Total Roles:" \ No newline at end of file diff --git a/Addons/addon.js b/Addons/addon.js index 7609fd7..675bc57 100644 --- a/Addons/addon.js +++ b/Addons/addon.js @@ -2,7 +2,7 @@ const Discord = require("discord.js"); const fs = require("fs"); const yaml = require("js-yaml"); const { Command } = require('../Structures/Addon.js'); // Adjust the path as needed -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); +const supportbot = require("../Structures/ConfigStore").supportbot; module.exports = new Command({ name: "addon", diff --git a/Addons/discord-stats.js b/Addons/discord-stats.js new file mode 100644 index 0000000..1299b91 --- /dev/null +++ b/Addons/discord-stats.js @@ -0,0 +1,139 @@ +// SupportBot | Emerald Services +// Discord Stats addon | Created by Xiled Emerald + +const fs = require("fs"); + +const Discord = require("discord.js"); +const yaml = require("js-yaml"); +const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); + +// If you need to read something from the main config! +const cmdconfig = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); + +// This is for your Addon Configuration +const config = yaml.load(fs.readFileSync('./Addons/Configs/discord-stats.yml', 'utf8')); + +const { Command, Event } = require("../Structures/Addon"); + +module.exports.commands = [ + + new Command({ + name: "stats", + description: "Create the discord statistic channels", + options: [], + permissions: ["SEND_MESSAGES"], + async run(interaction) { + const guild = interaction.guild; + const statsCategory = guild.channels.cache.find( + (category) => + category.name === config.StatsCategory || + category.id === config.StatsCategory + ); + + if (!statsCategory) { + const errorEmbed = new Discord.MessageEmbed() + .setTitle("Invalid Category") + .setDescription( + `${supportbot.InvalidChannel}\n\n**Category Required:** \`${config.StatsCategory}\`` + ) + .setColor(supportbot.ErrorColour); + + interaction.reply({ + embeds: [errorEmbed], + ephemeral: true, + }); + return; + } + + const guildName = guild.name; + const userCount = guild.memberCount; + const roleCount = guild.roles.cache.size; + const channelCount = guild.channels.cache.size; + + try { + const guildNameChannel = await guild.channels.create(guildName, { + type: "GUILD_VOICE", + }); + + const userCountChannel = await guild.channels.create( + `${config.UserCount_Name} ${userCount}`, + { + type: "GUILD_VOICE", + } + ); + + const channelCountChannel = await guild.channels.create( + `${config.ChannelCount_Name} ${channelCount}`, + { + type: "GUILD_VOICE", + } + ); + + const roleCountChannel = await guild.channels.create( + `${config.RoleCount_Name} ${roleCount}`, + { + type: "GUILD_VOICE", + } + ); + + guildNameChannel.permissionOverwrites.edit(guild.id, { + VIEW_CHANNEL: true, + CONNECT: false, + }); + + userCountChannel.permissionOverwrites.edit(guild.id, { + VIEW_CHANNEL: true, + CONNECT: false, + }); + + channelCountChannel.permissionOverwrites.edit(guild.id, { + VIEW_CHANNEL: true, + CONNECT: false, + }); + + roleCountChannel.permissionOverwrites.edit(guild.id, { + VIEW_CHANNEL: true, + CONNECT: false, + }); + + userCountChannel.setParent(statsCategory.id); + roleCountChannel.setParent(statsCategory.id); + guildNameChannel.setParent(statsCategory.id); + channelCountChannel.setParent(statsCategory.id); + + const successEmbed = new Discord.MessageEmbed() + .setDescription(":white_check_mark: **[DISCORD STATS]** Channels created successfully!") + .setColor(supportbot.EmbedColour); + + interaction.reply({ + embeds: [successEmbed], + ephemeral: true, + }); + + setInterval(() => { + userCountChannel.setName( + `${config.UserCount_Name} ${userCount}` + ); + channelCountChannel.setName( + `${config.ChannelCount_Name} ${channelCount}` + ); + roleCountChannel.setName( + `${config.RoleCount_Name} ${roleCount}` + ); + }, 30000); + } catch (error) { + console.error(error); + const errorEmbed = new Discord.MessageEmbed() + .setTitle("Error") + .setDescription("An error occurred while creating the stats channels.") + .setColor(supportbot.ErrorColour); + + interaction.reply({ + embeds: [errorEmbed], + ephemeral: true, + }); + } + }, + }), + +]; diff --git a/Commands/General/ai.js b/Commands/General/ai.js index 08cbffa..804fbbc 100644 --- a/Commands/General/ai.js +++ b/Commands/General/ai.js @@ -5,9 +5,9 @@ const fs = require("fs"); const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, MessageFlags } = require("discord.js"); const yaml = require("js-yaml"); -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); -const supportbotai = yaml.load(fs.readFileSync("./Configs/supportbot-ai.yml", "utf8")); +const supportbot = require("../../Structures/ConfigStore").supportbot; +const cmdconfig = require("../../Structures/ConfigStore").commands; +const supportbotai = require("../../Structures/ConfigStore").supportbotAi; const Command = require("../../Structures/Command.js"); const AIDatabase = require("../../Structures/AIDatabase.js"); diff --git a/Commands/General/help.js b/Commands/General/help.js index 9b0e469..eaf2a94 100644 --- a/Commands/General/help.js +++ b/Commands/General/help.js @@ -2,8 +2,8 @@ const fs = require("fs"); const Discord = require("discord.js"); const yaml = require("js-yaml"); -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); +const supportbot = require("../../Structures/ConfigStore").supportbot; +const cmdconfig = require("../../Structures/ConfigStore").commands; const Command = require("../../Structures/Command.js"); diff --git a/Commands/General/info.js b/Commands/General/info.js index fe4dba3..10a517f 100644 --- a/Commands/General/info.js +++ b/Commands/General/info.js @@ -1,22 +1,11 @@ // SupportBot | Emerald Services // Info Command -const fs = require("fs"); - const Discord = require("discord.js"); -const yaml = require("js-yaml"); -const supportbot = yaml.load( - fs.readFileSync("./Configs/supportbot.yml", "utf8") -); - -const cmdconfig = yaml.load( - fs.readFileSync("./Configs/commands.yml", "utf8") -); - -const msgconfig = yaml.load( - fs.readFileSync("./Configs/messages.yml", "utf8") -); +const supportbot = require("../../Structures/ConfigStore").supportbot; +const cmdconfig = require("../../Structures/ConfigStore").commands; +const msgconfig = require("../../Structures/ConfigStore").messages; const Command = require("../../Structures/Command.js"); diff --git a/Commands/General/ping.js b/Commands/General/ping.js index 99bb62c..fd7d421 100644 --- a/Commands/General/ping.js +++ b/Commands/General/ping.js @@ -5,10 +5,8 @@ const fs = require("fs"); const Discord = require("discord.js"); const yaml = require("js-yaml"); -const supportbot = yaml.load( - fs.readFileSync("./Configs/supportbot.yml", "utf8") -); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); +const supportbot = require("../../Structures/ConfigStore").supportbot; +const cmdconfig = require("../../Structures/ConfigStore").commands; const Command = require("../../Structures/Command.js"); diff --git a/Commands/General/profile.js b/Commands/General/profile.js index c48cd5d..989f394 100644 --- a/Commands/General/profile.js +++ b/Commands/General/profile.js @@ -20,9 +20,9 @@ const yaml = require("js-yaml"); const db = require("../../Structures/Database.js"); const Command = require("../../Structures/Command.js"); -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); -const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); +const supportbot = require("../../Structures/ConfigStore").supportbot; +const cmdconfig = require("../../Structures/ConfigStore").commands; +const msgconfig = require("../../Structures/ConfigStore").messages; const clockedInUsers = new Set(); diff --git a/Commands/General/translate.js b/Commands/General/translate.js index 4363f1b..161db24 100644 --- a/Commands/General/translate.js +++ b/Commands/General/translate.js @@ -7,9 +7,9 @@ const yaml = require("js-yaml"); const { translate } = require("@vitalets/google-translate-api"); -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); -const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); +const supportbot = require("../../Structures/ConfigStore").supportbot; +const cmdconfig = require("../../Structures/ConfigStore").commands; +const msgconfig = require("../../Structures/ConfigStore").messages; const Command = require("../../Structures/Command.js"); diff --git a/Commands/Suggestions/suggest.js b/Commands/Suggestions/suggest.js index 7379099..148af80 100644 --- a/Commands/Suggestions/suggest.js +++ b/Commands/Suggestions/suggest.js @@ -3,9 +3,9 @@ const Discord = require("discord.js"); const yaml = require("js-yaml"); const path = require("path"); -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); -const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); +const supportbot = require("../../Structures/ConfigStore").supportbot; +const cmdconfig = require("../../Structures/ConfigStore").commands; +const msgconfig = require("../../Structures/ConfigStore").messages; const Command = require("../../Structures/Command.js"); const DB = require("../../Structures/Database.js"); diff --git a/Commands/Suggestions/suggestadmin.js b/Commands/Suggestions/suggestadmin.js index 33b70ec..377cd91 100644 --- a/Commands/Suggestions/suggestadmin.js +++ b/Commands/Suggestions/suggestadmin.js @@ -2,9 +2,9 @@ const Discord = require("discord.js"); const fs = require("fs"); const yaml = require("js-yaml"); -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); +const supportbot = require("../../Structures/ConfigStore").supportbot; +const msgconfig = require("../../Structures/ConfigStore").messages; +const cmdconfig = require("../../Structures/ConfigStore").commands; const SuggestionDB = require("../../Structures/Database.js"); const Command = require("../../Structures/Command.js"); diff --git a/Commands/Tickets/close.js b/Commands/Tickets/close.js index 9d0e8bd..5eeaf48 100644 --- a/Commands/Tickets/close.js +++ b/Commands/Tickets/close.js @@ -8,9 +8,9 @@ const db = require("../../Structures/Database.js"); const TicketManager = require("../../Structures/TicketManager.js"); const Command = require("../../Structures/Command.js"); -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); -const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); +const supportbot = require("../../Structures/ConfigStore").supportbot; +const cmdconfig = require("../../Structures/ConfigStore").commands; +const msgconfig = require("../../Structures/ConfigStore").messages; function getDepartmentConfig(departmentKey) { return supportbot.Ticket?.DepartmentSystem?.Departments?.[departmentKey] || null; diff --git a/Commands/Tickets/manageticket.js b/Commands/Tickets/manageticket.js index 4be8c57..ec57184 100644 --- a/Commands/Tickets/manageticket.js +++ b/Commands/Tickets/manageticket.js @@ -12,9 +12,9 @@ const { TextInputStyle, } = require("discord.js"); -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); +const supportbot = require("../../Structures/ConfigStore").supportbot; +const msgconfig = require("../../Structures/ConfigStore").messages; +const cmdconfig = require("../../Structures/ConfigStore").commands; const TicketManager = require("../../Structures/TicketManager.js"); const Command = require("../../Structures/Command.js"); diff --git a/Commands/Tickets/ticket.js b/Commands/Tickets/ticket.js index 8b0952f..2256e30 100644 --- a/Commands/Tickets/ticket.js +++ b/Commands/Tickets/ticket.js @@ -19,14 +19,10 @@ const yaml = require("js-yaml"); const db = require("../../Structures/Database.js"); -const panelconfig = yaml.load( - fs.readFileSync("./Configs/ticket-panel.yml", "utf8"), -); -const supportbot = yaml.load( - fs.readFileSync("./Configs/supportbot.yml", "utf8"), -); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); -const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); +const panelconfig = require("../../Structures/ConfigStore").ticketPanel; +const supportbot = require("../../Structures/ConfigStore").supportbot; +const cmdconfig = require("../../Structures/ConfigStore").commands; +const msgconfig = require("../../Structures/ConfigStore").messages; const Command = require("../../Structures/Command.js"); const TicketNumberID = require("../../Structures/TicketID.js"); diff --git a/Commands/Tickets/ticketpanel.js b/Commands/Tickets/ticketpanel.js index 1fab59f..0862d12 100644 --- a/Commands/Tickets/ticketpanel.js +++ b/Commands/Tickets/ticketpanel.js @@ -19,9 +19,9 @@ const { } = require("discord.js"); const yaml = require("js-yaml"); -const panelconfig = yaml.load(fs.readFileSync("./Configs/ticket-panel.yml", "utf8")); -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); +const panelconfig = require("../../Structures/ConfigStore").ticketPanel; +const supportbot = require("../../Structures/ConfigStore").supportbot; +const cmdconfig = require("../../Structures/ConfigStore").commands; const Command = require("../../Structures/Command.js"); const db = require("../../Structures/Database.js"); diff --git a/Commands/Utils/embed.js b/Commands/Utils/embed.js index 3024436..9ce36a4 100644 --- a/Commands/Utils/embed.js +++ b/Commands/Utils/embed.js @@ -16,9 +16,9 @@ const Command = require('../../Structures/Command.js'); const fs = require('fs'); const yaml = require('js-yaml'); -const supportbot = yaml.load(fs.readFileSync('./Configs/supportbot.yml', 'utf8')); -const cmdconfig = yaml.load(fs.readFileSync('./Configs/commands.yml', 'utf8')); -const msgconfig = yaml.load(fs.readFileSync('./Configs/messages.yml', 'utf8')); +const supportbot = require('../../Structures/ConfigStore').supportbot; +const cmdconfig = require("../../Structures/ConfigStore").commands; +const msgconfig = require("../../Structures/ConfigStore").messages; function parseColor(color) { if (!color) return 0x5865F2; diff --git a/Commands/Utils/mod.js b/Commands/Utils/mod.js index 8cb7e56..c4e0692 100644 --- a/Commands/Utils/mod.js +++ b/Commands/Utils/mod.js @@ -1,9 +1,9 @@ const fs = require("fs"); const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, MessageFlags } = require("discord.js"); const yaml = require("js-yaml"); -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); -const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); +const supportbot = require("../../Structures/ConfigStore").supportbot; +const cmdconfig = require("../../Structures/ConfigStore").commands; +const msgconfig = require("../../Structures/ConfigStore").messages; const Command = require("../../Structures/Command.js"); diff --git a/Commands/Utils/settings.js b/Commands/Utils/settings.js index 6131921..036a705 100644 --- a/Commands/Utils/settings.js +++ b/Commands/Utils/settings.js @@ -4,8 +4,8 @@ const fs = require("fs"); const yaml = require("js-yaml"); const db = require("../../Structures/Database.js"); -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); +const supportbot = require("../../Structures/ConfigStore").supportbot; +const cmdconfig = require("../../Structures/ConfigStore").commands; async function loadSettings() { return new Promise((resolve, reject) => { diff --git a/Configs/api.yml b/Configs/api.yml index af83968..6fc136f 100644 --- a/Configs/api.yml +++ b/Configs/api.yml @@ -5,4 +5,19 @@ API: Enabled: true Port: 3000 # PORT NUMBER FOR YOUR BOT - SecretKey: "PUT_RANDOM_STRING_HERE" # DO NOT SHARE THIS KEY WITH ANYONE \ No newline at end of file + + # Used to sign dashboard login sessions (keep random; do not share) + SecretKey: "fuiesdfioewefioesfioiosnd" + + # Discord OAuth2 — create an app at https://discord.com/developers/applications + # Add redirect URL exactly matching RedirectUri below (OAuth2 → Redirects) + OAuth: + Enabled: true + ClientId: "1289713673123922032" + ClientSecret: "KwSoar0SLeHXWl5oePGNTb9J5q5NVKAV" + RedirectUri: "http://localhost:3000/api/auth/discord/callback" + # Permanent owners (full access, manage users). Add more dashboard users in the Users page. + OwnerUserIds: + - "829112572816130058" + # Legacy alias for OwnerUserIds (optional) + AllowedUserIds: [] diff --git a/Configs/supportbot.yml b/Configs/supportbot.yml index 72117b5..7288e49 100644 --- a/Configs/supportbot.yml +++ b/Configs/supportbot.yml @@ -19,21 +19,20 @@ SupportBot_Version: "v26.1.1" # ------------------------- General: - Name: "SupportBot" + Name: "SupportBo" Token: "BOT_TOKEN" - Addons: Enabled: true - + GuildId: "1198017820093599856" + # ------------------------- # Activity # ------------------------- Activity: - Status: "SupportBot v8" - Type: "Competing" + Status: "Char" + Type: "Watching" StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME" - # Available Types: # Playing # Watching @@ -43,9 +42,9 @@ Activity: # You can also update the activity via Discord using: /settings -# ------------------------- -# Embeds -# ------------------------- + # ------------------------- + # Embeds + # ------------------------- Embed: Colours: @@ -55,12 +54,11 @@ Embed: Warn: "#fca117" Footer: "SupportBot | Developed by Emerald Development" - # Use 0x to change the colour of the embeds. -# ------------------------- -# Roles -# ------------------------- + # ------------------------- + # Roles + # ------------------------- Roles: AutoRole: @@ -74,18 +72,17 @@ Roles: Mod: AllowSupportStaff: false - # Allow Support Staff to use the /mod command. (More in detail customisation in commands.yml) -# ---------------------------- -# System Messages -# ---------------------------- + # ---------------------------- + # System Messages + # ---------------------------- -Welcome: +Welcome: Enabled: true Channel: "CHANNEL_ID" -Leave: +Leave: Enabled: true Channel: "CHANNEL_ID" @@ -110,7 +107,7 @@ Suggestions: # If true this will allow the user to vote for a suggestion they created ShowUsers: true - + Threads: Enabled: true Title: "Suggestion-releated Thread" @@ -121,7 +118,6 @@ Suggestions: Downvote: "2" RemoveVote: "4" RemoveVote_Title: "Remove Vote" - # Available Button Styles # 1 - Blurple # 2 - Grey @@ -129,21 +125,21 @@ Suggestions: # 4 - Red Tags: - Created: "TAG_ID" - Considering: "TAG_ID" - Solved: "TAG_ID" - Denied: "TAG_ID" + Created: "TAG_ID" + Considering: "TAG_ID" + Solved: "TAG_ID" + Denied: "TAG_ID" # ------------------------- # Tickets # ------------------------- VoiceTickets: - Name: "%username%'s VC" + Name: "%username%'s VC" Category: "CATEGORY_ID" Ticket: - TicketType: "channels" + TicketType: "channels" # Available TicketTypes: # channels - Tickets will be created as channels in the TicketChannelsCategory @@ -161,7 +157,7 @@ Ticket: # - Channel where ticket threads are made. (only if ticket threads is enabled above.) # For TicketType: channels - TicketChannelsCategory: "CATEGORY_ID" + TicketChannelsCategory: "CATEGORY_ID" TicketChannelsCategory2: "CATEGORY_ID" # This will be used if the first category has reached the max channel limit. TicketArchiveCategory: "CATEGORY_ID" # --- @@ -184,14 +180,15 @@ Ticket: StaffOnly: true # Set to false is anyone is able to close a ticket. - Invites: # Invites is for ticket threads. + Invites: + # Invites is for ticket threads. StaffOnly: true ClaimTickets: Enabled: false - Channel: "CHANNEL_ID" + Channel: "CHANNEL_ID" # Channel where the ticket assignments are sent for staff to claim. - + ClaimAcceptTime: 120000 # How long the assigned user has to accept the ticket before it gets re-assigned to someone else. # 120000 = 2 Minutes @@ -203,7 +200,6 @@ Ticket: ButtonTitle: "Claim Ticket" ButtonEmoji: "šŸŽ«" Button: "1" - # Available Button Styles # 1 - Blurple # 2 - Grey @@ -211,12 +207,13 @@ Ticket: # 4 - Red ReviewSystem: - Enabled: false + Enabled: false Channel: "CHANNEL_ID" UseModal: true - Questions: # Default questions asked when a ticket is created, these are used if departments are disabled. - Enabled: false + Questions: + # Default questions asked when a ticket is created, these are used if departments are disabled. + Enabled: false List: - "Please provide your account email?" - "What is the nature of your issue?" @@ -232,9 +229,9 @@ Ticket: ThreadName: "Chat with SupportBot AI" WelcomeMessage: "Hi! I’m SupportBot AI. You can chat with me here about your issue." -# ------------------------- -# Ticket Departments -# ------------------------- + # ------------------------- + # Ticket Departments + # ------------------------- DepartmentSystem: Enabled: true @@ -339,14 +336,14 @@ Buttons: # 3 - Green # 4 - Red -# ------------------------- -# Select Menus -# ------------------------- + # ------------------------- + # Select Menus + # ------------------------- SelectMenus: Tickets: PanelEmoji: "āš™ļø" - + CloseEmoji: "šŸ—‘ļø" ArchiveEmoji: "ā™»ļø" LockEmoji: "šŸ”" diff --git a/Configs/ticket-panel.yml b/Configs/ticket-panel.yml index 9817200..099f267 100644 --- a/Configs/ticket-panel.yml +++ b/Configs/ticket-panel.yml @@ -21,7 +21,8 @@ Panel: true Style: Title: "## SupportBot Ticket Creation" - Description: "Click on the button to file a `Ticket` and our team will try their best to assist you in the best way we can!" + Description: "Click on the button to file a `Ticket` and our team will try their + best to assist you in the best way we can!" Image: "https://i.imgur.com/dejQvkg.png" Color: "#45b463" Layout: "4" @@ -45,7 +46,7 @@ Button: # 4 - Green DepartmentMenu: - CustomId: "ticketdepartmentselect" + CustomId: "ticketdepartmentselect" Placeholder: "Choose a ticket department" DefaultDescription: "Open a support ticket" DefaultEmoji: "šŸŽ«" @@ -58,7 +59,8 @@ Settings: BottomDivider: false Messages: - NotConfigured: "Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`." + NotConfigured: "Ticket Panel is not set up. Please configure it in + `/Configs/ticket-panel.yml`." InvalidChannel: "TicketHome channel not found. Please check your config file." PanelSent: "Ticket panel message has been sent!" PanelSentWithDepartments: "Ticket panel message with department selection has been sent!" diff --git a/Data/ai-memory.db b/Data/ai-memory.db new file mode 100644 index 0000000..73a0692 Binary files /dev/null and b/Data/ai-memory.db differ diff --git a/Data/ai-memory.db-shm b/Data/ai-memory.db-shm new file mode 100644 index 0000000..5ff9ccc Binary files /dev/null and b/Data/ai-memory.db-shm differ diff --git a/Data/ai-memory.db-wal b/Data/ai-memory.db-wal new file mode 100644 index 0000000..57e9d6c Binary files /dev/null and b/Data/ai-memory.db-wal differ diff --git a/Data/dashboard-users.json b/Data/dashboard-users.json new file mode 100644 index 0000000..0b0f900 --- /dev/null +++ b/Data/dashboard-users.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "users": { + "829112572816130058": { + "id": "829112572816130058", + "username": "Char", + "globalName": "Char", + "avatar": null, + "role": "owner", + "permissions": null, + "enabled": true, + "createdAt": 1778938759443, + "updatedAt": 1778939228361, + "createdBy": "system" + } + } +} \ No newline at end of file diff --git a/Data/supportbot.db b/Data/supportbot.db new file mode 100644 index 0000000..46c6891 Binary files /dev/null and b/Data/supportbot.db differ diff --git a/Events/Models/claude.js b/Events/Models/claude.js index 2b38198..af85e12 100644 --- a/Events/Models/claude.js +++ b/Events/Models/claude.js @@ -5,9 +5,7 @@ const fs = require("fs"); const yaml = require("js-yaml"); const axios = require("axios"); -const supportbotai = yaml.load( - fs.readFileSync("./Configs/supportbot-ai.yml", "utf8"), -); +const supportbotai = require("../../Structures/ConfigStore").supportbotAi; function extractTextFromMessage(message) { if (!message) return ""; diff --git a/Events/Models/groq.js b/Events/Models/groq.js index 8eab0ca..f44d126 100644 --- a/Events/Models/groq.js +++ b/Events/Models/groq.js @@ -5,9 +5,7 @@ const fs = require("fs"); const yaml = require("js-yaml"); const OpenAI = require("openai"); -const supportbotai = yaml.load( - fs.readFileSync("./Configs/supportbot-ai.yml", "utf8"), -); +const supportbotai = require("../../Structures/ConfigStore").supportbotAi; const client = new OpenAI({ apiKey: supportbotai.General.Model_API_Key, diff --git a/Events/Models/openai.js b/Events/Models/openai.js index 0df490f..8b29116 100644 --- a/Events/Models/openai.js +++ b/Events/Models/openai.js @@ -5,9 +5,7 @@ const fs = require("fs"); const yaml = require("js-yaml"); const OpenAI = require("openai"); -const supportbotai = yaml.load( - fs.readFileSync("./Configs/supportbot-ai.yml", "utf8"), -); +const supportbotai = require("../../Structures/ConfigStore").supportbotAi; const client = new OpenAI({ apiKey: supportbotai.General.Model_API_Key, diff --git a/Events/guildCreate.js b/Events/guildCreate.js index 16bd038..3365140 100644 --- a/Events/guildCreate.js +++ b/Events/guildCreate.js @@ -1,9 +1,31 @@ const Event = require("../Structures/Event.js"); -module.exports = new Event("guildCreate", async (client) => { - console.log( - `\u001b[31m`, - `${client.user.username} is not in the correct server set in your config. Please leave all other servers and restart the bot.` +const { + getConfiguredGuildId, + syncGuildHealth, + logGuildWarning, +} = require("../Structures/GuildManager.js"); + +module.exports = new Event("guildCreate", async (client, guild) => { + const configuredId = getConfiguredGuildId(); + + if (!configuredId) { + logGuildWarning( + client, + `Joined "${guild.name}" (${guild.id}). Set General.GuildId in config to lock the bot to one server.`, + ); + syncGuildHealth(client); + return; + } + + if (guild.id === configuredId) { + syncGuildHealth(client); + return; + } + + logGuildWarning( + client, + `Joined unauthorized server "${guild.name}" (${guild.id}). Leave it from the dashboard or remove the invite.`, ); - console.log(`\u001b[31m`, `${client.user.username} will now exit.`); - return process.exit(1); + + syncGuildHealth(client); }); diff --git a/Events/guildDelete.js b/Events/guildDelete.js index d3cfbd2..8fde2a7 100644 --- a/Events/guildDelete.js +++ b/Events/guildDelete.js @@ -1,10 +1,23 @@ const Event = require("../Structures/Event.js"); +const { + getConfiguredGuildId, + syncGuildHealth, + logGuildWarning, +} = require("../Structures/GuildManager.js"); -module.exports = new Event("guildDelete", async (client) => { - console.log( - `\u001b[31m`, - `${client.user.username} is not in the correct server set in your config. Please join the server and restart the bot.` - ); - console.log(`\u001b[31m`, `${client.user.username} will now exit.`); - return process.exit(1); +module.exports = new Event("guildDelete", async (client, guild) => { + const configuredId = getConfiguredGuildId(); + + if (configuredId && guild.id === configuredId) { + logGuildWarning( + client, + `Left the configured server "${guild.name}" (${guild.id}). Re-invite the bot or update General.GuildId.`, + ); + } else if (!configuredId) { + logGuildWarning(client, `Left server "${guild.name}" (${guild.id}).`); + } else { + logGuildWarning(client, `Left server "${guild.name}" (${guild.id}).`); + } + + syncGuildHealth(client); }); diff --git a/Events/guildMemberAdd.js b/Events/guildMemberAdd.js index d37fefa..9155eef 100644 --- a/Events/guildMemberAdd.js +++ b/Events/guildMemberAdd.js @@ -10,16 +10,10 @@ const { ThumbnailBuilder, MediaGalleryBuilder } = require("discord.js"); -const fs = require("fs"); -const yaml = require("js-yaml"); const Event = require("../Structures/Event.js"); -const supportbot = yaml.load( - fs.readFileSync("./Configs/supportbot.yml", "utf8") -); -const msgconfig = yaml.load( - fs.readFileSync("./Configs/messages.yml", "utf8") -); +const supportbot = require("../Structures/ConfigStore").supportbot; +const msgconfig = require("../Structures/ConfigStore").messages; function getWelcomeThumbnail(client, member, iconCfg) { switch ((iconCfg?.Mode || "").toUpperCase()) { diff --git a/Events/guildMemberRemove.js b/Events/guildMemberRemove.js index f4d1bb2..556a966 100644 --- a/Events/guildMemberRemove.js +++ b/Events/guildMemberRemove.js @@ -3,8 +3,8 @@ const fs = require("fs"); const yaml = require("js-yaml"); const Event = require("../Structures/Event.js"); -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); +const supportbot = require("../Structures/ConfigStore").supportbot; +const msgconfig = require("../Structures/ConfigStore").messages; module.exports = new Event("guildMemberRemove", async (client, member) => { diff --git a/Events/interactionCreate.js b/Events/interactionCreate.js index 54b32d2..c05a948 100644 --- a/Events/interactionCreate.js +++ b/Events/interactionCreate.js @@ -1,12 +1,8 @@ -const fs = require("fs"); const Discord = require("discord.js"); -const yaml = require("js-yaml"); -const supportbot = yaml.load( - fs.readFileSync("./Configs/supportbot.yml", "utf8"), -); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); -const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); +const supportbot = require("../Structures/ConfigStore").supportbot; +const cmdconfig = require("../Structures/ConfigStore").commands; +const msgconfig = require("../Structures/ConfigStore").messages; const Event = require("../Structures/Event.js"); const db = require("../Structures/Database.js"); diff --git a/Events/messageCreate.js b/Events/messageCreate.js index 35506b1..0d0a288 100644 --- a/Events/messageCreate.js +++ b/Events/messageCreate.js @@ -1,14 +1,8 @@ -const fs = require("fs"); const Discord = require("discord.js"); -const yaml = require("js-yaml"); const axios = require("axios"); -const supportbotai = yaml.load( - fs.readFileSync("./Configs/supportbot-ai.yml", "utf8"), -); -const supportbot = yaml.load( - fs.readFileSync("./Configs/supportbot.yml", "utf8"), -); +const supportbotai = require("../Structures/ConfigStore").supportbotAi; +const supportbot = require("../Structures/ConfigStore").supportbot; const Event = require("../Structures/Event.js"); const AIDatabase = require("../Structures/AIDatabase.js"); diff --git a/Events/messageDelete.js b/Events/messageDelete.js index 5490f9c..d2d2b5d 100644 --- a/Events/messageDelete.js +++ b/Events/messageDelete.js @@ -3,8 +3,8 @@ const fs = require("fs"); const yaml = require("js-yaml"); const Event = require("../Structures/Event.js"); -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); +const supportbot = require("../Structures/ConfigStore").supportbot; +const msgconfig = require("../Structures/ConfigStore").messages; module.exports = new Event("messageDelete", async (client, message) => { if (!message.guild) return; diff --git a/Events/messageReactionAdd.js b/Events/messageReactionAdd.js index 25e6508..fe9614e 100644 --- a/Events/messageReactionAdd.js +++ b/Events/messageReactionAdd.js @@ -1,13 +1,6 @@ -const fs = require("fs"); -const yaml = require("js-yaml"); - -const supportbot = yaml.load( - fs.readFileSync("./Configs/supportbot.yml", "utf8"), -); -const supportbotai = yaml.load( - fs.readFileSync("./Configs/supportbot-ai.yml", "utf8"), -); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); +const supportbot = require("../Structures/ConfigStore").supportbot; +const supportbotai = require("../Structures/ConfigStore").supportbotAi; +const cmdconfig = require("../Structures/ConfigStore").commands; const Event = require("../Structures/Event.js"); diff --git a/Events/messageUpdate.js b/Events/messageUpdate.js index 4fac6a0..ff963ee 100644 --- a/Events/messageUpdate.js +++ b/Events/messageUpdate.js @@ -4,8 +4,8 @@ const yaml = require("js-yaml"); const Event = require("../Structures/Event.js"); // Load the configuration files -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); +const supportbot = require("../Structures/ConfigStore").supportbot; +const msgconfig = require("../Structures/ConfigStore").messages; module.exports = new Event("messageUpdate", async (client, oldMessage, newMessage) => { diff --git a/Events/ready.js b/Events/ready.js index ccbb15c..2db3a7f 100644 --- a/Events/ready.js +++ b/Events/ready.js @@ -1,126 +1,8 @@ -const fs = require("fs"); -const Discord = require("discord.js"); -const yaml = require("js-yaml"); const Event = require("../Structures/Event.js"); - -// Load configs -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); - -const ACTIVITY_TYPES = { - PLAYING: Discord.ActivityType.Playing, - WATCHING: Discord.ActivityType.Watching, - LISTENING: Discord.ActivityType.Listening, - COMPETING: Discord.ActivityType.Competing -}; - -const setActivity = (client, type, status) => { - const activityType = ACTIVITY_TYPES[type?.toUpperCase()]; - if (!activityType) return; - - client.user.setPresence({ - activities: [{ name: status || "Online", type: activityType }], - status: "online" - }); -}; - -// simple colors -const c = { - reset: "\x1b[0m", - gray: "\x1b[90m", - green: "\x1b[32m", - yellow: "\x1b[33m", - cyan: "\x1b[36m", - bold: "\x1b[1m" -}; - -function line() { - console.log(`${c.gray}────────────────────────────────────────────────────────${c.reset}`); -} - -function section(title) { - console.log(`\n${c.green}${title}${c.reset}`); - console.log(`${c.gray}${"-".repeat(title.length)}${c.reset}`); -} - -function info(label, value) { - console.log(`${c.gray}${label.padEnd(14)}${c.reset} ${value}`); -} - -function status(label, ok, details = "") { - const icon = ok ? `${c.green}āœ“${c.reset}` : `${c.yellow}!${c.reset}`; - console.log(` ${icon} ${label}${details ? ` (${details})` : ""}`); -} - -const logStartup = async (client) => { - console.clear(); - - const botName = supportbot?.General?.Name || "SupportBot"; - const botVersion = supportbot?.SupportBot_Version || "Unknown"; - - const inviteUrl = `https://discord.com/api/oauth2/authorize?client_id=${client.user.id}&permissions=8&scope=bot%20applications.commands`; - - line(); - console.log(`${c.bold}${c.green}SupportBot${c.reset}`); - console.log(`${c.gray}Emerald Services${c.reset}`); - line(); - - section("System"); - info("Bot Name", botName); - info("Version", botVersion); - info("Node.js", process.version); - info("Discord.js", `v${Discord.version}`); - info("Logged in as", client.user.tag); - info("Guilds", `${client.guilds.cache.size}`); - - section("Links"); - info("Docs", "https://emerald-services.gitbook.io/"); - info("Discord", "https://dsc.gg/emerald-dev"); - info("Invite", inviteUrl); - - console.log(""); -}; - -const checkConfigs = async (client) => { - const guild = client.guilds.cache.first(); - if (!guild) return; - - section("Config Check"); - - const roles = [ - supportbot?.Roles?.StaffMember?.Admin, - supportbot?.Roles?.StaffMember?.Staff, - supportbot?.Roles?.AutoRole?.Role - ].filter(Boolean); - - const channels = [ - supportbot?.Ticket?.TicketHome - ].filter(Boolean); - - const [roleResults, channelResults] = await Promise.all([ - Promise.all(roles.map((role) => client.getRole(role, guild))), - Promise.all(channels.map((channel) => client.getChannel(channel, guild))) - ]); - - const missingRoles = roles.filter((_, i) => !roleResults[i]); - const missingChannels = channels.filter((_, i) => !channelResults[i]); - - if (!missingRoles.length && !missingChannels.length) { - status("All configurations validated successfully", true); - } else { - if (missingRoles.length) { - status("Missing roles", false, missingRoles.join(", ")); - } - - if (missingChannels.length) { - status("Missing channels", false, missingChannels.join(", ")); - } - } - - console.log(""); -}; +const { runBotReady } = require("../Structures/BotStartup.js"); module.exports = new Event("clientReady", async (client) => { - setActivity(client, supportbot?.Activity?.Type, supportbot?.Activity?.Status); - await logStartup(client); - await checkConfigs(client); -}); \ No newline at end of file + // restartDiscordBot runs startup manually — clientReady often does not re-fire anyway + if (client.__explicitStartup) return; + await runBotReady(client, { clearConsole: true, logCommands: false }); +}); diff --git a/README.md b/README.md index e40b8c3..4630bb3 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,193 @@ -

-
- -
- SupportBot -
-

+

+ + SupportBot Dashboard — manage tickets, monitor activity, and keep your server running great + +

-
- - - shield.png - - - - shield.png - - - - shield.png - - - - shield.png - - -## Supported Versions - -| SupportBot | Node.JS | Discord.JS | Supported | -|------------|---------|------------|------------| -| 8.x | 22.x | 14.x. | āœ… Yes -| 7.7.3 | 16.x | 13.x. | āŒ No -| 7.x | 16.x | 13.x. | āŒ No -| 6.x | 14.x | 13.x. | āŒ No -| 5.x | 12.x | 13.x. | āŒ No -| 4.x | 11.x | 13.x. | āŒ No -| Older | 10.x | 13.x. | āŒ No - -
+

SupportBot Dashboard

- Website - • - Wiki -
- About - • - Support - • - License - • - Third-Party - • - Contributors + An unofficial fork of Emerald-Services/SupportBot with a built-in, self-hosted web dashboard.

+

+ + MIT License + + + discord.js v14 + + + Node.js 22.x + + + Upstream SupportBot + +

+ +

+ About Ā· + Features Ā· + Requirements Ā· + Getting started Ā· + Dashboard access Ā· + Configuration Ā· + Addons Ā· + Development Ā· + Upstream & credits Ā· + License +

+ +--- + ## About -SupportBot is an Open-Source discord bot built with [discord.js](https://github.com/discordjs/discord.js). It's primarily focused to help servers organise and manage their support system in a unique way with full customisation to match your brand. +This repository is a **community fork** of [SupportBot](https://github.com/Emerald-Services/SupportBot) — the open-source Discord support/ticket bot by [Emerald Services](https://emeraldsrv.dev). It is **not** affiliated with or endorsed by Emerald Services. -## Support +What this fork adds is a **self-hosted dashboard**: a modern web UI and REST API that run in the **same Node process** as the bot. You configure tickets, messages, commands, AI settings, addons, and more from the browser instead of editing YAML by hand. The API starts immediately on launch, so the dashboard stays reachable even while the bot is connecting or restarting. - - Discord - +If you only need the original bot without the dashboard, use the [official SupportBot repository](https://github.com/Emerald-Services/SupportBot). +## Features + +### Self-hosted dashboard + +| Area | What you get | +|------|----------------| +| **Overview** | Live stats — servers, members, open/closed tickets, module setup status, RAM/CPU/uptime | +| **Visual config editors** | Edit `supportbot`, ticket panel, commands, messages, and AI config with form fields instead of raw YAML | +| **Discord pickers** | Choose channels, roles, and categories from dropdowns when configuring the bot (no copy-pasting IDs) | +| **Guild health** | Warnings if the bot is in the wrong server or extra guilds; quick link to fix `GuildId` or leave stray servers | +| **Logs** | Browse Output, Warn, and Error logs from the dashboard | +| **Transcripts** | View saved ticket HTML transcripts | +| **Notifications** | In-app alerts for bot restarts, update checks, and installs | +| **Settings** | Check for dashboard updates and apply them from the UI | +| **Users & roles** | Invite staff via Discord OAuth; assign owner, admin, editor, viewer, or custom per-page permissions | +| **Addons** | Browse and install addons from the [official Addons repo](https://github.com/Emerald-Services/Addons); edit addon configs in the sidebar like other config files | + +### Bot & API (unchanged upstream behaviour, plus improvements) + +- Full **SupportBot v26** ticket system, slash commands, AI assistant, and addon support +- **Discord OAuth** login for the dashboard (no shared API secret in the browser) +- **API key** (`SecretKey` in `api.yml`) still works for automation and scripts +- Config saves can **restart the Discord bot** while keeping the dashboard and API online +- Timeouts and caching so the UI does not hang when Discord or GitHub is slow + +## Requirements + +| Component | Version | +|-----------|---------| +| **Node.js** | 22.x (matches [SupportBot 8.x](https://github.com/Emerald-Services/SupportBot)) | +| **discord.js** | 14.x (installed via `package.json`) | +| **Discord bot** | Application with bot token and required intents | +| **Discord OAuth app** | For dashboard login (optional but recommended) | + +You need a machine or VPS that can run Node and reach Discord’s API (and GitHub, if you use in-dashboard updates or the addon marketplace). + +## Getting started + +### 1. Clone and install + +```bash +git clone https://github.com/C-h-a-r/SupportBot-Dashboard.git +cd SupportBot-Dashboard +npm install +``` + +### 2. Configure the bot + +Edit `Configs/supportbot.yml`: + +- Set `General.Token` to your bot token +- Set `General.GuildId` to your Discord server ID +- Enable addons if you use them: `General.Addons.Enabled: true` + +Other YAML files in `Configs/` can stay at defaults until you open the dashboard. + +### 3. Configure the API & dashboard + +Edit `Configs/api.yml`: + +```yaml +API: + Enabled: true + Port: 3000 + SecretKey: "generate-a-long-random-string" + OAuth: + Enabled: true + ClientId: "YOUR_DISCORD_APP_CLIENT_ID" + ClientSecret: "YOUR_DISCORD_APP_CLIENT_SECRET" + RedirectUri: "http://localhost:3000/api/auth/discord/callback" + OwnerUserIds: + - "YOUR_DISCORD_USER_ID" +``` + +**Discord Developer Portal** + +1. Create an application at [discord.com/developers/applications](https://discord.com/developers/applications) +2. Under **OAuth2 → Redirects**, add the exact `RedirectUri` (e.g. `http://localhost:3000/api/auth/discord/callback`) +3. Use the same app’s **Client ID** and **Client Secret** in `api.yml` +4. Put your Discord user ID in `OwnerUserIds` for full dashboard access + +For production, change `RedirectUri` to your public URL (e.g. `https://bot.example.com/api/auth/discord/callback`) and use HTTPS. + +### 4. Start the bot + +```bash +npm start +``` + +When startup succeeds you should see: + +- `[Dashboard] API listening on http://localhost:3000` +- Discord bot login and slash command registration + +Open **http://localhost:3000** in your browser and sign in with Discord. + +The web UI is shipped as static files in `public/dashboard/` (included in this repository). You do not need a separate frontend build step. + +> **Tip:** On first run, invite the bot to your server with the permissions SupportBot needs. Set `General.GuildId` to that server’s ID so channel/role pickers and guild health checks work. + +## Dashboard access + +| Method | Use case | +|--------|----------| +| **Discord OAuth** | Normal use — open the site, click **Continue with Discord**, sign in as an owner or invited user | +| **Bearer token** | Scripts and automation — `Authorization: Bearer ` on `/api/*` routes | + +Owners listed in `api.yml` (`OwnerUserIds`) always have full access. Additional users can be added under **Users** in the dashboard with granular permissions per config file and page. + +## Configuration + +| File | Purpose | +|------|---------| +| `Configs/supportbot.yml` | Bot token, guild, staff roles, ticket channels, embed colours, etc. | +| `Configs/ticket-panel.yml` | Ticket panel layout and buttons | +| `Configs/commands.yml` | Enable/disable slash commands | +| `Configs/messages.yml` | User-facing message strings | +| `Configs/supportbot-ai.yml` | AI assistant settings | +| `Configs/api.yml` | Dashboard port, session secret, OAuth | +| `Addons/Configs/*` | Per-addon config (after installing from **Addons** page) | + +Most of these are editable from the sidebar under **Tickets & support**, **Community**, **Server control**, and **Addons**. + +## Addons + +1. Open **Addons** in the dashboard (or install manually into `Addons/` as in the [upstream docs](https://github.com/Emerald-Services/Addons)) +2. Click **Install** on an addon from the marketplace — files are copied into `Addons/` and `Addons/Configs/`, then the bot restarts +3. Edit the addon’s config from the **Addons** section in the left sidebar (same editor as core configs) +4. Ensure `General.Addons.Enabled` is `true` in bot config + +Official addon list: [github.com/Emerald-Services/Addons](https://github.com/Emerald-Services/Addons) + +## Upstream & credits + +- **Original project:** [Emerald-Services/SupportBot](https://github.com/Emerald-Services/SupportBot) — MIT license, ticket bot for Discord +- **Official addons:** [Emerald-Services/Addons](https://github.com/Emerald-Services/Addons) +- **Community:** [Emerald Discord](https://emeraldsrv.dev/discord) Ā· [Wiki](https://github.com/Emerald-Services/SupportBot/wiki) + +This fork is maintained independently. For bugs in **core ticket/command behaviour** that also exist upstream, consider reporting to the [official repo](https://github.com/Emerald-Services/SupportBot/issues). For **dashboard-specific** issues, use this repository’s issues. ## License -Released under the [MIT](https://opensource.org/licenses/MIT) license. - - -## Third-Party - -- [Official addons repository](https://github.com/Emerald-Services/Addons/) -- [Syphers Addons](https://github.com/SypherRed/SB_Addons_Unofficial) -- [BuiltByBit]([https://builtbybit.com/supportbot](https://builtbybit.com/resources/supportbot-advanced-ticket-bot.8940/)) -- [Polymart](https://polymart.org/resource/supportbot-1-discord-ticket-bot.518) - - -## Contributors - - - - - -## Hosting - -You can use our [Pterodactyl egg](https://github.com/Emerald-Services/Pterodactyl-Egg) to host it on your own panel. We currently don't offer hosting for you, altough we might add this in the future. -If you don't know how to install pterodactyl, you could always host it on replit: - -[![Run on Repl.it](https://repl.it/badge/github/ItsNyoty/IKI-bot)]([https://repl.it/github/ItsNyoty/iki](https://repl.it/github/Emerald-Services/SupportBot -) - - - +Released under the [MIT License](https://opensource.org/licenses/MIT), same as upstream SupportBot. diff --git a/Structures/AddonCatalog.js b/Structures/AddonCatalog.js new file mode 100644 index 0000000..b43ddac --- /dev/null +++ b/Structures/AddonCatalog.js @@ -0,0 +1,270 @@ +const fs = require("fs"); +const path = require("path"); +const axios = require("axios"); +const { withTimeout } = require("./asyncTimeout.js"); + +const ADDONS_REPO = "Emerald-Services/Addons"; +const ADDONS_BRANCH = "main"; +const GITHUB_API = `https://api.github.com/repos/${ADDONS_REPO}/contents`; +const GITHUB_RAW = `https://raw.githubusercontent.com/${ADDONS_REPO}/${ADDONS_BRANCH}`; + +const README_PATTERN = /^readme\.md$/i; + +function ensureAddonDirs() { + if (!fs.existsSync("./Addons")) { + fs.mkdirSync("./Addons", { recursive: true }); + } + if (!fs.existsSync("./Addons/Configs")) { + fs.mkdirSync("./Addons/Configs", { recursive: true }); + } +} + +async function githubList(dirPath = "") { + const url = dirPath + ? `${GITHUB_API}/${encodeURIComponent(dirPath)}?ref=${ADDONS_BRANCH}` + : `${GITHUB_API}?ref=${ADDONS_BRANCH}`; + + const res = await withTimeout( + axios.get(url, { + headers: { Accept: "application/vnd.github+json" }, + timeout: 15_000, + }), + 18_000, + "GitHub API request timed out", + ); + + return res.data; +} + +async function listCatalogAddons() { + const root = await githubList(""); + const dirs = root.filter((entry) => entry.type === "dir"); + + const addons = []; + for (const dir of dirs) { + const summary = await summarizeAddon(dir.name); + if (summary) addons.push(summary); + } + + return addons.sort((a, b) => a.name.localeCompare(b.name)); +} + +async function summarizeAddon(name) { + const files = await listAddonRepoFiles(name); + const jsFiles = files.filter((f) => f.name.endsWith(".js")); + if (jsFiles.length === 0) return null; + + const configFiles = files + .filter((f) => f.relativePath.startsWith("Configs/")) + .map((f) => f.name); + + const readme = files.find((f) => README_PATTERN.test(f.name)); + + return { + id: name, + name, + description: readme + ? `Community addon from Emerald-Services/Addons` + : `Addon from ${name}`, + repositoryUrl: `https://github.com/${ADDONS_REPO}/tree/${ADDONS_BRANCH}/${name}`, + jsFiles: jsFiles.map((f) => f.name), + configFiles, + fileCount: files.length, + }; +} + +async function listAddonRepoFiles(addonName) { + const files = []; + const queue = [addonName]; + + while (queue.length) { + const dir = queue.shift(); + const entries = await githubList(dir); + + for (const entry of entries) { + if (entry.type === "dir") { + queue.push(entry.path); + continue; + } + if (entry.type !== "file") continue; + if (README_PATTERN.test(entry.name)) continue; + + const relativePath = entry.path.slice(`${addonName}/`.length); + files.push({ + name: entry.name, + path: entry.path, + relativePath, + downloadUrl: entry.download_url, + size: entry.size, + }); + } + } + + return files; +} + +function mapRepoFileToLocal(addonName, relativePath) { + const parts = relativePath.split("/").filter(Boolean); + if (parts[0] === "Configs") { + return path.join("Addons", "Configs", ...parts.slice(1)); + } + if (parts.length === 1 && parts[0].endsWith(".js")) { + return path.join("Addons", parts[0]); + } + return path.join("Addons", ...parts); +} + +async function downloadFile(url) { + const res = await withTimeout( + axios.get(url, { responseType: "arraybuffer", timeout: 30_000 }), + 35_000, + "Addon file download timed out", + ); + return Buffer.from(res.data); +} + +function listLocalAddonJs() { + ensureAddonDirs(); + return fs + .readdirSync("./Addons") + .filter((f) => f.endsWith(".js") && f !== "addon.js"); +} + +function listLocalAddonConfigs() { + ensureAddonDirs(); + const dir = "./Addons/Configs"; + if (!fs.existsSync(dir)) return []; + return fs + .readdirSync(dir) + .filter((f) => f.endsWith(".yml") || f.endsWith(".yaml") || f.endsWith(".json")); +} + +function isAddonInstalled(catalogEntry) { + const localJs = listLocalAddonJs(); + return catalogEntry.jsFiles.every((file) => localJs.includes(file)); +} + +async function listInstalledAddons(catalog) { + const localJs = listLocalAddonJs(); + const installed = []; + + for (const entry of catalog) { + const allPresent = entry.jsFiles.every((file) => localJs.includes(file)); + if (allPresent) { + installed.push({ + id: entry.id, + name: entry.name, + jsFiles: entry.jsFiles, + configFiles: entry.configFiles.filter((f) => { + const local = listLocalAddonConfigs(); + return local.includes(f); + }), + }); + } + } + + const orphanJs = localJs.filter( + (file) => !installed.some((a) => a.jsFiles.includes(file)), + ); + for (const file of orphanJs) { + installed.push({ + id: file.replace(/\.js$/, ""), + name: file.replace(/\.js$/, ""), + jsFiles: [file], + configFiles: [], + custom: true, + }); + } + + return installed; +} + +async function installAddonFromCatalog(addonName) { + const safeName = String(addonName).replace(/[^a-zA-Z0-9_-]/g, ""); + if (!safeName) { + throw new Error("Invalid addon name"); + } + + const summary = await summarizeAddon(safeName); + if (!summary) { + throw new Error(`Addon "${safeName}" was not found in the catalog`); + } + + const files = await listAddonRepoFiles(safeName); + if (files.length === 0) { + throw new Error(`Addon "${safeName}" has no installable files`); + } + + ensureAddonDirs(); + const written = []; + + for (const file of files) { + const localPath = mapRepoFileToLocal(safeName, file.relativePath); + const dir = path.dirname(localPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + const content = await downloadFile(file.downloadUrl); + fs.writeFileSync(localPath, content); + written.push(localPath.replace(/\\/g, "/")); + } + + return { + addon: summary, + filesWritten: written, + }; +} + +function readAddonConfig(filename) { + const safe = path.basename(filename); + if (!/^[a-zA-Z0-9_.-]+\.(yml|yaml|json)$/.test(safe)) { + throw new Error("Invalid config filename"); + } + + const filePath = path.join("./Addons/Configs", safe); + if (!fs.existsSync(filePath)) { + throw new Error("Addon config not found"); + } + + const raw = fs.readFileSync(filePath, "utf8"); + if (safe.endsWith(".json")) { + return { filename: safe, format: "json", data: JSON.parse(raw), raw }; + } + + const yaml = require("js-yaml"); + return { filename: safe, format: "yaml", data: yaml.load(raw) || {}, raw }; +} + +function writeAddonConfig(filename, data) { + const safe = path.basename(filename); + if (!/^[a-zA-Z0-9_.-]+\.(yml|yaml|json)$/.test(safe)) { + throw new Error("Invalid config filename"); + } + + ensureAddonDirs(); + const filePath = path.join("./Addons/Configs", safe); + let content; + + if (safe.endsWith(".json")) { + content = `${JSON.stringify(data, null, 2)}\n`; + } else { + const yaml = require("js-yaml"); + content = yaml.dump(data, { lineWidth: 120, noRefs: true }); + } + + fs.writeFileSync(filePath, content); + return { filename: safe, path: filePath.replace(/\\/g, "/") }; +} + +module.exports = { + ADDONS_REPO, + listCatalogAddons, + listInstalledAddons, + isAddonInstalled, + installAddonFromCatalog, + listLocalAddonConfigs, + readAddonConfig, + writeAddonConfig, + mapRepoFileToLocal, +}; diff --git a/Structures/BotInvite.js b/Structures/BotInvite.js new file mode 100644 index 0000000..95b47a1 --- /dev/null +++ b/Structures/BotInvite.js @@ -0,0 +1,6 @@ +function getBotInviteUrl(clientId) { + if (!clientId) return null; + return `https://discord.com/api/oauth2/authorize?client_id=${clientId}&permissions=8&scope=bot%20applications.commands`; +} + +module.exports = { getBotInviteUrl }; diff --git a/Structures/BotReload.js b/Structures/BotReload.js new file mode 100644 index 0000000..475de29 --- /dev/null +++ b/Structures/BotReload.js @@ -0,0 +1,150 @@ +const fs = require("fs"); +const path = require("path"); +const configStore = require("./ConfigStore.js"); +const { getPrimaryGuild } = require("./GuildManager.js"); +const { + applyPresence, + runBotReady, + logSlashCommandsRegistered, +} = require("./BotStartup.js"); + +function walk(dir, ext = ".js", fileList = []) { + if (!fs.existsSync(dir)) return fileList; + for (const file of fs.readdirSync(dir)) { + const full = path.join(dir, file); + if (fs.statSync(full).isDirectory()) { + walk(full, ext, fileList); + } else if (file.endsWith(ext)) { + fileList.push(full.replace(/\\/g, "/")); + } + } + return fileList; +} + +async function refreshSlashCommands(client) { + if (!client?.application) return { count: 0 }; + + const cmdconfig = configStore.commands; + const commandFiles = walk("./Commands"); + client.commands.clear(); + + for (const file of commandFiles) { + const resolved = path.resolve(file); + if (require.cache[resolved]) { + delete require.cache[resolved]; + } + } + + let count = 0; + for (const file of commandFiles) { + const command = require(path.resolve(file)); + const matchingConfig = Object.values(cmdconfig).find( + (entry) => + entry && + typeof entry === "object" && + entry.Command && + String(entry.Command).toLowerCase() === + String(command.name).toLowerCase(), + ); + + if (matchingConfig && matchingConfig.Enabled === false) { + continue; + } + + client.commands.set(command.name, command); + count++; + } + + const payload = client.commands.map((cmd) => cmd.data ?? cmd); + for (const guild of client.guilds.cache.values()) { + await guild.commands.set(payload); + } + + return { count }; +} + +async function reloadBot(client, options = {}) { + const { fileKey, refreshCommands = true } = options; + + let reloaded; + if (fileKey) { + configStore.reloadFile(fileKey); + reloaded = [fileKey]; + } else { + reloaded = configStore.reloadBotConfigs(); + } + + applyPresence(client); + + let commands = null; + if (refreshCommands && (!fileKey || fileKey === "commands")) { + commands = await refreshSlashCommands(client); + } + + return { + reloaded, + commands, + }; +} + +/** + * Reload configs from disk, reconnect the Discord session, and refresh presence/commands. + * API and dashboard keep running on the same process. + */ +async function restartDiscordBot(client) { + if (!client) { + const err = new Error("Bot client not available"); + err.code = "NO_CLIENT"; + throw err; + } + + const reloaded = configStore.reloadBotConfigs(); + const token = configStore.supportbot?.General?.Token; + + if (!token || String(token).trim() === "" || token === "BOT_TOKEN") { + const err = new Error("Invalid or missing bot token in supportbot.yml"); + err.code = "INVALID_TOKEN"; + throw err; + } + + console.log("[BotReload] Restarting Discord bot after config change…"); + client.__restarting = true; + + if (client.user) { + try { + await client.destroy(); + } catch (destroyErr) { + console.warn("[BotReload] destroy:", destroyErr.message); + } + } + + client.__explicitStartup = true; + try { + try { + await client.login(token); + } catch (loginErr) { + console.error("[BotReload] login failed:", loginErr.message); + const err = new Error(`Could not start bot: ${loginErr.message}`); + err.code = "LOGIN_FAILED"; + throw err; + } + + // clientReady does not fire again after destroy()+login() — run startup explicitly + await runBotReady(client, { clearConsole: true, logCommands: false }); + const commands = await refreshSlashCommands(client); + logSlashCommandsRegistered(client); + + console.log("[BotReload] Bot restarted successfully"); + return { restarted: true, reloaded, commands }; + } finally { + client.__explicitStartup = false; + client.__restarting = false; + } +} + +module.exports = { + applyPresence, + refreshSlashCommands, + reloadBot, + restartDiscordBot, +}; diff --git a/Structures/BotStartup.js b/Structures/BotStartup.js new file mode 100644 index 0000000..97b5522 --- /dev/null +++ b/Structures/BotStartup.js @@ -0,0 +1,174 @@ +const Discord = require("discord.js"); +const configStore = require("./ConfigStore.js"); +const { + getPrimaryGuild, + syncGuildHealth, +} = require("./GuildManager.js"); + +const ACTIVITY_TYPES = { + PLAYING: Discord.ActivityType.Playing, + WATCHING: Discord.ActivityType.Watching, + LISTENING: Discord.ActivityType.Listening, + COMPETING: Discord.ActivityType.Competing, + STREAMING: Discord.ActivityType.Streaming, +}; + +function applyPresence(client) { + if (!client?.user) return; + + const activity = configStore.supportbot?.Activity; + const typeKey = activity?.Type?.toUpperCase(); + const type = ACTIVITY_TYPES[typeKey] ?? Discord.ActivityType.Playing; + const name = activity?.Status || "Online"; + + const activityPayload = { name, type }; + + if (typeKey === "STREAMING" && activity?.StreamingURL) { + activityPayload.url = activity.StreamingURL; + } + + client.user.setPresence({ + activities: [activityPayload], + status: "online", + }); +} + +const c = { + reset: "\x1b[0m", + gray: "\x1b[90m", + green: "\x1b[32m", + yellow: "\x1b[33m", + bold: "\x1b[1m", + white: "\x1b[37m", +}; + +function line() { + console.log( + `${c.gray}────────────────────────────────────────────────────────${c.reset}`, + ); +} + +function section(title) { + console.log(`\n${c.green}${title}${c.reset}`); + console.log(`${c.gray}${"-".repeat(title.length)}${c.reset}`); +} + +function info(label, value) { + console.log(`${c.gray}${label.padEnd(14)}${c.reset} ${value}`); +} + +function status(label, ok, details = "") { + const icon = ok ? `${c.green}āœ“${c.reset}` : `${c.yellow}!${c.reset}`; + console.log(` ${icon} ${label}${details ? ` (${details})` : ""}`); +} + +async function logStartup(client, options = {}) { + const { clearConsole = true } = options; + const supportbot = configStore.supportbot; + + if (clearConsole) { + console.clear(); + } + + const botName = supportbot?.General?.Name || "SupportBot"; + const botVersion = supportbot?.SupportBot_Version || "Unknown"; + const { getBotInviteUrl } = require("./BotInvite.js"); + const inviteUrl = getBotInviteUrl(client.user.id); + + line(); + console.log(`${c.bold}${c.green}SupportBot${c.reset}`); + console.log(`${c.gray}Emerald Services${c.reset}`); + line(); + + section("System"); + info("Bot Name", botName); + info("Version", botVersion); + info("Node.js", process.version); + info("Discord.js", `v${Discord.version}`); + info("Logged in as", client.user.tag); + info("Guilds", `${client.guilds.cache.size}`); + + section("Links"); + info("Docs", "https://emerald-services.gitbook.io/"); + info("Discord", "https://dsc.gg/emerald-dev"); + info("Invite", inviteUrl); + + console.log(""); +} + +async function checkConfigs(client) { + const supportbot = configStore.supportbot; + const health = syncGuildHealth(client); + + if (health.status !== "ok") { + section("Guild"); + status("Guild setup", false, health.message || health.status); + console.log(""); + } + + const guild = getPrimaryGuild(client); + if (!guild) return; + + section("Config Check"); + + const roles = [ + supportbot?.Roles?.StaffMember?.Admin, + supportbot?.Roles?.StaffMember?.Staff, + supportbot?.Roles?.AutoRole?.Role, + ].filter(Boolean); + + const channels = [supportbot?.Ticket?.TicketHome].filter(Boolean); + + const [roleResults, channelResults] = await Promise.all([ + Promise.all(roles.map((role) => client.getRole(role, guild))), + Promise.all(channels.map((channel) => client.getChannel(channel, guild))), + ]); + + const missingRoles = roles.filter((_, i) => !roleResults[i]); + const missingChannels = channels.filter((_, i) => !channelResults[i]); + + if (!missingRoles.length && !missingChannels.length) { + status("All configurations validated successfully", true); + } else { + if (missingRoles.length) { + status("Missing roles", false, missingRoles.join(", ")); + } + if (missingChannels.length) { + status("Missing channels", false, missingChannels.join(", ")); + } + } + + console.log(""); +} + +function logSlashCommandsRegistered(client) { + const guild = getPrimaryGuild(client); + if (!guild) return; + console.log( + `\n${c.green}āœ“${c.reset} ${c.white}Slash Commands Registered${c.reset} ${c.gray}for ${guild.name}${c.reset}`, + ); +} + +/** + * Same work as Events/ready.js — safe to call after config reload reconnects. + */ +async function runBotReady(client, options = {}) { + const { clearConsole = true, logCommands = false } = options; + + syncGuildHealth(client); + applyPresence(client); + await logStartup(client, { clearConsole }); + await checkConfigs(client); + + if (logCommands) { + logSlashCommandsRegistered(client); + } +} + +module.exports = { + applyPresence, + logStartup, + checkConfigs, + logSlashCommandsRegistered, + runBotReady, +}; diff --git a/Structures/Client.js b/Structures/Client.js index 97392d6..b90734a 100644 --- a/Structures/Client.js +++ b/Structures/Client.js @@ -2,11 +2,12 @@ const fs = require("fs"); const path = require("path"); const Discord = require("discord.js"); const { GatewayIntentBits, Partials } = require("discord.js"); -const yaml = require("js-yaml"); const { Command, Event } = require("./Addon.js"); +const configStore = require("./ConfigStore.js"); +const { getPrimaryGuild } = require("./GuildManager.js"); -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8")); +const supportbot = configStore.supportbot; +const cmdconfig = configStore.commands; const c = { reset: "\x1b[0m", @@ -158,12 +159,12 @@ class Client extends Discord.Client { }); } - this.once("clientReady", async () => { - await this.guilds.cache.first()?.commands.set(this.commands); - - console.log( - `\n${c.green}āœ“${c.reset} ${c.white}Slash Commands Registered${c.reset} ${c.gray}for ${this.guilds.cache.first().name}${c.reset}`, - ); + this.on("clientReady", async () => { + const guild = getPrimaryGuild(this); + if (!guild) return; + await guild.commands.set(this.commands); + const { logSlashCommandsRegistered } = require("./BotStartup.js"); + logSlashCommandsRegistered(this); }); section("Events"); diff --git a/Structures/ConfigStore.js b/Structures/ConfigStore.js new file mode 100644 index 0000000..40a0416 --- /dev/null +++ b/Structures/ConfigStore.js @@ -0,0 +1,89 @@ +// Live config objects — reload mutates in place so existing references stay valid. +const fs = require("fs"); +const path = require("path"); +const yaml = require("js-yaml"); + +const CONFIG_DIR = path.join(__dirname, "../Configs"); + +const FILE_KEYS = [ + "supportbot", + "commands", + "messages", + "ticket-panel", + "supportbot-ai", + "api", +]; + +function readConfig(name) { + return yaml.load(fs.readFileSync(path.join(CONFIG_DIR, `${name}.yml`), "utf8")); +} + +function deepAssign(target, source) { + if (!target || typeof target !== "object" || Array.isArray(target)) { + return source; + } + if (!source || typeof source !== "object" || Array.isArray(source)) { + return source; + } + + for (const key of Object.keys(source)) { + const next = source[key]; + if ( + next && + typeof next === "object" && + !Array.isArray(next) && + target[key] && + typeof target[key] === "object" && + !Array.isArray(target[key]) + ) { + deepAssign(target[key], next); + } else { + target[key] = next; + } + } + + for (const key of Object.keys(target)) { + if (!(key in source)) { + delete target[key]; + } + } + + return target; +} + +const store = {}; +for (const name of FILE_KEYS) { + store[name] = readConfig(name); +} + +function reloadFile(fileKey) { + if (!FILE_KEYS.includes(fileKey)) { + throw new Error(`Unknown config: ${fileKey}`); + } + const fresh = readConfig(fileKey); + deepAssign(store[fileKey], fresh); + return store[fileKey]; +} + +function reloadBotConfigs() { + const reloaded = []; + for (const name of FILE_KEYS) { + if (name === "api") continue; + reloadFile(name); + reloaded.push(name); + } + return reloaded; +} + +module.exports = { + supportbot: store.supportbot, + commands: store.commands, + messages: store.messages, + ticketPanel: store["ticket-panel"], + supportbotAi: store["supportbot-ai"], + api: store.api, + store, + reloadFile, + reloadBotConfigs, + readConfig, +}; diff --git a/Structures/DashboardAuth.js b/Structures/DashboardAuth.js new file mode 100644 index 0000000..089be04 --- /dev/null +++ b/Structures/DashboardAuth.js @@ -0,0 +1,57 @@ +const { fullPermissions, hasPermission, canAccessConfig } = require("./DashboardPermissions.js"); + +function grantServiceAccess(req) { + req.dashboardService = true; + req.dashboardPermissions = fullPermissions(); + req.dashboardRole = "service"; + req.dashboardIsOwner = true; +} + +function forbid(res, message = "Forbidden") { + return res.status(403).json({ success: false, error: message }); +} + +function createPermissionMiddleware(getPermissions) { + return function requirePermission(permission) { + return (req, res, next) => { + if (req.dashboardService) return next(); + const perms = getPermissions(req) || req.dashboardPermissions; + if (hasPermission(perms, permission)) return next(); + return forbid(res); + }; + }; +} + +function createConfigMiddleware(getPermissions) { + return function requireConfigAccess(mode = "view") { + return (req, res, next) => { + if (req.dashboardService) return next(); + const perms = getPermissions(req) || req.dashboardPermissions; + const file = req.params?.file || req.body?.filename; + if (!file) return next(); + if (!canAccessConfig(perms, file, mode)) return forbid(res); + return next(); + }; + }; +} + +function assertConfigFilesEditable(permissions, files, res) { + if (!permissions) return forbid(res); + for (const file of files) { + if (!canAccessConfig(permissions, file, "edit")) { + return forbid(res, `No permission to edit ${file}.yml`); + } + } + return null; +} + +module.exports = { + grantServiceAccess, + forbid, + createPermissionMiddleware, + createConfigMiddleware, + assertConfigFilesEditable, + fullPermissions, + hasPermission, + canAccessConfig, +}; diff --git a/Structures/DashboardPermissions.js b/Structures/DashboardPermissions.js new file mode 100644 index 0000000..af44a07 --- /dev/null +++ b/Structures/DashboardPermissions.js @@ -0,0 +1,134 @@ +const CONFIG_FILES = [ + "supportbot", + "ticket-panel", + "commands", + "messages", + "supportbot-ai", +]; + +const ROLES = ["owner", "admin", "moderator", "editor", "viewer", "custom"]; + +function configMap(view, edit) { + const configs = {}; + for (const file of CONFIG_FILES) { + configs[file] = { view: Boolean(view), edit: Boolean(edit) }; + } + return configs; +} + +function fullPermissions() { + return { + overview: true, + logs: true, + transcripts: true, + settings: { view: true, update: true }, + configs: configMap(true, true), + users: { view: true, manage: true }, + }; +} + +function permissionsFromRole(role, customPermissions) { + switch (role) { + case "owner": + case "admin": + return fullPermissions(); + case "moderator": + return { + overview: true, + logs: true, + transcripts: true, + settings: { view: true, update: false }, + configs: configMap(true, false), + users: { view: false, manage: false }, + }; + case "editor": + return { + overview: true, + logs: true, + transcripts: false, + settings: { view: true, update: false }, + configs: configMap(true, true), + users: { view: false, manage: false }, + }; + case "viewer": + return { + overview: true, + logs: true, + transcripts: true, + settings: { view: true, update: false }, + configs: configMap(true, false), + users: { view: false, manage: false }, + }; + case "custom": + return normalizePermissions(customPermissions); + default: + return permissionsFromRole("viewer"); + } +} + +function normalizePermissions(input) { + const base = permissionsFromRole("viewer"); + if (!input || typeof input !== "object") return base; + + return { + overview: input.overview ?? base.overview, + logs: input.logs ?? base.logs, + transcripts: input.transcripts ?? base.transcripts, + settings: { + view: input.settings?.view ?? base.settings.view, + update: input.settings?.update ?? base.settings.update, + }, + configs: CONFIG_FILES.reduce((acc, file) => { + acc[file] = { + view: input.configs?.[file]?.view ?? base.configs[file].view, + edit: input.configs?.[file]?.edit ?? base.configs[file].edit, + }; + return acc; + }, {}), + users: { + view: input.users?.view ?? base.users.view, + manage: input.users?.manage ?? base.users.manage, + }, + }; +} + +function hasPermission(permissions, check) { + if (!permissions) return false; + + if (check === "overview") return Boolean(permissions.overview); + if (check === "logs") return Boolean(permissions.logs); + if (check === "transcripts") return Boolean(permissions.transcripts); + if (check === "settings.view") return Boolean(permissions.settings?.view); + if (check === "settings.update") return Boolean(permissions.settings?.update); + if (check === "users.view") return Boolean(permissions.users?.view); + if (check === "users.manage") return Boolean(permissions.users?.manage); + + const configView = check.match(/^configs\.([a-z0-9-]+)\.view$/); + if (configView) { + const file = configView[1]; + return Boolean(permissions.configs?.[file]?.view); + } + + const configEdit = check.match(/^configs\.([a-z0-9-]+)\.edit$/); + if (configEdit) { + const file = configEdit[1]; + return Boolean(permissions.configs?.[file]?.edit); + } + + return false; +} + +function canAccessConfig(permissions, file, mode = "view") { + if (!CONFIG_FILES.includes(file)) return false; + return hasPermission(permissions, `configs.${file}.${mode}`); +} + +module.exports = { + CONFIG_FILES, + ROLES, + fullPermissions, + permissionsFromRole, + normalizePermissions, + hasPermission, + canAccessConfig, +}; diff --git a/Structures/DashboardSession.js b/Structures/DashboardSession.js new file mode 100644 index 0000000..0363701 --- /dev/null +++ b/Structures/DashboardSession.js @@ -0,0 +1,71 @@ +const crypto = require("crypto"); + +const SESSION_COOKIE = "sb_dashboard"; +const SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; + +function signSession(payload, secret) { + const body = Buffer.from(JSON.stringify(payload)).toString("base64url"); + const sig = crypto + .createHmac("sha256", secret) + .update(body) + .digest("base64url"); + return `${body}.${sig}`; +} + +function verifySession(token, secret) { + if (!token || typeof token !== "string") return null; + const dot = token.lastIndexOf("."); + if (dot <= 0) return null; + + const body = token.slice(0, dot); + const sig = token.slice(dot + 1); + const expected = crypto + .createHmac("sha256", secret) + .update(body) + .digest("base64url"); + + if (sig.length !== expected.length) return null; + if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { + return null; + } + + try { + const payload = JSON.parse( + Buffer.from(body, "base64url").toString("utf8"), + ); + if (!payload?.userId || !payload?.exp) return null; + if (Date.now() > payload.exp) return null; + return payload; + } catch { + return null; + } +} + +function createSessionPayload(user) { + return { + userId: user.id, + username: user.username, + globalName: user.global_name || null, + avatar: user.avatar || null, + exp: Date.now() + SESSION_MAX_AGE_MS, + }; +} + +function sessionCookieOptions(maxAgeMs = SESSION_MAX_AGE_MS) { + return { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: maxAgeMs, + path: "/", + }; +} + +module.exports = { + SESSION_COOKIE, + SESSION_MAX_AGE_MS, + signSession, + verifySession, + createSessionPayload, + sessionCookieOptions, +}; diff --git a/Structures/DashboardUserStore.js b/Structures/DashboardUserStore.js new file mode 100644 index 0000000..58613d6 --- /dev/null +++ b/Structures/DashboardUserStore.js @@ -0,0 +1,291 @@ +const fs = require("fs"); +const path = require("path"); +const { + CONFIG_FILES, + ROLES, + fullPermissions, + permissionsFromRole, + normalizePermissions, + hasPermission, + canAccessConfig, +} = require("./DashboardPermissions.js"); +const { discordAvatarUrl } = require("./DiscordOAuth.js"); + +const STORE_PATH = path.join(__dirname, "../Data/dashboard-users.json"); + +function ensureDataDir() { + const dir = path.dirname(STORE_PATH); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); +} + +function emptyStore() { + return { version: 1, users: {} }; +} + +function readStore() { + ensureDataDir(); + if (!fs.existsSync(STORE_PATH)) return emptyStore(); + try { + const raw = JSON.parse(fs.readFileSync(STORE_PATH, "utf8")); + if (!raw.users || typeof raw.users !== "object") return emptyStore(); + return raw; + } catch { + return emptyStore(); + } +} + +function writeStore(store) { + ensureDataDir(); + fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2), "utf8"); +} + +function getOwnerIds(oauth) { + const owners = new Set(); + for (const id of oauth?.OwnerUserIds || []) owners.add(String(id)); + for (const id of oauth?.AllowedUserIds || []) owners.add(String(id)); + return owners; +} + +function isYamlOwner(userId, oauth) { + return getOwnerIds(oauth).has(String(userId)); +} + +function bootstrapFromOAuth(oauth) { + const store = readStore(); + const owners = getOwnerIds(oauth); + let changed = false; + + for (const id of owners) { + if (!store.users[id]) { + store.users[id] = { + id, + username: null, + globalName: null, + avatar: null, + role: "owner", + permissions: null, + enabled: true, + createdAt: Date.now(), + updatedAt: Date.now(), + createdBy: "system", + }; + changed = true; + } + } + + if (changed) writeStore(store); + return store; +} + +function syncDiscordProfile(userId, profile) { + const store = readStore(); + const entry = store.users[userId]; + if (!entry) return; + + entry.username = profile.username ?? entry.username; + entry.globalName = profile.globalName ?? entry.globalName; + entry.avatar = profile.avatar ?? entry.avatar; + entry.updatedAt = Date.now(); + writeStore(store); +} + +function resolvePermissions(entry, oauth, userId) { + if (isYamlOwner(userId, oauth)) { + return { permissions: fullPermissions(), role: "owner", isOwner: true }; + } + if (!entry?.enabled) return null; + + const role = entry.role || "viewer"; + const permissions = + role === "custom" + ? permissionsFromRole("custom", entry.permissions) + : permissionsFromRole(role); + + return { permissions, role, isOwner: false }; +} + +function getAccess(userId, oauth) { + bootstrapFromOAuth(oauth); + const store = readStore(); + const id = String(userId); + + if (isYamlOwner(id, oauth)) { + const entry = store.users[id]; + return { + entry: entry || { id, role: "owner" }, + permissions: fullPermissions(), + role: "owner", + isOwner: true, + yamlOwner: true, + }; + } + + const entry = store.users[id]; + const resolved = resolvePermissions(entry, oauth, id); + if (!resolved) return null; + + return { + entry, + permissions: resolved.permissions, + role: resolved.role, + isOwner: false, + yamlOwner: false, + }; +} + +function canLogin(userId, oauth) { + return Boolean(getAccess(userId, oauth)); +} + +function formatPublicUser(entry, access, discordProfile) { + const id = String(entry?.id || discordProfile?.id); + const avatarHash = entry?.avatar ?? discordProfile?.avatar ?? null; + + return { + id, + username: entry?.username || discordProfile?.username || "Unknown", + globalName: entry?.globalName ?? discordProfile?.global_name ?? null, + avatar: avatarHash + ? discordAvatarUrl({ id, avatar: avatarHash }) + : discordProfile + ? discordAvatarUrl(discordProfile) + : discordAvatarUrl({ id, avatar: null }), + role: access.role, + isOwner: access.isOwner, + permissions: access.permissions, + }; +} + +function listUsers(oauth) { + bootstrapFromOAuth(oauth); + const store = readStore(); + const yamlOwners = getOwnerIds(oauth); + + return Object.values(store.users).map((entry) => { + const access = resolvePermissions(entry, oauth, entry.id) || { + permissions: permissionsFromRole("viewer"), + role: entry.role, + isOwner: false, + }; + + return { + id: entry.id, + username: entry.username, + globalName: entry.globalName, + avatar: entry.avatar + ? discordAvatarUrl({ id: entry.id, avatar: entry.avatar }) + : discordAvatarUrl({ id: entry.id, avatar: null }), + role: yamlOwners.has(String(entry.id)) ? "owner" : entry.role, + permissions: + entry.role === "custom" + ? normalizePermissions(entry.permissions) + : access.permissions, + enabled: entry.enabled !== false, + isOwner: yamlOwners.has(String(entry.id)), + yamlOwner: yamlOwners.has(String(entry.id)), + createdAt: entry.createdAt, + updatedAt: entry.updatedAt, + }; + }); +} + +function addUser(payload, actorId, oauth) { + const id = String(payload.id || "").trim(); + if (!/^\d{17,20}$/.test(id)) { + throw new Error("Invalid Discord user ID"); + } + + const role = payload.role || "viewer"; + if (!ROLES.includes(role)) throw new Error("Invalid role"); + if (role === "owner" && !isYamlOwner(actorId, oauth)) { + throw new Error("Only api.yml owners can assign the owner role"); + } + + const store = readStore(); + if (store.users[id]) throw new Error("User already exists"); + + store.users[id] = { + id, + username: payload.username || null, + globalName: payload.globalName || null, + avatar: null, + role, + permissions: + role === "custom" ? normalizePermissions(payload.permissions) : null, + enabled: payload.enabled !== false, + createdAt: Date.now(), + updatedAt: Date.now(), + createdBy: String(actorId), + }; + + writeStore(store); + return listUsers(oauth).find((u) => u.id === id); +} + +function updateUser(id, payload, actorId, oauth) { + const store = readStore(); + const entry = store.users[String(id)]; + if (!entry) throw new Error("User not found"); + + if (isYamlOwner(id, oauth) && payload.enabled === false) { + throw new Error("Cannot disable an owner defined in api.yml"); + } + + if (payload.role) { + if (!ROLES.includes(payload.role)) throw new Error("Invalid role"); + if (payload.role === "owner" && !isYamlOwner(actorId, oauth)) { + throw new Error("Only api.yml owners can assign the owner role"); + } + if (isYamlOwner(id, oauth) && payload.role !== "owner") { + throw new Error("Cannot change role of an api.yml owner"); + } + entry.role = payload.role; + } + + if (payload.enabled !== undefined) entry.enabled = Boolean(payload.enabled); + if (payload.username !== undefined) entry.username = payload.username; + if (payload.globalName !== undefined) entry.globalName = payload.globalName; + + if (payload.permissions !== undefined) { + entry.permissions = normalizePermissions(payload.permissions); + if (entry.role !== "custom") entry.role = "custom"; + } + + if (entry.role === "custom" && payload.permissions === undefined && payload.role !== "custom") { + entry.permissions = null; + } + + entry.updatedAt = Date.now(); + writeStore(store); + return listUsers(oauth).find((u) => u.id === String(id)); +} + +function removeUser(id, oauth) { + if (isYamlOwner(id, oauth)) { + throw new Error("Remove this user from OwnerUserIds in api.yml instead"); + } + + const store = readStore(); + if (!store.users[String(id)]) throw new Error("User not found"); + delete store.users[String(id)]; + writeStore(store); +} + +module.exports = { + CONFIG_FILES, + STORE_PATH, + getOwnerIds, + isYamlOwner, + bootstrapFromOAuth, + syncDiscordProfile, + getAccess, + canLogin, + formatPublicUser, + listUsers, + addUser, + updateUser, + removeUser, + hasPermission, + canAccessConfig, + ROLES, +}; diff --git a/Structures/DiscordOAuth.js b/Structures/DiscordOAuth.js new file mode 100644 index 0000000..70e5bcb --- /dev/null +++ b/Structures/DiscordOAuth.js @@ -0,0 +1,106 @@ +const crypto = require("crypto"); +const axios = require("axios"); + +const DISCORD_API = "https://discord.com/api"; +const OAUTH_STATE_COOKIE = "sb_oauth_state"; +const OAUTH_STATE_MAX_AGE_MS = 10 * 60 * 1000; + +function discordAvatarUrl(user) { + if (user.avatar) { + const ext = user.avatar.startsWith("a_") ? "gif" : "png"; + return `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.${ext}?size=128`; + } + const index = Number((BigInt(user.id) >> 22n) % 6n); + return `https://cdn.discordapp.com/embed/avatars/${index}.png`; +} + +function buildAuthorizeUrl(oauth, state) { + const params = new URLSearchParams({ + client_id: oauth.ClientId, + redirect_uri: oauth.RedirectUri, + response_type: "code", + scope: "identify", + state, + prompt: "none", + }); + return `${DISCORD_API}/oauth2/authorize?${params.toString()}`; +} + +async function exchangeCode(oauth, code) { + const body = new URLSearchParams({ + client_id: oauth.ClientId, + client_secret: oauth.ClientSecret, + grant_type: "authorization_code", + code, + redirect_uri: oauth.RedirectUri, + }); + + const { data } = await axios.post(`${DISCORD_API}/oauth2/token`, body, { + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + timeout: 15000, + }); + + return data.access_token; +} + +async function fetchDiscordUser(accessToken) { + const { data } = await axios.get(`${DISCORD_API}/users/@me`, { + headers: { Authorization: `Bearer ${accessToken}` }, + timeout: 15000, + }); + return data; +} + +function createOAuthState() { + return crypto.randomBytes(24).toString("hex"); +} + +function oauthStateCookieOptions() { + return { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: OAUTH_STATE_MAX_AGE_MS, + path: "/", + }; +} + +function isOAuthConfigured(oauth) { + return Boolean( + oauth?.Enabled && + oauth.ClientId && + oauth.ClientSecret && + oauth.RedirectUri, + ); +} + +function isUserAllowed(userId, oauth) { + const allowed = oauth?.AllowedUserIds; + if (!Array.isArray(allowed) || allowed.length === 0) { + return false; + } + return allowed.map(String).includes(String(userId)); +} + +function formatPublicUser(user) { + return { + id: user.id, + username: user.username, + globalName: user.global_name || null, + avatar: discordAvatarUrl(user), + }; +} + +module.exports = { + OAUTH_STATE_COOKIE, + OAUTH_STATE_MAX_AGE_MS, + buildAuthorizeUrl, + exchangeCode, + fetchDiscordUser, + createOAuthState, + oauthStateCookieOptions, + isOAuthConfigured, + isUserAllowed, + formatPublicUser, + discordAvatarUrl, +}; diff --git a/Structures/GuildManager.js b/Structures/GuildManager.js new file mode 100644 index 0000000..7e9726c --- /dev/null +++ b/Structures/GuildManager.js @@ -0,0 +1,246 @@ +const { ChannelType } = require("discord.js"); +const configStore = require("./ConfigStore.js"); +const { withTimeout } = require("./asyncTimeout.js"); + +const RESOURCES_CACHE_MS = 90 * 1000; +let resourcesCache = { guildId: null, data: null, at: 0 }; +let resourcesInflight = null; + +function getConfiguredGuildId() { + const id = configStore.supportbot?.General?.GuildId; + if (!id || id === "GUILD_ID" || String(id).trim() === "") return null; + return String(id).trim(); +} + +function setConfiguredGuildId(guildId) { + if (!configStore.supportbot.General) { + configStore.supportbot.General = {}; + } + configStore.supportbot.General.GuildId = guildId ? String(guildId) : ""; +} + +function getPrimaryGuild(client) { + if (!client) return null; + + const configuredId = getConfiguredGuildId(); + if (configuredId) { + const match = client.guilds.cache.get(configuredId); + if (match) return match; + } + + return client.guilds.cache.first() || null; +} + +function evaluateGuildHealth(client) { + const configuredId = getConfiguredGuildId(); + const guilds = [...client.guilds.cache.values()]; + + if (!configuredId) { + return { + status: "missing_config", + configuredGuildId: null, + message: + "No guild ID is set. Add your Discord server ID under Bot config → General.", + configuredGuild: null, + extraGuilds: guilds.map(formatGuildSummary), + }; + } + + const configuredGuild = client.guilds.cache.get(configuredId) || null; + const extraGuilds = guilds + .filter((g) => g.id !== configuredId) + .map(formatGuildSummary); + + if (!configuredGuild) { + return { + status: "not_in_guild", + configuredGuildId: configuredId, + message: `The bot is not in the configured server (${configuredId}). Invite it or update the guild ID.`, + configuredGuild: null, + extraGuilds: guilds.map(formatGuildSummary), + }; + } + + if (extraGuilds.length > 0) { + return { + status: "extra_guilds", + configuredGuildId: configuredId, + message: `The bot is in ${extraGuilds.length} extra server(s). Leave them so only your configured server remains.`, + configuredGuild: formatGuildSummary(configuredGuild), + extraGuilds, + }; + } + + return { + status: "ok", + configuredGuildId: configuredId, + message: null, + configuredGuild: formatGuildSummary(configuredGuild), + extraGuilds: [], + }; +} + +function formatGuildSummary(guild) { + return { + id: guild.id, + name: guild.name, + memberCount: guild.memberCount ?? null, + icon: guild.iconURL?.({ size: 128 }) ?? null, + }; +} + +function syncGuildHealth(client) { + if (!client?.user) { + client.guildHealth = { + status: "offline", + configuredGuildId: getConfiguredGuildId(), + message: "Bot is not connected to Discord.", + configuredGuild: null, + extraGuilds: [], + }; + return client.guildHealth; + } + + client.guildHealth = evaluateGuildHealth(client); + return client.guildHealth; +} + +function logGuildWarning(client, message) { + const name = client?.user?.username || "Bot"; + console.warn(`\x1b[33m[Guild]\x1b[0m ${name}: ${message}`); +} + +async function leaveGuild(client, guildId) { + const guild = client.guilds.cache.get(String(guildId)); + if (!guild) { + throw new Error("Bot is not in that server."); + } + + const configuredId = getConfiguredGuildId(); + if (configuredId && guild.id === configuredId) { + throw new Error("Cannot leave the configured server. Change the guild ID first."); + } + + await guild.leave(); + syncGuildHealth(client); + return syncGuildHealth(client); +} + +function buildResourcesFromGuild(guild) { + const roles = [...guild.roles.cache.values()] + .filter((r) => r.id !== guild.id) + .sort((a, b) => b.position - a.position) + .map((r) => ({ + id: r.id, + name: r.name, + color: r.hexColor || null, + })); + + const channels = [...guild.channels.cache.values()] + .filter( + (c) => + c.type === ChannelType.GuildText || + c.type === ChannelType.GuildVoice || + c.type === ChannelType.GuildAnnouncement || + c.type === ChannelType.GuildStageVoice || + c.type === ChannelType.GuildForum, + ) + .sort((a, b) => a.name.localeCompare(b.name)) + .map((c) => ({ + id: c.id, + name: c.name, + type: c.type, + })); + + const categories = [...guild.channels.cache.values()] + .filter((c) => c.type === ChannelType.GuildCategory) + .sort((a, b) => a.name.localeCompare(b.name)) + .map((c) => ({ + id: c.id, + name: c.name, + })); + + return { + guildId: guild.id, + guildName: guild.name, + roles, + channels, + categories, + }; +} + +async function fetchGuildResourcesFromDiscord(client, guildId) { + const id = guildId || getConfiguredGuildId(); + if (!id) return null; + + const guild = client.guilds.cache.get(id); + if (!guild) return null; + + try { + await withTimeout( + Promise.all([guild.channels.fetch(), guild.roles.fetch()]), + 12_000, + "Discord channel/role fetch timed out", + ); + } catch (err) { + console.warn("[Guild] Failed to fetch channels/roles:", err.message); + if (guild.roles.cache.size === 0 && guild.channels.cache.size === 0) { + return null; + } + } + + return buildResourcesFromGuild(guild); +} + +async function fetchGuildResources(client, guildId) { + const id = guildId || getConfiguredGuildId(); + if (!id || !client?.user) return null; + + const now = Date.now(); + if ( + resourcesCache.data && + resourcesCache.guildId === id && + now - resourcesCache.at < RESOURCES_CACHE_MS + ) { + return resourcesCache.data; + } + + if (resourcesInflight) { + try { + return await resourcesInflight; + } catch { + /* fall through to retry */ + } + } + + resourcesInflight = (async () => { + const data = await fetchGuildResourcesFromDiscord(client, id); + if (data) { + resourcesCache = { guildId: id, data, at: Date.now() }; + } + return data; + })(); + + try { + return await resourcesInflight; + } finally { + resourcesInflight = null; + } +} + +function clearGuildResourcesCache() { + resourcesCache = { guildId: null, data: null, at: 0 }; + resourcesInflight = null; +} + +module.exports = { + getConfiguredGuildId, + setConfiguredGuildId, + getPrimaryGuild, + evaluateGuildHealth, + syncGuildHealth, + logGuildWarning, + leaveGuild, + fetchGuildResources, + clearGuildResourcesCache, +}; diff --git a/Structures/LogReader.js b/Structures/LogReader.js new file mode 100644 index 0000000..b0f4dbb --- /dev/null +++ b/Structures/LogReader.js @@ -0,0 +1,181 @@ +const fs = require("fs"); +const path = require("path"); + +const LOG_ROOT = path.resolve(__dirname, "../Logs"); +const LOG_TYPES = ["Output", "Warn", "Error"]; + +function todayDate() { + return new Date().toISOString().split("T")[0]; +} + +function getLogPath(type, date = todayDate()) { + return path.join(LOG_ROOT, type, `${type}-${date}.log`); +} + +/** Prefer today's file; fall back to the newest log in the folder. */ +function resolveLogPath(type) { + const todayPath = getLogPath(type); + if (fs.existsSync(todayPath)) return todayPath; + + const dir = path.join(LOG_ROOT, type); + if (!fs.existsSync(dir)) return todayPath; + + const files = fs + .readdirSync(dir) + .filter((f) => f.endsWith(".log")) + .sort() + .reverse(); + + if (!files.length) return todayPath; + return path.join(dir, files[0]); +} + +function stripAnsi(text) { + return String(text).replace(/\x1b\[[0-9;]*m/g, ""); +} + +function parseLogLine(line, type) { + const trimmed = line.trimEnd(); + if (!trimmed) return null; + + const match = trimmed.match(/^\[([^\]]+)\]\s?(.*)$/); + if (match) { + return { + type, + timestamp: match[1], + message: stripAnsi(match[2]), + }; + } + + return { + type, + timestamp: null, + message: stripAnsi(trimmed), + }; +} + +function readFromByte(filePath, fromByte) { + if (!fs.existsSync(filePath)) { + return { lines: [], position: 0 }; + } + + const stat = fs.statSync(filePath); + if (stat.size === 0) { + return { lines: [], position: 0 }; + } + + const start = Math.max(0, Math.min(fromByte, stat.size)); + if (start >= stat.size) { + return { lines: [], position: stat.size }; + } + + const fd = fs.openSync(filePath, "r"); + const buffer = Buffer.alloc(stat.size - start); + fs.readSync(fd, buffer, 0, buffer.length, start); + fs.closeSync(fd); + + const text = buffer.toString("utf8"); + const lines = text.split(/\r?\n/).filter((l) => l.length > 0); + + return { lines, position: stat.size }; +} + +function readLastLines(filePath, maxLines) { + if (!fs.existsSync(filePath)) { + return { lines: [], position: 0 }; + } + + const stat = fs.statSync(filePath); + if (stat.size === 0) { + return { lines: [], position: 0 }; + } + + const chunkSize = Math.min(stat.size, 512 * 1024); + const start = Math.max(0, stat.size - chunkSize); + const fd = fs.openSync(filePath, "r"); + const buffer = Buffer.alloc(chunkSize); + fs.readSync(fd, buffer, 0, chunkSize, start); + fs.closeSync(fd); + + const lines = buffer + .toString("utf8") + .split(/\r?\n/) + .filter((l) => l.length > 0); + const slice = lines.slice(-maxLines); + + return { lines: slice, position: stat.size }; +} + +/** + * @param {string[]} types + * @param {Record} cursor byte offsets per type + * @param {number} initialLines lines to load when cursor is empty + */ +function fetchLogs(types, cursor = {}, initialLines = 300) { + const validTypes = types.filter((t) => LOG_TYPES.includes(t)); + const entries = []; + const nextCursor = { ...cursor }; + + for (const type of validTypes) { + const filePath = resolveLogPath(type); + const prevPos = cursor[type] ?? 0; + let lines = []; + let position = 0; + + if (!fs.existsSync(filePath)) { + nextCursor[type] = 0; + continue; + } + + if (prevPos === 0 && initialLines > 0) { + const tail = readLastLines(filePath, initialLines); + lines = tail.lines; + position = tail.position; + } else if (prevPos > 0) { + const chunk = readFromByte(filePath, prevPos); + lines = chunk.lines; + position = chunk.position; + } else { + // No tail requested and no cursor — skip content, anchor at EOF + position = fs.statSync(filePath).size; + } + + nextCursor[type] = position; + + for (const line of lines) { + const entry = parseLogLine(line, type); + if (entry) entries.push(entry); + } + } + + entries.sort((a, b) => { + const ta = a.timestamp ? Date.parse(a.timestamp) : 0; + const tb = b.timestamp ? Date.parse(b.timestamp) : 0; + return ta - tb; + }); + + return { entries, cursor: nextCursor }; +} + +function getLogMeta() { + const date = todayDate(); + return LOG_TYPES.map((type) => { + const filePath = resolveLogPath(type); + const exists = fs.existsSync(filePath) && fs.statSync(filePath).size > 0; + return { + type, + date, + file: exists ? path.basename(filePath) : null, + size: exists ? fs.statSync(filePath).size : 0, + }; + }); +} + +module.exports = { + LOG_ROOT, + LOG_TYPES, + fetchLogs, + getLogMeta, + getLogPath, + resolveLogPath, +}; diff --git a/Structures/ModuleStatus.js b/Structures/ModuleStatus.js new file mode 100644 index 0000000..aa79ef5 --- /dev/null +++ b/Structures/ModuleStatus.js @@ -0,0 +1,269 @@ +const fs = require("fs"); +const path = require("path"); + +const PLACEHOLDER_PATTERNS = [ + /ROLE_ID/i, + /CHANNEL_ID/i, + /CATEGORY_ID/i, + /TAG_ID/i, + /FORUM_CHANNEL_ID/i, + /BOT_TOKEN/i, + /MODEL_API_KEY/i, + /PASTEBIN_API_KEY/i, + /YOUR_/i, + /PUT_RANDOM_STRING/i, + /example\.com/i, +]; + +function isEmpty(value) { + if (value == null) return true; + if (typeof value === "boolean") return false; + if (typeof value === "number") return false; + return String(value).trim() === ""; +} + +function isPlaceholder(value) { + if (isEmpty(value)) return true; + const s = String(value).trim(); + return PLACEHOLDER_PATTERNS.some((p) => p.test(s)); +} + +function isDiscordId(value) { + return /^\d{17,20}$/.test(String(value).trim()); +} + +function isConfiguredId(value) { + return isDiscordId(value) && !isPlaceholder(value); +} + +function isValidToken(value) { + if (isPlaceholder(value)) return false; + const s = String(value).trim(); + return s.length >= 50 && s.includes("."); +} + +function scoreChecks(checks) { + const total = checks.length; + const passed = checks.filter((c) => c.ok).length; + + if (total === 0) { + return { status: "done", subtitle: "Ready", passed: 0, total: 0 }; + } + + if (passed === 0) { + const hint = checks.find((c) => !c.ok)?.hint ?? "Not configured yet"; + return { status: "todo", subtitle: hint, passed, total }; + } + + if (passed === total) { + return { status: "done", subtitle: "Configured and ready", passed, total }; + } + + const next = checks.find((c) => !c.ok); + return { + status: "progress", + subtitle: next?.hint ?? `${passed} of ${total} steps complete`, + passed, + total, + }; +} + +function evaluateTickets(supportbot, ticketPanel) { + const ticket = supportbot?.Ticket ?? {}; + const ticketType = String(ticket.TicketType || "channels").toLowerCase(); + + return scoreChecks([ + { + ok: ticketPanel?.Panel === true, + hint: "Enable the ticket panel", + }, + { + ok: isConfiguredId(ticket.TicketHome), + hint: "Set TicketHome channel ID", + }, + { + ok: + ticketType !== "channels" || + isConfiguredId(ticket.TicketChannelsCategory), + hint: "Set ticket category ID", + }, + { + ok: !isPlaceholder(ticketPanel?.Button?.Text), + hint: "Configure panel button text", + }, + ]); +} + +function evaluateSupportbot(supportbot) { + const roles = supportbot?.Roles?.StaffMember ?? {}; + const welcome = supportbot?.Welcome ?? {}; + + return scoreChecks([ + { ok: isValidToken(supportbot?.General?.Token), hint: "Add your bot token" }, + { + ok: + isConfiguredId(roles.Staff) || + isConfiguredId(roles.Admin) || + isConfiguredId(roles.Moderator), + hint: "Set staff role IDs", + }, + { + ok: !welcome.Enabled || isConfiguredId(welcome.Channel), + hint: "Set welcome channel ID", + }, + { + ok: !isPlaceholder(supportbot?.Activity?.Status), + hint: "Set bot activity status", + }, + ]); +} + +function evaluateTranscripts(supportbot, transcriptCount) { + const ticketReady = isConfiguredId(supportbot?.Ticket?.TicketHome); + + if (!ticketReady) { + return { + status: "todo", + subtitle: "Set up tickets first", + passed: 0, + total: 2, + }; + } + + if (transcriptCount > 0) { + return { + status: "done", + subtitle: `${transcriptCount} transcript${transcriptCount === 1 ? "" : "s"} saved`, + passed: 2, + total: 2, + }; + } + + return { + status: "progress", + subtitle: "Close tickets to generate transcripts", + passed: 1, + total: 2, + }; +} + +function evaluateSuggestions(supportbot) { + const sug = supportbot?.Suggestions ?? {}; + const mode = String(sug.Mode || "Channel").toLowerCase(); + + const channelOk = + mode === "forum" + ? isConfiguredId(sug.ForumChannel) + : isConfiguredId(sug.Channel); + + return scoreChecks([ + { + ok: channelOk, + hint: + mode === "forum" + ? "Set suggestions forum channel" + : "Set suggestions channel ID", + }, + { + ok: !isPlaceholder(sug.UpvoteEmoji) && !isPlaceholder(sug.DownvoteEmoji), + hint: "Configure voting emojis", + }, + ]); +} + +function evaluateAi(supportbotAi) { + if (supportbotAi?.Enabled !== true) { + return { + status: "progress", + subtitle: "AI is disabled in config", + passed: 0, + total: 3, + }; + } + + return scoreChecks([ + { + ok: !isPlaceholder(supportbotAi?.General?.Model_API_Key), + hint: "Add your model API key", + }, + { + ok: isConfiguredId(supportbotAi?.Channels?.AIChannel), + hint: "Set AI channel ID", + }, + { + ok: !isPlaceholder(supportbotAi?.General?.Model), + hint: "Choose an AI model", + }, + ]); +} + +function evaluateCommands(commands) { + const entries = Object.entries(commands || {}).filter( + ([, v]) => v && typeof v === "object" && "Enabled" in v, + ); + + if (!entries.length) { + return { status: "todo", subtitle: "No commands configured", passed: 0, total: 0 }; + } + + const enabled = entries.filter(([, v]) => v.Enabled !== false).length; + const total = entries.length; + const ratio = enabled / total; + + if (ratio >= 0.85) { + return { + status: "done", + subtitle: `${enabled} of ${total} commands enabled`, + passed: enabled, + total, + }; + } + + if (enabled === 0) { + return { + status: "todo", + subtitle: "Enable slash commands", + passed: 0, + total, + }; + } + + return { + status: "progress", + subtitle: `${enabled} of ${total} commands enabled`, + passed: enabled, + total, + }; +} + +function countTranscripts() { + const dir = path.resolve(__dirname, "../Data/Transcripts"); + if (!fs.existsSync(dir)) return 0; + return fs + .readdirSync(dir) + .filter((f) => f.endsWith("-transcript.html")).length; +} + +function evaluateAllModules(configStore, options = {}) { + const supportbot = configStore.supportbot; + const ticketPanel = configStore.ticketPanel; + const supportbotAi = configStore.supportbotAi; + const commands = configStore.commands; + const transcriptCount = + options.transcriptCount ?? countTranscripts(); + + return { + tickets: evaluateTickets(supportbot, ticketPanel), + supportbot: evaluateSupportbot(supportbot), + transcripts: evaluateTranscripts(supportbot, transcriptCount), + suggestions: evaluateSuggestions(supportbot), + ai: evaluateAi(supportbotAi), + commands: evaluateCommands(commands), + }; +} + +module.exports = { + evaluateAllModules, + isPlaceholder, + isConfiguredId, +}; diff --git a/Structures/NotificationService.js b/Structures/NotificationService.js new file mode 100644 index 0000000..09e85e2 --- /dev/null +++ b/Structures/NotificationService.js @@ -0,0 +1,63 @@ +const notificationStore = require("./NotificationStore.js"); +const { fetchUpdateInfo } = require("./UpdateChecker.js"); + +function notifyBotRestart(success, errorMessage) { + if (success) { + return notificationStore.add({ + type: "bot", + title: "Bot restarted", + message: + "Discord bot reconnected with your latest config. API and dashboard stayed online.", + }); + } + + return notificationStore.add({ + type: "error", + title: "Bot failed to restart", + message: errorMessage || "Check the logs for details.", + href: "/logs", + }); +} + +function notifyUpdateAvailable(info) { + return notificationStore.add({ + id: "update-available", + type: "update", + title: "Update available", + message: `Version ${info.latest} is ready (you are on ${info.current}).`, + href: "/settings", + }); +} + +function notifyUpdateInstalled(message) { + return notificationStore.add({ + type: "update", + title: "Update installed", + message: + message || + "Files were updated. Restart your server (npm start) to finish applying changes.", + href: "/settings", + }); +} + +function clearUpdateAvailable() { + notificationStore.dismiss("update-available"); +} + +async function runUpdateCheck() { + const info = await fetchUpdateInfo(); + if (info.updateAvailable) { + notifyUpdateAvailable(info); + } else { + clearUpdateAvailable(); + } + return info; +} + +module.exports = { + notifyBotRestart, + notifyUpdateAvailable, + notifyUpdateInstalled, + clearUpdateAvailable, + runUpdateCheck, +}; diff --git a/Structures/NotificationStore.js b/Structures/NotificationStore.js new file mode 100644 index 0000000..b5876c2 --- /dev/null +++ b/Structures/NotificationStore.js @@ -0,0 +1,86 @@ +const MAX_NOTIFICATIONS = 50; + +/** @type {Map} */ +const byId = new Map(); +/** @type {string[]} */ +let order = []; + +/** + * @param {object} input + * @param {string} [input.id] Stable id replaces existing (e.g. update-available) + */ +function add(input) { + const id = + input.id || + `n-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + + const item = { + id, + type: input.type || "info", + title: input.title || "Notification", + message: input.message || "", + href: input.href || null, + createdAt: input.createdAt || Date.now(), + read: false, + }; + + if (byId.has(id)) { + order = order.filter((x) => x !== id); + } + + byId.set(id, item); + order.unshift(id); + + while (order.length > MAX_NOTIFICATIONS) { + const removed = order.pop(); + if (removed) byId.delete(removed); + } + + return item; +} + +function list() { + return order.map((id) => byId.get(id)).filter(Boolean); +} + +function unreadCount() { + let n = 0; + for (const item of byId.values()) { + if (!item.read) n++; + } + return n; +} + +function markRead(ids) { + if (ids === "all") { + for (const item of byId.values()) { + item.read = true; + } + return; + } + for (const id of ids) { + const item = byId.get(id); + if (item) item.read = true; + } +} + +function dismiss(id) { + if (!byId.has(id)) return false; + byId.delete(id); + order = order.filter((x) => x !== id); + return true; +} + +function clear() { + byId.clear(); + order = []; +} + +module.exports = { + add, + list, + unreadCount, + markRead, + dismiss, + clear, +}; diff --git a/Structures/TicketManager.js b/Structures/TicketManager.js index 85455f4..37be85a 100644 --- a/Structures/TicketManager.js +++ b/Structures/TicketManager.js @@ -4,8 +4,8 @@ const Discord = require("discord.js"); const yaml = require("js-yaml"); const db = require("./Database.js"); -const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8")); -const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8")); +const supportbot = require("./ConfigStore").supportbot; +const msgconfig = require("./ConfigStore").messages; function escapeHtml(str = "") { return String(str) diff --git a/Structures/UpdateChecker.js b/Structures/UpdateChecker.js new file mode 100644 index 0000000..34a73ab --- /dev/null +++ b/Structures/UpdateChecker.js @@ -0,0 +1,55 @@ +const fs = require("fs"); +const path = require("path"); + +const UPDATE_REPO = "C-h-a-r/SupportBot-Dashboard"; +const UPDATE_BRANCH = "release"; + +function compareVersions(a, b) { + const pa = String(a) + .replace(/^v/i, "") + .split(".") + .map((n) => parseInt(n, 10) || 0); + const pb = String(b) + .replace(/^v/i, "") + .split(".") + .map((n) => parseInt(n, 10) || 0); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + if ((pa[i] || 0) > (pb[i] || 0)) return 1; + if ((pa[i] || 0) < (pb[i] || 0)) return -1; + } + return 0; +} + +async function fetchUpdateInfo() { + const axios = require("axios"); + const pkgPath = path.join(__dirname, "../package.json"); + const currentPkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); + const current = currentPkg.version; + + const remoteRes = await axios.get( + `https://raw.githubusercontent.com/${UPDATE_REPO}/${UPDATE_BRANCH}/package.json`, + { + timeout: 15000, + headers: { "User-Agent": "SupportBot-Dashboard" }, + }, + ); + + const latest = remoteRes.data?.version || current; + const updateAvailable = compareVersions(latest, current) > 0; + + return { + current, + latest, + updateAvailable, + repository: `https://github.com/${UPDATE_REPO}`, + branch: UPDATE_BRANCH, + zipUrl: `https://github.com/${UPDATE_REPO}/archive/refs/heads/${UPDATE_BRANCH}.zip`, + }; +} + +module.exports = { + compareVersions, + fetchUpdateInfo, + UPDATE_REPO, + UPDATE_BRANCH, +}; diff --git a/Structures/asyncTimeout.js b/Structures/asyncTimeout.js new file mode 100644 index 0000000..a736a96 --- /dev/null +++ b/Structures/asyncTimeout.js @@ -0,0 +1,14 @@ +function withTimeout(promise, ms, message) { + let timer; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => { + reject(new Error(message || `Timed out after ${ms}ms`)); + }, ms); + }); + + return Promise.race([promise, timeout]).finally(() => { + clearTimeout(timer); + }); +} + +module.exports = { withTimeout }; diff --git a/docs/readme-header.png b/docs/readme-header.png new file mode 100644 index 0000000..5d5c936 Binary files /dev/null and b/docs/readme-header.png differ diff --git a/index.js b/index.js index 5c4040f..c541821 100644 --- a/index.js +++ b/index.js @@ -13,23 +13,33 @@ const fs = require("fs"); const path = require("path"); -const yaml = require("js-yaml"); -const supportbot = yaml.load( - fs.readFileSync("./Configs/supportbot.yml", "utf8") -); +const configStore = require("./Structures/ConfigStore.js"); const Client = require("./Structures/Client.js"); const client = new Client({ - intents: ['Guilds', 'GuildMembers', 'GuildMessages', 'MessageContent'] + intents: ["Guilds", "GuildMembers", "GuildMessages", "MessageContent"], }); const APIServer = require("./API/server.js"); -client.start(supportbot.General.Token); +// Start dashboard API immediately — do not wait for Discord clientReady +const api = new APIServer(client); +client.apiServer = api; -client.once('clientReady', () => { - const api = new APIServer(client); - api.start(); +if (api.config?.Enabled) { + const port = api.config.Port || 3000; + api.start(port); + console.log( + `[Dashboard] API listening on http://localhost:${port} (bot may still be connecting)`, + ); +} else { + console.warn("[Dashboard] API disabled in Configs/api.yml"); +} + +client.start(configStore.supportbot.General.Token); + +client.on("clientReady", () => { + console.log("[Dashboard] Discord bot is ready — full stats and guild tools are available"); }); // SupportBot - New Logging System diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c42ddad --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2479 @@ +{ + "name": "supportbot", + "version": "26.1.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "supportbot", + "version": "26.1.1", + "license": "MIT", + "dependencies": { + "@vitalets/google-translate-api": "^9.2.1", + "axios": "^1.7.2", + "better-sqlite3": "^12.2.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.6", + "discord.js": "^14.13.0", + "express": "^5.2.1", + "figlet": "^1.9.3", + "google-translate-api": "^2.3.0", + "gradient-string": "^3.0.0", + "js": "^0.1.0", + "js-yaml": "^4.1.0", + "moment": "^2.29.3", + "ms": "^2.1.3", + "openai": "^4.50.0", + "systeminformation": "^5.31.5", + "yaml": "^2.8.3" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz", + "integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.2", + "@discordjs/util": "^1.2.0", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.40", + "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.2", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", + "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz", + "integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.2.0", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.5", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.40", + "magic-bytes.js": "^1.13.0", + "tslib": "^2.6.3", + "undici": "6.24.1" + }, + "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/rest/node_modules/@sapphire/snowflake": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz", + "integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@discordjs/util": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", + "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "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/http-errors": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.2.tgz", + "integrity": "sha512-EqX+YQxINb+MeXaIqYDASb6U6FCHbWjkj4a1CKDBks3d/QiB2+PqBLyO72vLDgAO1wUI4O+9gweRcQK11bTL/w==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.8.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz", + "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@types/tinycolor2": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", + "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==", + "license": "MIT" + }, + "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/@vitalets/google-translate-api": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@vitalets/google-translate-api/-/google-translate-api-9.2.1.tgz", + "integrity": "sha512-zlwQWSjXUZhbZQ6qwtIQ7GdYXFQmJ4wYqzcrYJUxtvzQQwUP+uKUb/SRJaBOQuBntjBjzcdcJoLFrpCKUbIkOg==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "^1.8.2", + "http-errors": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz", + "integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/capture-stack-trace": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.2.tgz", + "integrity": "sha512-X/WM2UQs6VMHUtjUDnZTRI+i1crWteJySFzr9UpGoQa4WQffXVTTXuekjl7TjZRlcF2XfjgITT0HxZ9RnxeT0w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/configstore": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-2.1.0.tgz", + "integrity": "sha512-BOCxwwxF5WPspp1OBq9j0JLyL5JgJOTssz9PdOHr8VWjFijaC3PpjU48vFEX3uxx8sTusnVQckLbNzBq6fmkGw==", + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^3.0.0", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "object-assign": "^4.0.1", + "os-tmpdir": "^1.0.0", + "osenv": "^0.1.0", + "uuid": "^2.0.1", + "write-file-atomic": "^1.1.2", + "xdg-basedir": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha512-gYTKKexFO3kh200H1Nit76sRwRtOY32vQd3jpAQKpLtZqyNsSQNfI4N7o3eP2wUjV35pTWKRYqFUDBvUha/Pkw==", + "license": "MIT", + "dependencies": { + "capture-stack-trace": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/discord-api-types": { + "version": "0.38.47", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.47.tgz", + "integrity": "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/discord.js": { + "version": "14.26.4", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.4.tgz", + "integrity": "sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.14.1", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.2", + "@discordjs/rest": "^2.6.1", + "@discordjs/util": "^1.2.0", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.40", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.13.0", + "tslib": "^2.6.3", + "undici": "6.24.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/dot-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", + "integrity": "sha512-k4ELWeEU3uCcwub7+dWydqQBRjAjkV9L33HjVRG5Xo2QybI6ja/v+4W73SRi8ubCqJz0l9XsTP1NbewfyqaSlw==", + "license": "MIT", + "dependencies": { + "is-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer3": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", + "license": "BSD-3-Clause" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "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/figlet": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.11.0.tgz", + "integrity": "sha512-EEx3OS/l2bFqcUNN2NM9FPJp8vAMrgbCxsbl2hbcJNNxOEwVe3mEzrhan7TbJQViZa8mMqhihlbCaqD+LyYKTQ==", + "license": "MIT", + "dependencies": { + "commander": "^14.0.0" + }, + "bin": { + "figlet": "bin/index.js" + }, + "engines": { + "node": ">= 17.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/google-translate-api": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/google-translate-api/-/google-translate-api-2.3.0.tgz", + "integrity": "sha512-a7MRJpSAoS9HyQPE7Yqp5jYSRePWju53+Je/AkgU//zbSmZhy2tc+MvxlwbOegpptT7Ep6GCt7Q1/j7WmTntZw==", + "license": "MIT", + "dependencies": { + "configstore": "^2.0.0", + "google-translate-token": "latest", + "got": "^6.3.0", + "safe-eval": "^0.3.0" + } + }, + "node_modules/google-translate-token": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/google-translate-token/-/google-translate-token-1.0.0.tgz", + "integrity": "sha512-X+cONF24KI3PP94ih3L8QlqNgVxZxsfOyJtX93UISO7TRdTSrFpp4rmDpyS/x6xRxJOLcd6ApCTAkB+tNFtc3g==", + "license": "MIT", + "dependencies": { + "configstore": "^2.0.0", + "got": "^6.3.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha512-Y/K3EDuiQN9rTZhBvPRWMLXIKdeD1Rj0nzunfoi0Yyn5WBEbzxXKU9Ub2X41oZBagVWOBU3MuDonFMgPWQFnwg==", + "license": "MIT", + "dependencies": { + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/gradient-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gradient-string/-/gradient-string-3.0.0.tgz", + "integrity": "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "tinygradient": "^1.1.5" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-redirect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/js/-/js-0.1.0.tgz", + "integrity": "sha512-ZBbGYOpact8QAH9RprFWL4RAESYwbDodxiuDjOnzwzzk9pBzKycoifGuUrHHcDixE/eLMKPHRaXenTgu1qXBqA==", + "dependencies": { + "commander": "~1.1.1" + }, + "bin": { + "js": "bin/js" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/js/node_modules/commander": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-1.1.1.tgz", + "integrity": "sha512-71Rod2AhcH3JhkBikVpNd0pA+fWsmAaVoti6OR38T76chA7vE3pSerS0Jor4wDw+tOueD2zLVvFOw5H0Rcj7rA==", + "dependencies": { + "keypress": "0.1.x" + }, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/keypress": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/keypress/-/keypress-0.1.0.tgz", + "integrity": "sha512-x0yf9PL/nx9Nw9oLL8ZVErFAk85/lslwEP7Vz7s5SI1ODXZIgit3C5qyWjw4DxOuO/3Hb4866SQh28a1V1d+WA==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "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/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/magic-bytes.js": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz", + "integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/openai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-eval": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/safe-eval/-/safe-eval-0.3.0.tgz", + "integrity": "sha512-uPIAjU2zpyv2QJCZ1zaWZKnPv/5jgkaitE7WHomV4Mxu6kgHY1ruIQ1oTikEta/Sux3E8pZAozzJRsAUu3iDZA==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "license": "ISC", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/systeminformation": { + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.6.tgz", + "integrity": "sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA==", + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/tinygradient": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/tinygradient/-/tinygradient-1.1.5.tgz", + "integrity": "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==", + "license": "MIT", + "dependencies": { + "@types/tinycolor2": "^1.4.0", + "tinycolor2": "^1.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "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/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/undici": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha512-N0XH6lqDtFH84JxptQoZYmloF4nzrQqqrAymNj+/gW60AO2AZgOcf4O/nUXJcYfyQkqvMo9lSupBZmmgvuVXlw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==", + "license": "MIT", + "dependencies": { + "prepend-http": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", + "integrity": "sha512-FULf7fayPdpASncVy4DLh3xydlXEJJpvIELjYjNeQWYUZ9pclcpvCZSr2gkmN2FrrGcI7G/cJsIEwk5/8vfXpg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", + "integrity": "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "slide": "^1.1.5" + } + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "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 + } + } + }, + "node_modules/xdg-basedir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz", + "integrity": "sha512-NF1pPn594TaRSUO/HARoB4jK8I+rWgcpVlpQCK6/6o5PHyLUt2CSiDrpUZbQ6rROck+W2EwF8mBJcTs+W98J9w==", + "license": "MIT", + "dependencies": { + "os-homedir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/package.json b/package.json index a0e7018..1525f73 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "main": "index.js", "scripts": { "setup": "npm i && node index.js", - "start": "node index.js" + "start": "node index.js", + "prepare": "node scripts/setup-git-security.js" }, "repository": { "type": "git", @@ -21,6 +22,7 @@ "@vitalets/google-translate-api": "^9.2.1", "axios": "^1.7.2", "better-sqlite3": "^12.2.0", + "cookie-parser": "^1.4.7", "cors": "^2.8.6", "discord.js": "^14.13.0", "express": "^5.2.1",