diff --git a/.githooks/pre-commit b/.githooks/pre-commit deleted file mode 100644 index 719f7eb..0000000 --- a/.githooks/pre-commit +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -cd "$(git rev-parse --show-toplevel)" || exit 1 -node scripts/verify-supportbot-token.js pre-commit diff --git a/.githooks/pre-push b/.githooks/pre-push deleted file mode 100644 index 509c81b..0000000 --- a/.githooks/pre-push +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -cd "$(git rev-parse --show-toplevel)" || exit 1 -node scripts/verify-supportbot-token.js pre-push diff --git a/API/server.js b/API/server.js deleted file mode 100644 index a0b9245..0000000 --- a/API/server.js +++ /dev/null @@ -1,1478 +0,0 @@ -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 { evaluateDashboardAlerts } = require('../Structures/DashboardAlerts.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, - OAUTH_REDIRECT_COOKIE, - getTrustProxySetting, - resolveOAuthRedirectUri, - isSecureRequest, - buildAuthorizeUrl, - exchangeCode, - fetchDiscordUser, - createOAuthState, - oauthStateCookieOptions, - oauthRedirectCookieOptions, - 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 transcriptTemplate = require('../Structures/TranscriptTemplate.js'); -const transcriptStore = require('../Structures/TranscriptStore.js'); -const { refreshSlashCommands } = require('../Structures/BotReload.js'); -const { - isSetupComplete, - getSetupStatus, - validateSetupPayload, - applySetup, -} = require('../Structures/DashboardSetup.js'); -const dashboardBranding = require('../Structures/DashboardBranding.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) { - this.client = client; - this.app = express(); - - try { - const apiCfg = yaml.load(fs.readFileSync('./Configs/api.yml', 'utf8')).API; - this.config = apiCfg; - } catch (e) { - console.error('[API] Failed to load api.yml, defaulting to disabled.'); - this.config = { Enabled: false }; - } - - if (!this.config.Enabled) return; - - this.oauth = this.config.OAuth || {}; - bootstrapFromOAuth(this.oauth); - - const trustProxy = getTrustProxySetting(this.config); - if (trustProxy !== false) { - this.app.set('trust proxy', trustProxy); - } - - this.app.use(cors({ origin: true, credentials: true })); - this.app.use(cookieParser()); - this.app.use(express.json({ limit: '2mb' })); - - this.app.get('/api/branding', (req, res) => { - try { - const branding = dashboardBranding.readBranding(); - res.json({ - success: true, - data: dashboardBranding.publicPayload(branding), - }); - } catch (err) { - res.status(500).json({ success: false, error: 'Failed to load branding' }); - } - }); - - this.app.get('/api/branding/favicon', (req, res) => { - try { - const favicon = dashboardBranding.getFaviconPath(); - if (!favicon) { - return res.status(404).end(); - } - res.setHeader('Cache-Control', 'public, max-age=300'); - res.type(favicon.mime); - return res.sendFile(favicon.filePath); - } catch (err) { - return res.status(500).end(); - } - }); - - 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), - setupComplete: isSetupComplete(), - guild, - }); - }); - - this.setupOnboardingRoutes(); - this.setupAuthRoutes(); - - this.app.get('/transcripts/:id', (req, res) => { - transcriptStore.servePublicTranscript(req, res); - }); - - 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, - }, - ), - }); - } - - setupOnboardingRoutes() { - this.app.get('/api/setup/status', (req, res) => { - try { - res.json({ success: true, data: getSetupStatus(req) }); - } catch (err) { - res.status(500).json({ success: false, error: err.message }); - } - }); - - this.app.post('/api/setup/validate', async (req, res) => { - if (isSetupComplete()) { - return res.status(403).json({ - success: false, - error: 'Setup is already complete.', - }); - } - try { - const data = await validateSetupPayload(req.body || {}); - res.json({ success: true, data }); - } catch (err) { - res.status(500).json({ success: false, error: err.message }); - } - }); - - this.app.post('/api/setup/complete', async (req, res) => { - if (isSetupComplete()) { - return res.status(403).json({ - success: false, - error: 'Setup is already complete.', - }); - } - - try { - const body = req.body || {}; - const validation = await validateSetupPayload(body); - if (!validation.ok) { - return res.status(400).json({ - success: false, - error: 'Fix validation errors before saving.', - data: validation, - }); - } - - applySetup(body); - this.config = configStore.api?.API || configStore.api; - this.oauth = this.config.OAuth || {}; - bootstrapFromOAuth(this.oauth); - - let botRestart = { success: false, error: 'Bot client not available' }; - if (this.client) { - try { - botRestart = await restartDiscordBot(this.client); - } catch (restartErr) { - botRestart = { success: false, error: restartErr.message }; - } - } - - res.json({ - success: true, - message: 'Setup saved. Restart the server for changes to take effect, then sign in with Discord.', - data: { setupComplete: true, botRestart }, - }); - } catch (err) { - console.error('[API] Setup failed:', err); - res.status(400).json({ success: false, error: err.message }); - } - }); - } - - setupAuthRoutes() { - const secret = this.config.SecretKey; - const oauth = this.oauth; - const apiConfig = this.config; - - this.app.get('/api/auth/discord', (req, res) => { - if (!isSetupComplete() || !isOAuthConfigured(oauth)) { - return res.redirect('/setup'); - } - - const redirectUri = resolveOAuthRedirectUri(req, oauth, apiConfig); - const state = createOAuthState(); - const cookieOpts = oauthStateCookieOptions(req, oauth, apiConfig); - - res.cookie(OAUTH_STATE_COOKIE, state, cookieOpts); - res.cookie(OAUTH_REDIRECT_COOKIE, redirectUri, oauthRedirectCookieOptions(req, oauth, apiConfig)); - res.redirect(buildAuthorizeUrl(oauth, state, redirectUri)); - }); - - this.app.get('/api/auth/discord/callback', async (req, res) => { - if (!isSetupComplete() || !isOAuthConfigured(oauth)) { - return res.redirect('/setup'); - } - - 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]; - const savedRedirect = req.cookies?.[OAUTH_REDIRECT_COOKIE]; - const cookieOpts = oauthStateCookieOptions(req, oauth, apiConfig); - - res.clearCookie(OAUTH_STATE_COOKIE, { path: '/', secure: cookieOpts.secure }); - res.clearCookie(OAUTH_REDIRECT_COOKIE, { path: '/', secure: cookieOpts.secure }); - - if (!savedState || savedState !== state) { - return res.redirect('/login?error=invalid_state'); - } - - const redirectUri = - savedRedirect || resolveOAuthRedirectUri(req, oauth, apiConfig); - - try { - const accessToken = await exchangeCode(oauth, String(code), redirectUri); - 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(undefined, { - secure: isSecureRequest(req, oauth, apiConfig), - }), - ); - 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 }); - }); - } - - resolveDashboardDir() { - const candidates = [ - DASHBOARD_DIR, - path.join(process.cwd(), 'public', 'dashboard'), - ]; - for (const dir of candidates) { - if (fs.existsSync(path.join(dir, 'index.html'))) { - return dir; - } - } - return null; - } - - setupDashboard() { - const dashboardDir = this.resolveDashboardDir(); - const port = this.config.Port || 3000; - - if (!dashboardDir) { - console.warn( - '[API] Dashboard UI missing — add the public/dashboard/ folder from the repository (index.html + assets/).', - ); - this.app.get('/', (req, res) => { - res.status(503).type('html').send(` -SupportBot Dashboard - -

Dashboard files not found

-

The API is running, but public/dashboard/ is missing from this install (no index.html).

-

Clone or download the repo including public/dashboard/, then restart the bot.

-

Check API health

-`); - }); - return; - } - - console.log(`[API] Serving dashboard from ${dashboardDir}`); - - this.app.use( - express.static(dashboardDir, { - index: 'index.html', - fallthrough: true, - }), - ); - - this.app.use((req, res, next) => { - if (req.method !== 'GET' && req.method !== 'HEAD') return next(); - if (req.path.startsWith('/api')) return next(); - res.sendFile(path.join(dashboardDir, 'index.html'), (err) => { - if (err) next(err); - }); - }); - - console.log('[API] Dashboard available at http://localhost:' + port); - } - - start(port) { - if (this._httpServer) { - return this._httpServer; - } - - const listenPort = port || this.config.Port || 3000; - transcriptStore.startAutoDeleteScheduler(); - 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() { - const app = this.apiRouter; - const oauth = this.oauth; - const p = (permission) => this.requirePermission(permission); - const cv = (mode) => this.requireConfigAccess(mode); - - app.put('/branding', p('settings.update'), (req, res) => { - try { - const branding = dashboardBranding.updateBranding({ - title: req.body?.title, - pageTitle: req.body?.pageTitle, - }); - res.json({ - success: true, - data: dashboardBranding.publicPayload(branding), - message: 'Branding saved.', - }); - } catch (err) { - res.status(400).json({ success: false, error: err.message }); - } - }); - - app.post('/branding/favicon', p('settings.update'), (req, res) => { - try { - const branding = dashboardBranding.saveFaviconFromBase64(req.body?.dataUrl); - res.json({ - success: true, - data: dashboardBranding.publicPayload(branding), - message: 'Favicon updated.', - }); - } catch (err) { - res.status(400).json({ success: false, error: err.message }); - } - }); - - app.delete('/branding/favicon', p('settings.update'), (req, res) => { - try { - const branding = dashboardBranding.clearFavicon(); - res.json({ - success: true, - data: dashboardBranding.publicPayload(branding), - message: 'Favicon removed.', - }); - } catch (err) { - res.status(400).json({ success: false, error: err.message }); - } - }); - - app.post('/branding/reset', p('settings.update'), (req, res) => { - try { - const branding = dashboardBranding.resetBranding(); - res.json({ - success: true, - data: dashboardBranding.publicPayload(branding), - message: 'Branding reset to defaults.', - }); - } catch (err) { - res.status(400).json({ success: false, error: err.message }); - } - }); - - 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('/alerts', p('overview'), async (req, res) => { - try { - 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 data = await evaluateDashboardAlerts({ - client: this.client, - configStore, - transcriptCount, - }); - - res.json({ success: true, data }); - } catch (err) { - console.error('[API] Error fetching alerts:', 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.', - }); - } - if (req.query.refresh === '1') { - clearGuildResourcesCache(); - } - 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.' }); - } - - let userCount = 0; - this.client.guilds.cache.forEach(guild => { - userCount += guild.memberCount; - }); - - const allTickets = db.getAllTickets() || []; - 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(); - const usedMem = totalMem - freeMem; - const ramPercent = Math.round((usedMem / totalMem) * 100); - const cpuLoad = os.loadavg()[0].toFixed(2); - - res.json({ - success: true, - data: { - bot: { - 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(), - }, - servers: this.client.guilds.cache.size, - users: userCount, - tickets: { - total: allTickets.length, - open: openTickets, - closed: closedTickets - }, - modules, - hosting: { - ram_used: (usedMem / 1024 / 1024 / 1024).toFixed(2) + ' GB', - ram_total: (totalMem / 1024 / 1024 / 1024).toFixed(2) + ' GB', - ram_percent: ramPercent, - cpu_load: cpuLoad, - uptime: Math.floor(os.uptime() / 86400) + ' Days' - } - } - }); - } catch (err) { - console.error('[API] Error fetching stats:', err); - res.status(500).json({ success: false, error: 'Internal Server Error' }); - } - }); - - 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' }); - try { - const configData = fs.readFileSync(`./Configs/${file}.yml`, 'utf8'); - res.json({ success: true, data: configData }); - } catch (err) { - res.status(500).json({ success: false, error: 'Failed to read config' }); - } - }); - - app.put('/configs/raw', cv('edit'), async (req, res) => { - const validFiles = ['supportbot', 'ticket-panel', 'commands', 'messages', 'supportbot-ai']; - const file = req.body.filename; - 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); - 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' }); - } - }); - - 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' }); - try { - const configData = yaml.load(fs.readFileSync(`./Configs/${file}.yml`, 'utf8')); - res.json({ success: true, data: configData }); - } catch (err) { - res.status(500).json({ success: false, error: 'Failed to parse config' }); - } - }); - - 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, fieldPath] = keyPath.split(':'); - const fileContent = fs.readFileSync(`./Configs/${fileName}.yml`, 'utf8'); - const doc = YAML.parseDocument(fileContent); - const keys = fieldPath.split('.'); - doc.setIn(keys, value); - fs.writeFileSync(`./Configs/${fileName}.yml`, doc.toString()); - touched.add(fileName); - } - - 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 }); - } - }); - - 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 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 result = await reloadBot(this.client, { - fileKey: file || null, - refreshCommands: !file || file === 'commands', - }); - res.json({ - success: true, - reloaded: result.reloaded, - commands: result.commands, - }); - } catch (err) { - console.error('[API] Reload failed:', err); - res.status(500).json({ success: false, error: err.message }); - } - }); - - app.get('/system/logs/meta', p('logs'), (req, res) => { - try { - res.json({ success: true, data: { types: LOG_TYPES, files: getLogMeta() } }); - } catch (err) { - 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 --- - app.post('/system/update', p('settings.update'), async (req, res) => { - const { url, version } = req.body; - const downloadUrl = url || UPDATE_ZIP_URL; - - try { - const axios = require('axios'); - const { execSync } = require('child_process'); - const path = require('path'); - - console.log(`[Updater] Starting update to ${version || 'latest'}...`); - - // 1. Download - const tempZip = `./update_${Date.now()}.zip`; - const response = await axios({ method: 'get', url: downloadUrl, responseType: 'stream' }); - const writer = fs.createWriteStream(tempZip); - response.data.pipe(writer); - - await new Promise((resolve, reject) => { - writer.on('finish', resolve); - writer.on('error', reject); - }); - - // 2. Extract to temp dir - const tempDir = `./temp_update_${Date.now()}`; - if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir); - - const isWindows = process.platform === 'win32'; - try { - if (isWindows) { - execSync(`powershell -Command "Expand-Archive -Path '${tempZip}' -DestinationPath '${tempDir}' -Force"`); - } else { - execSync(`unzip -o "${tempZip}" -d "${tempDir}"`); - } - } catch (e) { - throw new Error(`Extraction failed. Ensure ${isWindows ? 'PowerShell' : 'unzip'} is installed. ${e.message}`); - } - - // Identify inner directory (sometimes zips contain a root folder) - let sourcePath = tempDir; - const items = fs.readdirSync(tempDir); - if (items.length === 1 && fs.statSync(path.join(tempDir, items[0])).isDirectory()) { - sourcePath = path.join(tempDir, items[0]); - } - - // 3. Smart Config Merge - const configFiles = ['supportbot', 'commands', 'messages', 'ticket-panel', 'supportbot-ai']; - configFiles.forEach(cf => { - const currentPath = `./Configs/${cf}.yml`; - const newPath = path.join(sourcePath, 'Configs', `${cf}.yml`); - - if (fs.existsSync(currentPath) && fs.existsSync(newPath)) { - const currentCfg = yaml.load(fs.readFileSync(currentPath, 'utf8')); - const newCfg = yaml.load(fs.readFileSync(newPath, 'utf8')); - - const merge = (target, source) => { - for (const key of Object.keys(source)) { - if (source[key] instanceof Object && !Array.isArray(source[key]) && target[key]) { - merge(target[key], source[key]); - } else if (target[key] === undefined) { - target[key] = source[key]; - console.log(`[Updater] Added missing key: ${key} to ${cf}.yml`); - } - } - }; - merge(currentCfg, newCfg); - fs.writeFileSync(currentPath, YAML.stringify(currentCfg)); - } - }); - - // 4. Overwrite core folders (keep Configs + Data) - const coreFolders = ['API', 'Commands', 'Events', 'Structures', 'public']; - coreFolders.forEach(folder => { - const src = path.join(sourcePath, folder); - if (fs.existsSync(src)) { - if (isWindows) { - execSync(`xcopy "${src}" ".\\${folder}" /E /I /Y`); - } else { - execSync(`cp -R "${src}/"* "./${folder}/"`); - } - } - }); - - // 5. Update package.json (version only) - const newPkgPath = path.join(sourcePath, 'package.json'); - if (fs.existsSync(newPkgPath)) { - const currentPkg = JSON.parse(fs.readFileSync('./package.json', 'utf8')); - const newPkg = JSON.parse(fs.readFileSync(newPkgPath, 'utf8')); - currentPkg.version = newPkg.version; - fs.writeFileSync('./package.json', JSON.stringify(currentPkg, null, 2)); - } - - fs.unlinkSync(tempZip); - if (isWindows) { - execSync(`rmdir /s /q "${tempDir}"`); - } else { - execSync(`rm -rf "${tempDir}"`); - } - - 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 --- - const canManageTranscripts = (req) => { - if (req.dashboardService) return true; - if (!hasPermission(req.dashboardPermissions, 'transcripts')) return false; - if (req.dashboardRole === 'viewer') return false; - return true; - }; - - const canEditTranscriptTemplate = (req) => { - if (req.dashboardService) return true; - if (!hasPermission(req.dashboardPermissions, 'transcripts')) return false; - if (req.dashboardRole === 'viewer') return false; - return true; - }; - - app.get('/system/transcript-template', p('transcripts'), (req, res) => { - try { - res.json({ success: true, data: transcriptTemplate.getTemplate() }); - } catch (err) { - res.status(500).json({ success: false, error: 'Failed to load transcript template' }); - } - }); - - app.put('/system/transcript-template', p('transcripts'), (req, res) => { - if (!canEditTranscriptTemplate(req)) { - return forbid(res); - } - try { - const saved = transcriptTemplate.saveTemplate(req.body); - res.json({ - success: true, - data: saved, - message: 'Transcript appearance saved. New transcripts will use this design.', - }); - } catch (err) { - res.status(500).json({ success: false, error: err.message || 'Failed to save template' }); - } - }); - - app.post('/system/transcript-template/preview', p('transcripts'), (req, res) => { - try { - const html = transcriptTemplate.renderPreview(req.body); - res.setHeader('Content-Type', 'text/html; charset=utf-8'); - res.setHeader('Cache-Control', 'no-store'); - res.send(html); - } catch (err) { - res.status(500).type('html').send('

Preview failed

'); - } - }); - - app.get('/system/transcript-settings', p('transcripts'), (req, res) => { - try { - const settings = transcriptStore.getSettings(); - res.json({ - success: true, - data: { - ...settings, - publicBasePath: '/transcripts', - }, - }); - } catch (err) { - res.status(500).json({ success: false, error: 'Failed to load transcript settings' }); - } - }); - - app.put('/system/transcript-settings', p('transcripts'), (req, res) => { - if (!canManageTranscripts(req)) { - return forbid(res); - } - try { - const saved = transcriptStore.saveSettings(req.body); - res.json({ - success: true, - data: { - ...saved, - publicBasePath: '/transcripts', - }, - message: 'Transcript settings saved.', - }); - } catch (err) { - res.status(500).json({ success: false, error: err.message || 'Failed to save settings' }); - } - }); - - app.post('/system/transcripts/purge', p('transcripts'), (req, res) => { - if (!canManageTranscripts(req)) { - return forbid(res); - } - try { - const result = transcriptStore.purgeExpiredTranscripts(); - res.json({ - success: true, - data: result, - message: result.skipped - ? 'Auto-delete is disabled. Enable it in settings to purge old transcripts.' - : `Removed ${result.deleted} transcript(s).`, - }); - } catch (err) { - res.status(500).json({ success: false, error: err.message || 'Purge failed' }); - } - }); - - app.get('/system/transcripts', p('transcripts'), (req, res) => { - try { - transcriptStore.purgeExpiredTranscripts(); - const transcripts = transcriptStore.listTranscripts(); - res.json({ success: true, data: transcripts }); - } catch (err) { - res.status(500).json({ success: false, error: 'Failed to list transcripts' }); - } - }); - - app.delete('/system/transcripts/:id', p('transcripts'), (req, res) => { - if (!canManageTranscripts(req)) { - return forbid(res); - } - try { - const removed = transcriptStore.deleteTranscript(req.params.id); - if (!removed) { - return res.status(404).json({ success: false, error: 'Transcript not found' }); - } - res.json({ success: true, message: 'Transcript deleted.' }); - } catch (err) { - res.status(500).json({ success: false, error: 'Failed to delete transcript' }); - } - }); - - app.get('/system/transcripts/:id', p('transcripts'), (req, res) => { - try { - const transcript = transcriptStore.readTranscriptFile(req.params.id); - if (!transcript) { - return res.status(404).json({ success: false, error: 'Transcript not found' }); - } - res.json({ success: true, data: transcript.content }); - } catch (err) { - res.status(500).json({ success: false, error: 'Failed to read transcript' }); - } - }); - - app.get('/system/transcripts/:id/view', p('transcripts'), (req, res) => { - try { - const transcript = transcriptStore.readTranscriptFile(req.params.id); - if (!transcript) { - return res.status(404).type('html').send( - '

Transcript not found

', - ); - } - res.setHeader('Content-Type', 'text/html; charset=utf-8'); - res.setHeader('Cache-Control', 'private, max-age=60'); - res.send(transcript.content); - } catch (err) { - res.status(500).type('html').send( - '

Failed to load transcript

', - ); - } - }); - - app.get('/system/transcripts/:id/download', p('transcripts'), (req, res) => { - try { - const transcript = transcriptStore.readTranscriptFile(req.params.id); - if (!transcript) { - return res.status(404).json({ success: false, error: 'Transcript not found' }); - } - res.setHeader('Content-Type', 'text/html; charset=utf-8'); - res.setHeader( - 'Content-Disposition', - `attachment; filename="${transcript.filename}"`, - ); - res.send(transcript.content); - } catch (err) { - res.status(500).json({ success: false, error: 'Failed to download transcript' }); - } - }); - } -} - -module.exports = APIServer; diff --git a/Addons/Configs/discord-stats.yml b/Addons/Configs/discord-stats.yml deleted file mode 100644 index f295c0e..0000000 --- a/Addons/Configs/discord-stats.yml +++ /dev/null @@ -1,11 +0,0 @@ - -# 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 deleted file mode 100644 index 675bc57..0000000 --- a/Addons/addon.js +++ /dev/null @@ -1,26 +0,0 @@ -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 = require("../Structures/ConfigStore").supportbot; - -module.exports = new Command({ - name: "addon", - description: "This is a sample addon!", - options: [], - permissions: ["Administrator"], - - async run(interaction) { - let disableCommand = true; - - const PingEmbed = new Discord.EmbedBuilder() - .setDescription( - `This is a sample addon.\n\nPong! \`${interaction.client.ws.ping} ms\`` - ) - .setColor(supportbot.Embed.Colours.General); - - interaction.reply({ - embeds: [PingEmbed], - }); - }, - }); \ No newline at end of file diff --git a/Addons/discord-stats.js b/Addons/discord-stats.js deleted file mode 100644 index 1299b91..0000000 --- a/Addons/discord-stats.js +++ /dev/null @@ -1,139 +0,0 @@ -// 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 deleted file mode 100644 index 804fbbc..0000000 --- a/Commands/General/ai.js +++ /dev/null @@ -1,122 +0,0 @@ -// SupportBot | Emerald Services -// AI Command (Consolidated) - -const fs = require("fs"); -const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, MessageFlags } = require("discord.js"); -const yaml = require("js-yaml"); - -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"); - -module.exports = new Command({ - name: cmdconfig.AI?.Command || "ai", - description: cmdconfig.AI?.Description || "Manage SupportBot AI settings and memory.", - type: ApplicationCommandType.ChatInput, - options: [ - { - name: cmdconfig.AI?.Train?.Command || "train", - description: cmdconfig.AI?.Train?.Description || "Train the AI by adding knowledge to its database.", - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: "content", - description: "The text/knowledge you want the bot to learn", - type: ApplicationCommandOptionType.String, - required: true, - }, - ], - }, - { - name: cmdconfig.AI?.Knowledge?.Command || "knowledge", - description: cmdconfig.AI?.Knowledge?.Description || "Manage the AI knowledge base.", - type: ApplicationCommandOptionType.SubcommandGroup, - options: [ - { - name: cmdconfig.AI?.Knowledge?.Clear?.Command || "clear", - description: cmdconfig.AI?.Knowledge?.Clear?.Description || "Clear all trained knowledge from the AI.", - type: ApplicationCommandOptionType.Subcommand, - }, - ], - }, - { - name: cmdconfig.AI?.Memory?.Command || "memory", - description: cmdconfig.AI?.Memory?.Description || "Manage the AI conversational memory.", - type: ApplicationCommandOptionType.SubcommandGroup, - options: [ - { - name: cmdconfig.AI?.Memory?.Clear?.Command || "clear", - description: cmdconfig.AI?.Memory?.Clear?.Description || "Clear recent conversation memory for this channel.", - type: ApplicationCommandOptionType.Subcommand, - }, - ], - }, - ], - permissions: cmdconfig.AI?.Permission || ["Administrator"], - - async run(interaction) { - if (!supportbotai.Enabled) { - return interaction.reply({ - content: "SupportBot AI is currently disabled.", - flags: MessageFlags.Ephemeral, - }); - } - - const subcommandGroup = interaction.options.getSubcommandGroup(false); - const subcommand = interaction.options.getSubcommand(false); - - try { - if (subcommand === "train" || subcommand === cmdconfig.AI?.Train?.Command) { - const content = interaction.options.getString("content"); - - AIDatabase.addKnowledge({ - sourceType: "user_command", - sourceName: interaction.user.tag, - title: "Trained Knowledge", - content: content, - }); - - const SuccessEmbed = new EmbedBuilder() - .setDescription(`✅ **Knowledge Added!**\n\nThe AI has learned the following information:\n\`\`\`${content}\`\`\``) - .setColor(supportbot.Embed.Colours.Success); - - return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral }); - } - - if (subcommandGroup === (cmdconfig.AI?.Knowledge?.Command || "knowledge")) { - if (subcommand === (cmdconfig.AI?.Knowledge?.Clear?.Command || "clear")) { - AIDatabase.clearKnowledge(); - - const SuccessEmbed = new EmbedBuilder() - .setDescription(`✅ **Knowledge Cleared!**\n\nAll manually trained knowledge has been wiped from the database.`) - .setColor(supportbot.Embed.Colours.Success); - - return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral }); - } - } - - if (subcommandGroup === (cmdconfig.AI?.Memory?.Command || "memory")) { - if (subcommand === (cmdconfig.AI?.Memory?.Clear?.Command || "clear")) { - AIDatabase.clearConversations(interaction.channel.id); - - const SuccessEmbed = new EmbedBuilder() - .setDescription(`✅ **Memory Cleared!**\n\nThe AI's recent conversation memory in this channel has been wiped. It will no longer remember previous messages sent here.`) - .setColor(supportbot.Embed.Colours.Success); - - return interaction.reply({ embeds: [SuccessEmbed], flags: MessageFlags.Ephemeral }); - } - } - - } catch (error) { - console.error("AI Command Error:", error); - const ErrorEmbed = new EmbedBuilder() - .setDescription(`❌ **Error:** Failed to process the AI command.`) - .setColor(supportbot.Embed.Colours.Error); - - return interaction.reply({ embeds: [ErrorEmbed], flags: MessageFlags.Ephemeral }); - } - }, -}); diff --git a/Commands/General/help.js b/Commands/General/help.js deleted file mode 100644 index c475bb5..0000000 --- a/Commands/General/help.js +++ /dev/null @@ -1,62 +0,0 @@ -const fs = require("fs"); -const Discord = require("discord.js"); -const yaml = require("js-yaml"); - -const supportbot = require("../../Structures/ConfigStore").supportbot; -const cmdconfig = require("../../Structures/ConfigStore").commands; - -const Command = require("../../Structures/Command.js"); - -module.exports = new Command({ - name: cmdconfig.Help.Command, - description: cmdconfig.Help.Description, - type: Discord.ApplicationCommandType.ChatInput, - options: [], - permissions: cmdconfig.Help.Permission, - - async run(interaction) { - const { getRole } = interaction.client; - let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); - let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); - if (!SupportStaff || !Admin) { - return interaction.reply( - "Some roles seem to be missing!\nPlease check for errors when starting the bot." - ); - } - - // Separate regular commands and addon commands - let botCommands = ""; - let addonCommands = ""; - - interaction.client.commands.forEach((command) => { - if (command.permissions) { - if (command.addon) { - addonCommands += `\`${command.name}\` `; - } else { - botCommands += `\`${command.name}\` `; - } - } - }); - - // Create embed - const HelpEmbed1 = new Discord.EmbedBuilder() - .setTitle(supportbot.General.Name + " Commands") - .setThumbnail(interaction.user.displayAvatarURL()) - .setColor(supportbot.Embed.Colours.General) - .addFields({ - name: "🖥️ Commands\n", - value: botCommands || "No commands available.", - inline: false, - }); - - HelpEmbed1.setFooter({ - text: supportbot.Embed.Footer, - iconURL: interaction.user.displayAvatarURL(), - }); - - await interaction.reply({ - flags: Discord.MessageFlags.Ephemeral, - embeds: [HelpEmbed1], - }); - }, -}); diff --git a/Commands/General/info.js b/Commands/General/info.js deleted file mode 100644 index 10a517f..0000000 --- a/Commands/General/info.js +++ /dev/null @@ -1,40 +0,0 @@ -// SupportBot | Emerald Services -// Info Command - -const Discord = require("discord.js"); - -const supportbot = require("../../Structures/ConfigStore").supportbot; -const cmdconfig = require("../../Structures/ConfigStore").commands; -const msgconfig = require("../../Structures/ConfigStore").messages; - -const Command = require("../../Structures/Command.js"); - -module.exports = new Command({ - name: cmdconfig.Info.Command, - description: cmdconfig.Info.Description, - options: [], - permissions: cmdconfig.Info.Permission, - - async run(interaction) { - let disableCommand = true; - - const InfoButton = new Discord.ButtonBuilder() - .setLabel(msgconfig.Info.Button) - .setURL(msgconfig.Info.URL) - .setStyle("Link"); - - const inforow = new Discord.ActionRowBuilder().addComponents(InfoButton); - - const InfoEmbed = new Discord.EmbedBuilder() - .setURL(msgconfig.Info.URL) - .setTitle(msgconfig.Info.Title) - .setDescription(msgconfig.Info.Description) - .setColor(msgconfig.Info.Colour) - - interaction.reply({ - embeds: [InfoEmbed], - components: [inforow], - }); - - }, -}); \ No newline at end of file diff --git a/Commands/General/ping.js b/Commands/General/ping.js deleted file mode 100644 index 9d50536..0000000 --- a/Commands/General/ping.js +++ /dev/null @@ -1,30 +0,0 @@ -// SupportBot | Emerald Services -// Ping Command - -const fs = require("fs"); - -const Discord = require("discord.js"); -const yaml = require("js-yaml"); -const supportbot = require("../../Structures/ConfigStore").supportbot; -const cmdconfig = require("../../Structures/ConfigStore").commands; - -const Command = require("../../Structures/Command.js"); - -module.exports = new Command({ - name: cmdconfig.Ping.Command, - description: cmdconfig.Ping.Description, - options: [], - permissions: cmdconfig.Ping.Permission, - - async run(interaction) { - const PingEmbed = new Discord.EmbedBuilder() - .setDescription( - `:ping_pong: **Ping:** \`${interaction.client.ws.ping} ms\``, - ) - .setColor(supportbot.Embed.Colours.General); - - await interaction.reply({ - embeds: [PingEmbed], - }); - }, -}); diff --git a/Commands/General/profile.js b/Commands/General/profile.js deleted file mode 100644 index 989f394..0000000 --- a/Commands/General/profile.js +++ /dev/null @@ -1,527 +0,0 @@ -const { - ModalBuilder, - TextInputBuilder, - TextInputStyle, - ApplicationCommandType, - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - ApplicationCommandOptionType, - MessageFlags, - ContainerBuilder, - TextDisplayBuilder, - SeparatorBuilder, - ThumbnailBuilder, - MediaGalleryBuilder, - MediaGalleryItemBuilder, -} = require("discord.js"); -const fs = require("fs"); -const yaml = require("js-yaml"); -const db = require("../../Structures/Database.js"); -const Command = require("../../Structures/Command.js"); - -const supportbot = require("../../Structures/ConfigStore").supportbot; -const cmdconfig = require("../../Structures/ConfigStore").commands; -const msgconfig = require("../../Structures/ConfigStore").messages; - -const clockedInUsers = new Set(); - -function safeGetProfile(userId) { - try { - if (typeof db.getProfile !== "function") { - console.error("db.getProfile is not a function"); - return { - bio: "", - timezone: "", - clockedIn: false, - }; - } - - const profile = db.getProfile(userId); - - return { - bio: profile?.bio || "", - timezone: profile?.timezone || "", - clockedIn: Boolean(profile?.clockedIn), - }; - } catch (error) { - console.error("Error getting profile:", error); - return { - bio: "", - timezone: "", - clockedIn: false, - }; - } -} - -function safeSetClockedIn(userId, value) { - try { - if (typeof db.setClockedIn !== "function") { - console.error("db.setClockedIn is not a function"); - return false; - } - - db.setClockedIn(userId, value); - return true; - } catch (error) { - console.error("Error setting clocked in status:", error); - return false; - } -} - -function buildProfileContainer({ - viewingUser, - viewingUserId, - bio, - timezone, - clockedIn, - isStaff, -}) { - const container = new ContainerBuilder(); - - if (supportbot.Embed?.Colours?.General) { - container.setAccentColor( - parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16), - ); - } - - const avatarUrl = viewingUser.displayAvatarURL({ size: 128, extension: "png" }); - - const gallery = new MediaGalleryBuilder().addItems( - new MediaGalleryItemBuilder().setURL(avatarUrl) - ); - - container.addMediaGalleryComponents(gallery); - - container.addTextDisplayComponents( - new TextDisplayBuilder().setContent(`## ${viewingUser.username}'s Profile`) - ); - - container.addSeparatorComponents(new SeparatorBuilder().setDivider(true)); - - container.addTextDisplayComponents( - new TextDisplayBuilder().setContent( - [ - `**Bio:** ${bio || "No bio set."}`, - `**Timezone:** ${timezone || "No timezone set."}`, - ...(isStaff - ? [`**Clocked In:** ${clockedIn ? "✅ Clocked In" : "❌ Clocked Out"}`] - : []), - ].join("\n") - ) - ); - - return container; -} - -function buildProfileButtons({ isOwnProfile, isStaff, clockedIn }) { - const row = new ActionRowBuilder(); - - if (isOwnProfile) { - row.addComponents( - new ButtonBuilder() - .setCustomId("editProfile") - .setLabel("Edit Profile") - .setStyle(ButtonStyle.Secondary), - ); - - if (isStaff) { - row.addComponents( - new ButtonBuilder() - .setCustomId("clockInOut") - .setLabel(clockedIn ? "Clock Out" : "Clock In") - .setStyle(ButtonStyle.Secondary), - ); - } - } - - if (isStaff && supportbot.Ticket?.ClaimTickets?.Enabled) { - row.addComponents( - new ButtonBuilder() - .setCustomId("viewTicketStats") - .setLabel("View Ticket Stats") - .setStyle(ButtonStyle.Secondary), - ); - } - - return row.components.length ? [row] : []; -} - -function buildTicketStatsContainer(viewingUserId, ticketsClaimed, ticketsOpen, avgMinutes) { - const container = new ContainerBuilder(); - - if (supportbot.Embed?.Colours?.General) { - container.setAccentColor( - parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16), - ); - } - - container.addTextDisplayComponents( - new TextDisplayBuilder().setContent(`# ${msgconfig.TicketStats.Title || "Ticket Stats"}`), - ); - - container.addSeparatorComponents(new SeparatorBuilder().setDivider(true)); - - container.addTextDisplayComponents( - new TextDisplayBuilder().setContent( - [ - `> Ticket stats for <@${viewingUserId}>`, - ``, - `**${msgconfig.TicketStats.OpenTickets || "Open Tickets"}:** ${ticketsOpen.length}`, - `**${msgconfig.TicketStats.TotalTickets || "Total Tickets"}:** ${ticketsClaimed.length}`, - `**${msgconfig.TicketStats.ResponseTime || "Average Response Time"}:** ${avgMinutes} minutes`, - ].join("\n"), - ), - ); - - return container; -} - -function buildOpenTicketsContainer(ticketsOpen) { - const container = new ContainerBuilder(); - - if (supportbot.Embed?.Colours?.General) { - container.setAccentColor( - parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16), - ); - } - - container.addTextDisplayComponents(new TextDisplayBuilder().setContent(`# Active Tickets`)); - - container.addSeparatorComponents(new SeparatorBuilder().setDivider(true)); - - container.addTextDisplayComponents( - new TextDisplayBuilder().setContent( - ticketsOpen.length ? ticketsOpen.map((t) => `<#${t.id}>`).join("\n") : "No active tickets.", - ), - ); - - return container; -} - -module.exports = new Command({ - name: cmdconfig.Profile.Command, - description: cmdconfig.Profile.Description, - type: ApplicationCommandType.ChatInput, - permissions: cmdconfig.Profile.Permission, - options: [ - { - name: "user", - description: "The user to view the profile of", - type: ApplicationCommandOptionType.User, - required: false, - }, - ], - - async run(interaction) { - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - - try { - const viewingUser = interaction.options.getUser("user") || interaction.user; - const viewingUserId = viewingUser.id; - const interactionUserId = interaction.user.id; - - const member = await interaction.guild.members.fetch(viewingUserId); - const { getRole } = interaction.client; - - const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); - const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); - - if (!Staff || !Admin) { - return interaction.editReply({ - content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.", - }); - } - - const isStaff = member.roles.cache.has(Staff.id) || member.roles.cache.has(Admin.id); - const isOwnProfile = interactionUserId === viewingUserId; - - if (typeof db.ensureProfile === "function") { - db.ensureProfile(viewingUserId); - } - - let { bio, timezone, clockedIn } = safeGetProfile(viewingUserId); - - const profileContainer = buildProfileContainer({ - viewingUser, - viewingUserId, - bio, - timezone, - clockedIn, - isStaff, - }); - - const buttonRows = buildProfileButtons({ - isOwnProfile, - isStaff, - clockedIn, - }); - - await interaction.editReply({ - flags: MessageFlags.IsComponentsV2, - components: [profileContainer, ...buttonRows], - }); - - const filter = (i) => - [ - "editProfile", - "clockInOut", - "viewTicketStats", - "showOpenTickets", - "backToStats", - "backToProfile", - ].includes(i.customId) && i.user.id === interactionUserId; - - const collector = interaction.channel.createMessageComponentCollector({ - filter, - time: 60000, - }); - - collector.on("collect", async (i) => { - if (i.customId === "editProfile") { - const modal = new ModalBuilder() - .setCustomId("editProfileModal") - .setTitle("Edit Profile") - .addComponents( - new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId("bio") - .setLabel("Bio") - .setStyle(TextInputStyle.Paragraph) - .setPlaceholder("Enter your bio") - .setValue(String(bio || "")) - .setRequired(false), - ), - new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId("timezone") - .setLabel("Timezone") - .setStyle(TextInputStyle.Short) - .setPlaceholder("Enter your timezone") - .setValue(String(timezone || "")) - .setRequired(false), - ), - ); - - return i.showModal(modal); - } - - if (i.customId === "clockInOut") { - const newStatus = !clockedIn; - const saved = safeSetClockedIn(viewingUserId, newStatus); - - if (!saved) { - return i.reply({ - content: - "Could not update clock in status because the profile database is not available.", - flags: MessageFlags.Ephemeral, - }); - } - - clockedIn = newStatus; - - if (newStatus) clockedInUsers.add(viewingUserId); - else clockedInUsers.delete(viewingUserId); - - const updatedProfileContainer = buildProfileContainer({ - viewingUser, - viewingUserId, - bio, - timezone, - clockedIn, - isStaff, - }); - - const updatedButtonRows = buildProfileButtons({ - isOwnProfile, - isStaff, - clockedIn, - }); - - await i.update({ - flags: MessageFlags.IsComponentsV2, - components: [updatedProfileContainer, ...updatedButtonRows], - }); - - return interaction.followUp({ - content: newStatus - ? "You have successfully clocked in." - : "You have successfully clocked out.", - flags: MessageFlags.Ephemeral, - }); - } - - if (i.customId === "viewTicketStats") { - let TicketData = { tickets: [] }; - - try { - TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); - } catch (error) { - console.error("Error reading TicketData.json:", error); - } - - const ticketsClaimed = TicketData.tickets.filter((t) => t.claimedBy === viewingUserId); - const ticketsOpen = ticketsClaimed.filter((t) => t.open); - const totalTickets = ticketsClaimed.length; - - let totalResponseTime = 0; - let responseCount = 0; - - ticketsClaimed.forEach((t) => { - if (t.claimedAt && t.createdAt) { - totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt); - responseCount++; - } - }); - - const avgMinutes = responseCount - ? Math.round(totalResponseTime / responseCount / 60000) - : 0; - - const statsContainer = buildTicketStatsContainer( - viewingUserId, - ticketsClaimed, - ticketsOpen, - avgMinutes, - ); - - const statsRow = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("showOpenTickets") - .setLabel("Show Open Tickets") - .setStyle(ButtonStyle.Secondary), - new ButtonBuilder() - .setCustomId("backToProfile") - .setLabel("Back to Profile") - .setStyle(ButtonStyle.Secondary), - ); - - return i.update({ - flags: MessageFlags.IsComponentsV2, - components: [statsContainer, statsRow], - }); - } - - if (i.customId === "showOpenTickets") { - let TicketData = { tickets: [] }; - - try { - TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); - } catch (error) { - console.error("Error reading TicketData.json:", error); - } - - const ticketsOpen = TicketData.tickets.filter( - (t) => t.claimedBy === viewingUserId && t.open, - ); - - const openContainer = buildOpenTicketsContainer(ticketsOpen); - - const backRow = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("backToStats") - .setLabel("Back to Stats") - .setStyle(ButtonStyle.Secondary), - ); - - return i.update({ - flags: MessageFlags.IsComponentsV2, - components: [openContainer, backRow], - }); - } - - if (i.customId === "backToStats") { - let TicketData = { tickets: [] }; - - try { - TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); - } catch (error) { - console.error("Error reading TicketData.json:", error); - } - - const ticketsClaimed = TicketData.tickets.filter((t) => t.claimedBy === viewingUserId); - const ticketsOpen = ticketsClaimed.filter((t) => t.open); - - let totalResponseTime = 0; - let responseCount = 0; - - ticketsClaimed.forEach((t) => { - if (t.claimedAt && t.createdAt) { - totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt); - responseCount++; - } - }); - - const avgMinutes = responseCount - ? Math.round(totalResponseTime / responseCount / 60000) - : 0; - - const statsContainer = buildTicketStatsContainer( - viewingUserId, - ticketsClaimed, - ticketsOpen, - avgMinutes, - ); - - const statsRow = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId("showOpenTickets") - .setLabel("Show Open Tickets") - .setStyle(ButtonStyle.Secondary), - new ButtonBuilder() - .setCustomId("backToProfile") - .setLabel("Back to Profile") - .setStyle(ButtonStyle.Secondary), - ); - - return i.update({ - flags: MessageFlags.IsComponentsV2, - components: [statsContainer, statsRow], - }); - } - - if (i.customId === "backToProfile") { - const updatedProfileContainer = buildProfileContainer({ - viewingUser, - viewingUserId, - bio, - timezone, - clockedIn, - isStaff, - }); - - const updatedButtonRows = buildProfileButtons({ - isOwnProfile, - isStaff, - clockedIn, - }); - - return i.update({ - flags: MessageFlags.IsComponentsV2, - components: [updatedProfileContainer, ...updatedButtonRows], - }); - } - }); - } catch (error) { - console.error("Error executing profile command:", error); - - if (interaction.deferred || interaction.replied) { - return interaction - .editReply({ - content: "An error occurred while loading the profile.", - components: [], - }) - .catch(() => {}); - } - - return interaction - .reply({ - content: "An error occurred while loading the profile.", - flags: MessageFlags.Ephemeral, - }) - .catch(() => {}); - } - }, -}); - -module.exports.clockedInUsers = clockedInUsers; diff --git a/Commands/General/translate.js b/Commands/General/translate.js deleted file mode 100644 index 161db24..0000000 --- a/Commands/General/translate.js +++ /dev/null @@ -1,206 +0,0 @@ -// SupportBot | Emerald Services -// Translate Command - -const fs = require("fs"); -const Discord = require("discord.js"); -const yaml = require("js-yaml"); - -const { translate } = require("@vitalets/google-translate-api"); - -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 LANGUAGES = [ - { name: "Auto Detect", value: "auto" }, - { name: "English", value: "en" }, - { name: "Spanish", value: "es" }, - { name: "French", value: "fr" }, - { name: "German", value: "de" }, - { name: "Italian", value: "it" }, - { name: "Portuguese", value: "pt" }, - { name: "Dutch", value: "nl" }, - { name: "Polish", value: "pl" }, - { name: "Russian", value: "ru" }, - { name: "Ukrainian", value: "uk" }, - { name: "Turkish", value: "tr" }, - { name: "Arabic", value: "ar" }, - { name: "Hebrew", value: "he" }, - { name: "Hindi", value: "hi" }, - { name: "Japanese", value: "ja" }, - { name: "Korean", value: "ko" }, - { name: "Chinese (Simplified)", value: "zh-cn" }, - { name: "Chinese (Traditional)", value: "zh-tw" }, - { name: "Thai", value: "th" }, - { name: "Vietnamese", value: "vi" }, - { name: "Indonesian", value: "id" }, - { name: "Greek", value: "el" }, - { name: "Swedish", value: "sv" }, - { name: "Romanian", value: "ro" }, -]; - -function buildContainer(title, lines = [], color = null) { - const container = new Discord.ContainerBuilder(); - - if (color) { - container.setAccentColor(parseInt(color.replace("#", ""), 16)); - } - - container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(`## ${title}`)); - - container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true)); - - container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(lines.join("\n"))); - - return container; -} - -function trim(text, max = 1800) { - if (text.length <= max) return text; - return text.substring(0, max) + "..."; -} - -module.exports = new Command({ - name: cmdconfig.Translate.Command, - description: cmdconfig.Translate.Description, - type: Discord.ApplicationCommandType.ChatInput, - - options: [ - { - type: Discord.ApplicationCommandOptionType.String, - name: "from", - description: "Language to translate FROM", - required: true, - choices: LANGUAGES, - }, - { - type: Discord.ApplicationCommandOptionType.String, - name: "to", - description: "Language to translate TO", - required: true, - choices: LANGUAGES.filter((l) => l.value !== "auto"), - }, - { - type: Discord.ApplicationCommandOptionType.String, - name: "text", - description: "Text to translate", - required: true, - }, - ], - - permissions: cmdconfig.Translate.Permission, - - async run(interaction) { - const { getRole, getChannel } = interaction.client; - - const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); - - const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); - - if (!Staff || !Admin) { - return interaction.reply({ - content: "Some roles seem to be missing!", - flags: Discord.MessageFlags.Ephemeral, - }); - } - - if ( - !interaction.member.roles.cache.has(Staff.id) && - !interaction.member.roles.cache.has(Admin.id) - ) { - return interaction.reply({ - flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral, - components: [ - buildContainer( - "Invalid Permissions", - [ - msgconfig.Error.IncorrectPerms, - "", - `Required role: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``, - ], - supportbot.Embed.Colours.Warn, - ), - ], - }); - } - - await interaction.deferReply({ flags: Discord.MessageFlags.Ephemeral }); - - const fromLang = interaction.options.getString("from"); - const toLang = interaction.options.getString("to"); - const text = interaction.options.getString("text"); - - const translateLog = await getChannel(supportbot.Translate?.TranslateLog, interaction.guild); - - let result; - - try { - result = await translate(text, { - from: fromLang === "auto" ? undefined : fromLang, - to: toLang, - }); - } catch (err) { - console.error("Translate error:", err); - - return interaction.editReply({ - flags: Discord.MessageFlags.IsComponentsV2, - components: [ - buildContainer( - "Translation Failed", - ["The translation service returned an error."], - supportbot.Embed.Colours.Warn, - ), - ], - }); - } - - const detectedLanguage = result.from?.language?.iso || result.from?.language || "unknown"; - - const container = buildContainer( - "Translation", - [ - `**Requested By:** <@${interaction.user.id}>`, - `**From:** \`${fromLang === "auto" ? detectedLanguage : fromLang}\``, - `**To:** \`${toLang}\``, - "", - "**Original Text**", - trim(text), - "", - "**Translated Text**", - trim(result.text), - ], - supportbot.Embed.Colours.Success, - ); - - await interaction.editReply({ - flags: Discord.MessageFlags.IsComponentsV2, - components: [container], - }); - - if (translateLog) { - const logContainer = buildContainer( - "Translation Log", - [ - `User: <@${interaction.user.id}>`, - `Channel: <#${interaction.channel.id}>`, - `From: ${fromLang}`, - `To: ${toLang}`, - "", - trim(text), - "", - trim(result.text), - ], - supportbot.Embed.Colours.General, - ); - - translateLog - .send({ - flags: Discord.MessageFlags.IsComponentsV2, - components: [logContainer], - }) - .catch(() => {}); - } - }, -}); diff --git a/Commands/Suggestions/suggest.js b/Commands/Suggestions/suggest.js deleted file mode 100644 index 148af80..0000000 --- a/Commands/Suggestions/suggest.js +++ /dev/null @@ -1,147 +0,0 @@ -const fs = require("fs"); -const Discord = require("discord.js"); -const yaml = require("js-yaml"); -const path = require("path"); - -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"); - -module.exports = new Command({ - name: "suggest", - description: cmdconfig.Suggestion?.Description || "Submit a new suggestion", - type: Discord.ApplicationCommandType.ChatInput, - options: [ - { - name: "text", - description: "Your suggestion text", - type: Discord.ApplicationCommandOptionType.String, - required: true - } - ], - - async run(interaction) { - const suggestion = interaction.options.getString("text"); - const mode = supportbot.Suggestions.Mode || "Channel"; - - const embed = new Discord.EmbedBuilder() - .addFields( - { name: "Suggestion", value: suggestion, inline: true }, - { name: "From", value: `<@${interaction.user.id}>` }, - { name: `${supportbot.Suggestions.UpvoteEmoji} ${supportbot.Suggestions.UpvoteTitle}`, value: "0", inline: true }, - { name: `${supportbot.Suggestions.DownvoteEmoji} ${supportbot.Suggestions.DownvoteTitle}`, value: "0", inline: true } - ) - .setThumbnail(interaction.user.displayAvatarURL()) - .setColor(supportbot.Embed.Colours.General); - - const row = new Discord.ActionRowBuilder().addComponents( - new Discord.ButtonBuilder() - .setCustomId("upvote") - .setEmoji(supportbot.Suggestions.UpvoteEmoji) - .setStyle(supportbot.Suggestions.Buttons.Upvote), - new Discord.ButtonBuilder() - .setCustomId("downvote") - .setEmoji(supportbot.Suggestions.DownvoteEmoji) - .setStyle(supportbot.Suggestions.Buttons.Downvote), - new Discord.ButtonBuilder() - .setCustomId("removevote") - .setLabel(supportbot.Suggestions.Buttons.RemoveVote_Title) - .setStyle(supportbot.Suggestions.Buttons.RemoveVote) - ); - - if (mode === "Forum") { - const forum = await interaction.guild.channels - .fetch(supportbot.Suggestions.ForumChannel) - .catch(() => null); - - if (!forum || forum.type !== Discord.ChannelType.GuildForum) { - return interaction.reply({ - embeds: [ - new Discord.EmbedBuilder() - .setTitle("⚠ Missing Forum Channel") - .setDescription(msgconfig.Error.MissingChannel) - .setColor(supportbot.Embed.Colours.Error) - ], - flags: Discord.MessageFlags.Ephemeral, - }); - } - - const appliedTags = supportbot.Suggestions.Tags?.Created - ? [supportbot.Suggestions.Tags.Created] - : []; - - const thread = await forum.threads.create({ - name: suggestion.slice(0, 100), - message: { - content: "\u200B", - embeds: [embed], - components: [row] - }, - appliedTags, - autoArchiveDuration: Discord.ThreadAutoArchiveDuration.OneDay, - reason: "New Suggestion" - }); - - embed.setFooter({ - text: `${supportbot.Embed.Footer} | Suggestion ID: ${thread.id}`, - iconURL: interaction.user.displayAvatarURL() - }); - const starter = await thread.fetchStarterMessage(); - await starter.edit({ embeds: [embed], components: [row] }); - - DB.add(thread.id, interaction.user.id, suggestion); - - return interaction.reply({ - embeds: [ - new Discord.EmbedBuilder() - .setTitle(msgconfig.Suggestions.Sent_Title) - .setDescription(`${msgconfig.Suggestions.Sent}`) - .addFields({ name: 'Posted in:', value: `<#${forum.id}>` }) - .setColor(supportbot.Embed.Colours.Success) - ], - flags: Discord.MessageFlags.Ephemeral, - }); - } - - // ---------- Channel Mode ---------- - const channel = await interaction.guild.channels - .fetch(supportbot.Suggestions.Channel) - .catch(() => null); - - if (!channel) { - return interaction.reply({ - embeds: [ - new Discord.EmbedBuilder() - .setTitle("⚠ Missing Suggestion Channel") - .setDescription(msgconfig.Error.MissingChannel) - .setColor(supportbot.Embed.Colours.Error) - ], - flags: Discord.MessageFlags.Ephemeral, - }); - } - - const message = await channel.send({ embeds: [embed], components: [row] }); - - embed.setFooter({ - text: `${supportbot.Embed.Footer} | Suggestion ID: ${message.id}`, - iconURL: interaction.user.displayAvatarURL() - }); - await message.edit({ embeds: [embed], components: [row] }); - - DB.add(message.id, interaction.user.id, suggestion); - - return interaction.reply({ - embeds: [ - new Discord.EmbedBuilder() - .setTitle(msgconfig.Suggestions.Sent_Title) - .setDescription(`${msgconfig.Suggestions.Sent}`) - .addFields({ name: 'Posted in:', value: `<#${channel.id}>` }) - .setColor(supportbot.Embed.Colours.Success) - ], - flags: Discord.MessageFlags.Ephemeral, - }); - } -}); diff --git a/Commands/Suggestions/suggestadmin.js b/Commands/Suggestions/suggestadmin.js deleted file mode 100644 index 377cd91..0000000 --- a/Commands/Suggestions/suggestadmin.js +++ /dev/null @@ -1,183 +0,0 @@ -const Discord = require("discord.js"); -const fs = require("fs"); -const yaml = require("js-yaml"); - -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"); - -module.exports = new Command({ - name: cmdconfig.SuggestionAdmin.Command, - description: cmdconfig.SuggestionAdmin.Description, - type: Discord.ApplicationCommandType.ChatInput, - permissions: cmdconfig.SuggestionAdmin.Permission, - options: [ - { - name: "status", - description: "New status", - type: Discord.ApplicationCommandOptionType.String, - required: true, - choices: [ - { name: "Solved", value: "Solved" }, - { name: "Denied", value: "Denied" }, - { name: "Considering", value: "Considering" } - ] - }, - { - name: "id", - description: "Message ID (required in Channel mode)", - type: Discord.ApplicationCommandOptionType.String, - required: false - } - ], - - async run(interaction) { - const status = interaction.options.getString("status"); - const idOption = interaction.options.getString("id"); - const mode = supportbot.Suggestions.Mode || "Channel"; - - const { getRole } = interaction.client; - const staffRole = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); - const adminRole = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); - if ( - !interaction.member.roles.cache.has(staffRole?.id) && - !interaction.member.roles.cache.has(adminRole?.id) - ) { - return interaction.reply({ - embeds: [ - new Discord.EmbedBuilder() - .setTitle("Invalid Permissions!") - .setDescription( - `${msgconfig.Error.IncorrectPerms}\nRequired: ${supportbot.Roles.StaffMember.Staff} or ${supportbot.Roles.StaffMember.Admin}` - ) - .setColor(supportbot.Embed.Colours.Warn) - ], - flags: Discord.MessageFlags.Ephemeral, - }); - } - - if (mode === "Forum") { - const thread = interaction.channel; - if (!thread.isThread() || thread.parentId !== supportbot.Suggestions.ForumChannel) { - return interaction.reply({ - embeds: [ - new Discord.EmbedBuilder() - .setTitle("Not a Suggestion Thread") - .setDescription(msgconfig.Error.MissingChannel) - .setColor(supportbot.Embed.Colours.Error) - ], - flags: Discord.MessageFlags.Ephemeral, - }); - } - - const tagMap = { - Solved: supportbot.Suggestions.Tags.Solved, - Denied: supportbot.Suggestions.Tags.Denied, - Considering: supportbot.Suggestions.Tags.Considering - }; - const tagId = tagMap[status]; - if (!tagId) { - return interaction.reply({ - embeds: [ - new Discord.EmbedBuilder() - .setTitle("Tag Missing") - .setDescription("No forum tag configured for this status.") - .setColor(supportbot.Embed.Colours.Error) - ], - flags: Discord.MessageFlags.Ephemeral, - }); - } - - await thread.setAppliedTags([tagId]).catch(() => null); - SuggestionDB.setStatus(thread.id, status); - - return interaction.reply({ - embeds: [ - new Discord.EmbedBuilder() - .setTitle("Suggestion Updated") - .setDescription(`Status set to **${status}**.`) - .setColor(supportbot.Embed.Colours.Success) - ], - flags: Discord.MessageFlags.Ephemeral, - }); - } - - if (!idOption) { - return interaction.reply({ - embeds: [ - new Discord.EmbedBuilder() - .setTitle("Message ID Required") - .setDescription("Provide the suggestion’s message ID when in Channel mode.") - .setColor(supportbot.Embed.Colours.Error) - ], - flags: Discord.MessageFlags.Ephemeral, - }); - } - - const channel = await interaction.guild.channels - .fetch(supportbot.Suggestions.Channel) - .catch(() => null); - if (!channel) { - return interaction.reply({ - embeds: [ - new Discord.EmbedBuilder() - .setTitle("Missing Suggestion Channel") - .setDescription(msgconfig.Error.MissingChannel) - .setColor(supportbot.Embed.Colours.Error) - ], - flags: Discord.MessageFlags.Ephemeral, - }); - } - - const targetMsg = await channel.messages.fetch(idOption).catch(() => null); - if (!targetMsg) { - return interaction.reply({ - embeds: [ - new Discord.EmbedBuilder() - .setTitle("Message Not Found") - .setDescription("Could not fetch a suggestion with that ID.") - .setColor(supportbot.Embed.Colours.Error) - ], - flags: Discord.MessageFlags.Ephemeral, - }); - } - - const oldEmbed = targetMsg.embeds[0]; - if (!oldEmbed) { - return interaction.reply({ - embeds: [ - new Discord.EmbedBuilder() - .setTitle("No Embed Found") - .setDescription("The target message has no suggestion embed.") - .setColor(supportbot.Embed.Colours.Error) - ], - flags: Discord.MessageFlags.Ephemeral, - }); - } - - const colorMap = { - Solved: supportbot.Embed.Colours.Success, - Denied: supportbot.Embed.Colours.Error, - Considering: supportbot.Embed.Colours.Warn - }; - - const newEmbed = Discord.EmbedBuilder.from(oldEmbed) - .setColor(colorMap[status]) - .addFields({ name: "Status", value: status, inline: true }); - - await targetMsg.edit({ embeds: [newEmbed], components: targetMsg.components }); - SuggestionDB.setStatus(idOption, status); - - return interaction.reply({ - embeds: [ - new Discord.EmbedBuilder() - .setTitle("Suggestion Updated") - .setDescription(`Suggestion **${idOption}** marked as **${status}**.`) - .setColor(supportbot.Embed.Colours.Success) - ], - flags: Discord.MessageFlags.Ephemeral, - }); - } -}); diff --git a/Commands/Tickets/close.js b/Commands/Tickets/close.js deleted file mode 100644 index 5eeaf48..0000000 --- a/Commands/Tickets/close.js +++ /dev/null @@ -1,493 +0,0 @@ - - -const fs = require("fs"); -const Discord = require("discord.js"); -const yaml = require("js-yaml"); - -const db = require("../../Structures/Database.js"); -const TicketManager = require("../../Structures/TicketManager.js"); -const Command = require("../../Structures/Command.js"); - -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; -} - -function getTicketUserId(ticket) { - return ticket?.user || ticket?.user_id || null; -} - -function buildContainer(title, lines = []) { - const container = new Discord.ContainerBuilder(); - - if (supportbot.Embed?.Colours?.General) { - container.setAccentColor( - parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16), - ); - } - - container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(`## ${title}`)); - - container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true)); - - if (lines.length > 0) { - container.addTextDisplayComponents( - new Discord.TextDisplayBuilder().setContent(lines.join("\n")), - ); - } - - return container; -} - -function buildReviewSummaryContainer(ticket, rating, comment) { - const departmentConfig = ticket.department ? getDepartmentConfig(ticket.department) : null; - const departmentName = departmentConfig?.Name || ticket.department || null; - const departmentEmoji = departmentConfig?.Emoji || "🎫"; - - const claimedByEnabled = supportbot.Ticket?.ClaimTickets?.Enabled === true; - const claimedBy = claimedByEnabled && ticket.claimedBy ? `<@${ticket.claimedBy}>` : null; - - const ticketUserId = getTicketUserId(ticket); - const lines = [ - `**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedByTitle}** ${ticketUserId ? `<@${ticketUserId}>` : "Unknown User"}`, - ]; - - if (claimedBy) { - lines.unshift(`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedStaffTitle}** ${claimedBy}`); - } - - if (supportbot.Ticket?.DepartmentSystem?.Enabled === true && departmentName) { - lines.push(`**Department** ${departmentEmoji} ${departmentName}`); - } - - lines.push(""); - lines.push(`**${msgconfig.ReviewSystem.ReviewEmbed.RatingTitle}**`); - lines.push(`${msgconfig.ReviewSystem.ReviewEmbed.ReviewEmoji.repeat(Number(rating))}`); - lines.push(""); - lines.push(`**${msgconfig.ReviewSystem.ReviewEmbed.CommentTitle}**`); - lines.push(comment || "No Comment Provided"); - - const container = new Discord.ContainerBuilder(); - - if (msgconfig.ReviewSystem?.ReviewEmbed?.Color) { - container.setAccentColor( - parseInt(String(msgconfig.ReviewSystem.ReviewEmbed.Color).replace("#", ""), 16), - ); - } else if (supportbot.Embed?.Colours?.General) { - container.setAccentColor( - parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16), - ); - } - - container.addTextDisplayComponents( - new Discord.TextDisplayBuilder().setContent(`## ${msgconfig.ReviewSystem.ReviewEmbed.Title}`), - ); - - container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true)); - - container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(lines.join("\n"))); - - return container; -} - -async function collectReview(interaction, ticket) { - const ticketUserId = getTicketUserId(ticket); - if (!ticketUserId) return null; - - const ratingContainer = buildContainer(msgconfig.ReviewSystem.Rate.Title, [ - msgconfig.ReviewSystem.Rate.Description, - "", - ]); - - const ratingRow = new Discord.ActionRowBuilder().addComponents( - new Discord.StringSelectMenuBuilder() - .setCustomId(`closeReviewRating:${interaction.id}`) - .setPlaceholder("Select a rating") - .addOptions([ - { label: msgconfig.ReviewSystem.Stars.One, value: "1" }, - { label: msgconfig.ReviewSystem.Stars.Two, value: "2" }, - { label: msgconfig.ReviewSystem.Stars.Three, value: "3" }, - { label: msgconfig.ReviewSystem.Stars.Four, value: "4" }, - { label: msgconfig.ReviewSystem.Stars.Five, value: "5" }, - ]), - ); - - ratingContainer.addActionRowComponents(ratingRow); - - const ratingPrompt = await interaction.followUp({ - flags: Discord.MessageFlags.IsComponentsV2, - components: [ratingContainer], - }); - - let rating = null; - - try { - const ratingInteraction = await ratingPrompt.awaitMessageComponent({ - componentType: Discord.ComponentType.StringSelect, - time: 120000, - filter: (i) => - i.customId === `closeReviewRating:${interaction.id}` && i.user.id === ticketUserId, - }); - - rating = ratingInteraction.values[0]; - await ratingInteraction.deferUpdate().catch(() => {}); - } catch { - return null; - } - - const commentContainer = buildContainer(msgconfig.ReviewSystem.Comment.Title, [ - msgconfig.ReviewSystem.Comment.Description, - "", - ]); - - const commentRow = new Discord.ActionRowBuilder().addComponents( - new Discord.ButtonBuilder() - .setCustomId(`closeReviewWrite:${interaction.id}`) - .setLabel("Write Comment") - .setStyle(Discord.ButtonStyle.Secondary), - new Discord.ButtonBuilder() - .setCustomId(`closeReviewSkip:${interaction.id}`) - .setLabel("Skip Comment") - .setStyle(Discord.ButtonStyle.Danger), - ); - - commentContainer.addActionRowComponents(commentRow); - - const commentPrompt = await interaction.followUp({ - flags: Discord.MessageFlags.IsComponentsV2, - components: [commentContainer], - }); - - let comment = "No Comment Provided"; - - try { - const buttonInteraction = await commentPrompt.awaitMessageComponent({ - componentType: Discord.ComponentType.Button, - time: 120000, - filter: (i) => - (i.customId === `closeReviewWrite:${interaction.id}` || - i.customId === `closeReviewSkip:${interaction.id}`) && - i.user.id === ticketUserId, - }); - - if (buttonInteraction.customId === `closeReviewSkip:${interaction.id}`) { - await buttonInteraction.deferUpdate().catch(() => {}); - return { rating, comment }; - } - - await buttonInteraction - .update({ - flags: Discord.MessageFlags.IsComponentsV2, - components: [ - buildContainer(msgconfig.ReviewSystem.Comment.Title, [ - `Requested from: <@${ticketUserId}>`, - "", - "Please send your review comment in the channel now.", - "Type `no` to skip.", - ]), - ], - }) - .catch(() => {}); - - const collected = await interaction.channel.awaitMessages({ - max: 1, - time: 120000, - filter: (m) => m.author.id === ticketUserId, - }); - - const message = collected.first(); - - if (message) { - const rawComment = message.content?.trim(); - if (rawComment && rawComment.toLowerCase() !== "no") { - comment = rawComment; - } - - await message.delete().catch(() => {}); - } - } catch { - return { rating, comment }; - } - - return { rating, comment }; -} - -function getTranscriptAttachmentFromResult(result) { - if (!result) return null; - - if (typeof result === "string") { - return result; - } - - if (result.filePath) return result.filePath; - if (result.attachment) return result.attachment; - if (result.path) return result.path; - - return null; -} - -async function maybeDMTranscript(interaction, ticket, transcriptResult) { - if (supportbot.Ticket?.UserManagement?.DMTranscript !== true) { - return; - } - - const ticketUserId = getTicketUserId(ticket); - if (!ticketUserId) return; - - const ticketUser = await interaction.client.users.fetch(ticketUserId).catch(() => null); - - if (!ticketUser) return; - - const attachmentPath = getTranscriptAttachmentFromResult(transcriptResult); - if (!attachmentPath) return; - - try { - await ticketUser.send({ - content: `Here is a copy of your ticket transcript from \`${interaction.guild.name}\`.`, - files: [attachmentPath], - }); - } catch (error) { - console.error("Failed to DM transcript to user:", error); - } -} - -async function collectCloseConfirmation(interaction, ticket, reason) { - const ticketUserId = getTicketUserId(ticket); - - const lines = [ - "Are you sure you want to close this ticket?", - "", - `**Ticket:** ${interaction.channel.name}`, - `**Reason:** ${reason || "No Reason Provided."}`, - ]; - - if (ticketUserId) { - lines.push(`**Ticket User:** <@${ticketUserId}>`); - } - - if (supportbot.Ticket?.ReviewSystem?.Enabled === true) { - lines.push(""); - lines.push("A review will be requested before the ticket is closed."); - } - - const confirmContainer = buildContainer("Close Confirmation", lines); - - const confirmRow = new Discord.ActionRowBuilder().addComponents( - new Discord.ButtonBuilder() - .setCustomId(`confirmClose:${interaction.id}`) - .setLabel("Confirm Close") - .setStyle(Discord.ButtonStyle.Danger), - new Discord.ButtonBuilder() - .setCustomId(`cancelClose:${interaction.id}`) - .setLabel("Cancel") - .setStyle(Discord.ButtonStyle.Secondary), - ); - - confirmContainer.addActionRowComponents(confirmRow); - - const confirmMessage = await interaction.editReply({ - flags: Discord.MessageFlags.IsComponentsV2, - components: [confirmContainer], - }); - - try { - const buttonInteraction = await confirmMessage.awaitMessageComponent({ - componentType: Discord.ComponentType.Button, - time: 120000, - filter: (i) => - (i.customId === `confirmClose:${interaction.id}` || - i.customId === `cancelClose:${interaction.id}`) && - i.user.id === interaction.user.id, - }); - - if (buttonInteraction.customId === `cancelClose:${interaction.id}`) { - await buttonInteraction - .update({ - flags: Discord.MessageFlags.IsComponentsV2, - components: [ - buildContainer("Close Cancelled", ["The ticket close process has been cancelled."]), - ], - }) - .catch(() => {}); - - return false; - } - - await buttonInteraction.deferUpdate().catch(() => {}); - return true; - } catch { - await interaction - .editReply({ - flags: Discord.MessageFlags.IsComponentsV2, - components: [buildContainer("Close Cancelled", ["The close confirmation timed out."])], - }) - .catch(() => {}); - - return false; - } -} - -async function postReview(interaction, ticket, reviewData) { - const { getChannel } = interaction.client; - const reviewChannel = await getChannel(supportbot.Ticket.ReviewSystem.Channel, interaction.guild); - - if (!reviewChannel || !reviewData) return; - - const reviewContainer = buildReviewSummaryContainer( - ticket, - reviewData.rating, - reviewData.comment, - ); - - await reviewChannel - .send({ - flags: Discord.MessageFlags.IsComponentsV2, - components: [reviewContainer], - }) - .catch(() => {}); -} - -async function handleCloseTicket(interaction, reason, ticket) { - try { - if (typeof db.updateTicketStatus === "function") { - db.updateTicketStatus(interaction.channel.id, "closed"); - } - - const transcriptResult = await TicketManager.createTranscript(interaction, ticket, reason); - - await maybeDMTranscript(interaction, ticket, transcriptResult); - - await interaction - .editReply({ - flags: Discord.MessageFlags.IsComponentsV2, - components: [ - buildContainer("Ticket Closed", [ - `**Ticket:** ${interaction.channel.name}`, - `**Closed By:** <@${interaction.user.id}>`, - `**Reason:** ${reason || "No Reason Provided."}`, - ]), - ], - }) - .catch(() => {}); - - await interaction.channel.delete().catch(() => {}); - } catch (err) { - console.error("Error handling ticket close:", err); - - if (interaction.deferred || interaction.replied) { - await interaction - .editReply({ - content: "An error occurred while closing the ticket.", - components: [], - }) - .catch(() => {}); - return; - } - - await interaction - .reply({ - content: "An error occurred while closing the ticket.", - flags: Discord.MessageFlags.Ephemeral, - }) - .catch(() => {}); - } -} - -module.exports = new Command({ - name: cmdconfig.CloseTicket.Command, - description: cmdconfig.CloseTicket.Description, - type: Discord.ApplicationCommandType.ChatInput, - options: [ - { - name: "reason", - description: "Ticket Close Reason", - type: Discord.ApplicationCommandOptionType.String, - }, - ], - permissions: cmdconfig.CloseTicket.Permission, - - async run(interaction) { - const { getRole } = interaction.client; - - if (supportbot.Ticket.Close.StaffOnly) { - const SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); - const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); - - if (!SupportStaff || !Admin) { - return interaction.reply({ - content: "Some roles seem to be missing! Please check for errors when starting the bot.", - flags: Discord.MessageFlags.Ephemeral, - }); - } - - if ( - !interaction.member.roles.cache.has(SupportStaff.id) && - !interaction.member.roles.cache.has(Admin.id) - ) { - return interaction.reply({ - flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral, - components: [ - buildContainer("Invalid Permissions!", [ - msgconfig.Error.IncorrectPerms, - "", - `Role Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``, - ]), - ], - }); - } - } - - const isThread = interaction.channel.type === Discord.ChannelType.PrivateThread; - - if ( - (supportbot.Ticket.TicketType === "threads" && !isThread) || - (supportbot.Ticket.TicketType === "channels" && - interaction.channel.type !== Discord.ChannelType.GuildText) - ) { - return interaction.reply({ - flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral, - components: [ - buildContainer("Invalid Channel!", [ - `This command can only be used in a ${ - supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel" - }.`, - ]), - ], - }); - } - - await interaction.deferReply({ flags: Discord.MessageFlags.Ephemeral }); - - const ticket = db.getTicket(interaction.channel.id); - - if (!ticket) { - return interaction.editReply({ - flags: Discord.MessageFlags.IsComponentsV2, - components: [buildContainer("No Ticket Found!", [msgconfig.Error.NoValidTicket])], - }); - } - - const reason = interaction.options?.getString("reason") || "No Reason Provided."; - - const confirmed = await collectCloseConfirmation(interaction, ticket, reason); - if (!confirmed) { - return; - } - - if (supportbot.Ticket?.ReviewSystem?.Enabled === true) { - const reviewData = await collectReview(interaction, ticket); - - if (reviewData) { - await postReview(interaction, ticket, reviewData); - } - - await handleCloseTicket(interaction, reason, ticket); - return; - } - - await handleCloseTicket(interaction, reason, ticket); - }, -}); diff --git a/Commands/Tickets/manageticket.js b/Commands/Tickets/manageticket.js deleted file mode 100644 index ec57184..0000000 --- a/Commands/Tickets/manageticket.js +++ /dev/null @@ -1,664 +0,0 @@ -const fs = require("fs"); -const yaml = require("js-yaml"); -const { - EmbedBuilder, - ApplicationCommandOptionType, - ApplicationCommandType, - ChannelType, - MessageFlags, - ModalBuilder, - ActionRowBuilder, - TextInputBuilder, - TextInputStyle, -} = require("discord.js"); - -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"); -const db = require("../../Structures/Database.js"); - -function getDepartments() { - return supportbot.Ticket?.DepartmentSystem?.Departments || {}; -} - -function getPriorities() { - return supportbot.Ticket?.PrioritySystem?.Priorities || {}; -} - -function prioritiesEnabled() { - return ( - supportbot.Ticket?.TicketType !== "threads" && - supportbot.Ticket?.PrioritySystem?.Enabled === true - ); -} - -function departmentsEnabled() { - return ( - supportbot.Ticket?.TicketType !== "threads" && - supportbot.Ticket?.DepartmentSystem?.Enabled === true - ); -} - -function buildTicketChannelName(ticketNumberID, priorityKey, departmentConfig) { - const defaultPrefix = supportbot.Ticket?.ChannelPrefix || supportbot.Ticket?.Channel || "ticket-"; - - const departmentPrefix = departmentConfig?.ChannelPrefix || defaultPrefix; - - const safePrefix = String(departmentPrefix).trim() || "ticket-"; - const baseName = `${safePrefix}${ticketNumberID}`; - - if (!prioritiesEnabled() || !priorityKey) { - return baseName; - } - - return `${baseName}-${String(priorityKey).toLowerCase()}`; -} - -module.exports = new Command({ - name: cmdconfig.TicketManage?.Command || "ticketmanage", - description: cmdconfig.TicketManage?.Description || "Manage tickets", - type: ApplicationCommandType.ChatInput, - permissions: cmdconfig.TicketManage?.Permission || ["ManageChannels"], - - options: [ - { - name: "transfer", - description: "Transfer a ticket to another department", - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: "department", - description: "Department to transfer this ticket to", - type: ApplicationCommandOptionType.String, - required: true, - choices: Object.entries(getDepartments()) - .slice(0, 25) - .map(([key, dept]) => ({ - name: dept.Name || key, - value: key, - })), - }, - { - name: "priority", - description: "Optional new priority", - type: ApplicationCommandOptionType.String, - required: false, - choices: Object.entries(getPriorities()) - .slice(0, 25) - .map(([key, priority]) => ({ - name: priority.Name || key, - value: key, - })), - }, - ], - }, - { - name: "priority", - description: "Change a ticket priority", - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: "level", - description: "New priority level", - type: ApplicationCommandOptionType.String, - required: true, - choices: Object.entries(getPriorities()) - .slice(0, 25) - .map(([key, priority]) => ({ - name: priority.Name || key, - value: key, - })), - }, - ], - }, - { - name: cmdconfig.TicketManage?.ForceAdd?.Command || "forceadd", - description: - cmdconfig.TicketManage?.ForceAdd?.Description || "Force add a user to the current ticket", - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: "user", - description: "The user to add", - type: ApplicationCommandOptionType.User, - required: true, - }, - ], - }, - { - name: cmdconfig.TicketManage?.ForceRemove?.Command || "forceremove", - description: - cmdconfig.TicketManage?.ForceRemove?.Description || "Remove a user from the current ticket", - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: "user", - description: "The user to remove", - type: ApplicationCommandOptionType.User, - required: true, - }, - ], - }, - { - name: cmdconfig.TicketManage?.Rename?.Command || "rename", - description: cmdconfig.TicketManage?.Rename?.Description || "Rename the current ticket", - type: ApplicationCommandOptionType.Subcommand, - options: [], - }, - { - name: cmdconfig.TicketManage?.Close?.Command || "close", - description: cmdconfig.TicketManage?.Close?.Description || "Close the current ticket", - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: "reason", - description: "Reason for closing the ticket", - type: ApplicationCommandOptionType.String, - required: false, - }, - ], - }, - ], - - async run(interaction) { - try { - const subcommand = interaction.options.getSubcommand(false); - const ticket = db.getTicket(interaction.channel.id); - - if (!ticket) { - return interaction.reply({ - content: "This channel is not a ticket.", - flags: MessageFlags.Ephemeral, - }); - } - - const { getRole } = interaction.client; - - let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); - let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); - let Moderator = await getRole(supportbot.Roles.StaffMember.Moderator, interaction.guild); - - if (!SupportStaff || !Admin || !Moderator) { - const missingRolesEmbed = new EmbedBuilder() - .setDescription(msgconfig.Error.InvalidChannel || "Required roles are missing!") - .setColor(supportbot.Embed.Colours.Warn); - - return interaction.reply({ - embeds: [missingRolesEmbed], - flags: MessageFlags.Ephemeral, - }); - } - - const NoPerms = new EmbedBuilder() - .setDescription( - msgconfig.Error.IncorrectPerms || "You do not have the correct permissions!", - ) - .setColor(supportbot.Embed.Colours.Warn); - - if ( - !interaction.member.roles.cache.has(Admin.id) && - !interaction.member.roles.cache.has(Moderator.id) && - (!supportbot.Roles?.Mod?.AllowSupportStaff || !interaction.member.roles.cache.has(SupportStaff.id)) - ) { - return interaction.reply({ - embeds: [NoPerms], - flags: MessageFlags.Ephemeral, - }); - } - - if (subcommand === "transfer") { - if (!departmentsEnabled()) { - return interaction.reply({ - content: "Departments are disabled, or ticket mode is set to threads.", - flags: MessageFlags.Ephemeral, - }); - } - - if (interaction.channel.type !== ChannelType.GuildText) { - return interaction.reply({ - content: "Ticket transfer only works for ticket channels.", - flags: MessageFlags.Ephemeral, - }); - } - - const departmentKey = interaction.options.getString("department"); - const requestedPriority = interaction.options.getString("priority"); - - const departments = getDepartments(); - const departmentConfig = departments[departmentKey]; - - if (!departmentConfig) { - return interaction.reply({ - content: "That department does not exist.", - flags: MessageFlags.Ephemeral, - }); - } - - const targetCategoryId = departmentConfig.Category; - const targetRoleId = departmentConfig.Role || null; - - if (!targetCategoryId) { - return interaction.reply({ - content: "That department does not have a category configured.", - flags: MessageFlags.Ephemeral, - }); - } - - const targetCategory = interaction.guild.channels.cache.get(targetCategoryId); - if (!targetCategory) { - return interaction.reply({ - content: "The target department category could not be found.", - flags: MessageFlags.Ephemeral, - }); - } - - let finalPriority = ticket.priority || null; - - if (requestedPriority) { - finalPriority = requestedPriority; - } else if (!finalPriority && departmentConfig.DefaultPriority) { - finalPriority = departmentConfig.DefaultPriority; - } - - const oldDepartmentKey = ticket.department || null; - const oldDepartmentConfig = oldDepartmentKey ? departments[oldDepartmentKey] : null; - const oldRoleId = oldDepartmentConfig?.Role || null; - - await interaction.channel.setParent(targetCategory.id); - - if (oldRoleId && oldRoleId !== targetRoleId) { - await interaction.channel.permissionOverwrites.delete(oldRoleId).catch(() => { }); - } - - if (targetRoleId) { - await interaction.channel.permissionOverwrites.create(targetRoleId, { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }); - } - - if (typeof db.updateTicketDepartment === "function") { - db.updateTicketDepartment(interaction.channel.id, departmentKey); - } - - if (finalPriority && typeof db.updateTicketPriority === "function") { - db.updateTicketPriority(interaction.channel.id, finalPriority); - } - - const ticketNumber = ticket.number || interaction.channel.name.replace(/\D/g, ""); - - const newChannelName = buildTicketChannelName( - ticketNumber, - finalPriority, - departmentConfig, - ); - - if (interaction.channel.name !== newChannelName) { - await interaction.channel.setName(newChannelName).catch(() => { }); - } - - const deptEmoji = departmentConfig.Emoji || "🎫"; - const deptName = departmentConfig.Name || departmentKey; - - let description = `> **Department:** ${deptEmoji} ${deptName}`; - - if (finalPriority && prioritiesEnabled()) { - const priorityConfig = getPriorities()[finalPriority]; - const priorityEmoji = priorityConfig?.Emoji || "🟡"; - const priorityName = priorityConfig?.Name || finalPriority; - description += `\n> **Priority:** ${priorityEmoji} ${priorityName}`; - } - - description += `\n> **Transferred By:** <@${interaction.user.id}>`; - - const embed = new EmbedBuilder() - .setTitle("Ticket Updated") - .setDescription(description) - .setColor(supportbot.Embed.Colours.Success); - - return interaction.reply({ - embeds: [embed], - }); - } - - if (subcommand === "priority") { - if (!prioritiesEnabled()) { - return interaction.reply({ - content: "Priority is disabled, or ticket mode is set to threads.", - flags: MessageFlags.Ephemeral, - }); - } - - const level = interaction.options.getString("level"); - const priorityConfig = getPriorities()[level]; - - if (!priorityConfig) { - return interaction.reply({ - content: "That priority does not exist.", - flags: MessageFlags.Ephemeral, - }); - } - - if (typeof db.updateTicketPriority === "function") { - db.updateTicketPriority(interaction.channel.id, level); - } - - const departments = getDepartments(); - const departmentConfig = ticket.department ? departments[ticket.department] : null; - const ticketNumber = ticket.number || interaction.channel.name.replace(/\D/g, ""); - - const newChannelName = buildTicketChannelName(ticketNumber, level, departmentConfig); - - if (interaction.channel.name !== newChannelName) { - await interaction.channel.setName(newChannelName).catch(() => { }); - } - - const priorityEmoji = priorityConfig.Emoji || "🟡"; - const priorityName = priorityConfig.Name || level; - - const embed = new EmbedBuilder() - .setTitle("Ticket Updated") - .setDescription( - `> **Priority:** ${priorityEmoji} ${priorityName}\n> **Updated By:** <@${interaction.user.id}>`, - ) - .setColor(supportbot.Embed.Colours.Success); - - return interaction.reply({ - embeds: [embed], - }); - } - - if (subcommand === (cmdconfig.TicketManage?.ForceAdd?.Command || "forceadd")) { - const userToAdd = interaction.options.getUser("user"); - const ticketChannel = interaction.channel; - - if ( - (supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) || - (supportbot.Ticket.TicketType === "channels" && - ticketChannel.type !== ChannelType.GuildText) - ) { - const onlyInTicket = new EmbedBuilder() - .setTitle(msgconfig.ForceAddUser.NotInTicket_Title) - .setDescription(msgconfig.ForceAddUser.NotInTicket_Description) - .setColor(supportbot.Embed.Colours.Error); - - return interaction.reply({ - embeds: [onlyInTicket], - flags: MessageFlags.Ephemeral, - }); - } - - const ticketRow = db.getTicket(ticketChannel.id); - if (!ticketRow) { - const notFound = new EmbedBuilder() - .setTitle(msgconfig.ForceAddUser.NotInTicket_Title) - .setDescription(msgconfig.ForceAddUser.NotInTicket_Description) - .setColor(supportbot.Embed.Colours.Error); - - return interaction.reply({ - embeds: [notFound], - flags: MessageFlags.Ephemeral, - }); - } - - try { - if (supportbot.Ticket.TicketType === "threads") { - const botMember = await ticketChannel.members - .fetch(interaction.client.user.id) - .catch(() => null); - - if (!botMember) await ticketChannel.join(); - - await new Promise((res) => setTimeout(res, 1000)); - await ticketChannel.members.add(userToAdd.id); - } else { - await ticketChannel.permissionOverwrites.create(userToAdd.id, { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }); - } - - db.addUserToTicket(ticketChannel.id, userToAdd.id); - - const addedEmbed = new EmbedBuilder() - .setTitle(msgconfig.ForceAddUser.Added_Title) - .setDescription( - msgconfig.ForceAddUser.Added_Description.replace("%username%", userToAdd.username), - ) - .setColor(supportbot.Embed.Colours.Success); - - await interaction.reply({ - embeds: [addedEmbed], - flags: MessageFlags.Ephemeral, - }); - - if (supportbot.Ticket?.UserManagement?.DMOnAdd !== false) { - const addedToTicketEmbed = new EmbedBuilder() - .setTitle(msgconfig.ForceAddUser.AddedToTicket_Title) - .setDescription( - msgconfig.ForceAddUser.AddedToTicket_Description.replace( - "%channel_link%", - `[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})`, - ), - ) - .setColor(supportbot.Embed.Colours.General); - - await userToAdd.send({ embeds: [addedToTicketEmbed] }).catch(() => { }); - } - } catch (err) { - console.error("Error adding user to the ticket:", err); - - const errorEmbed = new EmbedBuilder() - .setTitle(msgconfig.ForceAddUser.Error_Title) - .setDescription(msgconfig.ForceAddUser.Error_Adding) - .setColor(supportbot.Embed.Colours.Error); - - await interaction.reply({ - embeds: [errorEmbed], - flags: MessageFlags.Ephemeral, - }); - } - - return; - } - - if (subcommand === (cmdconfig.TicketManage?.ForceRemove?.Command || "forceremove")) { - const userToRemove = interaction.options.getUser("user"); - const ticketChannel = interaction.channel; - - if ( - (supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) || - (supportbot.Ticket.TicketType === "channels" && - ticketChannel.type !== ChannelType.GuildText) - ) { - const onlyInTicket = new EmbedBuilder() - .setTitle(msgconfig.RemoveUser.NotInTicket_Title) - .setDescription(msgconfig.RemoveUser.NotInTicket_Description) - .setColor(supportbot.Embed.Colours.Error); - - return interaction.reply({ - embeds: [onlyInTicket], - flags: MessageFlags.Ephemeral, - }); - } - - try { - if (supportbot.Ticket.TicketType === "threads") { - await ticketChannel.members.remove(userToRemove.id); - } else { - await ticketChannel.permissionOverwrites.delete(userToRemove.id); - } - - db.removeUserFromTicket(ticketChannel.id, userToRemove.id); - - const removedEmbed = new EmbedBuilder() - .setTitle(msgconfig.RemoveUser.Removed_Title) - .setDescription(msgconfig.RemoveUser.Removed_Message) - .setColor(supportbot.Embed.Colours.Success); - - await interaction.reply({ - embeds: [removedEmbed], - flags: MessageFlags.Ephemeral, - }); - - if (supportbot.Ticket?.UserManagement?.DMOnRemove !== false) { - const removedFromTicketEmbed = new EmbedBuilder() - .setTitle(msgconfig.RemoveUser.RemovedFromTicket_Title) - .setDescription( - msgconfig.RemoveUser.RemovedFromTicket_Description.replace( - "%channel_link%", - `[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})`, - ), - ) - .setColor(supportbot.Embed.Colours.General); - - await userToRemove.send({ embeds: [removedFromTicketEmbed] }).catch(() => { }); - } - } catch (err) { - console.error("Error removing user from the ticket:", err); - - const errorEmbed = new EmbedBuilder() - .setTitle(msgconfig.RemoveUser.Error_Title) - .setDescription(msgconfig.RemoveUser.Error_Removing) - .setColor(supportbot.Embed.Colours.Error); - - await interaction.reply({ - embeds: [errorEmbed], - flags: MessageFlags.Ephemeral, - }); - } - - return; - } - - if (subcommand === (cmdconfig.TicketManage?.Rename?.Command || "rename")) { - const modal = new ModalBuilder() - .setCustomId("renameTicketModal") - .setTitle("Rename Channel") - .addComponents( - new ActionRowBuilder().addComponents( - new TextInputBuilder() - .setCustomId("renameTicket") - .setLabel("New Channel Name") - .setStyle(TextInputStyle.Short) - .setRequired(true), - ), - ); - - return interaction.showModal(modal); - } - - if (subcommand === (cmdconfig.TicketManage?.Close?.Command || "close")) { - const reason = interaction.options.getString("reason") || "Closed by staff."; - - if (supportbot.Ticket.Close.StaffOnly) { - let CloseSupportStaff = await getRole( - supportbot.Roles.StaffMember.Staff, - interaction.guild, - ); - let CloseAdmin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); - - if (!CloseSupportStaff || !CloseAdmin) { - return interaction.reply({ - content: - "Some roles seem to be missing! Please check for errors when starting the bot.", - flags: MessageFlags.Ephemeral, - }); - } - - const noClosePerms = new EmbedBuilder() - .setTitle("Invalid Permissions!") - .setDescription( - `${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``, - ) - .setColor(supportbot.Embed.Colours.Warn); - - if ( - !interaction.member.roles.cache.has(CloseSupportStaff.id) && - !interaction.member.roles.cache.has(CloseAdmin.id) - ) { - return interaction.reply({ - embeds: [noClosePerms], - flags: MessageFlags.Ephemeral, - }); - } - } - - const isThread = interaction.channel.type === ChannelType.PrivateThread; - - if ( - (supportbot.Ticket.TicketType === "threads" && !isThread) || - (supportbot.Ticket.TicketType === "channels" && - interaction.channel.type !== ChannelType.GuildText) - ) { - const notTicketChannel = new EmbedBuilder() - .setTitle("Invalid Channel!") - .setDescription( - `This command can only be used in a ${supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"}.`, - ) - .setColor(supportbot.Embed.Colours.Warn); - - return interaction.reply({ - embeds: [notTicketChannel], - flags: MessageFlags.Ephemeral, - }); - } - - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); - - const ticketData = db.getTicket(interaction.channel.id); - - if (!ticketData) { - const exists = new EmbedBuilder() - .setTitle("No Ticket Found!") - .setDescription(msgconfig.Error.NoValidTicket) - .setColor(supportbot.Embed.Colours.Warn); - - return interaction.followUp({ - embeds: [exists], - flags: MessageFlags.Ephemeral, - }); - } - - try { - await TicketManager.createTranscript(interaction, ticketData, reason); - - await interaction.followUp({ - content: "Ticket closed successfully.", - flags: MessageFlags.Ephemeral, - }); - - await interaction.channel.delete().catch(() => { }); - } catch (err) { - console.error("Error closing ticket:", err); - - await interaction.followUp({ - content: "An error occurred while closing the ticket.", - flags: MessageFlags.Ephemeral, - }); - } - - return; - } - - return interaction.reply({ - content: "Unknown ticket management action.", - flags: MessageFlags.Ephemeral, - }); - } catch (error) { - console.error("Error in ticket manage command:", error); - - if (!interaction.replied && !interaction.deferred) { - return interaction.reply({ - content: "An error occurred while managing the ticket.", - flags: MessageFlags.Ephemeral, - }); - } - } - }, -}); diff --git a/Commands/Tickets/ticket.js b/Commands/Tickets/ticket.js deleted file mode 100644 index 2256e30..0000000 --- a/Commands/Tickets/ticket.js +++ /dev/null @@ -1,1060 +0,0 @@ -const fs = require("fs"); -const { - EmbedBuilder, - ApplicationCommandOptionType, - ApplicationCommandType, - ChannelType, - ActionRowBuilder, - ButtonBuilder, - StringSelectMenuBuilder, - StringSelectMenuOptionBuilder, - ButtonStyle, - MessageCollector, - MessageFlags, - SeparatorBuilder, - ContainerBuilder, - TextDisplayBuilder, -} = require("discord.js"); -const yaml = require("js-yaml"); - -const db = require("../../Structures/Database.js"); - -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"); - -async function isBlacklisted(userId) { - let blacklistData; - try { - blacklistData = JSON.parse( - fs.readFileSync("./Data/BlacklistedUsers.json", "utf8"), - ); - } catch (error) { - console.error("Error reading BlacklistedUsers.json:", error); - return false; - } - - if (!blacklistData || !Array.isArray(blacklistData.blacklistedUsers)) { - console.error("blacklistedUsers is not defined or is not an array"); - return false; - } - - return blacklistData.blacklistedUsers.includes(userId); -} - -async function getClockedInUsers(guild) { - const clockedInUsers = new Set(); - - if (typeof db.getProfile !== "function") { - console.error( - "db.getProfile is not a function. Check Structures/Database.js", - ); - return clockedInUsers; - } - - const rows = guild.members.cache.map((m) => m.id); - - for (const id of rows) { - try { - const profile = db.getProfile(id); - if (profile && profile.clockedIn) { - clockedInUsers.add(id); - } - } catch (error) { - console.error(`Error getting profile for ${id}:`, error); - } - } - - return clockedInUsers; -} - -function getDepartmentSystem() { - return supportbot.Ticket?.DepartmentSystem || {}; -} - -function getPrioritySystem() { - return supportbot.Ticket?.PrioritySystem || {}; -} - -function getAIModeConfig() { - return supportbot.Ticket?.AIMode || {}; -} - -function isAIModeEnabledGlobally() { - return getAIModeConfig().Enabled === true; -} - -function areDepartmentsEnabled() { - return ( - supportbot.Ticket?.TicketType !== "threads" && - supportbot.Ticket?.DepartmentSystem?.Enabled === true - ); -} - -function arePrioritiesEnabled() { - return ( - supportbot.Ticket?.TicketType !== "threads" && - supportbot.Ticket?.PrioritySystem?.Enabled === true - ); -} - -function getSelectedDepartmentKey(interaction) { - const departmentSystem = getDepartmentSystem(); - const departmentsEnabled = areDepartmentsEnabled(); - const departments = departmentSystem.Departments || {}; - - if (!departmentsEnabled) { - return departmentSystem.DefaultDepartment || "general"; - } - - const selectedDepartment = - interaction.options?.getString?.("department") || - interaction.department || - departmentSystem.DefaultDepartment || - "general"; - - if (departments[selectedDepartment]) { - return selectedDepartment; - } - - const firstDepartmentKey = Object.keys(departments)[0]; - return firstDepartmentKey || departmentSystem.DefaultDepartment || "general"; -} - -function getDepartmentConfig(departmentKey) { - if (!departmentKey) return null; - return getDepartmentSystem().Departments?.[departmentKey] || null; -} - -function getPriorityKey(interaction, departmentKey) { - if (!arePrioritiesEnabled()) { - return null; - } - - const prioritySystem = getPrioritySystem(); - const departmentConfig = getDepartmentConfig(departmentKey); - - return ( - interaction.options?.getString?.("priority") || - interaction.priority || - departmentConfig?.DefaultPriority || - prioritySystem.DefaultPriority || - "medium" - ); -} - -function getPriorityConfig(priorityKey) { - if (!priorityKey) return null; - return getPrioritySystem().Priorities?.[priorityKey] || null; -} - -function buildTicketChannelName(ticketNumberID, priorityKey, departmentConfig) { - const defaultPrefix = - supportbot.Ticket?.ChannelPrefix || supportbot.Ticket?.Channel || "ticket-"; - - const departmentPrefix = departmentConfig?.ChannelPrefix || defaultPrefix; - const safePrefix = String(departmentPrefix).trim() || "ticket-"; - const baseName = `${safePrefix}${ticketNumberID}`; - - if (!arePrioritiesEnabled() || !priorityKey) { - return baseName; - } - - return `${baseName}-${String(priorityKey).toLowerCase()}`; -} - -function buildAIModeButtons(ticketId, aiEnabled = false) { - const enableButton = new ButtonBuilder() - .setCustomId(`ticket_ai_enable:${ticketId}`) - .setLabel(msgconfig.Ticket.AIMode.EnableButton) - .setStyle(ButtonStyle.Success) - .setDisabled(aiEnabled); - - const disableButton = new ButtonBuilder() - .setCustomId(`ticket_ai_disable:${ticketId}`) - .setLabel(msgconfig.Ticket.AIMode.DisableButton) - .setStyle(ButtonStyle.Secondary) - .setDisabled(!aiEnabled); - - return new ActionRowBuilder().addComponents(enableButton, disableButton); -} - -async function createAITicketThread(parentTicketChannel, interaction) { - const threadName = supportbot.Ticket.AIMode?.ThreadName || "ai-chat"; - - const aiThread = await parentTicketChannel.threads.create({ - name: threadName, - autoArchiveDuration: 60, - reason: "ticket ai mode thread", - }); - - const welcomeEmbed = new EmbedBuilder() - .setTitle(msgconfig.Ticket.AIMode.ThreadWelcomeTitle) - .setDescription(msgconfig.Ticket.AIMode.ThreadWelcomeMessage) - .setColor(supportbot.Embed.Colours.General); - - await aiThread.send({ embeds: [welcomeEmbed] }); - - if (typeof db.setTicketAIState === "function") { - db.setTicketAIState(parentTicketChannel.id, { - enabled: true, - aiChannelId: aiThread.id, - aiType: "thread", - enabledBy: interaction.user.id, - enabledAt: new Date().toISOString(), - }); - } - - return aiThread; -} - -async function askTicketQuestions(ticketChannel, interaction, questions) { - return new Promise(async (resolve) => { - const answers = []; - let questionIndex = 0; - - const sendNextQuestion = async () => { - if (questionIndex < questions.length) { - const questionsEmbed = new EmbedBuilder() - .setDescription( - `**${questionIndex + 1}.** ${questions[questionIndex]}`, - ) - .setColor(supportbot.Embed.Colours.General); - - const questionMessage = await ticketChannel.send({ - embeds: [questionsEmbed], - }); - - const collector = new MessageCollector(ticketChannel, { - filter: (m) => m.author.id === interaction.user.id, - max: 1, - time: 60000, - }); - - collector.on("collect", async (message) => { - answers.push({ - question: questions[questionIndex], - answer: message.content, - }); - - questionIndex++; - - await message.delete().catch(() => {}); - await questionMessage.delete().catch(() => {}); - - sendNextQuestion(); - }); - } else { - const summaryEmbed = new EmbedBuilder() - .setTitle(msgconfig.Ticket.TicketQuestions.Details.Title) - .setDescription( - msgconfig.Ticket.TicketQuestions.Details.Description.replace( - "%user%", - interaction.user.id, - ), - ) - .setColor(supportbot.Embed.Colours.General) - .addFields( - answers.map((a) => ({ - name: `Q: ${a.question}`, - value: `A: ${a.answer}`, - inline: false, - })), - ) - .setTimestamp(); - - await ticketChannel.send({ embeds: [summaryEmbed] }); - - resolve(answers); - } - }; - - sendNextQuestion(); - }); -} - -async function assignTicketToUser( - ticketChannel, - Staff, - Admin, - interaction, - departmentRole = null, - attempts = 0, -) { - try { - const clockedInUsers = await getClockedInUsers(interaction.guild); - - let eligibleClockedInUsers = Array.from(clockedInUsers); - - if (departmentRole) { - eligibleClockedInUsers = eligibleClockedInUsers.filter((userId) => { - const member = interaction.guild.members.cache.get(userId); - return member?.roles?.cache?.has(departmentRole.id); - }); - } - - const claimChannel = interaction.guild.channels.cache.get( - supportbot.Ticket.ClaimTickets.Channel, - ); - - if (!claimChannel) { - console.error("Claim channel not found"); - return null; - } - - const claimPing = departmentRole - ? `<@&${departmentRole.id}> <@&${Admin.id}>` - : `<@&${Staff.id}> <@&${Admin.id}>`; - - const claimEmbed = new EmbedBuilder() - .setTitle(msgconfig.Ticket.ClaimTickets.ClaimTitle) - .setDescription( - msgconfig.Ticket.ClaimTickets.ClaimMessage.replace( - "%user%", - interaction.user.id, - ), - ) - .setColor(supportbot.Embed.Colours.General); - - const claimButton = new ButtonBuilder() - .setCustomId(`claimticket-${ticketChannel.id}`) - .setLabel(supportbot.Ticket.ClaimTickets.ButtonTitle || "Claim Ticket") - .setEmoji(supportbot.Ticket.ClaimTickets.ButtonEmoji || null) - .setStyle(ButtonStyle.Primary); - - const row = new ActionRowBuilder().addComponents(claimButton); - - if (eligibleClockedInUsers.length === 0 || attempts >= 2) { - const claimMessage = await claimChannel.send({ - content: claimPing, - embeds: [claimEmbed], - components: [row], - }); - - const filter = (i) => { - if (i.customId !== `claimticket-${ticketChannel.id}`) return false; - - const hasAdmin = i.member.roles.cache.has(Admin.id); - const hasStaff = i.member.roles.cache.has(Staff.id); - const hasDepartmentRole = departmentRole - ? i.member.roles.cache.has(departmentRole.id) - : false; - - return hasAdmin || hasStaff || hasDepartmentRole; - }; - - const collector = claimMessage.createMessageComponentCollector({ - filter, - time: 120000, - }); - - collector.on("collect", async (i) => { - await i.deferUpdate().catch(() => {}); - - if (supportbot.Ticket.TicketType === "threads") { - await ticketChannel.members.add(i.user.id).catch(() => {}); - } - - await ticketChannel.send({ - content: `<@${i.user.id}> has claimed the ticket.`, - }); - - if (typeof db.claimTicket === "function") { - db.claimTicket(ticketChannel.id, i.user.id); - } - - await claimMessage.edit({ components: [] }).catch(() => {}); - }); - - collector.on("end", async (collected) => { - if (collected.size === 0) { - await claimMessage.delete().catch(() => {}); - await claimChannel.send({ - content: `Ticket is still unclaimed.\nCC: ${claimPing}`, - embeds: [ - new EmbedBuilder() - .setDescription( - `Ticket is still unclaimed: <#${ticketChannel.id}>`, - ) - .setColor(supportbot.Embed.Colours.Warn), - ], - }); - } - }); - - return null; - } - - const assignedUserId = - eligibleClockedInUsers[ - Math.floor(Math.random() * eligibleClockedInUsers.length) - ]; - - const assignedUser = await interaction.guild.members - .fetch(assignedUserId) - .catch(() => null); - - if (!assignedUser) { - return null; - } - - const claimMessage = await claimChannel.send({ - content: `${assignedUser}`, - embeds: [claimEmbed], - components: [row], - }); - - const filter = (i) => { - if (i.customId !== `claimticket-${ticketChannel.id}`) return false; - return i.user.id === assignedUserId; - }; - - const collector = claimMessage.createMessageComponentCollector({ - filter, - time: 120000, - }); - - collector.on("collect", async (i) => { - await i.deferUpdate().catch(() => {}); - - if (supportbot.Ticket.TicketType === "threads") { - await ticketChannel.members.add(i.user.id).catch(() => {}); - } - - await ticketChannel - .send({ - content: `<@${assignedUserId}> has claimed the ticket.`, - }) - .catch(() => {}); - - if (typeof db.claimTicket === "function") { - db.claimTicket(ticketChannel.id, assignedUserId); - } - - await claimMessage.edit({ components: [] }).catch(() => {}); - }); - - collector.on("end", async (collected) => { - if (collected.size === 0) { - await claimMessage.delete().catch(() => {}); - - if (attempts >= 1) { - await claimChannel.send({ - content: `No one claimed the ticket. Tagging roles: ${claimPing}`, - embeds: [ - new EmbedBuilder() - .setDescription( - `Ticket is still unclaimed: <#${ticketChannel.id}>`, - ) - .setColor(supportbot.Embed.Colours.Warn), - ], - }); - - const retryClaimMessage = await claimChannel.send({ - content: claimPing, - embeds: [claimEmbed], - components: [row], - }); - - const retryFilter = (i) => { - if (i.customId !== `claimticket-${ticketChannel.id}`) return false; - - const hasAdmin = i.member.roles.cache.has(Admin.id); - const hasStaff = i.member.roles.cache.has(Staff.id); - const hasDepartmentRole = departmentRole - ? i.member.roles.cache.has(departmentRole.id) - : false; - - return hasAdmin || hasStaff || hasDepartmentRole; - }; - - const retryCollector = - retryClaimMessage.createMessageComponentCollector({ - filter: retryFilter, - time: 120000, - }); - - retryCollector.on("collect", async (i) => { - await i.deferUpdate().catch(() => {}); - - if (supportbot.Ticket.TicketType === "threads") { - await ticketChannel.members.add(i.user.id).catch(() => {}); - } - - await ticketChannel.send({ - content: `<@${i.user.id}> has claimed the ticket.`, - }); - - if (typeof db.claimTicket === "function") { - db.claimTicket(ticketChannel.id, i.user.id); - } - - await retryClaimMessage.edit({ components: [] }).catch(() => {}); - }); - - retryCollector.on("end", async (retryCollected) => { - if (retryCollected.size === 0) { - await retryClaimMessage.delete().catch(() => {}); - await claimChannel.send({ - content: `No one claimed the ticket. Tagging roles: ${claimPing}`, - }); - } - }); - } else { - const ReassigningEmbed = new EmbedBuilder() - .setTitle(msgconfig.Ticket.ClaimTickets.ReassigningTitle) - .setDescription( - msgconfig.Ticket.ClaimTickets.ReassigningMessage.replace( - "%user%", - assignedUserId, - ), - ) - .setColor(supportbot.Embed.Colours.General); - - await claimChannel.send({ - embeds: [ReassigningEmbed], - components: [], - }); - - await assignTicketToUser( - ticketChannel, - Staff, - Admin, - interaction, - departmentRole, - attempts + 1, - ); - } - } - }); - - return assignedUserId; - } catch (error) { - console.error("Error in assignTicketToUser function:", error); - return null; - } -} - -module.exports = new Command({ - name: cmdconfig.OpenTicket.Command, - description: cmdconfig.OpenTicket.Description, - type: ApplicationCommandType.ChatInput, - - options: [ - ...(areDepartmentsEnabled() - ? [ - { - name: "department", - description: "Choose a department", - type: ApplicationCommandOptionType.String, - required: true, - choices: Object.entries(getDepartmentSystem().Departments || {}) - .slice(0, 25) - .map(([key, dept]) => ({ - name: dept.Name || key, - value: key, - })), - }, - ] - : []), - - ...(arePrioritiesEnabled() - ? [ - { - name: "priority", - description: "Choose a priority", - type: ApplicationCommandOptionType.String, - required: false, - choices: Object.entries(getPrioritySystem().Priorities || {}) - .slice(0, 25) - .map(([key, priority]) => ({ - name: priority.Name || key, - value: key, - })), - }, - ] - : []), - - { - name: "reason", - description: "Ticket Reason", - type: ApplicationCommandOptionType.String, - required: false, - }, - ], - - permissions: cmdconfig.OpenTicket.Permission, - - async run(interaction) { - try { - if (!interaction.deferred && !interaction.replied) { - await interaction.deferReply({ flags: MessageFlags.Ephemeral }).catch(() => {}); - } - - if (await isBlacklisted(interaction.user.id)) { - return interaction.editReply({ - content: msgconfig.Ticket.Blacklisted, - flags: MessageFlags.Ephemeral, - }); - } - - let TicketReason = null; - if (supportbot.Ticket.TicketReason) { - TicketReason = - interaction.options?.getString?.("reason") || - interaction.reason || - null; - } - - let TicketData = { tickets: db.getAllTickets() }; - - const { getRole, getChannel } = interaction.client; - - if ( - supportbot.Ticket.TicketsPerUser && - TicketData.tickets.filter( - (t) => t.user === interaction.user.id && t.open, - ).length >= supportbot.Ticket.TicketsPerUser - ) { - return interaction.editReply({ - embeds: [ - { - title: "Too Many Tickets!", - description: `You can't have more than ${supportbot.Ticket.TicketsPerUser} open tickets!`, - color: supportbot.Embed.Colours.Warn, - }, - ], - flags: MessageFlags.Ephemeral, - }); - } - - const usingThreads = supportbot.Ticket.TicketType === "threads"; - const departmentsEnabled = areDepartmentsEnabled(); - const prioritiesEnabled = arePrioritiesEnabled(); - const aiModeEnabled = isAIModeEnabledGlobally(); - - const departmentKey = departmentsEnabled - ? getSelectedDepartmentKey(interaction) - : null; - const departmentConfig = departmentsEnabled - ? getDepartmentConfig(departmentKey) - : null; - - const priorityKey = prioritiesEnabled - ? getPriorityKey(interaction, departmentKey) - : null; - const priorityConfig = prioritiesEnabled - ? getPriorityConfig(priorityKey) - : null; - - let ticketNumberID = await TicketNumberID.pad(); - const TicketSubject = TicketReason || msgconfig.Ticket.InvalidSubject; - const ticketChannelName = buildTicketChannelName( - ticketNumberID, - priorityKey, - departmentConfig, - ); - - const TicketExists = new EmbedBuilder() - .setTitle("Ticket Exists!") - .setDescription(msgconfig.Ticket.TicketExists); - - if ( - await interaction.guild.channels.cache.find( - (ticketChannel) => ticketChannel.name === ticketChannelName, - ) - ) { - return await interaction.editReply({ - embeds: [TicketExists], - flags: MessageFlags.Ephemeral, - }); - } - - const Staff = await getRole( - supportbot.Roles.StaffMember.Staff, - interaction.guild, - ); - const Admin = await getRole( - supportbot.Roles.StaffMember.Admin, - interaction.guild, - ); - - const departmentRoleId = departmentsEnabled - ? departmentConfig?.Role || null - : null; - const DepartmentRole = departmentRoleId - ? await getRole(departmentRoleId, interaction.guild) - : null; - - if (!Staff || !Admin) { - return interaction.editReply({ - content: - "Some roles seem to be missing!\nPlease check for errors when starting the bot.", - flags: MessageFlags.Ephemeral, - }); - } - - let ticketChannel; - - if (usingThreads) { - const TicketHome = interaction.guild.channels.cache.find( - (c) => - c.name === supportbot.Ticket.TicketHome || - c.id === supportbot.Ticket.TicketHome, - ); - - const channel = await getChannel( - supportbot.Ticket.TicketHome, - interaction.guild, - ); - - ticketChannel = await channel.threads.create({ - name: ticketChannelName, - type: ChannelType.PrivateThread, - parent: TicketHome, - autoArchiveDuration: 60, - reason: "support ticket", - }); - } else { - let targetCategoryId = supportbot.Ticket.TicketChannelsCategory; - - if (departmentsEnabled && departmentConfig?.Category) { - targetCategoryId = departmentConfig.Category; - } - - const category = interaction.guild.channels.cache.find( - (c) => c.name === targetCategoryId || c.id === targetCategoryId, - ); - - if (!category) { - return interaction.editReply({ - content: "The ticket category does not exist!", - flags: MessageFlags.Ephemeral, - }); - } - - ticketChannel = await interaction.guild.channels.create({ - name: ticketChannelName, - type: ChannelType.GuildText, - parent: category.id, - reason: "support ticket", - }); - - if ( - !departmentsEnabled && - category.children.cache && - category.children.cache.size >= 50 - ) { - const secondaryCategory = interaction.guild.channels.cache.find( - (c) => - c.name === supportbot.Ticket.TicketChannelsCategory2 || - c.id === supportbot.Ticket.TicketChannelsCategory2, - ); - - if (secondaryCategory) { - await ticketChannel.setParent(secondaryCategory.id); - } else { - return interaction.editReply({ - content: "The secondary ticket category does not exist!", - flags: MessageFlags.Ephemeral, - }); - } - } - } - - if (supportbot.Ticket.ClaimTickets.Enabled) { - await assignTicketToUser( - ticketChannel, - Staff, - Admin, - interaction, - DepartmentRole || null, - ); - } - - if (usingThreads) { - await ticketChannel.members.add(interaction.user.id); - } else { - await ticketChannel.permissionOverwrites.create(interaction.user.id, { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }); - - await ticketChannel.permissionOverwrites.create(Admin.id, { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }); - - await ticketChannel.permissionOverwrites.create(Staff.id, { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }); - - if (DepartmentRole) { - await ticketChannel.permissionOverwrites.create(DepartmentRole.id, { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }); - } - - await ticketChannel.permissionOverwrites.create( - interaction.guild.roles.everyone.id, - { - ViewChannel: false, - }, - ); - } - - db.addTicket({ - id: ticketChannel.id, - name: ticketChannel.name, - user: interaction.user.id, - number: ticketNumberID, - reason: TicketSubject, - open: true, - subUsers: [], - createdAt: new Date().toISOString(), - claimedAt: null, - department: departmentKey, - priority: priorityKey, - aiModeEnabled: !usingThreads && aiModeEnabled, - aiChannelId: null, - questionAnswers: [], - }); - - TicketData.tickets = db.getAllTickets(); - - const CreatedTicket = new EmbedBuilder() - .setDescription( - msgconfig.Ticket.TicketCreatedAlert.replace( - /%ticketauthor%/g, - interaction.user.id, - ) - .replace(/%ticketid%/g, ticketChannel.id) - .replace(/%ticketusername%/g, interaction.user.username) - .replace(/%ticketreason%/g, TicketSubject), - ) - .setColor(supportbot.Embed.Colours.General); - - await interaction.editReply({ - embeds: [CreatedTicket], - flags: MessageFlags.Ephemeral, - }); - - const ticketMsgContainer = new ContainerBuilder(); - - if (supportbot.Embed.Colours.General) { - ticketMsgContainer.setAccentColor( - parseInt(supportbot.Embed.Colours.General.replace("#", ""), 16), - ); - } - - const ticketAuthor = new TextDisplayBuilder().setContent( - msgconfig.Ticket.TicketAuthorTitle.replace( - /%ticketauthor%/g, - interaction.user.id, - ) - .replace(/%ticketid%/g, ticketChannel.id) - .replace(/%ticketusername%/g, interaction.user.username), - ); - - ticketMsgContainer.addTextDisplayComponents(ticketAuthor); - - const ticketTitle = new TextDisplayBuilder().setContent( - msgconfig.Ticket.TicketTitle.replace( - /%ticketauthor%/g, - interaction.user.id, - ) - .replace(/%ticketid%/g, ticketChannel.id) - .replace(/%ticketusername%/g, interaction.user.username), - ); - - ticketMsgContainer.addTextDisplayComponents(ticketTitle); - - const seperator2 = new SeparatorBuilder().setDivider(true); - ticketMsgContainer.addSeparatorComponents(seperator2); - - const ticketDescription = new TextDisplayBuilder().setContent( - msgconfig.Ticket.TicketMessage.replace( - /%ticketauthor%/g, - interaction.user.id, - ) - .replace(/%ticketid%/g, ticketChannel.id) - .replace(/%ticketusername%/g, interaction.user.username) - .replace(/%ticketreason%/g, TicketSubject), - ); - - ticketMsgContainer.addTextDisplayComponents(ticketDescription); - - const seperator3 = new SeparatorBuilder().setDivider(true); - ticketMsgContainer.addSeparatorComponents(seperator3); - - if (supportbot.Ticket.TicketReason && TicketReason) { - const ticketReason = new TextDisplayBuilder().setContent( - `**Reason:**\n${TicketReason}`, - ); - ticketMsgContainer.addTextDisplayComponents(ticketReason); - } - - if (departmentsEnabled && departmentConfig) { - const deptLabel = departmentConfig?.Name || departmentKey || "General"; - const deptEmoji = departmentConfig?.Emoji || "🎫"; - - const departmentText = new TextDisplayBuilder().setContent( - `**Department:** ${deptEmoji} ${deptLabel}`, - ); - - ticketMsgContainer.addTextDisplayComponents(departmentText); - } - - if (prioritiesEnabled && priorityConfig && priorityKey) { - const priorityLabel = priorityConfig?.Name || priorityKey || "Medium"; - const priorityEmoji = priorityConfig?.Emoji || "🟡"; - - const priorityText = new TextDisplayBuilder().setContent( - `**Priority:** ${priorityEmoji} ${priorityLabel}`, - ); - - ticketMsgContainer.addTextDisplayComponents(priorityText); - } - - if (usingThreads && aiModeEnabled) { - const aiSeparator = new SeparatorBuilder().setDivider(true); - ticketMsgContainer.addSeparatorComponents(aiSeparator); - - const aiTitleText = new TextDisplayBuilder().setContent( - `**${msgconfig.Ticket.AIMode.Title}**`, - ); - ticketMsgContainer.addTextDisplayComponents(aiTitleText); - - const aiBodyText = new TextDisplayBuilder().setContent( - msgconfig.Ticket.AIMode.Description, - ); - ticketMsgContainer.addTextDisplayComponents(aiBodyText); - - const aiRow = buildAIModeButtons(ticketChannel.id, false); - ticketMsgContainer.addActionRowComponents(aiRow); - } - - const selectMenuRow = new ActionRowBuilder(); - - const SelectMenus = new StringSelectMenuBuilder() - .setCustomId("ticketcontrolpanel") - .setPlaceholder("Ticket Control Panel") - .addOptions( - new StringSelectMenuOptionBuilder() - .setLabel("Close") - .setDescription("Close the ticket.") - .setEmoji(supportbot.SelectMenus.Tickets.CloseEmoji) - .setValue("ticketclose"), - new StringSelectMenuOptionBuilder() - .setLabel("Voice Channel") - .setDescription("Create a support voice channel.") - .setEmoji(supportbot.SelectMenus.Tickets.VCEmoji) - .setValue("supportvc"), - new StringSelectMenuOptionBuilder() - .setLabel("Archive") - .setDescription("Archive the ticket.") - .setEmoji(supportbot.SelectMenus.Tickets.ArchiveEmoji) - .setValue("archiveticket"), - ...(supportbot.Ticket.TicketType === "threads" - ? [ - new StringSelectMenuOptionBuilder() - .setLabel("Lock Ticket") - .setDescription("Lock the ticket.") - .setEmoji(supportbot.SelectMenus.Tickets.LockEmoji) - .setValue("lockticket"), - ] - : []), - new StringSelectMenuOptionBuilder() - .setLabel("Rename Ticket") - .setDescription("Rename the ticket.") - .setEmoji(supportbot.SelectMenus.Tickets.RenameEmoji) - .setValue("renameticket"), - ); - - if (supportbot.Ticket.TicketType === "threads") { - SelectMenus.addOptions( - new StringSelectMenuOptionBuilder() - .setLabel("Enable Invites") - .setDescription("Enable invites.") - .setEmoji(supportbot.SelectMenus.Tickets.EnableInvitesEmoji) - .setValue("enableinvites"), - new StringSelectMenuOptionBuilder() - .setLabel("Disable Invites") - .setDescription("Disable invites.") - .setEmoji(supportbot.SelectMenus.Tickets.DisableInvitesEmoji) - .setValue("disableinvites"), - ); - } - - selectMenuRow.addComponents(SelectMenus); - ticketMsgContainer.addActionRowComponents(selectMenuRow); - - await ticketChannel.send({ - flags: MessageFlags.IsComponentsV2, - components: [ticketMsgContainer], - }); - - const departmentQuestions = - departmentsEnabled && departmentConfig?.Questions - ? departmentConfig.Questions - : []; - - const globalQuestionsEnabled = supportbot.Ticket.Questions?.Enabled; - const globalQuestions = supportbot.Ticket.Questions?.List || []; - - let questionsToAsk = []; - - if (departmentQuestions.length > 0) { - questionsToAsk = departmentQuestions; - } else if (globalQuestionsEnabled && globalQuestions.length > 0) { - questionsToAsk = globalQuestions; - } - - let ticketQuestionsAndAnswers = []; - - if (questionsToAsk.length > 0) { - ticketQuestionsAndAnswers = await askTicketQuestions( - ticketChannel, - interaction, - questionsToAsk, - ); - - if (typeof db.updateTicketQuestionAnswers === "function") { - db.updateTicketQuestionAnswers( - ticketChannel.id, - ticketQuestionsAndAnswers, - ); - } - } - - if (!usingThreads && aiModeEnabled) { - const aiThread = await createAITicketThread( - ticketChannel, - interaction, - ).catch((error) => { - console.error("Error creating AI mode thread:", error); - return null; - }); - } - } catch (error) { - console.error("Error in run method:", error); - - if (!interaction.replied && !interaction.deferred) { - return interaction.editReply({ - content: "An error occurred while creating the ticket.", - flags: MessageFlags.Ephemeral, - }); - } - } - }, -}); diff --git a/Commands/Tickets/ticketpanel.js b/Commands/Tickets/ticketpanel.js deleted file mode 100644 index 0862d12..0000000 --- a/Commands/Tickets/ticketpanel.js +++ /dev/null @@ -1,342 +0,0 @@ -// SupportBot | Emerald Services -// Ticket Panel Command - -const fs = require("fs"); -const { - ApplicationCommandType, - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - MessageFlags, - ContainerBuilder, - TextDisplayBuilder, - MediaGalleryBuilder, - SeparatorBuilder, - SectionBuilder, - StringSelectMenuBuilder, - ThumbnailBuilder, - EmbedBuilder, -} = require("discord.js"); -const yaml = require("js-yaml"); - -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"); - -function parseButtonStyle(style) { - switch (String(style || "1")) { - case "1": - return ButtonStyle.Primary; - case "2": - return ButtonStyle.Secondary; - case "3": - return ButtonStyle.Danger; - case "4": - return ButtonStyle.Success; - default: - return ButtonStyle.Primary; - } -} - -function getPanelMessage(key, fallback) { - return panelconfig.Messages?.[key] || fallback; -} - -module.exports = new Command({ - name: cmdconfig.TicketPanel.Command, - description: cmdconfig.TicketPanel.Description, - type: ApplicationCommandType.ChatInput, - options: [], - permissions: cmdconfig.TicketPanel.Permission, - - async run(interaction) { - if (!panelconfig.Panel) { - console.log( - "\u001b[31m", - `[TICKET PANEL]`, - "\u001b[33m", - "Ticket Panel is not setup, You can set this in", - `\u001b[31m`, - "/Configs/ticket-panel.yml", - `\n `, - ); - - return interaction.reply({ - content: getPanelMessage( - "NotConfigured", - "Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`.", - ), - flags: MessageFlags.Ephemeral, - }); - } - - const { getChannel } = interaction.client; - - const channel = await getChannel(supportbot.Ticket.TicketHome, interaction.guild); - - if (!channel) { - console.log(`[TICKET PANEL] TicketHome channel not found. Please check your config file.`); - return interaction.reply({ - content: getPanelMessage( - "InvalidChannel", - "TicketHome channel not found. Please check your config file.", - ), - flags: MessageFlags.Ephemeral, - }); - } - - const panelRow = db.getTicketPanel(); - - if (panelRow) { - try { - const oldMessage = await channel.messages.fetch(panelRow.message_id); - if (oldMessage) { - await oldMessage.delete().catch(() => { }); - } - } catch (err) { - console.log(`[TICKET PANEL] Previous panel message not found, creating a new one.`); - } - } - - const departmentSystem = supportbot.Ticket?.DepartmentSystem || {}; - const prioritySystem = supportbot.Ticket?.PrioritySystem || {}; - const usingThreads = supportbot.Ticket?.TicketType === "threads"; - - const departmentsEnabled = departmentSystem.Enabled === true && !usingThreads; - const prioritiesEnabled = prioritySystem.Enabled === true && !usingThreads; - const departments = departmentSystem.Departments || {}; - - if (usingThreads && (departmentSystem.Enabled || prioritySystem.Enabled)) { - console.log( - "[SupportBot] Departments and Priority are disabled because TicketType is set to 'threads'.", - ); - } - - const departmentOptions = Object.entries(departments) - .slice(0, 25) - .map(([key, dept]) => ({ - label: dept.Name || key, - description: - dept.PanelDescription || - dept.Description || - panelconfig.DepartmentMenu?.DefaultDescription || - `Open a ${String(dept.Name || key).toLowerCase()} ticket`, - value: key, - emoji: - dept.Emoji || - panelconfig.DepartmentMenu?.DefaultEmoji || - panelconfig.Button?.Emoji || - undefined, - })); - - const shouldUseDepartments = departmentsEnabled && departmentOptions.length > 0; - - let openerComponent; - - if (shouldUseDepartments) { - openerComponent = new StringSelectMenuBuilder() - .setCustomId(panelconfig.DepartmentMenu?.CustomId || "ticketdepartmentselect") - .setPlaceholder( - panelconfig.DepartmentMenu?.Placeholder || - departmentSystem.Placeholder || - panelconfig.Button?.Text || - "Choose a ticket department", - ) - .addOptions(departmentOptions); - } else { - openerComponent = new ButtonBuilder() - .setCustomId(panelconfig.Button?.CustomId || "createticket") - .setLabel(panelconfig.Button?.Text || "Create Ticket") - .setStyle(parseButtonStyle(panelconfig.Button?.Color)); - - if (panelconfig.Button?.Emoji) { - openerComponent.setEmoji(panelconfig.Button.Emoji); - } - } - - const panelContainer = new ContainerBuilder(); - - if (panelconfig.Style?.Color) { - panelContainer.setAccentColor(parseInt(String(panelconfig.Style.Color).replace("#", ""), 16)); - } - - const layout = String(panelconfig.Style?.Layout || "3"); - const showImage = panelconfig.Settings?.Image === true && !!panelconfig.Style?.Image; - const showTopTitle = panelconfig.Settings?.TopTitle === true; - const showTopDivider = panelconfig.Settings?.TopDivider === true; - const showBottomTitle = panelconfig.Settings?.BottomTitle === true; - const showBottomDivider = panelconfig.Settings?.BottomDivider === true; - - const panelTitle = new TextDisplayBuilder().setContent( - panelconfig.Style?.Title || "## SupportBot Ticket Creation", - ); - - const rawTitle = (panelconfig.Style?.Title || "SupportBot Ticket Creation").replace(/^#+\s*/, ""); - - const panelDesc = new TextDisplayBuilder().setContent( - panelconfig.Style?.Description || "Click on the button below to create a support ticket.", - ); - - if (showTopTitle) { - panelContainer.addTextDisplayComponents(panelTitle); - } - - if (showTopDivider) { - panelContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true)); - } - - if (layout === "1" || layout === "3") { - panelContainer.addTextDisplayComponents(panelDesc); - } - - if (showImage && layout === "2") { - const panelImage = new MediaGalleryBuilder().addItems([ - { - media: { - url: panelconfig.Style.Image, - }, - }, - ]); - - panelContainer.addMediaGalleryComponents(panelImage); - } - - if (layout === "1") { - const middleSection = new SectionBuilder().addTextDisplayComponents(panelDesc); - - if (!shouldUseDepartments) { - middleSection.setButtonAccessory(openerComponent); - panelContainer.addSectionComponents(middleSection); - } else { - panelContainer.addSectionComponents(middleSection); - panelContainer.addActionRowComponents( - new ActionRowBuilder().addComponents(openerComponent), - ); - } - } - - if (showImage && layout === "3") { - const panelImage = new MediaGalleryBuilder().addItems([ - { - media: { - url: panelconfig.Style.Image, - }, - }, - ]); - - panelContainer.addMediaGalleryComponents(panelImage); - } - - if (showBottomTitle) { - panelContainer.addTextDisplayComponents(panelTitle); - } - - if (showBottomDivider) { - panelContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true)); - } - - if (layout === "2") { - const middleSection = new SectionBuilder().addTextDisplayComponents(panelDesc); - - if (!shouldUseDepartments) { - middleSection.setButtonAccessory(openerComponent); - panelContainer.addSectionComponents(middleSection); - } else { - panelContainer.addSectionComponents(middleSection); - panelContainer.addActionRowComponents( - new ActionRowBuilder().addComponents(openerComponent), - ); - } - } - - if (layout === "3") { - panelContainer.addActionRowComponents(new ActionRowBuilder().addComponents(openerComponent)); - } - - if (layout === "4") { - // Create a classic Discord Embed for Layout 4 - const oldEmbed = new EmbedBuilder() - .setTitle(rawTitle) - .setDescription(panelconfig.Style?.Description || "Click on the button below to create a support ticket."); - - if (panelconfig.Style?.Color) { - oldEmbed.setColor(panelconfig.Style.Color); - } - - if (showImage && panelconfig.Style?.Image) { - oldEmbed.setThumbnail(panelconfig.Style.Image); - } - - try { - const message = await channel.send({ - embeds: [oldEmbed], - components: [new ActionRowBuilder().addComponents(openerComponent)], - }); - - db.saveTicketPanel(message.id, channel.id); - - return interaction.reply({ - content: shouldUseDepartments - ? getPanelMessage( - "PanelSentWithDepartments", - "Ticket panel message with department selection has been sent!", - ) - : getPanelMessage("PanelSent", "Ticket panel message has been sent!"), - flags: MessageFlags.Ephemeral, - }); - } catch (e) { - console.log("Error sending message:", e); - return interaction.reply({ - content: getPanelMessage( - "SendError", - "An error occurred while sending the ticket panel message.", - ), - flags: MessageFlags.Ephemeral, - }); - } - } - - if (showImage && layout === "1") { - const panelImage = new MediaGalleryBuilder().addItems([ - { - media: { - url: panelconfig.Style.Image, - }, - }, - ]); - - panelContainer.addMediaGalleryComponents(panelImage); - } - - try { - const message = await channel.send({ - flags: MessageFlags.IsComponentsV2, - components: [panelContainer], - }); - - db.saveTicketPanel(message.id, channel.id); - - return interaction.reply({ - content: shouldUseDepartments - ? getPanelMessage( - "PanelSentWithDepartments", - "Ticket panel message with department selection has been sent!", - ) - : getPanelMessage("PanelSent", "Ticket panel message has been sent!"), - flags: MessageFlags.Ephemeral, - }); - } catch (e) { - console.log("Error sending message:", e); - return interaction.reply({ - content: getPanelMessage( - "SendError", - "An error occurred while sending the ticket panel message.", - ), - flags: MessageFlags.Ephemeral, - }); - } - }, -}); diff --git a/Commands/Utils/embed.js b/Commands/Utils/embed.js deleted file mode 100644 index 9ce36a4..0000000 --- a/Commands/Utils/embed.js +++ /dev/null @@ -1,468 +0,0 @@ -const { - ApplicationCommandType, - ApplicationCommandOptionType, - ActionRowBuilder, - ButtonBuilder, - EmbedBuilder, - StringSelectMenuBuilder, - ModalBuilder, - TextInputBuilder, - TextInputStyle, - ButtonStyle, - ComponentType, - MessageFlags, -} = require('discord.js'); -const Command = require('../../Structures/Command.js'); -const fs = require('fs'); -const yaml = require('js-yaml'); - -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; - if (typeof color === 'number') return color; - if (typeof color !== 'string') return 0x5865F2; - color = color.replace('#', ''); - if (/^[0-9A-F]{6}$/i.test(color)) { - return parseInt(color, 16); - } - return 0x5865F2; -} - -module.exports = new Command({ - name: cmdconfig.Embed.Command, - description: cmdconfig.Embed.Description, - type: ApplicationCommandType.ChatInput, - options: [ - { - name: 'channel', - description: 'Select channel to send the message', - type: ApplicationCommandOptionType.Channel, - required: true - } - ], - permissions: cmdconfig.Embed.Permission, - - async run(interaction) { - const { getRole } = interaction.client; - let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); - let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); - - if (!SupportStaff || !Admin) { - return interaction.reply('Some roles seem to be missing!\nPlease check for errors when starting the bot.'); - } - - if (!interaction.member.roles.cache.has(SupportStaff.id) && - !interaction.member.roles.cache.has(Admin.id)) { - const NoPerms = new EmbedBuilder() - .setTitle('Invalid Permissions!') - .setDescription(`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``) - .setColor(0xFF0000); - return interaction.reply({ embeds: [NoPerms], flags: MessageFlags.Ephemeral }); - } - - const channel = interaction.options.getChannel('channel'); - - const messageData = { - type: 'embed', - title: 'New Embed', - description: 'Click the buttons below to edit this embed', - color: 0x5865F2, - fields: [], - footer: null, - thumbnail: null, - image: null, - timestamp: false - }; - - function createEmbed() { - const embed = new EmbedBuilder() - .setTitle(messageData.title) - .setDescription(messageData.description) - .setColor(messageData.color); - - if (messageData.fields.length > 0) { - embed.addFields(messageData.fields); - } - if (messageData.footer) { - embed.setFooter({ text: messageData.footer }); - } - if (messageData.thumbnail) { - embed.setThumbnail(messageData.thumbnail); - } - if (messageData.image) { - embed.setImage(messageData.image); - } - if (messageData.timestamp) { - embed.setTimestamp(); - } - - return embed; - } - - const typeRow = new ActionRowBuilder() - .addComponents( - new StringSelectMenuBuilder() - .setCustomId('message_type') - .setPlaceholder('Select message type') - .addOptions([ - { - label: 'Embed Message', - description: 'Send as an embedded message', - value: 'embed' - }, - { - label: 'Text Message', - description: 'Send as a plain text message', - value: 'text' - } - ]) - ); - - function createButtons(messageType) { - const buttons = []; - - if (messageType === 'embed') { - const row1 = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId('edit_title') - .setLabel('Edit Title') - .setStyle(ButtonStyle.Secondary), - new ButtonBuilder() - .setCustomId('edit_description') - .setLabel('Edit Description') - .setStyle(ButtonStyle.Secondary), - new ButtonBuilder() - .setCustomId('edit_color') - .setLabel('Edit Color') - .setStyle(ButtonStyle.Secondary), - new ButtonBuilder() - .setCustomId('add_field') - .setLabel('Add Field') - .setStyle(ButtonStyle.Secondary) - ); - - const row2 = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId('edit_footer') - .setLabel('Edit Footer') - .setStyle(ButtonStyle.Secondary), - new ButtonBuilder() - .setCustomId('edit_image') - .setLabel('Add Image') - .setStyle(ButtonStyle.Secondary), - new ButtonBuilder() - .setCustomId('edit_thumbnail') - .setLabel('Add Thumbnail') - .setStyle(ButtonStyle.Secondary), - new ButtonBuilder() - .setCustomId('toggle_timestamp') - .setLabel('Toggle Timestamp') - .setStyle(ButtonStyle.Secondary) - ); - - buttons.push(row1, row2); - } else { - const textRow = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId('edit_description') - .setLabel('Edit Text') - .setStyle(ButtonStyle.Secondary) - ); - - buttons.push(textRow); - } - - // Send button always shows - const sendRow = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId('send_message') - .setLabel('Send') - .setStyle(ButtonStyle.Success) - ); - - buttons.push(sendRow); - return buttons; - } - - async function updatePreview(i) { - if (messageData.type === 'embed') { - await i.update({ - content: 'Customize your message:', - embeds: [createEmbed()], - components: [typeRow, ...createButtons('embed')] - }); - } else { - await i.update({ - content: messageData.description, - embeds: [], - components: [typeRow, ...createButtons('text')] - }); - } - } - - const message = await interaction.reply({ - content: messageData.type === 'text' ? messageData.description : 'Customize your message:', - components: [typeRow, ...createButtons(messageData.type)], - embeds: messageData.type === 'embed' ? [createEmbed()] : [], - flags: MessageFlags.Ephemeral - }); - - const collector = message.createMessageComponentCollector({ - time: 900000 - }); - - collector.on('collect', async (i) => { - if (i.user.id !== interaction.user.id) { - return i.reply({ - content: 'You cannot edit this message.', - flags: MessageFlags.Ephemeral - }); - } - - switch (i.customId) { - case 'message_type': - messageData.type = i.values[0]; - await updatePreview(i); - break; - - case 'edit_description': - const descModal = new ModalBuilder() - .setCustomId('desc_modal') - .setTitle(messageData.type === 'embed' ? 'Edit Description' : 'Edit Text'); - - descModal.addComponents( - new ActionRowBuilder() - .addComponents( - new TextInputBuilder() - .setCustomId('desc_input') - .setLabel(messageData.type === 'embed' ? 'Description' : 'Message Text') - .setStyle(TextInputStyle.Paragraph) - .setValue(messageData.description) - .setMaxLength(4000) - ) - ); - - await i.showModal(descModal); - break; - - case 'edit_title': - const titleModal = new ModalBuilder() - .setCustomId('title_modal') - .setTitle('Edit Title'); - - titleModal.addComponents( - new ActionRowBuilder() - .addComponents( - new TextInputBuilder() - .setCustomId('title_input') - .setLabel('Title') - .setStyle(TextInputStyle.Short) - .setValue(messageData.title) - .setMaxLength(256) - ) - ); - - await i.showModal(titleModal); - break; - - case 'edit_color': - const colorModal = new ModalBuilder() - .setCustomId('color_modal') - .setTitle('Edit Color'); - - colorModal.addComponents( - new ActionRowBuilder() - .addComponents( - new TextInputBuilder() - .setCustomId('color_input') - .setLabel('Color (HEX)') - .setStyle(TextInputStyle.Short) - .setPlaceholder('#FF0000') - .setMinLength(6) - .setMaxLength(7) - .setValue(messageData.color.toString(16).padStart(6, '0')) - ) - ); - - await i.showModal(colorModal); - break; - - case 'add_field': - const fieldModal = new ModalBuilder() - .setCustomId('field_modal') - .setTitle('Add Field'); - - fieldModal.addComponents( - new ActionRowBuilder() - .addComponents( - new TextInputBuilder() - .setCustomId('field_name') - .setLabel('Field Name') - .setStyle(TextInputStyle.Short) - .setMaxLength(256) - ), - new ActionRowBuilder() - .addComponents( - new TextInputBuilder() - .setCustomId('field_value') - .setLabel('Field Value') - .setStyle(TextInputStyle.Paragraph) - .setMaxLength(1024) - ), - new ActionRowBuilder() - .addComponents( - new TextInputBuilder() - .setCustomId('field_inline') - .setLabel('Inline? (true/false)') - .setStyle(TextInputStyle.Short) - .setValue('false') - .setPlaceholder('true or false') - ) - ); - - await i.showModal(fieldModal); - break; - - case 'edit_footer': - const footerModal = new ModalBuilder() - .setCustomId('footer_modal') - .setTitle('Edit Footer'); - - footerModal.addComponents( - new ActionRowBuilder() - .addComponents( - new TextInputBuilder() - .setCustomId('footer_input') - .setLabel('Footer Text') - .setStyle(TextInputStyle.Short) - .setValue(messageData.footer || '') - .setMaxLength(2048) - ) - ); - - await i.showModal(footerModal); - break; - - case 'edit_image': - const imageModal = new ModalBuilder() - .setCustomId('image_modal') - .setTitle('Add Image'); - - imageModal.addComponents( - new ActionRowBuilder() - .addComponents( - new TextInputBuilder() - .setCustomId('image_input') - .setLabel('Image URL') - .setStyle(TextInputStyle.Short) - .setValue(messageData.image || '') - .setPlaceholder('https://example.com/image.png') - ) - ); - - await i.showModal(imageModal); - break; - - case 'edit_thumbnail': - const thumbnailModal = new ModalBuilder() - .setCustomId('thumbnail_modal') - .setTitle('Add Thumbnail'); - - thumbnailModal.addComponents( - new ActionRowBuilder() - .addComponents( - new TextInputBuilder() - .setCustomId('thumbnail_input') - .setLabel('Thumbnail URL') - .setStyle(TextInputStyle.Short) - .setValue(messageData.thumbnail || '') - .setPlaceholder('https://example.com/thumbnail.png') - ) - ); - - await i.showModal(thumbnailModal); - break; - - case 'toggle_timestamp': - messageData.timestamp = !messageData.timestamp; - await updatePreview(i); - break; - - case 'send_message': - try { - if (messageData.type === 'embed') { - await channel.send({ embeds: [createEmbed()] }); - } else { - await channel.send(messageData.description); - } - await i.update({ - content: 'Message sent successfully!', - components: [], - embeds: [] - }); - collector.stop(); - } catch (error) { - await i.reply({ - content: 'Failed to send message. Please check channel permissions.', - flags: MessageFlags.Ephemeral - }); - } - break; - } - }); - - // Handle modal submissions - interaction.client.on('interactionCreate', async (modal) => { - if (!modal.isModalSubmit()) return; - if (modal.user.id !== interaction.user.id) return; - - switch (modal.customId) { - case 'title_modal': - messageData.title = modal.fields.getTextInputValue('title_input'); - break; - case 'desc_modal': - messageData.description = modal.fields.getTextInputValue('desc_input'); - break; - case 'color_modal': - messageData.color = parseColor(modal.fields.getTextInputValue('color_input')); - break; - case 'field_modal': - const inlineValue = modal.fields.getTextInputValue('field_inline').toLowerCase(); - messageData.fields.push({ - name: modal.fields.getTextInputValue('field_name'), - value: modal.fields.getTextInputValue('field_value'), - inline: inlineValue === 'true' - }); - break; - case 'footer_modal': - messageData.footer = modal.fields.getTextInputValue('footer_input'); - break; - case 'image_modal': - messageData.image = modal.fields.getTextInputValue('image_input'); - break; - case 'thumbnail_modal': - messageData.thumbnail = modal.fields.getTextInputValue('thumbnail_input'); - break; - default: - return; - } - - await updatePreview(modal); - }); - - collector.on('end', () => { - interaction.editReply({ - content: 'Message editor timed out.', - components: [], - embeds: [] - }).catch(() => {}); - }); - } -}); \ No newline at end of file diff --git a/Commands/Utils/mod.js b/Commands/Utils/mod.js deleted file mode 100644 index c4e0692..0000000 --- a/Commands/Utils/mod.js +++ /dev/null @@ -1,261 +0,0 @@ -const fs = require("fs"); -const { ApplicationCommandOptionType, ApplicationCommandType, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, MessageFlags } = require("discord.js"); -const yaml = require("js-yaml"); -const supportbot = require("../../Structures/ConfigStore").supportbot; -const cmdconfig = require("../../Structures/ConfigStore").commands; -const msgconfig = require("../../Structures/ConfigStore").messages; - -const Command = require("../../Structures/Command.js"); - -// Function to chunk array into smaller arrays -function chunkArray(arr, size) { - const chunks = []; - for (let i = 0; i < arr.length; i += size) { - chunks.push(arr.slice(i, i + size)); - } - return chunks; -} - -module.exports = new Command({ - name: cmdconfig.Mod.Command, - description: cmdconfig.Mod.Command, - type: ApplicationCommandType.ChatInput, - options: [ - { - name: cmdconfig.Mod.TicketBlacklist.Command, - description: cmdconfig.Mod.TicketBlacklist.Description, - type: ApplicationCommandOptionType.SubcommandGroup, - options: [ - { - name: cmdconfig.Mod.TicketBlacklist.Add.Command, - description: msgconfig.Mod.TicketBlacklist.Add.Description || "Add a user to the ticket blacklist", - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: 'user', - description: msgconfig.Mod.TicketBlacklist.Add.UserDescription || "User to blacklist", - type: ApplicationCommandOptionType.User, - required: true - }, - { - name: 'reason', - description: msgconfig.Mod.TicketBlacklist.Add.ReasonDescription || "Reason for blacklisting", - type: ApplicationCommandOptionType.String, - required: false - } - ] - }, - { - name: cmdconfig.Mod.TicketBlacklist.Remove.Command, - description: msgconfig.Mod.TicketBlacklist.Remove.Description || "Remove a user from the ticket blacklist", - type: ApplicationCommandOptionType.Subcommand, - options: [ - { - name: 'user', - description: msgconfig.Mod.TicketBlacklist.Remove.UserDescription || "User to remove from blacklist", - type: ApplicationCommandOptionType.User, - required: true - }, - { - name: 'reason', - description: msgconfig.Mod.TicketBlacklist.Remove.ReasonDescription || "Reason for removing from blacklist", - type: ApplicationCommandOptionType.String, - required: false - } - ] - }, - { - name: "view", - description: msgconfig.Mod.TicketBlacklist.View.Description || "View all blacklisted users", - type: ApplicationCommandOptionType.Subcommand, - } - ] - } - ], - permissions: cmdconfig.Mod.Permission, - - async run(interaction) { - const { getRole } = interaction.client; - - let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild); - let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild); - let Moderator = await getRole(supportbot.Roles.StaffMember.Moderator, interaction.guild); - - if (!SupportStaff || !Admin || !Moderator) { - const missingRolesEmbed = new EmbedBuilder() - .setDescription(msgconfig.Error.InvalidChannel || "Required roles are missing!") - .setColor(supportbot.Embed.Colours.Warn); - return interaction.reply({ embeds: [missingRolesEmbed] }); - } - - const NoPerms = new EmbedBuilder() - .setDescription(msgconfig.Error.IncorrectPerms || "You do not have the correct permissions!") - .setColor(supportbot.Embed.Colours.Warn); - - if (!interaction.member.roles.cache.has(Admin.id) && !interaction.member.roles.cache.has(Moderator.id) && (!supportbot.Mod.AllowSupportStaff || !interaction.member.roles.cache.has(SupportStaff.id))) { - return interaction.reply({ embeds: [NoPerms] }); - } - - const subcommand = interaction.options.getSubcommand(false); - const user = interaction.options.getUser('user'); - const reason = interaction.options.getString('reason') || "No reason provided"; - - try { - let blacklistedUsers; - try { - blacklistedUsers = JSON.parse(fs.readFileSync("./Data/BlacklistedUsers.json", "utf8")).blacklistedUsers; - if (!Array.isArray(blacklistedUsers)) { - blacklistedUsers = []; - } - } catch (error) { - blacklistedUsers = []; - } - - if (subcommand === cmdconfig.Mod.TicketBlacklist.Add.Command) { - if (blacklistedUsers.includes(user.id)) { - const alreadyBlacklistedEmbed = new EmbedBuilder() - .setDescription(msgconfig.Mod.TicketBlacklist.Add.AlreadyBlacklisted.replace("{userTag}", user.tag) || ":x: User is already blacklisted.") - .setColor(supportbot.Embed.Colours.Warn); - return interaction.reply({ embeds: [alreadyBlacklistedEmbed], flags: MessageFlags.Ephemeral }); - } - - blacklistedUsers.push(user.id); - fs.writeFileSync("./Data/BlacklistedUsers.json", JSON.stringify({ "blacklistedUsers": blacklistedUsers }, null, 4)); - - const successEmbed = new EmbedBuilder() - .setDescription(msgconfig.Mod.TicketBlacklist.Add.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been blacklisted.") - .setColor(supportbot.Embed.Colours.Success); - - const blacklistChannel = interaction.guild.channels.cache.get(supportbot.Ticket.Log.TicketBlacklistLog); - if (blacklistChannel) { - const blacklistLogEmbed = new EmbedBuilder() - .setTitle(msgconfig.TicketBlacklistLog.Title || "Ticket Blacklist Log") - .setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success) - .setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() }) - .setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`) - .addFields({ name: "Action", value: "\`\`\`Removed from blacklist\`\`\`", inline: false }) - .addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false }); - - blacklistChannel.send({ embeds: [blacklistLogEmbed] }); - } - - return interaction.reply({ - embeds: [successEmbed], - flags: MessageFlags.Ephemeral - }); - - } else if (subcommand === cmdconfig.Mod.TicketBlacklist.Remove.Command) { - if (!blacklistedUsers.includes(user.id)) { - const notBlacklistedEmbed = new EmbedBuilder() - .setDescription(msgconfig.Mod.TicketBlacklist.Remove.NotBlacklisted.replace("{userTag}", user.tag) || ":x: User is not blacklisted.") - .setColor(supportbot.Embed.Colours.Warn); - return interaction.reply({ embeds: [notBlacklistedEmbed], flags: MessageFlags.Ephemeral }); - } - - blacklistedUsers = blacklistedUsers.filter(id => id !== user.id); - fs.writeFileSync("./Data/BlacklistedUsers.json", JSON.stringify({ "blacklistedUsers": blacklistedUsers }, null, 4)); - - const removedEmbed = new EmbedBuilder() - .setDescription(msgconfig.Mod.TicketBlacklist.Remove.Success.replace("{userTag}", user.tag) || ":white_check_mark: User has been removed from the blacklist.") - .setColor(supportbot.Embed.Colours.Success); - - const blacklistChannel = interaction.guild.channels.cache.get(supportbot.Ticket.Log.TicketBlacklistLog); - if (blacklistChannel) { - const blacklistLogEmbed = new EmbedBuilder() - .setTitle(msgconfig.TicketBlacklistLog.Title || "Ticket Blacklist Log") - .setColor(msgconfig.TicketBlacklistLog.Colour || supportbot.Embed.Colours.Success) - .setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() }) - .setDescription(`> **User:** ${user.tag} (\`${user.id}\`)\n> **Actioned by:** <@${interaction.user.id}>`) - .addFields({ name: "Action", value: "\`\`\`Removed from blacklist\`\`\`", inline: false }) - .addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: false }); - - blacklistChannel.send({ embeds: [blacklistLogEmbed] }); - } - - return interaction.reply({ embeds: [removedEmbed], flags: MessageFlags.Ephemeral }); - - } else if (subcommand === "view") { - if (blacklistedUsers.length === 0) { - const noBlacklistedUsersEmbed = new EmbedBuilder() - .setDescription(msgconfig.Mod.TicketBlacklist.View.NoBlacklistedUsers || "No users are blacklisted.") - .setColor(supportbot.Embed.Colours.Success); - return interaction.reply({ embeds: [noBlacklistedUsersEmbed], flags: MessageFlags.Ephemeral }); - } - - const chunkedUsers = chunkArray(blacklistedUsers, 5); - let currentPage = 0; - - const createEmbed = (page) => { - return new EmbedBuilder() - .setTitle(msgconfig.Mod.TicketBlacklist.View.EmbedTitle || "Blacklisted Users") - .setDescription(chunkedUsers[page].map(id => `<@${id}>`).join("\n")) - .setColor(supportbot.Embed.Colours.Success) - .setFooter({ text: `Page ${page + 1} of ${chunkedUsers.length}` }); - }; - - const buttons = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId('previous') - .setLabel('Previous') - .setStyle(ButtonStyle.Secondary) - .setDisabled(currentPage === 0), - new ButtonBuilder() - .setCustomId('next') - .setLabel('Next') - .setStyle(ButtonStyle.Secondary) - .setDisabled(currentPage === chunkedUsers.length - 1) - ); - - const response = await interaction.reply({ - embeds: [createEmbed(currentPage)], - components: chunkedUsers.length > 1 ? [buttons] : [], - flags: MessageFlags.Ephemeral - }); - - if (chunkedUsers.length <= 1) return; - - const collector = response.createMessageComponentCollector({ - componentType: ComponentType.Button, - time: 60000 - }); - - collector.on('collect', async (i) => { - if (i.user.id !== interaction.user.id) { - return i.reply({ - content: 'You cannot use these buttons.', - flags: MessageFlags.Ephemeral - }); - } - - if (i.customId === 'previous' && currentPage > 0) { - currentPage--; - } else if (i.customId === 'next' && currentPage < chunkedUsers.length - 1) { - currentPage++; - } - - buttons.components[0].setDisabled(currentPage === 0); - buttons.components[1].setDisabled(currentPage === chunkedUsers.length - 1); - - await i.update({ - embeds: [createEmbed(currentPage)], - components: [buttons] - }); - }); - - collector.on('end', () => { - buttons.components.forEach(button => button.setDisabled(true)); - interaction.editReply({ - components: [buttons] - }).catch(() => {}); - }); - } - } catch (error) { - console.error("Error in mod command:", error); - const errorEmbed = new EmbedBuilder() - .setDescription(msgconfig.Error.ActionFailed || "An error occurred while processing your request.") - .setColor(supportbot.Embed.Colours.Warn); - interaction.reply({ embeds: [errorEmbed], flags: MessageFlags.Ephemeral }); - } - } -}); \ No newline at end of file diff --git a/Commands/Utils/settings.js b/Commands/Utils/settings.js deleted file mode 100644 index 036a705..0000000 --- a/Commands/Utils/settings.js +++ /dev/null @@ -1,237 +0,0 @@ -const { Command } = require('../../Structures/Addon.js'); -const Discord = require("discord.js"); -const fs = require("fs"); -const yaml = require("js-yaml"); -const db = require("../../Structures/Database.js"); - -const supportbot = require("../../Structures/ConfigStore").supportbot; -const cmdconfig = require("../../Structures/ConfigStore").commands; - -async function loadSettings() { - return new Promise((resolve, reject) => { - db.get("SELECT data FROM settings WHERE id = 1", (err, row) => { - if (err) return reject(err); - if (!row) return resolve({}); - try { - resolve(JSON.parse(row.data)); - } catch (e) { - resolve({}); - } - }); - }); -} - -async function saveSettings(settings) { - const data = JSON.stringify(settings); - return new Promise((resolve, reject) => { - db.run( - "INSERT INTO settings (id, data) VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data", - [data], - (err) => (err ? reject(err) : resolve()) - ); - }); -} - -module.exports = new Command({ - name: cmdconfig.Settings.Command, - description: cmdconfig.Settings.Description, - permissions: cmdconfig.Settings.Permission, - options: [ - { - name: 'setstatus', - type: Discord.ApplicationCommandOptionType.Subcommand, - description: 'Set the bot status', - options: [ - { - name: 'status', - type: Discord.ApplicationCommandOptionType.String, - description: 'Choose the bot status', - required: true, - choices: [ - { name: 'Online 🟢', value: 'online' }, - { name: 'Idle 🌙', value: 'idle' }, - { name: 'Do Not Disturb ⛔', value: 'dnd' }, - { name: 'Invisible 👻', value: 'invisible' } - ], - }, - ], - }, - { - name: 'setactivity', - type: Discord.ApplicationCommandOptionType.Subcommand, - description: 'Set the bot activity', - options: [ - { - name: 'type', - type: Discord.ApplicationCommandOptionType.String, - description: 'Choose the activity type', - required: true, - choices: [ - { name: 'Playing 🎮', value: 'PLAYING' }, - { name: 'Watching 👀', value: 'WATCHING' }, - { name: 'Listening 🎧', value: 'LISTENING' }, - { name: 'Competing 🏆', value: 'COMPETING' } - ], - }, - { - name: 'message', - type: Discord.ApplicationCommandOptionType.String, - description: 'The activity message', - required: true, - }, - ], - }, - { - name: 'streammode', - type: Discord.ApplicationCommandOptionType.Subcommand, - description: 'Set the bot streaming status', - options: [ - { - name: 'toggle', - type: Discord.ApplicationCommandOptionType.String, - description: 'Turn streaming mode on or off', - required: true, - choices: [ - { name: 'On', value: 'on' }, - { name: 'Off', value: 'off' } - ], - }, - { - name: 'twitchurl', - type: Discord.ApplicationCommandOptionType.String, - description: 'Twitch stream URL (Required if turning on)', - required: false, - }, - ], - }, - { - name: 'setnickname', - type: Discord.ApplicationCommandOptionType.Subcommand, - description: 'Set the bot nickname', - options: [ - { - name: 'nickname', - type: Discord.ApplicationCommandOptionType.String, - description: 'The new nickname for the bot', - required: true, - }, - ], - }, - ], - - async run(interaction) { - const subcommand = interaction.options.getSubcommand(); - const settings = await loadSettings(); - - if (subcommand === 'setstatus') { - const status = interaction.options.getString('status'); - - try { - interaction.client.user.setPresence({ - status: status, - activities: interaction.client.user.presence.activities || [], - }); - - settings.status = status; - await saveSettings(settings); - - const embed = new Discord.EmbedBuilder() - .setDescription(`Status set to **${status.charAt(0).toUpperCase() + status.slice(1)}**`) - .setColor(supportbot.Embed.Colours.General); - - return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral }); - } catch (error) { - return interaction.reply({ content: `Failed to set status: ${error.message}`, flags: Discord.MessageFlags.Ephemeral }); - } - } - - if (subcommand === 'setactivity') { - const activityType = interaction.options.getString('type'); - const activityMessage = interaction.options.getString('message'); - - try { - interaction.client.user.setPresence({ - activities: [{ name: activityMessage, type: Discord.ActivityType[activityType] }], - status: interaction.client.user.presence.status || 'online', - }); - - settings.activity = { type: activityType, message: activityMessage }; - await saveSettings(settings); - - const embed = new Discord.EmbedBuilder() - .setDescription(`Activity set to **${activityType.toLowerCase()} ${activityMessage}**`) - .setColor(supportbot.Embed.Colours.General); - - return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral }); - } catch (error) { - return interaction.reply({ content: `Failed to set activity: ${error.message}`, flags: Discord.MessageFlags.Ephemeral }); - } - } else if (subcommand === 'streammode') { - const toggle = interaction.options.getString('toggle'); - const twitchUrl = interaction.options.getString('twitchurl'); - - if (toggle === 'on') { - if (!twitchUrl) { - return interaction.reply({ content: 'Please provide a Twitch URL to stream.', flags: Discord.MessageFlags.Ephemeral }); - } - - try { - interaction.client.user.setPresence({ - activities: [{ name: 'Streaming', type: Discord.ActivityType.Streaming, url: twitchUrl }], - status: 'online', - }); - - settings.streaming = { active: true, url: twitchUrl }; - await saveSettings(settings); - - const embed = new Discord.EmbedBuilder() - .setDescription(`**Streaming mode enabled!**\nStreaming: [Twitch](${twitchUrl})`) - .setColor(supportbot.Embed.Colours.General); - - return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral }); - } catch (error) { - return interaction.reply({ content: `Failed to enable streaming mode: ${error.message}`, flags: Discord.MessageFlags.Ephemeral }); - } - } else if (toggle === 'off') { - try { - interaction.client.user.setPresence({ - activities: interaction.client.user.presence.activities.filter(a => a.type !== Discord.ActivityType.Streaming), - status: settings.status || 'online', - }); - - settings.streaming = { active: false }; - await saveSettings(settings); - - const embed = new Discord.EmbedBuilder() - .setDescription(`**Streaming mode disabled!**`) - .setColor(supportbot.Embed.Colours.General); - - return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral }); - } catch (error) { - return interaction.reply({ content: `Failed to disable streaming mode: ${error.message}`, flags: Discord.MessageFlags.Ephemeral }); - } - } - } else if (subcommand === 'setnickname') { - const nickname = interaction.options.getString('nickname'); - - try { - const guild = interaction.guild; - const botMember = guild.members.cache.get(interaction.client.user.id); - await botMember.setNickname(nickname); - - settings.nickname = nickname; - await saveSettings(settings); - - const embed = new Discord.EmbedBuilder() - .setDescription(`Bot nickname set to **${nickname}**`) - .setColor(supportbot.Embed.Colours.General); - - return interaction.reply({ embeds: [embed], flags: Discord.MessageFlags.Ephemeral }); - } catch (error) { - return interaction.reply({ content: `Failed to set nickname: ${error.message}`, flags: Discord.MessageFlags.Ephemeral }); - } - } else { - return interaction.reply({ content: 'Invalid command usage.', flags: Discord.MessageFlags.Ephemeral }); - } - }, -}); diff --git a/Configs/api.yml b/Configs/api.yml deleted file mode 100644 index d6bb5e4..0000000 --- a/Configs/api.yml +++ /dev/null @@ -1,29 +0,0 @@ -# ------------------------------- -# API Server Configuration -# ------------------------------- -# Copy to api.yml and fill in your values (api.yml is not committed with real secrets). - -API: - Enabled: true - Port: 3000 - - SecretKey: "CHANGE_ME" - # Set to 1 when the dashboard is behind nginx, Cloudflare, Pterodactyl proxy, etc. - # so http/https is detected from X-Forwarded-Proto. - TrustProxy: false - - OAuth: - Enabled: true - ClientId: "DISCORD_CLIENT_ID" - ClientSecret: "DISCORD_CLIENT_SECRET" - # Canonical callback URL (host + path). Protocol (http/https) can follow the - # browser when UseRequestOrigin is true (default). - RedirectUri: "http://localhost:3000/api/auth/discord/callback" - # Optional: register extra callback URLs in Discord and list them here. - # RedirectUris: - # - "https://bot.example.com/api/auth/discord/callback" - # UseRequestOrigin: true - # CookieSecure: false - OwnerUserIds: - - "YOUR_DISCORD_USER_ID" - AllowedUserIds: [] diff --git a/Configs/api.yml.example b/Configs/api.yml.example deleted file mode 100644 index 0a35d20..0000000 --- a/Configs/api.yml.example +++ /dev/null @@ -1,30 +0,0 @@ -# ------------------------------- -# API Server Configuration -# ------------------------------- -# Copy to api.yml and fill in your values (api.yml is not committed with real secrets). - -API: - Enabled: true - Port: 3000 - - SecretKey: "CHANGE_ME" - - # Set to 1 when the dashboard is behind nginx, Cloudflare, Pterodactyl proxy, etc. - # so http/https is detected from X-Forwarded-Proto. - TrustProxy: false - - OAuth: - Enabled: true - ClientId: "DISCORD_CLIENT_ID" - ClientSecret: "DISCORD_CLIENT_SECRET" - # Canonical callback URL (host + path). Protocol (http/https) can follow the - # browser when UseRequestOrigin is true (default). - RedirectUri: "http://localhost:3000/api/auth/discord/callback" - # Optional: register extra callback URLs in Discord and list them here. - # RedirectUris: - # - "https://bot.example.com/api/auth/discord/callback" - # UseRequestOrigin: true - # CookieSecure: false - OwnerUserIds: - - "YOUR_DISCORD_USER_ID" - AllowedUserIds: [] diff --git a/Configs/commands.yml b/Configs/commands.yml deleted file mode 100644 index 766e647..0000000 --- a/Configs/commands.yml +++ /dev/null @@ -1,196 +0,0 @@ -# ___ _ ___ _ -# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_ -# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | | -# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_| -# |_| |_| -# -# SupportBot created by Emerald Services -# Installed with MIT License -# -# Discord Support: https://discord.gg/emeraldsrv -# Official Documentation: https://docs.emeraldsrv.com -# Addons Marketplace: https://community.emeraldsrv.com -# -# Commands Configuration (commands.yml) - -Help: - Command: "help" - Description: "Get a list of all the commands." - Permission: ["SendMessages"] - Enabled: true - -Info: - Command: "info" - Description: "Get information about the **ServerName**." - Permission: ["SendMessages"] - Enabled: true - -TicketPanel: - Command: "ticketpanel" - Description: "Create the ticket creation panel" - Permission: ["Administrator"] - Enabled: true - -Profile: - Command: "profile" - Description: "Shows the profile for a discord member." - Permission: ["SendMessages"] - # Command is automatically disabled when the TicketClaims is disabled. - -OpenTicket: - Command: "ticket" - Description: "Create a support ticket." - Permission: ["SendMessages"] - Enabled: true - -AddUser: - Command: "add" - Description: "Add a user to a support ticket." - Permission: ["SendMessages"] - Enabled: true - -CloseTicket: - Command: "close" - Description: "Close a support ticket" - Permission: ["SendMessages"] - Enabled: true - -TicketManage: - Command: "ticketmanage" - Description: "Manage an existing support ticket." - Permission: ["SendMessages"] - Enabled: true - - Close: - Command: "close" - Description: "Close a support ticket." - - ForceAdd: - Command: "forceadd" - Description: "Add a user to a support ticket by bypassing the invite system." - - ForceRemove: - Command: "forceremove" - Description: "Remove a user from a support ticket by bypassing the normal restrictions." - - Rename: - Command: "rename" - Description: "Rename a support ticket by bypassing the normal restrictions." - -Suggestion: - Command: "suggest" - Description: "Create a suggestion." - Permission: ["SendMessages"] - Enabled: true - -SuggestionAdmin: - Command: "suggestadmin" - Description: "Update a suggestion’s status." - Permission: ['SendMessages'] - # Disable "Suggestion" to disable this command. - -Embed: - Command: "embed" - Description: "Build a discord embed." - Permission: ["SendMessages"] - Enabled: true - -Translate: - Command: "translate" - Description: "Translate something to english" - Permission: ["SendMessages"] - Enabled: true - -UserInfo: - Command: "userinfo" - Description: "Get info about a user." - Permission: ["SendMessages"] - Enabled: true - -Ping: - Command: "ping" - Description: "Pong." - Permission: ["SendMessages"] - Enabled: true - -# --------------------------------------- -# Bot Moderator Commands -# --------------------------------------- - -Mod: - Command: "mod" - Description: "Execute a moderation action." - Permission: ["BanMembers", "KickMembers"] - Enabled: true - - TicketBlacklist: - Command: "ticket-blacklist" - Description: "Blacklist a user from creating Tickets." - - Add: - Command: "add" - Description: "Add a user to the blacklist." - - Remove: - Command: "remove" - Description: "Remove a user from the blacklist." - - View: - Command: "view" - Description: "View all blacklisted users." - -# --------------------------------------- -# Bot Administrator Commands -# --------------------------------------- - -Settings: - Command: "settings" - Description: "Update bot settings." - Permission: ["Administrator"] - Enabled: true - -AI: - Command: "ai" - Description: "Manage SupportBot AI settings and memory." - Permission: ["Administrator"] - Enabled: true - - Train: - Command: "train" - Description: "Train the AI by adding knowledge to its database." - - Knowledge: - Command: "knowledge" - Description: "Manage the AI knowledge base." - Clear: - Command: "clear" - Description: "Clear all trained knowledge from the AI." - - Memory: - Command: "memory" - Description: "Manage the AI conversational memory." - Clear: - Command: "clear" - Description: "Clear recent conversation memory for this channel." - -# --------------------------------------- -# Command Description Configuration -# --------------------------------------- - -BlacklistDesc: "Blacklist a user from creating Tickets." -TicketAddDesc: "Add a user to a support ticket." -TicketRemoveDesc: "Remove a user from a support ticket." -TranslateCommandDesc: "Translates the language provided, along with the abbreviation (2 char) long language code" -OpenTicketDesc: "Open a new support ticket." -CloseTicketDesc: "Close your support ticket." -InfoCommandDesc: "Get information about the brand." -PingCommandDesc: "Pong!" -HelpCommandDesc: "Get a list of all the commands." -EmbedCommandDesc: "Send a message as an embed." -SuggestCommandDesc: "Suggest a feature for the server." -UserInfoCommandDesc: "Get info about a user." -TicketManageDesc: "Manage an existing support ticket." -TicketManageCloseDesc: "Close a support ticket." -TicketManageForceAddDesc: "Add a user to a support ticket by bypassing the invite system." -TicketManageForceRemoveDesc: "Remove a user from a support ticket by bypassing the normal restrictions." -TicketManageForceRenameDesc: "Rename a support ticket by bypassing the normal restrictions." \ No newline at end of file diff --git a/Configs/messages.yml b/Configs/messages.yml deleted file mode 100644 index 35f70eb..0000000 --- a/Configs/messages.yml +++ /dev/null @@ -1,333 +0,0 @@ -# ___ _ ___ _ -# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_ -# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | | -# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_| -# |_| |_| -# -# SupportBot created by Emerald Services -# Installed with MIT License -# -# Discord Support: https://discord.gg/emeraldsrv -# Official Documentation: https://docs.emeraldsrv.com -# Addons Marketplace: https://community.emeraldsrv.com -# -# Messages Configuration (messages.yml) - -# ---------------------------- -# Info Command -# ---------------------------- - -Info: - Title: "About Us" - Description: "SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customization to match your own brand." - Button: "Visit Site" - URL: "https://docs.emeraldsrv.com" - Colour: "#45b463" - -# ---------------------------- -# Welcome/Leave Messages -# ---------------------------- - -Welcome: - Enabled: true - LayoutStyle: "modern" - # Available Layout Styles - # "modern" - For the new Discord v2 components - # "classic" - Use the classic Discord look. - - Embed: - Colour: "#45b463" - Title: "Welcome to ServerName" - Message: ":smile: %joined_user% has just landed!\n\n🔨 **Server Rules:** <#CHANNEL_ID>" - Icon: - Mode: "USER" # USER | BOT | CUSTOM - CustomURL: "https://example.com/custom.png" - Image: - Enabled: false - URL: "https://example.com/banner.png" - Buttons: - Enabled: true - List: - - label: "BuiltByBit" - style: "Link" - url: "https://your-website.com/how-to-play-java" - emoji: "⌨️" - - label: "GitHub" - style: "Link" - url: "https://your-website.com/how-to-play-bedrock" - emoji: "👾" - - label: "Tebex" - style: "Link" - url: "https://your-store.tebex.io/" - emoji: "🛒" - - Modern: - Colour: "#45b463" - Title: "**Welcome to ServerName**" - Body: ":smile: %joined_user% has just landed!\n\n🔨 **Server Rules:** <#CHANNEL_ID>" - Icon: - Mode: "BOT" # USER | BOT | CUSTOM - CustomURL: "https://example.com/custom-icon.png" - Image: - Enabled: false - URL: "https://example.com/banner.png" - Buttons: - - label: "BuiltByBit" - style: "Link" - url: "https://your-website.com/how-to-play-java" - emoji: "⌨️" - - label: "GitHub" - style: "Link" - url: "https://your-website.com/how-to-play-bedrock" - emoji: "👾" - - label: "Tebex" - style: "Link" - url: "https://your-store.tebex.io/" - emoji: "🛒" - -Leave: - Embed: - Colour: "#f74343" - Title: "Goodbye!" - Message: ":slight_frown: %left_user% has recently dropped out!" - Thumbnail: "BOT" - - ImageEnabled: false - ImageURL: "https://animagelink.png/" - -# ---------------------------- -# Suggestion Messages -# ---------------------------- - -Suggestions: - Sent_Title: "Suggestion Sent!" - Sent: ":white_check_mark: **You have successfully sent a suggestion.**" - NoSelfVote: ":x: **You cannot vote on your own suggestion.**" - RemoveVoteButton: "Remove Vote" - -# ---------------------------- -# Ticket Messages -# ---------------------------- - -Ticket: - -# Available Placeholders: - -# %ticketusername% - User who created the ticket. -# <@%ticketauthor%> - Tags the user who created the ticket. -# <#%ticketid%> - Mentions the ticket channel. - - # You can use Markdown formatting in this message: - # # Heading 1 - # ## Heading 2 - # ### Heading 3 - # **Bold** text - # *Italic* text - # -# Small text - # - Bullet points - # Links: [text](https://example.com) - - TicketAuthorTitle: "-# %ticketusername%'s Ticket" - TicketTitle: "## Support Ticket" - TicketMessage: "Hello <@%ticketauthor%>, Thank you for creating a support ticket\nPlease wait patiently whilst a member of our team reaches out to you." - TicketThumbnail: "https://images.emojiterra.com/google/android-pie/512px/1f39f.png" - TicketCreatedAlert: ":ticket: Ticked Created: <#%ticketid%>" - TicketExists: "❌ Ticket already exists" - TicketClaim: ":white_check_mark: Ticket claimed by" #The user who claimed the ticket will be added to the end of this message - TicketUnclaim: ":white_check_mark: Ticket unclaimed by" #The user who claimed the ticket will be added to the end of this message - - TicketArchived: ":white_check_mark: Ticket archived." - TicketUnarchived: ":white_check_mark: Ticket unarchived." - TicketLocked: ":lock: Ticket is locked." - DisableInvites: "🔒 Invites have been disabled for this ticket." - EnableInvites: "🔓 Invites have been enabled for this ticket." - -# Available Placeholders: - -# <@%user%> - Tags the user who was added to the ticket. - - Blacklisted: ":x: You are not allowed to open a ticket" - Muted: ":x: You are `Muted` and therefore not allowed to open any tickets" - InvalidSubject: "No Reason Provided!" - - ConfirmClose: "Please confirm closing the ticket by clicking the button below." - CloseTimeout: "Ticket close confirmation timed out." - - RenamedSuccess: "🏷️ Ticket renamed successfully!" - - ClaimTickets: - ClaimTitle: "Incoming Support Request!" - ClaimMessage: "A new ticket has been created by <@!%user%>. Click the button below to claim this ticket." - ClaimMessage_Edit: "<#%channel%> was claimed by <@!%user%>." - - ClaimedTitle: "Ticket Claimed!" - Claimed: "You have successfully claimed this ticket. <#%channel%>" - NoPermsToClaimTitle: "Error, no permissions!" - NoPermsToClaim: ":x: **Err!** You do not permissions to claim this ticket." - - ReassigningTitle: "Reassigning!..." - ReassigningMessage: "<@%user%> did not claim the ticket. **Reassigning...**" - - TicketQuestions: - Details: - Title: "Ticket Details" - Description: "Questions by <@%user%>" # Use <@%user%> to display the username. - - AIMode: - Title: "SupportBot AI" - Description: "Use the buttons below to enable or disable AI chat in this ticket." - EnabledMessage: "AI Mode has been enabled for this ticket." - DisabledMessage: "AI Mode has been disabled for this ticket." - - ThreadWelcomeTitle: "SupportBot AI" - ThreadWelcomeMessage: "Hi! I’m SupportBot AI. You can chat with me here about your issue." - - EnableButton: "Enable AI Mode" - DisableButton: "Disable AI Mode" - - TicketNotFound: "Ticket not found." - EnableError: "There was an error enabling SupportBot AI." - DisableError: "There was an error disabling SupportBot AI." - -# ---------------------------- -# Review Messages -# ---------------------------- - -ReviewSystem: - Rate: - Title: "Review us!" - Description: "Please rate our support for this ticket (1-5 stars)." - - Stars: - One: "⭐ 1 Star" - Two: "⭐⭐ 2 Stars" - Three: "⭐⭐⭐ 3 Stars" - Four: "⭐⭐⭐⭐ 4 Stars" - Five: "⭐⭐⭐⭐⭐ 5 Stars" - - Comment: - Title: "Write a comment." - Description: "Write a comment about your experience." - - ReviewEmbed: - Title: "Ticket Review" - CommentTitle: "Comment" - RatingTitle: "Rating" - ReviewedStaffTitle: "Reviewed Staff:" - ReviewedByTitle: "Reviewed By:" - ReviewEmoji: "⭐" - Color: "#f7a943" - -# ---------------------------- -# Mod Command Messages -# ---------------------------- - -Mod: - - TicketBlacklist: - Add: - Description: "Add a user to the ticket blacklist" - UserDescription: "User to blacklist" - AlreadyBlacklisted: ":x: {userTag} is already blacklisted." - Success: ":white_check_mark: {userTag} has been added to the blacklist. They can no longer use the ticket system." - - Remove: - Description: "Remove a user from the ticket blacklist" - UserDescription: "User to remove from blacklist" - NotBlacklisted: ":x: {userTag} is not blacklisted." - Success: ":white_check_mark: {userTag} has been removed from the blacklist. They can now use the ticket system again." - - View: - Description: "View the list of all blacklisted users" - NoBlacklistedUsers: ":information_source: There are currently no users on the blacklist." - - -# ---------------------------- -# Logging Messages -# ---------------------------- - -TicketLog: - Title: "Ticket Log" - Colour: "#0cc559" - -TicketBlacklistLog: - Title: "Ticket Blacklist Log" - Colour: "#0cc559" - -# ---------------------------- -# Misc Messages -# ---------------------------- - -AddUser: - NotInTicket_Title: "Ticket Invitation" - NotInTicket_Description: "This command can only be used within a ticket thread." - - Invite_Title: "User Invited" - Invite_Description: "%username% has been invited to join the ticket." - - DM_Title: "Ticket Invitation" - DM_Description: "You have been invited to join the ticket %channelname%. Do you accept?" - - Accept_Label: "Accept" - Reject_Label: "Reject" - ViewTicket_Label: "Go to Ticket" - - Accepted_Title: "Invitation Accepted" - Accepted_Message: "You have been added to the ticket." - AddedToTicket_Title: "Added to Ticket" - AddedToTicket_Description: "You have been added to the ticket %channel_link%." - - # Added Message for inside the ticket, this informs everyone inside the ticket that the user has been added. - Added_Title: "User Added" - Added_Description: "Successfully added **%username%** to the ticket." - - Declined_Title: "Invitation Declined" - Declined_Message: "You have declined the invitation." - - NoResponse_Title: "No Response" - NoResponse_Message: "You did not respond in time. Please contact staff if you wish to join the ticket." - - Error_Title: "Error" - Error_Adding: "There was an error adding you to the ticket. Please try again." - - DM_Error_Title: "Error" - DM_Error_Description: "Unable to send a DM to %username%. Please ensure your DMs are enabled." - - Unexpected_Error_Title: "Error" - Unexpected_Error_Description: "An unexpected error occurred: %error%" - -RemoveUser: - NotInTicket_Title: "Error" - NotInTicket_Description: "This command can only be used in ticket threads." - - Removed_Title: "User Removed" - Removed_Message: "The user has been successfully removed from the ticket." - - RemovedFromTicket_Title: "Removed from Ticket" - RemovedFromTicket_Description: "You have been removed from the ticket channel %channel_link%." - - Error_Title: "Error" - Error_Removing: "There was an error removing the user from the ticket." - -ForceAddUser: - NotInTicket_Title: "Not a Ticket" - NotInTicket_Description: "This command can only be used within a ticket thread." - - Added_Title: "User Added" - Added_Description: "Successfully added %username% to the ticket." - - AddedToTicket_Title: "You have been added to a Ticket" - AddedToTicket_Description: "You have been added to the ticket channel [%channel_link%]." - - Error_Title: "Error" - Error_Adding: "There was an error adding the user to the ticket." - -Error: - IncorrectPermissions: ":x: **Error!** You do not have the correct permissions to use this command." - MissingChannel: ":x: **Error!** Required channel not found. Please ensure it is set up correctly!" - UserNotFound: ":x: **Error!** Specified user not found. Please check if they are in this server." - CommandNotAllowedHere: ":warning: This command cannot be used in this channel." - EntryAlreadyExists: ":warning: Cannot complete the action, as the entry already exists." - EntryDoesNotExist: ":warning: Cannot complete the action, as the entry does not exist." - ActionFailed: ":x: **Error!** An error occurred while processing your request. Please try again later." - InvalidChannel: ":x: **Error!** The suggestions channel has not been set up correctly." diff --git a/Configs/supportbot-ai.yml b/Configs/supportbot-ai.yml deleted file mode 100644 index 6a2f371..0000000 --- a/Configs/supportbot-ai.yml +++ /dev/null @@ -1,76 +0,0 @@ -# ------------------------- -# SupportBot AI -# -# In order to use the SupportBot AI, You need to setup an OpenAI or Groq Developer Account. -# this can be done at https://platform.openai.com/ or https://groq.com/ -# ------------------------- - -Enabled: true - -General: - Provider: "claude" - Model: "claude-sonnet-4-6" - Model_API_Key: "MODEL_API_KEY" - # Providers Available: - # "openai" - Use OpenAI's GPT models. Example: "gpt-5.4" "gpt-5-mini" - # "groq" - Use Groq's GroqAI models. Example: "meta-llama/llama-4-scout-17b-16e-instruct" - # "claude" - Use Anthropic's Claude models. Example: "claude-sonnet-4-6" - - Tokens: 1200 - - Temperature: 0.4 - Name: "SupportBot AI" - - PastebinAPI_Key: "PASTEBIN_API_KEY" - PastebinAPI_URL: "https://pastebin.com/api/api_post.php" - -Style: - MessageType: "text" - # Available Styles for AI: - # "embed" - AI responses will be sent as Discord embeds. - # "text" - AI responses will be sent as plain messages. - # "components_v2" - AI responses will be sent with the new components style. - -Features: - SuggestTickets: true # Should SupportBot AI suggest creating a ticket if the user needs more help? - -Channels: - AIChannel: "CHANNEL_ID" - -Embed: - Color: "#45b463" - Footer: "SupportBot AI" - -Memory: - Enabled: true - StoreFacts: true - # StoreFacts - Enables saving important facts the AI learns. - - StoreSummaries: true - # StoreSummaries - Enables saving conversation summaries. - - MaxRecentMessages: 15 - # MaxRecentMessages - Number of recent messages used for context. - - Scope: "guild" # channel | guild - # Available Scopes for Memory: - # "channel" - Memory is per channel only. - # "guild" - Memory is shared across the server. - -CustomInstructions: - - "You are SupportBot AI made by Emerald Services" - - "You are built with DiscordJS/NodeJS and currently running on dev build 26.0 build 2" - - "You're current build can be found here https://github.com/Emerald-Services/SupportBot/tree/26.0" - - "You're main build can be found here https://builtbybit.com/resources/supportbot-free-advanced-ticket-bot.8940/ and Polymart at https://polymart.org/product/518/supportbot-1-discord-ticket-bot which is version 8.4" - - "dev build 26.0 build 2 includes all-new AI features for SupportBot such as memory system that allows you to remember facts and summaries of conversations, which can be used to provide better responses." - - "SupportBot AI also have a new tickets AI Mode which is enabled in supportbot.ymll, which allows you to assist users in support tickets and provide better responses based on the context of the ticket." - - "Your main purpose is to assist users in the support server and provide helpful responses to their questions and issues. Always be accurate and if you do not have answer then simply ask the person to create a ticket if they need more help." - #- "Add custom instructions here" - -Messages: - ErrorResponse: "I couldn't generate a response." - AIError: "There was an error contacting the AI provider." - AIDisabled: "AI is currently disabled." - MessageTooLong: "My response was too long, so I uploaded it here: {pastebin}" - SuggestTicketText: "-# Need more help? React with 🎫 to open a ticket." - SuggestTicketReaction : "🎫" diff --git a/Configs/supportbot.yml b/Configs/supportbot.yml deleted file mode 100644 index 35677c0..0000000 --- a/Configs/supportbot.yml +++ /dev/null @@ -1,369 +0,0 @@ -# ___ _ ___ _ -# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_ -# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | | -# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_| -# |_| |_| -# -# SupportBot created by Emerald Services -# Installed with MIT License -# -# Discord Support: https://discord.gg/emeraldsrv -# Website: https://emeraldsrv.dev -# -# SupportBot Configuration (supportbot.yml) - -SupportBot_Version: "v26.3.0" - -# ------------------------- -# General -# ------------------------- - -General: - Name: "SupportBo" - Token: "BOT_TOKEN" - Addons: - Enabled: true - GuildId: "" - -# ------------------------- -# Activity -# ------------------------- - -Activity: - Status: "Char" - Type: "Watching" - StreamingURL: "https://www.twitch.tv/YOUR_TWITCH_NAME" - # Available Types: - # Playing - # Watching - # Listening - # Competing - # Streaming (URL required) - - # You can also update the activity via Discord using: /settings - - # ------------------------- - # Embeds - # ------------------------- - -Embed: - Colours: - General: "#0cc559" - Success: "#04b545" - Error: "#d13030" - Warn: "#fca117" - - Footer: "SupportBot | Developed by Emerald Development" - # Use 0x to change the colour of the embeds. - - # ------------------------- - # Roles - # ------------------------- - -Roles: - AutoRole: - Enabled: true - Role: "ROLE_ID" - - StaffMember: - Staff: "ROLE_ID" - Admin: "ROLE_ID" - Moderator: "ROLE_ID" - - Mod: - AllowSupportStaff: false - # Allow Support Staff to use the /mod command. (More in detail customisation in commands.yml) - - # ---------------------------- - # System Messages - # ---------------------------- - -Welcome: - Enabled: false - Channel: "CHANNEL_ID" - -Leave: - Enabled: false - Channel: "CHANNEL_ID" - -# ---------------------------- -# Suggestions -# ---------------------------- - -Suggestions: - Mode: "Channel" # Channel or Forum - ForumChannel: "FORUM_CHANNEL_ID" # A forum channel, disabled if mode is set to Channel. - Channel: "CHANNEL_ID" # A text channel, disabled if mode is set to Forum. - - UpvoteEmoji: "✅" - DownvoteEmoji: "❌" - - UpvoteTitle: "Yes" - DownvoteTitle: "No" - - OwnSuggestion: true - - # If false this will not allow the user to vote for a suggestion they created - # If true this will allow the user to vote for a suggestion they created - - ShowUsers: true - - Threads: - Enabled: true - Title: "Suggestion-releated Thread" - Reason: "A thread releted to this suggestion" - - Buttons: - Upvote: "2" - Downvote: "2" - RemoveVote: "4" - RemoveVote_Title: "Remove Vote" - # Available Button Styles - # 1 - Blurple - # 2 - Grey - # 3 - Green - # 4 - Red - - Tags: - Created: "TAG_ID" - Considering: "TAG_ID" - Solved: "TAG_ID" - Denied: "TAG_ID" - -# ------------------------- -# Tickets -# ------------------------- - -VoiceTickets: - Name: "%username%'s VC" - Category: "CATEGORY_ID" - -Ticket: - TicketType: "channels" - - # Available TicketTypes: - # channels - Tickets will be created as channels in the TicketChannelsCategory - # threads - Tickets will be created as threads through the "TicketHome" channel. - - UserManagement: - DMOnAdd: true # DM the user when they are added to the ticket - DMOnRemove: true # DM the user when they are removed from the ticket - DMTranscript: true # DM the user a transcript of the ticket when it is closed - - TicketHome: "CHANNEL_ID" - - # What is TicketHome? - # - Channel where the Ticket creation panel is sent. - # - Channel where ticket threads are made. (only if ticket threads is enabled above.) - - # For TicketType: channels - TicketChannelsCategory: "CATEGORY_ID" - TicketChannelsCategory2: "CATEGORY_ID" # This will be used if the first category has reached the max channel limit. - TicketArchiveCategory: "CATEGORY_ID" - # --- - - Channel: "ticket-" # The prefix for ticket channel names - TicketsPerUser: 100 - TicketReason: true - # If ticket reason is set to false, It will not prompt to create a reason - - TicketSubject: "description" - - # Available TicketSubjects: - # "description" will display the ticket reason within the channel description - # "embed" will display the ticket reason within the ticket embed - - Timeout: 5 - # Set to true to allow all staff to access the ticket - - Close: - StaffOnly: true - # Set to false is anyone is able to close a ticket. - - Invites: - # Invites is for ticket threads. - StaffOnly: true - - ClaimTickets: - Enabled: false - 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 - # 180000 = 3 Minutes - # 240000 = 4 Minutes - # 300000 = 5 Minutes - # Time in Mliseconds - - ButtonTitle: "Claim Ticket" - ButtonEmoji: "🎫" - Button: "1" - # Available Button Styles - # 1 - Blurple - # 2 - Grey - # 3 - Green - # 4 - Red - - ReviewSystem: - 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 - List: - - "Please provide your account email?" - - "What is the nature of your issue?" - - "Have you tried resolving the issue yourself?" - - Log: - TicketDataLog: "CHANNEL_ID" - TickeDataTitle: "Ticket Data" - TicketBlacklistLog: "CHANNEL_ID" - - AIMode: - Enabled: false - ThreadName: "Chat with SupportBot AI" - WelcomeMessage: "Hi! I’m SupportBot AI. You can chat with me here about your issue." - - # ------------------------- - # Ticket Departments - # ------------------------- - - DepartmentSystem: - Enabled: false - Placeholder: "Choose a ticket department" - DefaultDepartment: "general" - - Departments: - general: - Name: "General Support" - Description: "Open a general support ticket" - Emoji: "🎫" - Category: "CATEGORY_ID" - Role: "ROLE_ID" - ChannelPrefix: "support-" - DefaultPriority: "normal" - Questions: - - "What do you need help with?" - - "When did the issue start?" - - purchase: - Name: "Purchase Support" - Description: "Get help with store purchases" - Emoji: "💳" - Category: "CATEGORY_ID" - Role: "ROLE_ID" - ChannelPrefix: "purchase-" - DefaultPriority: "high" - Questions: - - "What is your order ID?" - - "What product did you purchase?" - - "What payment issue are you having?" - - reports: - Name: "Player Report" - Description: "Report a player for breaking the rules" - Emoji: "🚨" - Category: "CATEGORY_ID" - Role: "ROLE_ID" - ChannelPrefix: "report-" - DefaultPriority: "normal" - Questions: - - "Who are you reporting?" - - "What happened?" - - "Do you have evidence?" - - appeals: - Name: "Ban Appeals" - Description: "Appeal a server punishment" - Emoji: "⚖️" - Category: "CATEGORY_ID" - Role: "ROLE_ID" - ChannelPrefix: "appeals-" - DefaultPriority: "low" - Questions: - - "What punishment are you appealing?" - - "Why should it be reviewed?" - - "Anything else staff should know?" - - PrioritySystem: - Enabled: false - DefaultPriority: "Normal" - AllowUsersToChooseOnOpen: true - Priorities: - low: - Name: "Low" - Emoji: "🟢" - normal: - Name: "Normal" - Emoji: "🟡" - high: - Name: "High" - Emoji: "🟠" - urgent: - Name: "Urgent" - Emoji: "🔴" - -# ------------------------- -# Buttons -# ------------------------- -Buttons: - General: - Delete: "🗑️" - DeleteStyle: "4" - - Voice: - TicketDeleteText: "Delete Voice Channel" - - Tickets: - ClaimEmoji: "🧑‍💻" - ClaimStyle: "1" - - Unarchive: "Unarchive Ticket" - Unarchive_Emoji: "♻️" - Unarchive_Style: "2" - - Unlock: "Unlock Ticket" - Unlock_Emoji: "🔓" - Unlock_Style: "2" - # Available Button Styles - # 1 - Blurple - # 2 - Grey - # 3 - Green - # 4 - Red - - # ------------------------- - # Select Menus - # ------------------------- - -SelectMenus: - Tickets: - PanelEmoji: "⚙️" - - CloseEmoji: "🗑️" - ArchiveEmoji: "♻️" - LockEmoji: "🔐" - RenameEmoji: "🏷️" - VCEmoji: "🔊" - - EnableInvitesEmoji: "📤" - DisableInvitesEmoji: "📥" - -# ------------------------- -# Logging -# ------------------------- - -MessageDelete: - Channel: "CHANNEL_ID" - Colour: "#d92525" - -MessageUpdate: - Channel: "CHANNEL_ID" - Colour: "#f2a024" - -Translate: - TranslateLog: "CHANNEL_ID" diff --git a/Configs/ticket-panel.yml b/Configs/ticket-panel.yml deleted file mode 100644 index 099f267..0000000 --- a/Configs/ticket-panel.yml +++ /dev/null @@ -1,67 +0,0 @@ -# ___ _ ___ _ -# / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_ -# \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | | -# <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_| -# |_| |_| -# -# SupportBot created by Emerald Services -# Installed with MIT License -# -# Discord Support: https://discord.gg/emeraldsrv -# Official Documentation: https://docs.emeraldsrv.com -# Addons Marketplace: https://community.emeraldsrv.com -# -# Ticket Creation Panel Configuration (ticket-panel.yml) - -# ------------------------------- -# Ticket Panel Configuration -# ------------------------------- - -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!" - Image: "https://i.imgur.com/dejQvkg.png" - Color: "#45b463" - Layout: "4" - -# Available Layout Styles -# "1" - Style 1 - Title & Text, Button at the top -- Image at the bottom -# "2" - Style 2 - Title & Text, Button at the bottom -- Image at the top -# "3" - Style 3 - The classic look - Title & Text at the top -- Image in the middle -- Button at the bottom -# "4" - Style 4 - Classic Embed (Title & Text, Image on right, Button at bottom) - -Button: - Text: "Create Ticket" - Emoji: "🎫" - Color: "2" - CustomId: "createticket" - -# Available Button Styles -# 1 - Blurple -# 2 - Grey -# 3 - Red -# 4 - Green - -DepartmentMenu: - CustomId: "ticketdepartmentselect" - Placeholder: "Choose a ticket department" - DefaultDescription: "Open a support ticket" - DefaultEmoji: "🎫" - -Settings: - Image: true - TopTitle: true - TopDivider: true - BottomTitle: false - BottomDivider: false - -Messages: - 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!" - SendError: "An error occurred while sending the ticket panel message." diff --git a/Configs/transcript-template.yml b/Configs/transcript-template.yml deleted file mode 100644 index 824920f..0000000 --- a/Configs/transcript-template.yml +++ /dev/null @@ -1,33 +0,0 @@ -Theme: - Background: '#0e0e0e' - Panel: '#1d1d1d' - PanelSecondary: '#222222' - PanelTertiary: '#1d2938' - Border: '#2e2e2e' - Text: '#e6edf3' - Muted: '#606060' - Accent: '#396ff8' - AccentSoft: rgba(34, 197, 94, 0.12) - AccentBorder: rgba(34, 197, 94, 0.25) - AccentText: '#8df0ad' - Link: '#396ff8' - AttachmentLink: '#396ff8' - CodeBackground: '#0e0e0e' -Typography: - FontFamily: Poppins -Header: - Title: Ticket Transcript - Subtitle: Exported conversation log for support ticket **{{ticketName}}** - Badge: Closed Transcript - ShowMetaCards: true -Meta: - ChannelLabel: Ticket Name - TicketIdLabel: Ticket ID - MessageCountLabel: Message Count - CloseReasonLabel: Close Reason - DefaultCloseReason: No Reason Provided. -Messages: - SectionTitle: Messages - EmptyState: No messages were found in this ticket. -Advanced: - CustomCss: '' diff --git a/Data/BlacklistedUsers.json b/Data/BlacklistedUsers.json deleted file mode 100644 index 257b226..0000000 --- a/Data/BlacklistedUsers.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "blacklistedUsers": [] -} \ No newline at end of file diff --git a/Data/SupportBot-AI.json b/Data/SupportBot-AI.json deleted file mode 100644 index 01ad2f9..0000000 --- a/Data/SupportBot-AI.json +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Data/ai-memory.db b/Data/ai-memory.db deleted file mode 100644 index 73a0692..0000000 Binary files a/Data/ai-memory.db and /dev/null differ diff --git a/Data/ai-memory.db-shm b/Data/ai-memory.db-shm deleted file mode 100644 index 5ff9ccc..0000000 Binary files a/Data/ai-memory.db-shm and /dev/null differ diff --git a/Data/ai-memory.db-wal b/Data/ai-memory.db-wal deleted file mode 100644 index 57e9d6c..0000000 Binary files a/Data/ai-memory.db-wal and /dev/null differ diff --git a/Data/dashboard-users.json b/Data/dashboard-users.json deleted file mode 100644 index 841e1de..0000000 --- a/Data/dashboard-users.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "version": 1, - "users": { - "829112572816130058": { - "id": "829112572816130058", - "username": "ch_ar.", - "globalName": "Char", - "avatar": "823630e777efe85c7fe71402f00ab0c1", - "role": "owner", - "permissions": null, - "enabled": true, - "createdAt": 1779820911269, - "updatedAt": 1779822114501, - "createdBy": "system" - }, - "YOUR_DISCORD_USER_ID": { - "id": "YOUR_DISCORD_USER_ID", - "username": null, - "globalName": null, - "avatar": null, - "role": "owner", - "permissions": null, - "enabled": true, - "createdAt": 1779822020405, - "updatedAt": 1779822020405, - "createdBy": "system" - } - } -} \ No newline at end of file diff --git a/Data/supportbot.db b/Data/supportbot.db deleted file mode 100644 index b3d2dc8..0000000 Binary files a/Data/supportbot.db and /dev/null differ diff --git a/Data/transcript-settings.json b/Data/transcript-settings.json deleted file mode 100644 index f42e896..0000000 --- a/Data/transcript-settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "autoDelete": { - "enabled": false, - "afterDays": 30 - }, - "publicAccess": { - "enabled": false - } -} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 7ad05ea..0000000 --- a/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM node:22 - -WORKDIR /app - -COPY package*.json ./ -RUN npm install - -COPY . . - -EXPOSE 3000 - -CMD ["node", "index.js"] diff --git a/Events/Models/claude.js b/Events/Models/claude.js deleted file mode 100644 index af85e12..0000000 --- a/Events/Models/claude.js +++ /dev/null @@ -1,82 +0,0 @@ -// Emerald Services | SupportBot -// Claude integration for SupportBot AI - -const fs = require("fs"); -const yaml = require("js-yaml"); -const axios = require("axios"); - -const supportbotai = require("../../Structures/ConfigStore").supportbotAi; - -function extractTextFromMessage(message) { - if (!message) return ""; - - if (typeof message === "string") { - return message.trim(); - } - - if (message.text) { - return message.text.trim(); - } - - if (Array.isArray(message)) { - return message - .map((part) => { - if (typeof part === "string") return part; - if (part?.type === "text" && typeof part.text === "string") { - return part.text; - } - return ""; - }) - .join("") - .trim(); - } - - return ""; -} - -async function chat({ messages, temperature, maxTokens }) { - const model = supportbotai.General.Model; - const apiKey = supportbotai.General.Model_API_Key; - - const systemMessages = messages.filter((m) => m.role === "system"); - const systemPrompt = systemMessages.map((m) => m.content).join("\n\n"); - - const userAssistantMessages = messages.filter((m) => m.role !== "system"); - - const response = await axios.post( - "https://api.anthropic.com/v1/messages", - { - model, - system: systemPrompt || undefined, - messages: userAssistantMessages, - temperature: temperature || supportbotai.General.Temperature || 0.4, - max_tokens: maxTokens || supportbotai.General.Tokens || 800, - }, - { - headers: { - "x-api-key": apiKey, - "anthropic-version": "2023-06-01", - "content-type": "application/json", - }, - } - ); - - const data = response.data; - const content = extractTextFromMessage(data.content); - - if (!content) { - console.error("Claude raw response:", JSON.stringify(data, null, 2)); - throw new Error( - `Claude returned empty response. stop_reason=${data?.stop_reason || "unknown"}`, - ); - } - - return { - content, - raw: data, - }; -} - -module.exports = { - chat, -}; diff --git a/Events/Models/groq.js b/Events/Models/groq.js deleted file mode 100644 index f44d126..0000000 --- a/Events/Models/groq.js +++ /dev/null @@ -1,70 +0,0 @@ -// Emerald Services | SupportBot -// Groq integration for SupportBot AI - -const fs = require("fs"); -const yaml = require("js-yaml"); -const OpenAI = require("openai"); - -const supportbotai = require("../../Structures/ConfigStore").supportbotAi; - -const client = new OpenAI({ - apiKey: supportbotai.General.Model_API_Key, - baseURL: "https://api.groq.com/openai/v1", -}); - -function extractTextFromMessage(message) { - if (!message) return ""; - - if (typeof message.content === "string") { - return message.content.trim(); - } - - if (Array.isArray(message.content)) { - return message.content - .map((part) => { - if (typeof part === "string") return part; - if (part?.type === "text" && typeof part.text === "string") { - return part.text; - } - if (typeof part?.text?.value === "string") { - return part.text.value; - } - return ""; - }) - .join("") - .trim(); - } - - return ""; -} - -async function chat({ messages, temperature, maxTokens }) { - const model = supportbotai.General.Model; - - const response = await client.chat.completions.create({ - model, - messages, - temperature: temperature || supportbotai.General.Temperature || 0.4, - max_tokens: maxTokens || supportbotai.General.Tokens || 800, - }); - - const choice = response?.choices?.[0]; - const message = choice?.message; - const content = extractTextFromMessage(message); - - if (!content) { - console.error("Groq raw response:", JSON.stringify(response, null, 2)); - throw new Error( - `Groq returned empty response. finish_reason=${choice?.finish_reason || "unknown"}`, - ); - } - - return { - content, - raw: response, - }; -} - -module.exports = { - chat, -}; diff --git a/Events/Models/openai.js b/Events/Models/openai.js deleted file mode 100644 index 8b29116..0000000 --- a/Events/Models/openai.js +++ /dev/null @@ -1,66 +0,0 @@ -// Emerald Services | SupportBot -// Open AI integration for SupportBot AI - -const fs = require("fs"); -const yaml = require("js-yaml"); -const OpenAI = require("openai"); - -const supportbotai = require("../../Structures/ConfigStore").supportbotAi; - -const client = new OpenAI({ - apiKey: supportbotai.General.Model_API_Key, -}); - -function extractTextFromMessage(message) { - if (!message) return ""; - - if (typeof message.content === "string") { - return message.content.trim(); - } - - if (Array.isArray(message.content)) { - return message.content - .map((part) => { - if (typeof part === "string") return part; - if (part?.type === "text" && typeof part.text === "string") - return part.text; - if (typeof part?.text?.value === "string") return part.text.value; - return ""; - }) - .join("") - .trim(); - } - - return ""; -} - -async function chat({ messages, temperature, maxTokens }) { - const model = supportbotai.General.Model; - - const response = await client.chat.completions.create({ - model, - messages, - temperature: temperature || supportbotai.General.Temperature || 0.4, - max_completion_tokens: maxTokens || supportbotai.General.Tokens || 800, - }); - - const choice = response?.choices?.[0]; - const message = choice?.message; - const content = extractTextFromMessage(message); - - if (!content) { - console.error("OpenAI raw response:", JSON.stringify(response, null, 2)); - throw new Error( - `OpenAI returned empty response. finish_reason=${choice?.finish_reason || "unknown"}`, - ); - } - - return { - content, - raw: response, - }; -} - -module.exports = { - chat, -}; diff --git a/Events/guildCreate.js b/Events/guildCreate.js deleted file mode 100644 index 3365140..0000000 --- a/Events/guildCreate.js +++ /dev/null @@ -1,31 +0,0 @@ -const Event = require("../Structures/Event.js"); -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.`, - ); - - syncGuildHealth(client); -}); diff --git a/Events/guildDelete.js b/Events/guildDelete.js deleted file mode 100644 index 8fde2a7..0000000 --- a/Events/guildDelete.js +++ /dev/null @@ -1,23 +0,0 @@ -const Event = require("../Structures/Event.js"); -const { - getConfiguredGuildId, - syncGuildHealth, - logGuildWarning, -} = require("../Structures/GuildManager.js"); - -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 deleted file mode 100644 index 9155eef..0000000 --- a/Events/guildMemberAdd.js +++ /dev/null @@ -1,151 +0,0 @@ -const { - EmbedBuilder, - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - MessageFlags, - ContainerBuilder, - TextDisplayBuilder, - SectionBuilder, - ThumbnailBuilder, - MediaGalleryBuilder -} = require("discord.js"); -const Event = require("../Structures/Event.js"); - -const supportbot = require("../Structures/ConfigStore").supportbot; -const msgconfig = require("../Structures/ConfigStore").messages; - -function getWelcomeThumbnail(client, member, iconCfg) { - switch ((iconCfg?.Mode || "").toUpperCase()) { - case "USER": - return member.user.displayAvatarURL(); - case "BOT": - return client.user.displayAvatarURL(); - case "CUSTOM": - return iconCfg.CustomURL; - default: - return null; - } -} - -function buildModernComponents(client, member) { - const container = new ContainerBuilder(); - - if (msgconfig.Welcome.Modern.Colour) { - container.setAccentColor(parseInt(msgconfig.Welcome.Modern.Colour.replace("#", ""), 16)); - } - - const section = new SectionBuilder().addTextDisplayComponents( - new TextDisplayBuilder().setContent( - msgconfig.Welcome.Modern.Title.replace(/%joined_user%/g, member.user) - ), - new TextDisplayBuilder().setContent( - msgconfig.Welcome.Modern.Body.replace(/%joined_user%/g, member.user) - ) - ); - - const thumbUrl = getWelcomeThumbnail(client, member, msgconfig.Welcome.Modern.Icon); - if (thumbUrl) { - section.setThumbnailAccessory( - new ThumbnailBuilder() - .setURL(thumbUrl) - ); - } - - container.addSectionComponents(section); - - // FIXED: Using MediaGalleryBuilder instead of TextDisplayBuilder for images - if (msgconfig.Welcome.Modern.Image?.Enabled && msgconfig.Welcome.Modern.Image.URL) { - container.addMediaGalleryComponents( - new MediaGalleryBuilder().addItems([ - { - media: { - url: msgconfig.Welcome.Modern.Image.URL - } - } - ]) - ); - } - - if (Array.isArray(msgconfig.Welcome.Modern.Buttons)) { - const row = new ActionRowBuilder(); - msgconfig.Welcome.Modern.Buttons.slice(0, 5).forEach(b => { - const btn = new ButtonBuilder() - .setLabel(b.label) - .setStyle(ButtonStyle[b.style]) - .setEmoji(b.emoji || null); - if (b.style === "Link") btn.setURL(b.url); - else if (b.customId) btn.setCustomId(b.customId); - row.addComponents(btn); - }); - - container.addActionRowComponents(row); - } - - return [container]; -} - -module.exports = new Event("guildMemberAdd", async (client, member) => { - if (!supportbot.Welcome.Enabled) return; - - const WelcomeChannel = - member.guild.channels.cache.get(supportbot.Welcome.Channel) || - member.guild.channels.cache.find(c => c.name === supportbot.Welcome.Channel); - if (!WelcomeChannel) return; - - const style = (msgconfig.Welcome.LayoutStyle || "classic").toLowerCase(); - - if (style === "classic") { - const WelcomeEmbed = new EmbedBuilder() - .setColor(msgconfig.Welcome.Embed.Colour) - .setTitle(msgconfig.Welcome.Embed.Title || null) - .setDescription( - msgconfig.Welcome.Embed.Message.replace(/%joined_user%/g, member.user) - ) - .setTimestamp(); - - const thumb = getWelcomeThumbnail(client, member, msgconfig.Welcome.Embed.Icon); - if (thumb) WelcomeEmbed.setThumbnail(thumb); - - if (msgconfig.Welcome.Embed.Image?.Enabled && msgconfig.Welcome.Embed.Image.URL) { - WelcomeEmbed.setImage(msgconfig.Welcome.Embed.Image.URL); - } - - const components = []; - if ( - msgconfig.Welcome.Embed.Buttons?.Enabled && - Array.isArray(msgconfig.Welcome.Embed.Buttons.List) - ) { - const row = new ActionRowBuilder(); - msgconfig.Welcome.Embed.Buttons.List.slice(0, 5).forEach(b => { - const btn = new ButtonBuilder() - .setLabel(b.label) - .setStyle(ButtonStyle[b.style]) - .setEmoji(b.emoji || null); - if (b.style === "Link") btn.setURL(b.url); - else if (b.customId) btn.setCustomId(b.customId); - row.addComponents(btn); - }); - components.push(row); - } - - await WelcomeChannel.send({ embeds: [WelcomeEmbed], components }); - } else if (style === "modern") { - const components = buildModernComponents(client, member); - if (components.length > 0) { - await WelcomeChannel.send({ - components, - flags: MessageFlags.IsComponentsV2 - }); - } - } - - if (supportbot.Roles.AutoRole.Enabled) { - const role = - member.guild.roles.cache.get(supportbot.Roles.AutoRole.Role) || - member.guild.roles.cache.find(r => r.name === supportbot.Roles.AutoRole.Role); - if (role) await member.roles.add(role); - } - - console.log("\u001b[32m", "[+]", "\u001b[33m", `${member.user.username} joined the server!`); -}); diff --git a/Events/guildMemberRemove.js b/Events/guildMemberRemove.js deleted file mode 100644 index 556a966..0000000 --- a/Events/guildMemberRemove.js +++ /dev/null @@ -1,40 +0,0 @@ -const { EmbedBuilder } = require("discord.js"); -const fs = require("fs"); -const yaml = require("js-yaml"); -const Event = require("../Structures/Event.js"); - -const supportbot = require("../Structures/ConfigStore").supportbot; -const msgconfig = require("../Structures/ConfigStore").messages; - -module.exports = new Event("guildMemberRemove", async (client, member) => { - - if (!supportbot.Leave || !supportbot.Leave.Enabled) return; - - const leaveChannel = - member.guild.channels.cache.get(supportbot.Leave.Channel) || - member.guild.channels.cache.find(c => c.name === supportbot.Leave.Channel); - - if (!leaveChannel) return; - - const leaveEmbed = new EmbedBuilder() - .setColor(msgconfig.Leave.Embed.Colour) - .setTitle(msgconfig.Leave.Embed.Title) - .setDescription( - msgconfig.Leave.Embed.Message.replace(/%left_user%/g, member.user.toString()) - ) - .setTimestamp(); - - if (msgconfig.Leave.Embed.Thumbnail === "BOT") { - leaveEmbed.setThumbnail(client.user.displayAvatarURL()); - } else if (msgconfig.Leave.Embed.Thumbnail === "USER") { - leaveEmbed.setThumbnail(member.user.displayAvatarURL()); - } - - if (msgconfig.Leave.Embed.ImageEnabled) { - leaveEmbed.setImage(msgconfig.Leave.Embed.ImageURL); - } - - await leaveChannel.send({ embeds: [leaveEmbed] }); - - console.log(`[-] ${member.user.tag} left the server`); -}); diff --git a/Events/interactionCreate.js b/Events/interactionCreate.js deleted file mode 100644 index 379182b..0000000 --- a/Events/interactionCreate.js +++ /dev/null @@ -1,1035 +0,0 @@ -const Discord = require("discord.js"); - -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"); - -module.exports = new Event("interactionCreate", async (client, interaction) => { - if (interaction.type === Discord.InteractionType.ApplicationCommand) { - const command = client.commands.find( - (cmd) => cmd.name.toLowerCase() === interaction.commandName, - ); - - if (interaction.user.bot || !interaction.guild) return; - - const NotValid = new Discord.EmbedBuilder() - .setDescription(`:x: \`Invalid Command\``) - .setColor(supportbot.Embed.Colours.Error); - - if (!command) - return interaction.reply({ - embeds: [NotValid], - flags: Discord.MessageFlags.Ephemeral, - }); - - const permission = interaction.member.permissions.has(command.permissions); - - const ValidPerms = new Discord.EmbedBuilder() - .setDescription( - "> :x: `Invalid Permissions` Do you have the correct permissions to execute this command?", - ) - .setColor(supportbot.Embed.Colours.Error); - - if (!permission) - return interaction.reply({ - embeds: [ValidPerms], - flags: Discord.MessageFlags.Ephemeral, - }); - - try { - if ( - command.name === cmdconfig.OpenTicket.Command && - supportbot.Ticket.TicketReason - ) { - const modal = new Discord.ModalBuilder() - .setCustomId("ticketReasonModal") - .setTitle("Ticket Reason") - .addComponents( - new Discord.ActionRowBuilder().addComponents( - new Discord.TextInputBuilder() - .setCustomId("reasonInput") - .setLabel("Reason") - .setStyle(Discord.TextInputStyle.Paragraph) - .setRequired(true), - ), - ); - - await interaction.showModal(modal); - } else { - console.log( - "\u001b[32m", - "[Executed]", - "\u001b[37;1m", - `${interaction.user.username} has executed ${command.name}`, - ); - - await command.run(interaction); - } - } catch (error) { - console.error("Error executing command:", error); - await interaction.reply({ - content: "An error occurred while executing the command.", - flags: Discord.MessageFlags.Ephemeral, - }); - } - } - - if (interaction.isModalSubmit()) { - if (interaction.customId.startsWith("ticketReasonModal")) { - const reason = interaction.fields.getTextInputValue("reasonInput"); - const department = interaction.customId.includes(":") - ? interaction.customId.split(":")[1] - : supportbot.Ticket.DepartmentSystem?.DefaultDepartment || "general"; - - const cmd = client.commands.get(cmdconfig.OpenTicket.Command); - if (!cmd) { - return interaction.reply({ - content: "Open ticket command not found.", - flags: Discord.MessageFlags.Ephemeral, - }); - } - - interaction.reason = reason; - interaction.department = department; - interaction.priority = - supportbot.Ticket.DepartmentSystem?.Departments?.[department] - ?.DefaultPriority || - supportbot.Ticket.PrioritySystem?.DefaultPriority || - "medium"; - - return await cmd.run(interaction); - } - } - - if (interaction.customId === "renameTicketModal") { - const newTicketName = interaction.fields.getTextInputValue("renameTicket"); - const ticketChannelId = interaction.channel.id; - let ticketChannel; - if (supportbot.Ticket.TicketType === "threads") { - ticketChannel = interaction.guild.channels.cache.get(ticketChannelId); - } else if (supportbot.Ticket.TicketType === "channels") { - ticketChannel = interaction.guild.channels.cache.get(ticketChannelId); - } - if (!ticketChannel) { - return interaction.reply({ - content: "Ticket channel not found.", - flags: Discord.MessageFlags.Ephemeral, - }); - } - try { - await ticketChannel.setName(newTicketName); - const renamedsuccess = new Discord.EmbedBuilder() - .setDescription(msgconfig.Ticket.RenamedSuccess) - .setColor(supportbot.Embed.Colours.Success); - return interaction.reply({ - embeds: [renamedsuccess], - flags: Discord.MessageFlags.Ephemeral, - }); - } catch (error) { - console.error("Error renaming the ticket:", error); - return interaction.reply({ - content: "There was an error renaming the ticket.", - flags: Discord.MessageFlags.Ephemeral, - }); - } - } - - if (interaction.isStringSelectMenu()) { - if (interaction.customId === "ticketdepartmentselect") { - const selectedDepartment = interaction.values[0]; - - if (supportbot.Ticket.TicketReason) { - const modal = new Discord.ModalBuilder() - .setCustomId(`ticketReasonModal:${selectedDepartment}`) - .setTitle("Ticket Reason") - .addComponents( - new Discord.ActionRowBuilder().addComponents( - new Discord.TextInputBuilder() - .setCustomId("reasonInput") - .setLabel("Reason") - .setStyle(Discord.TextInputStyle.Paragraph) - .setRequired(true), - ), - ); - - return await interaction.showModal(modal); - } - - const cmd = client.commands.get(cmdconfig.OpenTicket.Command); - if (!cmd) { - return interaction.reply({ - content: "Open ticket command not found.", - flags: Discord.MessageFlags.Ephemeral, - }); - } - - interaction.department = selectedDepartment; - interaction.priority = - supportbot.Ticket.DepartmentSystem?.Departments?.[selectedDepartment] - ?.DefaultPriority || - supportbot.Ticket.PrioritySystem?.DefaultPriority || - "medium"; - - return await cmd.run(interaction); - } - - if (interaction.customId === "ticketcontrolpanel") { - const selectedOption = interaction.values[0]; - const { getRole, getChannel } = interaction.client; - switch (selectedOption) { - case "ticketclose": - const closeCommand = client.commands.get( - cmdconfig.CloseTicket.Command, - ); - if (closeCommand) { - await closeCommand.run(interaction); - } else { - console.error("[X] Close Command Not Found"); - } - break; - - case "supportvc": - const ticketInfo = db.getTicket(interaction.channel.id); - if (!ticketInfo) { - return interaction.reply({ - content: "⚠️ This channel isn’t a valid ticket.", - flags: Discord.MessageFlags.Ephemeral, - }); - } - - const user = - interaction.guild.members.cache.get(ticketInfo.user_id) || - ticketInfo.user_id; - const username = user?.user?.username || "Unknown User"; - - const VCcategory = interaction.guild.channels.cache.find( - (c) => - c.name === supportbot.VoiceTickets.Category || - c.id === supportbot.VoiceTickets.Category, - ); - - if (!VCcategory) { - return interaction.reply({ - content: "⚠️ The Support VC category does not exist!", - flags: Discord.MessageFlags.Ephemeral, - }); - } - - const SupportVoice = await interaction.guild.channels.create({ - name: supportbot.VoiceTickets.Name.replace(/%username%/g, username), - type: Discord.ChannelType.GuildVoice, - parent: VCcategory, - permissionOverwrites: [ - { - id: interaction.guild.id, - deny: [Discord.PermissionsBitField.Flags.Connect], - }, - { - id: user.id || user, // works if user is a Member or just ID - allow: [ - Discord.PermissionsBitField.Flags.Connect, - Discord.PermissionsBitField.Flags.Speak, - Discord.PermissionsBitField.Flags.ViewChannel, - ], - }, - ], - }); - - db.updateTicketVoice(ticketInfo.ticket_id, SupportVoice.id); - - const vcmadeembed = new Discord.EmbedBuilder() - .setDescription(`> **Support VC Created:** <#${SupportVoice.id}>`) - .setColor(supportbot.Embed.Colours.Success); - - const VCDeleteButton = new Discord.ButtonBuilder() - .setCustomId("deleteSupportVC") - .setLabel(supportbot.Buttons.Voice.TicketDeleteText) - .setEmoji(supportbot.Buttons.General.Delete) - .setStyle(supportbot.Buttons.General.DeleteStyle); - - const VCDeleteRow = new Discord.ActionRowBuilder().addComponents( - VCDeleteButton, - ); - - await interaction.reply({ - embeds: [vcmadeembed], - components: [VCDeleteRow], - }); - break; - - case "archiveticket": - const arcembed = new Discord.EmbedBuilder() - .setDescription(msgconfig.Ticket.TicketArchived) - .setColor(supportbot.Embed.Colours.Success); - const CloseButton = new Discord.ButtonBuilder() - .setCustomId("confirmCloseTicket") - .setLabel(supportbot.Ticket.Close.Confirmation_Button) - .setEmoji(supportbot.Ticket.Close.Confirmation_Emoji) - .setStyle(supportbot.Ticket.Close.Confirmation_Style); - const unarchiveButton = new Discord.ButtonBuilder() - .setCustomId("unarchiveTicket") - .setLabel(supportbot.Buttons.Tickets.Unarchive) - .setEmoji(supportbot.Buttons.Tickets.Unarchive_Emoji) - .setStyle(supportbot.Buttons.Tickets.Unarchive_Style); - const ArchivedOptions = new Discord.ActionRowBuilder().addComponents( - CloseButton, - unarchiveButton, - ); - await interaction.channel.send({ - embeds: [arcembed], - components: [ArchivedOptions], - }); - if (supportbot.Ticket.TicketType === "threads") { - await interaction.channel.setArchived(true); - } - if (supportbot.Ticket.TicketType === "channels") { - const { getRole, getChannel } = interaction.client; - await interaction.channel.setParent( - supportbot.Ticket.TicketArchiveCategory, - ); - const tickets = db.getAllTickets(); - const ticketInfo = tickets.find( - (t) => t.ticket_id === interaction.channel.id, - ); - if (ticketInfo) { - const user = - interaction.guild.members.cache.get(ticketInfo.user_id) || - ticketInfo.user_id; - const claimedBy = - interaction.guild.members.cache.get(ticketInfo.claimed_by) || - ticketInfo.claimed_by; - const Admin = await interaction.guild.roles.fetch( - supportbot.Roles.StaffMember.Admin, - ); - const Staff = await interaction.guild.roles.fetch( - supportbot.Roles.StaffMember.Staff, - ); - if (user && user.id) { - await interaction.channel.permissionOverwrites.edit(user.id, { - ViewChannel: false, - SendMessages: false, - }); - } - if (supportbot.ClaimTickets === true) { - if (claimedBy && claimedBy.id) { - await interaction.channel.permissionOverwrites.edit( - claimedBy.id, - { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }, - ); - } - } - if (supportbot.ClaimTickets === false) { - if (Staff && Staff.id) { - await interaction.channel.permissionOverwrites.edit( - claimedBy.id, - { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }, - ); - } - } - if (Admin && Admin.id) { - await interaction.channel.permissionOverwrites.edit(Admin.id, { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }); - } - await interaction.channel.permissionOverwrites.edit( - interaction.guild.roles.everyone.id, - { - ViewChannel: false, - SendMessages: false, - ReadMessageHistory: false, - }, - ); - } - } - break; - - case "lockticket": - const lockembed = new Discord.EmbedBuilder() - .setDescription(msgconfig.Ticket.TicketLocked) - .setColor(supportbot.Embed.Colours.Success); - const unlockButton = new Discord.ButtonBuilder() - .setCustomId("unlockTicket") - .setLabel(supportbot.Buttons.Tickets.Unlock) - .setEmoji(supportbot.Buttons.Tickets.Unlock_Emoji) - .setStyle(supportbot.Buttons.Tickets.Unlock_Style); - const LockedOptions = new Discord.ActionRowBuilder().addComponents( - unlockButton, - ); - await interaction.channel.send({ - embeds: [lockembed], - components: [LockedOptions], - ephemeral: false, - }); - if (supportbot.Ticket.TicketType === "threads") { - await interaction.channel.setLocked(true); - } - break; - - case "unlockTicket": - const unlockEmbed = new Discord.EmbedBuilder() - .setDescription(msgconfig.Ticket.TicketUnlocked) - .setColor(supportbot.Embed.Colours.Success); - await interaction.channel.send({ - embeds: [unlockEmbed], - ephemeral: false, - }); - if (supportbot.Ticket.TicketType === "threads") { - await interaction.channel.setLocked(false); - } - break; - - case "enableinvites": - await handleInvites(interaction, true); - break; - - case "disableinvites": - await handleInvites(interaction, false); - break; - - case "renameticket": - const modal = new Discord.ModalBuilder() - .setCustomId("renameTicketModal") - .setTitle("Rename Channel") - .addComponents( - new Discord.ActionRowBuilder().addComponents( - new Discord.TextInputBuilder() - .setCustomId("renameTicket") - .setLabel("New Channel Name") - .setStyle(Discord.TextInputStyle.Short) - .setRequired(true), - ), - ); - await interaction.showModal(modal); - break; - } - } - } - - if (interaction.customId === "deleteSupportVC") { - const ticketData = db.getAllTickets(); - const ticketInfo = ticketData.find( - (t) => t.ticket_id === interaction.channel.id, - ); - if (ticketInfo && ticketInfo.voiceChannelId) { - try { - const voiceChannel = await interaction.guild.channels - .fetch(ticketInfo.voiceChannelId) - .catch(() => null); - if (voiceChannel) { - await voiceChannel.delete(); - const successEmbed = new Discord.EmbedBuilder() - .setDescription( - "✅ Support voice channel has been successfully deleted!", - ) - .setColor(supportbot.Embed.Colours.Success); - await interaction.reply({ - embeds: [successEmbed], - flags: Discord.MessageFlags.Ephemeral, - }); - } else { - const errorEmbed = new Discord.EmbedBuilder() - .setDescription( - "❌ The associated voice channel could not be found.", - ) - .setColor(supportbot.Embed.Colours.Error); - await interaction.reply({ - embeds: [errorEmbed], - flags: Discord.MessageFlags.Ephemeral, - }); - } - } catch (error) { - console.error("Error deleting the Voice Channel:", error); - const errorEmbed = new Discord.EmbedBuilder() - .setDescription( - "❌ **There was an error trying to delete this voice channel. Please try again later.**", - ) - .setColor(supportbot.Embed.Colours.Error); - await interaction.reply({ - embeds: [errorEmbed], - flags: Discord.MessageFlags.Ephemeral, - }); - } - } else { - const errorEmbed = new Discord.EmbedBuilder() - .setDescription( - "⚠️ This button is not linked to a valid voice channel.", - ) - .setColor(supportbot.Embed.Colours.Warn); - await interaction.reply({ - embeds: [errorEmbed], - flags: Discord.MessageFlags.Ephemeral, - }); - } - } - - if (interaction.isButton()) { - if (interaction.customId.startsWith("ticket_ai_enable:")) { - const ticketId = interaction.customId.split(":")[1]; - const ticket = db.getTicket(ticketId); - - if (!ticket) { - return interaction.reply({ - content: msgconfig.Ticket.AIMode.TicketNotFound, - flags: Discord.MessageFlags.Ephemeral, - }); - } - - try { - const usingThreads = supportbot.Ticket.TicketType === "threads"; - - if (usingThreads) { - db.setTicketAIState(ticketId, { - enabled: true, - aiChannelId: interaction.channel.id, - aiType: "thread", - enabledBy: interaction.user.id, - enabledAt: new Date().toISOString(), - }); - - const embed = new Discord.EmbedBuilder() - .setDescription(msgconfig.Ticket.AIMode.EnabledMessage) - .setColor(supportbot.Embed.Colours.Success); - - return interaction.reply({ - embeds: [embed], - flags: Discord.MessageFlags.Ephemeral, - }); - } - - let aiChannelId = ticket.aiChannelId; - - if (!aiChannelId) { - const threadName = - supportbot.Ticket.AIMode?.ThreadName || "Chat with SupportBot AI"; - - const aiThread = await interaction.channel.threads.create({ - name: threadName, - autoArchiveDuration: 60, - reason: "ticket ai mode thread", - }); - - await aiThread.members.add(interaction.user.id).catch(() => {}); - - aiChannelId = aiThread.id; - - const welcomeEmbed = new Discord.EmbedBuilder() - .setTitle(msgconfig.Ticket.AIMode.ThreadWelcomeTitle) - .setDescription(msgconfig.Ticket.AIMode.ThreadWelcomeMessage) - .setColor(supportbot.Embed.Colours.General); - - await aiThread.send({ embeds: [welcomeEmbed] }); - } - - db.setTicketAIState(ticketId, { - enabled: true, - aiChannelId, - aiType: "thread", - enabledBy: interaction.user.id, - enabledAt: new Date().toISOString(), - }); - - const embed = new Discord.EmbedBuilder() - .setDescription(msgconfig.Ticket.AIMode.EnabledMessage) - .setColor(supportbot.Embed.Colours.Success); - - return interaction.reply({ - embeds: [embed], - flags: Discord.MessageFlags.Ephemeral, - }); - } catch (error) { - console.error("Error enabling ticket AI:", error); - - return interaction.reply({ - content: msgconfig.Ticket.AIMode.EnableError, - flags: Discord.MessageFlags.Ephemeral, - }); - } - } - - if (interaction.customId.startsWith("ticket_ai_disable:")) { - const ticketId = interaction.customId.split(":")[1]; - const ticket = db.getTicket(ticketId); - - if (!ticket) { - return interaction.reply({ - content: msgconfig.Ticket.AIMode.TicketNotFound, - flags: Discord.MessageFlags.Ephemeral, - }); - } - - try { - db.setTicketAIState(ticketId, { - enabled: false, - aiChannelId: null, - aiType: null, - enabledBy: interaction.user.id, - enabledAt: null, - }); - - const embed = new Discord.EmbedBuilder() - .setDescription(msgconfig.Ticket.AIMode.DisabledMessage) - .setColor(supportbot.Embed.Colours.Success); - - return interaction.reply({ - embeds: [embed], - flags: Discord.MessageFlags.Ephemeral, - }); - } catch (error) { - console.error("Error disabling ticket AI:", error); - - return interaction.reply({ - content: msgconfig.Ticket.AIMode.DisableError, - flags: Discord.MessageFlags.Ephemeral, - }); - } - } - - if (interaction.customId === "unarchiveTicket") { - if (supportbot.Ticket.TicketType === "threads") { - await interaction.channel.setArchived(false); - } - if (supportbot.Ticket.TicketType === "channels") { - const { getRole, getChannel } = interaction.client; - await interaction.channel.setParent( - supportbot.Ticket.TicketChannelsCategory, - ); - const ticketData = db.getAllTickets(); - const ticketInfo = ticketData.find( - (t) => t.ticket_id === interaction.channel.id, - ); - if (ticketInfo) { - const user = - interaction.guild.members.cache.get(ticketInfo.user_id) || - ticketInfo.user_id; - const claimedBy = - interaction.guild.members.cache.get(ticketInfo.claimed_by) || - ticketInfo.claimed_by; - const Admin = await interaction.guild.roles.fetch( - supportbot.Roles.StaffMember.Admin, - ); - const Staff = await interaction.guild.roles.fetch( - supportbot.Roles.StaffMember.Staff, - ); - if (user && user.id) { - await interaction.channel.permissionOverwrites.edit(user.id, { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }); - } - if (supportbot.ClaimTickets === true) { - if (claimedBy && claimedBy.id) { - await interaction.channel.permissionOverwrites.edit( - claimedBy.id, - { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }, - ); - } - } - if (supportbot.ClaimTickets === false) { - if (Staff && Staff.id) { - await interaction.channel.permissionOverwrites.edit( - claimedBy.id, - { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }, - ); - } - } - if (Admin && Admin.id) { - await interaction.channel.permissionOverwrites.edit(Admin.id, { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }); - } - await interaction.channel.permissionOverwrites.edit( - interaction.guild.roles.everyone.id, - { - ViewChannel: false, - SendMessages: false, - ReadMessageHistory: false, - }, - ); - } - } - const unrcembed = new Discord.EmbedBuilder() - .setDescription(msgconfig.Ticket.TicketUnarchived) - .setColor(supportbot.Embed.Colours.Success); - await interaction.update({ - embeds: [unrcembed], - components: [], - }); - } - - if (interaction.customId === "createticket") { - if (supportbot.Ticket.TicketReason) { - const modal = new Discord.ModalBuilder() - .setCustomId("ticketReasonModal") - .setTitle("Ticket Reason") - .addComponents( - new Discord.ActionRowBuilder().addComponents( - new Discord.TextInputBuilder() - .setCustomId("reasonInput") - .setLabel("Reason") - .setStyle(Discord.TextInputStyle.Paragraph) - .setRequired(true), - ), - ); - await interaction.showModal(modal); - } else { - try { - const cmd = client.commands.get(cmdconfig.OpenTicket.Command); - if (!cmd) return; - await cmd.run(interaction); - } catch (error) { - console.error("Error executing the ticket command:", error); - await interaction.reply({ - content: "An error occurred while creating the ticket.", - flags: Discord.MessageFlags.Ephemeral, - }); - } - } - } - - if (interaction.customId === "openticket") { - if (supportbot.Ticket.TicketReason) { - const modal = new Discord.ModalBuilder() - .setCustomId("ticketReasonModal") - .setTitle("Ticket Reason") - .addComponents( - new Discord.ActionRowBuilder().addComponents( - new Discord.TextInputBuilder() - .setCustomId("reasonInput") - .setLabel("Reason") - .setStyle(Discord.TextInputStyle.Paragraph) - .setRequired(true), - ), - ); - await interaction.showModal(modal); - } else { - try { - const cmd = client.commands.get(cmdconfig.OpenTicket.Command); - if (!cmd) return; - await cmd.run(interaction); - } catch (error) { - console.error(error); - } - } - } - - if (interaction.customId.startsWith("claimticket-")) { - const ticketChannelId = interaction.customId.split("-")[1]; - const ticketChannel = - interaction.guild.channels.cache.get(ticketChannelId); - if (!ticketChannel) { - return interaction.reply({ - content: "Ticket channel not found.", - flags: Discord.MessageFlags.Ephemeral, - }); - } - - const { getRole } = interaction.client; - const Staff = await getRole( - supportbot.Roles.StaffMember.Staff, - interaction.guild, - ); - const Admin = await getRole( - supportbot.Roles.StaffMember.Admin, - interaction.guild, - ); - - const RolePerms = new Discord.EmbedBuilder() - .setTitle("Invalid Permissions!") - .setDescription( - `${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``, - ) - .setColor(supportbot.Embed.Colours.Warn); - - if (!Staff || !Admin) { - return interaction.reply({ embeds: [RolePerms], ephemeral: true }); - } - - const NoPermsClaim = new Discord.EmbedBuilder() - .setTitle(msgconfig.Ticket.ClaimTickets.NoPermsToClaimTitle) - .setDescription( - msgconfig.Ticket.ClaimTickets.NoPermsToClaim.replace( - "%channel%", - ticketChannel.name, - ), - ) - .setColor(supportbot.Embed.Colours.Error); - - if ( - !interaction.member.roles.cache.has(Staff.id) && - !interaction.member.roles.cache.has(Admin.id) - ) { - return interaction.reply({ embeds: [NoPermsClaim], ephemeral: true }); - } - - if (supportbot.Ticket.TicketType === "threads") { - await ticketChannel.members.add(interaction.user.id); - } else { - await ticketChannel.permissionOverwrites.create(interaction.user.id, { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }); - await ticketChannel.permissionOverwrites.create(Admin, { - ViewChannel: true, - SendMessages: true, - ReadMessageHistory: true, - }); - await ticketChannel.permissionOverwrites.create( - interaction.guild.roles.everyone, - { - ViewChannel: false, - }, - ); - } - - const pingclaimedstaff = await ticketChannel.send( - `<@${interaction.user.id}>`, - ); - setTimeout(() => pingclaimedstaff.delete(), 2000); - - const ticketInfo = db.getTicket(ticketChannelId); - if (ticketInfo) { - db.updateTicketStatus(ticketChannelId, ticketInfo.status); - ticketInfo.claimed_by = interaction.user.id; - } - - const claimMessage = await interaction.message.fetch(); - const claimedEmbed = new Discord.EmbedBuilder(claimMessage.embeds[0].data) - .setTitle(msgconfig.Ticket.ClaimTickets.ClaimedTitle) - .setDescription( - msgconfig.Ticket.ClaimTickets.ClaimMessage_Edit.replace( - "%user%", - `${interaction.user.id}`, - ).replace("%channel%", `${ticketChannel.id}`), - ) - .setColor(supportbot.Embed.Colours.Success); - - const successfullyClaimed = new Discord.EmbedBuilder() - .setTitle(msgconfig.Ticket.ClaimTickets.ClaimedTitle) - .setDescription( - msgconfig.Ticket.ClaimTickets.Claimed.replace( - "%channel%", - `${ticketChannel.id}`, - ), - ) - .setColor(supportbot.Embed.Colours.Success); - - await claimMessage.edit({ embeds: [claimedEmbed], components: [] }); - await interaction.reply({ - embeds: [successfullyClaimed], - ephemeral: true, - }); - } - - // SUGGESTION INTERACTIONS [START] - - if ( - interaction.customId === "upvote" || - interaction.customId === "downvote" || - interaction.customId === "removevote" - ) { - const suggestion = suggestions[interaction.message.id]; - if (!suggestion) - return interaction.reply({ - content: "Suggestion not found.", - ephemeral: true, - }); - - const userId = interaction.user.id; - const hasUpvoted = suggestion.upvotes.includes(userId); - const hasDownvoted = suggestion.downvotes.includes(userId); - - if (interaction.customId === "upvote") { - if (hasUpvoted) { - suggestion.upvotes = suggestion.upvotes.filter((id) => id !== userId); - } else { - suggestion.upvotes.push(userId); - if (hasDownvoted) { - suggestion.downvotes = suggestion.downvotes.filter( - (id) => id !== userId, - ); - } - } - } else if (interaction.customId === "downvote") { - if (hasDownvoted) { - suggestion.downvotes = suggestion.downvotes.filter( - (id) => id !== userId, - ); - } else { - suggestion.downvotes.push(userId); - if (hasUpvoted) { - suggestion.upvotes = suggestion.upvotes.filter( - (id) => id !== userId, - ); - } - } - } else if (interaction.customId === "removevote") { - suggestion.upvotes = suggestion.upvotes.filter((id) => id !== userId); - suggestion.downvotes = suggestion.downvotes.filter( - (id) => id !== userId, - ); - } - - fs.writeFileSync( - "./Data/SuggestionData.json", - JSON.stringify(suggestions, null, 2), - ); - - const upvoteCount = suggestion.upvotes.length; - const downvoteCount = suggestion.downvotes.length; - - const updatedEmbed = new Discord.EmbedBuilder( - interaction.message.embeds[0].data, - ).setFields([ - { name: "Suggestion", value: suggestion.suggestion, inline: true }, - { name: "From", value: `<@${suggestion.author}>` }, - { - name: `${supportbot.Suggestions.UpvoteEmoji} ${supportbot.Suggestions.UpvoteTitle}`, - value: `${upvoteCount}`, - inline: true, - }, - { - name: `${supportbot.Suggestions.DownvoteEmoji} ${supportbot.Suggestions.DownvoteTitle}`, - value: `${downvoteCount}`, - inline: true, - }, - ]); - - await interaction.update({ embeds: [updatedEmbed] }); - } - } - - async function handleInvites(interaction, enable) { - if (supportbot.Ticket.Invites.StaffOnly) { - const { getRole, getChannel } = interaction.client; - const SupportStaff = await getRole( - supportbot.Roles.StaffMember.Staff, - interaction.guild, - ); - const Admin = await getRole( - supportbot.Roles.StaffMember.Admin, - interaction.guild, - ); - - if (!SupportStaff || !Admin) { - return interaction.reply( - "Some roles seem to be missing!\nPlease check for errors when starting the bot.", - ); - } - - const NoPerms = new Discord.EmbedBuilder() - .setTitle("Invalid Permissions!") - .setDescription( - `${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``, - ) - .setColor(supportbot.Embed.Colours.Warn); - - if ( - !interaction.member.roles.cache.has(SupportStaff.id) && - !interaction.member.roles.cache.has(Admin.id) - ) { - return interaction.reply({ embeds: [NoPerms] }); - } - } - - await interaction.channel.setInvitable(enable); - - const message = enable - ? msgconfig.Ticket.EnableInvites - : msgconfig.Ticket.DisableInvites; - const embed = new Discord.EmbedBuilder() - .setDescription(message) - .setColor(supportbot.Embed.Colours.General); - - await interaction.reply({ - embeds: [embed], - flags: Discord.MessageFlags.Ephemeral, - }); - } - - if ( - interaction.type === Discord.InteractionType.ModalSubmit && - interaction.customId === "editProfileModal" - ) { - const userId = interaction.user.id; - const profilePath = `./Data/Profiles/${userId}.json`; - - let profileData; - if (fs.existsSync(profilePath)) { - profileData = JSON.parse(fs.readFileSync(profilePath, "utf8")); - } else { - profileData = { - bio: "", - timezone: "", - clockedIn: false, - }; - } - - try { - const newBio = interaction.fields.getTextInputValue("bio"); - const newTimezone = interaction.fields.getTextInputValue("timezone"); - - profileData.bio = newBio; - profileData.timezone = newTimezone; - - fs.writeFileSync(profilePath, JSON.stringify(profileData, null, 2)); - - const updatedProfileEmbed = new Discord.EmbedBuilder() - .setTitle(`${interaction.user.username}'s Profile`) - .setColor(supportbot.Embed.Colours.General) - .setThumbnail(interaction.user.displayAvatarURL()) - .addFields( - { name: "Bio", value: newBio || "No bio set.", inline: false }, - { - name: "Timezone", - value: newTimezone || "No timezone set.", - inline: true, - }, - ); - - if (profileData.clockedIn !== undefined) { - updatedProfileEmbed.addFields({ - name: "Clocked In Status", - value: profileData.clockedIn ? "✅ Clocked In" : "❌ Clocked Out", - inline: true, - }); - } - - await interaction.reply({ - embeds: [updatedProfileEmbed], - ephemeral: true, - }); - } catch (error) { - console.error("Failed to update profile data: ", error); - await interaction.reply({ - content: - "There was an error while updating your profile. Please try again later.", - flags: Discord.MessageFlags.Ephemeral, - }); - } - } -}); diff --git a/Events/messageCreate.js b/Events/messageCreate.js deleted file mode 100644 index 0d0a288..0000000 --- a/Events/messageCreate.js +++ /dev/null @@ -1,578 +0,0 @@ -const Discord = require("discord.js"); -const axios = require("axios"); - -const supportbotai = require("../Structures/ConfigStore").supportbotAi; -const supportbot = require("../Structures/ConfigStore").supportbot; - -const Event = require("../Structures/Event.js"); -const AIDatabase = require("../Structures/AIDatabase.js"); -const db = require("../Structures/Database.js"); - -const openai = require("./Models/openai.js"); -const groq = require("./Models/groq.js"); -const claude = require("./Models/claude.js"); - -const PASTEBIN_API_KEY = supportbotai.General.PastebinAPI_Key; -const PASTEBIN_API_URL = supportbotai.General.PastebinAPI_URL; - -const CHANNELS = [supportbotai.Channels.AIChannel].filter(Boolean); - -async function createPaste(content) { - try { - if (!PASTEBIN_API_KEY || !PASTEBIN_API_URL) return null; - - const response = await axios.post( - PASTEBIN_API_URL, - new URLSearchParams({ - api_dev_key: PASTEBIN_API_KEY, - api_option: "paste", - api_paste_code: content, - api_paste_private: 1, - api_paste_expire_date: "1D", - api_paste_format: "text", - }), - ); - - return response.data; - } catch (error) { - console.error("Pastebin Error:", error); - return null; - } -} - -function getProvider() { - const provider = String( - supportbotai.General.Provider || "openai", - ).toLowerCase(); - - switch (provider) { - case "openai": - return openai; - - case "groq": - return groq; - - case "claude": - return claude; - - default: - throw new Error(`Unsupported AI provider: ${provider}`); - } -} - -function formatTicketQuestionAnswers(questionAnswers) { - if (!Array.isArray(questionAnswers) || questionAnswers.length === 0) { - return "No ticket question answers were saved."; - } - - return questionAnswers - .map((item, index) => { - const question = item?.question || `Question ${index + 1}`; - const answer = item?.answer || "No answer provided"; - return `${index + 1}. ${question}\nAnswer: ${answer}`; - }) - .join("\n\n"); -} - -function buildTicketContext(ticket, message) { - if (!ticket) return ""; - - return ` -Ticket Information: -Ticket ID: ${ticket.ticket_id || ticket.id || "Unknown"} -Subject: ${ticket.subject || ticket.reason || "No subject provided"} -Department: ${ticket.department || "Not set"} -Priority: ${ticket.priority || "Not set"} -Created By: ${ticket.user_id || ticket.user || "Unknown"} -Current User: ${message?.author?.tag || "Unknown"} (${message?.author?.id || "Unknown"}) - -Ticket Question Answers: -${formatTicketQuestionAnswers(ticket.questionAnswers)} - -Use this information when helping the user with their ticket. -Do not invent missing ticket details. -`; -} - -function buildSystemPrompt(message) { - const sections = []; - - if (Array.isArray(supportbotai.CustomInstructions)) { - sections.push(...supportbotai.CustomInstructions); - } - - sections.push( - `I am currently powered by the ${supportbotai.General.Provider || "Unknown"} provider, running the ${supportbotai.General.Model || "Unknown"} model.`, - ); - sections.push( - `The current guild is: ${message.guild?.name || "Unknown Guild"}`, - ); - sections.push( - `The current channel is: #${message.channel?.name || "unknown-channel"}`, - ); - - return sections.join("\n- ").replace(/^/, "- "); -} - -function buildConversationFromMemory(currentMessage) { - const memoryConfig = supportbotai.Memory || {}; - const memoryEnabled = memoryConfig.Enabled !== false; - const maxRecentMessages = memoryConfig.MaxRecentMessages || 15; - const memoryScope = String(memoryConfig.Scope || "guild").toLowerCase(); - - const conversation = [ - { - role: "system", - content: buildSystemPrompt(currentMessage), - }, - ]; - - if (!memoryEnabled) return conversation; - - const recentMessages = - typeof AIDatabase.getRecentMessagesByScope === "function" - ? AIDatabase.getRecentMessagesByScope({ - guildId: currentMessage.guild?.id || null, - channelId: currentMessage.channel?.id || null, - limit: maxRecentMessages, - scope: memoryScope, - }) - : AIDatabase.getRecentMessages( - currentMessage.channel?.id, - maxRecentMessages, - ); - - const latestSummary = - memoryConfig.StoreSummaries !== false - ? typeof AIDatabase.getLatestSummaryByScope === "function" - ? AIDatabase.getLatestSummaryByScope({ - guildId: currentMessage.guild?.id || null, - channelId: currentMessage.channel?.id || null, - scope: memoryScope, - }) - : AIDatabase.getLatestSummary(currentMessage.channel?.id) - : null; - - const facts = - memoryConfig.StoreFacts !== false - ? typeof AIDatabase.getFactsByScope === "function" - ? AIDatabase.getFactsByScope({ - guildId: currentMessage.guild?.id || null, - channelId: currentMessage.channel?.id || null, - scope: memoryScope, - }) - : AIDatabase.getFacts(currentMessage.channel?.id) - : []; - - const userFacts = - memoryConfig.StoreFacts !== false && - typeof AIDatabase.getUserFactsByScope === "function" - ? AIDatabase.getUserFactsByScope({ - guildId: currentMessage.guild?.id || null, - userId: currentMessage.author?.id || null, - scope: memoryScope, - }) - : memoryConfig.StoreFacts !== false && - typeof AIDatabase.getUserFacts === "function" - ? AIDatabase.getUserFacts( - currentMessage.guild?.id, - currentMessage.author?.id, - ) - : []; - - const knowledgeBase = typeof AIDatabase.getKnowledge === "function" - ? AIDatabase.getKnowledge(25) - : []; - - if (latestSummary?.summary) { - conversation.push({ - role: "system", - content: `Shared memory summary (${memoryScope} scope):\n${latestSummary.summary}`, - }); - } - - if (facts.length > 0) { - const factText = facts - .map( - (fact) => `- [${fact.fact_type}] ${fact.fact_key}: ${fact.fact_value}`, - ) - .join("\n"); - - conversation.push({ - role: "system", - content: `Known shared facts (${memoryScope} scope):\n${factText}`, - }); - } - - if (userFacts.length > 0) { - const userFactText = userFacts - .map( - (fact) => `- [${fact.fact_type}] ${fact.fact_key}: ${fact.fact_value}`, - ) - .join("\n"); - - conversation.push({ - role: "system", - content: `Known facts about this user across chats:\n${userFactText}`, - }); - } - - if (knowledgeBase.length > 0) { - const knowledgeText = knowledgeBase - .map((k) => `- ${k.content}`) - .join("\n"); - - conversation.push({ - role: "system", - content: `Server Knowledge Base (use this to answer questions):\n${knowledgeText}`, - }); - } - - for (const msg of recentMessages) { - conversation.push({ - role: msg.role === "assistant" ? "assistant" : "user", - content: msg.content, - }); - } - - return conversation; -} - -function getTicketContextForMessage(message) { - if (!message?.channel) return null; - - if (typeof db.getTicketByAIChannel === "function") { - const aiTicket = db.getTicketByAIChannel(message.channel.id); - if (aiTicket) return aiTicket; - } - - if (typeof db.getTicket === "function") { - const directTicket = db.getTicket(message.channel.id); - if (directTicket) return directTicket; - } - - if (message.channel.isThread() && typeof db.getTicket === "function") { - const parentTicket = db.getTicket(message.channel.parentId); - if (parentTicket) return parentTicket; - } - - return null; -} - -function getAIMessageType() { - return String(supportbotai.Style?.MessageType || "embed").toLowerCase(); -} - -function shouldSuggestTicket(message, ticket) { - if (supportbotai.Features?.SuggestTickets !== true) return false; - if (ticket) return false; - if (message.channel.isThread()) return false; - return CHANNELS.includes(message.channel.id); -} - -function getSuggestTicketText() { - return ( - supportbotai.Messages?.SuggestTicketText || - "-# Need more help? React with 🎫 to open a ticket." - ); -} - -async function sendAIResponse({ - client, - message, - replyContent, - suggestTicket = false, -}) { - const messageType = getAIMessageType(); - const ticketHint = suggestTicket ? `\n\n${getSuggestTicketText()}` : ""; - - if (messageType === "text") { - return message.reply({ - content: `${replyContent}${ticketHint}`, - }); - } - - if (messageType === "components_v2") { - const { - ContainerBuilder, - TextDisplayBuilder, - SeparatorBuilder, - MessageFlags, - } = Discord; - - const container = new ContainerBuilder(); - - if (supportbot.Embed?.Colours?.General) { - container.setAccentColor( - parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16), - ); - } - - const title = new TextDisplayBuilder().setContent( - `**${supportbotai.General?.Name || "SupportBot AI"}**`, - ); - - const body = new TextDisplayBuilder().setContent(replyContent); - - container.addTextDisplayComponents(title); - container.addSeparatorComponents(new SeparatorBuilder().setDivider(true)); - container.addTextDisplayComponents(body); - - if (suggestTicket) { - container.addSeparatorComponents(new SeparatorBuilder().setDivider(true)); - container.addTextDisplayComponents( - new TextDisplayBuilder().setContent(getSuggestTicketText()), - ); - } - - return message.reply({ - flags: MessageFlags.IsComponentsV2, - components: [container], - }); - } - - const embed = new Discord.EmbedBuilder() - .setAuthor({ - name: supportbotai.General.Name, - iconURL: client.user.displayAvatarURL(), - }) - .setDescription(`${replyContent}${ticketHint}`) - .setColor(supportbotai.Embed.Color) - .setFooter({ - text: supportbotai.Embed.Footer, - iconURL: message.author.displayAvatarURL(), - }) - .setTimestamp(); - - return message.reply({ embeds: [embed] }); -} - -function rememberUserFacts(message) { - if (!message?.guild || !message?.author || !message?.content) return; - - const content = message.content.trim(); - - const nameMatch = content.match(/^my name is\s+(.+)$/i); - if (nameMatch && typeof AIDatabase.saveUserFact === "function") { - AIDatabase.saveUserFact({ - guildId: message.guild.id, - userId: message.author.id, - factType: "profile", - factKey: "name", - factValue: nameMatch[1].trim(), - }); - } - - const nicknameMatch = content.match(/^i(?:'| a)?m\s+(.+)$/i); - if ( - nicknameMatch && - nicknameMatch[1] && - nicknameMatch[1].length <= 32 && - typeof AIDatabase.saveUserFact === "function" - ) { - AIDatabase.saveUserFact({ - guildId: message.guild.id, - userId: message.author.id, - factType: "profile", - factKey: "display_name", - factValue: nicknameMatch[1].trim(), - }); - } - - const usesMatch = content.match(/^i use\s+(.+)$/i); - if (usesMatch && typeof AIDatabase.saveUserFact === "function") { - AIDatabase.saveUserFact({ - guildId: message.guild.id, - userId: message.author.id, - factType: "preference", - factKey: "uses", - factValue: usesMatch[1].trim(), - }); - } -} - -module.exports = new Event("messageCreate", async (client, message) => { - if (message.author.bot || !message.guild) return; - - let shouldRespond = - CHANNELS.includes(message.channelId) || - message.mentions.users.has(client.user.id); - - if (!shouldRespond && message.channel.isThread()) { - let ticket = null; - - if (typeof db.getTicketByAIChannel === "function") { - ticket = db.getTicketByAIChannel(message.channel.id); - } - - if (!ticket && typeof db.getTicket === "function") { - ticket = db.getTicket(message.channel.id); - } - - if ( - !ticket && - message.channel.parentId && - typeof db.getTicket === "function" - ) { - ticket = db.getTicket(message.channel.parentId); - } - - if ( - ticket && - ticket.aiModeEnabled && - ticket.aiChannelId === message.channel.id - ) { - shouldRespond = true; - } - } - - if (!shouldRespond) return; - - if (!supportbotai.Enabled) { - const embed = new Discord.EmbedBuilder() - .setAuthor({ - name: supportbotai.General.Name, - iconURL: client.user.displayAvatarURL(), - }) - .setDescription( - supportbotai.Messages?.AIDisabled || "AI is currently disabled.", - ) - .setColor(supportbot.Embed.Colours.Error) - .setFooter({ - text: supportbotai.Embed.Footer, - iconURL: message.author.displayAvatarURL(), - }); - - return message.reply({ embeds: [embed] }); - } - - const typingInterval = setInterval(() => { - message.channel.sendTyping().catch(() => { }); - }, 5000); - - try { - const memoryConfig = supportbotai.Memory || {}; - const memoryEnabled = memoryConfig.Enabled !== false; - - if (memoryEnabled) { - AIDatabase.addMessage({ - guildId: message.guild.id, - channelId: message.channel.id, - userId: message.author.id, - role: "user", - content: message.content, - }); - - rememberUserFacts(message); - } - - const ticket = getTicketContextForMessage(message); - - const conversation = buildConversationFromMemory(message); - - if (ticket) { - conversation.push({ - role: "system", - content: buildTicketContext(ticket, message), - }); - } - - const provider = getProvider(); - - const response = await provider.chat({ - messages: conversation, - temperature: supportbotai.General.Temperature || 0.4, - maxTokens: supportbotai.General.Tokens || 800, - }); - - clearInterval(typingInterval); - - if (!response?.content) { - const embed = new Discord.EmbedBuilder() - .setDescription( - supportbotai.Messages?.ErrorResponse || - "I couldn't generate a response.", - ) - .setColor(supportbotai.Embed.Color); - - return message.reply({ embeds: [embed] }); - } - - const replyContent = response.content; - - if (memoryEnabled) { - AIDatabase.addMessage({ - guildId: message.guild.id, - channelId: message.channel.id, - userId: client.user.id, - role: "assistant", - content: replyContent, - }); - } - - if (replyContent.length > 4000) { - const pasteLink = await createPaste(replyContent); - - if (pasteLink) { - const embed = new Discord.EmbedBuilder() - .setDescription( - ( - supportbotai.Messages?.MessageTooLong || - "My response was too long, so I uploaded it here: {pastebin}" - ).replace("{pastebin}", pasteLink), - ) - .setColor(supportbotai.Embed.Color); - - return message.reply({ embeds: [embed] }); - } - - return message.reply({ - content: replyContent.slice(0, 1900), - }); - } - - const suggestTicket = shouldSuggestTicket(message, ticket); - - const sentMessage = await sendAIResponse({ - client, - message, - replyContent, - suggestTicket: false, - }); - - if ( - shouldSuggestTicket(message, ticket) && - supportbotai.Features?.SuggestTickets === true - ) { - const hintMessage = await message.channel.send( - supportbotai.Messages?.SuggestTicketText || - "Need more help? React with 🎫 to open a ticket.", - ); - - await hintMessage - .react(supportbotai.Messages?.SuggestTicketReaction || "🎫") - .catch(() => { }); - - setTimeout( - () => { - hintMessage.delete().catch(() => { }); - }, - 5 * 60 * 1000, - ); - } - } catch (error) { - clearInterval(typingInterval); - - console.error("AI Error:", error); - - const embed = new Discord.EmbedBuilder() - .setDescription( - supportbotai.Messages?.AIError || - supportbotai.Messages?.ErrorResponse || - "There was an error contacting the AI provider.", - ) - .setColor(supportbot.Embed.Colours.Error); - - message.reply({ embeds: [embed] }).catch(() => { }); - } -}); \ No newline at end of file diff --git a/Events/messageDelete.js b/Events/messageDelete.js deleted file mode 100644 index d2d2b5d..0000000 --- a/Events/messageDelete.js +++ /dev/null @@ -1,36 +0,0 @@ -const { EmbedBuilder } = require('discord.js'); -const fs = require("fs"); -const yaml = require("js-yaml"); -const Event = require("../Structures/Event.js"); - -const supportbot = require("../Structures/ConfigStore").supportbot; -const msgconfig = require("../Structures/ConfigStore").messages; - -module.exports = new Event("messageDelete", async (client, message) => { - if (!message.guild) return; - - const author = message.author || (message.partial ? await message.fetch().then(m => m.author).catch(() => null) : null); - if (!author || author.bot) return; - - const content = message.content || (message.attachments.size > 0 ? "Contains attachment(s)" : "*No content*"); - - const deleteLogChannel = message.guild.channels.cache.get(supportbot.MessageDelete.Channel) || - message.guild.channels.cache.find(c => c.name === supportbot.MessageDelete.Channel); - - const deletedMessageEmbed = new EmbedBuilder() - .setColor(supportbot.MessageDelete.Colour) - .setTitle("Message Deleted") - .setDescription(`> **Channel:** <#${message.channel.id}>\n> **Message ID:** ${message.id}\n> **Message author:** ${author.tag} (${author.id})\n> **Message Created:** `) - .addFields({ name: "Deleted Content", value: content, inline: false }) - .setThumbnail(author.displayAvatarURL()) - .setTimestamp(); - - if (message.attachments.size > 0) { - const attachment = message.attachments.first(); - deletedMessageEmbed.addFields({ name: "Attachment", value: `[${attachment.name}](${attachment.proxyURL})` }); - } - - if (deleteLogChannel) { - deleteLogChannel.send({ embeds: [deletedMessageEmbed] }); - } -}); diff --git a/Events/messageReactionAdd.js b/Events/messageReactionAdd.js deleted file mode 100644 index fe9614e..0000000 --- a/Events/messageReactionAdd.js +++ /dev/null @@ -1,68 +0,0 @@ -const supportbot = require("../Structures/ConfigStore").supportbot; -const supportbotai = require("../Structures/ConfigStore").supportbotAi; -const cmdconfig = require("../Structures/ConfigStore").commands; - -const Event = require("../Structures/Event.js"); - -module.exports = new Event( - "messageReactionAdd", - async (client, reaction, user) => { - try { - if (user.bot) return; - - if (reaction.partial) { - await reaction.fetch().catch(() => null); - } - - const message = reaction.message; - if (!message || !message.guild) return; - - const reactionEmoji = reaction.emoji?.name || reaction.emoji?.id || null; - const expectedReaction = - supportbotai.Messages?.SuggestTicketReaction || "🎫"; - - if (reactionEmoji !== expectedReaction) return; - if (supportbotai.Features?.SuggestTickets !== true) return; - if (message.author?.id !== client.user.id) return; - - const expectedText = - supportbotai.Messages?.SuggestTicketText || - "Need more help? React with 🎫 to open a ticket."; - - if ((message.content || "").trim() !== expectedText.trim()) return; - - await message.delete().catch(() => {}); - - const cmd = client.commands.get(cmdconfig.OpenTicket.Command); - if (!cmd) { - console.error("Open ticket command not found."); - return; - } - - const member = await message.guild.members - .fetch(user.id) - .catch(() => null); - - if (!member) return; - - const fakeInteraction = { - user, - member, - guild: message.guild, - channel: message.channel, - client, - replied: true, - deferred: false, - reason: `Opened from AI suggestion in #${message.channel.name}`, - options: { - getString: () => null, - }, - reply: async () => null, - }; - - await cmd.run(fakeInteraction); - } catch (error) { - console.error("Error handling AI ticket reaction:", error); - } - }, -); diff --git a/Events/messageUpdate.js b/Events/messageUpdate.js deleted file mode 100644 index ff963ee..0000000 --- a/Events/messageUpdate.js +++ /dev/null @@ -1,57 +0,0 @@ -const { EmbedBuilder } = require('discord.js'); -const fs = require("fs"); -const yaml = require("js-yaml"); -const Event = require("../Structures/Event.js"); - -// Load the configuration files -const supportbot = require("../Structures/ConfigStore").supportbot; -const msgconfig = require("../Structures/ConfigStore").messages; - -module.exports = new Event("messageUpdate", async (client, oldMessage, newMessage) => { - - // Check if the message is from a guild and not from a bot - if (!newMessage.guild || newMessage.author.bot) return; - - // Fetch old message if it's not cached (important for message content) - if (!oldMessage.content && oldMessage.partial) { - try { - oldMessage = await oldMessage.fetch(); - } catch (err) { - console.error('Error fetching old message:', err); - return; - } - } - - // Fallback if content is missing - const oldMessageContent = oldMessage.content || (oldMessage.attachments.size > 0 ? 'Contains attachment(s)' : '*No content*'); - const newMessageContent = newMessage.content || (newMessage.attachments.size > 0 ? 'Contains attachment(s)' : '*No content*'); - - // Get the update log channel either by ID or by name - const updateLogChannel = newMessage.guild.channels.cache.get(supportbot.MessageUpdate.Channel) || - newMessage.guild.channels.cache.find(channel => channel.name === supportbot.MessageUpdate.Channel); - - // Create the embed for the updated message - const updatedMessageEmbed = new EmbedBuilder() - .setColor(supportbot.MessageUpdate.Colour) - .setTitle("Message Updated") - .setDescription(`> **Channel:** <#${newMessage.channel.id}>\n> **Message ID:** [${newMessage.id}](https://discord.com/channels/${newMessage.guild.id}/${newMessage.channel.id}/${newMessage.id})\n> **Message author:** ${newMessage.author.tag} (${newMessage.author.id})\n> **Message Created:** `) - .addFields( - { name: 'Old Message', value: oldMessageContent, inline: false }, - { name: 'New Message', value: newMessageContent, inline: false } - ) - .setThumbnail(newMessage.author.displayAvatarURL()) - .setTimestamp(); - - // If the new message contains an attachment, add it to the embed - if (newMessage.attachments.size > 0) { - const attachment = newMessage.attachments.first(); - updatedMessageEmbed.addFields({ name: 'Attachment', value: `[${attachment.name}](${attachment.proxyURL})` }); - } - - // Send the embed to the update log channel if the channel is found - if (updateLogChannel) { - updateLogChannel.send({ - embeds: [updatedMessageEmbed] - }); - } -}); diff --git a/Events/ready.js b/Events/ready.js deleted file mode 100644 index 2db3a7f..0000000 --- a/Events/ready.js +++ /dev/null @@ -1,8 +0,0 @@ -const Event = require("../Structures/Event.js"); -const { runBotReady } = require("../Structures/BotStartup.js"); - -module.exports = new Event("clientReady", async (client) => { - // 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/Logs/Error/Error-2026-05-20.log b/Logs/Error/Error-2026-05-20.log deleted file mode 100644 index d73d89e..0000000 --- a/Logs/Error/Error-2026-05-20.log +++ /dev/null @@ -1,2 +0,0 @@ -[2026-05-20T12:58:22.767Z] (node:15252) Warning: Accessing non-existent property 'getIntrinsicHookSteps' of module exports inside circular dependency -(Use `node --trace-warnings ...` to show where the warning was created) diff --git a/Logs/Error/Error-2026-05-26.log b/Logs/Error/Error-2026-05-26.log deleted file mode 100644 index 7f9a9a2..0000000 --- a/Logs/Error/Error-2026-05-26.log +++ /dev/null @@ -1,4 +0,0 @@ -[2026-05-26T18:08:20.799Z] Unhandled Rejection: DiscordjsError [TokenInvalid]: An invalid token was provided. -[2026-05-26T18:30:31.132Z] Unhandled Rejection: DiscordjsError [TokenInvalid]: An invalid token was provided. -[2026-05-26T18:34:34.094Z] Unhandled Rejection: DiscordjsError [TokenInvalid]: An invalid token was provided. -[2026-05-26T19:00:20.833Z] Unhandled Rejection: DiscordjsError [TokenInvalid]: An invalid token was provided. diff --git a/Logs/Output/Output-2026-05-19.log b/Logs/Output/Output-2026-05-19.log deleted file mode 100644 index 01ef306..0000000 --- a/Logs/Output/Output-2026-05-19.log +++ /dev/null @@ -1,29 +0,0 @@ -[2026-05-19T22:25:25.691Z] [API] Server running on port 3000 -[2026-05-19T22:25:26.507Z] ──────────────────────────────────────────────────────── -[2026-05-19T22:25:26.508Z] SupportBot -[2026-05-19T22:25:26.508Z] Emerald Services -[2026-05-19T22:25:26.509Z] ──────────────────────────────────────────────────────── -[2026-05-19T22:25:26.509Z] -System -[2026-05-19T22:25:26.519Z] ------ -[2026-05-19T22:25:26.520Z] Bot Name  SupportBo -[2026-05-19T22:25:26.520Z] Version  v26.3.0 -[2026-05-19T22:25:26.520Z] Node.js  v24.14.1 -[2026-05-19T22:25:26.521Z] Discord.js  v14.26.4 -[2026-05-19T22:25:26.521Z] Logged in as  BLITZ#7183 -[2026-05-19T22:25:26.522Z] Guilds  1 -[2026-05-19T22:25:26.522Z] -Links -[2026-05-19T22:25:26.523Z] ----- -[2026-05-19T22:25:26.523Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-19T22:25:26.524Z] Discord  https://dsc.gg/emerald-dev -[2026-05-19T22:25:26.525Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-19T22:25:26.525Z] -[2026-05-19T22:25:26.526Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-19T22:25:26.527Z] -Config Check -[2026-05-19T22:25:26.528Z] ------------ -[2026-05-19T22:25:26.530Z] ✓ All configurations validated successfully -[2026-05-19T22:25:26.531Z] -[2026-05-19T22:25:26.702Z] -✓ Slash Commands Registered for Char's Test Server diff --git a/Logs/Output/Output-2026-05-20.log b/Logs/Output/Output-2026-05-20.log deleted file mode 100644 index 1b48abe..0000000 --- a/Logs/Output/Output-2026-05-20.log +++ /dev/null @@ -1,292 +0,0 @@ -[2026-05-20T12:19:19.626Z] [API] Server running on port 3000 -[2026-05-20T12:19:20.498Z] ──────────────────────────────────────────────────────── -[2026-05-20T12:19:20.499Z] SupportBot -[2026-05-20T12:19:20.499Z] Emerald Services -[2026-05-20T12:19:20.500Z] ──────────────────────────────────────────────────────── -[2026-05-20T12:19:20.500Z] -System -[2026-05-20T12:19:20.667Z] ------ -[2026-05-20T12:19:20.667Z] Bot Name  SupportBo -[2026-05-20T12:19:20.668Z] Version  v26.3.0 -[2026-05-20T12:19:20.668Z] Node.js  v24.14.1 -[2026-05-20T12:19:20.668Z] Discord.js  v14.26.4 -[2026-05-20T12:19:20.669Z] Logged in as  BLITZ#7183 -[2026-05-20T12:19:20.669Z] Guilds  1 -[2026-05-20T12:19:20.669Z] -Links -[2026-05-20T12:19:20.670Z] ----- -[2026-05-20T12:19:20.670Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-20T12:19:20.670Z] Discord  https://dsc.gg/emerald-dev -[2026-05-20T12:19:20.671Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-20T12:19:20.671Z] -[2026-05-20T12:19:20.672Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-20T12:19:20.675Z] -Config Check -[2026-05-20T12:19:20.676Z] ------------ -[2026-05-20T12:19:20.678Z] ✓ All configurations validated successfully -[2026-05-20T12:19:20.678Z] -[2026-05-20T12:19:20.887Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-20T12:24:38.643Z] [API] Server running on port 3000 -[2026-05-20T12:24:39.659Z] ──────────────────────────────────────────────────────── -[2026-05-20T12:24:39.660Z] SupportBot -[2026-05-20T12:24:39.660Z] Emerald Services -[2026-05-20T12:24:39.660Z] ──────────────────────────────────────────────────────── -[2026-05-20T12:24:39.661Z] -System -[2026-05-20T12:24:39.661Z] ------ -[2026-05-20T12:24:39.662Z] Bot Name  SupportBo -[2026-05-20T12:24:39.662Z] Version  v26.3.0 -[2026-05-20T12:24:39.663Z] Node.js  v24.14.1 -[2026-05-20T12:24:39.663Z] Discord.js  v14.26.4 -[2026-05-20T12:24:39.663Z] Logged in as  BLITZ#7183 -[2026-05-20T12:24:39.664Z] Guilds  1 -[2026-05-20T12:24:39.664Z] -Links -[2026-05-20T12:24:39.665Z] ----- -[2026-05-20T12:24:39.665Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-20T12:24:39.666Z] Discord  https://dsc.gg/emerald-dev -[2026-05-20T12:24:39.666Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-20T12:24:39.667Z] -[2026-05-20T12:24:39.667Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-20T12:24:39.670Z] -Config Check -[2026-05-20T12:24:39.671Z] ------------ -[2026-05-20T12:24:39.674Z] ✓ All configurations validated successfully -[2026-05-20T12:24:39.674Z] -[2026-05-20T12:24:39.875Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-20T12:26:52.985Z] [API] Server running on port 3000 -[2026-05-20T12:26:54.033Z] ──────────────────────────────────────────────────────── -[2026-05-20T12:26:54.034Z] SupportBot -[2026-05-20T12:26:54.034Z] Emerald Services -[2026-05-20T12:26:54.035Z] ──────────────────────────────────────────────────────── -[2026-05-20T12:26:54.035Z] -System -[2026-05-20T12:26:54.035Z] ------ -[2026-05-20T12:26:54.036Z] Bot Name  SupportBo -[2026-05-20T12:26:54.036Z] Version  v26.3.0 -[2026-05-20T12:26:54.037Z] Node.js  v24.14.1 -[2026-05-20T12:26:54.037Z] Discord.js  v14.26.4 -[2026-05-20T12:26:54.037Z] Logged in as  BLITZ#7183 -[2026-05-20T12:26:54.038Z] Guilds  1 -[2026-05-20T12:26:54.038Z] -Links -[2026-05-20T12:26:54.039Z] ----- -[2026-05-20T12:26:54.039Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-20T12:26:54.040Z] Discord  https://dsc.gg/emerald-dev -[2026-05-20T12:26:54.040Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-20T12:26:54.041Z] -[2026-05-20T12:26:54.042Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-20T12:26:54.044Z] -Config Check -[2026-05-20T12:26:54.045Z] ------------ -[2026-05-20T12:26:54.047Z] ✓ All configurations validated successfully -[2026-05-20T12:26:54.048Z] -[2026-05-20T12:26:54.256Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-20T12:27:29.337Z] [BotReload] Restarting Discord bot after config change… -[2026-05-20T12:27:30.140Z] ──────────────────────────────────────────────────────── -[2026-05-20T12:27:30.141Z] SupportBot -[2026-05-20T12:27:30.141Z] Emerald Services -[2026-05-20T12:27:30.141Z] ──────────────────────────────────────────────────────── -[2026-05-20T12:27:30.142Z] -System -[2026-05-20T12:27:30.142Z] ------ -[2026-05-20T12:27:30.143Z] Bot Name  SupportBo -[2026-05-20T12:27:30.144Z] Version  v26.3.0 -[2026-05-20T12:27:30.144Z] Node.js  v24.14.1 -[2026-05-20T12:27:30.144Z] Discord.js  v14.26.4 -[2026-05-20T12:27:30.145Z] Logged in as  BLITZ#7183 -[2026-05-20T12:27:30.145Z] Guilds  1 -[2026-05-20T12:27:30.146Z] -Links -[2026-05-20T12:27:30.146Z] ----- -[2026-05-20T12:27:30.147Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-20T12:27:30.147Z] Discord  https://dsc.gg/emerald-dev -[2026-05-20T12:27:30.147Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-20T12:27:30.148Z] -[2026-05-20T12:27:30.149Z] -Config Check -[2026-05-20T12:27:30.149Z] ------------ -[2026-05-20T12:27:30.150Z] ✓ All configurations validated successfully -[2026-05-20T12:27:30.150Z] -[2026-05-20T12:27:30.433Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-20T12:27:30.434Z] [BotReload] Bot restarted successfully -[2026-05-20T12:27:34.617Z]  [Executed]  ch_ar. has executed ticketpanel -[2026-05-20T12:27:34.856Z] [TICKET PANEL] Previous panel message not found, creating a new one. -[2026-05-20T12:39:22.918Z] [API] Server running on port 3000 -[2026-05-20T12:39:23.738Z] ──────────────────────────────────────────────────────── -[2026-05-20T12:39:23.739Z] SupportBot -[2026-05-20T12:39:23.739Z] Emerald Services -[2026-05-20T12:39:23.739Z] ──────────────────────────────────────────────────────── -[2026-05-20T12:39:23.740Z] -System -[2026-05-20T12:39:23.740Z] ------ -[2026-05-20T12:39:23.741Z] Bot Name  SupportBo -[2026-05-20T12:39:23.741Z] Version  v26.3.0 -[2026-05-20T12:39:23.741Z] Node.js  v24.14.1 -[2026-05-20T12:39:23.741Z] Discord.js  v14.26.4 -[2026-05-20T12:39:23.742Z] Logged in as  BLITZ#7183 -[2026-05-20T12:39:23.742Z] Guilds  1 -[2026-05-20T12:39:23.743Z] -Links -[2026-05-20T12:39:23.743Z] ----- -[2026-05-20T12:39:23.743Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-20T12:39:23.744Z] Discord  https://dsc.gg/emerald-dev -[2026-05-20T12:39:23.745Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-20T12:39:23.745Z] -[2026-05-20T12:39:23.746Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-20T12:39:23.748Z] -Config Check -[2026-05-20T12:39:23.750Z] ------------ -[2026-05-20T12:39:23.752Z] ✓ All configurations validated successfully -[2026-05-20T12:39:23.752Z] -[2026-05-20T12:39:23.966Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-20T12:46:34.090Z] [API] Server running on port 3000 -[2026-05-20T12:46:34.880Z] ──────────────────────────────────────────────────────── -[2026-05-20T12:46:34.880Z] SupportBot -[2026-05-20T12:46:34.881Z] Emerald Services -[2026-05-20T12:46:34.881Z] ──────────────────────────────────────────────────────── -[2026-05-20T12:46:34.882Z] -System -[2026-05-20T12:46:34.882Z] ------ -[2026-05-20T12:46:34.882Z] Bot Name  SupportBo -[2026-05-20T12:46:34.883Z] Version  v26.3.0 -[2026-05-20T12:46:34.883Z] Node.js  v24.14.1 -[2026-05-20T12:46:34.884Z] Discord.js  v14.26.4 -[2026-05-20T12:46:34.884Z] Logged in as  BLITZ#7183 -[2026-05-20T12:46:34.885Z] Guilds  1 -[2026-05-20T12:46:34.885Z] -Links -[2026-05-20T12:46:34.886Z] ----- -[2026-05-20T12:46:34.887Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-20T12:46:34.887Z] Discord  https://dsc.gg/emerald-dev -[2026-05-20T12:46:34.888Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-20T12:46:34.888Z] -[2026-05-20T12:46:34.889Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-20T12:46:34.890Z] -Config Check -[2026-05-20T12:46:34.891Z] ------------ -[2026-05-20T12:46:34.893Z] ✓ All configurations validated successfully -[2026-05-20T12:46:34.894Z] -[2026-05-20T12:46:35.096Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-20T12:52:39.673Z] [API] Server running on port 3000 -[2026-05-20T12:52:40.422Z] ──────────────────────────────────────────────────────── -[2026-05-20T12:52:40.423Z] SupportBot -[2026-05-20T12:52:40.423Z] Emerald Services -[2026-05-20T12:52:40.423Z] ──────────────────────────────────────────────────────── -[2026-05-20T12:52:40.424Z] -System -[2026-05-20T12:52:40.424Z] ------ -[2026-05-20T12:52:40.424Z] Bot Name  SupportBo -[2026-05-20T12:52:40.425Z] Version  v26.3.0 -[2026-05-20T12:52:40.425Z] Node.js  v24.14.1 -[2026-05-20T12:52:40.425Z] Discord.js  v14.26.4 -[2026-05-20T12:52:40.426Z] Logged in as  BLITZ#7183 -[2026-05-20T12:52:40.426Z] Guilds  1 -[2026-05-20T12:52:40.426Z] -Links -[2026-05-20T12:52:40.427Z] ----- -[2026-05-20T12:52:40.427Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-20T12:52:40.428Z] Discord  https://dsc.gg/emerald-dev -[2026-05-20T12:52:40.428Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-20T12:52:40.429Z] -[2026-05-20T12:52:40.429Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-20T12:52:40.431Z] -Config Check -[2026-05-20T12:52:40.431Z] ------------ -[2026-05-20T12:52:40.435Z] ✓ All configurations validated successfully -[2026-05-20T12:52:40.436Z] -[2026-05-20T12:52:40.672Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-20T12:58:22.768Z] [API] Server running on port 3000 -[2026-05-20T12:58:23.534Z] ──────────────────────────────────────────────────────── -[2026-05-20T12:58:23.534Z] SupportBot -[2026-05-20T12:58:23.534Z] Emerald Services -[2026-05-20T12:58:23.535Z] ──────────────────────────────────────────────────────── -[2026-05-20T12:58:23.535Z] -System -[2026-05-20T12:58:23.536Z] ------ -[2026-05-20T12:58:23.536Z] Bot Name  SupportBo -[2026-05-20T12:58:23.536Z] Version  v26.3.0 -[2026-05-20T12:58:23.537Z] Node.js  v24.14.1 -[2026-05-20T12:58:23.537Z] Discord.js  v14.26.4 -[2026-05-20T12:58:23.538Z] Logged in as  BLITZ#7183 -[2026-05-20T12:58:23.538Z] Guilds  1 -[2026-05-20T12:58:23.539Z] -Links -[2026-05-20T12:58:23.539Z] ----- -[2026-05-20T12:58:23.540Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-20T12:58:23.540Z] Discord  https://dsc.gg/emerald-dev -[2026-05-20T12:58:23.541Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-20T12:58:23.541Z] -[2026-05-20T12:58:23.542Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-20T12:58:23.543Z] -Config Check -[2026-05-20T12:58:23.544Z] ------------ -[2026-05-20T12:58:23.546Z] ✓ All configurations validated successfully -[2026-05-20T12:58:23.549Z] -[2026-05-20T12:58:23.750Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-20T13:00:24.932Z] [API] Server running on port 3000 -[2026-05-20T13:00:25.900Z] ──────────────────────────────────────────────────────── -[2026-05-20T13:00:25.901Z] SupportBot -[2026-05-20T13:00:25.901Z] Emerald Services -[2026-05-20T13:00:25.901Z] ──────────────────────────────────────────────────────── -[2026-05-20T13:00:25.902Z] -System -[2026-05-20T13:00:25.902Z] ------ -[2026-05-20T13:00:25.903Z] Bot Name  SupportBo -[2026-05-20T13:00:25.903Z] Version  v26.3.0 -[2026-05-20T13:00:25.903Z] Node.js  v24.14.1 -[2026-05-20T13:00:25.904Z] Discord.js  v14.26.4 -[2026-05-20T13:00:25.904Z] Logged in as  BLITZ#7183 -[2026-05-20T13:00:25.905Z] Guilds  1 -[2026-05-20T13:00:25.905Z] -Links -[2026-05-20T13:00:25.906Z] ----- -[2026-05-20T13:00:25.906Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-20T13:00:25.907Z] Discord  https://dsc.gg/emerald-dev -[2026-05-20T13:00:25.907Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-20T13:00:25.907Z] -[2026-05-20T13:00:25.908Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-20T13:00:25.909Z] -Config Check -[2026-05-20T13:00:25.910Z] ------------ -[2026-05-20T13:00:25.912Z] ✓ All configurations validated successfully -[2026-05-20T13:00:25.913Z] -[2026-05-20T13:00:26.169Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-20T13:06:04.723Z] [API] Server running on port 3000 -[2026-05-20T13:06:05.516Z] ──────────────────────────────────────────────────────── -[2026-05-20T13:06:05.517Z] SupportBot -[2026-05-20T13:06:05.517Z] Emerald Services -[2026-05-20T13:06:05.518Z] ──────────────────────────────────────────────────────── -[2026-05-20T13:06:05.518Z] -System -[2026-05-20T13:06:05.519Z] ------ -[2026-05-20T13:06:05.519Z] Bot Name  SupportBo -[2026-05-20T13:06:05.519Z] Version  v26.3.0 -[2026-05-20T13:06:05.520Z] Node.js  v24.14.1 -[2026-05-20T13:06:05.520Z] Discord.js  v14.26.4 -[2026-05-20T13:06:05.521Z] Logged in as  BLITZ#7183 -[2026-05-20T13:06:05.521Z] Guilds  1 -[2026-05-20T13:06:05.522Z] -Links -[2026-05-20T13:06:05.522Z] ----- -[2026-05-20T13:06:05.523Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-20T13:06:05.524Z] Discord  https://dsc.gg/emerald-dev -[2026-05-20T13:06:05.525Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-20T13:06:05.525Z] -[2026-05-20T13:06:05.526Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-20T13:06:05.527Z] -Config Check -[2026-05-20T13:06:05.528Z] ------------ -[2026-05-20T13:06:05.530Z] ✓ All configurations validated successfully -[2026-05-20T13:06:05.531Z] -[2026-05-20T13:06:05.724Z] -✓ Slash Commands Registered for Char's Test Server diff --git a/Logs/Output/Output-2026-05-26.log b/Logs/Output/Output-2026-05-26.log deleted file mode 100644 index 0bdae2b..0000000 --- a/Logs/Output/Output-2026-05-26.log +++ /dev/null @@ -1,416 +0,0 @@ -[2026-05-26T18:08:20.392Z] [API] Server running on port 3000 -[2026-05-26T18:09:50.058Z] [API] Server running on port 3000 -[2026-05-26T18:09:50.812Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:09:50.812Z] SupportBot -[2026-05-26T18:09:50.813Z] Emerald Services -[2026-05-26T18:09:50.813Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:09:50.814Z] -System -[2026-05-26T18:09:50.814Z] ------ -[2026-05-26T18:09:50.815Z] Bot Name  SupportBo -[2026-05-26T18:09:50.815Z] Version  v26.3.0 -[2026-05-26T18:09:50.815Z] Node.js  v24.14.1 -[2026-05-26T18:09:50.816Z] Discord.js  v14.26.4 -[2026-05-26T18:09:50.816Z] Logged in as  BLITZ#7183 -[2026-05-26T18:09:50.817Z] Guilds  1 -[2026-05-26T18:09:50.817Z] -Links -[2026-05-26T18:09:50.818Z] ----- -[2026-05-26T18:09:50.818Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-26T18:09:50.819Z] Discord  https://dsc.gg/emerald-dev -[2026-05-26T18:09:50.819Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-26T18:09:50.820Z] -[2026-05-26T18:09:50.820Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-26T18:09:50.822Z] -Config Check -[2026-05-26T18:09:50.822Z] ------------ -[2026-05-26T18:09:50.824Z] ✓ All configurations validated successfully -[2026-05-26T18:09:50.825Z] -[2026-05-26T18:09:51.093Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-26T18:18:05.034Z] [API] Server running on port 3000 -[2026-05-26T18:18:05.886Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:18:05.886Z] SupportBot -[2026-05-26T18:18:05.887Z] Emerald Services -[2026-05-26T18:18:05.887Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:18:05.887Z] -System -[2026-05-26T18:18:05.888Z] ------ -[2026-05-26T18:18:05.888Z] Bot Name  SupportBo -[2026-05-26T18:18:05.889Z] Version  v26.3.0 -[2026-05-26T18:18:05.889Z] Node.js  v24.14.1 -[2026-05-26T18:18:05.889Z] Discord.js  v14.26.4 -[2026-05-26T18:18:05.890Z] Logged in as  BLITZ#7183 -[2026-05-26T18:18:05.890Z] Guilds  1 -[2026-05-26T18:18:05.891Z] -Links -[2026-05-26T18:18:05.892Z] ----- -[2026-05-26T18:18:05.893Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-26T18:18:05.894Z] Discord  https://dsc.gg/emerald-dev -[2026-05-26T18:18:05.895Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-26T18:18:05.901Z] -[2026-05-26T18:18:05.903Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-26T18:18:05.907Z] -Config Check -[2026-05-26T18:18:05.908Z] ------------ -[2026-05-26T18:18:05.910Z] ✓ All configurations validated successfully -[2026-05-26T18:18:05.911Z] -[2026-05-26T18:18:06.116Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-26T18:24:36.850Z] [API] Server running on port 3000 -[2026-05-26T18:24:37.648Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:24:37.648Z] SupportBot -[2026-05-26T18:24:37.649Z] Emerald Services -[2026-05-26T18:24:37.649Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:24:37.649Z] -System -[2026-05-26T18:24:37.650Z] ------ -[2026-05-26T18:24:37.650Z] Bot Name  SupportBo -[2026-05-26T18:24:37.650Z] Version  v26.3.0 -[2026-05-26T18:24:37.651Z] Node.js  v24.14.1 -[2026-05-26T18:24:37.651Z] Discord.js  v14.26.4 -[2026-05-26T18:24:37.652Z] Logged in as  BLITZ#7183 -[2026-05-26T18:24:37.652Z] Guilds  1 -[2026-05-26T18:24:37.653Z] -Links -[2026-05-26T18:24:37.653Z] ----- -[2026-05-26T18:24:37.654Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-26T18:24:37.654Z] Discord  https://dsc.gg/emerald-dev -[2026-05-26T18:24:37.656Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-26T18:24:37.659Z] -[2026-05-26T18:24:37.659Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-26T18:24:37.661Z] -Config Check -[2026-05-26T18:24:37.662Z] ------------ -[2026-05-26T18:24:37.664Z] ✓ All configurations validated successfully -[2026-05-26T18:24:37.664Z] -[2026-05-26T18:24:37.857Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-26T18:27:00.991Z] [API] Server running on port 3000 -[2026-05-26T18:27:01.875Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:27:01.876Z] SupportBot -[2026-05-26T18:27:01.876Z] Emerald Services -[2026-05-26T18:27:01.877Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:27:01.877Z] -System -[2026-05-26T18:27:01.878Z] ------ -[2026-05-26T18:27:01.878Z] Bot Name  SupportBo -[2026-05-26T18:27:01.879Z] Version  v26.3.0 -[2026-05-26T18:27:01.879Z] Node.js  v24.14.1 -[2026-05-26T18:27:01.879Z] Discord.js  v14.26.4 -[2026-05-26T18:27:01.880Z] Logged in as  BLITZ#7183 -[2026-05-26T18:27:01.880Z] Guilds  1 -[2026-05-26T18:27:01.881Z] -Links -[2026-05-26T18:27:01.881Z] ----- -[2026-05-26T18:27:01.882Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-26T18:27:01.883Z] Discord  https://dsc.gg/emerald-dev -[2026-05-26T18:27:01.883Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-26T18:27:01.884Z] -[2026-05-26T18:27:01.885Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-26T18:27:01.886Z] -Config Check -[2026-05-26T18:27:01.888Z] ------------ -[2026-05-26T18:27:01.890Z] ✓ All configurations validated successfully -[2026-05-26T18:27:01.891Z] -[2026-05-26T18:27:02.152Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-26T18:29:06.705Z] [API] Server running on port 3000 -[2026-05-26T18:29:07.459Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:29:07.459Z] SupportBot -[2026-05-26T18:29:07.460Z] Emerald Services -[2026-05-26T18:29:07.460Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:29:07.461Z] -System -[2026-05-26T18:29:07.461Z] ------ -[2026-05-26T18:29:07.462Z] Bot Name  SupportBo -[2026-05-26T18:29:07.462Z] Version  v26.3.0 -[2026-05-26T18:29:07.462Z] Node.js  v24.14.1 -[2026-05-26T18:29:07.462Z] Discord.js  v14.26.4 -[2026-05-26T18:29:07.463Z] Logged in as  BLITZ#7183 -[2026-05-26T18:29:07.463Z] Guilds  1 -[2026-05-26T18:29:07.465Z] -Links -[2026-05-26T18:29:07.466Z] ----- -[2026-05-26T18:29:07.466Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-26T18:29:07.467Z] Discord  https://dsc.gg/emerald-dev -[2026-05-26T18:29:07.467Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-26T18:29:07.468Z] -[2026-05-26T18:29:07.469Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-26T18:29:07.470Z] -Config Check -[2026-05-26T18:29:07.471Z] ------------ -[2026-05-26T18:29:07.473Z] ✓ All configurations validated successfully -[2026-05-26T18:29:07.474Z] -[2026-05-26T18:29:07.656Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-26T18:30:31.130Z] [API] Server running on port 3000 -[2026-05-26T18:31:30.298Z] [BotReload] Restarting Discord bot after config change… -[2026-05-26T18:31:31.048Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:31:31.050Z] SupportBot -[2026-05-26T18:31:31.054Z] Emerald Services -[2026-05-26T18:31:31.058Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:31:31.061Z] -System -[2026-05-26T18:31:31.066Z] ------ -[2026-05-26T18:31:31.069Z] Bot Name  SupportBo -[2026-05-26T18:31:31.071Z] Version  v26.3.0 -[2026-05-26T18:31:31.075Z] Node.js  v24.14.1 -[2026-05-26T18:31:31.078Z] Discord.js  v14.26.4 -[2026-05-26T18:31:31.082Z] Logged in as  BLITZ#7183 -[2026-05-26T18:31:31.085Z] Guilds  1 -[2026-05-26T18:31:31.091Z] -Links -[2026-05-26T18:31:31.097Z] ----- -[2026-05-26T18:31:31.101Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-26T18:31:31.106Z] Discord  https://dsc.gg/emerald-dev -[2026-05-26T18:31:31.113Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-26T18:31:31.118Z] -[2026-05-26T18:31:31.123Z] -Config Check -[2026-05-26T18:31:31.127Z] ------------ -[2026-05-26T18:31:31.134Z] ! Missing roles (1198295676128329799, 1198299671576195213, 1491881628065337404) -[2026-05-26T18:31:31.139Z] ! Missing channels (1290752269880660079) -[2026-05-26T18:31:31.142Z] -[2026-05-26T18:31:31.183Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-26T18:31:31.412Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-26T18:31:31.413Z] [BotReload] Bot restarted successfully -[2026-05-26T18:31:31.682Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-26T18:31:46.891Z] [API] Server running on port 3000 -[2026-05-26T18:31:47.594Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:31:47.594Z] SupportBot -[2026-05-26T18:31:47.595Z] Emerald Services -[2026-05-26T18:31:47.595Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:31:47.596Z] -System -[2026-05-26T18:31:47.597Z] ------ -[2026-05-26T18:31:47.597Z] Bot Name  SupportBo -[2026-05-26T18:31:47.598Z] Version  v26.3.0 -[2026-05-26T18:31:47.598Z] Node.js  v24.14.1 -[2026-05-26T18:31:47.599Z] Discord.js  v14.26.4 -[2026-05-26T18:31:47.600Z] Logged in as  BLITZ#7183 -[2026-05-26T18:31:47.600Z] Guilds  1 -[2026-05-26T18:31:47.601Z] -Links -[2026-05-26T18:31:47.602Z] ----- -[2026-05-26T18:31:47.602Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-26T18:31:47.603Z] Discord  https://dsc.gg/emerald-dev -[2026-05-26T18:31:47.604Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-26T18:31:47.604Z] -[2026-05-26T18:31:47.605Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-26T18:31:47.607Z] -Config Check -[2026-05-26T18:31:47.608Z] ------------ -[2026-05-26T18:31:47.610Z] ✓ All configurations validated successfully -[2026-05-26T18:31:47.611Z] -[2026-05-26T18:32:31.812Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-26T18:34:34.091Z] [API] Server running on port 3000 -[2026-05-26T18:35:12.583Z] [BotReload] Restarting Discord bot after config change… -[2026-05-26T18:35:13.391Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:35:13.391Z] SupportBot -[2026-05-26T18:35:13.392Z] Emerald Services -[2026-05-26T18:35:13.392Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:35:13.393Z] -System -[2026-05-26T18:35:13.393Z] ------ -[2026-05-26T18:35:13.394Z] Bot Name  SupportBo -[2026-05-26T18:35:13.394Z] Version  v26.3.0 -[2026-05-26T18:35:13.395Z] Node.js  v24.14.1 -[2026-05-26T18:35:13.395Z] Discord.js  v14.26.4 -[2026-05-26T18:35:13.395Z] Logged in as  BLITZ#7183 -[2026-05-26T18:35:13.396Z] Guilds  1 -[2026-05-26T18:35:13.396Z] -Links -[2026-05-26T18:35:13.397Z] ----- -[2026-05-26T18:35:13.398Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-26T18:35:13.399Z] Discord  https://dsc.gg/emerald-dev -[2026-05-26T18:35:13.400Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-26T18:35:13.400Z] -[2026-05-26T18:35:13.401Z] -Config Check -[2026-05-26T18:35:13.402Z] ------------ -[2026-05-26T18:35:13.403Z] ! Missing roles (1198295676128329799, 1198299671576195213, 1491881628065337404) -[2026-05-26T18:35:13.404Z] ! Missing channels (1290752269880660079) -[2026-05-26T18:35:13.404Z] -[2026-05-26T18:35:13.429Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-26T18:35:13.621Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-26T18:35:13.621Z] [BotReload] Bot restarted successfully -[2026-05-26T18:35:13.853Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-26T18:35:27.209Z] [API] Server running on port 3000 -[2026-05-26T18:35:27.933Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:35:27.933Z] SupportBot -[2026-05-26T18:35:27.933Z] Emerald Services -[2026-05-26T18:35:27.934Z] ──────────────────────────────────────────────────────── -[2026-05-26T18:35:27.934Z] -System -[2026-05-26T18:35:27.935Z] ------ -[2026-05-26T18:35:27.935Z] Bot Name  SupportBo -[2026-05-26T18:35:27.936Z] Version  v26.3.0 -[2026-05-26T18:35:27.936Z] Node.js  v24.14.1 -[2026-05-26T18:35:27.937Z] Discord.js  v14.26.4 -[2026-05-26T18:35:27.937Z] Logged in as  BLITZ#7183 -[2026-05-26T18:35:27.938Z] Guilds  1 -[2026-05-26T18:35:27.938Z] -Links -[2026-05-26T18:35:27.938Z] ----- -[2026-05-26T18:35:27.939Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-26T18:35:27.940Z] Discord  https://dsc.gg/emerald-dev -[2026-05-26T18:35:27.940Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-26T18:35:27.941Z] -[2026-05-26T18:35:27.941Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-26T18:35:27.943Z] -Config Check -[2026-05-26T18:35:27.943Z] ------------ -[2026-05-26T18:35:27.945Z] ✓ All configurations validated successfully -[2026-05-26T18:35:27.946Z] -[2026-05-26T18:36:14.082Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-26T19:00:20.564Z] [API] Server running on port 3000 -[2026-05-26T19:01:08.704Z] [BotReload] Restarting Discord bot after config change… -[2026-05-26T19:01:09.362Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-26T19:01:09.365Z] ──────────────────────────────────────────────────────── -[2026-05-26T19:01:09.366Z] SupportBot -[2026-05-26T19:01:09.366Z] Emerald Services -[2026-05-26T19:01:09.366Z] ──────────────────────────────────────────────────────── -[2026-05-26T19:01:09.367Z] -System -[2026-05-26T19:01:09.367Z] ------ -[2026-05-26T19:01:09.367Z] Bot Name  SupportBo -[2026-05-26T19:01:09.368Z] Version  v26.3.0 -[2026-05-26T19:01:09.368Z] Node.js  v24.14.1 -[2026-05-26T19:01:09.368Z] Discord.js  v14.26.4 -[2026-05-26T19:01:09.369Z] Logged in as  BLITZ#7183 -[2026-05-26T19:01:09.369Z] Guilds  1 -[2026-05-26T19:01:09.370Z] -Links -[2026-05-26T19:01:09.371Z] ----- -[2026-05-26T19:01:09.371Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-26T19:01:09.372Z] Discord  https://dsc.gg/emerald-dev -[2026-05-26T19:01:09.373Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-26T19:01:09.373Z] -[2026-05-26T19:01:09.375Z] -Guild -[2026-05-26T19:01:09.376Z] ----- -[2026-05-26T19:01:09.377Z] ! Guild setup (No guild ID is set. Add your Discord server ID under Bot config → General.) -[2026-05-26T19:01:09.377Z] -[2026-05-26T19:01:09.378Z] -Config Check -[2026-05-26T19:01:09.378Z] ------------ -[2026-05-26T19:01:09.379Z] ! Missing roles (ROLE_ID, ROLE_ID, ROLE_ID) -[2026-05-26T19:01:09.380Z] ! Missing channels (CHANNEL_ID) -[2026-05-26T19:01:09.381Z] -[2026-05-26T19:01:09.547Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-26T19:01:09.923Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-26T19:01:09.923Z] [BotReload] Bot restarted successfully -[2026-05-26T19:01:44.692Z] [API] Server running on port 3000 -[2026-05-26T19:01:45.455Z] ──────────────────────────────────────────────────────── -[2026-05-26T19:01:45.455Z] SupportBot -[2026-05-26T19:01:45.456Z] Emerald Services -[2026-05-26T19:01:45.456Z] ──────────────────────────────────────────────────────── -[2026-05-26T19:01:45.457Z] -System -[2026-05-26T19:01:45.457Z] ------ -[2026-05-26T19:01:45.458Z] Bot Name  SupportBo -[2026-05-26T19:01:45.458Z] Version  v26.3.0 -[2026-05-26T19:01:45.458Z] Node.js  v24.14.1 -[2026-05-26T19:01:45.459Z] Discord.js  v14.26.4 -[2026-05-26T19:01:45.459Z] Logged in as  BLITZ#7183 -[2026-05-26T19:01:45.460Z] Guilds  1 -[2026-05-26T19:01:45.462Z] -Links -[2026-05-26T19:01:45.462Z] ----- -[2026-05-26T19:01:45.463Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-26T19:01:45.464Z] Discord  https://dsc.gg/emerald-dev -[2026-05-26T19:01:45.466Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-26T19:01:45.467Z] -[2026-05-26T19:01:45.468Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-26T19:01:45.469Z] -Guild -[2026-05-26T19:01:45.470Z] ----- -[2026-05-26T19:01:45.471Z] ! Guild setup (No guild ID is set. Add your Discord server ID under Bot config → General.) -[2026-05-26T19:01:45.471Z] -[2026-05-26T19:01:45.471Z] -Config Check -[2026-05-26T19:01:45.472Z] ------------ -[2026-05-26T19:01:45.473Z] ! Missing roles (ROLE_ID, ROLE_ID, ROLE_ID) -[2026-05-26T19:01:45.474Z] ! Missing channels (CHANNEL_ID) -[2026-05-26T19:01:45.474Z] -[2026-05-26T19:01:45.807Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-26T19:05:40.700Z] [API] Server running on port 3000 -[2026-05-26T19:05:41.522Z] ──────────────────────────────────────────────────────── -[2026-05-26T19:05:41.522Z] SupportBot -[2026-05-26T19:05:41.523Z] Emerald Services -[2026-05-26T19:05:41.523Z] ──────────────────────────────────────────────────────── -[2026-05-26T19:05:41.524Z] -System -[2026-05-26T19:05:41.524Z] ------ -[2026-05-26T19:05:41.524Z] Bot Name  SupportBo -[2026-05-26T19:05:41.525Z] Version  v26.3.0 -[2026-05-26T19:05:41.525Z] Node.js  v24.14.1 -[2026-05-26T19:05:41.525Z] Discord.js  v14.26.4 -[2026-05-26T19:05:41.526Z] Logged in as  BLITZ#7183 -[2026-05-26T19:05:41.526Z] Guilds  1 -[2026-05-26T19:05:41.527Z] -Links -[2026-05-26T19:05:41.527Z] ----- -[2026-05-26T19:05:41.528Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-26T19:05:41.528Z] Discord  https://dsc.gg/emerald-dev -[2026-05-26T19:05:41.529Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-26T19:05:41.530Z] -[2026-05-26T19:05:41.534Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-26T19:05:41.537Z] -Guild -[2026-05-26T19:05:41.537Z] ----- -[2026-05-26T19:05:41.538Z] ! Guild setup (No guild ID is set. Add your Discord server ID under Bot config → General.) -[2026-05-26T19:05:41.539Z] -[2026-05-26T19:05:41.539Z] -Config Check -[2026-05-26T19:05:41.540Z] ------------ -[2026-05-26T19:05:41.542Z] ! Missing roles (ROLE_ID, ROLE_ID, ROLE_ID) -[2026-05-26T19:05:41.543Z] ! Missing channels (CHANNEL_ID) -[2026-05-26T19:05:41.543Z] -[2026-05-26T19:05:41.761Z] -✓ Slash Commands Registered for Char's Test Server -[2026-05-26T19:07:41.900Z] [API] Server running on port 3000 -[2026-05-26T19:07:42.738Z] ──────────────────────────────────────────────────────── -[2026-05-26T19:07:42.739Z] SupportBot -[2026-05-26T19:07:42.739Z] Emerald Services -[2026-05-26T19:07:42.739Z] ──────────────────────────────────────────────────────── -[2026-05-26T19:07:42.740Z] -System -[2026-05-26T19:07:42.740Z] ------ -[2026-05-26T19:07:42.741Z] Bot Name  SupportBo -[2026-05-26T19:07:42.741Z] Version  v26.3.0 -[2026-05-26T19:07:42.741Z] Node.js  v24.14.1 -[2026-05-26T19:07:42.742Z] Discord.js  v14.26.4 -[2026-05-26T19:07:42.742Z] Logged in as  BLITZ#7183 -[2026-05-26T19:07:42.743Z] Guilds  1 -[2026-05-26T19:07:42.743Z] -Links -[2026-05-26T19:07:42.744Z] ----- -[2026-05-26T19:07:42.745Z] Docs  https://emerald-services.gitbook.io/ -[2026-05-26T19:07:42.745Z] Discord  https://dsc.gg/emerald-dev -[2026-05-26T19:07:42.746Z] Invite  https://discord.com/api/oauth2/authorize?client_id=1289713673123922032&permissions=8&scope=bot%20applications.commands -[2026-05-26T19:07:42.746Z] -[2026-05-26T19:07:42.747Z] [Dashboard] Discord bot is ready — full stats and guild tools are available -[2026-05-26T19:07:42.748Z] -Guild -[2026-05-26T19:07:42.751Z] ----- -[2026-05-26T19:07:42.752Z] ! Guild setup (No guild ID is set. Add your Discord server ID under Bot config → General.) -[2026-05-26T19:07:42.752Z] -[2026-05-26T19:07:42.753Z] -Config Check -[2026-05-26T19:07:42.753Z] ------------ -[2026-05-26T19:07:42.756Z] ! Missing roles (ROLE_ID, ROLE_ID, ROLE_ID) -[2026-05-26T19:07:42.756Z] ! Missing channels (CHANNEL_ID) -[2026-05-26T19:07:42.757Z] -[2026-05-26T19:07:42.952Z] -✓ Slash Commands Registered for Char's Test Server diff --git a/README.md b/README.md deleted file mode 100644 index 0fe213c..0000000 --- a/README.md +++ /dev/null @@ -1,222 +0,0 @@ -

-
- -
- SupportBot -
-

- -
- - - shield.png - - - - shield.png - - - - shield.png - - - - shield.png - - -## Supported Versions - -| SupportBot | Node.JS | Discord.JS | Supported | -|------------|---------|------------|------------| -| 26.x | 22.x | 14.x. | ✅ Yes -| 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 - -
- -

- - 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 - -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. - -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. - -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 your callback URL(s). If users may sign in over **both** HTTP and HTTPS, register **both** (same host/path, different protocol), e.g. `http://bot.example.com/api/auth/discord/callback` and `https://bot.example.com/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 - -Set `RedirectUri` to your usual URL (host and path). By default, `UseRequestOrigin` keeps the **same protocol** as the page you opened (http vs https). Behind a reverse proxy, set `TrustProxy: 1` so forwarded `X-Forwarded-Proto` is respected. - -### 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 License](https://opensource.org/licenses/MIT), same as upstream SupportBot. diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index c8dbd00..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,20 +0,0 @@ -# Security Policy - -## Supported Versions - -Use this section to tell people about which versions of your project are -currently being supported with security updates. - -| Version | Supported | -| ------- | ------------------ | -| 7.7.2 | :white_check_mark: | -| 7.7.1 | :white_check_mark: | -| < 4.0 | :x: | - -## Reporting a Vulnerability - -Use this section to tell people how to report a vulnerability. - -Tell them where to go, how often they can expect to get an update on a -reported vulnerability, what to expect if the vulnerability is accepted or -declined, etc. diff --git a/Structures/AIDatabase.js b/Structures/AIDatabase.js deleted file mode 100644 index 0a672a1..0000000 --- a/Structures/AIDatabase.js +++ /dev/null @@ -1,438 +0,0 @@ -const Database = require("better-sqlite3"); -const path = require("path"); - -const db = new Database(path.join(__dirname, "../Data/ai-memory.db")); - -db.pragma("journal_mode = WAL"); - -db.prepare( - ` - CREATE TABLE IF NOT EXISTS ai_conversations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - guild_id TEXT, - channel_id TEXT, - user_id TEXT, - role TEXT NOT NULL, - content TEXT NOT NULL, - created_at INTEGER NOT NULL - ) -`, -).run(); - -db.prepare( - ` - CREATE TABLE IF NOT EXISTS ai_summaries ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - guild_id TEXT, - channel_id TEXT, - summary TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) -`, -).run(); - -db.prepare( - ` - CREATE TABLE IF NOT EXISTS ai_facts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - guild_id TEXT, - channel_id TEXT, - user_id TEXT, - fact_type TEXT NOT NULL, - fact_key TEXT NOT NULL, - fact_value TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) -`, -).run(); - -db.prepare( - ` - CREATE TABLE IF NOT EXISTS ai_knowledge ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - source_type TEXT NOT NULL, - source_name TEXT NOT NULL, - title TEXT NOT NULL, - content TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) -`, -).run(); - -db.prepare( - ` - CREATE TABLE IF NOT EXISTS ai_sessions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - guild_id TEXT, - channel_id TEXT, - user_id TEXT, - model TEXT, - mode TEXT, - last_message_at INTEGER, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) -`, -).run(); - -try { - db.prepare(`ALTER TABLE ai_facts ADD COLUMN user_id TEXT`).run(); -} catch (e) { - if (!String(e).includes("duplicate column name")) throw e; -} - -module.exports = { - saveUserFact({ guildId, userId, factType, factKey, factValue }) { - const existing = db - .prepare( - ` - SELECT id FROM ai_facts - WHERE guild_id IS ? AND user_id IS ? AND fact_type = ? AND fact_key = ? - LIMIT 1 - `, - ) - .get(guildId || null, userId || null, factType, factKey); - - if (existing) { - db.prepare( - ` - UPDATE ai_facts - SET fact_value = ?, updated_at = ? - WHERE id = ? - `, - ).run(factValue, Date.now(), existing.id); - return; - } - - db.prepare( - ` - INSERT INTO ai_facts ( - guild_id, channel_id, user_id, fact_type, fact_key, fact_value, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) - `, - ).run( - guildId || null, - null, - userId || null, - factType, - factKey, - factValue, - Date.now(), - Date.now(), - ); - }, - - getUserFacts(guildId, userId) { - return db - .prepare( - ` - SELECT * FROM ai_facts - WHERE guild_id IS ? AND user_id IS ? - ORDER BY updated_at DESC - `, - ) - .all(guildId || null, userId || null); - }, - - getUserFactsByScope({ guildId = null, userId = null, scope = "guild" }) { - if (!userId) return []; - - if (scope === "global") { - return db - .prepare( - ` - SELECT * FROM ai_facts - WHERE user_id IS ? - ORDER BY updated_at DESC - `, - ) - .all(userId || null); - } - - return db - .prepare( - ` - SELECT * FROM ai_facts - WHERE guild_id IS ? AND user_id IS ? - ORDER BY updated_at DESC - `, - ) - .all(guildId || null, userId || null); - }, - - addMessage({ guildId, channelId, userId, role, content }) { - db.prepare( - ` - INSERT INTO ai_conversations ( - guild_id, channel_id, user_id, role, content, created_at - ) VALUES (?, ?, ?, ?, ?, ?) - `, - ).run( - guildId || null, - channelId || null, - userId || null, - role, - content, - Date.now(), - ); - }, - - getRecentMessages(channelId, limit = 15) { - return db - .prepare( - ` - SELECT * FROM ai_conversations - WHERE channel_id = ? - ORDER BY created_at DESC - LIMIT ? - `, - ) - .all(channelId, limit) - .reverse(); - }, - - getRecentMessagesByScope({ guildId = null, channelId = null, limit = 15, scope = "guild" }) { - if (scope === "channel") { - return db - .prepare( - ` - SELECT * FROM ai_conversations - WHERE channel_id = ? - ORDER BY created_at DESC - LIMIT ? - `, - ) - .all(channelId || null, limit) - .reverse(); - } - - if (scope === "global") { - return db - .prepare( - ` - SELECT * FROM ai_conversations - ORDER BY created_at DESC - LIMIT ? - `, - ) - .all(limit) - .reverse(); - } - - return db - .prepare( - ` - SELECT * FROM ai_conversations - WHERE guild_id IS ? - ORDER BY created_at DESC - LIMIT ? - `, - ) - .all(guildId || null, limit) - .reverse(); - }, - - saveSummary({ guildId, channelId, summary }) { - const existing = db - .prepare( - ` - SELECT id FROM ai_summaries - WHERE guild_id IS ? AND channel_id IS ? - ORDER BY updated_at DESC - LIMIT 1 - `, - ) - .get(guildId || null, channelId || null); - - if (existing) { - db.prepare( - ` - UPDATE ai_summaries - SET summary = ?, updated_at = ? - WHERE id = ? - `, - ).run(summary, Date.now(), existing.id); - return; - } - - db.prepare( - ` - INSERT INTO ai_summaries ( - guild_id, channel_id, summary, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?) - `, - ).run(guildId || null, channelId || null, summary, Date.now(), Date.now()); - }, - - getLatestSummary(channelId) { - return db - .prepare( - ` - SELECT * FROM ai_summaries - WHERE channel_id = ? - ORDER BY updated_at DESC - LIMIT 1 - `, - ) - .get(channelId); - }, - - getLatestSummaryByScope({ guildId = null, channelId = null, scope = "guild" }) { - if (scope === "channel") { - return db - .prepare( - ` - SELECT * FROM ai_summaries - WHERE channel_id = ? - ORDER BY updated_at DESC - LIMIT 1 - `, - ) - .get(channelId || null); - } - - if (scope === "global") { - return db - .prepare( - ` - SELECT * FROM ai_summaries - ORDER BY updated_at DESC - LIMIT 1 - `, - ) - .get(); - } - - return db - .prepare( - ` - SELECT * FROM ai_summaries - WHERE guild_id IS ? - ORDER BY updated_at DESC - LIMIT 1 - `, - ) - .get(guildId || null); - }, - - saveFact({ guildId, channelId, factType, factKey, factValue }) { - const existing = db - .prepare( - ` - SELECT id FROM ai_facts - WHERE guild_id IS ? AND channel_id IS ? AND fact_type = ? AND fact_key = ? - LIMIT 1 - `, - ) - .get(guildId || null, channelId || null, factType, factKey); - - if (existing) { - db.prepare( - ` - UPDATE ai_facts - SET fact_value = ?, updated_at = ? - WHERE id = ? - `, - ).run(factValue, Date.now(), existing.id); - return; - } - - db.prepare( - ` - INSERT INTO ai_facts ( - guild_id, channel_id, fact_type, fact_key, fact_value, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?) - `, - ).run( - guildId || null, - channelId || null, - factType, - factKey, - factValue, - Date.now(), - Date.now(), - ); - }, - - getFacts(channelId) { - return db - .prepare( - ` - SELECT * FROM ai_facts - WHERE channel_id = ? - ORDER BY updated_at DESC - `, - ) - .all(channelId); - }, - - getFactsByScope({ guildId = null, channelId = null, scope = "guild" }) { - if (scope === "channel") { - return db - .prepare( - ` - SELECT * FROM ai_facts - WHERE channel_id = ? AND user_id IS NULL - ORDER BY updated_at DESC - `, - ) - .all(channelId || null); - } - - if (scope === "global") { - return db - .prepare( - ` - SELECT * FROM ai_facts - WHERE user_id IS NULL - ORDER BY updated_at DESC - `, - ) - .all(); - } - - return db - .prepare( - ` - SELECT * FROM ai_facts - WHERE guild_id IS ? AND user_id IS NULL - ORDER BY updated_at DESC - `, - ) - .all(guildId || null); - }, - - addKnowledge({ sourceType, sourceName, title, content }) { - db.prepare( - ` - INSERT INTO ai_knowledge ( - source_type, source_name, title, content, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?) - `, - ).run(sourceType, sourceName, title, content, Date.now(), Date.now()); - }, - - getKnowledge(limit = 25) { - return db - .prepare( - ` - SELECT * FROM ai_knowledge - ORDER BY updated_at DESC - LIMIT ? - `, - ) - .all(limit); - }, - - clearKnowledge() { - db.prepare(`DELETE FROM ai_knowledge`).run(); - }, - - clearConversations(channelId) { - if (!channelId) return; - db.prepare(`DELETE FROM ai_conversations WHERE channel_id = ?`).run(channelId); - }, -}; \ No newline at end of file diff --git a/Structures/Addon.js b/Structures/Addon.js deleted file mode 100644 index 5705d49..0000000 --- a/Structures/Addon.js +++ /dev/null @@ -1,27 +0,0 @@ - -// SupportBot | Emerald Services -// Addon Structure -class Command { - constructor(options) { - this.name = options.name; - this.description = options.description; - this.permissions = options.permissions; - this.options = options.options || []; - this.run = options.run; - this.defaultPermission = true; - } - - // Method to dynamically update options - setOptions(options) { - this.options = options; - } -} - -class Event { - constructor(event, run) { - this.event = event; - this.run = run; - } -} - -module.exports = { Command, Event }; diff --git a/Structures/AddonCatalog.js b/Structures/AddonCatalog.js deleted file mode 100644 index b43ddac..0000000 --- a/Structures/AddonCatalog.js +++ /dev/null @@ -1,270 +0,0 @@ -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 deleted file mode 100644 index 95b47a1..0000000 --- a/Structures/BotInvite.js +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 475de29..0000000 --- a/Structures/BotReload.js +++ /dev/null @@ -1,150 +0,0 @@ -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 deleted file mode 100644 index 97b5522..0000000 --- a/Structures/BotStartup.js +++ /dev/null @@ -1,174 +0,0 @@ -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 deleted file mode 100644 index 9db0cbe..0000000 --- a/Structures/Client.js +++ /dev/null @@ -1,193 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const Discord = require("discord.js"); -const { GatewayIntentBits, Partials } = require("discord.js"); -const { Command, Event } = require("./Addon.js"); -const configStore = require("./ConfigStore.js"); -const { getPrimaryGuild } = require("./GuildManager.js"); - -const supportbot = configStore.supportbot; -const cmdconfig = configStore.commands; - -const c = { - reset: "\x1b[0m", - gray: "\x1b[90m", - green: "\x1b[32m", - yellow: "\x1b[33m", - cyan: "\x1b[36m", - red: "\x1b[31m", - white: "\x1b[37m", - 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 walk(dir, ext = ".js", fileList = []) { - const files = fs.readdirSync(dir); - for (const file of files) { - const full = `${dir}/${file}`; - const stat = fs.statSync(full); - if (stat.isDirectory()) { - walk(full, ext, fileList); - } else if (file.endsWith(ext)) { - fileList.push(full); - } - } - return fileList; -} - -class Client extends Discord.Client { - constructor() { - super({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMembers, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.GuildMessageReactions, - GatewayIntentBits.GuildEmojisAndStickers, - GatewayIntentBits.MessageContent, - ], - partials: [Partials.Message, Partials.Channel, Partials.Reaction], - }); - - this.commands = new Discord.Collection(); - } - - async getChannel(channel, guild) { - if (!channel) return null; - const channelStr = String(channel).toLowerCase(); - return guild.channels.cache.find( - (c) => - (c.type === Discord.ChannelType.GuildText || c.type === Discord.ChannelType.GuildNews) && - (c.id === channel || (c.name && c.name.toLowerCase() === channelStr)), - ); - } - - async getRole(role, guild) { - if (!role) return null; - const roleStr = String(role).toLowerCase(); - return guild.roles.cache.find( - (r) => r.id === role || (r.name && r.name.toLowerCase() === roleStr), - ); - } - - async getCategory(category, guild) { - if (!category) return null; - const catStr = String(category).toLowerCase(); - return guild.channels.cache.find( - (c) => - c.type === Discord.ChannelType.GuildCategory && - (c.id === category || (c.name && c.name.toLowerCase() === catStr)), - ); - } - - async start(token) { - const commandFiles = walk("./Commands"); - - const allCommands = commandFiles.map((file) => ({ - file, - command: require(`../${file}`), - })); - - const enabledCommands = allCommands.filter(({ command }) => { - const matchingConfig = Object.values(cmdconfig).find( - (entry) => - entry && - typeof entry === "object" && - entry.Command && - String(entry.Command).toLowerCase() === String(command.name).toLowerCase(), - ); - - if (!matchingConfig) { - return true; - } - - return matchingConfig.Enabled !== false; - }); - - section("Commands"); - - enabledCommands.forEach(({ command, file }) => { - console.log( - `${c.green}✓${c.reset} ${c.white}${command.name}${c.reset} ${c.gray}(${path.basename(file)})${c.reset}`, - ); - this.commands.set(command.name, command); - }); - - if (supportbot.General.Addons.Enabled) { - const addonFiles = fs.readdirSync("./Addons").filter((file) => file.endsWith(".js")); - - section("Addons"); - - addonFiles.forEach((file) => { - let addon = require(`../Addons/${file}`); - - // Handle single or multiple addons per file - const addonArray = Array.isArray(addon) ? addon : [addon]; - - addonArray.forEach((item, index) => { - // If name is missing, use filename (handling index for arrays) - if (!item.name) { - item.name = index === 0 ? file.split(".")[0] : `${file.split(".")[0]}_${index}`; - } - - console.log(`${c.green}✓${c.reset} ${c.white}${item.name}${c.reset}`); - - if (item instanceof Command) { - this.commands.set(item.name, item); - } - - if (item.events && Array.isArray(item.events)) { - item.events.forEach((event) => { - if (event instanceof Event) { - this.on(event.event, (...args) => event.run(this, ...args)); - } - }); - } - - // If it's a direct Event instance - if (item instanceof Event) { - this.on(item.event, (...args) => item.run(this, ...args)); - } - }); - }); - } - - 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"); - - fs.readdirSync("./Events") - .filter((file) => file.endsWith(".js")) - .forEach((file) => { - const event = require(`../Events/${file}`); - console.log( - `${c.green}✓${c.reset} ${c.white}${event.event}${c.reset} ${c.gray}(${file})${c.reset}`, - ); - this.on(event.event, (...args) => event.run(this, ...args)); - }); - - console.log(""); - - if (process.argv[2] !== "test") { - await this.login(token); - } else { - console.log(`${c.red}RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start${c.reset}`); - } - } -} - -module.exports = Client; diff --git a/Structures/Command.js b/Structures/Command.js deleted file mode 100644 index 77f17c6..0000000 --- a/Structures/Command.js +++ /dev/null @@ -1,14 +0,0 @@ -// SupportBot | Emerald Services -// Command Structure - -class Command { - constructor(options) { - this.name = options.name; - this.description = options.description; - this.permissions = options.permissions || []; - this.options = options.options || []; - this.run = options.run; - this.defaultPermission = true; - } -} -module.exports = Command; diff --git a/Structures/ConfigStore.js b/Structures/ConfigStore.js deleted file mode 100644 index 40a0416..0000000 --- a/Structures/ConfigStore.js +++ /dev/null @@ -1,89 +0,0 @@ -// 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/DashboardAlerts.js b/Structures/DashboardAlerts.js deleted file mode 100644 index c37dc42..0000000 --- a/Structures/DashboardAlerts.js +++ /dev/null @@ -1,229 +0,0 @@ -/** - * Dashboard alert registry — server-side providers for actionable banners. - * - * Register a provider to surface new alerts anywhere in the dashboard: - * - * const { dashboardAlerts } = require("./DashboardAlerts.js"); - * dashboardAlerts.register("my-addon", (ctx) => { - * if (!ctx.configStore.myAddon?.Configured) { - * return [{ - * id: "my-addon:setup", - * severity: "warning", - * category: "my-addon", - * title: "My addon needs setup", - * message: "Finish configuration to enable this feature.", - * action: { label: "Configure", href: "/configs?file=my-addon" }, - * }]; - * } - * return []; - * }); - */ -const { evaluateGuildHealth } = require("./GuildManager.js"); -const { evaluateAllModules } = require("./ModuleStatus.js"); - -const SEVERITY_ORDER = { error: 0, warning: 1, info: 2 }; - -const MODULE_LINKS = { - tickets: { - title: "Ticket system", - href: "/configs?file=ticket-panel", - }, - supportbot: { - title: "Core bot settings", - href: "/configs?file=supportbot", - }, - transcripts: { - title: "Ticket transcripts", - href: "/transcripts", - }, - suggestions: { - title: "Suggestions", - href: "/configs?file=messages", - }, - ai: { - title: "AI assistant", - href: "/configs?file=supportbot-ai", - }, - commands: { - title: "Commands", - href: "/configs?file=commands", - }, -}; - -class DashboardAlertRegistry { - constructor() { - /** @type {Map DashboardAlert[] | Promise>} */ - this.providers = new Map(); - } - - /** - * @param {string} id Unique provider id (used for debugging / overrides) - * @param {(ctx: DashboardAlertContext) => DashboardAlert[] | Promise} fn - */ - register(id, fn) { - this.providers.set(id, fn); - return this; - } - - unregister(id) { - this.providers.delete(id); - } - - /** @param {DashboardAlertContext} ctx */ - async evaluate(ctx) { - const alerts = []; - for (const fn of this.providers.values()) { - const result = await fn(ctx); - if (Array.isArray(result) && result.length) { - alerts.push(...result); - } - } - return sortAlerts(alerts); - } -} - -function sortAlerts(alerts) { - return [...alerts].sort((a, b) => { - const sa = SEVERITY_ORDER[a.severity] ?? 9; - const sb = SEVERITY_ORDER[b.severity] ?? 9; - if (sa !== sb) return sa - sb; - return a.title.localeCompare(b.title); - }); -} - -function guildSeverity(status) { - if (status === "offline") return "error"; - return "warning"; -} - -function shouldSkipModuleAlert(moduleId, info) { - if (info.status === "done") return true; - if (moduleId === "ai" && info.subtitle === "AI is disabled in config") return true; - // Transcripts are automatic — empty archive is normal, not a setup step. - if (moduleId === "transcripts") return true; - // Partially configured modules are tracked on Overview — only banner when nothing is done yet. - if (info.status === "progress" && (info.passed ?? 0) > 0) return true; - return false; -} - -function moduleSeverity(moduleId, info) { - if (info.status === "todo") { - return moduleId === "tickets" || moduleId === "supportbot" ? "warning" : "info"; - } - return "info"; -} - -function evaluateGuildAlerts(ctx) { - const { client } = ctx; - if (!client?.user) { - return [ - { - id: "guild:offline", - severity: "error", - category: "guild", - title: "Bot offline", - message: "The bot is not connected to Discord. Check your token and restart the server.", - action: { label: "Bot config", href: "/configs?file=supportbot" }, - meta: { status: "offline" }, - }, - ]; - } - - const health = evaluateGuildHealth(client); - if (health.status === "ok") return []; - - return [ - { - id: "guild:health", - severity: guildSeverity(health.status), - category: "guild", - title: "Server setup required", - message: health.message, - action: { label: "Bot config", href: "/configs?file=supportbot" }, - meta: { - status: health.status, - configuredGuildId: health.configuredGuildId, - configuredGuild: health.configuredGuild, - extraGuilds: health.extraGuilds, - }, - }, - ]; -} - -function evaluateModuleAlerts(ctx) { - const { configStore, transcriptCount = 0 } = ctx; - const modules = evaluateAllModules(configStore, { transcriptCount }); - const alerts = []; - - for (const [moduleId, info] of Object.entries(modules)) { - if (shouldSkipModuleAlert(moduleId, info)) continue; - - const link = MODULE_LINKS[moduleId] || { - title: moduleId, - href: "/configs", - }; - - const progress = - info.total > 0 ? `${info.passed ?? 0} of ${info.total} checks complete` : null; - - alerts.push({ - id: `module:${moduleId}`, - severity: moduleSeverity(moduleId, info), - category: "modules", - title: - info.status === "todo" - ? `${link.title} — setup required` - : `${link.title} — in progress`, - message: info.subtitle, - action: { label: "Open module", href: link.href }, - meta: { - moduleId, - status: info.status, - passed: info.passed, - total: info.total, - progress, - }, - }); - } - - return alerts; -} - -const dashboardAlerts = new DashboardAlertRegistry(); -dashboardAlerts.register("guild", evaluateGuildAlerts); -dashboardAlerts.register("modules", evaluateModuleAlerts); - -/** - * @typedef {Object} DashboardAlertAction - * @property {string} label - * @property {string} href - */ - -/** - * @typedef {Object} DashboardAlert - * @property {string} id - * @property {"error"|"warning"|"info"} severity - * @property {string} category - * @property {string} title - * @property {string} message - * @property {DashboardAlertAction} [action] - * @property {Record} [meta] - */ - -/** - * @typedef {Object} DashboardAlertContext - * @property {import("discord.js").Client} [client] - * @property {import("./ConfigStore.js")} configStore - * @property {number} [transcriptCount] - */ - -async function evaluateDashboardAlerts(ctx) { - return dashboardAlerts.evaluate(ctx); -} - -module.exports = { - dashboardAlerts, - evaluateDashboardAlerts, - sortAlerts, - MODULE_LINKS, -}; diff --git a/Structures/DashboardAuth.js b/Structures/DashboardAuth.js deleted file mode 100644 index 089be04..0000000 --- a/Structures/DashboardAuth.js +++ /dev/null @@ -1,57 +0,0 @@ -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/DashboardBranding.js b/Structures/DashboardBranding.js deleted file mode 100644 index 5db07e7..0000000 --- a/Structures/DashboardBranding.js +++ /dev/null @@ -1,168 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const BRANDING_DIR = path.join(__dirname, '../Data/branding'); -const BRANDING_FILE = path.join(BRANDING_DIR, 'branding.json'); -const FAVICON_BASENAME = 'favicon'; - -const DEFAULTS = { - title: 'SupportBot', - pageTitle: 'SupportBot Dashboard', - faviconFile: null, - faviconMime: null, - updatedAt: null, -}; - -const ALLOWED_MIME = new Set([ - 'image/png', - 'image/jpeg', - 'image/x-icon', - 'image/vnd.microsoft.icon', - 'image/svg+xml', - 'image/webp', -]); - -const MIME_EXT = { - 'image/png': '.png', - 'image/jpeg': '.jpg', - 'image/x-icon': '.ico', - 'image/vnd.microsoft.icon': '.ico', - 'image/svg+xml': '.svg', - 'image/webp': '.webp', -}; - -function ensureDir() { - if (!fs.existsSync(BRANDING_DIR)) { - fs.mkdirSync(BRANDING_DIR, { recursive: true }); - } -} - -function readBranding() { - ensureDir(); - if (!fs.existsSync(BRANDING_FILE)) { - return { ...DEFAULTS }; - } - try { - const raw = JSON.parse(fs.readFileSync(BRANDING_FILE, 'utf8')); - return { - title: String(raw.title || DEFAULTS.title).slice(0, 64), - pageTitle: String(raw.pageTitle || DEFAULTS.pageTitle).slice(0, 80), - faviconFile: raw.faviconFile || null, - faviconMime: raw.faviconMime || null, - updatedAt: raw.updatedAt || null, - }; - } catch { - return { ...DEFAULTS }; - } -} - -function writeBranding(data) { - ensureDir(); - const next = { - title: String(data.title || DEFAULTS.title).slice(0, 64), - pageTitle: String(data.pageTitle || DEFAULTS.pageTitle).slice(0, 80), - faviconFile: data.faviconFile ?? null, - faviconMime: data.faviconMime ?? null, - updatedAt: Date.now(), - }; - fs.writeFileSync(BRANDING_FILE, JSON.stringify(next, null, 2), 'utf8'); - return next; -} - -function publicPayload(branding) { - const hasFavicon = Boolean(branding.faviconFile); - return { - title: branding.title, - pageTitle: branding.pageTitle, - hasFavicon, - faviconUrl: hasFavicon ? '/api/branding/favicon' : null, - updatedAt: branding.updatedAt, - }; -} - -function updateBranding({ title, pageTitle }) { - const current = readBranding(); - return writeBranding({ - ...current, - title: title !== undefined ? title : current.title, - pageTitle: pageTitle !== undefined ? pageTitle : current.pageTitle, - }); -} - -function removeExistingFavicons() { - if (!fs.existsSync(BRANDING_DIR)) return; - for (const file of fs.readdirSync(BRANDING_DIR)) { - if (file.startsWith(`${FAVICON_BASENAME}.`)) { - fs.unlinkSync(path.join(BRANDING_DIR, file)); - } - } -} - -function saveFaviconFromBase64(dataUrl) { - const match = String(dataUrl || '').match( - /^data:([\w/+.-]+);base64,([A-Za-z0-9+/=]+)$/, - ); - if (!match) { - throw new Error('Invalid image data. Upload a PNG, ICO, SVG, or WebP file.'); - } - - const mime = match[1]; - if (!ALLOWED_MIME.has(mime)) { - throw new Error('Unsupported image type. Use PNG, ICO, SVG, JPEG, or WebP.'); - } - - const buffer = Buffer.from(match[2], 'base64'); - if (buffer.length > 512 * 1024) { - throw new Error('Favicon must be 512 KB or smaller.'); - } - - ensureDir(); - removeExistingFavicons(); - - const ext = MIME_EXT[mime] || '.png'; - const filename = `${FAVICON_BASENAME}${ext}`; - const filePath = path.join(BRANDING_DIR, filename); - fs.writeFileSync(filePath, buffer); - - const current = readBranding(); - return writeBranding({ - ...current, - faviconFile: filename, - faviconMime: mime, - }); -} - -function clearFavicon() { - removeExistingFavicons(); - const current = readBranding(); - return writeBranding({ - ...current, - faviconFile: null, - faviconMime: null, - }); -} - -function getFaviconPath() { - const branding = readBranding(); - if (!branding.faviconFile) return null; - const filePath = path.join(BRANDING_DIR, branding.faviconFile); - if (!fs.existsSync(filePath)) return null; - return { filePath, mime: branding.faviconMime || 'image/png' }; -} - -function resetBranding() { - clearFavicon(); - return writeBranding({ ...DEFAULTS }); -} - -module.exports = { - readBranding, - writeBranding, - publicPayload, - updateBranding, - saveFaviconFromBase64, - clearFavicon, - getFaviconPath, - resetBranding, - DEFAULTS, -}; diff --git a/Structures/DashboardPermissions.js b/Structures/DashboardPermissions.js deleted file mode 100644 index af44a07..0000000 --- a/Structures/DashboardPermissions.js +++ /dev/null @@ -1,134 +0,0 @@ -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 deleted file mode 100644 index a6e12d8..0000000 --- a/Structures/DashboardSession.js +++ /dev/null @@ -1,75 +0,0 @@ -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, opts = {}) { - const secure = - opts.secure !== undefined - ? Boolean(opts.secure) - : process.env.NODE_ENV === "production"; - return { - httpOnly: true, - secure, - sameSite: "lax", - maxAge: maxAgeMs, - path: "/", - }; -} - -module.exports = { - SESSION_COOKIE, - SESSION_MAX_AGE_MS, - signSession, - verifySession, - createSessionPayload, - sessionCookieOptions, -}; diff --git a/Structures/DashboardSetup.js b/Structures/DashboardSetup.js deleted file mode 100644 index 311c84e..0000000 --- a/Structures/DashboardSetup.js +++ /dev/null @@ -1,421 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const axios = require("axios"); -const YAML = require("yaml"); -const configStore = require("./ConfigStore.js"); -const { CALLBACK_PATH } = require("./DiscordOAuth.js"); - -const DISCORD_API = "https://discord.com/api/v10"; -const CONFIG_DIR = path.join(__dirname, "../Configs"); -const API_FILE = path.join(CONFIG_DIR, "api.yml"); -const SUPPORTBOT_FILE = path.join(CONFIG_DIR, "supportbot.yml"); - -const PLACEHOLDER_PATTERNS = [ - /DISCORD_CLIENT_ID/i, - /DISCORD_CLIENT_SECRET/i, - /YOUR_DISCORD_USER_ID/i, - /CHANGE_ME/i, - /BOT_TOKEN/i, - /PUT_RANDOM_STRING/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 isDiscordSnowflake(value) { - return /^\d{17,20}$/.test(String(value).trim()); -} - -function isValidBotToken(value) { - if (isPlaceholder(value)) return false; - const s = String(value).trim(); - return s.length >= 50 && s.includes("."); -} - -function isValidSecretKey(value) { - if (isPlaceholder(value)) return false; - return String(value).trim().length >= 16; -} - -function readApiRoot() { - if (!fs.existsSync(API_FILE)) return null; - try { - const raw = YAML.parse(fs.readFileSync(API_FILE, "utf8")); - return raw?.API ?? raw ?? null; - } catch { - return null; - } -} - -function readSupportbotRoot() { - if (!fs.existsSync(SUPPORTBOT_FILE)) return {}; - try { - return YAML.parse(fs.readFileSync(SUPPORTBOT_FILE, "utf8")) || {}; - } catch { - return {}; - } -} - -function getOwnerIds(oauth) { - const ids = []; - for (const id of oauth?.OwnerUserIds || []) { - const s = String(id).trim(); - if (s) ids.push(s); - } - if (ids.length === 0) { - for (const id of oauth?.AllowedUserIds || []) { - const s = String(id).trim(); - if (s) ids.push(s); - } - } - return ids; -} - -function buildSteps(apiRoot, supportbot) { - const oauth = apiRoot?.OAuth || {}; - const token = supportbot?.General?.Token; - - return [ - { - id: "botToken", - title: "Bot token", - description: "From the Discord Developer Portal → your application → Bot → Reset Token.", - complete: isValidBotToken(token), - }, - { - id: "secretKey", - title: "Dashboard session secret", - description: "Random string used to sign login cookies. Keep it private.", - complete: isValidSecretKey(apiRoot?.SecretKey), - }, - { - id: "oauth", - title: "Discord OAuth app", - description: "Same application as your bot — Client ID and Client Secret from OAuth2.", - complete: - !isPlaceholder(oauth.ClientId) && - !isPlaceholder(oauth.ClientSecret) && - isDiscordSnowflake(oauth.ClientId), - }, - { - id: "redirectUri", - title: "OAuth redirect URL", - description: `Add this exact URL under OAuth2 → Redirects in the Discord portal.`, - complete: isValidRedirectUriConfigured(oauth.RedirectUri), - }, - { - id: "owners", - title: "Dashboard owners", - description: "Discord user IDs that can sign in and manage the dashboard (at least one).", - complete: getOwnerIds(oauth).length > 0 && getOwnerIds(oauth).every(isDiscordSnowflake), - }, - ]; -} - -function isValidRedirectUriConfigured(uri) { - if (isPlaceholder(uri)) return false; - try { - const u = new URL(String(uri).trim()); - if (!["http:", "https:"].includes(u.protocol)) return false; - return u.pathname.replace(/\/$/, "") === CALLBACK_PATH.replace(/\/$/, ""); - } catch { - return false; - } -} - -function isSetupComplete() { - const apiRoot = readApiRoot(); - if (!apiRoot?.Enabled) return false; - const oauth = apiRoot.OAuth || {}; - if (!oauth.Enabled) return false; - const steps = buildSteps(apiRoot, readSupportbotRoot()); - return steps.every((s) => s.complete); -} - -function getSetupStatus(req) { - const apiRoot = readApiRoot() || {}; - const steps = buildSteps(apiRoot, readSupportbotRoot()); - const complete = steps.every((s) => s.complete); - - let suggestedRedirectUri = null; - if (req) { - const proto = - req.get("x-forwarded-proto")?.split(",")[0]?.trim() || - req.protocol || - "http"; - const host = req.get("x-forwarded-host")?.split(",")[0]?.trim() || req.get("host"); - if (host) { - suggestedRedirectUri = `${proto}://${host}${CALLBACK_PATH}`; - } - } - if (!suggestedRedirectUri) { - suggestedRedirectUri = `http://localhost:${apiRoot.Port || 3000}${CALLBACK_PATH}`; - } - - return { - complete, - steps, - suggestedRedirectUri, - oauthCallbackPath: CALLBACK_PATH, - }; -} - -async function validateBotToken(token) { - const trimmed = String(token || "").trim(); - if (!isValidBotToken(trimmed)) { - return { ok: false, error: "Enter a valid bot token from the Discord Developer Portal." }; - } - - try { - const [userRes, appRes] = await Promise.all([ - axios.get(`${DISCORD_API}/users/@me`, { - headers: { Authorization: `Bot ${trimmed}` }, - timeout: 12_000, - }), - axios.get(`${DISCORD_API}/oauth2/applications/@me`, { - headers: { Authorization: `Bot ${trimmed}` }, - timeout: 12_000, - }), - ]); - - return { - ok: true, - bot: { - id: userRes.data.id, - username: userRes.data.username, - discriminator: userRes.data.discriminator, - }, - applicationId: String(appRes.data.id), - }; - } catch (err) { - const status = err.response?.status; - if (status === 401) { - return { ok: false, error: "Discord rejected this bot token (401 Unauthorized)." }; - } - return { - ok: false, - error: err.response?.data?.message || err.message || "Could not verify bot token.", - }; - } -} - -async function validateOAuthCredentials(clientId, clientSecret, botToken) { - const id = String(clientId || "").trim(); - const secret = String(clientSecret || "").trim(); - - if (!isDiscordSnowflake(id)) { - return { ok: false, error: "Client ID must be a numeric Discord application ID." }; - } - if (isPlaceholder(secret) || secret.length < 20) { - return { ok: false, error: "Enter the OAuth Client Secret from the Discord portal." }; - } - - if (botToken) { - const botCheck = await validateBotToken(botToken); - if (botCheck.ok && botCheck.applicationId && botCheck.applicationId !== id) { - return { - ok: false, - error: `Client ID does not match the bot application (${botCheck.applicationId}). Use the same app's credentials.`, - }; - } - } - - try { - await axios.post( - `${DISCORD_API}/oauth2/token`, - new URLSearchParams({ grant_type: "client_credentials", scope: "identify" }), - { - auth: { username: id, password: secret }, - headers: { "Content-Type": "application/x-www-form-urlencoded" }, - timeout: 12_000, - }, - ); - return { ok: true, clientId: id }; - } catch (err) { - const status = err.response?.status; - if (botToken) { - const botCheck = await validateBotToken(botToken); - if (botCheck.ok && botCheck.applicationId === id) { - return { - ok: true, - clientId: id, - warning: - "Could not verify the client secret with Discord (token endpoint). ID matches your bot — double-check the secret in the portal.", - }; - } - } - if (status === 401 || status === 400) { - return { ok: false, error: "Invalid Client ID or Client Secret." }; - } - return { - ok: false, - error: err.response?.data?.error_description || err.message || "OAuth validation failed.", - }; - } -} - -function validateRedirectUri(uri) { - const trimmed = String(uri || "").trim(); - if (!isValidRedirectUriConfigured(trimmed)) { - return { - ok: false, - error: `Redirect URL must end with ${CALLBACK_PATH} and use http or https.`, - }; - } - return { ok: true, redirectUri: trimmed }; -} - -async function validateOwnerUserIds(ownerIds, botToken) { - const ids = (Array.isArray(ownerIds) ? ownerIds : [ownerIds]) - .map((id) => String(id).trim()) - .filter(Boolean); - - if (ids.length === 0) { - return { ok: false, error: "Add at least one Discord user ID." }; - } - - const invalid = ids.filter((id) => !isDiscordSnowflake(id)); - if (invalid.length) { - return { ok: false, error: `Invalid user ID: ${invalid[0]}` }; - } - - if (!botToken || !isValidBotToken(botToken)) { - return { ok: true, ownerUserIds: ids, warning: "User IDs format OK (bot token not verified yet)." }; - } - - const users = []; - try { - for (const id of ids) { - const res = await axios.get(`${DISCORD_API}/users/${id}`, { - headers: { Authorization: `Bot ${String(botToken).trim()}` }, - timeout: 12_000, - }); - users.push({ - id: res.data.id, - username: res.data.username, - globalName: res.data.global_name || null, - }); - } - return { ok: true, ownerUserIds: ids, users }; - } catch (err) { - if (err.response?.status === 404) { - return { ok: false, error: "One or more user IDs were not found. Check Developer Mode → Copy User ID." }; - } - if (err.response?.status === 403) { - return { - ok: false, - error: "Bot could not look up users. Ensure the bot is in a server with those users, or verify IDs manually.", - }; - } - return { ok: false, error: err.message || "Failed to verify owner user IDs." }; - } -} - -function validateSecretKey(secretKey) { - if (!isValidSecretKey(secretKey)) { - return { ok: false, error: "Use at least 16 characters (not CHANGE_ME)." }; - } - return { ok: true }; -} - -async function validateSetupPayload(payload) { - const results = {}; - - if (payload.botToken != null) { - results.botToken = await validateBotToken(payload.botToken); - } - if (payload.secretKey != null) { - results.secretKey = validateSecretKey(payload.secretKey); - } - if (payload.clientId != null && payload.clientSecret != null) { - results.oauth = await validateOAuthCredentials( - payload.clientId, - payload.clientSecret, - payload.botToken, - ); - } - if (payload.redirectUri != null) { - results.redirectUri = validateRedirectUri(payload.redirectUri); - } - if (payload.ownerUserIds != null) { - results.owners = await validateOwnerUserIds(payload.ownerUserIds, payload.botToken); - } - - const allOk = Object.values(results).every((r) => r?.ok); - return { ok: allOk, results }; -} - -function ensureApiFile() { - if (fs.existsSync(API_FILE)) return; - const example = path.join(CONFIG_DIR, "api.yml.example"); - if (fs.existsSync(example)) { - fs.copyFileSync(example, API_FILE); - configStore.reloadFile("api"); - } else { - throw new Error("Configs/api.yml is missing. Copy Configs/api.yml.example to api.yml."); - } -} - -function applySetup(payload) { - ensureApiFile(); - - const ownerIds = (Array.isArray(payload.ownerUserIds) - ? payload.ownerUserIds - : String(payload.ownerUserIds || "").split(/[\s,]+/) - ) - .map((id) => String(id).trim()) - .filter(Boolean); - - const apiDoc = YAML.parseDocument(fs.readFileSync(API_FILE, "utf8")); - if (!apiDoc.get("API")) apiDoc.set("API", {}); - - apiDoc.setIn(["API", "Enabled"], true); - apiDoc.setIn(["API", "SecretKey"], String(payload.secretKey).trim()); - if (payload.port != null && payload.port !== "") { - apiDoc.setIn(["API", "Port"], Number(payload.port) || 3000); - } - if (!apiDoc.getIn(["API", "OAuth"])) apiDoc.setIn(["API", "OAuth"], {}); - - apiDoc.setIn(["API", "OAuth", "Enabled"], true); - apiDoc.setIn(["API", "OAuth", "ClientId"], String(payload.clientId).trim()); - apiDoc.setIn(["API", "OAuth", "ClientSecret"], String(payload.clientSecret).trim()); - apiDoc.setIn(["API", "OAuth", "RedirectUri"], String(payload.redirectUri).trim()); - apiDoc.setIn(["API", "OAuth", "OwnerUserIds"], ownerIds); - apiDoc.setIn(["API", "OAuth", "AllowedUserIds"], []); - - fs.writeFileSync(API_FILE, apiDoc.toString()); - - const sbDoc = YAML.parseDocument(fs.readFileSync(SUPPORTBOT_FILE, "utf8")); - sbDoc.setIn(["General", "Token"], String(payload.botToken).trim()); - fs.writeFileSync(SUPPORTBOT_FILE, sbDoc.toString()); - - configStore.reloadFile("api"); - configStore.reloadFile("supportbot"); - - return { - ownerUserIds: ownerIds, - api: configStore.api?.API || configStore.api, - }; -} - -module.exports = { - isSetupComplete, - getSetupStatus, - validateBotToken, - validateOAuthCredentials, - validateRedirectUri, - validateOwnerUserIds, - validateSecretKey, - validateSetupPayload, - applySetup, -}; diff --git a/Structures/DashboardUserStore.js b/Structures/DashboardUserStore.js deleted file mode 100644 index 58613d6..0000000 --- a/Structures/DashboardUserStore.js +++ /dev/null @@ -1,291 +0,0 @@ -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/Database.js b/Structures/Database.js deleted file mode 100644 index 3ec4803..0000000 --- a/Structures/Database.js +++ /dev/null @@ -1,474 +0,0 @@ -// SupportBot | Emerald Services -const Database = require("better-sqlite3"); -const path = require("path"); - -const db = new Database(path.join(__dirname, "../Data/supportbot.db")); - -db.prepare( - ` - CREATE TABLE IF NOT EXISTS suggestions ( - thread_id TEXT PRIMARY KEY, - author_id TEXT, - text TEXT, - status TEXT DEFAULT 'Created' - ) -`, -).run(); - -db.prepare( - ` - CREATE TABLE IF NOT EXISTS profiles ( - user_id TEXT PRIMARY KEY, - bio TEXT, - timezone TEXT, - clockedIn INTEGER DEFAULT 0 - ) -`, -).run(); - -db.prepare( - ` - CREATE TABLE IF NOT EXISTS tickets ( - ticket_id TEXT PRIMARY KEY, - user_id TEXT, - status TEXT DEFAULT 'open', - created_at INTEGER, - updated_at INTEGER, - subject TEXT, - description TEXT - ) -`, -).run(); - -// Safe column upgrades -const safeAlter = (sql) => { - try { - db.prepare(sql).run(); - } catch (e) { - if (!String(e).includes("duplicate column name")) throw e; - } -}; - -safeAlter(`ALTER TABLE tickets ADD COLUMN voiceChannelId TEXT`); -safeAlter(`ALTER TABLE tickets ADD COLUMN department TEXT DEFAULT 'general'`); -safeAlter(`ALTER TABLE tickets ADD COLUMN priority TEXT DEFAULT 'medium'`); -safeAlter(`ALTER TABLE tickets ADD COLUMN aiModeEnabled INTEGER DEFAULT 0`); -safeAlter(`ALTER TABLE tickets ADD COLUMN aiChannelId TEXT`); -safeAlter(`ALTER TABLE tickets ADD COLUMN aiType TEXT`); -safeAlter(`ALTER TABLE tickets ADD COLUMN aiEnabledBy TEXT`); -safeAlter(`ALTER TABLE tickets ADD COLUMN aiEnabledAt INTEGER`); -safeAlter(`ALTER TABLE tickets ADD COLUMN questionAnswers TEXT`); -safeAlter(`ALTER TABLE tickets ADD COLUMN ticketName TEXT`); -safeAlter(`ALTER TABLE tickets ADD COLUMN ticketNumber TEXT`); - -db.prepare( - ` - CREATE TABLE IF NOT EXISTS ticket_panel ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - message_id TEXT, - channel_id TEXT, - created_at INTEGER - ) -`, -).run(); - -db.prepare( - ` - CREATE TABLE IF NOT EXISTS ticket_users ( - ticket_id TEXT, - user_id TEXT, - PRIMARY KEY(ticket_id, user_id) - ) -`, -).run(); - -db.prepare( - ` - CREATE TABLE IF NOT EXISTS settings ( - id INTEGER PRIMARY KEY, - data TEXT - ) -`, -).run(); - -function ensureProfile(userId) { - const existing = db - .prepare( - ` - SELECT user_id FROM profiles WHERE user_id = ? - `, - ) - .get(userId); - - if (!existing) { - db.prepare( - ` - INSERT INTO profiles (user_id, bio, timezone, clockedIn) - VALUES (?, '', '', 0) - `, - ).run(userId); - } -} - -function normaliseTicketRow(row) { - if (!row) return null; - - let questionAnswers = []; - - if (row.questionAnswers) { - try { - questionAnswers = JSON.parse(row.questionAnswers); - } catch { - questionAnswers = []; - } - } - - return { - ...row, - aiModeEnabled: Boolean(row.aiModeEnabled), - clockedIn: - row.clockedIn !== undefined ? Boolean(row.clockedIn) : row.clockedIn, - questionAnswers, - }; -} - -module.exports = { - addTicket(ticket) { - const now = Date.now(); - - db.prepare( - ` - INSERT INTO tickets ( - ticket_id, - user_id, - subject, - description, - created_at, - updated_at, - department, - priority, - aiModeEnabled, - aiChannelId, - aiType, - aiEnabledBy, - aiEnabledAt, - questionAnswers, - ticketName, - ticketNumber - ) - VALUES ( - @ticket_id, - @user_id, - @subject, - @description, - @created_at, - @updated_at, - @department, - @priority, - @aiModeEnabled, - @aiChannelId, - @aiType, - @aiEnabledBy, - @aiEnabledAt, - @questionAnswers, - @ticketName, - @ticketNumber - ) - ON CONFLICT(ticket_id) DO NOTHING - `, - ).run({ - ticket_id: ticket.id, - user_id: ticket.user, - subject: ticket.reason || "", - description: ticket.description || "", - created_at: now, - updated_at: now, - department: ticket.department || "general", - priority: ticket.priority || "medium", - aiModeEnabled: ticket.aiModeEnabled ? 1 : 0, - aiChannelId: ticket.aiChannelId || null, - aiType: ticket.aiType || null, - aiEnabledBy: ticket.aiEnabledBy || null, - aiEnabledAt: ticket.aiEnabledAt || null, - questionAnswers: JSON.stringify(ticket.questionAnswers || []), - ticketName: ticket.name || null, - ticketNumber: ticket.number || null, - }); - }, - - updateTicketQuestionAnswers(ticketId, questionAnswers = []) { - db.prepare( - ` - UPDATE tickets - SET questionAnswers=@questionAnswers, updated_at=@updatedAt - WHERE ticket_id=@ticketId - `, - ).run({ - ticketId, - questionAnswers: JSON.stringify(questionAnswers), - updatedAt: Date.now(), - }); - }, - - updateTicketVoice(ticketId, voiceChannelId) { - db.prepare( - ` - UPDATE tickets - SET voiceChannelId=@voiceChannelId, updated_at=@updatedAt - WHERE ticket_id=@ticketId - `, - ).run({ - ticketId, - voiceChannelId, - updatedAt: Date.now(), - }); - }, - - updateTicketStatus(ticketId, status) { - db.prepare( - ` - UPDATE tickets - SET status=@status, updated_at=@updatedAt - WHERE ticket_id=@ticketId - `, - ).run({ - status, - updatedAt: Date.now(), - ticketId, - }); - }, - - updateTicketDepartment(ticketId, department) { - db.prepare( - ` - UPDATE tickets - SET department=@department, updated_at=@updatedAt - WHERE ticket_id=@ticketId - `, - ).run({ - ticketId, - department, - updatedAt: Date.now(), - }); - }, - - updateTicketPriority(ticketId, priority) { - db.prepare( - ` - UPDATE tickets - SET priority=@priority, updated_at=@updatedAt - WHERE ticket_id=@ticketId - `, - ).run({ - ticketId, - priority, - updatedAt: Date.now(), - }); - }, - - setTicketAIState(ticketId, aiState = {}) { - db.prepare( - ` - UPDATE tickets - SET - aiModeEnabled=@aiModeEnabled, - aiChannelId=@aiChannelId, - aiType=@aiType, - aiEnabledBy=@aiEnabledBy, - aiEnabledAt=@aiEnabledAt, - updated_at=@updatedAt - WHERE ticket_id=@ticketId - `, - ).run({ - ticketId, - aiModeEnabled: aiState.enabled ? 1 : 0, - aiChannelId: aiState.aiChannelId || null, - aiType: aiState.aiType || null, - aiEnabledBy: aiState.enabledBy || null, - aiEnabledAt: aiState.enabledAt - ? new Date(aiState.enabledAt).getTime() - : aiState.enabled - ? Date.now() - : null, - updatedAt: Date.now(), - }); - }, - - enableTicketAI( - ticketId, - aiChannelId = null, - aiType = null, - enabledBy = null, - ) { - db.prepare( - ` - UPDATE tickets - SET - aiModeEnabled=1, - aiChannelId=@aiChannelId, - aiType=@aiType, - aiEnabledBy=@aiEnabledBy, - aiEnabledAt=@aiEnabledAt, - updated_at=@updatedAt - WHERE ticket_id=@ticketId - `, - ).run({ - ticketId, - aiChannelId, - aiType, - aiEnabledBy, - aiEnabledAt: Date.now(), - updatedAt: Date.now(), - }); - }, - - disableTicketAI(ticketId) { - db.prepare( - ` - UPDATE tickets - SET - aiModeEnabled=0, - updated_at=@updatedAt - WHERE ticket_id=@ticketId - `, - ).run({ - ticketId, - updatedAt: Date.now(), - }); - }, - - getTicket(ticketId) { - const row = db - .prepare( - ` - SELECT * FROM tickets WHERE ticket_id=@ticketId - `, - ) - .get({ ticketId }); - - return normaliseTicketRow(row); - }, - - getTicketByAIChannel(aiChannelId) { - const row = db - .prepare( - ` - SELECT * FROM tickets WHERE aiChannelId=@aiChannelId LIMIT 1 - `, - ) - .get({ aiChannelId }); - - return normaliseTicketRow(row); - }, - - getAllTickets() { - return db.prepare(`SELECT * FROM tickets`).all().map(normaliseTicketRow); - }, - - saveTicketPanel(messageId, channelId) { - db.prepare( - ` - INSERT INTO ticket_panel (message_id, channel_id, created_at) - VALUES (@messageId, @channelId, @createdAt) - `, - ).run({ messageId, channelId, createdAt: Date.now() }); - }, - - getTicketPanel() { - return db - .prepare( - ` - SELECT * FROM ticket_panel ORDER BY id DESC LIMIT 1 - `, - ) - .get(); - }, - - addUserToTicket(ticketId, userId) { - db.prepare( - ` - INSERT INTO ticket_users (ticket_id, user_id) - VALUES (?, ?) - ON CONFLICT(ticket_id, user_id) DO NOTHING - `, - ).run(ticketId, userId); - }, - - removeUserFromTicket(ticketId, userId) { - db.prepare( - ` - DELETE FROM ticket_users WHERE ticket_id=? AND user_id=? - `, - ).run(ticketId, userId); - }, - - getTicketUsers(ticketId) { - return db - .prepare( - ` - SELECT user_id FROM ticket_users WHERE ticket_id=? - `, - ) - .all(ticketId) - .map((r) => r.user_id); - }, - - getSettings() { - const row = db.prepare(`SELECT data FROM settings WHERE id=1`).get(); - if (!row) return {}; - try { - return JSON.parse(row.data); - } catch { - return {}; - } - }, - - getProfile(userId) { - ensureProfile(userId); - - const row = db - .prepare( - ` - SELECT * FROM profiles WHERE user_id = ? - `, - ) - .get(userId); - - if (!row) { - return { - user_id: userId, - bio: "", - timezone: "", - clockedIn: false, - }; - } - - return { - ...row, - clockedIn: Boolean(row.clockedIn), - }; - }, - - setClockedIn(userId, clockedIn) { - ensureProfile(userId); - - db.prepare( - ` - UPDATE profiles - SET clockedIn = ? - WHERE user_id = ? - `, - ).run(clockedIn ? 1 : 0, userId); - }, - - saveSettings(obj) { - const data = JSON.stringify(obj); - db.prepare( - ` - INSERT INTO settings (id, data) - VALUES (1, @data) - ON CONFLICT(id) DO UPDATE SET data = excluded.data - `, - ).run({ data }); - }, -}; diff --git a/Structures/DiscordOAuth.js b/Structures/DiscordOAuth.js deleted file mode 100644 index 36e1175..0000000 --- a/Structures/DiscordOAuth.js +++ /dev/null @@ -1,233 +0,0 @@ -const crypto = require("crypto"); -const axios = require("axios"); - -const DISCORD_API = "https://discord.com/api"; -const OAUTH_STATE_COOKIE = "sb_oauth_state"; -const OAUTH_REDIRECT_COOKIE = "sb_oauth_redirect"; -const OAUTH_STATE_MAX_AGE_MS = 10 * 60 * 1000; -const CALLBACK_PATH = "/api/auth/discord/callback"; - -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 getTrustProxySetting(apiConfig) { - if (apiConfig?.TrustProxy === false) return false; - if (typeof apiConfig?.TrustProxy === "number") return apiConfig.TrustProxy; - if (apiConfig?.TrustProxy === true) return 1; - return false; -} - -function getRequestProto(req, apiConfig) { - if (apiConfig?.TrustProxy !== false) { - const forwarded = req.get("x-forwarded-proto"); - if (forwarded) { - return forwarded.split(",")[0].trim().toLowerCase(); - } - } - return (req.protocol || "http").toLowerCase(); -} - -function getRequestHost(req) { - const forwarded = req.get("x-forwarded-host"); - if (forwarded) { - return forwarded.split(",")[0].trim(); - } - return req.get("host") || ""; -} - -function getRequestOrigin(req, apiConfig) { - const proto = getRequestProto(req, apiConfig); - const host = getRequestHost(req); - if (!host) return null; - return `${proto}://${host}`; -} - -function normalizeRedirectUri(uri) { - try { - const u = new URL(uri); - u.hash = ""; - u.search = ""; - return u.toString(); - } catch { - return null; - } -} - -function redirectUriMatchesHostPath(a, b) { - try { - const ua = new URL(a); - const ub = new URL(b); - return ua.host === ub.host && ua.pathname === ub.pathname; - } catch { - return false; - } -} - -function collectConfiguredRedirectUris(oauth) { - const list = []; - if (oauth?.RedirectUri) { - const normalized = normalizeRedirectUri(oauth.RedirectUri); - if (normalized) list.push(normalized); - } - if (Array.isArray(oauth?.RedirectUris)) { - for (const entry of oauth.RedirectUris) { - const normalized = normalizeRedirectUri(entry); - if (normalized && !list.includes(normalized)) { - list.push(normalized); - } - } - } - return list; -} - -/** - * Pick the OAuth redirect_uri for this request. When UseRequestOrigin is enabled - * (default), uses the same host/path as RedirectUri but the protocol the user - * actually used (http vs https). Register every variant in the Discord portal. - */ -function resolveOAuthRedirectUri(req, oauth, apiConfig) { - const configured = collectConfiguredRedirectUris(oauth); - const fallback = configured[0] || oauth?.RedirectUri || ""; - - const useRequestOrigin = oauth?.UseRequestOrigin !== false; - if (!useRequestOrigin || !req) { - return fallback; - } - - const origin = getRequestOrigin(req, apiConfig); - if (!origin) return fallback; - - const dynamic = `${origin}${CALLBACK_PATH}`; - const normalizedDynamic = normalizeRedirectUri(dynamic); - if (!normalizedDynamic) return fallback; - - if (configured.includes(normalizedDynamic)) { - return normalizedDynamic; - } - - const matchesConfigured = configured.some((uri) => - redirectUriMatchesHostPath(uri, normalizedDynamic), - ); - if (matchesConfigured) { - return normalizedDynamic; - } - - return fallback; -} - -function isSecureRequest(req, oauth, apiConfig) { - if (oauth?.CookieSecure === true) return true; - if (oauth?.CookieSecure === false) return false; - if (req) { - if (typeof req.secure === "boolean") return req.secure; - return getRequestProto(req, apiConfig) === "https"; - } - return process.env.NODE_ENV === "production"; -} - -function buildAuthorizeUrl(oauth, state, redirectUri) { - const params = new URLSearchParams({ - client_id: oauth.ClientId, - redirect_uri: redirectUri, - response_type: "code", - scope: "identify", - state, - prompt: "none", - }); - return `${DISCORD_API}/oauth2/authorize?${params.toString()}`; -} - -async function exchangeCode(oauth, code, redirectUri) { - const body = new URLSearchParams({ - client_id: oauth.ClientId, - client_secret: oauth.ClientSecret, - grant_type: "authorization_code", - code, - redirect_uri: 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(req, oauth, apiConfig) { - return { - httpOnly: true, - secure: isSecureRequest(req, oauth, apiConfig), - sameSite: "lax", - maxAge: OAUTH_STATE_MAX_AGE_MS, - path: "/", - }; -} - -function oauthRedirectCookieOptions(req, oauth, apiConfig) { - return oauthStateCookieOptions(req, oauth, apiConfig); -} - -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_REDIRECT_COOKIE, - OAUTH_STATE_MAX_AGE_MS, - CALLBACK_PATH, - getTrustProxySetting, - resolveOAuthRedirectUri, - isSecureRequest, - buildAuthorizeUrl, - exchangeCode, - fetchDiscordUser, - createOAuthState, - oauthStateCookieOptions, - oauthRedirectCookieOptions, - isOAuthConfigured, - isUserAllowed, - formatPublicUser, - discordAvatarUrl, -}; diff --git a/Structures/Event.js b/Structures/Event.js deleted file mode 100644 index 06363cb..0000000 --- a/Structures/Event.js +++ /dev/null @@ -1,11 +0,0 @@ -// SupportBot | Emerald Services -// Event Structure - -class Event { - constructor(event, run) { - this.event = event; - this.run = run; - } -} - -module.exports = Event; diff --git a/Structures/GuildManager.js b/Structures/GuildManager.js deleted file mode 100644 index 537117b..0000000 --- a/Structures/GuildManager.js +++ /dev/null @@ -1,276 +0,0 @@ -const { ChannelType } = require("discord.js"); -const configStore = require("./ConfigStore.js"); -const { withTimeout } = require("./asyncTimeout.js"); - -const RESOURCES_CACHE_MS = 90 * 1000; -/** Bump when resource payload shape changes (e.g. added emojis). */ -const RESOURCES_CACHE_VERSION = 2; -let resourcesCache = { guildId: null, data: null, at: 0, version: 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, - })); - - const emojis = [...guild.emojis.cache.values()] - .sort((a, b) => a.name.localeCompare(b.name)) - .map((e) => { - const animated = Boolean(e.animated); - const ext = animated ? "gif" : "png"; - return { - id: e.id, - name: e.name, - animated, - url: - e.imageURL({ extension: ext, size: 64 }) || - `https://cdn.discordapp.com/emojis/${e.id}.${ext}?size=64`, - }; - }); - - return { - guildId: guild.id, - guildName: guild.name, - roles, - channels, - categories, - emojis, - }; -} - -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; - } - } - - try { - await withTimeout(guild.emojis.fetch(), 12_000, "Discord emoji fetch timed out"); - } catch (err) { - console.warn("[Guild] Failed to fetch guild emojis:", err.message); - } - - 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.version === RESOURCES_CACHE_VERSION && - 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(), - version: RESOURCES_CACHE_VERSION, - }; - } - return data; - })(); - - try { - return await resourcesInflight; - } finally { - resourcesInflight = null; - } -} - -function clearGuildResourcesCache() { - resourcesCache = { guildId: null, data: null, at: 0, version: 0 }; - resourcesInflight = null; -} - -module.exports = { - getConfiguredGuildId, - setConfiguredGuildId, - getPrimaryGuild, - evaluateGuildHealth, - syncGuildHealth, - logGuildWarning, - leaveGuild, - fetchGuildResources, - clearGuildResourcesCache, -}; diff --git a/Structures/LogReader.js b/Structures/LogReader.js deleted file mode 100644 index b0f4dbb..0000000 --- a/Structures/LogReader.js +++ /dev/null @@ -1,181 +0,0 @@ -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 deleted file mode 100644 index d809c93..0000000 --- a/Structures/ModuleStatus.js +++ /dev/null @@ -1,270 +0,0 @@ -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) { - // Ticket setup is tracked by the tickets module — avoid a misleading transcripts alert. - return { - status: "done", - subtitle: "Available after ticket channels are configured", - passed: 0, - total: 0, - }; - } - - if (transcriptCount > 0) { - return { - status: "done", - subtitle: `${transcriptCount} transcript${transcriptCount === 1 ? "" : "s"} saved`, - passed: 1, - total: 1, - }; - } - - return { - status: "done", - subtitle: "No transcripts yet — they are created when tickets close", - passed: 1, - total: 1, - }; -} - -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 deleted file mode 100644 index 09e85e2..0000000 --- a/Structures/NotificationService.js +++ /dev/null @@ -1,63 +0,0 @@ -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 deleted file mode 100644 index b5876c2..0000000 --- a/Structures/NotificationStore.js +++ /dev/null @@ -1,86 +0,0 @@ -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/TicketID.js b/Structures/TicketID.js deleted file mode 100644 index 7fb941e..0000000 --- a/Structures/TicketID.js +++ /dev/null @@ -1,30 +0,0 @@ -// SupportBot | Emerald Services -// Ticket ID Structure - -const db = require("./Database") - -class TicketNumberID { - static async pad() { - let num = await this.get() - let n = num + "" - await this.set(num + 1) - return n.length >= 4 ? n : new Array(4 - n.length + 1).join("0") + n - } - - static async get() { - const row = db.getTicketPanel() - if (!row) return await this.set(0) - return row.id - } - - static async set(value) { - db.saveTicketPanel(`panel-${value}`, "none") - return value - } - - static async reset() { - return await this.set(0) - } -} - -module.exports = TicketNumberID diff --git a/Structures/TicketManager.js b/Structures/TicketManager.js deleted file mode 100644 index 991631f..0000000 --- a/Structures/TicketManager.js +++ /dev/null @@ -1,250 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const Discord = require("discord.js"); -const yaml = require("js-yaml"); -const db = require("./Database.js"); - -const supportbot = require("./ConfigStore").supportbot; -const msgconfig = require("./ConfigStore").messages; -const { createTranscriptHTML } = require("./TranscriptTemplate.js"); - -function escapeHtml(str = "") { - return String(str) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """); -} - -function parseMarkdown(content = "") { - let safe = escapeHtml(content); - - safe = safe.replace(/```([\s\S]*?)```/g, "
$1
"); - safe = safe.replace(/`(.*?)`/g, "$1"); - safe = safe.replace(/\*\*(.*?)\*\*/g, "$1"); - safe = safe.replace(/\*(.*?)\*/g, "$1"); - safe = safe.replace(/_(.*?)_/g, "$1"); - safe = safe.replace( - /\[(.*?)\]\((.*?)\)/g, - '$1', - ); - - return safe.replace(/\n/g, "
"); -} - -function formatDate(iso) { - try { - return new Date(iso).toLocaleString("en-GB", { - dateStyle: "medium", - timeStyle: "short", - }); - } catch { - return iso; - } -} - -function getFileTypeInfo(att) { - const fileName = String(att.name || "attachment").toLowerCase(); - - if (/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i.test(fileName)) { - return "image"; - } - - if (/\.(mp4|webm|mov|m4v)$/i.test(fileName)) { - return "video"; - } - - if (/\.(mp3|wav|ogg|m4a|flac)$/i.test(fileName)) { - return "audio"; - } - - return "file"; -} - -function renderAttachment(att) { - const url = escapeHtml(att.url || ""); - const name = escapeHtml(att.name || "attachment"); - const type = getFileTypeInfo(att); - - if (type === "image") { - return ` -
- 📎 ${name} -
- ${name} -
-
- `; - } - - if (type === "video") { - return ` -
- 📎 ${name} -
- -
-
- `; - } - - if (type === "audio") { - return ` -
- 📎 ${name} -
- -
-
- `; - } - - return ` - - `; -} - -function renderEmbeds(embeds = []) { - return embeds - .map((embed) => { - const fields = (embed.fields || []) - .map( - (field) => ` -
-
${escapeHtml(field.name || "Field")}
-
${parseMarkdown(field.value || "")}
-
- `, - ) - .join(""); - - return ` -
- ${embed.title ? `
${escapeHtml(embed.title)}
` : ""} - ${embed.description ? `
${parseMarkdown(embed.description)}
` : ""} - ${fields} -
- `; - }) - .join(""); -} - -async function createTranscript(interaction, ticket, reason) { - const { getChannel } = interaction.client; - - const transcriptChannel = await getChannel( - supportbot.Ticket.Log.TicketDataLog, - interaction.guild, - ); - - if (!transcriptChannel) { - throw new Error("Transcript log channel missing or inaccessible."); - } - - let allMessages = []; - let lastId = null; - - while (true) { - const options = { limit: 100 }; - if (lastId) options.before = lastId; - - const messages = await interaction.channel.messages.fetch(options); - if (messages.size === 0) break; - - allMessages = [...allMessages, ...messages.values()]; - lastId = messages.last().id; - - if (messages.size < 100) break; - } - - allMessages.sort((a, b) => a.createdTimestamp - b.createdTimestamp); - - const transcriptData = allMessages.map((msg) => ({ - content: msg.content || "", - username: msg.author.username, - userId: msg.author.id, - avatar: msg.author.displayAvatarURL({ extension: "png", size: 128 }), - timestamp: msg.createdAt.toISOString(), - attachments: Array.from(msg.attachments.values()).map((att) => ({ - url: att.url, - name: att.name || "attachment", - contentType: att.contentType || null, - size: att.size || 0, - })), - embeds: msg.embeds.map((embed) => ({ - title: embed.title || "", - description: embed.description || "", - fields: embed.fields || [], - })), - })); - - if (typeof db.updateTicketStatus === "function") { - db.updateTicketStatus(interaction.channel.id, "closed"); - } - - const ticketUserId = ticket.user_id || ticket.user; - const tUser = - interaction.client.users.cache.get(ticketUserId) || - (ticketUserId - ? await interaction.client.users.fetch(ticketUserId).catch(() => null) - : null); - - const transcriptEmbed = new Discord.EmbedBuilder() - .setTitle(msgconfig.TicketLog.Title) - .setColor(msgconfig.TicketLog.Colour) - .setFooter({ - text: supportbot.Embed.Footer, - iconURL: interaction.user.displayAvatarURL(), - }) - .setDescription( - `> **Ticket:** ${interaction.channel.name} (\`${interaction.channel.id}\`)\n` + - `> **User:** ${tUser?.tag || "Unknown User"} (\`${tUser?.id || ticketUserId || "Unknown"}\`)\n` + - `> **Closed by:** <@${interaction.user.id}>\n` + - `> **Message Count:** ${transcriptData.length}`, - ) - .addFields({ - name: "Reason", - value: `\`\`\`${reason || "No Reason Provided."}\`\`\``, - inline: false, - }); - - const transcriptDir = path.join(process.cwd(), "./Data/Transcripts"); - if (!fs.existsSync(transcriptDir)) { - fs.mkdirSync(transcriptDir, { recursive: true }); - } - - const htmlFileName = `${interaction.channel.id}-transcript.html`; - const htmlPath = path.join(transcriptDir, htmlFileName); - - const html = createTranscriptHTML( - { - id: interaction.channel.id, - name: interaction.channel.name, - messages: transcriptData, - }, - reason, - ); - - fs.writeFileSync(htmlPath, html, "utf8"); - - await transcriptChannel.send({ - embeds: [transcriptEmbed], - files: [ - new Discord.AttachmentBuilder(htmlPath, { - name: `SPOILER_${htmlFileName}`, - }), - ], - }); - - return { htmlPath }; -} - -module.exports = { - createTranscript, -}; diff --git a/Structures/TranscriptStore.js b/Structures/TranscriptStore.js deleted file mode 100644 index ed337ec..0000000 --- a/Structures/TranscriptStore.js +++ /dev/null @@ -1,223 +0,0 @@ -const fs = require('fs'); -const path = require('path'); - -const TRANSCRIPT_DIR = path.join(__dirname, '../Data/Transcripts'); -const SETTINGS_FILE = path.join(__dirname, '../Data/transcript-settings.json'); - -const DEFAULT_SETTINGS = { - autoDelete: { - enabled: false, - afterDays: 30, - }, - publicAccess: { - enabled: false, - }, -}; - -let purgeTimer = null; - -function ensureTranscriptDir() { - if (!fs.existsSync(TRANSCRIPT_DIR)) { - fs.mkdirSync(TRANSCRIPT_DIR, { recursive: true }); - } -} - -function getSettings() { - try { - if (!fs.existsSync(SETTINGS_FILE)) { - return JSON.parse(JSON.stringify(DEFAULT_SETTINGS)); - } - const raw = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')); - const days = Number(raw?.autoDelete?.afterDays); - return { - autoDelete: { - enabled: Boolean(raw?.autoDelete?.enabled), - afterDays: - Number.isFinite(days) && days >= 1 - ? Math.min(Math.floor(days), 3650) - : DEFAULT_SETTINGS.autoDelete.afterDays, - }, - publicAccess: { - enabled: Boolean(raw?.publicAccess?.enabled), - }, - }; - } catch { - return JSON.parse(JSON.stringify(DEFAULT_SETTINGS)); - } -} - -function saveSettings(patch) { - const current = getSettings(); - const next = { - autoDelete: { - enabled: - patch?.autoDelete?.enabled !== undefined - ? Boolean(patch.autoDelete.enabled) - : current.autoDelete.enabled, - afterDays: - patch?.autoDelete?.afterDays !== undefined - ? Math.min( - Math.max(Math.floor(Number(patch.autoDelete.afterDays) || 1), 1), - 3650, - ) - : current.autoDelete.afterDays, - }, - publicAccess: { - enabled: - patch?.publicAccess?.enabled !== undefined - ? Boolean(patch.publicAccess.enabled) - : current.publicAccess.enabled, - }, - }; - - const dir = path.dirname(SETTINGS_FILE); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - fs.writeFileSync(SETTINGS_FILE, JSON.stringify(next, null, 2), 'utf8'); - return next; -} - -function normalizeId(rawId) { - return String(rawId).replace(/[^0-9]/g, ''); -} - -function readTranscriptFile(rawId) { - const id = normalizeId(rawId); - if (!id) return null; - ensureTranscriptDir(); - const filePath = path.join(TRANSCRIPT_DIR, `${id}-transcript.html`); - if (!fs.existsSync(filePath)) return null; - const content = fs.readFileSync(filePath, 'utf8'); - const titleMatch = content.match(/\s*Transcript\s*-\s*([^<]+)\s*<\/title>/i); - return { - id, - filePath, - filename: `${id}-transcript.html`, - content, - ticketName: titleMatch ? titleMatch[1].trim() : null, - }; -} - -function listTranscripts() { - ensureTranscriptDir(); - if (!fs.existsSync(TRANSCRIPT_DIR)) { - return []; - } - return fs - .readdirSync(TRANSCRIPT_DIR) - .filter((f) => f.endsWith('-transcript.html')) - .map((f) => { - const id = f.replace('-transcript.html', ''); - const stats = fs.statSync(path.join(TRANSCRIPT_DIR, f)); - const parsed = readTranscriptFile(id); - return { - id, - filename: f, - createdAt: stats.mtime.toISOString(), - size: stats.size, - ticketName: parsed?.ticketName ?? null, - }; - }) - .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); -} - -function deleteTranscript(rawId) { - const id = normalizeId(rawId); - if (!id) return false; - const filePath = path.join(TRANSCRIPT_DIR, `${id}-transcript.html`); - if (!fs.existsSync(filePath)) return false; - fs.unlinkSync(filePath); - return true; -} - -function purgeExpiredTranscripts() { - const settings = getSettings(); - if (!settings.autoDelete.enabled) { - return { deleted: 0, skipped: true }; - } - - ensureTranscriptDir(); - const maxAgeMs = settings.autoDelete.afterDays * 24 * 60 * 60 * 1000; - const cutoff = Date.now() - maxAgeMs; - let deleted = 0; - - for (const f of fs.readdirSync(TRANSCRIPT_DIR)) { - if (!f.endsWith('-transcript.html')) continue; - const filePath = path.join(TRANSCRIPT_DIR, f); - const stats = fs.statSync(filePath); - if (stats.mtime.getTime() < cutoff) { - fs.unlinkSync(filePath); - deleted += 1; - } - } - - return { deleted, skipped: false }; -} - -function notFoundHtml(message = 'Transcript not found') { - return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Transcript - -

${message}

`; -} - -function servePublicTranscript(req, res) { - const settings = getSettings(); - if (!settings.publicAccess.enabled) { - return res.status(404).type('html').send(notFoundHtml('Transcript not found')); - } - - const transcript = readTranscriptFile(req.params.id); - if (!transcript) { - return res.status(404).type('html').send(notFoundHtml('Transcript not found')); - } - - res.setHeader('Content-Type', 'text/html; charset=utf-8'); - res.setHeader('Cache-Control', 'public, max-age=120'); - return res.send(transcript.content); -} - -function startAutoDeleteScheduler(intervalMs = 60 * 60 * 1000) { - if (purgeTimer) return; - - const run = () => { - try { - const result = purgeExpiredTranscripts(); - if (result.deleted > 0) { - console.log( - `[Transcripts] Auto-deleted ${result.deleted} transcript(s) older than ${getSettings().autoDelete.afterDays} day(s)`, - ); - } - } catch (err) { - console.error('[Transcripts] Auto-delete failed:', err.message); - } - }; - - run(); - purgeTimer = setInterval(run, intervalMs); - if (typeof purgeTimer.unref === 'function') { - purgeTimer.unref(); - } -} - -function getPublicUrl(req, id) { - const settings = getSettings(); - if (!settings.publicAccess.enabled) return null; - const host = req.get('host'); - const proto = req.get('x-forwarded-proto') || req.protocol || 'http'; - return `${proto}://${host}/transcripts/${id}`; -} - -module.exports = { - TRANSCRIPT_DIR, - DEFAULT_SETTINGS, - getSettings, - saveSettings, - readTranscriptFile, - listTranscripts, - deleteTranscript, - purgeExpiredTranscripts, - servePublicTranscript, - startAutoDeleteScheduler, - getPublicUrl, -}; diff --git a/Structures/TranscriptTemplate.js b/Structures/TranscriptTemplate.js deleted file mode 100644 index 543181e..0000000 --- a/Structures/TranscriptTemplate.js +++ /dev/null @@ -1,680 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const yaml = require("js-yaml"); -const { - normalizeFontFamily, - resolveTypography, -} = require("./googleFonts.js"); - -const TEMPLATE_PATH = path.join(__dirname, "../Configs/transcript-template.yml"); - -let cachedTemplate = null; - -function escapeHtml(str = "") { - return String(str) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """); -} - -function parseMarkdown(content = "") { - let safe = escapeHtml(content); - - safe = safe.replace(/```([\s\S]*?)```/g, "
$1
"); - safe = safe.replace(/`(.*?)`/g, "$1"); - safe = safe.replace(/\*\*(.*?)\*\*/g, "$1"); - safe = safe.replace(/\*(.*?)\*/g, "$1"); - safe = safe.replace(/_(.*?)_/g, "$1"); - safe = safe.replace( - /\[(.*?)\]\((.*?)\)/g, - '$1', - ); - - return safe.replace(/\n/g, "
"); -} - -function formatDate(iso) { - try { - return new Date(iso).toLocaleString("en-GB", { - dateStyle: "medium", - timeStyle: "short", - }); - } catch { - return iso; - } -} - -function getFileTypeInfo(att) { - const fileName = String(att.name || "attachment").toLowerCase(); - - if (/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i.test(fileName)) { - return "image"; - } - if (/\.(mp4|webm|mov|m4v)$/i.test(fileName)) { - return "video"; - } - if (/\.(mp3|wav|ogg|m4a|flac)$/i.test(fileName)) { - return "audio"; - } - return "file"; -} - -function renderAttachment(att) { - const url = escapeHtml(att.url || ""); - const name = escapeHtml(att.name || "attachment"); - const type = getFileTypeInfo(att); - - if (type === "image") { - return ` -
- 📎 ${name} -
- ${name} -
-
- `; - } - - if (type === "video") { - return ` -
- 📎 ${name} -
- -
-
- `; - } - - if (type === "audio") { - return ` -
- 📎 ${name} -
- -
-
- `; - } - - return ` - - `; -} - -function renderEmbeds(embeds = []) { - return embeds - .map((embed) => { - const fields = (embed.fields || []) - .map( - (field) => ` -
-
${escapeHtml(field.name || "Field")}
-
${parseMarkdown(field.value || "")}
-
- `, - ) - .join(""); - - return ` -
- ${embed.title ? `
${escapeHtml(embed.title)}
` : ""} - ${embed.description ? `
${parseMarkdown(embed.description)}
` : ""} - ${fields} -
- `; - }) - .join(""); -} - -function deepMerge(base, patch) { - const out = structuredClone(base); - for (const [key, val] of Object.entries(patch || {})) { - if ( - val && - typeof val === "object" && - !Array.isArray(val) && - out[key] && - typeof out[key] === "object" && - !Array.isArray(out[key]) - ) { - out[key] = deepMerge(out[key], val); - } else { - out[key] = val; - } - } - return out; -} - -function getDefaultTemplate() { - return { - Theme: { - Background: "#0b0f14", - Panel: "#111821", - PanelSecondary: "#182230", - PanelTertiary: "#1d2938", - Border: "#283548", - Text: "#e6edf3", - Muted: "#9fb0c3", - Accent: "#22c55e", - AccentSoft: "rgba(34, 197, 94, 0.12)", - AccentBorder: "rgba(34, 197, 94, 0.25)", - AccentText: "#8df0ad", - Link: "#8fd9ff", - AttachmentLink: "#8fd9a8", - CodeBackground: "#0f1720", - }, - Typography: { - FontFamily: "Inter", - }, - Header: { - Title: "Ticket Transcript", - Subtitle: - "Exported conversation log for support ticket **{{ticketName}}**", - Badge: "Closed Transcript", - ShowMetaCards: true, - }, - Meta: { - ChannelLabel: "Channel", - TicketIdLabel: "Ticket ID", - MessageCountLabel: "Message Count", - CloseReasonLabel: "Close Reason", - DefaultCloseReason: "No Reason Provided.", - }, - Messages: { - SectionTitle: "Messages", - EmptyState: "No messages were found in this ticket.", - }, - Advanced: { - CustomCss: "", - }, - }; -} - -function normalizeTemplate(raw) { - const merged = deepMerge(getDefaultTemplate(), raw || {}); - if (merged.Typography) { - merged.Typography.FontFamily = normalizeFontFamily( - merged.Typography.FontFamily, - ); - } - return merged; -} - -function loadTemplateFromDisk() { - try { - if (!fs.existsSync(TEMPLATE_PATH)) { - const defaults = getDefaultTemplate(); - fs.mkdirSync(path.dirname(TEMPLATE_PATH), { recursive: true }); - fs.writeFileSync(TEMPLATE_PATH, yaml.dump(defaults), "utf8"); - return defaults; - } - const parsed = yaml.load(fs.readFileSync(TEMPLATE_PATH, "utf8")); - return normalizeTemplate(parsed); - } catch (err) { - console.error("[TranscriptTemplate] Failed to load template:", err.message); - return getDefaultTemplate(); - } -} - -function getTemplate() { - if (!cachedTemplate) { - cachedTemplate = loadTemplateFromDisk(); - } - return structuredClone(cachedTemplate); -} - -function reloadTemplate() { - cachedTemplate = loadTemplateFromDisk(); - return getTemplate(); -} - -function saveTemplate(data) { - const merged = normalizeTemplate(data); - fs.mkdirSync(path.dirname(TEMPLATE_PATH), { recursive: true }); - fs.writeFileSync(TEMPLATE_PATH, yaml.dump(merged), "utf8"); - cachedTemplate = merged; - return merged; -} - -function applyPlaceholders(text, ctx) { - return String(text || "") - .replace(/\{\{ticketName\}\}/g, ctx.name || "") - .replace(/\{\{ticketId\}\}/g, ctx.id || "") - .replace(/\{\{messageCount\}\}/g, String(ctx.messageCount ?? 0)) - .replace(/\{\{reason\}\}/g, ctx.reason || ""); -} - -function formatHeaderText(text, ctx) { - const withPlaceholders = applyPlaceholders(text, ctx); - const escaped = escapeHtml(withPlaceholders); - return escaped.replace(/\*\*(.+?)\*\*/g, "$1"); -} - -function buildStyles(template) { - const t = template.Theme; - const { fontFamilyCss } = resolveTypography(template.Typography?.FontFamily); - const custom = String(template.Advanced?.CustomCss || "").trim(); - - return ` - :root { - --bg: ${t.Background}; - --panel: ${t.Panel}; - --panel-2: ${t.PanelSecondary}; - --panel-3: ${t.PanelTertiary}; - --border: ${t.Border}; - --text: ${t.Text}; - --muted: ${t.Muted}; - --accent: ${t.Accent}; - --accent-soft: ${t.AccentSoft}; - --accent-border: ${t.AccentBorder}; - --accent-text: ${t.AccentText}; - --link: ${t.Link}; - --attachment-link: ${t.AttachmentLink}; - --code: ${t.CodeBackground}; - } - - * { box-sizing: border-box; } - - html, body { - margin: 0; - padding: 0; - background: var(--bg); - color: var(--text); - font-family: ${fontFamilyCss}; - } - - body { padding: 32px 20px; } - - .page { max-width: 1100px; margin: 0 auto; } - - .header { - background: var(--panel); - border: 1px solid var(--border); - border-radius: 16px; - padding: 24px; - margin-bottom: 18px; - } - - .header-top { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 16px; - flex-wrap: wrap; - } - - .title-wrap h1 { - margin: 0; - font-size: 1.8rem; - line-height: 1.2; - font-weight: 800; - letter-spacing: -0.02em; - } - - .title-wrap p { - margin: 8px 0 0; - color: var(--muted); - font-size: 0.98rem; - } - - .badge { - display: inline-flex; - align-items: center; - gap: 8px; - background: var(--accent-soft); - color: var(--accent-text); - border: 1px solid var(--accent-border); - padding: 8px 12px; - border-radius: 999px; - font-size: 0.9rem; - font-weight: 700; - white-space: nowrap; - } - - .meta-grid { - margin-top: 18px; - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 12px; - } - - .meta-card { - background: var(--panel-2); - border: 1px solid var(--border); - border-radius: 12px; - padding: 14px; - } - - .meta-label { - color: var(--muted); - font-size: 0.82rem; - margin-bottom: 6px; - text-transform: uppercase; - letter-spacing: 0.08em; - font-weight: 700; - } - - .meta-value { - font-size: 0.98rem; - font-weight: 600; - word-break: break-word; - } - - .messages-wrap { - background: var(--panel); - border: 1px solid var(--border); - border-radius: 16px; - overflow: hidden; - } - - .messages-header { - padding: 18px 20px; - border-bottom: 1px solid var(--border); - background: var(--panel-2); - font-weight: 800; - font-size: 1rem; - } - - .message { - display: flex; - gap: 14px; - padding: 18px 20px; - border-bottom: 1px solid var(--border); - } - - .message:last-child { border-bottom: none; } - - .avatar { - width: 42px; - height: 42px; - min-width: 42px; - border-radius: 50%; - object-fit: cover; - background: var(--panel-3); - border: 1px solid var(--border); - } - - .message-body { min-width: 0; flex: 1; } - - .message-header { - display: flex; - align-items: baseline; - gap: 10px; - flex-wrap: wrap; - margin-bottom: 8px; - } - - .username { - font-weight: 800; - color: var(--text); - font-size: 0.98rem; - } - - .userid { - color: var(--muted); - font-size: 0.8rem; - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - } - - .timestamp { - color: var(--muted); - font-size: 0.82rem; - margin-left: auto; - } - - .content { - color: var(--text); - font-size: 0.96rem; - line-height: 1.6; - word-break: break-word; - } - - .content:empty { display: none; } - - .discord-embed { - margin-top: 12px; - background: var(--panel-2); - border: 1px solid var(--border); - border-left: 4px solid var(--accent); - border-radius: 10px; - padding: 12px 14px; - } - - .discord-embed-title { font-weight: 800; margin-bottom: 6px; } - .discord-embed-description { color: var(--text); line-height: 1.5; } - - .embed-field { - margin-top: 10px; - padding-top: 10px; - border-top: 1px solid rgba(255,255,255,0.06); - } - - .embed-field-name { font-weight: 700; margin-bottom: 4px; } - .embed-field-value { color: var(--text); line-height: 1.5; } - - .attachment { - margin-top: 12px; - background: var(--panel-2); - border: 1px solid var(--border); - border-radius: 12px; - padding: 12px; - } - - .attachment-file { - display: inline-block; - color: var(--attachment-link); - text-decoration: none; - font-weight: 700; - word-break: break-word; - } - - .attachment-file:hover { text-decoration: underline; } - - .attachment-preview { margin-top: 10px; } - - .attachment-preview img, - .attachment-preview video { - width: 100%; - max-width: 560px; - border-radius: 10px; - border: 1px solid var(--border); - display: block; - background: #0a0f15; - } - - .attachment-preview audio { - width: 100%; - max-width: 560px; - display: block; - } - - code { - background: var(--code); - border: 1px solid var(--border); - padding: 2px 6px; - border-radius: 6px; - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - font-size: 0.9em; - } - - pre { - background: var(--code); - border: 1px solid var(--border); - padding: 12px; - border-radius: 10px; - overflow-x: auto; - white-space: pre-wrap; - word-break: break-word; - } - - pre code { background: transparent; border: none; padding: 0; } - - a { color: var(--link); } - - .empty-state { padding: 24px 20px; color: var(--muted); } - - @media (max-width: 700px) { - body { padding: 18px 12px; } - .header, .messages-wrap { border-radius: 12px; } - .message { padding: 14px; } - .timestamp { width: 100%; margin-left: 0; } - } - - ${custom} - `; -} - -function getSampleTicket() { - return { - id: "123456789012345678", - name: "ticket-support-demo", - messages: [ - { - username: "SupportBot", - userId: "100000000000000001", - avatar: "https://cdn.discordapp.com/embed/avatars/0.png", - timestamp: new Date().toISOString(), - content: "Hello! Thanks for opening a ticket. How can we help?", - embeds: [], - attachments: [], - }, - { - username: "Member", - userId: "100000000000000002", - avatar: "https://cdn.discordapp.com/embed/avatars/1.png", - timestamp: new Date(Date.now() - 60000).toISOString(), - content: "I need help with my purchase — order **#48291**.", - embeds: [ - { - title: "Order details", - description: "Payment received · awaiting delivery", - fields: [{ name: "Status", value: "Pending" }], - }, - ], - attachments: [], - }, - ], - }; -} - -function createTranscriptHTML(ticket, reason, templateInput) { - const template = templateInput - ? normalizeTemplate(templateInput) - : getTemplate(); - const header = template.Header; - const meta = template.Meta; - const messagesCfg = template.Messages; - const closeReason = reason || meta.DefaultCloseReason; - const ctx = { - name: ticket.name, - id: ticket.id, - messageCount: ticket.messages.length, - reason: closeReason, - }; - - const { headLinks } = resolveTypography(template.Typography?.FontFamily); - - const metaSection = header.ShowMetaCards - ? ` -
-
-
${escapeHtml(meta.ChannelLabel)}
-
${escapeHtml(ticket.name)}
-
-
-
${escapeHtml(meta.TicketIdLabel)}
-
${escapeHtml(ticket.id)}
-
-
-
${escapeHtml(meta.MessageCountLabel)}
-
${ticket.messages.length}
-
-
-
${escapeHtml(meta.CloseReasonLabel)}
-
${escapeHtml(closeReason)}
-
-
- ` - : ""; - - return ` - - - - - Transcript - ${escapeHtml(ticket.name)} - ${headLinks} - - - -
-
-
-
-

${escapeHtml(header.Title)}

-

${formatHeaderText(header.Subtitle, ctx)}

-
-
${escapeHtml(header.Badge)}
-
- ${metaSection} -
- -
-
${escapeHtml(messagesCfg.SectionTitle)}
- ${ - ticket.messages.length - ? ticket.messages - .map( - (msg) => ` -
- Avatar -
-
- ${escapeHtml(msg.username || "Unknown User")} - ${escapeHtml(msg.userId || "Unknown ID")} - ${formatDate(msg.timestamp)} -
-
${parseMarkdown(msg.content || "")}
- ${renderEmbeds(msg.embeds || [])} - ${(msg.attachments || []).map(renderAttachment).join("")} -
-
- `, - ) - .join("") - : `
${escapeHtml(messagesCfg.EmptyState)}
` - } -
-
- -`; -} - -function renderPreview(templateInput) { - const sample = getSampleTicket(); - return createTranscriptHTML( - sample, - "Resolved — thank you for contacting support.", - templateInput, - ); -} - -module.exports = { - TEMPLATE_PATH, - getDefaultTemplate, - getTemplate, - reloadTemplate, - saveTemplate, - normalizeTemplate, - getSampleTicket, - createTranscriptHTML, - renderPreview, -}; diff --git a/Structures/UpdateChecker.js b/Structures/UpdateChecker.js deleted file mode 100644 index 34a73ab..0000000 --- a/Structures/UpdateChecker.js +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index a736a96..0000000 --- a/Structures/asyncTimeout.js +++ /dev/null @@ -1,14 +0,0 @@ -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/Structures/googleFonts.js b/Structures/googleFonts.js deleted file mode 100644 index 87b427a..0000000 --- a/Structures/googleFonts.js +++ /dev/null @@ -1,84 +0,0 @@ -/** Curated Google Fonts for transcript HTML (keep in sync with dashboard/src/lib/google-fonts.ts). */ - -const SYSTEM_FALLBACK = - 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; - -const GOOGLE_FONTS = [ - { id: "Inter", label: "Inter" }, - { id: "Roboto", label: "Roboto" }, - { id: "Open Sans", label: "Open Sans" }, - { id: "Lato", label: "Lato" }, - { id: "Montserrat", label: "Montserrat" }, - { id: "Poppins", label: "Poppins" }, - { id: "Nunito", label: "Nunito" }, - { id: "Raleway", label: "Raleway" }, - { id: "Work Sans", label: "Work Sans" }, - { id: "Source Sans 3", label: "Source Sans 3" }, - { id: "DM Sans", label: "DM Sans" }, - { id: "Manrope", label: "Manrope" }, - { id: "Outfit", label: "Outfit" }, - { id: "Rubik", label: "Rubik" }, - { id: "Ubuntu", label: "Ubuntu" }, - { id: "Fira Sans", label: "Fira Sans" }, - { id: "PT Sans", label: "PT Sans" }, - { id: "Noto Sans", label: "Noto Sans" }, - { id: "Oswald", label: "Oswald" }, - { id: "Merriweather", label: "Merriweather" }, - { id: "Playfair Display", label: "Playfair Display" }, - { id: "Libre Baskerville", label: "Libre Baskerville" }, - { id: "JetBrains Mono", label: "JetBrains Mono" }, - { id: "Fira Code", label: "Fira Code" }, -]; - -const GOOGLE_FONT_IDS = new Set(GOOGLE_FONTS.map((f) => f.id)); - -function normalizeFontFamily(fontFamily) { - if (!fontFamily || typeof fontFamily !== "string") return "Inter"; - const trimmed = fontFamily.trim(); - if (trimmed === "system") return "system"; - if (!trimmed.includes(",")) { - return GOOGLE_FONT_IDS.has(trimmed) ? trimmed : "Inter"; - } - const match = trimmed.match(/^["']?([^"',]+)["']?/); - const first = match?.[1]?.trim(); - if (first && GOOGLE_FONT_IDS.has(first)) return first; - return "Inter"; -} - -function googleFontsCssUrl(familyIds) { - const families = familyIds - .map((id) => { - const encoded = encodeURIComponent(id).replace(/%20/g, "+"); - return `family=${encoded}:wght@400;600;700;800`; - }) - .join("&"); - return `https://fonts.googleapis.com/css2?${families}&display=swap`; -} - -function resolveTypography(fontFamily) { - const id = normalizeFontFamily(fontFamily); - - if (id === "system") { - return { - fontId: "system", - fontFamilyCss: SYSTEM_FALLBACK, - headLinks: "", - }; - } - - const url = googleFontsCssUrl([id]); - return { - fontId: id, - fontFamilyCss: `"${id}", ${SYSTEM_FALLBACK}`, - headLinks: ``, - }; -} - -module.exports = { - GOOGLE_FONTS, - GOOGLE_FONT_IDS, - SYSTEM_FALLBACK, - normalizeFontFamily, - googleFontsCssUrl, - resolveTypography, -}; diff --git a/docs/readme-header.png b/docs/readme-header.png deleted file mode 100644 index 5d5c936..0000000 Binary files a/docs/readme-header.png and /dev/null differ diff --git a/index.js b/index.js deleted file mode 100644 index c541821..0000000 --- a/index.js +++ /dev/null @@ -1,89 +0,0 @@ -// ___ _ ___ _ -// / __> _ _ ___ ___ ___ _ _ _| |_ | . > ___ _| |_ -// \__ \| | || . \| . \/ . \| '_> | | | . \/ . \ | | -// <___/`___|| _/| _/\___/|_| |_| |___/\___/ |_| -// |_| |_| -// -// SupportBot created by Emerald Services -// Installed with MIT License -// -// Discord Support: https://emeraldsrv.dev/discord -// Community Resources: https://community.emeraldsrv.dev - -const fs = require("fs"); -const path = require("path"); - -const configStore = require("./Structures/ConfigStore.js"); - -const Client = require("./Structures/Client.js"); -const client = new Client({ - intents: ["Guilds", "GuildMembers", "GuildMessages", "MessageContent"], -}); - -const APIServer = require("./API/server.js"); - -// Start dashboard API immediately — do not wait for Discord clientReady -const api = new APIServer(client); -client.apiServer = api; - -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 - -const logTypes = ["Output", "Warn", "Error"]; - -if (!fs.existsSync("./Logs")) { - fs.mkdirSync("./Logs"); -} -logTypes.forEach((type) => { - const dir = `./Logs/${type}`; - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); - } -}); - -function logToFile(type, data) { - const date = new Date().toISOString().split("T")[0]; - const file = path.join(`./Logs/${type}`, `${type}-${date}.log`); - fs.appendFileSync(file, `[${new Date().toISOString()}] ${data}\n`); -} - -const origLog = console.log; -console.log = (...args) => { - origLog(...args); - logToFile("Output", args.join(" ")); -}; - -const origWarn = console.warn; -console.warn = (...args) => { - origWarn(...args); - logToFile("Warn", args.join(" ")); -}; - -const origError = console.error; -console.error = (...args) => { - origError(...args); - logToFile("Error", args.join(" ")); -}; - -process.on("unhandledRejection", (reason) => { - console.error("Unhandled Rejection:", reason); -}); - -process.on("uncaughtException", (err) => { - console.error("Uncaught Exception:", err); -}); diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index a3bd9c9..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2479 +0,0 @@ -{ - "name": "supportbot", - "version": "26.3.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "supportbot", - "version": "26.3.0", - "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 deleted file mode 100644 index 62c0f8d..0000000 --- a/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "supportbot", - "version": "26.3.0", - "description": "SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customization to match your own brand.", - "main": "index.js", - "scripts": { - "setup": "npm i && node index.js", - "start": "node index.js", - "git:setup": "node scripts/setup-git-security.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/Emerald-Services/SupportBot.git" - }, - "author": "Emerald Services", - "license": "MIT", - "bugs": { - "url": "https://github.com/Emerald-Services/SupportBot/issues" - }, - "homepage": "https://community.emeraldsrv.dev", - "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" - } -} diff --git a/public/dashboard/assets/index-C_d968Vp.css b/public/dashboard/assets/index-C_d968Vp.css deleted file mode 100644 index b69d5a9..0000000 --- a/public/dashboard/assets/index-C_d968Vp.css +++ /dev/null @@ -1 +0,0 @@ -*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Poppins,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 240 10% 4%;--foreground: 0 0% 98%;--card: 240 7% 11%;--card-foreground: 0 0% 98%;--popover: 240 8% 10%;--popover-foreground: 0 0% 98%;--primary: 141 45% 49%;--primary-foreground: 0 0% 100%;--secondary: 240 6% 14%;--secondary-foreground: 0 0% 90%;--muted: 240 6% 14%;--muted-foreground: 240 5% 55%;--accent: 141 29% 18%;--accent-foreground: 141 52% 78%;--destructive: 0 72% 55%;--destructive-foreground: 0 0% 100%;--border: 240 6% 18%;--input: 240 6% 14%;--ring: 141 45% 49%;--radius: .75rem;--chart-1: 141 45% 49%;--chart-2: 199 89% 48%;--chart-3: 38 92% 50%;--chart-4: 280 65% 60%;--chart-5: 0 72% 55%;--sidebar-background: 240 10% 5%;--sidebar-foreground: 240 5% 90%;--sidebar-primary: 141 45% 49%;--sidebar-primary-foreground: 0 0% 100%;--sidebar-accent: 141 26% 14%;--sidebar-accent-foreground: 141 48% 82%;--sidebar-border: 240 6% 14%;--sidebar-ring: 141 45% 49%;--scrollbar-size: 6px;--scrollbar-thumb: 240 4% 32%;--scrollbar-thumb-hover: 240 4% 44%}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));font-family:Poppins,system-ui,sans-serif;color:hsl(var(--foreground));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}html{color-scheme:dark}html,body,*{scrollbar-width:thin;scrollbar-color:hsl(var(--scrollbar-thumb)) transparent}*::-webkit-scrollbar{width:var(--scrollbar-size);height:var(--scrollbar-size)}*::-webkit-scrollbar-button{display:none;width:0;height:0}*::-webkit-scrollbar-track{background:transparent}*::-webkit-scrollbar-thumb{background-color:hsl(var(--scrollbar-thumb));border-radius:9999px;border:2px solid transparent;background-clip:content-box}*::-webkit-scrollbar-thumb:hover{background-color:hsl(var(--scrollbar-thumb-hover))}*::-webkit-scrollbar-thumb:active{background-color:hsl(var(--scrollbar-thumb-hover))}*::-webkit-scrollbar-corner{background:transparent}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.inset-y-0{top:0;bottom:0}.-right-0\.5{right:-.125rem}.-top-0\.5{top:-.125rem}.bottom-0{bottom:0}.left-0{left:0}.left-2{left:.5rem}.left-3{left:.75rem}.left-\[50\%\]{left:50%}.right-0{right:0}.right-1{right:.25rem}.right-2{right:.5rem}.right-3{right:.75rem}.right-4{right:1rem}.top-0{top:0}.top-1\.5{top:.375rem}.top-1\/2{top:50%}.top-3\.5{top:.875rem}.top-4{top:1rem}.top-\[50\%\]{top:50%}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.m-0{margin:0}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-3\.5{margin-left:.875rem;margin-right:.875rem}.mx-5{margin-left:1.25rem;margin-right:1.25rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.-ml-1{margin-left:-.25rem}.-ml-2{margin-left:-.5rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-1\.5{margin-right:.375rem}.mr-2{margin-right:.5rem}.mt-0{margin-top:0}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mt-auto{margin-top:auto}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.line-clamp-3{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1 / 1}.size-1\.5{width:.375rem;height:.375rem}.size-10{width:2.5rem;height:2.5rem}.size-11{width:2.75rem;height:2.75rem}.size-12{width:3rem;height:3rem}.size-14{width:3.5rem;height:3.5rem}.size-16{width:4rem;height:4rem}.size-2{width:.5rem;height:.5rem}.size-28{width:7rem;height:7rem}.size-36{width:9rem;height:9rem}.size-4{width:1rem;height:1rem}.size-7{width:1.75rem;height:1.75rem}.size-8{width:2rem;height:2rem}.size-9{width:2.25rem;height:2.25rem}.size-full{width:100%;height:100%}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-20{height:5rem}.h-24{height:6rem}.h-3\.5{height:.875rem}.h-32{height:8rem}.h-4{height:1rem}.h-40{height:10rem}.h-44{height:11rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-56{height:14rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-80{height:20rem}.h-9{height:2.25rem}.h-96{height:24rem}.h-\[1px\]{height:1px}.h-\[520px\]{height:520px}.h-\[calc\(100svh-8rem\)\]{height:calc(100svh - 8rem)}.h-\[min\(400px\,50vh\)\]{height:min(400px,50vh)}.h-\[min\(50vh\,320px\)\]{height:min(50vh,320px)}.h-\[min\(520px\,calc\(100svh-16rem\)\)\]{height:min(520px,calc(100svh - 16rem))}.h-\[min\(640px\,calc\(100svh-14rem\)\)\]{height:min(640px,calc(100svh - 14rem))}.h-\[var\(--radix-select-trigger-height\)\]{height:var(--radix-select-trigger-height)}.h-auto{height:auto}.h-fit{height:-moz-fit-content;height:fit-content}.h-full{height:100%}.h-px{height:1px}.h-svh{height:100svh}.max-h-64{max-height:16rem}.max-h-\[--radix-select-content-available-height\]{max-height:var(--radix-select-content-available-height)}.max-h-\[300px\]{max-height:300px}.max-h-\[90vh\]{max-height:90vh}.max-h-\[calc\(100svh-18rem\)\]{max-height:calc(100svh - 18rem)}.max-h-\[min\(320px\,50vh\)\]{max-height:min(320px,50vh)}.max-h-\[min\(360px\,50vh\)\]{max-height:min(360px,50vh)}.max-h-\[min\(720px\,calc\(100svh-12rem\)\)\]{max-height:min(720px,calc(100svh - 12rem))}.max-h-\[min\(90vh\,720px\)\]{max-height:min(90vh,720px)}.max-h-\[var\(--radix-dropdown-menu-content-available-height\)\]{max-height:var(--radix-dropdown-menu-content-available-height)}.min-h-0{min-height:0px}.min-h-\[120px\]{min-height:120px}.min-h-\[420px\]{min-height:420px}.min-h-\[480px\]{min-height:480px}.min-h-\[520px\]{min-height:520px}.min-h-\[60px\]{min-height:60px}.min-h-\[88px\]{min-height:88px}.min-h-\[min\(520px\,60vh\)\]{min-height:min(520px,60vh)}.min-h-\[min\(520px\,70vh\)\]{min-height:min(520px,70vh)}.min-h-\[min\(560px\,calc\(100svh-18rem\)\)\]{min-height:min(560px,calc(100svh - 18rem))}.min-h-svh{min-height:100svh}.w-10{width:2.5rem}.w-2{width:.5rem}.w-3\.5{width:.875rem}.w-3\/4{width:75%}.w-32{width:8rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-72{width:18rem}.w-80{width:20rem}.w-9{width:2.25rem}.w-\[--radix-dropdown-menu-trigger-width\]{width:var(--radix-dropdown-menu-trigger-width)}.w-\[--sidebar-width\]{width:var(--sidebar-width)}.w-\[1px\]{width:1px}.w-\[calc\(100\%-1rem\)\]{width:calc(100% - 1rem)}.w-\[min\(100\%\,320px\)\]{width:min(100%,320px)}.w-auto{width:auto}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-4{min-width:1rem}.min-w-5{min-width:1.25rem}.min-w-\[200px\]{min-width:200px}.min-w-\[220px\]{min-width:220px}.min-w-\[280px\]{min-width:280px}.min-w-\[8rem\]{min-width:8rem}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.max-w-2xl{max-width:42rem}.max-w-6xl{max-width:72rem}.max-w-\[--skeleton-width\]{max-width:var(--skeleton-width)}.max-w-\[100px\]{max-width:100px}.max-w-\[200px\]{max-width:200px}.max-w-\[8rem\]{max-width:8rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.origin-\[--radix-dropdown-menu-content-transform-origin\]{transform-origin:var(--radix-dropdown-menu-content-transform-origin)}.origin-\[--radix-popover-content-transform-origin\]{transform-origin:var(--radix-popover-content-transform-origin)}.origin-\[--radix-select-content-transform-origin\]{transform-origin:var(--radix-select-content-transform-origin)}.origin-\[--radix-tooltip-content-transform-origin\]{transform-origin:var(--radix-tooltip-content-transform-origin)}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-px{--tw-translate-x: -1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-\[-50\%\]{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-px{--tw-translate-x: 1px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-\[-50\%\]{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rotate-90{--tw-rotate: -90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.touch-none{touch-action:none}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.place-content-center{place-content:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0{gap:0px}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-y-2{row-gap:.5rem}.-space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(-.5rem * var(--tw-space-x-reverse));margin-left:calc(-.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(0px * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px * var(--tw-space-y-reverse))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-border\/60>:not([hidden])~:not([hidden]){border-color:hsl(var(--border) / .6)}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-\[inherit\]{border-radius:inherit}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-500\/40{border-color:#f59e0b66}.border-border{border-color:hsl(var(--border))}.border-border\/40{border-color:hsl(var(--border) / .4)}.border-border\/50{border-color:hsl(var(--border) / .5)}.border-card{border-color:hsl(var(--card))}.border-destructive\/40{border-color:hsl(var(--destructive) / .4)}.border-destructive\/50{border-color:hsl(var(--destructive) / .5)}.border-emerald-500\/30{border-color:#10b9814d}.border-input{border-color:hsl(var(--input))}.border-primary{border-color:hsl(var(--primary))}.border-primary\/30{border-color:hsl(var(--primary) / .3)}.border-primary\/50{border-color:hsl(var(--primary) / .5)}.border-sidebar-border{border-color:hsl(var(--sidebar-border))}.border-sky-500\/40{border-color:#0ea5e966}.border-transparent{border-color:transparent}.border-l-transparent{border-left-color:transparent}.border-t-transparent{border-top-color:transparent}.bg-\[\#0d0d12\]{--tw-bg-opacity: 1;background-color:rgb(13 13 18 / var(--tw-bg-opacity, 1))}.bg-\[hsl\(240_4\%_32\%\)\]{--tw-bg-opacity: 1;background-color:hsl(240 4% 32% / var(--tw-bg-opacity, 1))}.bg-accent{background-color:hsl(var(--accent))}.bg-amber-400{--tw-bg-opacity: 1;background-color:rgb(251 191 36 / var(--tw-bg-opacity, 1))}.bg-amber-500{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-amber-500\/15{background-color:#f59e0b26}.bg-background{background-color:hsl(var(--background))}.bg-background\/40{background-color:hsl(var(--background) / .4)}.bg-background\/50{background-color:hsl(var(--background) / .5)}.bg-black\/80{background-color:#000c}.bg-border{background-color:hsl(var(--border))}.bg-card{background-color:hsl(var(--card))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-destructive\/10{background-color:hsl(var(--destructive) / .1)}.bg-emerald-500{--tw-bg-opacity: 1;background-color:rgb(16 185 129 / var(--tw-bg-opacity, 1))}.bg-emerald-500\/10{background-color:#10b9811a}.bg-emerald-500\/15{background-color:#10b98126}.bg-muted{background-color:hsl(var(--muted))}.bg-muted-foreground{background-color:hsl(var(--muted-foreground))}.bg-popover{background-color:hsl(var(--popover))}.bg-primary{background-color:hsl(var(--primary))}.bg-primary\/10{background-color:hsl(var(--primary) / .1)}.bg-primary\/15{background-color:hsl(var(--primary) / .15)}.bg-primary\/20{background-color:hsl(var(--primary) / .2)}.bg-primary\/25{background-color:hsl(var(--primary) / .25)}.bg-primary\/5{background-color:hsl(var(--primary) / .05)}.bg-rose-500{--tw-bg-opacity: 1;background-color:rgb(244 63 94 / var(--tw-bg-opacity, 1))}.bg-rose-500\/15{background-color:#f43f5e26}.bg-secondary{background-color:hsl(var(--secondary))}.bg-secondary\/20{background-color:hsl(var(--secondary) / .2)}.bg-secondary\/30{background-color:hsl(var(--secondary) / .3)}.bg-secondary\/40{background-color:hsl(var(--secondary) / .4)}.bg-secondary\/50{background-color:hsl(var(--secondary) / .5)}.bg-secondary\/60{background-color:hsl(var(--secondary) / .6)}.bg-sidebar{background-color:hsl(var(--sidebar-background))}.bg-sidebar-accent{background-color:hsl(var(--sidebar-accent))}.bg-sidebar-border{background-color:hsl(var(--sidebar-border))}.bg-sky-400{--tw-bg-opacity: 1;background-color:rgb(56 189 248 / var(--tw-bg-opacity, 1))}.bg-sky-500\/10{background-color:#0ea5e91a}.bg-sky-500\/15{background-color:#0ea5e926}.bg-transparent{background-color:transparent}.bg-white\/70{background-color:#ffffffb3}.fill-current{fill:currentColor}.stroke-white{stroke:#fff}.stroke-white\/20{stroke:#fff3}.object-contain{-o-object-fit:contain;object-fit:contain}.p-0{padding:0}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-px{padding:1px}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pl-2{padding-left:.5rem}.pl-8{padding-left:2rem}.pl-9{padding-left:2.25rem}.pr-1{padding-right:.25rem}.pr-2{padding-right:.5rem}.pr-8{padding-right:2rem}.pt-0{padding-top:0}.pt-0\.5{padding-top:.125rem}.pt-1{padding-top:.25rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-6xl{font-size:3.75rem;line-height:1}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-accent-foreground{color:hsl(var(--accent-foreground))}.text-amber-200{--tw-text-opacity: 1;color:rgb(253 230 138 / var(--tw-text-opacity, 1))}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-amber-500{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity, 1))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-current{color:currentColor}.text-destructive{color:hsl(var(--destructive))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-emerald-400{--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity, 1))}.text-emerald-500{--tw-text-opacity: 1;color:rgb(16 185 129 / var(--tw-text-opacity, 1))}.text-emerald-600{--tw-text-opacity: 1;color:rgb(5 150 105 / var(--tw-text-opacity, 1))}.text-emerald-600\/80{color:#059669cc}.text-foreground{color:hsl(var(--foreground))}.text-foreground\/90{color:hsl(var(--foreground) / .9)}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-muted-foreground\/80{color:hsl(var(--muted-foreground) / .8)}.text-popover-foreground{color:hsl(var(--popover-foreground))}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-rose-400{--tw-text-opacity: 1;color:rgb(251 113 133 / var(--tw-text-opacity, 1))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.text-sidebar-foreground{color:hsl(var(--sidebar-foreground))}.text-sidebar-foreground\/70{color:hsl(var(--sidebar-foreground) / .7)}.text-sky-100{--tw-text-opacity: 1;color:rgb(224 242 254 / var(--tw-text-opacity, 1))}.text-sky-400{--tw-text-opacity: 1;color:rgb(56 189 248 / var(--tw-text-opacity, 1))}.text-white\/80{color:#fffc}.text-white\/90{color:#ffffffe6}.underline-offset-4{text-underline-offset:4px}.accent-primary{accent-color:hsl(var(--primary))}.opacity-0{opacity:0}.opacity-30{opacity:.3}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-\[0_0_0_1px_hsl\(var\(--sidebar-border\)\)\]{--tw-shadow: 0 0 0 1px hsl(var(--sidebar-border));--tw-shadow-colored: 0 0 0 1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-none{--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-black\/20{--tw-shadow-color: rgb(0 0 0 / .2);--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-0{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-primary\/30{--tw-ring-color: hsl(var(--primary) / .3)}.ring-sidebar-ring{--tw-ring-color: hsl(var(--sidebar-ring))}.ring-offset-background{--tw-ring-offset-color: hsl(var(--background))}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[left\,right\,width\]{transition-property:left,right,width;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[margin\,opacity\]{transition-property:margin,opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[width\,height\,padding\]{transition-property:width,height,padding;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-\[width\]{transition-property:width;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-linear{transition-timing-function:linear}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}@keyframes enter{0%{opacity:var(--tw-enter-opacity, 1);transform:translate3d(var(--tw-enter-translate-x, 0),var(--tw-enter-translate-y, 0),0) scale3d(var(--tw-enter-scale, 1),var(--tw-enter-scale, 1),var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity, 1);transform:translate3d(var(--tw-exit-translate-x, 0),var(--tw-exit-translate-y, 0),0) scale3d(var(--tw-exit-scale, 1),var(--tw-exit-scale, 1),var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))}}.animate-in{animation-name:enter;animation-duration:.15s;--tw-enter-opacity: initial;--tw-enter-scale: initial;--tw-enter-rotate: initial;--tw-enter-translate-x: initial;--tw-enter-translate-y: initial}.fade-in-0{--tw-enter-opacity: 0}.zoom-in-95{--tw-enter-scale: .95}.duration-200{animation-duration:.2s}.duration-300{animation-duration:.3s}.ease-in-out{animation-timing-function:cubic-bezier(.4,0,.2,1)}.ease-linear{animation-timing-function:linear}.ease-out{animation-timing-function:cubic-bezier(0,0,.2,1)}.running{animation-play-state:running}.\!paused{animation-play-state:paused!important}.paused{animation-play-state:paused}.color-picker-swatch.react-colorful{width:220px;height:140px}.color-picker-swatch .react-colorful__saturation{border-radius:calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0 0}.color-picker-swatch .react-colorful__hue{height:12px;border-radius:0 0 calc(var(--radius) - 2px) calc(var(--radius) - 2px);margin-top:10px}.color-picker-swatch .react-colorful__pointer{width:16px;height:16px}.unicode-emoji-panel-host emoji-picker.unicode-emoji-picker-el{width:100%;height:min(50vh,380px);--background: hsl(var(--card));--border-color: hsl(var(--border));--button-active-background: hsl(var(--primary) / .25);--button-hover-background: hsl(var(--accent));--input-border-color: hsl(var(--border));--input-font-color: hsl(var(--foreground));--input-placeholder-color: hsl(var(--muted-foreground));--outline-color: hsl(var(--ring));--category-font-color: hsl(var(--muted-foreground));--indicator-color: hsl(var(--primary))}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.file\:text-foreground::file-selector-button{color:hsl(var(--foreground))}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:-inset-2:after{content:var(--tw-content);top:-.5rem;right:-.5rem;bottom:-.5rem;left:-.5rem}.after\:inset-y-0:after{content:var(--tw-content);top:0;bottom:0}.after\:left-1\/2:after{content:var(--tw-content);left:50%}.after\:w-\[2px\]:after{content:var(--tw-content);width:2px}.first\:pt-0:first-child{padding-top:0}.last\:border-0:last-child{border-width:0px}.last\:pb-0:last-child{padding-bottom:0}.hover\:border-primary:hover{border-color:hsl(var(--primary))}.hover\:border-primary\/30:hover{border-color:hsl(var(--primary) / .3)}.hover\:bg-\[hsl\(240_4\%_44\%\)\]:hover{--tw-bg-opacity: 1;background-color:hsl(240 4% 44% / var(--tw-bg-opacity, 1))}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-destructive\/10:hover{background-color:hsl(var(--destructive) / .1)}.hover\:bg-destructive\/80:hover{background-color:hsl(var(--destructive) / .8)}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive) / .9)}.hover\:bg-emerald-500\/15:hover{background-color:#10b98126}.hover\:bg-primary\/80:hover{background-color:hsl(var(--primary) / .8)}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary) / .9)}.hover\:bg-secondary:hover{background-color:hsl(var(--secondary))}.hover\:bg-secondary\/40:hover{background-color:hsl(var(--secondary) / .4)}.hover\:bg-secondary\/50:hover{background-color:hsl(var(--secondary) / .5)}.hover\:bg-secondary\/60:hover{background-color:hsl(var(--secondary) / .6)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary) / .8)}.hover\:bg-sidebar-accent:hover{background-color:hsl(var(--sidebar-accent))}.hover\:bg-white\/\[0\.03\]:hover{background-color:#ffffff08}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:text-destructive:hover{color:hsl(var(--destructive))}.hover\:text-foreground:hover{color:hsl(var(--foreground))}.hover\:text-sidebar-accent-foreground:hover{color:hsl(var(--sidebar-accent-foreground))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-\[0_0_0_1px_hsl\(var\(--sidebar-accent\)\)\]:hover{--tw-shadow: 0 0 0 1px hsl(var(--sidebar-accent));--tw-shadow-colored: 0 0 0 1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:after\:bg-sidebar-border:hover:after{content:var(--tw-content);background-color:hsl(var(--sidebar-border))}.focus\:bg-accent:focus{background-color:hsl(var(--accent))}.focus\:text-accent-foreground:focus{color:hsl(var(--accent-foreground))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-ring:focus{--tw-ring-color: hsl(var(--ring))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-1:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color: hsl(var(--ring))}.focus-visible\:ring-sidebar-ring:focus-visible{--tw-ring-color: hsl(var(--sidebar-ring))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width: 2px}.focus-visible\:ring-offset-background:focus-visible{--tw-ring-offset-color: hsl(var(--background))}.active\:bg-sidebar-accent:active{background-color:hsl(var(--sidebar-accent))}.active\:text-sidebar-accent-foreground:active{color:hsl(var(--sidebar-accent-foreground))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group\/menu-item:focus-within .group-focus-within\/menu-item\:opacity-100{opacity:1}.group:hover .group-hover\:text-primary{color:hsl(var(--primary))}.group\/menu-item:hover .group-hover\/menu-item\:opacity-100,.group:hover .group-hover\:opacity-100{opacity:1}.group.toaster .group-\[\.toaster\]\:border-border{border-color:hsl(var(--border))}.group.toaster .group-\[\.toaster\]\:bg-card{background-color:hsl(var(--card))}.group.toast .group-\[\.toast\]\:text-muted-foreground{color:hsl(var(--muted-foreground))}.group.toaster .group-\[\.toaster\]\:text-foreground{color:hsl(var(--foreground))}.group.toaster .group-\[\.toaster\]\:shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.peer\/menu-button:hover~.peer-hover\/menu-button\:text-sidebar-accent-foreground{color:hsl(var(--sidebar-accent-foreground))}.peer:disabled~.peer-disabled\:cursor-not-allowed{cursor:not-allowed}.peer:disabled~.peer-disabled\:opacity-70{opacity:.7}.has-\[\[data-variant\=inset\]\]\:bg-sidebar:has([data-variant=inset]){background-color:hsl(var(--sidebar-background))}.group\/menu-item:has([data-sidebar=menu-action]) .group-has-\[\[data-sidebar\=menu-action\]\]\/menu-item\:pr-8{padding-right:2rem}.aria-disabled\:pointer-events-none[aria-disabled=true]{pointer-events:none}.aria-disabled\:opacity-50[aria-disabled=true]{opacity:.5}.aria-selected\:bg-accent[aria-selected=true]{background-color:hsl(var(--accent))}.data-\[disabled\=true\]\:pointer-events-none[data-disabled=true],.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[side\=bottom\]\:translate-y-1[data-side=bottom]{--tw-translate-y: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[side\=left\]\:-translate-x-1[data-side=left]{--tw-translate-x: -.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[side\=right\]\:translate-x-1[data-side=right]{--tw-translate-x: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[side\=top\]\:-translate-y-1[data-side=top]{--tw-translate-y: -.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=checked\]\:translate-x-4[data-state=checked]{--tw-translate-x: 1rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[state\=unchecked\]\:translate-x-0[data-state=unchecked]{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.data-\[active\=true\]\:bg-sidebar-accent[data-active=true]{background-color:hsl(var(--sidebar-accent))}.data-\[selected\=true\]\:bg-accent[data-selected=true]{background-color:hsl(var(--accent))}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:hsl(var(--background))}.data-\[state\=checked\]\:bg-primary[data-state=checked]{background-color:hsl(var(--primary))}.data-\[state\=open\]\:bg-accent[data-state=open]{background-color:hsl(var(--accent))}.data-\[state\=open\]\:bg-secondary[data-state=open]{background-color:hsl(var(--secondary))}.data-\[state\=unchecked\]\:bg-input[data-state=unchecked]{background-color:hsl(var(--input))}.data-\[active\=true\]\:font-medium[data-active=true]{font-weight:500}.data-\[active\=true\]\:text-sidebar-accent-foreground[data-active=true]{color:hsl(var(--sidebar-accent-foreground))}.data-\[placeholder\]\:text-muted-foreground[data-placeholder]{color:hsl(var(--muted-foreground))}.data-\[selected\=true\]\:text-accent-foreground[data-selected=true]{color:hsl(var(--accent-foreground))}.data-\[state\=active\]\:text-foreground[data-state=active]{color:hsl(var(--foreground))}.data-\[state\=checked\]\:text-primary-foreground[data-state=checked]{color:hsl(var(--primary-foreground))}.data-\[state\=open\]\:text-muted-foreground[data-state=open]{color:hsl(var(--muted-foreground))}.data-\[disabled\=true\]\:opacity-50[data-disabled=true],.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[state\=open\]\:opacity-100[data-state=open]{opacity:1}.data-\[state\=active\]\:shadow[data-state=active]{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.data-\[state\=active\]\:shadow-sm[data-state=active]{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.data-\[state\=closed\]\:duration-300[data-state=closed]{transition-duration:.3s}.data-\[state\=open\]\:duration-500[data-state=open]{transition-duration:.5s}.data-\[state\=open\]\:animate-in[data-state=open]{animation-name:enter;animation-duration:.15s;--tw-enter-opacity: initial;--tw-enter-scale: initial;--tw-enter-rotate: initial;--tw-enter-translate-x: initial;--tw-enter-translate-y: initial}.data-\[state\=closed\]\:animate-out[data-state=closed]{animation-name:exit;animation-duration:.15s;--tw-exit-opacity: initial;--tw-exit-scale: initial;--tw-exit-rotate: initial;--tw-exit-translate-x: initial;--tw-exit-translate-y: initial}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity: 0}.data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity: 0}.data-\[state\=closed\]\:zoom-out-95[data-state=closed]{--tw-exit-scale: .95}.data-\[state\=open\]\:zoom-in-95[data-state=open]{--tw-enter-scale: .95}.data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y: -.5rem}.data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x: .5rem}.data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x: -.5rem}.data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y: .5rem}.data-\[state\=closed\]\:slide-out-to-bottom[data-state=closed]{--tw-exit-translate-y: 100%}.data-\[state\=closed\]\:slide-out-to-left[data-state=closed]{--tw-exit-translate-x: -100%}.data-\[state\=closed\]\:slide-out-to-left-1\/2[data-state=closed]{--tw-exit-translate-x: -50%}.data-\[state\=closed\]\:slide-out-to-right[data-state=closed]{--tw-exit-translate-x: 100%}.data-\[state\=closed\]\:slide-out-to-top[data-state=closed]{--tw-exit-translate-y: -100%}.data-\[state\=closed\]\:slide-out-to-top-\[48\%\][data-state=closed]{--tw-exit-translate-y: -48%}.data-\[state\=open\]\:slide-in-from-bottom[data-state=open]{--tw-enter-translate-y: 100%}.data-\[state\=open\]\:slide-in-from-left[data-state=open]{--tw-enter-translate-x: -100%}.data-\[state\=open\]\:slide-in-from-left-1\/2[data-state=open]{--tw-enter-translate-x: -50%}.data-\[state\=open\]\:slide-in-from-right[data-state=open]{--tw-enter-translate-x: 100%}.data-\[state\=open\]\:slide-in-from-top[data-state=open]{--tw-enter-translate-y: -100%}.data-\[state\=open\]\:slide-in-from-top-\[48\%\][data-state=open]{--tw-enter-translate-y: -48%}.data-\[state\=closed\]\:duration-300[data-state=closed]{animation-duration:.3s}.data-\[state\=open\]\:duration-500[data-state=open]{animation-duration:.5s}.data-\[state\=open\]\:hover\:bg-sidebar-accent:hover[data-state=open]{background-color:hsl(var(--sidebar-accent))}.data-\[state\=open\]\:hover\:text-sidebar-accent-foreground:hover[data-state=open]{color:hsl(var(--sidebar-accent-foreground))}.group[data-collapsible=offcanvas] .group-data-\[collapsible\=offcanvas\]\:left-\[calc\(var\(--sidebar-width\)\*-1\)\]{left:calc(var(--sidebar-width) * -1)}.group[data-collapsible=offcanvas] .group-data-\[collapsible\=offcanvas\]\:right-\[calc\(var\(--sidebar-width\)\*-1\)\]{right:calc(var(--sidebar-width) * -1)}.group[data-side=left] .group-data-\[side\=left\]\:-right-4{right:-1rem}.group[data-side=right] .group-data-\[side\=right\]\:left-0{left:0}.group[data-collapsible=icon] .group-data-\[collapsible\=icon\]\:-mt-8{margin-top:-2rem}.group[data-collapsible=icon] .group-data-\[collapsible\=icon\]\:hidden{display:none}.group[data-collapsible=icon] .group-data-\[collapsible\=icon\]\:\!size-8{width:2rem!important;height:2rem!important}.group[data-collapsible=icon] .group-data-\[collapsible\=icon\]\:w-\[--sidebar-width-icon\]{width:var(--sidebar-width-icon)}.group[data-collapsible=icon] .group-data-\[collapsible\=icon\]\:w-\[calc\(var\(--sidebar-width-icon\)_\+_theme\(spacing\.4\)\)\]{width:calc(var(--sidebar-width-icon) + 1rem)}.group[data-collapsible=icon] .group-data-\[collapsible\=icon\]\:w-\[calc\(var\(--sidebar-width-icon\)_\+_theme\(spacing\.4\)_\+2px\)\]{width:calc(var(--sidebar-width-icon) + 1rem + 2px)}.group[data-collapsible=offcanvas] .group-data-\[collapsible\=offcanvas\]\:w-0{width:0px}.group[data-collapsible=offcanvas] .group-data-\[collapsible\=offcanvas\]\:translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group[data-side=right] .group-data-\[side\=right\]\:rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group[data-collapsible=icon] .group-data-\[collapsible\=icon\]\:overflow-hidden{overflow:hidden}.group[data-variant=floating] .group-data-\[variant\=floating\]\:rounded-lg{border-radius:var(--radius)}.group[data-variant=floating] .group-data-\[variant\=floating\]\:border{border-width:1px}.group[data-side=left] .group-data-\[side\=left\]\:border-r{border-right-width:1px}.group[data-side=right] .group-data-\[side\=right\]\:border-l{border-left-width:1px}.group[data-variant=floating] .group-data-\[variant\=floating\]\:border-sidebar-border{border-color:hsl(var(--sidebar-border))}.group[data-collapsible=icon] .group-data-\[collapsible\=icon\]\:\!p-0{padding:0!important}.group[data-collapsible=icon] .group-data-\[collapsible\=icon\]\:\!p-2{padding:.5rem!important}.group[data-collapsible=icon] .group-data-\[collapsible\=icon\]\:opacity-0{opacity:0}.group[data-variant=floating] .group-data-\[variant\=floating\]\:shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.group[data-collapsible=offcanvas] .group-data-\[collapsible\=offcanvas\]\:after\:left-full:after{content:var(--tw-content);left:100%}.group[data-collapsible=offcanvas] .group-data-\[collapsible\=offcanvas\]\:hover\:bg-sidebar:hover{background-color:hsl(var(--sidebar-background))}.peer\/menu-button[data-size=default]~.peer-data-\[size\=default\]\/menu-button\:top-1\.5{top:.375rem}.peer\/menu-button[data-size=lg]~.peer-data-\[size\=lg\]\/menu-button\:top-2\.5{top:.625rem}.peer\/menu-button[data-size=sm]~.peer-data-\[size\=sm\]\/menu-button\:top-1{top:.25rem}.peer\/menu-button[data-active=true]~.peer-data-\[active\=true\]\/menu-button\:text-sidebar-accent-foreground{color:hsl(var(--sidebar-accent-foreground))}.dark\:border-destructive:is(.dark *){border-color:hsl(var(--destructive))}.dark\:text-emerald-400:is(.dark *){--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity, 1))}.dark\:text-emerald-400\/80:is(.dark *){color:#34d399cc}@media(min-width:640px){.sm\:col-span-2{grid-column:span 2 / span 2}.sm\:mx-0{margin-left:0;margin-right:0}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:inline-flex{display:inline-flex}.sm\:size-32{width:8rem;height:8rem}.sm\:min-w-\[300px\]{min-width:300px}.sm\:max-w-sm{max-width:24rem}.sm\:max-w-xl{max-width:36rem}.sm\:max-w-xs{max-width:20rem}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-start{align-items:flex-start}.sm\:items-end{align-items:flex-end}.sm\:items-center{align-items:center}.sm\:justify-end{justify-content:flex-end}.sm\:justify-between{justify-content:space-between}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:var(--radius)}.sm\:p-6{padding:1.5rem}.sm\:text-left{text-align:left}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media(min-width:768px){.md\:block{display:block}.md\:flex{display:flex}.md\:hidden{display:none}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:justify-between{justify-content:space-between}.md\:p-6{padding:1.5rem}.md\:p-8{padding:2rem}.md\:px-10{padding-left:2.5rem;padding-right:2.5rem}.md\:px-6{padding-left:1.5rem;padding-right:1.5rem}.md\:px-8{padding-left:2rem;padding-right:2rem}.md\:py-10{padding-top:2.5rem;padding-bottom:2.5rem}.md\:text-3xl{font-size:1.875rem;line-height:2.25rem}.md\:text-base{font-size:1rem;line-height:1.5rem}.md\:text-sm{font-size:.875rem;line-height:1.25rem}.md\:opacity-0{opacity:0}.after\:md\:hidden:after{content:var(--tw-content);display:none}.peer[data-variant=inset]~.md\:peer-data-\[variant\=inset\]\:m-2{margin:.5rem}.peer[data-state=collapsed][data-variant=inset]~.md\:peer-data-\[state\=collapsed\]\:peer-data-\[variant\=inset\]\:ml-2{margin-left:.5rem}.peer[data-variant=inset]~.md\:peer-data-\[variant\=inset\]\:ml-0{margin-left:0}.peer[data-variant=inset]~.md\:peer-data-\[variant\=inset\]\:rounded-xl{border-radius:.75rem}.peer[data-variant=inset]~.md\:peer-data-\[variant\=inset\]\:shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}}@media(min-width:1024px){.lg\:block{display:block}.lg\:grid{display:grid}.lg\:min-w-0{min-width:0px}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-\[220px_1fr\]{grid-template-columns:220px 1fr}.lg\:grid-cols-\[280px_1fr\]{grid-template-columns:280px 1fr}.lg\:grid-cols-\[minmax\(0\,280px\)_1fr\]{grid-template-columns:minmax(0,280px) 1fr}.lg\:grid-cols-\[minmax\(240px\,320px\)_1fr\]{grid-template-columns:minmax(240px,320px) 1fr}.lg\:overflow-visible{overflow:visible}.lg\:p-8{padding:2rem}}@media(min-width:1280px){.xl\:block{display:block}.xl\:grid-cols-\[minmax\(320px\,420px\)_1fr\]{grid-template-columns:minmax(320px,420px) 1fr}.xl\:flex-row{flex-direction:row}.xl\:items-start{align-items:flex-start}}.\[\&\>button\]\:hidden>button{display:none}.\[\&\>span\:last-child\]\:truncate>span:last-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.\[\&\>span\]\:line-clamp-1>span{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1}.\[\&\>svg\+div\]\:translate-y-\[-3px\]>svg+div{--tw-translate-y: -3px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.\[\&\>svg\]\:absolute>svg{position:absolute}.\[\&\>svg\]\:left-4>svg{left:1rem}.\[\&\>svg\]\:top-4>svg{top:1rem}.\[\&\>svg\]\:size-4>svg{width:1rem;height:1rem}.\[\&\>svg\]\:shrink-0>svg{flex-shrink:0}.\[\&\>svg\]\:text-destructive>svg{color:hsl(var(--destructive))}.\[\&\>svg\]\:text-foreground>svg{color:hsl(var(--foreground))}.\[\&\>svg\]\:text-sidebar-accent-foreground>svg{color:hsl(var(--sidebar-accent-foreground))}.\[\&\>svg\~\*\]\:pl-7>svg~*{padding-left:1.75rem}.\[\&_\[cmdk-group-heading\]\]\:px-2 [cmdk-group-heading]{padding-left:.5rem;padding-right:.5rem}.\[\&_\[cmdk-group-heading\]\]\:py-1\.5 [cmdk-group-heading]{padding-top:.375rem;padding-bottom:.375rem}.\[\&_\[cmdk-group-heading\]\]\:text-xs [cmdk-group-heading]{font-size:.75rem;line-height:1rem}.\[\&_\[cmdk-group-heading\]\]\:font-medium [cmdk-group-heading]{font-weight:500}.\[\&_\[cmdk-group-heading\]\]\:text-muted-foreground [cmdk-group-heading]{color:hsl(var(--muted-foreground))}.\[\&_\[cmdk-group\]\:not\(\[hidden\]\)_\~\[cmdk-group\]\]\:pt-0 [cmdk-group]:not([hidden])~[cmdk-group]{padding-top:0}.\[\&_\[cmdk-group\]\]\:px-2 [cmdk-group]{padding-left:.5rem;padding-right:.5rem}.\[\&_\[cmdk-input-wrapper\]_svg\]\:h-5 [cmdk-input-wrapper] svg{height:1.25rem}.\[\&_\[cmdk-input-wrapper\]_svg\]\:w-5 [cmdk-input-wrapper] svg{width:1.25rem}.\[\&_\[cmdk-input\]\]\:h-12 [cmdk-input]{height:3rem}.\[\&_\[cmdk-item\]\]\:px-2 [cmdk-item]{padding-left:.5rem;padding-right:.5rem}.\[\&_\[cmdk-item\]\]\:py-3 [cmdk-item]{padding-top:.75rem;padding-bottom:.75rem}.\[\&_\[cmdk-item\]_svg\]\:h-5 [cmdk-item] svg{height:1.25rem}.\[\&_\[cmdk-item\]_svg\]\:w-5 [cmdk-item] svg{width:1.25rem}.\[\&_p\]\:leading-relaxed p{line-height:1.625}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:size-4 svg{width:1rem;height:1rem}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}[data-side=left][data-collapsible=offcanvas] .\[\[data-side\=left\]\[data-collapsible\=offcanvas\]_\&\]\:-right-2{right:-.5rem}[data-side=left][data-state=collapsed] .\[\[data-side\=left\]\[data-state\=collapsed\]_\&\]\:cursor-e-resize{cursor:e-resize}[data-side=left] .\[\[data-side\=left\]_\&\]\:cursor-w-resize{cursor:w-resize}[data-side=right][data-collapsible=offcanvas] .\[\[data-side\=right\]\[data-collapsible\=offcanvas\]_\&\]\:-left-2{left:-.5rem}[data-side=right][data-state=collapsed] .\[\[data-side\=right\]\[data-state\=collapsed\]_\&\]\:cursor-w-resize{cursor:w-resize}[data-side=right] .\[\[data-side\=right\]_\&\]\:cursor-e-resize{cursor:e-resize} diff --git a/public/dashboard/assets/index-D6vuEh1I.js b/public/dashboard/assets/index-D6vuEh1I.js deleted file mode 100644 index aae1802..0000000 --- a/public/dashboard/assets/index-D6vuEh1I.js +++ /dev/null @@ -1,191 +0,0 @@ -var RN=Object.defineProperty;var AN=(e,n,a)=>n in e?RN(e,n,{enumerable:!0,configurable:!0,writable:!0,value:a}):e[n]=a;var m1=(e,n,a)=>AN(e,typeof n!="symbol"?n+"":n,a);function _N(e,n){for(var a=0;ao[l]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))o(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const d of i.addedNodes)d.tagName==="LINK"&&d.rel==="modulepreload"&&o(d)}).observe(document,{childList:!0,subtree:!0});function a(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function o(l){if(l.ep)return;l.ep=!0;const i=a(l);fetch(l.href,i)}})();function Xx(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Rm={exports:{}},Nl={};/** - * @license React - * react-jsx-runtime.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var p1;function MN(){if(p1)return Nl;p1=1;var e=Symbol.for("react.transitional.element"),n=Symbol.for("react.fragment");function a(o,l,i){var d=null;if(i!==void 0&&(d=""+i),l.key!==void 0&&(d=""+l.key),"key"in l){i={};for(var m in l)m!=="key"&&(i[m]=l[m])}else i=l;return l=i.ref,{$$typeof:e,type:o,key:d,ref:l!==void 0?l:null,props:i}}return Nl.Fragment=n,Nl.jsx=a,Nl.jsxs=a,Nl}var h1;function ON(){return h1||(h1=1,Rm.exports=MN()),Rm.exports}var c=ON(),Am={exports:{}},_e={};/** - * @license React - * react.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var g1;function DN(){if(g1)return _e;g1=1;var e=Symbol.for("react.transitional.element"),n=Symbol.for("react.portal"),a=Symbol.for("react.fragment"),o=Symbol.for("react.strict_mode"),l=Symbol.for("react.profiler"),i=Symbol.for("react.consumer"),d=Symbol.for("react.context"),m=Symbol.for("react.forward_ref"),h=Symbol.for("react.suspense"),p=Symbol.for("react.memo"),y=Symbol.for("react.lazy"),v=Symbol.for("react.activity"),x=Symbol.iterator;function S(_){return _===null||typeof _!="object"?null:(_=x&&_[x]||_["@@iterator"],typeof _=="function"?_:null)}var j={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},C=Object.assign,N={};function E(_,O,H){this.props=_,this.context=O,this.refs=N,this.updater=H||j}E.prototype.isReactComponent={},E.prototype.setState=function(_,O){if(typeof _!="object"&&typeof _!="function"&&_!=null)throw Error("takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,_,O,"setState")},E.prototype.forceUpdate=function(_){this.updater.enqueueForceUpdate(this,_,"forceUpdate")};function k(){}k.prototype=E.prototype;function A(_,O,H){this.props=_,this.context=O,this.refs=N,this.updater=H||j}var R=A.prototype=new k;R.constructor=A,C(R,E.prototype),R.isPureReactComponent=!0;var L=Array.isArray;function z(){}var B={H:null,A:null,T:null,S:null},U=Object.prototype.hasOwnProperty;function q(_,O,H){var I=H.ref;return{$$typeof:e,type:_,key:O,ref:I!==void 0?I:null,props:H}}function Z(_,O){return q(_.type,O,_.props)}function Q(_){return typeof _=="object"&&_!==null&&_.$$typeof===e}function fe(_){var O={"=":"=0",":":"=2"};return"$"+_.replace(/[=:]/g,function(H){return O[H]})}var de=/\/+/g;function ce(_,O){return typeof _=="object"&&_!==null&&_.key!=null?fe(""+_.key):O.toString(36)}function he(_){switch(_.status){case"fulfilled":return _.value;case"rejected":throw _.reason;default:switch(typeof _.status=="string"?_.then(z,z):(_.status="pending",_.then(function(O){_.status==="pending"&&(_.status="fulfilled",_.value=O)},function(O){_.status==="pending"&&(_.status="rejected",_.reason=O)})),_.status){case"fulfilled":return _.value;case"rejected":throw _.reason}}throw _}function M(_,O,H,I,V){var ne=typeof _;(ne==="undefined"||ne==="boolean")&&(_=null);var J=!1;if(_===null)J=!0;else switch(ne){case"bigint":case"string":case"number":J=!0;break;case"object":switch(_.$$typeof){case e:case n:J=!0;break;case y:return J=_._init,M(J(_._payload),O,H,I,V)}}if(J)return V=V(_),J=I===""?"."+ce(_,0):I,L(V)?(H="",J!=null&&(H=J.replace(de,"$&/")+"/"),M(V,O,H,"",function(G){return G})):V!=null&&(Q(V)&&(V=Z(V,H+(V.key==null||_&&_.key===V.key?"":(""+V.key).replace(de,"$&/")+"/")+J)),O.push(V)),1;J=0;var me=I===""?".":I+":";if(L(_))for(var pe=0;pe<_.length;pe++)I=_[pe],ne=me+ce(I,pe),J+=M(I,O,H,ne,V);else if(pe=S(_),typeof pe=="function")for(_=pe.call(_),pe=0;!(I=_.next()).done;)I=I.value,ne=me+ce(I,pe++),J+=M(I,O,H,ne,V);else if(ne==="object"){if(typeof _.then=="function")return M(he(_),O,H,I,V);throw O=String(_),Error("Objects are not valid as a React child (found: "+(O==="[object Object]"?"object with keys {"+Object.keys(_).join(", ")+"}":O)+"). If you meant to render a collection of children, use an array instead.")}return J}function K(_,O,H){if(_==null)return _;var I=[],V=0;return M(_,I,"","",function(ne){return O.call(H,ne,V++)}),I}function re(_){if(_._status===-1){var O=_._result;O=O(),O.then(function(H){(_._status===0||_._status===-1)&&(_._status=1,_._result=H)},function(H){(_._status===0||_._status===-1)&&(_._status=2,_._result=H)}),_._status===-1&&(_._status=0,_._result=O)}if(_._status===1)return _._result.default;throw _._result}var oe=typeof reportError=="function"?reportError:function(_){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var O=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof _=="object"&&_!==null&&typeof _.message=="string"?String(_.message):String(_),error:_});if(!window.dispatchEvent(O))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",_);return}console.error(_)},ee={map:K,forEach:function(_,O,H){K(_,function(){O.apply(this,arguments)},H)},count:function(_){var O=0;return K(_,function(){O++}),O},toArray:function(_){return K(_,function(O){return O})||[]},only:function(_){if(!Q(_))throw Error("React.Children.only expected to receive a single React element child.");return _}};return _e.Activity=v,_e.Children=ee,_e.Component=E,_e.Fragment=a,_e.Profiler=l,_e.PureComponent=A,_e.StrictMode=o,_e.Suspense=h,_e.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=B,_e.__COMPILER_RUNTIME={__proto__:null,c:function(_){return B.H.useMemoCache(_)}},_e.cache=function(_){return function(){return _.apply(null,arguments)}},_e.cacheSignal=function(){return null},_e.cloneElement=function(_,O,H){if(_==null)throw Error("The argument must be a React element, but you passed "+_+".");var I=C({},_.props),V=_.key;if(O!=null)for(ne in O.key!==void 0&&(V=""+O.key),O)!U.call(O,ne)||ne==="key"||ne==="__self"||ne==="__source"||ne==="ref"&&O.ref===void 0||(I[ne]=O[ne]);var ne=arguments.length-2;if(ne===1)I.children=H;else if(1>>1,ee=M[oe];if(0>>1;oe<_;){var O=2*(oe+1)-1,H=M[O],I=O+1,V=M[I];if(0>l(H,re))Il(V,H)?(M[oe]=V,M[I]=re,oe=I):(M[oe]=H,M[O]=re,oe=O);else if(Il(V,re))M[oe]=V,M[I]=re,oe=I;else break e}}return K}function l(M,K){var re=M.sortIndex-K.sortIndex;return re!==0?re:M.id-K.id}if(e.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var d=Date,m=d.now();e.unstable_now=function(){return d.now()-m}}var h=[],p=[],y=1,v=null,x=3,S=!1,j=!1,C=!1,N=!1,E=typeof setTimeout=="function"?setTimeout:null,k=typeof clearTimeout=="function"?clearTimeout:null,A=typeof setImmediate<"u"?setImmediate:null;function R(M){for(var K=a(p);K!==null;){if(K.callback===null)o(p);else if(K.startTime<=M)o(p),K.sortIndex=K.expirationTime,n(h,K);else break;K=a(p)}}function L(M){if(C=!1,R(M),!j)if(a(h)!==null)j=!0,z||(z=!0,fe());else{var K=a(p);K!==null&&he(L,K.startTime-M)}}var z=!1,B=-1,U=5,q=-1;function Z(){return N?!0:!(e.unstable_now()-qM&&Z());){var oe=v.callback;if(typeof oe=="function"){v.callback=null,x=v.priorityLevel;var ee=oe(v.expirationTime<=M);if(M=e.unstable_now(),typeof ee=="function"){v.callback=ee,R(M),K=!0;break t}v===a(h)&&o(h),R(M)}else o(h);v=a(h)}if(v!==null)K=!0;else{var _=a(p);_!==null&&he(L,_.startTime-M),K=!1}}break e}finally{v=null,x=re,S=!1}K=void 0}}finally{K?fe():z=!1}}}var fe;if(typeof A=="function")fe=function(){A(Q)};else if(typeof MessageChannel<"u"){var de=new MessageChannel,ce=de.port2;de.port1.onmessage=Q,fe=function(){ce.postMessage(null)}}else fe=function(){E(Q,0)};function he(M,K){B=E(function(){M(e.unstable_now())},K)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(M){M.callback=null},e.unstable_forceFrameRate=function(M){0>M||125oe?(M.sortIndex=re,n(p,M),a(h)===null&&M===a(p)&&(C?(k(B),B=-1):C=!0,he(L,re-oe))):(M.sortIndex=ee,n(h,M),j||S||(j=!0,z||(z=!0,fe()))),M},e.unstable_shouldYield=Z,e.unstable_wrapCallback=function(M){var K=x;return function(){var re=x;x=K;try{return M.apply(this,arguments)}finally{x=re}}}})(Om)),Om}var y1;function zN(){return y1||(y1=1,Mm.exports=LN()),Mm.exports}var Dm={exports:{}},Gt={};/** - * @license React - * react-dom.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var x1;function PN(){if(x1)return Gt;x1=1;var e=Cu();function n(h){var p="https://react.dev/errors/"+h;if(1"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(n){console.error(n)}}return e(),Dm.exports=PN(),Dm.exports}/** - * @license React - * react-dom-client.production.js - * - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var w1;function BN(){if(w1)return kl;w1=1;var e=zN(),n=Cu(),a=Zx();function o(t){var r="https://react.dev/errors/"+t;if(1ee||(t.current=oe[ee],oe[ee]=null,ee--)}function H(t,r){ee++,oe[ee]=t.current,t.current=r}var I=_(null),V=_(null),ne=_(null),J=_(null);function me(t,r){switch(H(ne,r),H(V,t),H(I,null),r.nodeType){case 9:case 11:t=(t=r.documentElement)&&(t=t.namespaceURI)?zy(t):0;break;default:if(t=r.tagName,r=r.namespaceURI)r=zy(r),t=Py(r,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}O(I),H(I,t)}function pe(){O(I),O(V),O(ne)}function G(t){t.memoizedState!==null&&H(J,t);var r=I.current,s=Py(r,t.type);r!==s&&(H(V,t),H(I,s))}function se(t){V.current===t&&(O(I),O(V)),J.current===t&&(O(J),wl._currentValue=re)}var ge,je;function Ae(t){if(ge===void 0)try{throw Error()}catch(s){var r=s.stack.trim().match(/\n( *(at )?)/);ge=r&&r[1]||"",je=-1)":-1g||D[u]!==Y[g]){var te=` -`+D[u].replace(" at new "," at ");return t.displayName&&te.includes("")&&(te=te.replace("",t.displayName)),te}while(1<=u&&0<=g);break}}}finally{Qe=!1,Error.prepareStackTrace=s}return(s=t?t.displayName||t.name:"")?Ae(s):""}function Tt(t,r){switch(t.tag){case 26:case 27:case 5:return Ae(t.type);case 16:return Ae("Lazy");case 13:return t.child!==r&&r!==null?Ae("Suspense Fallback"):Ae("Suspense");case 19:return Ae("SuspenseList");case 0:case 15:return ct(t.type,!1);case 11:return ct(t.type.render,!1);case 1:return ct(t.type,!0);case 31:return Ae("Activity");default:return""}}function Ca(t){try{var r="",s=null;do r+=Tt(t,s),s=t,t=t.return;while(t);return r}catch(u){return` -Error generating stack: `+u.message+` -`+u.stack}}var Rt=Object.prototype.hasOwnProperty,Os=e.unstable_scheduleCallback,Ds=e.unstable_cancelCallback,Yt=e.unstable_shouldYield,$r=e.unstable_requestPaint,Wt=e.unstable_now,gd=e.unstable_getCurrentPriorityLevel,ja=e.unstable_ImmediatePriority,xi=e.unstable_UserBlockingPriority,Ea=e.unstable_NormalPriority,Ls=e.unstable_LowPriority,dr=e.unstable_IdlePriority,Si=e.log,Ir=e.unstable_setDisableYieldValue,Na=null,qt=null;function $n(t){if(typeof Si=="function"&&Ir(t),qt&&typeof qt.setStrictMode=="function")try{qt.setStrictMode(Na,t)}catch{}}var Ht=Math.clz32?Math.clz32:Xn,vd=Math.log,zs=Math.LN2;function Xn(t){return t>>>=0,t===0?32:31-(vd(t)/zs|0)|0}var fo=256,mo=262144,ka=4194304;function Zn(t){var r=t&42;if(r!==0)return r;switch(t&-t){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return t&261888;case 262144:case 524288:case 1048576:case 2097152:return t&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return t&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return t}}function Te(t,r,s){var u=t.pendingLanes;if(u===0)return 0;var g=0,b=t.suspendedLanes,w=t.pingedLanes;t=t.warmLanes;var T=u&134217727;return T!==0?(u=T&~b,u!==0?g=Zn(u):(w&=T,w!==0?g=Zn(w):s||(s=T&~t,s!==0&&(g=Zn(s))))):(T=u&~b,T!==0?g=Zn(T):w!==0?g=Zn(w):s||(s=u&~t,s!==0&&(g=Zn(s)))),g===0?0:r!==0&&r!==g&&(r&b)===0&&(b=g&-g,s=r&-r,b>=s||b===32&&(s&4194048)!==0)?r:g}function ut(t,r){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&r)===0}function At(t,r){switch(t){case 1:case 2:case 4:case 8:case 64:return r+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return r+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Vt(){var t=ka;return ka<<=1,(ka&62914560)===0&&(ka=4194304),t}function Ur(t){for(var r=[],s=0;31>s;s++)r.push(t);return r}function mt(t,r){t.pendingLanes|=r,r!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function Zt(t,r,s,u,g,b){var w=t.pendingLanes;t.pendingLanes=s,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=s,t.entangledLanes&=s,t.errorRecoveryDisabledLanes&=s,t.shellSuspendCounter=0;var T=t.entanglements,D=t.expirationTimes,Y=t.hiddenUpdates;for(s=w&~s;0"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var C5=/[\n"\\]/g;function En(t){return t.replace(C5,function(r){return"\\"+r.charCodeAt(0).toString(16)+" "})}function wd(t,r,s,u,g,b,w,T){t.name="",w!=null&&typeof w!="function"&&typeof w!="symbol"&&typeof w!="boolean"?t.type=w:t.removeAttribute("type"),r!=null?w==="number"?(r===0&&t.value===""||t.value!=r)&&(t.value=""+jn(r)):t.value!==""+jn(r)&&(t.value=""+jn(r)):w!=="submit"&&w!=="reset"||t.removeAttribute("value"),r!=null?Cd(t,w,jn(r)):s!=null?Cd(t,w,jn(s)):u!=null&&t.removeAttribute("value"),g==null&&b!=null&&(t.defaultChecked=!!b),g!=null&&(t.checked=g&&typeof g!="function"&&typeof g!="symbol"),T!=null&&typeof T!="function"&&typeof T!="symbol"&&typeof T!="boolean"?t.name=""+jn(T):t.removeAttribute("name")}function Ag(t,r,s,u,g,b,w,T){if(b!=null&&typeof b!="function"&&typeof b!="symbol"&&typeof b!="boolean"&&(t.type=b),r!=null||s!=null){if(!(b!=="submit"&&b!=="reset"||r!=null)){Sd(t);return}s=s!=null?""+jn(s):"",r=r!=null?""+jn(r):s,T||r===t.value||(t.value=r),t.defaultValue=r}u=u??g,u=typeof u!="function"&&typeof u!="symbol"&&!!u,t.checked=T?t.checked:!!u,t.defaultChecked=!!u,w!=null&&typeof w!="function"&&typeof w!="symbol"&&typeof w!="boolean"&&(t.name=w),Sd(t)}function Cd(t,r,s){r==="number"&&ji(t.ownerDocument)===t||t.defaultValue===""+s||(t.defaultValue=""+s)}function xo(t,r,s,u){if(t=t.options,r){r={};for(var g=0;g"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Td=!1;if(pr)try{var Is={};Object.defineProperty(Is,"passive",{get:function(){Td=!0}}),window.addEventListener("test",Is,Is),window.removeEventListener("test",Is,Is)}catch{Td=!1}var Vr=null,Rd=null,Ni=null;function Pg(){if(Ni)return Ni;var t,r=Rd,s=r.length,u,g="value"in Vr?Vr.value:Vr.textContent,b=g.length;for(t=0;t=Vs),Vg=" ",Gg=!1;function Fg(t,r){switch(t){case"keyup":return Z5.indexOf(r.keyCode)!==-1;case"keydown":return r.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Yg(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var jo=!1;function J5(t,r){switch(t){case"compositionend":return Yg(r);case"keypress":return r.which!==32?null:(Gg=!0,Vg);case"textInput":return t=r.data,t===Vg&&Gg?null:t;default:return null}}function eE(t,r){if(jo)return t==="compositionend"||!Dd&&Fg(t,r)?(t=Pg(),Ni=Rd=Vr=null,jo=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(r.ctrlKey||r.altKey||r.metaKey)||r.ctrlKey&&r.altKey){if(r.char&&1=r)return{node:s,offset:r-t};t=u}e:{for(;s;){if(s.nextSibling){s=s.nextSibling;break e}s=s.parentNode}s=void 0}s=ev(s)}}function nv(t,r){return t&&r?t===r?!0:t&&t.nodeType===3?!1:r&&r.nodeType===3?nv(t,r.parentNode):"contains"in t?t.contains(r):t.compareDocumentPosition?!!(t.compareDocumentPosition(r)&16):!1:!1}function rv(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var r=ji(t.document);r instanceof t.HTMLIFrameElement;){try{var s=typeof r.contentWindow.location.href=="string"}catch{s=!1}if(s)t=r.contentWindow;else break;r=ji(t.document)}return r}function Pd(t){var r=t&&t.nodeName&&t.nodeName.toLowerCase();return r&&(r==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||r==="textarea"||t.contentEditable==="true")}var iE=pr&&"documentMode"in document&&11>=document.documentMode,Eo=null,Bd=null,Ws=null,$d=!1;function av(t,r,s){var u=s.window===s?s.document:s.nodeType===9?s:s.ownerDocument;$d||Eo==null||Eo!==ji(u)||(u=Eo,"selectionStart"in u&&Pd(u)?u={start:u.selectionStart,end:u.selectionEnd}:(u=(u.ownerDocument&&u.ownerDocument.defaultView||window).getSelection(),u={anchorNode:u.anchorNode,anchorOffset:u.anchorOffset,focusNode:u.focusNode,focusOffset:u.focusOffset}),Ws&&Ys(Ws,u)||(Ws=u,u=yc(Bd,"onSelect"),0>=w,g-=w,Qn=1<<32-Ht(r)+g|s<Oe?($e=Ce,Ce=null):$e=Ce.sibling;var Ge=W($,Ce,F[Oe],ae);if(Ge===null){Ce===null&&(Ce=$e);break}t&&Ce&&Ge.alternate===null&&r($,Ce),P=b(Ge,P,Oe),Ve===null?Ee=Ge:Ve.sibling=Ge,Ve=Ge,Ce=$e}if(Oe===F.length)return s($,Ce),Ie&&gr($,Oe),Ee;if(Ce===null){for(;OeOe?($e=Ce,Ce=null):$e=Ce.sibling;var da=W($,Ce,Ge.value,ae);if(da===null){Ce===null&&(Ce=$e);break}t&&Ce&&da.alternate===null&&r($,Ce),P=b(da,P,Oe),Ve===null?Ee=da:Ve.sibling=da,Ve=da,Ce=$e}if(Ge.done)return s($,Ce),Ie&&gr($,Oe),Ee;if(Ce===null){for(;!Ge.done;Oe++,Ge=F.next())Ge=le($,Ge.value,ae),Ge!==null&&(P=b(Ge,P,Oe),Ve===null?Ee=Ge:Ve.sibling=Ge,Ve=Ge);return Ie&&gr($,Oe),Ee}for(Ce=u(Ce);!Ge.done;Oe++,Ge=F.next())Ge=X(Ce,$,Oe,Ge.value,ae),Ge!==null&&(t&&Ge.alternate!==null&&Ce.delete(Ge.key===null?Oe:Ge.key),P=b(Ge,P,Oe),Ve===null?Ee=Ge:Ve.sibling=Ge,Ve=Ge);return t&&Ce.forEach(function(TN){return r($,TN)}),Ie&&gr($,Oe),Ee}function tt($,P,F,ae){if(typeof F=="object"&&F!==null&&F.type===C&&F.key===null&&(F=F.props.children),typeof F=="object"&&F!==null){switch(F.$$typeof){case S:e:{for(var Ee=F.key;P!==null;){if(P.key===Ee){if(Ee=F.type,Ee===C){if(P.tag===7){s($,P.sibling),ae=g(P,F.props.children),ae.return=$,$=ae;break e}}else if(P.elementType===Ee||typeof Ee=="object"&&Ee!==null&&Ee.$$typeof===U&&$a(Ee)===P.type){s($,P.sibling),ae=g(P,F.props),Js(ae,F),ae.return=$,$=ae;break e}s($,P);break}else r($,P);P=P.sibling}F.type===C?(ae=Da(F.props.children,$.mode,ae,F.key),ae.return=$,$=ae):(ae=zi(F.type,F.key,F.props,null,$.mode,ae),Js(ae,F),ae.return=$,$=ae)}return w($);case j:e:{for(Ee=F.key;P!==null;){if(P.key===Ee)if(P.tag===4&&P.stateNode.containerInfo===F.containerInfo&&P.stateNode.implementation===F.implementation){s($,P.sibling),ae=g(P,F.children||[]),ae.return=$,$=ae;break e}else{s($,P);break}else r($,P);P=P.sibling}ae=Yd(F,$.mode,ae),ae.return=$,$=ae}return w($);case U:return F=$a(F),tt($,P,F,ae)}if(he(F))return we($,P,F,ae);if(fe(F)){if(Ee=fe(F),typeof Ee!="function")throw Error(o(150));return F=Ee.call(F),Ne($,P,F,ae)}if(typeof F.then=="function")return tt($,P,Vi(F),ae);if(F.$$typeof===A)return tt($,P,$i($,F),ae);Gi($,F)}return typeof F=="string"&&F!==""||typeof F=="number"||typeof F=="bigint"?(F=""+F,P!==null&&P.tag===6?(s($,P.sibling),ae=g(P,F),ae.return=$,$=ae):(s($,P),ae=Fd(F,$.mode,ae),ae.return=$,$=ae),w($)):s($,P)}return function($,P,F,ae){try{Qs=0;var Ee=tt($,P,F,ae);return zo=null,Ee}catch(Ce){if(Ce===Lo||Ce===Ui)throw Ce;var Ve=fn(29,Ce,null,$.mode);return Ve.lanes=ae,Ve.return=$,Ve}finally{}}}var Ua=kv(!0),Tv=kv(!1),qr=!1;function af(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function of(t,r){t=t.updateQueue,r.updateQueue===t&&(r.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,callbacks:null})}function Kr(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function Xr(t,r,s){var u=t.updateQueue;if(u===null)return null;if(u=u.shared,(Ye&2)!==0){var g=u.pending;return g===null?r.next=r:(r.next=g.next,g.next=r),u.pending=r,r=Li(t),dv(t,null,s),r}return Di(t,u,r,s),Li(t)}function el(t,r,s){if(r=r.updateQueue,r!==null&&(r=r.shared,(s&4194048)!==0)){var u=r.lanes;u&=t.pendingLanes,s|=u,r.lanes=s,Qt(t,s)}}function sf(t,r){var s=t.updateQueue,u=t.alternate;if(u!==null&&(u=u.updateQueue,s===u)){var g=null,b=null;if(s=s.firstBaseUpdate,s!==null){do{var w={lane:s.lane,tag:s.tag,payload:s.payload,callback:null,next:null};b===null?g=b=w:b=b.next=w,s=s.next}while(s!==null);b===null?g=b=r:b=b.next=r}else g=b=r;s={baseState:u.baseState,firstBaseUpdate:g,lastBaseUpdate:b,shared:u.shared,callbacks:u.callbacks},t.updateQueue=s;return}t=s.lastBaseUpdate,t===null?s.firstBaseUpdate=r:t.next=r,s.lastBaseUpdate=r}var lf=!1;function tl(){if(lf){var t=Do;if(t!==null)throw t}}function nl(t,r,s,u){lf=!1;var g=t.updateQueue;qr=!1;var b=g.firstBaseUpdate,w=g.lastBaseUpdate,T=g.shared.pending;if(T!==null){g.shared.pending=null;var D=T,Y=D.next;D.next=null,w===null?b=Y:w.next=Y,w=D;var te=t.alternate;te!==null&&(te=te.updateQueue,T=te.lastBaseUpdate,T!==w&&(T===null?te.firstBaseUpdate=Y:T.next=Y,te.lastBaseUpdate=D))}if(b!==null){var le=g.baseState;w=0,te=Y=D=null,T=b;do{var W=T.lane&-536870913,X=W!==T.lane;if(X?(Be&W)===W:(u&W)===W){W!==0&&W===Oo&&(lf=!0),te!==null&&(te=te.next={lane:0,tag:T.tag,payload:T.payload,callback:null,next:null});e:{var we=t,Ne=T;W=r;var tt=s;switch(Ne.tag){case 1:if(we=Ne.payload,typeof we=="function"){le=we.call(tt,le,W);break e}le=we;break e;case 3:we.flags=we.flags&-65537|128;case 0:if(we=Ne.payload,W=typeof we=="function"?we.call(tt,le,W):we,W==null)break e;le=v({},le,W);break e;case 2:qr=!0}}W=T.callback,W!==null&&(t.flags|=64,X&&(t.flags|=8192),X=g.callbacks,X===null?g.callbacks=[W]:X.push(W))}else X={lane:W,tag:T.tag,payload:T.payload,callback:T.callback,next:null},te===null?(Y=te=X,D=le):te=te.next=X,w|=W;if(T=T.next,T===null){if(T=g.shared.pending,T===null)break;X=T,T=X.next,X.next=null,g.lastBaseUpdate=X,g.shared.pending=null}}while(!0);te===null&&(D=le),g.baseState=D,g.firstBaseUpdate=Y,g.lastBaseUpdate=te,b===null&&(g.shared.lanes=0),ta|=w,t.lanes=w,t.memoizedState=le}}function Rv(t,r){if(typeof t!="function")throw Error(o(191,t));t.call(r)}function Av(t,r){var s=t.callbacks;if(s!==null)for(t.callbacks=null,t=0;tb?b:8;var w=M.T,T={};M.T=T,kf(t,!1,r,s);try{var D=g(),Y=M.S;if(Y!==null&&Y(T,D),D!==null&&typeof D=="object"&&typeof D.then=="function"){var te=vE(D,u);ol(t,r,te,vn(t))}else ol(t,r,u,vn(t))}catch(le){ol(t,r,{then:function(){},status:"rejected",reason:le},vn())}finally{K.p=b,w!==null&&T.types!==null&&(w.types=T.types),M.T=w}}function CE(){}function Ef(t,r,s,u){if(t.tag!==5)throw Error(o(476));var g=ib(t).queue;lb(t,g,r,re,s===null?CE:function(){return cb(t),s(u)})}function ib(t){var r=t.memoizedState;if(r!==null)return r;r={memoizedState:re,baseState:re,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:xr,lastRenderedState:re},next:null};var s={};return r.next={memoizedState:s,baseState:s,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:xr,lastRenderedState:s},next:null},t.memoizedState=r,t=t.alternate,t!==null&&(t.memoizedState=r),r}function cb(t){var r=ib(t);r.next===null&&(r=t.alternate.memoizedState),ol(t,r.next.queue,{},vn())}function Nf(){return Pt(wl)}function ub(){return bt().memoizedState}function db(){return bt().memoizedState}function jE(t){for(var r=t.return;r!==null;){switch(r.tag){case 24:case 3:var s=vn();t=Kr(s);var u=Xr(r,t,s);u!==null&&(sn(u,r,s),el(u,r,s)),r={cache:ef()},t.payload=r;return}r=r.return}}function EE(t,r,s){var u=vn();s={lane:u,revertLane:0,gesture:null,action:s,hasEagerState:!1,eagerState:null,next:null},ec(t)?mb(r,s):(s=Vd(t,r,s,u),s!==null&&(sn(s,t,u),pb(s,r,u)))}function fb(t,r,s){var u=vn();ol(t,r,s,u)}function ol(t,r,s,u){var g={lane:u,revertLane:0,gesture:null,action:s,hasEagerState:!1,eagerState:null,next:null};if(ec(t))mb(r,g);else{var b=t.alternate;if(t.lanes===0&&(b===null||b.lanes===0)&&(b=r.lastRenderedReducer,b!==null))try{var w=r.lastRenderedState,T=b(w,s);if(g.hasEagerState=!0,g.eagerState=T,dn(T,w))return Di(t,r,g,0),nt===null&&Oi(),!1}catch{}finally{}if(s=Vd(t,r,g,u),s!==null)return sn(s,t,u),pb(s,r,u),!0}return!1}function kf(t,r,s,u){if(u={lane:2,revertLane:om(),gesture:null,action:u,hasEagerState:!1,eagerState:null,next:null},ec(t)){if(r)throw Error(o(479))}else r=Vd(t,s,u,2),r!==null&&sn(r,t,2)}function ec(t){var r=t.alternate;return t===Me||r!==null&&r===Me}function mb(t,r){Bo=Wi=!0;var s=t.pending;s===null?r.next=r:(r.next=s.next,s.next=r),t.pending=r}function pb(t,r,s){if((s&4194048)!==0){var u=r.lanes;u&=t.pendingLanes,s|=u,r.lanes=s,Qt(t,s)}}var sl={readContext:Pt,use:Xi,useCallback:ht,useContext:ht,useEffect:ht,useImperativeHandle:ht,useLayoutEffect:ht,useInsertionEffect:ht,useMemo:ht,useReducer:ht,useRef:ht,useState:ht,useDebugValue:ht,useDeferredValue:ht,useTransition:ht,useSyncExternalStore:ht,useId:ht,useHostTransitionStatus:ht,useFormState:ht,useActionState:ht,useOptimistic:ht,useMemoCache:ht,useCacheRefresh:ht};sl.useEffectEvent=ht;var hb={readContext:Pt,use:Xi,useCallback:function(t,r){return Kt().memoizedState=[t,r===void 0?null:r],t},useContext:Pt,useEffect:Qv,useImperativeHandle:function(t,r,s){s=s!=null?s.concat([t]):null,Qi(4194308,4,nb.bind(null,r,t),s)},useLayoutEffect:function(t,r){return Qi(4194308,4,t,r)},useInsertionEffect:function(t,r){Qi(4,2,t,r)},useMemo:function(t,r){var s=Kt();r=r===void 0?null:r;var u=t();if(Ha){$n(!0);try{t()}finally{$n(!1)}}return s.memoizedState=[u,r],u},useReducer:function(t,r,s){var u=Kt();if(s!==void 0){var g=s(r);if(Ha){$n(!0);try{s(r)}finally{$n(!1)}}}else g=r;return u.memoizedState=u.baseState=g,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:g},u.queue=t,t=t.dispatch=EE.bind(null,Me,t),[u.memoizedState,t]},useRef:function(t){var r=Kt();return t={current:t},r.memoizedState=t},useState:function(t){t=xf(t);var r=t.queue,s=fb.bind(null,Me,r);return r.dispatch=s,[t.memoizedState,s]},useDebugValue:Cf,useDeferredValue:function(t,r){var s=Kt();return jf(s,t,r)},useTransition:function(){var t=xf(!1);return t=lb.bind(null,Me,t.queue,!0,!1),Kt().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,r,s){var u=Me,g=Kt();if(Ie){if(s===void 0)throw Error(o(407));s=s()}else{if(s=r(),nt===null)throw Error(o(349));(Be&127)!==0||zv(u,r,s)}g.memoizedState=s;var b={value:s,getSnapshot:r};return g.queue=b,Qv(Bv.bind(null,u,b,t),[t]),u.flags|=2048,Io(9,{destroy:void 0},Pv.bind(null,u,b,s,r),null),s},useId:function(){var t=Kt(),r=nt.identifierPrefix;if(Ie){var s=Jn,u=Qn;s=(u&~(1<<32-Ht(u)-1)).toString(32)+s,r="_"+r+"R_"+s,s=qi++,0<\/script>",b=b.removeChild(b.firstChild);break;case"select":b=typeof u.is=="string"?w.createElement("select",{is:u.is}):w.createElement("select"),u.multiple?b.multiple=!0:u.size&&(b.size=u.size);break;default:b=typeof u.is=="string"?w.createElement(g,{is:u.is}):w.createElement(g)}}b[Lt]=r,b[en]=u;e:for(w=r.child;w!==null;){if(w.tag===5||w.tag===6)b.appendChild(w.stateNode);else if(w.tag!==4&&w.tag!==27&&w.child!==null){w.child.return=w,w=w.child;continue}if(w===r)break e;for(;w.sibling===null;){if(w.return===null||w.return===r)break e;w=w.return}w.sibling.return=w.return,w=w.sibling}r.stateNode=b;e:switch($t(b,g,u),g){case"button":case"input":case"select":case"textarea":u=!!u.autoFocus;break e;case"img":u=!0;break e;default:u=!1}u&&wr(r)}}return it(r),Uf(r,r.type,t===null?null:t.memoizedProps,r.pendingProps,s),null;case 6:if(t&&r.stateNode!=null)t.memoizedProps!==u&&wr(r);else{if(typeof u!="string"&&r.stateNode===null)throw Error(o(166));if(t=ne.current,_o(r)){if(t=r.stateNode,s=r.memoizedProps,u=null,g=zt,g!==null)switch(g.tag){case 27:case 5:u=g.memoizedProps}t[Lt]=r,t=!!(t.nodeValue===s||u!==null&&u.suppressHydrationWarning===!0||Dy(t.nodeValue,s)),t||Yr(r,!0)}else t=xc(t).createTextNode(u),t[Lt]=r,r.stateNode=t}return it(r),null;case 31:if(s=r.memoizedState,t===null||t.memoizedState!==null){if(u=_o(r),s!==null){if(t===null){if(!u)throw Error(o(318));if(t=r.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(o(557));t[Lt]=r}else La(),(r.flags&128)===0&&(r.memoizedState=null),r.flags|=4;it(r),t=!1}else s=Xd(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=s),t=!0;if(!t)return r.flags&256?(pn(r),r):(pn(r),null);if((r.flags&128)!==0)throw Error(o(558))}return it(r),null;case 13:if(u=r.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(g=_o(r),u!==null&&u.dehydrated!==null){if(t===null){if(!g)throw Error(o(318));if(g=r.memoizedState,g=g!==null?g.dehydrated:null,!g)throw Error(o(317));g[Lt]=r}else La(),(r.flags&128)===0&&(r.memoizedState=null),r.flags|=4;it(r),g=!1}else g=Xd(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=g),g=!0;if(!g)return r.flags&256?(pn(r),r):(pn(r),null)}return pn(r),(r.flags&128)!==0?(r.lanes=s,r):(s=u!==null,t=t!==null&&t.memoizedState!==null,s&&(u=r.child,g=null,u.alternate!==null&&u.alternate.memoizedState!==null&&u.alternate.memoizedState.cachePool!==null&&(g=u.alternate.memoizedState.cachePool.pool),b=null,u.memoizedState!==null&&u.memoizedState.cachePool!==null&&(b=u.memoizedState.cachePool.pool),b!==g&&(u.flags|=2048)),s!==t&&s&&(r.child.flags|=8192),oc(r,r.updateQueue),it(r),null);case 4:return pe(),t===null&&cm(r.stateNode.containerInfo),it(r),null;case 10:return br(r.type),it(r),null;case 19:if(O(vt),u=r.memoizedState,u===null)return it(r),null;if(g=(r.flags&128)!==0,b=u.rendering,b===null)if(g)il(u,!1);else{if(gt!==0||t!==null&&(t.flags&128)!==0)for(t=r.child;t!==null;){if(b=Yi(t),b!==null){for(r.flags|=128,il(u,!1),t=b.updateQueue,r.updateQueue=t,oc(r,t),r.subtreeFlags=0,t=s,s=r.child;s!==null;)fv(s,t),s=s.sibling;return H(vt,vt.current&1|2),Ie&&gr(r,u.treeForkCount),r.child}t=t.sibling}u.tail!==null&&Wt()>uc&&(r.flags|=128,g=!0,il(u,!1),r.lanes=4194304)}else{if(!g)if(t=Yi(b),t!==null){if(r.flags|=128,g=!0,t=t.updateQueue,r.updateQueue=t,oc(r,t),il(u,!0),u.tail===null&&u.tailMode==="hidden"&&!b.alternate&&!Ie)return it(r),null}else 2*Wt()-u.renderingStartTime>uc&&s!==536870912&&(r.flags|=128,g=!0,il(u,!1),r.lanes=4194304);u.isBackwards?(b.sibling=r.child,r.child=b):(t=u.last,t!==null?t.sibling=b:r.child=b,u.last=b)}return u.tail!==null?(t=u.tail,u.rendering=t,u.tail=t.sibling,u.renderingStartTime=Wt(),t.sibling=null,s=vt.current,H(vt,g?s&1|2:s&1),Ie&&gr(r,u.treeForkCount),t):(it(r),null);case 22:case 23:return pn(r),uf(),u=r.memoizedState!==null,t!==null?t.memoizedState!==null!==u&&(r.flags|=8192):u&&(r.flags|=8192),u?(s&536870912)!==0&&(r.flags&128)===0&&(it(r),r.subtreeFlags&6&&(r.flags|=8192)):it(r),s=r.updateQueue,s!==null&&oc(r,s.retryQueue),s=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(s=t.memoizedState.cachePool.pool),u=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(u=r.memoizedState.cachePool.pool),u!==s&&(r.flags|=2048),t!==null&&O(Ba),null;case 24:return s=null,t!==null&&(s=t.memoizedState.cache),r.memoizedState.cache!==s&&(r.flags|=2048),br(yt),it(r),null;case 25:return null;case 30:return null}throw Error(o(156,r.tag))}function AE(t,r){switch(qd(r),r.tag){case 1:return t=r.flags,t&65536?(r.flags=t&-65537|128,r):null;case 3:return br(yt),pe(),t=r.flags,(t&65536)!==0&&(t&128)===0?(r.flags=t&-65537|128,r):null;case 26:case 27:case 5:return se(r),null;case 31:if(r.memoizedState!==null){if(pn(r),r.alternate===null)throw Error(o(340));La()}return t=r.flags,t&65536?(r.flags=t&-65537|128,r):null;case 13:if(pn(r),t=r.memoizedState,t!==null&&t.dehydrated!==null){if(r.alternate===null)throw Error(o(340));La()}return t=r.flags,t&65536?(r.flags=t&-65537|128,r):null;case 19:return O(vt),null;case 4:return pe(),null;case 10:return br(r.type),null;case 22:case 23:return pn(r),uf(),t!==null&&O(Ba),t=r.flags,t&65536?(r.flags=t&-65537|128,r):null;case 24:return br(yt),null;case 25:return null;default:return null}}function $b(t,r){switch(qd(r),r.tag){case 3:br(yt),pe();break;case 26:case 27:case 5:se(r);break;case 4:pe();break;case 31:r.memoizedState!==null&&pn(r);break;case 13:pn(r);break;case 19:O(vt);break;case 10:br(r.type);break;case 22:case 23:pn(r),uf(),t!==null&&O(Ba);break;case 24:br(yt)}}function cl(t,r){try{var s=r.updateQueue,u=s!==null?s.lastEffect:null;if(u!==null){var g=u.next;s=g;do{if((s.tag&t)===t){u=void 0;var b=s.create,w=s.inst;u=b(),w.destroy=u}s=s.next}while(s!==g)}}catch(T){Xe(r,r.return,T)}}function Jr(t,r,s){try{var u=r.updateQueue,g=u!==null?u.lastEffect:null;if(g!==null){var b=g.next;u=b;do{if((u.tag&t)===t){var w=u.inst,T=w.destroy;if(T!==void 0){w.destroy=void 0,g=r;var D=s,Y=T;try{Y()}catch(te){Xe(g,D,te)}}}u=u.next}while(u!==b)}}catch(te){Xe(r,r.return,te)}}function Ib(t){var r=t.updateQueue;if(r!==null){var s=t.stateNode;try{Av(r,s)}catch(u){Xe(t,t.return,u)}}}function Ub(t,r,s){s.props=Va(t.type,t.memoizedProps),s.state=t.memoizedState;try{s.componentWillUnmount()}catch(u){Xe(t,r,u)}}function ul(t,r){try{var s=t.ref;if(s!==null){switch(t.tag){case 26:case 27:case 5:var u=t.stateNode;break;case 30:u=t.stateNode;break;default:u=t.stateNode}typeof s=="function"?t.refCleanup=s(u):s.current=u}}catch(g){Xe(t,r,g)}}function er(t,r){var s=t.ref,u=t.refCleanup;if(s!==null)if(typeof u=="function")try{u()}catch(g){Xe(t,r,g)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof s=="function")try{s(null)}catch(g){Xe(t,r,g)}else s.current=null}function Hb(t){var r=t.type,s=t.memoizedProps,u=t.stateNode;try{e:switch(r){case"button":case"input":case"select":case"textarea":s.autoFocus&&u.focus();break e;case"img":s.src?u.src=s.src:s.srcSet&&(u.srcset=s.srcSet)}}catch(g){Xe(t,t.return,g)}}function Hf(t,r,s){try{var u=t.stateNode;QE(u,t.type,s,r),u[en]=r}catch(g){Xe(t,t.return,g)}}function Vb(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&sa(t.type)||t.tag===4}function Vf(t){e:for(;;){for(;t.sibling===null;){if(t.return===null||Vb(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.tag===27&&sa(t.type)||t.flags&2||t.child===null||t.tag===4)continue e;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function Gf(t,r,s){var u=t.tag;if(u===5||u===6)t=t.stateNode,r?(s.nodeType===9?s.body:s.nodeName==="HTML"?s.ownerDocument.body:s).insertBefore(t,r):(r=s.nodeType===9?s.body:s.nodeName==="HTML"?s.ownerDocument.body:s,r.appendChild(t),s=s._reactRootContainer,s!=null||r.onclick!==null||(r.onclick=mr));else if(u!==4&&(u===27&&sa(t.type)&&(s=t.stateNode,r=null),t=t.child,t!==null))for(Gf(t,r,s),t=t.sibling;t!==null;)Gf(t,r,s),t=t.sibling}function sc(t,r,s){var u=t.tag;if(u===5||u===6)t=t.stateNode,r?s.insertBefore(t,r):s.appendChild(t);else if(u!==4&&(u===27&&sa(t.type)&&(s=t.stateNode),t=t.child,t!==null))for(sc(t,r,s),t=t.sibling;t!==null;)sc(t,r,s),t=t.sibling}function Gb(t){var r=t.stateNode,s=t.memoizedProps;try{for(var u=t.type,g=r.attributes;g.length;)r.removeAttributeNode(g[0]);$t(r,u,s),r[Lt]=t,r[en]=s}catch(b){Xe(t,t.return,b)}}var Cr=!1,wt=!1,Ff=!1,Fb=typeof WeakSet=="function"?WeakSet:Set,Mt=null;function _E(t,r){if(t=t.containerInfo,fm=kc,t=rv(t),Pd(t)){if("selectionStart"in t)var s={start:t.selectionStart,end:t.selectionEnd};else e:{s=(s=t.ownerDocument)&&s.defaultView||window;var u=s.getSelection&&s.getSelection();if(u&&u.rangeCount!==0){s=u.anchorNode;var g=u.anchorOffset,b=u.focusNode;u=u.focusOffset;try{s.nodeType,b.nodeType}catch{s=null;break e}var w=0,T=-1,D=-1,Y=0,te=0,le=t,W=null;t:for(;;){for(var X;le!==s||g!==0&&le.nodeType!==3||(T=w+g),le!==b||u!==0&&le.nodeType!==3||(D=w+u),le.nodeType===3&&(w+=le.nodeValue.length),(X=le.firstChild)!==null;)W=le,le=X;for(;;){if(le===t)break t;if(W===s&&++Y===g&&(T=w),W===b&&++te===u&&(D=w),(X=le.nextSibling)!==null)break;le=W,W=le.parentNode}le=X}s=T===-1||D===-1?null:{start:T,end:D}}else s=null}s=s||{start:0,end:0}}else s=null;for(mm={focusedElem:t,selectionRange:s},kc=!1,Mt=r;Mt!==null;)if(r=Mt,t=r.child,(r.subtreeFlags&1028)!==0&&t!==null)t.return=r,Mt=t;else for(;Mt!==null;){switch(r=Mt,b=r.alternate,t=r.flags,r.tag){case 0:if((t&4)!==0&&(t=r.updateQueue,t=t!==null?t.events:null,t!==null))for(s=0;s title"))),$t(b,u,s),b[Lt]=t,_t(b),u=b;break e;case"link":var w=Zy("link","href",g).get(u+(s.href||""));if(w){for(var T=0;Ttt&&(w=tt,tt=Ne,Ne=w);var $=tv(T,Ne),P=tv(T,tt);if($&&P&&(X.rangeCount!==1||X.anchorNode!==$.node||X.anchorOffset!==$.offset||X.focusNode!==P.node||X.focusOffset!==P.offset)){var F=le.createRange();F.setStart($.node,$.offset),X.removeAllRanges(),Ne>tt?(X.addRange(F),X.extend(P.node,P.offset)):(F.setEnd(P.node,P.offset),X.addRange(F))}}}}for(le=[],X=T;X=X.parentNode;)X.nodeType===1&&le.push({element:X,left:X.scrollLeft,top:X.scrollTop});for(typeof T.focus=="function"&&T.focus(),T=0;Ts?32:s,M.T=null,s=Qf,Qf=null;var b=ra,w=Tr;if(jt=0,Fo=ra=null,Tr=0,(Ye&6)!==0)throw Error(o(331));var T=Ye;if(Ye|=4,ny(b.current),Jb(b,b.current,w,s),Ye=T,gl(0,!1),qt&&typeof qt.onPostCommitFiberRoot=="function")try{qt.onPostCommitFiberRoot(Na,b)}catch{}return!0}finally{K.p=g,M.T=u,xy(t,r)}}function wy(t,r,s){r=kn(s,r),r=_f(t.stateNode,r,2),t=Xr(t,r,2),t!==null&&(mt(t,2),tr(t))}function Xe(t,r,s){if(t.tag===3)wy(t,t,s);else for(;r!==null;){if(r.tag===3){wy(r,t,s);break}else if(r.tag===1){var u=r.stateNode;if(typeof r.type.getDerivedStateFromError=="function"||typeof u.componentDidCatch=="function"&&(na===null||!na.has(u))){t=kn(s,t),s=Cb(2),u=Xr(r,s,2),u!==null&&(jb(s,u,r,t),mt(u,2),tr(u));break}}r=r.return}}function nm(t,r,s){var u=t.pingCache;if(u===null){u=t.pingCache=new DE;var g=new Set;u.set(r,g)}else g=u.get(r),g===void 0&&(g=new Set,u.set(r,g));g.has(s)||(qf=!0,g.add(s),t=$E.bind(null,t,r,s),r.then(t,t))}function $E(t,r,s){var u=t.pingCache;u!==null&&u.delete(r),t.pingedLanes|=t.suspendedLanes&s,t.warmLanes&=~s,nt===t&&(Be&s)===s&&(gt===4||gt===3&&(Be&62914560)===Be&&300>Wt()-cc?(Ye&2)===0&&Yo(t,0):Kf|=s,Go===Be&&(Go=0)),tr(t)}function Cy(t,r){r===0&&(r=Vt()),t=Oa(t,r),t!==null&&(mt(t,r),tr(t))}function IE(t){var r=t.memoizedState,s=0;r!==null&&(s=r.retryLane),Cy(t,s)}function UE(t,r){var s=0;switch(t.tag){case 31:case 13:var u=t.stateNode,g=t.memoizedState;g!==null&&(s=g.retryLane);break;case 19:u=t.stateNode;break;case 22:u=t.stateNode._retryCache;break;default:throw Error(o(314))}u!==null&&u.delete(r),Cy(t,s)}function HE(t,r){return Os(t,r)}var gc=null,qo=null,rm=!1,vc=!1,am=!1,oa=0;function tr(t){t!==qo&&t.next===null&&(qo===null?gc=qo=t:qo=qo.next=t),vc=!0,rm||(rm=!0,GE())}function gl(t,r){if(!am&&vc){am=!0;do for(var s=!1,u=gc;u!==null;){if(t!==0){var g=u.pendingLanes;if(g===0)var b=0;else{var w=u.suspendedLanes,T=u.pingedLanes;b=(1<<31-Ht(42|t)+1)-1,b&=g&~(w&~T),b=b&201326741?b&201326741|1:b?b|2:0}b!==0&&(s=!0,ky(u,b))}else b=Be,b=Te(u,u===nt?b:0,u.cancelPendingCommit!==null||u.timeoutHandle!==-1),(b&3)===0||ut(u,b)||(s=!0,ky(u,b));u=u.next}while(s);am=!1}}function VE(){jy()}function jy(){vc=rm=!1;var t=0;oa!==0&&eN()&&(t=oa);for(var r=Wt(),s=null,u=gc;u!==null;){var g=u.next,b=Ey(u,r);b===0?(u.next=null,s===null?gc=g:s.next=g,g===null&&(qo=s)):(s=u,(t!==0||(b&3)!==0)&&(vc=!0)),u=g}jt!==0&&jt!==5||gl(t),oa!==0&&(oa=0)}function Ey(t,r){for(var s=t.suspendedLanes,u=t.pingedLanes,g=t.expirationTimes,b=t.pendingLanes&-62914561;0T)break;var te=D.transferSize,le=D.initiatorType;te&&Ly(le)&&(D=D.responseEnd,w+=te*(D"u"?null:document;function Wy(t,r,s){var u=Ko;if(u&&typeof r=="string"&&r){var g=En(r);g='link[rel="'+t+'"][href="'+g+'"]',typeof s=="string"&&(g+='[crossorigin="'+s+'"]'),Yy.has(g)||(Yy.add(g),t={rel:t,crossOrigin:s,href:r},u.querySelector(g)===null&&(r=u.createElement("link"),$t(r,"link",t),_t(r),u.head.appendChild(r)))}}function cN(t){Rr.D(t),Wy("dns-prefetch",t,null)}function uN(t,r){Rr.C(t,r),Wy("preconnect",t,r)}function dN(t,r,s){Rr.L(t,r,s);var u=Ko;if(u&&t&&r){var g='link[rel="preload"][as="'+En(r)+'"]';r==="image"&&s&&s.imageSrcSet?(g+='[imagesrcset="'+En(s.imageSrcSet)+'"]',typeof s.imageSizes=="string"&&(g+='[imagesizes="'+En(s.imageSizes)+'"]')):g+='[href="'+En(t)+'"]';var b=g;switch(r){case"style":b=Xo(t);break;case"script":b=Zo(t)}On.has(b)||(t=v({rel:"preload",href:r==="image"&&s&&s.imageSrcSet?void 0:t,as:r},s),On.set(b,t),u.querySelector(g)!==null||r==="style"&&u.querySelector(xl(b))||r==="script"&&u.querySelector(Sl(b))||(r=u.createElement("link"),$t(r,"link",t),_t(r),u.head.appendChild(r)))}}function fN(t,r){Rr.m(t,r);var s=Ko;if(s&&t){var u=r&&typeof r.as=="string"?r.as:"script",g='link[rel="modulepreload"][as="'+En(u)+'"][href="'+En(t)+'"]',b=g;switch(u){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":b=Zo(t)}if(!On.has(b)&&(t=v({rel:"modulepreload",href:t},r),On.set(b,t),s.querySelector(g)===null)){switch(u){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(s.querySelector(Sl(b)))return}u=s.createElement("link"),$t(u,"link",t),_t(u),s.head.appendChild(u)}}}function mN(t,r,s){Rr.S(t,r,s);var u=Ko;if(u&&t){var g=bo(u).hoistableStyles,b=Xo(t);r=r||"default";var w=g.get(b);if(!w){var T={loading:0,preload:null};if(w=u.querySelector(xl(b)))T.loading=5;else{t=v({rel:"stylesheet",href:t,"data-precedence":r},s),(s=On.get(b))&&xm(t,s);var D=w=u.createElement("link");_t(D),$t(D,"link",t),D._p=new Promise(function(Y,te){D.onload=Y,D.onerror=te}),D.addEventListener("load",function(){T.loading|=1}),D.addEventListener("error",function(){T.loading|=2}),T.loading|=4,wc(w,r,u)}w={type:"stylesheet",instance:w,count:1,state:T},g.set(b,w)}}}function pN(t,r){Rr.X(t,r);var s=Ko;if(s&&t){var u=bo(s).hoistableScripts,g=Zo(t),b=u.get(g);b||(b=s.querySelector(Sl(g)),b||(t=v({src:t,async:!0},r),(r=On.get(g))&&Sm(t,r),b=s.createElement("script"),_t(b),$t(b,"link",t),s.head.appendChild(b)),b={type:"script",instance:b,count:1,state:null},u.set(g,b))}}function hN(t,r){Rr.M(t,r);var s=Ko;if(s&&t){var u=bo(s).hoistableScripts,g=Zo(t),b=u.get(g);b||(b=s.querySelector(Sl(g)),b||(t=v({src:t,async:!0,type:"module"},r),(r=On.get(g))&&Sm(t,r),b=s.createElement("script"),_t(b),$t(b,"link",t),s.head.appendChild(b)),b={type:"script",instance:b,count:1,state:null},u.set(g,b))}}function qy(t,r,s,u){var g=(g=ne.current)?Sc(g):null;if(!g)throw Error(o(446));switch(t){case"meta":case"title":return null;case"style":return typeof s.precedence=="string"&&typeof s.href=="string"?(r=Xo(s.href),s=bo(g).hoistableStyles,u=s.get(r),u||(u={type:"style",instance:null,count:0,state:null},s.set(r,u)),u):{type:"void",instance:null,count:0,state:null};case"link":if(s.rel==="stylesheet"&&typeof s.href=="string"&&typeof s.precedence=="string"){t=Xo(s.href);var b=bo(g).hoistableStyles,w=b.get(t);if(w||(g=g.ownerDocument||g,w={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},b.set(t,w),(b=g.querySelector(xl(t)))&&!b._p&&(w.instance=b,w.state.loading=5),On.has(t)||(s={rel:"preload",as:"style",href:s.href,crossOrigin:s.crossOrigin,integrity:s.integrity,media:s.media,hrefLang:s.hrefLang,referrerPolicy:s.referrerPolicy},On.set(t,s),b||gN(g,t,s,w.state))),r&&u===null)throw Error(o(528,""));return w}if(r&&u!==null)throw Error(o(529,""));return null;case"script":return r=s.async,s=s.src,typeof s=="string"&&r&&typeof r!="function"&&typeof r!="symbol"?(r=Zo(s),s=bo(g).hoistableScripts,u=s.get(r),u||(u={type:"script",instance:null,count:0,state:null},s.set(r,u)),u):{type:"void",instance:null,count:0,state:null};default:throw Error(o(444,t))}}function Xo(t){return'href="'+En(t)+'"'}function xl(t){return'link[rel="stylesheet"]['+t+"]"}function Ky(t){return v({},t,{"data-precedence":t.precedence,precedence:null})}function gN(t,r,s,u){t.querySelector('link[rel="preload"][as="style"]['+r+"]")?u.loading=1:(r=t.createElement("link"),u.preload=r,r.addEventListener("load",function(){return u.loading|=1}),r.addEventListener("error",function(){return u.loading|=2}),$t(r,"link",s),_t(r),t.head.appendChild(r))}function Zo(t){return'[src="'+En(t)+'"]'}function Sl(t){return"script[async]"+t}function Xy(t,r,s){if(r.count++,r.instance===null)switch(r.type){case"style":var u=t.querySelector('style[data-href~="'+En(s.href)+'"]');if(u)return r.instance=u,_t(u),u;var g=v({},s,{"data-href":s.href,"data-precedence":s.precedence,href:null,precedence:null});return u=(t.ownerDocument||t).createElement("style"),_t(u),$t(u,"style",g),wc(u,s.precedence,t),r.instance=u;case"stylesheet":g=Xo(s.href);var b=t.querySelector(xl(g));if(b)return r.state.loading|=4,r.instance=b,_t(b),b;u=Ky(s),(g=On.get(g))&&xm(u,g),b=(t.ownerDocument||t).createElement("link"),_t(b);var w=b;return w._p=new Promise(function(T,D){w.onload=T,w.onerror=D}),$t(b,"link",u),r.state.loading|=4,wc(b,s.precedence,t),r.instance=b;case"script":return b=Zo(s.src),(g=t.querySelector(Sl(b)))?(r.instance=g,_t(g),g):(u=s,(g=On.get(b))&&(u=v({},s),Sm(u,g)),t=t.ownerDocument||t,g=t.createElement("script"),_t(g),$t(g,"link",u),t.head.appendChild(g),r.instance=g);case"void":return null;default:throw Error(o(443,r.type))}else r.type==="stylesheet"&&(r.state.loading&4)===0&&(u=r.instance,r.state.loading|=4,wc(u,s.precedence,t));return r.instance}function wc(t,r,s){for(var u=s.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),g=u.length?u[u.length-1]:null,b=g,w=0;w title"):null)}function vN(t,r,s){if(s===1||r.itemProp!=null)return!1;switch(t){case"meta":case"title":return!0;case"style":if(typeof r.precedence!="string"||typeof r.href!="string"||r.href==="")break;return!0;case"link":if(typeof r.rel!="string"||typeof r.href!="string"||r.href===""||r.onLoad||r.onError)break;switch(r.rel){case"stylesheet":return t=r.disabled,typeof r.precedence=="string"&&t==null;default:return!0}case"script":if(r.async&&typeof r.async!="function"&&typeof r.async!="symbol"&&!r.onLoad&&!r.onError&&r.src&&typeof r.src=="string")return!0}return!1}function Jy(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function bN(t,r,s,u){if(s.type==="stylesheet"&&(typeof u.media!="string"||matchMedia(u.media).matches!==!1)&&(s.state.loading&4)===0){if(s.instance===null){var g=Xo(u.href),b=r.querySelector(xl(g));if(b){r=b._p,r!==null&&typeof r=="object"&&typeof r.then=="function"&&(t.count++,t=jc.bind(t),r.then(t,t)),s.state.loading|=4,s.instance=b,_t(b);return}b=r.ownerDocument||r,u=Ky(u),(g=On.get(g))&&xm(u,g),b=b.createElement("link"),_t(b);var w=b;w._p=new Promise(function(T,D){w.onload=T,w.onerror=D}),$t(b,"link",u),s.instance=b}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(s,r),(r=s.state.preload)&&(s.state.loading&3)===0&&(t.count++,s=jc.bind(t),r.addEventListener("load",s),r.addEventListener("error",s))}}var wm=0;function yN(t,r){return t.stylesheets&&t.count===0&&Nc(t,t.stylesheets),0wm?50:800)+r);return t.unsuspend=s,function(){t.unsuspend=null,clearTimeout(u),clearTimeout(g)}}:null}function jc(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Nc(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var Ec=null;function Nc(t,r){t.stylesheets=null,t.unsuspend!==null&&(t.count++,Ec=new Map,r.forEach(xN,t),Ec=null,jc.call(t))}function xN(t,r){if(!(r.state.loading&4)){var s=Ec.get(t);if(s)var u=s.get(null);else{s=new Map,Ec.set(t,s);for(var g=t.querySelectorAll("link[data-precedence],style[data-precedence]"),b=0;b"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(n){console.error(n)}}return e(),_m.exports=BN(),_m.exports}var IN=$N();/** - * react-router v7.15.1 - * - * Copyright (c) Remix Software Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE.md file in the root directory of this source tree. - * - * @license MIT - */var j1="popstate";function E1(e){return typeof e=="object"&&e!=null&&"pathname"in e&&"search"in e&&"hash"in e&&"state"in e&&"key"in e}function UN(e={}){function n(o,l){var p;let i=(p=l.state)==null?void 0:p.masked,{pathname:d,search:m,hash:h}=i||o.location;return pp("",{pathname:d,search:m,hash:h},l.state&&l.state.usr||null,l.state&&l.state.key||"default",i?{pathname:o.location.pathname,search:o.location.search,hash:o.location.hash}:void 0)}function a(o,l){return typeof l=="string"?l:Il(l)}return VN(n,a,null,e)}function ft(e,n){if(e===!1||e===null||typeof e>"u")throw new Error(n)}function Pn(e,n){if(!e){typeof console<"u"&&console.warn(n);try{throw new Error(n)}catch{}}}function HN(){return Math.random().toString(36).substring(2,10)}function N1(e,n){return{usr:e.state,key:e.key,idx:n,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function pp(e,n,a=null,o,l){return{pathname:typeof e=="string"?e:e.pathname,search:"",hash:"",...typeof n=="string"?ws(n):n,state:a,key:n&&n.key||o||HN(),mask:l}}function Il({pathname:e="/",search:n="",hash:a=""}){return n&&n!=="?"&&(e+=n.charAt(0)==="?"?n:"?"+n),a&&a!=="#"&&(e+=a.charAt(0)==="#"?a:"#"+a),e}function ws(e){let n={};if(e){let a=e.indexOf("#");a>=0&&(n.hash=e.substring(a),e=e.substring(0,a));let o=e.indexOf("?");o>=0&&(n.search=e.substring(o),e=e.substring(0,o)),e&&(n.pathname=e)}return n}function VN(e,n,a,o={}){let{window:l=document.defaultView,v5Compat:i=!1}=o,d=l.history,m="POP",h=null,p=y();p==null&&(p=0,d.replaceState({...d.state,idx:p},""));function y(){return(d.state||{idx:null}).idx}function v(){m="POP";let N=y(),E=N==null?null:N-p;p=N,h&&h({action:m,location:C.location,delta:E})}function x(N,E){m="PUSH";let k=E1(N)?N:pp(C.location,N,E);p=y()+1;let A=N1(k,p),R=C.createHref(k.mask||k);try{d.pushState(A,"",R)}catch(L){if(L instanceof DOMException&&L.name==="DataCloneError")throw L;l.location.assign(R)}i&&h&&h({action:m,location:C.location,delta:1})}function S(N,E){m="REPLACE";let k=E1(N)?N:pp(C.location,N,E);p=y();let A=N1(k,p),R=C.createHref(k.mask||k);d.replaceState(A,"",R),i&&h&&h({action:m,location:C.location,delta:0})}function j(N){return GN(N)}let C={get action(){return m},get location(){return e(l,d)},listen(N){if(h)throw new Error("A history only accepts one active listener");return l.addEventListener(j1,v),h=N,()=>{l.removeEventListener(j1,v),h=null}},createHref(N){return n(l,N)},createURL:j,encodeLocation(N){let E=j(N);return{pathname:E.pathname,search:E.search,hash:E.hash}},push:x,replace:S,go(N){return d.go(N)}};return C}function GN(e,n=!1){let a="http://localhost";typeof window<"u"&&(a=window.location.origin!=="null"?window.location.origin:window.location.href),ft(a,"No window.location.(origin|href) available to create URL");let o=typeof e=="string"?e:Il(e);return o=o.replace(/ $/,"%20"),!n&&o.startsWith("//")&&(o=a+o),new URL(o,a)}function Qx(e,n,a="/"){return FN(e,n,a,!1)}function FN(e,n,a,o,l){let i=typeof n=="string"?ws(n):n,d=Or(i.pathname||"/",a);if(d==null)return null;let m=YN(e),h=null,p=a3(d);for(let y=0;h==null&&y{let y={relativePath:p===void 0?d.path||"":p,caseSensitive:d.caseSensitive===!0,childrenIndex:m,route:d};if(y.relativePath.startsWith("/")){if(!y.relativePath.startsWith(o)&&h)return;ft(y.relativePath.startsWith(o),`Absolute route path "${y.relativePath}" nested under path "${o}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),y.relativePath=y.relativePath.slice(o.length)}let v=Fn([o,y.relativePath]),x=a.concat(y);d.children&&d.children.length>0&&(ft(d.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${v}".`),Jx(d.children,n,x,v,h)),!(d.path==null&&!d.index)&&n.push({path:v,score:e3(v,d.index),routesMeta:x})};return e.forEach((d,m)=>{var h;if(d.path===""||!((h=d.path)!=null&&h.includes("?")))i(d,m);else for(let p of e0(d.path))i(d,m,!0,p)}),n}function e0(e){let n=e.split("/");if(n.length===0)return[];let[a,...o]=n,l=a.endsWith("?"),i=a.replace(/\?$/,"");if(o.length===0)return l?[i,""]:[i];let d=e0(o.join("/")),m=[];return m.push(...d.map(h=>h===""?i:[i,h].join("/"))),l&&m.push(...d),m.map(h=>e.startsWith("/")&&h===""?"/":h)}function WN(e){e.sort((n,a)=>n.score!==a.score?a.score-n.score:t3(n.routesMeta.map(o=>o.childrenIndex),a.routesMeta.map(o=>o.childrenIndex)))}var qN=/^:[\w-]+$/,KN=3,XN=2,ZN=1,QN=10,JN=-2,k1=e=>e==="*";function e3(e,n){let a=e.split("/"),o=a.length;return a.some(k1)&&(o+=JN),n&&(o+=XN),a.filter(l=>!k1(l)).reduce((l,i)=>l+(qN.test(i)?KN:i===""?ZN:QN),o)}function t3(e,n){return e.length===n.length&&e.slice(0,-1).every((o,l)=>o===n[l])?e[e.length-1]-n[n.length-1]:0}function n3(e,n,a=!1){let{routesMeta:o}=e,l={},i="/",d=[];for(let m=0;m{if(y==="*"){let j=m[x]||"";d=i.slice(0,i.length-j.length).replace(/(.)\/+$/,"$1")}const S=m[x];return v&&!S?p[y]=void 0:p[y]=(S||"").replace(/%2F/g,"/"),p},{}),pathname:i,pathnameBase:d,pattern:e}}function r3(e,n=!1,a=!0){Pn(e==="*"||!e.endsWith("*")||e.endsWith("/*"),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,"/*")}".`);let o=[],l="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(d,m,h,p,y)=>{if(o.push({paramName:m,isOptional:h!=null}),h){let v=y.charAt(p+d.length);return v&&v!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return e.endsWith("*")?(o.push({paramName:"*"}),l+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):a?l+="\\/*$":e!==""&&e!=="/"&&(l+="(?:(?=\\/|$))"),[new RegExp(l,n?void 0:"i"),o]}function a3(e){try{return e.split("/").map(n=>decodeURIComponent(n).replace(/\//g,"%2F")).join("/")}catch(n){return Pn(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${n}).`),e}}function Or(e,n){if(n==="/")return e;if(!e.toLowerCase().startsWith(n.toLowerCase()))return null;let a=n.endsWith("/")?n.length-1:n.length,o=e.charAt(a);return o&&o!=="/"?null:e.slice(a)||"/"}var o3=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function s3(e,n="/"){let{pathname:a,search:o="",hash:l=""}=typeof e=="string"?ws(e):e,i;return a?(a=t0(a),a.startsWith("/")?i=T1(a.substring(1),"/"):i=T1(a,n)):i=n,{pathname:i,search:c3(o),hash:u3(l)}}function T1(e,n){let a=ou(n).split("/");return e.split("/").forEach(l=>{l===".."?a.length>1&&a.pop():l!=="."&&a.push(l)}),a.length>1?a.join("/"):"/"}function Lm(e,n,a,o){return`Cannot include a '${e}' character in a manually specified \`to.${n}\` field [${JSON.stringify(o)}]. Please separate it out to the \`to.${a}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function l3(e){return e.filter((n,a)=>a===0||n.route.path&&n.route.path.length>0)}function rh(e){let n=l3(e);return n.map((a,o)=>o===n.length-1?a.pathname:a.pathnameBase)}function ju(e,n,a,o=!1){let l;typeof e=="string"?l=ws(e):(l={...e},ft(!l.pathname||!l.pathname.includes("?"),Lm("?","pathname","search",l)),ft(!l.pathname||!l.pathname.includes("#"),Lm("#","pathname","hash",l)),ft(!l.search||!l.search.includes("#"),Lm("#","search","hash",l)));let i=e===""||l.pathname==="",d=i?"/":l.pathname,m;if(d==null)m=a;else{let v=n.length-1;if(!o&&d.startsWith("..")){let x=d.split("/");for(;x[0]==="..";)x.shift(),v-=1;l.pathname=x.join("/")}m=v>=0?n[v]:"/"}let h=s3(l,m),p=d&&d!=="/"&&d.endsWith("/"),y=(i||d===".")&&a.endsWith("/");return!h.pathname.endsWith("/")&&(p||y)&&(h.pathname+="/"),h}var t0=e=>e.replace(/\/\/+/g,"/"),Fn=e=>t0(e.join("/")),ou=e=>e.replace(/\/+$/,""),i3=e=>ou(e).replace(/^\/*/,"/"),c3=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,u3=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e,d3=class{constructor(e,n,a,o=!1){this.status=e,this.statusText=n||"",this.internal=o,a instanceof Error?(this.data=a.toString(),this.error=a):this.data=a}};function f3(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}function m3(e){let n=e.map(a=>a.route.path).filter(Boolean);return Fn(n)||"/"}var n0=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function r0(e,n){let a=e;if(typeof a!="string"||!o3.test(a))return{absoluteURL:void 0,isExternal:!1,to:a};let o=a,l=!1;if(n0)try{let i=new URL(window.location.href),d=a.startsWith("//")?new URL(i.protocol+a):new URL(a),m=Or(d.pathname,n);d.origin===i.origin&&m!=null?a=m+d.search+d.hash:l=!0}catch{Pn(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:o,isExternal:l,to:a}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var a0=["POST","PUT","PATCH","DELETE"];new Set(a0);var p3=["GET",...a0];new Set(p3);var Cs=f.createContext(null);Cs.displayName="DataRouter";var Eu=f.createContext(null);Eu.displayName="DataRouterState";var o0=f.createContext(!1);function h3(){return f.useContext(o0)}var s0=f.createContext({isTransitioning:!1});s0.displayName="ViewTransition";var g3=f.createContext(new Map);g3.displayName="Fetchers";var v3=f.createContext(null);v3.displayName="Await";var xn=f.createContext(null);xn.displayName="Navigation";var Zl=f.createContext(null);Zl.displayName="Location";var qn=f.createContext({outlet:null,matches:[],isDataRoute:!1});qn.displayName="Route";var ah=f.createContext(null);ah.displayName="RouteError";var l0="REACT_ROUTER_ERROR",b3="REDIRECT",y3="ROUTE_ERROR_RESPONSE";function x3(e){if(e.startsWith(`${l0}:${b3}:{`))try{let n=JSON.parse(e.slice(28));if(typeof n=="object"&&n&&typeof n.status=="number"&&typeof n.statusText=="string"&&typeof n.location=="string"&&typeof n.reloadDocument=="boolean"&&typeof n.replace=="boolean")return n}catch{}}function S3(e){if(e.startsWith(`${l0}:${y3}:{`))try{let n=JSON.parse(e.slice(40));if(typeof n=="object"&&n&&typeof n.status=="number"&&typeof n.statusText=="string")return new d3(n.status,n.statusText,n.data)}catch{}}function w3(e,{relative:n}={}){ft(js(),"useHref() may be used only in the context of a component.");let{basename:a,navigator:o}=f.useContext(xn),{hash:l,pathname:i,search:d}=Ql(e,{relative:n}),m=i;return a!=="/"&&(m=i==="/"?a:Fn([a,i])),o.createHref({pathname:m,search:d,hash:l})}function js(){return f.useContext(Zl)!=null}function Sn(){return ft(js(),"useLocation() may be used only in the context of a component."),f.useContext(Zl).location}var i0="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function c0(e){f.useContext(xn).static||f.useLayoutEffect(e)}function oo(){let{isDataRoute:e}=f.useContext(qn);return e?P3():C3()}function C3(){ft(js(),"useNavigate() may be used only in the context of a component.");let e=f.useContext(Cs),{basename:n,navigator:a}=f.useContext(xn),{matches:o}=f.useContext(qn),{pathname:l}=Sn(),i=JSON.stringify(rh(o)),d=f.useRef(!1);return c0(()=>{d.current=!0}),f.useCallback((h,p={})=>{if(Pn(d.current,i0),!d.current)return;if(typeof h=="number"){a.go(h);return}let y=ju(h,JSON.parse(i),l,p.relative==="path");e==null&&n!=="/"&&(y.pathname=y.pathname==="/"?n:Fn([n,y.pathname])),(p.replace?a.replace:a.push)(y,p.state,p)},[n,a,i,l,e])}var j3=f.createContext(null);function E3(e){let n=f.useContext(qn).outlet;return f.useMemo(()=>n&&f.createElement(j3.Provider,{value:e},n),[n,e])}function Ql(e,{relative:n}={}){let{matches:a}=f.useContext(qn),{pathname:o}=Sn(),l=JSON.stringify(rh(a));return f.useMemo(()=>ju(e,JSON.parse(l),o,n==="path"),[e,l,o,n])}function N3(e,n){return u0(e,n)}function u0(e,n,a){var N;ft(js(),"useRoutes() may be used only in the context of a component.");let{navigator:o}=f.useContext(xn),{matches:l}=f.useContext(qn),i=l[l.length-1],d=i?i.params:{},m=i?i.pathname:"/",h=i?i.pathnameBase:"/",p=i&&i.route;{let E=p&&p.path||"";f0(m,!p||E.endsWith("*")||E.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${m}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. - -Please change the parent to .`)}let y=Sn(),v;if(n){let E=typeof n=="string"?ws(n):n;ft(h==="/"||((N=E.pathname)==null?void 0:N.startsWith(h)),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${h}" but pathname "${E.pathname}" was given in the \`location\` prop.`),v=E}else v=y;let x=v.pathname||"/",S=x;if(h!=="/"){let E=h.replace(/^\//,"").split("/");S="/"+x.replace(/^\//,"").split("/").slice(E.length).join("/")}let j=a&&a.state.matches.length?a.state.matches.map(E=>Object.assign(E,{route:a.manifest[E.route.id]||E.route})):Qx(e,{pathname:S});Pn(p||j!=null,`No routes matched location "${v.pathname}${v.search}${v.hash}" `),Pn(j==null||j[j.length-1].route.element!==void 0||j[j.length-1].route.Component!==void 0||j[j.length-1].route.lazy!==void 0,`Matched leaf route at location "${v.pathname}${v.search}${v.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let C=_3(j&&j.map(E=>Object.assign({},E,{params:Object.assign({},d,E.params),pathname:Fn([h,o.encodeLocation?o.encodeLocation(E.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:E.pathname]),pathnameBase:E.pathnameBase==="/"?h:Fn([h,o.encodeLocation?o.encodeLocation(E.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:E.pathnameBase])})),l,a);return n&&C?f.createElement(Zl.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",mask:void 0,...v},navigationType:"POP"}},C):C}function k3(){let e=z3(),n=f3(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),a=e instanceof Error?e.stack:null,o="rgba(200,200,200, 0.5)",l={padding:"0.5rem",backgroundColor:o},i={padding:"2px 4px",backgroundColor:o},d=null;return console.error("Error handled by React Router default ErrorBoundary:",e),d=f.createElement(f.Fragment,null,f.createElement("p",null,"💿 Hey developer 👋"),f.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",f.createElement("code",{style:i},"ErrorBoundary")," or"," ",f.createElement("code",{style:i},"errorElement")," prop on your route.")),f.createElement(f.Fragment,null,f.createElement("h2",null,"Unexpected Application Error!"),f.createElement("h3",{style:{fontStyle:"italic"}},n),a?f.createElement("pre",{style:l},a):null,d)}var T3=f.createElement(k3,null),d0=class extends f.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,n){return n.location!==e.location||n.revalidation!=="idle"&&e.revalidation==="idle"?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error!==void 0?e.error:n.error,location:n.location,revalidation:e.revalidation||n.revalidation}}componentDidCatch(e,n){this.props.onError?this.props.onError(e,n):console.error("React Router caught the following error during render",e)}render(){let e=this.state.error;if(this.context&&typeof e=="object"&&e&&"digest"in e&&typeof e.digest=="string"){const a=S3(e.digest);a&&(e=a)}let n=e!==void 0?f.createElement(qn.Provider,{value:this.props.routeContext},f.createElement(ah.Provider,{value:e,children:this.props.component})):this.props.children;return this.context?f.createElement(R3,{error:e},n):n}};d0.contextType=o0;var zm=new WeakMap;function R3({children:e,error:n}){let{basename:a}=f.useContext(xn);if(typeof n=="object"&&n&&"digest"in n&&typeof n.digest=="string"){let o=x3(n.digest);if(o){let l=zm.get(n);if(l)throw l;let i=r0(o.location,a);if(n0&&!zm.get(n))if(i.isExternal||o.reloadDocument)window.location.href=i.absoluteURL||i.to;else{const d=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:o.replace}));throw zm.set(n,d),d}return f.createElement("meta",{httpEquiv:"refresh",content:`0;url=${i.absoluteURL||i.to}`})}}return e}function A3({routeContext:e,match:n,children:a}){let o=f.useContext(Cs);return o&&o.static&&o.staticContext&&(n.route.errorElement||n.route.ErrorBoundary)&&(o.staticContext._deepestRenderedBoundaryId=n.route.id),f.createElement(qn.Provider,{value:e},a)}function _3(e,n=[],a){let o=a==null?void 0:a.state;if(e==null){if(!o)return null;if(o.errors)e=o.matches;else if(n.length===0&&!o.initialized&&o.matches.length>0)e=o.matches;else return null}let l=e,i=o==null?void 0:o.errors;if(i!=null){let y=l.findIndex(v=>v.route.id&&(i==null?void 0:i[v.route.id])!==void 0);ft(y>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(i).join(",")}`),l=l.slice(0,Math.min(l.length,y+1))}let d=!1,m=-1;if(a&&o){d=o.renderFallback;for(let y=0;y=0?l=l.slice(0,m+1):l=[l[0]];break}}}}let h=a==null?void 0:a.onError,p=o&&h?(y,v)=>{var x,S;h(y,{location:o.location,params:((S=(x=o.matches)==null?void 0:x[0])==null?void 0:S.params)??{},pattern:m3(o.matches),errorInfo:v})}:void 0;return l.reduceRight((y,v,x)=>{let S,j=!1,C=null,N=null;o&&(S=i&&v.route.id?i[v.route.id]:void 0,C=v.route.errorElement||T3,d&&(m<0&&x===0?(f0("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),j=!0,N=null):m===x&&(j=!0,N=v.route.hydrateFallbackElement||null)));let E=n.concat(l.slice(0,x+1)),k=()=>{let A;return S?A=C:j?A=N:v.route.Component?A=f.createElement(v.route.Component,null):v.route.element?A=v.route.element:A=y,f.createElement(A3,{match:v,routeContext:{outlet:y,matches:E,isDataRoute:o!=null},children:A})};return o&&(v.route.ErrorBoundary||v.route.errorElement||x===0)?f.createElement(d0,{location:o.location,revalidation:o.revalidation,component:C,error:S,children:k(),routeContext:{outlet:null,matches:E,isDataRoute:!0},onError:p}):k()},null)}function oh(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function M3(e){let n=f.useContext(Cs);return ft(n,oh(e)),n}function O3(e){let n=f.useContext(Eu);return ft(n,oh(e)),n}function D3(e){let n=f.useContext(qn);return ft(n,oh(e)),n}function sh(e){let n=D3(e),a=n.matches[n.matches.length-1];return ft(a.route.id,`${e} can only be used on routes that contain a unique "id"`),a.route.id}function L3(){return sh("useRouteId")}function z3(){var o;let e=f.useContext(ah),n=O3("useRouteError"),a=sh("useRouteError");return e!==void 0?e:(o=n.errors)==null?void 0:o[a]}function P3(){let{router:e}=M3("useNavigate"),n=sh("useNavigate"),a=f.useRef(!1);return c0(()=>{a.current=!0}),f.useCallback(async(l,i={})=>{Pn(a.current,i0),a.current&&(typeof l=="number"?await e.navigate(l):await e.navigate(l,{fromRouteId:n,...i}))},[e,n])}var R1={};function f0(e,n,a){!n&&!R1[e]&&(R1[e]=!0,Pn(!1,a))}f.memo(B3);function B3({routes:e,manifest:n,future:a,state:o,isStatic:l,onError:i}){return u0(e,void 0,{manifest:n,state:o,isStatic:l,onError:i})}function Mr({to:e,replace:n,state:a,relative:o}){ft(js()," may be used only in the context of a component.");let{static:l}=f.useContext(xn);Pn(!l," must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.");let{matches:i}=f.useContext(qn),{pathname:d}=Sn(),m=oo(),h=ju(e,rh(i),d,o==="path"),p=JSON.stringify(h);return f.useEffect(()=>{m(JSON.parse(p),{replace:n,state:a,relative:o})},[m,p,o,n,a]),null}function $3(e){return E3(e.context)}function ln(e){ft(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function I3({basename:e="/",children:n=null,location:a,navigationType:o="POP",navigator:l,static:i=!1,useTransitions:d}){ft(!js(),"You cannot render a inside another . You should never have more than one in your app.");let m=e.replace(/^\/*/,"/"),h=f.useMemo(()=>({basename:m,navigator:l,static:i,useTransitions:d,future:{}}),[m,l,i,d]);typeof a=="string"&&(a=ws(a));let{pathname:p="/",search:y="",hash:v="",state:x=null,key:S="default",mask:j}=a,C=f.useMemo(()=>{let N=Or(p,m);return N==null?null:{location:{pathname:N,search:y,hash:v,state:x,key:S,mask:j},navigationType:o}},[m,p,y,v,x,S,o,j]);return Pn(C!=null,` is not able to match the URL "${p}${y}${v}" because it does not start with the basename, so the won't render anything.`),C==null?null:f.createElement(xn.Provider,{value:h},f.createElement(Zl.Provider,{children:n,value:C}))}function A1({children:e,location:n}){return N3(hp(e),n)}function hp(e,n=[]){let a=[];return f.Children.forEach(e,(o,l)=>{if(!f.isValidElement(o))return;let i=[...n,l];if(o.type===f.Fragment){a.push.apply(a,hp(o.props.children,i));return}ft(o.type===ln,`[${typeof o.type=="string"?o.type:o.type.name}] is not a component. All component children of must be a or `),ft(!o.props.index||!o.props.children,"An index route cannot have child routes.");let d={id:o.props.id||i.join("-"),caseSensitive:o.props.caseSensitive,element:o.props.element,Component:o.props.Component,index:o.props.index,path:o.props.path,middleware:o.props.middleware,loader:o.props.loader,action:o.props.action,hydrateFallbackElement:o.props.hydrateFallbackElement,HydrateFallback:o.props.HydrateFallback,errorElement:o.props.errorElement,ErrorBoundary:o.props.ErrorBoundary,hasErrorBoundary:o.props.hasErrorBoundary===!0||o.props.ErrorBoundary!=null||o.props.errorElement!=null,shouldRevalidate:o.props.shouldRevalidate,handle:o.props.handle,lazy:o.props.lazy};o.props.children&&(d.children=hp(o.props.children,i)),a.push(d)}),a}var qc="get",Kc="application/x-www-form-urlencoded";function Nu(e){return typeof HTMLElement<"u"&&e instanceof HTMLElement}function U3(e){return Nu(e)&&e.tagName.toLowerCase()==="button"}function H3(e){return Nu(e)&&e.tagName.toLowerCase()==="form"}function V3(e){return Nu(e)&&e.tagName.toLowerCase()==="input"}function G3(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function F3(e,n){return e.button===0&&(!n||n==="_self")&&!G3(e)}function gp(e=""){return new URLSearchParams(typeof e=="string"||Array.isArray(e)||e instanceof URLSearchParams?e:Object.keys(e).reduce((n,a)=>{let o=e[a];return n.concat(Array.isArray(o)?o.map(l=>[a,l]):[[a,o]])},[]))}function Y3(e,n){let a=gp(e);return n&&n.forEach((o,l)=>{a.has(l)||n.getAll(l).forEach(i=>{a.append(l,i)})}),a}var Dc=null;function W3(){if(Dc===null)try{new FormData(document.createElement("form"),0),Dc=!1}catch{Dc=!0}return Dc}var q3=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function Pm(e){return e!=null&&!q3.has(e)?(Pn(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Kc}"`),null):e}function K3(e,n){let a,o,l,i,d;if(H3(e)){let m=e.getAttribute("action");o=m?Or(m,n):null,a=e.getAttribute("method")||qc,l=Pm(e.getAttribute("enctype"))||Kc,i=new FormData(e)}else if(U3(e)||V3(e)&&(e.type==="submit"||e.type==="image")){let m=e.form;if(m==null)throw new Error('Cannot submit a `,L=>`${R}-${L.id}`)}const N=x`
${n.i18n.searchDescription}
${n.i18n.skinToneDescription}
${S(n.skinTones,(k,A)=>x`
${k}
`,k=>k)}
${S(n.currentEmojisWithCategories,(k,A)=>x`
${j(k.emojis,n.searchMode,"emo")}
`,k=>k.category)}
`,E=(k,A)=>{for(const R of e.querySelectorAll(`[${k}]`))A(R,R.getAttribute(k))};if(h){e.appendChild(N);for(const k of["click","focusout","input","keydown","keyup"])E(`data-on-${k}`,(A,R)=>{A.addEventListener(k,o[R])});E("data-ref",(k,A)=>{i[A]=k}),d.addEventListener("abort",()=>{e.removeChild(N)})}E("data-action",(k,A)=>{let R=m.get(A);R||m.set(A,R=new WeakSet),R.has(k)||(R.add(k),l[A](k))})}const wu=typeof queueMicrotask=="function"?queueMicrotask:e=>Promise.resolve().then(e);function Ez(e){let n=!1,a;const o=new Map,l=new Set;let i;const d=()=>{if(n)return;const p=[...l];l.clear();try{for(const y of p)y()}finally{i=!1,l.size&&(i=!0,wu(d))}},m=new Proxy({},{get(p,y){if(a){let v=o.get(y);v||(v=new Set,o.set(y,v)),v.add(a)}return p[y]},set(p,y,v){if(p[y]!==v){p[y]=v;const x=o.get(y);if(x){for(const S of x)l.add(S);i||(i=!0,wu(d))}}return!0}}),h=p=>{const y=()=>{const v=a;a=y;try{return p()}finally{a=v}};return y()};return e.addEventListener("abort",()=>{n=!0}),{state:m,createEffect:h}}function fp(e,n,a){if(e.length!==n.length)return!1;for(let o=0;o{l.disconnect()})),l.observe(e)}}const mp=[],{assign:Yc}=Object;function kz(e,n){const a={},o=new AbortController,l=o.signal,{state:i,createEffect:d}=Ez(l),m=new Map;Yc(i,{skinToneEmoji:void 0,i18n:void 0,database:void 0,customEmoji:void 0,customCategorySorting:void 0,emojiVersion:void 0}),Yc(i,n),Yc(i,{initialLoad:!0,currentEmojis:[],currentEmojisWithCategories:[],rawSearchText:"",searchText:"",searchMode:!1,activeSearchItem:-1,message:void 0,skinTonePickerExpanded:!1,skinTonePickerExpandedAfterAnimation:!1,currentSkinTone:0,activeSkinTone:0,skinToneButtonText:void 0,pickerStyle:void 0,skinToneButtonLabel:"",skinTones:[],currentFavorites:[],defaultFavoriteEmojis:void 0,numColumns:YL,isRtl:!1,currentGroupIndex:0,groups:lp,databaseLoaded:!1,activeSearchItemId:void 0}),d(()=>{i.currentGroup!==i.groups[i.currentGroupIndex]&&(i.currentGroup=i.groups[i.currentGroupIndex])});const h=G=>{e.getElementById(G).focus()},p=G=>e.getElementById(`emo-${G.id}`),y=(G,se)=>{a.rootElement.dispatchEvent(new CustomEvent(G,{detail:se,bubbles:!0,composed:!0}))},v=(G,se)=>G.id===se.id,x=(G,se)=>{const{category:ge,emojis:je}=G,{category:Ae,emojis:Qe}=se;return ge!==Ae?!1:fp(je,Qe,v)},S=G=>{fp(i.currentEmojis,G,v)||(i.currentEmojis=G)},j=G=>{i.searchMode!==G&&(i.searchMode=G)},C=G=>{fp(i.currentEmojisWithCategories,G,x)||(i.currentEmojisWithCategories=G)},N=(G,se)=>se&&G.skins&&G.skins[se]||G.unicode,A={labelWithSkin:(G,se)=>cz([G.name||N(G,se),G.annotation,...G.shortcodes||mp].filter(Boolean)).join(", "),titleForEmoji:G=>G.annotation||(G.shortcodes||mp).join(", "),unicodeWithSkin:N},R={onClickSkinToneButton:V,onEmojiClick:O,onNavClick:re,onNavKeydown:oe,onSearchKeydown:K,onSkinToneOptionsClick:I,onSkinToneOptionsFocusOut:me,onSkinToneOptionsKeydown:ne,onSkinToneOptionsKeyup:J,onSearchInput:pe},L={calculateEmojiGridStyle:U,updateOnIntersection:q};let z=!0;d(()=>{jz(e,i,A,R,L,a,l,m,z),z=!1}),i.emojiVersion||cp().then(G=>{G||(i.message=i.i18n.emojiUnsupportedMessage)}),d(()=>{async function G(){let se=!1;const ge=setTimeout(()=>{se=!0,i.message=i.i18n.loadingMessage},GL);try{await i.database.ready(),i.databaseLoaded=!0}catch(je){console.error(je),i.message=i.i18n.networkErrorMessage}finally{clearTimeout(ge),se&&(se=!1,i.message="")}}i.database&&G()}),d(()=>{i.pickerStyle=` - --num-groups: ${i.groups.length}; - --indicator-opacity: ${i.searchMode?0:1}; - --num-skintones: ${Mx};`}),d(()=>{i.customEmoji&&i.database&&B()}),d(()=>{i.customEmoji&&i.customEmoji.length?i.groups!==Zp&&(i.groups=Zp):i.groups!==lp&&(i.currentGroupIndex&&i.currentGroupIndex--,i.groups=lp)}),d(()=>{async function G(){i.databaseLoaded&&(i.currentSkinTone=await i.database.getPreferredSkinTone())}G()}),d(()=>{i.skinTones=Array(Mx).fill().map((G,se)=>rz(i.skinToneEmoji,se))}),d(()=>{i.skinToneButtonText=i.skinTones[i.currentSkinTone]}),d(()=>{i.skinToneButtonLabel=i.i18n.skinToneLabel.replace("{skinTone}",i.i18n.skinTones[i.currentSkinTone])}),d(()=>{async function G(){const{database:se}=i,ge=(await Promise.all(WL.map(je=>se.getEmojiByUnicodeOrName(je)))).filter(Boolean);i.defaultFavoriteEmojis=ge}i.databaseLoaded&&G()});function B(){const{customEmoji:G,database:se}=i,ge=G||mp;se.customEmoji!==ge&&(se.customEmoji=ge)}d(()=>{async function G(){B();const{database:se,defaultFavoriteEmojis:ge,numColumns:je}=i,Ae=await se.getTopFavoriteEmoji(je),Qe=await ce(c5([...Ae,...ge],ct=>ct.unicode||ct.name).slice(0,je));i.currentFavorites=Qe}i.databaseLoaded&&i.defaultFavoriteEmojis&&G()});function U(G){sz(G,l,()=>{{const se=getComputedStyle(a.rootElement),ge=parseInt(se.getPropertyValue("--num-columns"),10),je=se.getPropertyValue("direction")==="rtl";i.numColumns=ge,i.isRtl=je}})}function q(G){Nz(G,l,se=>{for(const{target:ge,isIntersecting:je}of se)ge.classList.toggle("onscreen",je)})}d(()=>{async function G(){const{searchText:se,currentGroup:ge,databaseLoaded:je,customEmoji:Ae}=i;if(!je)i.currentEmojis=[],i.searchMode=!1;else if(se.length>=HL){const Qe=await M(se);i.searchText===se&&(S(Qe),j(!0))}else{const{id:Qe}=ge;if(Qe!==-1||Ae&&Ae.length){const ct=await he(Qe);i.currentGroup.id===Qe&&(S(ct),j(!1))}}}G()});const Z=()=>{tu(()=>uz(a.tabpanelElement))};d(()=>{const{currentEmojis:G,emojiVersion:se}=i,ge=G.filter(je=>je.unicode).filter(je=>Ox(je)&&!Qp.has(je.unicode));if(!se&&ge.length)S(G),tu(()=>Q(ge));else{const je=se?G:G.filter(fe);S(je),Z()}});function Q(G){iz(G,a.baselineEmoji,p)?Z():i.currentEmojis=[...i.currentEmojis]}function fe(G){return!G.unicode||!Ox(G)||Qp.get(G.unicode)}async function de(G){const se=i.emojiVersion||await cp();return G.filter(({version:ge})=>!ge||ge<=se)}async function ce(G){return az(G,i.emojiVersion||await cp())}async function he(G){const se=G===-1?i.customEmoji:await i.database.getEmojiByGroup(G);return ce(await de(se))}async function M(G){return ce(await de(await i.database.getEmojiBySearchQuery(G)))}d(()=>{}),d(()=>{function G(){const{searchMode:ge,currentEmojis:je}=i;if(ge)return[{category:"",emojis:je}];const Ae=new Map;for(const Qe of je){const ct=Qe.category||"";let Tt=Ae.get(ct);Tt||(Tt=[],Ae.set(ct,Tt)),Tt.push(Qe)}return[...Ae.entries()].map(([Qe,ct])=>({category:Qe,emojis:ct})).sort((Qe,ct)=>i.customCategorySorting(Qe.category,ct.category))}const se=G();C(se)}),d(()=>{i.activeSearchItemId=i.activeSearchItem!==-1&&i.currentEmojis[i.activeSearchItem].id}),d(()=>{const{rawSearchText:G}=i;l5(()=>{i.searchText=(G||"").trim(),i.activeSearchItem=-1})});function K(G){if(!i.searchMode||!i.currentEmojis.length)return;const se=ge=>{rr(G),i.activeSearchItem=up(ge,i.activeSearchItem,i.currentEmojis)};switch(G.key){case"ArrowDown":return se(!1);case"ArrowUp":return se(!0);case"Enter":if(i.activeSearchItem===-1)i.activeSearchItem=0;else return rr(G),_(i.currentEmojis[i.activeSearchItem].id)}}function re(G){const{target:se}=G,ge=se.closest(".nav-button");if(!ge)return;const je=parseInt(ge.dataset.groupId,10);a.searchElement.value="",i.rawSearchText="",i.searchText="",i.activeSearchItem=-1,i.currentGroupIndex=i.groups.findIndex(Ae=>Ae.id===je)}function oe(G){const{target:se,key:ge}=G,je=Ae=>{Ae&&(rr(G),Ae.focus())};switch(ge){case"ArrowLeft":return je(se.previousElementSibling);case"ArrowRight":return je(se.nextElementSibling);case"Home":return je(se.parentElement.firstElementChild);case"End":return je(se.parentElement.lastElementChild)}}async function ee(G){const se=await i.database.getEmojiByUnicodeOrName(G),ge=[...i.currentEmojis,...i.currentFavorites].find(Ae=>Ae.id===G),je=ge.unicode&&N(ge,i.currentSkinTone);return await i.database.incrementFavoriteEmojiCount(G),{emoji:se,skinTone:i.currentSkinTone,...je&&{unicode:je},...ge.name&&{name:ge.name}}}async function _(G){const se=ee(G);y("emoji-click-sync",se),y("emoji-click",await se)}function O(G){const{target:se}=G;if(!se.classList.contains("emoji"))return;rr(G);const ge=se.id.substring(4);_(ge)}function H(G){i.currentSkinTone=G,i.skinTonePickerExpanded=!1,h("skintone-button"),y("skin-tone-change",{skinTone:G}),i.database.setPreferredSkinTone(G)}function I(G){const{target:{id:se}}=G,ge=se&&se.match(/^skintone-(\d)/);if(!ge)return;rr(G);const je=parseInt(ge[1],10);H(je)}function V(G){i.skinTonePickerExpanded=!i.skinTonePickerExpanded,i.activeSkinTone=i.currentSkinTone,i.skinTonePickerExpanded&&(rr(G),tu(()=>h("skintone-list")))}d(()=>{i.skinTonePickerExpanded?a.skinToneDropdown.addEventListener("transitionend",()=>{i.skinTonePickerExpandedAfterAnimation=!0},{once:!0}):i.skinTonePickerExpandedAfterAnimation=!1});function ne(G){if(!i.skinTonePickerExpanded)return;const se=async ge=>{rr(G),i.activeSkinTone=ge};switch(G.key){case"ArrowUp":return se(up(!0,i.activeSkinTone,i.skinTones));case"ArrowDown":return se(up(!1,i.activeSkinTone,i.skinTones));case"Home":return se(0);case"End":return se(i.skinTones.length-1);case"Enter":return rr(G),H(i.activeSkinTone);case"Escape":return rr(G),i.skinTonePickerExpanded=!1,h("skintone-button")}}function J(G){if(i.skinTonePickerExpanded)switch(G.key){case" ":return rr(G),H(i.activeSkinTone)}}async function me(G){const{relatedTarget:se}=G;(!se||se.id!=="skintone-list")&&(i.skinTonePickerExpanded=!1)}function pe(G){i.rawSearchText=G.target.value}return{$set(G){Yc(i,G)},$destroy(){o.abort()}}}const Tz="https://cdn.jsdelivr.net/npm/emoji-picker-element-data@^1/en/emojibase/data.json",Rz="en";var Az={categoriesLabel:"Categories",emojiUnsupportedMessage:"Your browser does not support color emoji.",favoritesLabel:"Favorites",loadingMessage:"Loading…",networkErrorMessage:"Could not load emoji.",regionLabel:"Emoji picker",searchDescription:"When search results are available, press up or down to select and enter to choose.",searchLabel:"Search",searchResultsLabel:"Search results",skinToneDescription:"When expanded, press up or down to select and enter to choose.",skinToneLabel:"Choose a skin tone (currently {skinTone})",skinTonesLabel:"Skin tones",skinTones:["Default","Light","Medium-Light","Medium","Medium-Dark","Dark"],categories:{custom:"Custom","smileys-emotion":"Smileys and emoticons","people-body":"People and body","animals-nature":"Animals and nature","food-drink":"Food and drink","travel-places":"Travel and places",activities:"Activities",objects:"Objects",symbols:"Symbols",flags:"Flags"}},_z=':host{--emoji-size:1.375rem;--emoji-padding:0.5rem;--category-emoji-size:var(--emoji-size);--category-emoji-padding:var(--emoji-padding);--indicator-height:3px;--input-border-radius:0.5rem;--input-border-size:1px;--input-font-size:1rem;--input-line-height:1.5;--input-padding:0.25rem;--num-columns:8;--outline-size:2px;--border-size:1px;--border-radius:0;--skintone-border-radius:1rem;--category-font-size:1rem;display:flex;width:min-content;height:400px}:host,:host(.light){color-scheme:light;--background:#fff;--border-color:#e0e0e0;--indicator-color:#385ac1;--input-border-color:#999;--input-font-color:#111;--input-placeholder-color:#999;--outline-color:#999;--category-font-color:#111;--button-active-background:#e6e6e6;--button-hover-background:#d9d9d9}:host(.dark){color-scheme:dark;--background:#222;--border-color:#444;--indicator-color:#5373ec;--input-border-color:#ccc;--input-font-color:#efefef;--input-placeholder-color:#ccc;--outline-color:#fff;--category-font-color:#efefef;--button-active-background:#555555;--button-hover-background:#484848}@media (prefers-color-scheme:dark){:host{color-scheme:dark;--background:#222;--border-color:#444;--indicator-color:#5373ec;--input-border-color:#ccc;--input-font-color:#efefef;--input-placeholder-color:#ccc;--outline-color:#fff;--category-font-color:#efefef;--button-active-background:#555555;--button-hover-background:#484848}}:host([hidden]){display:none}button{margin:0;padding:0;border:0;background:0 0;box-shadow:none;-webkit-tap-highlight-color:transparent}button::-moz-focus-inner{border:0}input{padding:0;margin:0;line-height:1.15;font-family:inherit}input[type=search]{-webkit-appearance:none}:focus{outline:var(--outline-color) solid var(--outline-size);outline-offset:calc(-1*var(--outline-size))}:host([data-js-focus-visible]) :focus:not([data-focus-visible-added]){outline:0}:focus:not(:focus-visible){outline:0}.hide-focus{outline:0}*{box-sizing:border-box}.picker{contain:content;display:flex;flex-direction:column;background:var(--background);border:var(--border-size) solid var(--border-color);border-radius:var(--border-radius);width:100%;height:100%;overflow:hidden;--total-emoji-size:calc(var(--emoji-size) + (2 * var(--emoji-padding)));--total-category-emoji-size:calc(var(--category-emoji-size) + (2 * var(--category-emoji-padding)))}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.hidden{opacity:0;pointer-events:none}.abs-pos{position:absolute;left:0;top:0}.gone{display:none!important}.skintone-button-wrapper,.skintone-list{background:var(--background);z-index:3}.skintone-button-wrapper.expanded{z-index:1}.skintone-list{position:absolute;inset-inline-end:0;top:0;z-index:2;overflow:visible;border-bottom:var(--border-size) solid var(--border-color);border-radius:0 0 var(--skintone-border-radius) var(--skintone-border-radius);will-change:transform;transition:transform .2s ease-in-out;transform-origin:center 0}@media (prefers-reduced-motion:reduce){.skintone-list{transition-duration:.001s}}@supports not (inset-inline-end:0){.skintone-list{right:0}}.skintone-list.no-animate{transition:none}.tabpanel{overflow-y:auto;scrollbar-gutter:stable;-webkit-overflow-scrolling:touch;will-change:transform;min-height:0;flex:1;contain:content}.emoji-menu{display:grid;grid-template-columns:repeat(var(--num-columns),var(--total-emoji-size));justify-content:space-around;align-items:flex-start;width:100%}.emoji-menu.visibility-auto{content-visibility:auto;contain-intrinsic-size:calc(var(--num-columns)*var(--total-emoji-size)) calc(var(--num-rows)*var(--total-emoji-size))}.category{padding:var(--emoji-padding);font-size:var(--category-font-size);color:var(--category-font-color)}.emoji,button.emoji{font-size:var(--emoji-size);display:flex;align-items:center;justify-content:center;border-radius:100%;height:var(--total-emoji-size);width:var(--total-emoji-size);line-height:1;overflow:hidden;font-family:var(--emoji-font-family);cursor:pointer}@media (hover:hover) and (pointer:fine){.emoji:hover,button.emoji:hover{background:var(--button-hover-background)}}.emoji.active,.emoji:active,button.emoji.active,button.emoji:active{background:var(--button-active-background)}.onscreen .custom-emoji::after{content:"";width:var(--emoji-size);height:var(--emoji-size);background-repeat:no-repeat;background-position:center center;background-size:contain;background-image:var(--custom-emoji-background)}.nav,.nav-button{align-items:center}.nav{display:grid;justify-content:space-between;contain:content}.nav-button{display:flex;justify-content:center}.nav-emoji{font-size:var(--category-emoji-size);width:var(--total-category-emoji-size);height:var(--total-category-emoji-size)}.indicator-wrapper{display:flex;border-bottom:1px solid var(--border-color)}.indicator{width:calc(100%/var(--num-groups));height:var(--indicator-height);opacity:var(--indicator-opacity);background-color:var(--indicator-color);will-change:transform,opacity;transition:opacity .1s linear,transform .25s ease-in-out}@media (prefers-reduced-motion:reduce){.indicator{will-change:opacity;transition:opacity .1s linear}}.pad-top,input.search{background:var(--background);width:100%}.pad-top{height:var(--emoji-padding);z-index:3}.search-row{display:flex;align-items:center;position:relative;padding-inline-start:var(--emoji-padding);padding-bottom:var(--emoji-padding)}.search-wrapper{flex:1;min-width:0}input.search{padding:var(--input-padding);border-radius:var(--input-border-radius);border:var(--input-border-size) solid var(--input-border-color);color:var(--input-font-color);font-size:var(--input-font-size);line-height:var(--input-line-height)}input.search::placeholder{color:var(--input-placeholder-color)}.favorites{overflow-y:auto;scrollbar-gutter:stable;display:flex;flex-direction:row;border-top:var(--border-size) solid var(--border-color);contain:content}.message{padding:var(--emoji-padding)}';const u5=["customEmoji","customCategorySorting","database","dataSource","i18n","locale","skinToneEmoji","emojiVersion"],Mz=`:host{--emoji-font-family:${i5}}`;class bg extends HTMLElement{constructor(n){super(),this.attachShadow({mode:"open"});const a=document.createElement("style");a.textContent=_z+Mz,this.shadowRoot.appendChild(a),this._ctx={locale:Rz,dataSource:Tz,skinToneEmoji:FL,customCategorySorting:qL,customEmoji:null,i18n:Az,emojiVersion:null,...n};for(const o of u5)o!=="database"&&Object.prototype.hasOwnProperty.call(this,o)&&(this._ctx[o]=this[o],delete this[o]);this._dbFlush()}connectedCallback(){Ix(this),this._cmp||(this._cmp=kz(this.shadowRoot,this._ctx))}disconnectedCallback(){Ix(this),wu(()=>{if(!this.isConnected&&this._cmp){this._cmp.$destroy(),this._cmp=void 0;const{database:n}=this._ctx;n.close().catch(a=>console.error(a))}})}static get observedAttributes(){return["locale","data-source","skin-tone-emoji","emoji-version"]}attributeChangedCallback(n,a,o){this._set(n.replace(/-([a-z])/g,(l,i)=>i.toUpperCase()),n==="emoji-version"?parseFloat(o):o)}_set(n,a){this._ctx[n]=a,this._cmp&&this._cmp.$set({[n]:a}),["locale","dataSource"].includes(n)&&this._dbFlush()}_dbCreate(){const{locale:n,dataSource:a,database:o}=this._ctx;(!o||o.locale!==n||o.dataSource!==a)&&this._set("database",new UL({locale:n,dataSource:a}))}_dbFlush(){wu(()=>this._dbCreate())}}const d5={};for(const e of u5)d5[e]={get(){return e==="database"&&this._dbCreate(),this._ctx[e]},set(n){if(e==="database")throw new Error("database is read-only");this._set(e,n)}};Object.defineProperties(bg.prototype,d5);function Ix(e){e instanceof bg||Object.setPrototypeOf(e,customElements.get(e.tagName.toLowerCase()).prototype)}customElements.get("emoji-picker")||customElements.define("emoji-picker",bg);function Oz({onPick:e}){const n=f.useRef(null);return f.useEffect(()=>{const a=n.current;if(!a)return;const o=document.createElement("emoji-picker");o.className="unicode-emoji-picker-el",o.setAttribute("data-theme","dark");const l=i=>{var h;const d=i.detail,m=(d==null?void 0:d.unicode)??((h=d==null?void 0:d.emoji)==null?void 0:h.unicode);m&&e(m)};return o.addEventListener("emoji-click",l),a.appendChild(o),()=>{o.removeEventListener("emoji-click",l),o.remove()}},[e]),c.jsx("div",{ref:n,className:"unicode-emoji-panel-host overflow-hidden rounded-lg border border-border/50"})}const Dz=/^<(a?):([^:>]+):(\d{17,20})>$/;function nu(e,n,a=!1){return`${a?"`}function f5(e){const n=String(e??"").trim();if(!n)return{kind:"empty",value:""};const a=n.match(Dz);return a?{kind:"custom",value:n,animated:a[1]==="a",name:a[2],id:a[3]}:{kind:"unicode",value:n}}function m5(e,n=!1){return`https://cdn.discordapp.com/emojis/${e}.${n?"gif":"png"}?size=64`}function Lz(e,n=[]){const a=f5(e);if(a.kind==="custom"&&a.id&&!a.name){const o=n.find(l=>l.id===a.id);if(o)return{kind:"custom",value:nu(o.name,o.id,o.animated),name:o.name,id:o.id,animated:o.animated}}if(a.kind==="custom"&&a.id){const o=n.find(l=>l.id===a.id);if(o&&a.value!==nu(o.name,o.id,o.animated))return{kind:"custom",value:nu(o.name,o.id,o.animated),name:o.name,id:o.id,animated:o.animated}}return a}function zz({value:e,guildEmojis:n,size:a="md"}){const o=Lz(e,n),l=a==="lg"?"size-10":"size-7",i=a==="lg"?"text-3xl":"text-2xl";return o.kind==="empty"?c.jsx("span",{className:"text-sm text-muted-foreground",children:"No emoji selected"}):o.kind==="custom"&&o.id?c.jsxs("span",{className:"inline-flex items-center gap-3",children:[c.jsx("img",{src:m5(o.id,o.animated),alt:o.name??"custom emoji",className:ie(l,"shrink-0 rounded-md"),loading:"lazy"}),c.jsxs("span",{className:"flex flex-col items-start gap-0.5",children:[c.jsxs("span",{className:"text-sm font-medium",children:[":",o.name,":"]}),c.jsx("span",{className:"font-mono text-xs text-muted-foreground",children:o.value})]})]}):c.jsx("span",{className:ie(i,"leading-none"),title:o.value,children:o.value})}function Pz({id:e,label:n,description:a,value:o,onChange:l,readOnly:i}){const[d,m]=f.useState(!1),[h,p]=f.useState("server"),[y,v]=f.useState(null),[x,S]=f.useState(""),[j,C]=f.useState(!1),[N,E]=f.useState(""),k=f.useCallback(async(Z=!1)=>{C(!0),S("");try{const Q=await ke.getGuildResources(Z);Q.data?(v(Q.data),Array.isArray(Q.data.emojis)||S("Server response is missing emojis. Restart the bot after updating.")):S("No guild data returned.")}catch(Q){S(Q instanceof Error?Q.message:"Could not load server emojis")}finally{C(!1)}},[]);f.useEffect(()=>{d&&k(!0)},[d,k]);const A=f.useMemo(()=>Array.isArray(y==null?void 0:y.emojis)?y.emojis:[],[y]),R=f.useMemo(()=>{const Z=N.trim().toLowerCase();return Z?A.filter(Q=>Q.name.toLowerCase().includes(Z)):A},[A,N]),L=String(o??""),z=f5(L);function B(Z){l(Z),m(!1)}function U(Z){l(nu(Z.name,Z.id,Z.animated)),m(!1)}function q(){l("")}return c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{htmlFor:e,children:n}),a?c.jsx("p",{className:"text-xs text-muted-foreground",children:a}):null,c.jsxs("div",{className:"flex flex-col gap-3 rounded-xl border border-border bg-card p-3 sm:flex-row sm:items-center sm:justify-between",children:[c.jsx("div",{className:"min-w-0 flex-1",children:c.jsx(zz,{value:o,guildEmojis:A,size:"lg"})}),c.jsxs("div",{className:"flex shrink-0 gap-2",children:[c.jsx(be,{type:"button",variant:"secondary",size:"sm",disabled:i,onClick:()=>m(!0),children:"Choose emoji"}),L?c.jsx(be,{type:"button",variant:"ghost",size:"sm",disabled:i,onClick:q,children:"Clear"}):null]})]}),c.jsx($w,{open:d,onOpenChange:m,children:c.jsxs(Zh,{className:"max-h-[min(90vh,720px)] max-w-lg gap-0 overflow-hidden p-0 sm:max-w-xl",children:[c.jsxs(Qh,{className:"space-y-1 border-b border-border px-5 py-4",children:[c.jsx(Jh,{children:"Choose emoji"}),c.jsxs(eg,{children:["Server emojis save as"," ",c.jsx("code",{className:"rounded bg-muted px-1 text-xs",children:"<:name:id>"}),". Standard emojis save as Unicode characters."]})]}),c.jsxs(Yj,{value:h,onValueChange:Z=>p(Z),className:"flex min-h-0 flex-1 flex-col",children:[c.jsxs(fg,{className:"mx-5 mt-3 grid h-9 w-auto grid-cols-2",children:[c.jsx(Ka,{value:"server",children:"Server"}),c.jsx(Ka,{value:"unicode",children:"All emojis"})]}),c.jsxs(Wp,{value:"server",className:"mt-0 flex-1 px-5 pb-4 pt-3",children:[c.jsxs("div",{className:"relative mb-3",children:[c.jsx(ye,{icon:_u,size:16,className:"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"}),c.jsx(rt,{placeholder:"Search by name…",value:N,onChange:Z=>E(Z.target.value),className:"h-9 border-border bg-secondary/30 pl-9"})]}),y?c.jsxs("p",{className:"mb-2 text-xs text-muted-foreground",children:[y.guildName," · ",A.length," custom emoji",A.length===1?"":"s"]}):null,c.jsx(hi,{className:"h-[min(50vh,320px)] rounded-lg border border-border/50 bg-secondary/20",children:j?c.jsx("div",{className:"grid grid-cols-6 gap-2 p-3 sm:grid-cols-8",children:Array.from({length:24}).map((Z,Q)=>c.jsx(Fe,{className:"aspect-square rounded-lg"},Q))}):x?c.jsxs("div",{className:"space-y-3 p-4 text-center",children:[c.jsx("p",{className:"text-sm text-destructive",children:x}),c.jsx(be,{type:"button",variant:"outline",size:"sm",onClick:()=>void k(!0),children:"Retry"})]}):R.length===0?c.jsx("p",{className:"p-6 text-center text-sm text-muted-foreground",children:A.length===0?"No custom emojis in this server, or the bot needs the Guild Emojis intent (restart after update).":"No emojis match your search."}):c.jsx("div",{className:"grid grid-cols-6 gap-1.5 p-2 sm:grid-cols-8",children:R.map(Z=>{const Q=z.kind==="custom"&&z.id===Z.id;return c.jsx("button",{type:"button",title:`:${Z.name}:`,className:ie("flex aspect-square items-center justify-center rounded-lg border border-transparent bg-background/50 transition-colors hover:border-primary/30 hover:bg-accent",Q&&"border-primary/50 bg-primary/15 ring-2 ring-primary/30"),onClick:()=>U(Z),children:c.jsx("img",{src:Z.url??m5(Z.id,Z.animated),alt:Z.name,className:"size-8 object-contain",loading:"lazy"})},Z.id)})})})]}),c.jsx(Wp,{value:"unicode",className:"mt-0 px-5 pb-4 pt-3",children:c.jsx(Oz,{onPick:B})})]})]})}),c.jsx(rt,{value:L,disabled:i,onChange:Z=>l(Z.target.value),placeholder:"Unicode or <:name:id>",className:"border-border bg-secondary/30 font-mono text-xs"})]})}const Ux="BOT_TOKEN",Bz="MODEL_API_KEY";function yg({field:e,value:n,onChange:a,readOnly:o}){const l=e.path.replace(/\./g,"-");if(e.type==="googleFont")return c.jsx(UD,{field:e,value:n,onChange:a,readOnly:o});if(e.type==="boolean")return c.jsxs("div",{className:"flex items-center justify-between gap-4 rounded-lg border border-border bg-secondary/20 px-3 py-2.5",children:[c.jsxs("div",{children:[c.jsx(Ze,{htmlFor:l,className:"font-medium",children:e.label}),e.description?c.jsx("p",{className:"text-xs text-muted-foreground",children:e.description}):null]}),c.jsx(Dn,{id:l,checked:!!n,disabled:o,onCheckedChange:a})]});if(e.type==="stringList")return c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{children:e.label}),e.description?c.jsx("p",{className:"text-xs text-muted-foreground",children:e.description}):null,c.jsx(BD,{value:Array.isArray(n)?n:[],onChange:a})]});if(e.type==="select"&&e.options)return c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{htmlFor:l,children:e.label}),c.jsxs(ud,{value:String(n??""),disabled:o,onValueChange:a,children:[c.jsx(gi,{id:l,className:"border-border bg-secondary/30",children:c.jsx(dd,{placeholder:"Select…"})}),c.jsx(vi,{children:e.options.map(d=>c.jsx(no,{value:d.value,children:d.label},d.value))})]})]});if(e.type==="color"){const d=lr(String(n??"#000000"));return c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{htmlFor:l,children:e.label}),e.description?c.jsx("p",{className:"text-xs text-muted-foreground",children:e.description}):null,c.jsxs("div",{className:"flex gap-2",children:[c.jsx(cg,{value:d,onChange:a}),c.jsx(rt,{id:l,value:d,disabled:o,onChange:m=>a(m.target.value),onBlur:m=>a(lr(m.target.value,d)),className:"border-border bg-secondary/30 font-mono uppercase",placeholder:"#000000",maxLength:7})]})]})}if(e.type==="emoji")return c.jsx(Pz,{id:l,label:e.label,description:e.description,value:n,onChange:a,readOnly:o});if(e.type==="discordChannel"||e.type==="discordRole"||e.type==="discordCategory"){const d=e.type==="discordRole"?"role":e.type==="discordCategory"?"category":"channel";return c.jsx(HD,{id:l,label:e.label,description:e.description,kind:d,value:n,onChange:a,readOnly:o})}if(e.type==="secret"){const d=n===Ux||n===Bz||!n?"":"••••••••••••";return c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{htmlFor:l,children:e.label}),e.description?c.jsx("p",{className:"text-xs text-muted-foreground",children:e.description}):null,c.jsx(rt,{id:l,type:"password",value:d,disabled:o,placeholder:e.placeholder??"Unchanged if empty",onChange:m=>{const h=m.target.value;a(h||(e.placeholder??Ux))},className:"border-border bg-secondary/30"})]})}const i="border-border bg-secondary/30";return e.type==="textarea"?c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{htmlFor:l,children:e.label}),c.jsx(sd,{id:l,value:String(n??""),disabled:o,onChange:d=>a(d.target.value),className:`min-h-[88px] ${i}`})]}):c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{htmlFor:l,children:e.label}),e.description?c.jsx("p",{className:"text-xs text-muted-foreground",children:e.description}):null,c.jsx(rt,{id:l,type:e.type==="number"?"number":"text",value:n==null?"":String(n),disabled:o,onChange:d=>a(e.type==="number"?Number(d.target.value):d.target.value),placeholder:e.placeholder??(e.type==="discordId"?"Discord snowflake ID":void 0),className:i})]})}const We=f.forwardRef(({className:e,...n},a)=>c.jsx("div",{ref:a,className:ie("rounded-xl border border-border bg-card text-card-foreground shadow-none",e),...n}));We.displayName="Card";const at=f.forwardRef(({className:e,...n},a)=>c.jsx("div",{ref:a,className:ie("flex flex-col space-y-1.5 p-6",e),...n}));at.displayName="CardHeader";const ot=f.forwardRef(({className:e,...n},a)=>c.jsx("div",{ref:a,className:ie("font-semibold leading-none tracking-tight",e),...n}));ot.displayName="CardTitle";const Nt=f.forwardRef(({className:e,...n},a)=>c.jsx("div",{ref:a,className:ie("text-sm text-muted-foreground",e),...n}));Nt.displayName="CardDescription";const qe=f.forwardRef(({className:e,...n},a)=>c.jsx("div",{ref:a,className:ie("p-6 pt-0",e),...n}));qe.displayName="CardContent";const p5=f.forwardRef(({className:e,...n},a)=>c.jsx("div",{ref:a,className:ie("flex items-center p-6 pt-0",e),...n}));p5.displayName="CardFooter";function Hx(e){return e.replace(/_/g," ").replace(/([a-z])([A-Z])/g,"$1 $2").replace(/\b\w/g,n=>n.toUpperCase())}function $z(e,n){if(typeof n=="boolean")return"boolean";if(typeof n=="number")return"number";if(Array.isArray(n))return"stringList";if(typeof n=="string"){const a=e.toLowerCase();return/emoji/.test(a)?"emoji":/guild.?id|guild_id/.test(a)?"discordId":/category/.test(a)?"discordCategory":/channel|forum/.test(a)?"discordChannel":/role|staff|admin|moderator/.test(a)?"discordRole":n.length>100?"textarea":"text"}return"text"}function xg(e,n=""){const a=[];for(const[o,l]of Object.entries(e)){const i=n?`${n}.${o}`:o;if(Array.isArray(l)){if(l.length>0&&typeof l[0]=="object"&&l[0]!==null&&!Array.isArray(l[0]))continue;a.push({path:i,label:Hx(o),type:"stringList"});continue}if(l&&typeof l=="object"&&Object.keys(l).length>0){a.push(...xg(l,i));continue}a.push({path:i,label:Hx(o),type:$z(o,l)})}return a}function Iz(e){const n=xg(e);return n.length===0?[{title:"Configuration",fields:[]}]:[{title:"Settings",description:"Values from this addon’s config file.",fields:n}]}function ao(e,n){const a=n.split(".");let o=e;for(const l of a){if(o==null||typeof o!="object")return;o=o[l]}return o}function Xl(e,n,a){const o=n.split("."),l=structuredClone(e);let i=l;for(let d=0;d"u")return;let n=document.head||document.getElementsByTagName("head")[0],a=document.createElement("style");a.type="text/css",n.appendChild(a),a.styleSheet?a.styleSheet.cssText=e:a.appendChild(document.createTextNode(e))}const Hz=e=>{switch(e){case"success":return Fz;case"info":return Wz;case"warning":return Yz;case"error":return qz;default:return null}},Vz=Array(12).fill(0),Gz=({visible:e,className:n})=>ue.createElement("div",{className:["sonner-loading-wrapper",n].filter(Boolean).join(" "),"data-visible":e},ue.createElement("div",{className:"sonner-spinner"},Vz.map((a,o)=>ue.createElement("div",{className:"sonner-loading-bar",key:`spinner-bar-${o}`})))),Fz=ue.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor",height:"20",width:"20"},ue.createElement("path",{fillRule:"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z",clipRule:"evenodd"})),Yz=ue.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor",height:"20",width:"20"},ue.createElement("path",{fillRule:"evenodd",d:"M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z",clipRule:"evenodd"})),Wz=ue.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor",height:"20",width:"20"},ue.createElement("path",{fillRule:"evenodd",d:"M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z",clipRule:"evenodd"})),qz=ue.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 20 20",fill:"currentColor",height:"20",width:"20"},ue.createElement("path",{fillRule:"evenodd",d:"M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z",clipRule:"evenodd"})),Kz=ue.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",width:"12",height:"12",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"},ue.createElement("line",{x1:"18",y1:"6",x2:"6",y2:"18"}),ue.createElement("line",{x1:"6",y1:"6",x2:"18",y2:"18"})),Xz=()=>{const[e,n]=ue.useState(document.hidden);return ue.useEffect(()=>{const a=()=>{n(document.hidden)};return document.addEventListener("visibilitychange",a),()=>window.removeEventListener("visibilitychange",a)},[]),e};let Jp=1;class Zz{constructor(){this.subscribe=n=>(this.subscribers.push(n),()=>{const a=this.subscribers.indexOf(n);this.subscribers.splice(a,1)}),this.publish=n=>{this.subscribers.forEach(a=>a(n))},this.addToast=n=>{this.publish(n),this.toasts=[...this.toasts,n]},this.create=n=>{var a;const{message:o,...l}=n,i=typeof(n==null?void 0:n.id)=="number"||((a=n.id)==null?void 0:a.length)>0?n.id:Jp++,d=this.toasts.find(h=>h.id===i),m=n.dismissible===void 0?!0:n.dismissible;return this.dismissedToasts.has(i)&&this.dismissedToasts.delete(i),d?this.toasts=this.toasts.map(h=>h.id===i?(this.publish({...h,...n,id:i,title:o}),{...h,...n,id:i,dismissible:m,title:o}):h):this.addToast({title:o,...l,dismissible:m,id:i}),i},this.dismiss=n=>(n?(this.dismissedToasts.add(n),requestAnimationFrame(()=>this.subscribers.forEach(a=>a({id:n,dismiss:!0})))):this.toasts.forEach(a=>{this.subscribers.forEach(o=>o({id:a.id,dismiss:!0}))}),n),this.message=(n,a)=>this.create({...a,message:n}),this.error=(n,a)=>this.create({...a,message:n,type:"error"}),this.success=(n,a)=>this.create({...a,type:"success",message:n}),this.info=(n,a)=>this.create({...a,type:"info",message:n}),this.warning=(n,a)=>this.create({...a,type:"warning",message:n}),this.loading=(n,a)=>this.create({...a,type:"loading",message:n}),this.promise=(n,a)=>{if(!a)return;let o;a.loading!==void 0&&(o=this.create({...a,promise:n,type:"loading",message:a.loading,description:typeof a.description!="function"?a.description:void 0}));const l=Promise.resolve(n instanceof Function?n():n);let i=o!==void 0,d;const m=l.then(async p=>{if(d=["resolve",p],ue.isValidElement(p))i=!1,this.create({id:o,type:"default",message:p});else if(Jz(p)&&!p.ok){i=!1;const v=typeof a.error=="function"?await a.error(`HTTP error! status: ${p.status}`):a.error,x=typeof a.description=="function"?await a.description(`HTTP error! status: ${p.status}`):a.description,j=typeof v=="object"&&!ue.isValidElement(v)?v:{message:v};this.create({id:o,type:"error",description:x,...j})}else if(p instanceof Error){i=!1;const v=typeof a.error=="function"?await a.error(p):a.error,x=typeof a.description=="function"?await a.description(p):a.description,j=typeof v=="object"&&!ue.isValidElement(v)?v:{message:v};this.create({id:o,type:"error",description:x,...j})}else if(a.success!==void 0){i=!1;const v=typeof a.success=="function"?await a.success(p):a.success,x=typeof a.description=="function"?await a.description(p):a.description,j=typeof v=="object"&&!ue.isValidElement(v)?v:{message:v};this.create({id:o,type:"success",description:x,...j})}}).catch(async p=>{if(d=["reject",p],a.error!==void 0){i=!1;const y=typeof a.error=="function"?await a.error(p):a.error,v=typeof a.description=="function"?await a.description(p):a.description,S=typeof y=="object"&&!ue.isValidElement(y)?y:{message:y};this.create({id:o,type:"error",description:v,...S})}}).finally(()=>{i&&(this.dismiss(o),o=void 0),a.finally==null||a.finally.call(a)}),h=()=>new Promise((p,y)=>m.then(()=>d[0]==="reject"?y(d[1]):p(d[1])).catch(y));return typeof o!="string"&&typeof o!="number"?{unwrap:h}:Object.assign(o,{unwrap:h})},this.custom=(n,a)=>{const o=(a==null?void 0:a.id)||Jp++;return this.create({jsx:n(o),id:o,...a}),o},this.getActiveToasts=()=>this.toasts.filter(n=>!this.dismissedToasts.has(n.id)),this.subscribers=[],this.toasts=[],this.dismissedToasts=new Set}}const cn=new Zz,Qz=(e,n)=>{const a=(n==null?void 0:n.id)||Jp++;return cn.addToast({title:e,...n,id:a}),a},Jz=e=>e&&typeof e=="object"&&"ok"in e&&typeof e.ok=="boolean"&&"status"in e&&typeof e.status=="number",eP=Qz,tP=()=>cn.toasts,nP=()=>cn.getActiveToasts(),rs=Object.assign(eP,{success:cn.success,info:cn.info,warning:cn.warning,error:cn.error,custom:cn.custom,message:cn.message,promise:cn.promise,dismiss:cn.dismiss,loading:cn.loading},{getHistory:tP,getToasts:nP});Uz("[data-sonner-toaster][dir=ltr],html[dir=ltr]{--toast-icon-margin-start:-3px;--toast-icon-margin-end:4px;--toast-svg-margin-start:-1px;--toast-svg-margin-end:0px;--toast-button-margin-start:auto;--toast-button-margin-end:0;--toast-close-button-start:0;--toast-close-button-end:unset;--toast-close-button-transform:translate(-35%, -35%)}[data-sonner-toaster][dir=rtl],html[dir=rtl]{--toast-icon-margin-start:4px;--toast-icon-margin-end:-3px;--toast-svg-margin-start:0px;--toast-svg-margin-end:-1px;--toast-button-margin-start:0;--toast-button-margin-end:auto;--toast-close-button-start:unset;--toast-close-button-end:0;--toast-close-button-transform:translate(35%, -35%)}[data-sonner-toaster]{position:fixed;width:var(--width);font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;--gray1:hsl(0, 0%, 99%);--gray2:hsl(0, 0%, 97.3%);--gray3:hsl(0, 0%, 95.1%);--gray4:hsl(0, 0%, 93%);--gray5:hsl(0, 0%, 90.9%);--gray6:hsl(0, 0%, 88.7%);--gray7:hsl(0, 0%, 85.8%);--gray8:hsl(0, 0%, 78%);--gray9:hsl(0, 0%, 56.1%);--gray10:hsl(0, 0%, 52.3%);--gray11:hsl(0, 0%, 43.5%);--gray12:hsl(0, 0%, 9%);--border-radius:8px;box-sizing:border-box;padding:0;margin:0;list-style:none;outline:0;z-index:999999999;transition:transform .4s ease}@media (hover:none) and (pointer:coarse){[data-sonner-toaster][data-lifted=true]{transform:none}}[data-sonner-toaster][data-x-position=right]{right:var(--offset-right)}[data-sonner-toaster][data-x-position=left]{left:var(--offset-left)}[data-sonner-toaster][data-x-position=center]{left:50%;transform:translateX(-50%)}[data-sonner-toaster][data-y-position=top]{top:var(--offset-top)}[data-sonner-toaster][data-y-position=bottom]{bottom:var(--offset-bottom)}[data-sonner-toast]{--y:translateY(100%);--lift-amount:calc(var(--lift) * var(--gap));z-index:var(--z-index);position:absolute;opacity:0;transform:var(--y);touch-action:none;transition:transform .4s,opacity .4s,height .4s,box-shadow .2s;box-sizing:border-box;outline:0;overflow-wrap:anywhere}[data-sonner-toast][data-styled=true]{padding:16px;background:var(--normal-bg);border:1px solid var(--normal-border);color:var(--normal-text);border-radius:var(--border-radius);box-shadow:0 4px 12px rgba(0,0,0,.1);width:var(--width);font-size:13px;display:flex;align-items:center;gap:6px}[data-sonner-toast]:focus-visible{box-shadow:0 4px 12px rgba(0,0,0,.1),0 0 0 2px rgba(0,0,0,.2)}[data-sonner-toast][data-y-position=top]{top:0;--y:translateY(-100%);--lift:1;--lift-amount:calc(1 * var(--gap))}[data-sonner-toast][data-y-position=bottom]{bottom:0;--y:translateY(100%);--lift:-1;--lift-amount:calc(var(--lift) * var(--gap))}[data-sonner-toast][data-styled=true] [data-description]{font-weight:400;line-height:1.4;color:#3f3f3f}[data-rich-colors=true][data-sonner-toast][data-styled=true] [data-description]{color:inherit}[data-sonner-toaster][data-sonner-theme=dark] [data-description]{color:#e8e8e8}[data-sonner-toast][data-styled=true] [data-title]{font-weight:500;line-height:1.5;color:inherit}[data-sonner-toast][data-styled=true] [data-icon]{display:flex;height:16px;width:16px;position:relative;justify-content:flex-start;align-items:center;flex-shrink:0;margin-left:var(--toast-icon-margin-start);margin-right:var(--toast-icon-margin-end)}[data-sonner-toast][data-promise=true] [data-icon]>svg{opacity:0;transform:scale(.8);transform-origin:center;animation:sonner-fade-in .3s ease forwards}[data-sonner-toast][data-styled=true] [data-icon]>*{flex-shrink:0}[data-sonner-toast][data-styled=true] [data-icon] svg{margin-left:var(--toast-svg-margin-start);margin-right:var(--toast-svg-margin-end)}[data-sonner-toast][data-styled=true] [data-content]{display:flex;flex-direction:column;gap:2px}[data-sonner-toast][data-styled=true] [data-button]{border-radius:4px;padding-left:8px;padding-right:8px;height:24px;font-size:12px;color:var(--normal-bg);background:var(--normal-text);margin-left:var(--toast-button-margin-start);margin-right:var(--toast-button-margin-end);border:none;font-weight:500;cursor:pointer;outline:0;display:flex;align-items:center;flex-shrink:0;transition:opacity .4s,box-shadow .2s}[data-sonner-toast][data-styled=true] [data-button]:focus-visible{box-shadow:0 0 0 2px rgba(0,0,0,.4)}[data-sonner-toast][data-styled=true] [data-button]:first-of-type{margin-left:var(--toast-button-margin-start);margin-right:var(--toast-button-margin-end)}[data-sonner-toast][data-styled=true] [data-cancel]{color:var(--normal-text);background:rgba(0,0,0,.08)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast][data-styled=true] [data-cancel]{background:rgba(255,255,255,.3)}[data-sonner-toast][data-styled=true] [data-close-button]{position:absolute;left:var(--toast-close-button-start);right:var(--toast-close-button-end);top:0;height:20px;width:20px;display:flex;justify-content:center;align-items:center;padding:0;color:var(--gray12);background:var(--normal-bg);border:1px solid var(--gray4);transform:var(--toast-close-button-transform);border-radius:50%;cursor:pointer;z-index:1;transition:opacity .1s,background .2s,border-color .2s}[data-sonner-toast][data-styled=true] [data-close-button]:focus-visible{box-shadow:0 4px 12px rgba(0,0,0,.1),0 0 0 2px rgba(0,0,0,.2)}[data-sonner-toast][data-styled=true] [data-disabled=true]{cursor:not-allowed}[data-sonner-toast][data-styled=true]:hover [data-close-button]:hover{background:var(--gray2);border-color:var(--gray5)}[data-sonner-toast][data-swiping=true]::before{content:'';position:absolute;left:-100%;right:-100%;height:100%;z-index:-1}[data-sonner-toast][data-y-position=top][data-swiping=true]::before{bottom:50%;transform:scaleY(3) translateY(50%)}[data-sonner-toast][data-y-position=bottom][data-swiping=true]::before{top:50%;transform:scaleY(3) translateY(-50%)}[data-sonner-toast][data-swiping=false][data-removed=true]::before{content:'';position:absolute;inset:0;transform:scaleY(2)}[data-sonner-toast][data-expanded=true]::after{content:'';position:absolute;left:0;height:calc(var(--gap) + 1px);bottom:100%;width:100%}[data-sonner-toast][data-mounted=true]{--y:translateY(0);opacity:1}[data-sonner-toast][data-expanded=false][data-front=false]{--scale:var(--toasts-before) * 0.05 + 1;--y:translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale)));height:var(--front-toast-height)}[data-sonner-toast]>*{transition:opacity .4s}[data-sonner-toast][data-x-position=right]{right:0}[data-sonner-toast][data-x-position=left]{left:0}[data-sonner-toast][data-expanded=false][data-front=false][data-styled=true]>*{opacity:0}[data-sonner-toast][data-visible=false]{opacity:0;pointer-events:none}[data-sonner-toast][data-mounted=true][data-expanded=true]{--y:translateY(calc(var(--lift) * var(--offset)));height:var(--initial-height)}[data-sonner-toast][data-removed=true][data-front=true][data-swipe-out=false]{--y:translateY(calc(var(--lift) * -100%));opacity:0}[data-sonner-toast][data-removed=true][data-front=false][data-swipe-out=false][data-expanded=true]{--y:translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%));opacity:0}[data-sonner-toast][data-removed=true][data-front=false][data-swipe-out=false][data-expanded=false]{--y:translateY(40%);opacity:0;transition:transform .5s,opacity .2s}[data-sonner-toast][data-removed=true][data-front=false]::before{height:calc(var(--initial-height) + 20%)}[data-sonner-toast][data-swiping=true]{transform:var(--y) translateY(var(--swipe-amount-y,0)) translateX(var(--swipe-amount-x,0));transition:none}[data-sonner-toast][data-swiped=true]{user-select:none}[data-sonner-toast][data-swipe-out=true][data-y-position=bottom],[data-sonner-toast][data-swipe-out=true][data-y-position=top]{animation-duration:.2s;animation-timing-function:ease-out;animation-fill-mode:forwards}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=left]{animation-name:swipe-out-left}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=right]{animation-name:swipe-out-right}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=up]{animation-name:swipe-out-up}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=down]{animation-name:swipe-out-down}@keyframes swipe-out-left{from{transform:var(--y) translateX(var(--swipe-amount-x));opacity:1}to{transform:var(--y) translateX(calc(var(--swipe-amount-x) - 100%));opacity:0}}@keyframes swipe-out-right{from{transform:var(--y) translateX(var(--swipe-amount-x));opacity:1}to{transform:var(--y) translateX(calc(var(--swipe-amount-x) + 100%));opacity:0}}@keyframes swipe-out-up{from{transform:var(--y) translateY(var(--swipe-amount-y));opacity:1}to{transform:var(--y) translateY(calc(var(--swipe-amount-y) - 100%));opacity:0}}@keyframes swipe-out-down{from{transform:var(--y) translateY(var(--swipe-amount-y));opacity:1}to{transform:var(--y) translateY(calc(var(--swipe-amount-y) + 100%));opacity:0}}@media (max-width:600px){[data-sonner-toaster]{position:fixed;right:var(--mobile-offset-right);left:var(--mobile-offset-left);width:100%}[data-sonner-toaster][dir=rtl]{left:calc(var(--mobile-offset-left) * -1)}[data-sonner-toaster] [data-sonner-toast]{left:0;right:0;width:calc(100% - var(--mobile-offset-left) * 2)}[data-sonner-toaster][data-x-position=left]{left:var(--mobile-offset-left)}[data-sonner-toaster][data-y-position=bottom]{bottom:var(--mobile-offset-bottom)}[data-sonner-toaster][data-y-position=top]{top:var(--mobile-offset-top)}[data-sonner-toaster][data-x-position=center]{left:var(--mobile-offset-left);right:var(--mobile-offset-right);transform:none}}[data-sonner-toaster][data-sonner-theme=light]{--normal-bg:#fff;--normal-border:var(--gray4);--normal-text:var(--gray12);--success-bg:hsl(143, 85%, 96%);--success-border:hsl(145, 92%, 87%);--success-text:hsl(140, 100%, 27%);--info-bg:hsl(208, 100%, 97%);--info-border:hsl(221, 91%, 93%);--info-text:hsl(210, 92%, 45%);--warning-bg:hsl(49, 100%, 97%);--warning-border:hsl(49, 91%, 84%);--warning-text:hsl(31, 92%, 45%);--error-bg:hsl(359, 100%, 97%);--error-border:hsl(359, 100%, 94%);--error-text:hsl(360, 100%, 45%)}[data-sonner-toaster][data-sonner-theme=light] [data-sonner-toast][data-invert=true]{--normal-bg:#000;--normal-border:hsl(0, 0%, 20%);--normal-text:var(--gray1)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast][data-invert=true]{--normal-bg:#fff;--normal-border:var(--gray3);--normal-text:var(--gray12)}[data-sonner-toaster][data-sonner-theme=dark]{--normal-bg:#000;--normal-bg-hover:hsl(0, 0%, 12%);--normal-border:hsl(0, 0%, 20%);--normal-border-hover:hsl(0, 0%, 25%);--normal-text:var(--gray1);--success-bg:hsl(150, 100%, 6%);--success-border:hsl(147, 100%, 12%);--success-text:hsl(150, 86%, 65%);--info-bg:hsl(215, 100%, 6%);--info-border:hsl(223, 43%, 17%);--info-text:hsl(216, 87%, 65%);--warning-bg:hsl(64, 100%, 6%);--warning-border:hsl(60, 100%, 9%);--warning-text:hsl(46, 87%, 65%);--error-bg:hsl(358, 76%, 10%);--error-border:hsl(357, 89%, 16%);--error-text:hsl(358, 100%, 81%)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast] [data-close-button]{background:var(--normal-bg);border-color:var(--normal-border);color:var(--normal-text)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast] [data-close-button]:hover{background:var(--normal-bg-hover);border-color:var(--normal-border-hover)}[data-rich-colors=true][data-sonner-toast][data-type=success]{background:var(--success-bg);border-color:var(--success-border);color:var(--success-text)}[data-rich-colors=true][data-sonner-toast][data-type=success] [data-close-button]{background:var(--success-bg);border-color:var(--success-border);color:var(--success-text)}[data-rich-colors=true][data-sonner-toast][data-type=info]{background:var(--info-bg);border-color:var(--info-border);color:var(--info-text)}[data-rich-colors=true][data-sonner-toast][data-type=info] [data-close-button]{background:var(--info-bg);border-color:var(--info-border);color:var(--info-text)}[data-rich-colors=true][data-sonner-toast][data-type=warning]{background:var(--warning-bg);border-color:var(--warning-border);color:var(--warning-text)}[data-rich-colors=true][data-sonner-toast][data-type=warning] [data-close-button]{background:var(--warning-bg);border-color:var(--warning-border);color:var(--warning-text)}[data-rich-colors=true][data-sonner-toast][data-type=error]{background:var(--error-bg);border-color:var(--error-border);color:var(--error-text)}[data-rich-colors=true][data-sonner-toast][data-type=error] [data-close-button]{background:var(--error-bg);border-color:var(--error-border);color:var(--error-text)}.sonner-loading-wrapper{--size:16px;height:var(--size);width:var(--size);position:absolute;inset:0;z-index:10}.sonner-loading-wrapper[data-visible=false]{transform-origin:center;animation:sonner-fade-out .2s ease forwards}.sonner-spinner{position:relative;top:50%;left:50%;height:var(--size);width:var(--size)}.sonner-loading-bar{animation:sonner-spin 1.2s linear infinite;background:var(--gray11);border-radius:6px;height:8%;left:-10%;position:absolute;top:-3.9%;width:24%}.sonner-loading-bar:first-child{animation-delay:-1.2s;transform:rotate(.0001deg) translate(146%)}.sonner-loading-bar:nth-child(2){animation-delay:-1.1s;transform:rotate(30deg) translate(146%)}.sonner-loading-bar:nth-child(3){animation-delay:-1s;transform:rotate(60deg) translate(146%)}.sonner-loading-bar:nth-child(4){animation-delay:-.9s;transform:rotate(90deg) translate(146%)}.sonner-loading-bar:nth-child(5){animation-delay:-.8s;transform:rotate(120deg) translate(146%)}.sonner-loading-bar:nth-child(6){animation-delay:-.7s;transform:rotate(150deg) translate(146%)}.sonner-loading-bar:nth-child(7){animation-delay:-.6s;transform:rotate(180deg) translate(146%)}.sonner-loading-bar:nth-child(8){animation-delay:-.5s;transform:rotate(210deg) translate(146%)}.sonner-loading-bar:nth-child(9){animation-delay:-.4s;transform:rotate(240deg) translate(146%)}.sonner-loading-bar:nth-child(10){animation-delay:-.3s;transform:rotate(270deg) translate(146%)}.sonner-loading-bar:nth-child(11){animation-delay:-.2s;transform:rotate(300deg) translate(146%)}.sonner-loading-bar:nth-child(12){animation-delay:-.1s;transform:rotate(330deg) translate(146%)}@keyframes sonner-fade-in{0%{opacity:0;transform:scale(.8)}100%{opacity:1;transform:scale(1)}}@keyframes sonner-fade-out{0%{opacity:1;transform:scale(1)}100%{opacity:0;transform:scale(.8)}}@keyframes sonner-spin{0%{opacity:1}100%{opacity:.15}}@media (prefers-reduced-motion){.sonner-loading-bar,[data-sonner-toast],[data-sonner-toast]>*{transition:none!important;animation:none!important}}.sonner-loader{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);transform-origin:center;transition:opacity .2s,transform .2s}.sonner-loader[data-visible=false]{opacity:0;transform:scale(.8) translate(-50%,-50%)}");function Wc(e){return e.label!==void 0}const rP=3,aP="24px",oP="16px",Vx=4e3,sP=356,lP=14,iP=45,cP=200;function ar(...e){return e.filter(Boolean).join(" ")}function uP(e){const[n,a]=e.split("-"),o=[];return n&&o.push(n),a&&o.push(a),o}const dP=e=>{var n,a,o,l,i,d,m,h,p;const{invert:y,toast:v,unstyled:x,interacting:S,setHeights:j,visibleToasts:C,heights:N,index:E,toasts:k,expanded:A,removeToast:R,defaultRichColors:L,closeButton:z,style:B,cancelButtonStyle:U,actionButtonStyle:q,className:Z="",descriptionClassName:Q="",duration:fe,position:de,gap:ce,expandByDefault:he,classNames:M,icons:K,closeButtonAriaLabel:re="Close toast"}=e,[oe,ee]=ue.useState(null),[_,O]=ue.useState(null),[H,I]=ue.useState(!1),[V,ne]=ue.useState(!1),[J,me]=ue.useState(!1),[pe,G]=ue.useState(!1),[se,ge]=ue.useState(!1),[je,Ae]=ue.useState(0),[Qe,ct]=ue.useState(0),Tt=ue.useRef(v.duration||fe||Vx),Ca=ue.useRef(null),Rt=ue.useRef(null),Os=E===0,Ds=E+1<=C,Yt=v.type,$r=v.dismissible!==!1,Wt=v.className||"",gd=v.descriptionClassName||"",ja=ue.useMemo(()=>N.findIndex(Te=>Te.toastId===v.id)||0,[N,v.id]),xi=ue.useMemo(()=>{var Te;return(Te=v.closeButton)!=null?Te:z},[v.closeButton,z]),Ea=ue.useMemo(()=>v.duration||fe||Vx,[v.duration,fe]),Ls=ue.useRef(0),dr=ue.useRef(0),Si=ue.useRef(0),Ir=ue.useRef(null),[Na,qt]=de.split("-"),$n=ue.useMemo(()=>N.reduce((Te,ut,At)=>At>=ja?Te:Te+ut.height,0),[N,ja]),Ht=Xz(),vd=v.invert||y,zs=Yt==="loading";dr.current=ue.useMemo(()=>ja*ce+$n,[ja,$n]),ue.useEffect(()=>{Tt.current=Ea},[Ea]),ue.useEffect(()=>{I(!0)},[]),ue.useEffect(()=>{const Te=Rt.current;if(Te){const ut=Te.getBoundingClientRect().height;return ct(ut),j(At=>[{toastId:v.id,height:ut,position:v.position},...At]),()=>j(At=>At.filter(Vt=>Vt.toastId!==v.id))}},[j,v.id]),ue.useLayoutEffect(()=>{if(!H)return;const Te=Rt.current,ut=Te.style.height;Te.style.height="auto";const At=Te.getBoundingClientRect().height;Te.style.height=ut,ct(At),j(Vt=>Vt.find(mt=>mt.toastId===v.id)?Vt.map(mt=>mt.toastId===v.id?{...mt,height:At}:mt):[{toastId:v.id,height:At,position:v.position},...Vt])},[H,v.title,v.description,j,v.id,v.jsx,v.action,v.cancel]);const Xn=ue.useCallback(()=>{ne(!0),Ae(dr.current),j(Te=>Te.filter(ut=>ut.toastId!==v.id)),setTimeout(()=>{R(v)},cP)},[v,R,j,dr]);ue.useEffect(()=>{if(v.promise&&Yt==="loading"||v.duration===1/0||v.type==="loading")return;let Te;return A||S||Ht?(()=>{if(Si.current{Tt.current!==1/0&&(Ls.current=new Date().getTime(),Te=setTimeout(()=>{v.onAutoClose==null||v.onAutoClose.call(v,v),Xn()},Tt.current))})(),()=>clearTimeout(Te)},[A,S,v,Yt,Ht,Xn]),ue.useEffect(()=>{v.delete&&(Xn(),v.onDismiss==null||v.onDismiss.call(v,v))},[Xn,v.delete]);function fo(){var Te;if(K!=null&&K.loading){var ut;return ue.createElement("div",{className:ar(M==null?void 0:M.loader,v==null||(ut=v.classNames)==null?void 0:ut.loader,"sonner-loader"),"data-visible":Yt==="loading"},K.loading)}return ue.createElement(Gz,{className:ar(M==null?void 0:M.loader,v==null||(Te=v.classNames)==null?void 0:Te.loader),visible:Yt==="loading"})}const mo=v.icon||(K==null?void 0:K[Yt])||Hz(Yt);var ka,Zn;return ue.createElement("li",{tabIndex:0,ref:Rt,className:ar(Z,Wt,M==null?void 0:M.toast,v==null||(n=v.classNames)==null?void 0:n.toast,M==null?void 0:M.default,M==null?void 0:M[Yt],v==null||(a=v.classNames)==null?void 0:a[Yt]),"data-sonner-toast":"","data-rich-colors":(ka=v.richColors)!=null?ka:L,"data-styled":!(v.jsx||v.unstyled||x),"data-mounted":H,"data-promise":!!v.promise,"data-swiped":se,"data-removed":V,"data-visible":Ds,"data-y-position":Na,"data-x-position":qt,"data-index":E,"data-front":Os,"data-swiping":J,"data-dismissible":$r,"data-type":Yt,"data-invert":vd,"data-swipe-out":pe,"data-swipe-direction":_,"data-expanded":!!(A||he&&H),"data-testid":v.testId,style:{"--index":E,"--toasts-before":E,"--z-index":k.length-E,"--offset":`${V?je:dr.current}px`,"--initial-height":he?"auto":`${Qe}px`,...B,...v.style},onDragEnd:()=>{me(!1),ee(null),Ir.current=null},onPointerDown:Te=>{Te.button!==2&&(zs||!$r||(Ca.current=new Date,Ae(dr.current),Te.target.setPointerCapture(Te.pointerId),Te.target.tagName!=="BUTTON"&&(me(!0),Ir.current={x:Te.clientX,y:Te.clientY})))},onPointerUp:()=>{var Te,ut,At;if(pe||!$r)return;Ir.current=null;const Vt=Number(((Te=Rt.current)==null?void 0:Te.style.getPropertyValue("--swipe-amount-x").replace("px",""))||0),Ur=Number(((ut=Rt.current)==null?void 0:ut.style.getPropertyValue("--swipe-amount-y").replace("px",""))||0),mt=new Date().getTime()-((At=Ca.current)==null?void 0:At.getTime()),Zt=oe==="x"?Vt:Ur,Ta=Math.abs(Zt)/mt;if(Math.abs(Zt)>=iP||Ta>.11){Ae(dr.current),v.onDismiss==null||v.onDismiss.call(v,v),O(oe==="x"?Vt>0?"right":"left":Ur>0?"down":"up"),Xn(),G(!0);return}else{var Qt,Jt;(Qt=Rt.current)==null||Qt.style.setProperty("--swipe-amount-x","0px"),(Jt=Rt.current)==null||Jt.style.setProperty("--swipe-amount-y","0px")}ge(!1),me(!1),ee(null)},onPointerMove:Te=>{var ut,At,Vt;if(!Ir.current||!$r||((ut=window.getSelection())==null?void 0:ut.toString().length)>0)return;const mt=Te.clientY-Ir.current.y,Zt=Te.clientX-Ir.current.x;var Ta;const Qt=(Ta=e.swipeDirections)!=null?Ta:uP(de);!oe&&(Math.abs(Zt)>1||Math.abs(mt)>1)&&ee(Math.abs(Zt)>Math.abs(mt)?"x":"y");let Jt={x:0,y:0};const po=In=>1/(1.5+Math.abs(In)/20);if(oe==="y"){if(Qt.includes("top")||Qt.includes("bottom"))if(Qt.includes("top")&&mt<0||Qt.includes("bottom")&&mt>0)Jt.y=mt;else{const In=mt*po(mt);Jt.y=Math.abs(In)0)Jt.x=Zt;else{const In=Zt*po(Zt);Jt.x=Math.abs(In)0||Math.abs(Jt.y)>0)&&ge(!0),(At=Rt.current)==null||At.style.setProperty("--swipe-amount-x",`${Jt.x}px`),(Vt=Rt.current)==null||Vt.style.setProperty("--swipe-amount-y",`${Jt.y}px`)}},xi&&!v.jsx&&Yt!=="loading"?ue.createElement("button",{"aria-label":re,"data-disabled":zs,"data-close-button":!0,onClick:zs||!$r?()=>{}:()=>{Xn(),v.onDismiss==null||v.onDismiss.call(v,v)},className:ar(M==null?void 0:M.closeButton,v==null||(o=v.classNames)==null?void 0:o.closeButton)},(Zn=K==null?void 0:K.close)!=null?Zn:Kz):null,(Yt||v.icon||v.promise)&&v.icon!==null&&((K==null?void 0:K[Yt])!==null||v.icon)?ue.createElement("div",{"data-icon":"",className:ar(M==null?void 0:M.icon,v==null||(l=v.classNames)==null?void 0:l.icon)},v.promise||v.type==="loading"&&!v.icon?v.icon||fo():null,v.type!=="loading"?mo:null):null,ue.createElement("div",{"data-content":"",className:ar(M==null?void 0:M.content,v==null||(i=v.classNames)==null?void 0:i.content)},ue.createElement("div",{"data-title":"",className:ar(M==null?void 0:M.title,v==null||(d=v.classNames)==null?void 0:d.title)},v.jsx?v.jsx:typeof v.title=="function"?v.title():v.title),v.description?ue.createElement("div",{"data-description":"",className:ar(Q,gd,M==null?void 0:M.description,v==null||(m=v.classNames)==null?void 0:m.description)},typeof v.description=="function"?v.description():v.description):null),ue.isValidElement(v.cancel)?v.cancel:v.cancel&&Wc(v.cancel)?ue.createElement("button",{"data-button":!0,"data-cancel":!0,style:v.cancelButtonStyle||U,onClick:Te=>{Wc(v.cancel)&&$r&&(v.cancel.onClick==null||v.cancel.onClick.call(v.cancel,Te),Xn())},className:ar(M==null?void 0:M.cancelButton,v==null||(h=v.classNames)==null?void 0:h.cancelButton)},v.cancel.label):null,ue.isValidElement(v.action)?v.action:v.action&&Wc(v.action)?ue.createElement("button",{"data-button":!0,"data-action":!0,style:v.actionButtonStyle||q,onClick:Te=>{Wc(v.action)&&(v.action.onClick==null||v.action.onClick.call(v.action,Te),!Te.defaultPrevented&&Xn())},className:ar(M==null?void 0:M.actionButton,v==null||(p=v.classNames)==null?void 0:p.actionButton)},v.action.label):null)};function Gx(){if(typeof window>"u"||typeof document>"u")return"ltr";const e=document.documentElement.getAttribute("dir");return e==="auto"||!e?window.getComputedStyle(document.documentElement).direction:e}function fP(e,n){const a={};return[e,n].forEach((o,l)=>{const i=l===1,d=i?"--mobile-offset":"--offset",m=i?oP:aP;function h(p){["top","right","bottom","left"].forEach(y=>{a[`${d}-${y}`]=typeof p=="number"?`${p}px`:p})}typeof o=="number"||typeof o=="string"?h(o):typeof o=="object"?["top","right","bottom","left"].forEach(p=>{o[p]===void 0?a[`${d}-${p}`]=m:a[`${d}-${p}`]=typeof o[p]=="number"?`${o[p]}px`:o[p]}):h(m)}),a}const mP=ue.forwardRef(function(n,a){const{id:o,invert:l,position:i="bottom-right",hotkey:d=["altKey","KeyT"],expand:m,closeButton:h,className:p,offset:y,mobileOffset:v,theme:x="light",richColors:S,duration:j,style:C,visibleToasts:N=rP,toastOptions:E,dir:k=Gx(),gap:A=lP,icons:R,containerAriaLabel:L="Notifications"}=n,[z,B]=ue.useState([]),U=ue.useMemo(()=>o?z.filter(H=>H.toasterId===o):z.filter(H=>!H.toasterId),[z,o]),q=ue.useMemo(()=>Array.from(new Set([i].concat(U.filter(H=>H.position).map(H=>H.position)))),[U,i]),[Z,Q]=ue.useState([]),[fe,de]=ue.useState(!1),[ce,he]=ue.useState(!1),[M,K]=ue.useState(x!=="system"?x:typeof window<"u"&&window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"),re=ue.useRef(null),oe=d.join("+").replace(/Key/g,"").replace(/Digit/g,""),ee=ue.useRef(null),_=ue.useRef(!1),O=ue.useCallback(H=>{B(I=>{var V;return(V=I.find(ne=>ne.id===H.id))!=null&&V.delete||cn.dismiss(H.id),I.filter(({id:ne})=>ne!==H.id)})},[]);return ue.useEffect(()=>cn.subscribe(H=>{if(H.dismiss){requestAnimationFrame(()=>{B(I=>I.map(V=>V.id===H.id?{...V,delete:!0}:V))});return}setTimeout(()=>{g0.flushSync(()=>{B(I=>{const V=I.findIndex(ne=>ne.id===H.id);return V!==-1?[...I.slice(0,V),{...I[V],...H},...I.slice(V+1)]:[H,...I]})})})}),[z]),ue.useEffect(()=>{if(x!=="system"){K(x);return}if(x==="system"&&(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?K("dark"):K("light")),typeof window>"u")return;const H=window.matchMedia("(prefers-color-scheme: dark)");try{H.addEventListener("change",({matches:I})=>{K(I?"dark":"light")})}catch{H.addListener(({matches:V})=>{try{K(V?"dark":"light")}catch(ne){console.error(ne)}})}},[x]),ue.useEffect(()=>{z.length<=1&&de(!1)},[z]),ue.useEffect(()=>{const H=I=>{var V;if(d.every(me=>I[me]||I.code===me)){var J;de(!0),(J=re.current)==null||J.focus()}I.code==="Escape"&&(document.activeElement===re.current||(V=re.current)!=null&&V.contains(document.activeElement))&&de(!1)};return document.addEventListener("keydown",H),()=>document.removeEventListener("keydown",H)},[d]),ue.useEffect(()=>{if(re.current)return()=>{ee.current&&(ee.current.focus({preventScroll:!0}),ee.current=null,_.current=!1)}},[re.current]),ue.createElement("section",{ref:a,"aria-label":`${L} ${oe}`,tabIndex:-1,"aria-live":"polite","aria-relevant":"additions text","aria-atomic":"false",suppressHydrationWarning:!0},q.map((H,I)=>{var V;const[ne,J]=H.split("-");return U.length?ue.createElement("ol",{key:H,dir:k==="auto"?Gx():k,tabIndex:-1,ref:re,className:p,"data-sonner-toaster":!0,"data-sonner-theme":M,"data-y-position":ne,"data-x-position":J,style:{"--front-toast-height":`${((V=Z[0])==null?void 0:V.height)||0}px`,"--width":`${sP}px`,"--gap":`${A}px`,...C,...fP(y,v)},onBlur:me=>{_.current&&!me.currentTarget.contains(me.relatedTarget)&&(_.current=!1,ee.current&&(ee.current.focus({preventScroll:!0}),ee.current=null))},onFocus:me=>{me.target instanceof HTMLElement&&me.target.dataset.dismissible==="false"||_.current||(_.current=!0,ee.current=me.relatedTarget)},onMouseEnter:()=>de(!0),onMouseMove:()=>de(!0),onMouseLeave:()=>{ce||de(!1)},onDragEnd:()=>de(!1),onPointerDown:me=>{me.target instanceof HTMLElement&&me.target.dataset.dismissible==="false"||he(!0)},onPointerUp:()=>he(!1)},U.filter(me=>!me.position&&I===0||me.position===H).map((me,pe)=>{var G,se;return ue.createElement(dP,{key:me.id,icons:R,index:pe,toast:me,defaultRichColors:S,duration:(G=E==null?void 0:E.duration)!=null?G:j,className:E==null?void 0:E.className,descriptionClassName:E==null?void 0:E.descriptionClassName,invert:l,visibleToasts:N,closeButton:(se=E==null?void 0:E.closeButton)!=null?se:h,interacting:ce,position:H,style:E==null?void 0:E.style,unstyled:E==null?void 0:E.unstyled,classNames:E==null?void 0:E.classNames,cancelButtonStyle:E==null?void 0:E.cancelButtonStyle,actionButtonStyle:E==null?void 0:E.actionButtonStyle,closeButtonAriaLabel:E==null?void 0:E.closeButtonAriaLabel,removeToast:O,toasts:U.filter(ge=>ge.position==me.position),heights:Z.filter(ge=>ge.position==me.position),setHeights:Q,expandByDefault:m,gap:A,expanded:fe,swipeDirections:n.swipeDirections})})):null}))});async function ru(e){const n=rs.loading("Saving changes…");try{const a=await e();if(rs.success("Changes saved",{id:n}),a.botRestart){const o=rs.loading("Restarting bot…");a.botRestart.success?rs.success("Bot started",{id:o}):rs.error(a.botRestart.error??"Could not start bot",{id:o})}return a}catch(a){throw rs.error(a instanceof Error?a.message:"Save failed",{id:n}),a}}function pP({filename:e}){const{canEditConfig:n}=Ft(),a=!n("supportbot"),[o,l]=f.useState(null),[i,d]=f.useState(null),[m,h]=f.useState(!0),[p,y]=f.useState(!1),[v,x]=f.useState(""),[S,j]=f.useState(""),C=f.useCallback(async()=>{h(!0),x(""),j("");try{const L=(await ke.getAddonConfigJson(e)).data??{};l(L),d(structuredClone(L))}catch(R){x(R instanceof Error?R.message:"Failed to load config")}finally{h(!1)}},[e]);f.useEffect(()=>{C()},[C]);const N=o?Iz(o):null;function E(R,L){l(z=>z&&Xl(z,R,L))}async function k(){if(!(a||!o)){y(!0),x(""),j("");try{const R=await ru(()=>ke.saveAddonConfig(e,o));j(R.message||"Saved."),await C()}catch(R){x(R instanceof Error?R.message:"Save failed")}finally{y(!1)}}}if(m)return c.jsxs("div",{className:"space-y-4",children:[c.jsx(Fe,{className:"h-24 w-full rounded-xl"}),c.jsx(Fe,{className:"h-48 w-full rounded-xl"})]});if(!o||!N)return c.jsx(Ue,{variant:"destructive",children:c.jsx(He,{children:v||"Failed to load"})});const A=i&&JSON.stringify(o)!==JSON.stringify(i);return c.jsxs("div",{className:"space-y-4",children:[c.jsxs("div",{className:"flex flex-wrap items-center justify-end gap-2",children:[A?c.jsx("span",{className:"text-xs text-amber-500",children:"Unsaved changes"}):null,c.jsx(be,{variant:"outline",size:"sm",onClick:C,disabled:m||p,children:"Reload"}),a?c.jsx("span",{className:"text-xs text-muted-foreground",children:"View only"}):c.jsxs(be,{size:"sm",onClick:k,disabled:p||!A,children:[c.jsx(ye,{icon:Au,size:16,className:"mr-1.5"}),p?"Saving…":"Save"]})]}),v?c.jsxs(Ue,{variant:"destructive",children:[c.jsx(ye,{icon:mh,size:18}),c.jsx(He,{children:v})]}):null,S?c.jsx(Ue,{className:"border-primary/30 bg-primary/10 text-primary",children:c.jsx(He,{children:S})}):null,N.map(R=>c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{className:"pb-3",children:[c.jsx(ot,{className:"text-base",children:R.title}),R.description?c.jsx(Nt,{children:R.description}):null]}),c.jsx(qe,{className:"grid gap-4 sm:grid-cols-2",children:R.fields.map(L=>c.jsx("div",{className:L.type==="textarea"||L.type==="stringList"?"sm:col-span-2":"",children:c.jsx(yg,{field:L,value:ao(o,L.path),readOnly:a,onChange:z=>E(L.path,z)})},L.path))})]},R.title))]})}function hP(e){return e.replace(/_/g," ").replace(/([a-z])([A-Z])/g,"$1 $2").replace(/\b\w/g,n=>n.toUpperCase())}function gP(e,n){const a=new Set(e.flatMap(d=>d.fields.map(m=>m.path))),o=xg(n).filter(d=>!a.has(d.path));if(o.length===0)return e;const l=new Map;for(const d of o){const m=d.path.split(".")[0]??"Other",h=l.get(m)??[];h.push(d),l.set(m,h)}const i=[...l.entries()].sort(([d],[m])=>d.localeCompare(m)).map(([d,m])=>({title:hP(d),description:"These options exist in your config file but are not in the main editor sections.",fields:m.sort((h,p)=>h.path.localeCompare(p.path))}));return[...e,...i]}function vP(e){return e!==null&&typeof e=="object"&&"Command"in e}function bP(e){return Object.entries(e).filter(([,n])=>vP(n)).map(([n,a])=>({key:n,...a}))}function yP({data:e,onChange:n}){const a=bP(e);function o(l,i){const d=e[l];n({...e,[l]:{...d,...i}})}return a.length===0?c.jsx("p",{className:"text-sm text-muted-foreground",children:"No commands found in this config."}):c.jsx("div",{className:"grid gap-3 sm:grid-cols-2",children:a.map(l=>c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{className:"flex flex-row items-center gap-2 space-y-0 pb-2",children:[c.jsx("div",{className:"flex size-9 items-center justify-center rounded-lg bg-primary/15 text-primary",children:c.jsx(ye,{icon:ms,size:18})}),c.jsx(ot,{className:"text-base",children:l.key})]}),c.jsxs(qe,{className:"space-y-3",children:[c.jsxs("div",{className:"flex items-center justify-between",children:[c.jsx(Ze,{htmlFor:`${l.key}-enabled`,children:"Enabled"}),c.jsx(Dn,{id:`${l.key}-enabled`,checked:l.Enabled!==!1,onCheckedChange:i=>o(l.key,{Enabled:i})})]}),c.jsxs("div",{className:"space-y-1.5",children:[c.jsx(Ze,{children:"Slash command"}),c.jsx(rt,{value:l.Command??"",onChange:i=>o(l.key,{Command:i.target.value}),className:"border-border bg-secondary/30 font-mono"})]}),c.jsxs("div",{className:"space-y-1.5",children:[c.jsx(Ze,{children:"Description"}),c.jsx(rt,{value:l.Description??"",onChange:i=>o(l.key,{Description:i.target.value}),className:"border-border bg-secondary/30"})]})]})]},l.key))})}function xP(e,n){let a=structuredClone(n);if(e==="supportbot"){const o=ao(a,"General.Token");o&&o!=="BOT_TOKEN"&&(a=Xl(a,"General.Token","BOT_TOKEN"))}if(e==="supportbot-ai"){const o=ao(a,"General.Model_API_Key");o&&o!=="MODEL_API_KEY"&&(a=Xl(a,"General.Model_API_Key","MODEL_API_KEY"))}return a}function SP(e,n,a,o){const l={};for(const i of n)for(const d of i.fields){const m=ao(a,d.path),h=ao(o,d.path);d.type==="secret"&&(m==="BOT_TOKEN"||m==="MODEL_API_KEY"||m===""||m===void 0)||JSON.stringify(m)!==JSON.stringify(h)&&(l[`${e}:${d.path}`]=m)}return l}function wP({configFile:e}){const{canEditConfig:n}=Ft(),a=!n(e),[o,l]=f.useState(null),[i,d]=f.useState(null),[m,h]=f.useState(!0),[p,y]=f.useState(!1),[v,x]=f.useState(""),[S,j]=f.useState(""),[C,N]=f.useState("visual"),[E,k]=f.useState(""),[A,R]=f.useState(""),L=V7(e),z=f.useMemo(()=>!L||L==="commands"||!o?L:gP(L,o),[L,o]),B=f.useCallback(async()=>{h(!0),x(""),j("");try{const he=(await ke.getConfigJson(e)).data??{},M=xP(e,he);l(M),d(M);const re=(await ke.getConfigRaw(e)).data??"",oe=D4(re,e);k(oe),R(oe)}catch(ce){x(ce instanceof Error?ce.message:"Failed to load config")}finally{h(!1)}},[e]);f.useEffect(()=>{B()},[B]);function U(ce,he){l(M=>M&&Xl(M,ce,he))}async function q(){if(!(a||E===A)){y(!0),x(""),j("");try{const ce=await ru(()=>ke.saveConfigRaw(e,E));j(ce.message||"Config saved."),await B()}catch(ce){x(ce instanceof Error?ce.message:"Save failed")}finally{y(!1)}}}async function Z(){if(!(a||!o||!i)){if(C==="yaml"){await q();return}y(!0),x(""),j("");try{if(L==="commands"){const ce={};for(const M of Object.keys(o))JSON.stringify(o[M])!==JSON.stringify(i[M])&&(ce[`${e}:${M}`]=o[M]);if(Object.keys(ce).length===0){j("No changes to save.");return}const he=await ru(()=>ke.updateConfigFields(ce));j(he.message||"")}else if(z&&z!=="commands"){const ce=SP(e,z,o,i);if(Object.keys(ce).length===0){j("No changes to save.");return}const he=await ru(()=>ke.updateConfigFields(ce));j(he.message||"")}await B()}catch(ce){x(ce instanceof Error?ce.message:"Save failed")}finally{y(!1)}}}if(L===null)return c.jsx(Ue,{children:c.jsx(He,{children:"No visual editor available for this file."})});if(m)return c.jsxs("div",{className:"space-y-4",children:[c.jsx(Fe,{className:"h-24 w-full rounded-xl"}),c.jsx(Fe,{className:"h-48 w-full rounded-xl"})]});if(!o)return c.jsx(Ue,{variant:"destructive",children:c.jsx(He,{children:v||"Failed to load"})});const Q=i&&JSON.stringify(o)!==JSON.stringify(i),de=C==="yaml"?E!==A:Q;return c.jsxs("div",{className:"space-y-4",children:[c.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-2",children:[c.jsxs("div",{className:"flex rounded-lg border border-border p-0.5",children:[c.jsx(be,{type:"button",variant:C==="visual"?"secondary":"ghost",size:"sm",className:"h-8",onClick:()=>N("visual"),children:"Visual"}),c.jsx(be,{type:"button",variant:C==="yaml"?"secondary":"ghost",size:"sm",className:"h-8",onClick:()=>N("yaml"),children:"YAML"})]}),c.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[de?c.jsx("span",{className:"text-xs text-amber-500",children:"Unsaved changes"}):null,c.jsx(be,{variant:"outline",size:"sm",onClick:B,disabled:m||p,children:"Reload"}),a?c.jsx("span",{className:"text-xs text-muted-foreground",children:"View only"}):c.jsxs(be,{size:"sm",onClick:Z,disabled:p||!de,children:[c.jsx(ye,{icon:Au,size:16,className:"mr-1.5"}),p?"Saving…":"Save"]})]})]}),v?c.jsxs(Ue,{variant:"destructive",children:[c.jsx(ye,{icon:mh,size:18}),c.jsx(He,{children:v})]}):null,S?c.jsx(Ue,{className:"border-primary/30 bg-primary/10 text-primary",children:c.jsx(He,{children:S})}):null,C==="yaml"?c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{className:"pb-3",children:[c.jsx(ot,{className:"text-base",children:"Raw YAML"}),c.jsx(Nt,{children:"Edit the full file. Use this for nested lists (e.g. welcome buttons) or any option not in Visual mode."})]}),c.jsx(qe,{children:c.jsx(sd,{value:E,disabled:a,onChange:ce=>k(ce.target.value),className:"min-h-[min(520px,60vh)] font-mono text-xs leading-relaxed",spellCheck:!1})})]}):L==="commands"?c.jsx(yP,{data:o,onChange:ce=>l(ce)}):z&&z!=="commands"?z.map(ce=>c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{className:"pb-3",children:[c.jsx(ot,{className:"text-base",children:ce.title}),ce.description?c.jsx(Nt,{children:ce.description}):null]}),c.jsx(qe,{className:"grid gap-4 sm:grid-cols-2",children:ce.fields.map(he=>c.jsx("div",{className:he.type==="textarea"||he.type==="stringList"?"sm:col-span-2":"",children:c.jsx(yg,{field:he,value:ao(o,he.path),readOnly:a,onChange:M=>U(he.path,M)})},he.path))})]},ce.title)):null]})}const CP=ke.listConfigs(),jP={supportbot:"Bot config","ticket-panel":"Ticket system",commands:"Commands",messages:"Messages","supportbot-ai":"AI assistant"};function EP(){var R,L;const{permissions:e,canViewConfig:n}=Ft(),{files:a}=R0(),[o,l]=dh(),i=o.get("file"),d=n("supportbot"),m=f.useMemo(()=>CP.filter(z=>n(z)),[n]),h=f.useMemo(()=>d?a:[],[d,a]),p=f.useMemo(()=>[...m,...h.map(z=>is(z))],[m,h]),y=p[0]??"",[v,x]=f.useState(()=>i&&p.includes(i)?i:y);if(f.useEffect(()=>{i&&p.includes(i)?x(i):p.includes(v)||(x(y),y&&l({file:y}))},[i,p,v,y,l]),p.length===0)return c.jsx(Mr,{to:Sh(e),replace:!0});function S(z){x(z),l({file:z})}const j=V4(v),C=(j==null?void 0:j.type)==="addon",N=C?j.file:null,E=C?A0(N):jP[v]??"Configuration",k=C?!!((R=e==null?void 0:e.configs.supportbot)!=null&&R.edit):e?!!((L=e.configs[v])!=null&&L.edit):!1,A=C?N:`${v}.yml`;return c.jsxs("div",{className:"space-y-6",children:[c.jsxs("div",{children:[c.jsx("h1",{className:"text-2xl font-bold tracking-tight",children:E}),c.jsx("p",{className:"text-muted-foreground",children:k?`Edit ${A} — changes apply to the running bot without restarting the dashboard.`:`Viewing ${A} — you do not have permission to save changes.`})]}),c.jsxs("div",{className:"grid gap-4 lg:grid-cols-[220px_1fr]",children:[c.jsxs(We,{className:"h-fit border-border bg-card",children:[c.jsx(at,{className:"pb-2",children:c.jsx(ot,{className:"text-sm font-medium text-muted-foreground",children:"Configuration files"})}),c.jsx(qe,{className:"p-0",children:c.jsx(hi,{className:"h-[min(400px,50vh)]",children:c.jsxs("ul",{className:"space-y-3 p-2",children:[m.length>0?c.jsxs("li",{children:[c.jsx("p",{className:"px-3 pb-1 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/80",children:"Core"}),c.jsx("ul",{className:"space-y-0.5",children:m.map(z=>c.jsx("li",{children:c.jsxs("button",{type:"button",onClick:()=>S(z),className:ie("w-full rounded-lg px-3 py-2.5 text-left text-sm transition-colors",z===v?"bg-primary/15 font-medium text-primary":"text-muted-foreground hover:bg-secondary hover:text-foreground"),children:[z,".yml"]})},z))})]}):null,h.length>0?c.jsxs("li",{children:[c.jsx("p",{className:"px-3 pb-1 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/80",children:"Addons"}),c.jsx("ul",{className:"space-y-0.5",children:h.map(z=>{const B=is(z);return c.jsx("li",{children:c.jsx("button",{type:"button",onClick:()=>S(B),className:ie("w-full rounded-lg px-3 py-2.5 text-left text-sm transition-colors",B===v?"bg-primary/15 font-medium text-primary":"text-muted-foreground hover:bg-secondary hover:text-foreground"),children:z})},z)})})]}):null]})})})]}),c.jsx("div",{className:"min-w-0",children:C&&N?c.jsx(pP,{filename:N},N):c.jsx(wP,{configFile:v},v)})]})]})}const NP={oauth_not_configured:"Discord OAuth is not configured. Complete setup at /setup first.",not_allowed:"Your Discord account is not allowed. Ask an owner to add you under Users, or add your ID to OwnerUserIds in Configs/api.yml.",invalid_state:"Login session expired. Please try again.",missing_code:"Discord did not return an authorization code. Please try again.",oauth_failed:"Could not complete Discord sign-in. Check your OAuth settings.",access_denied:"You cancelled Discord sign-in."};function kP(e){return e?NP[e]||"Sign-in failed. Please try again.":""}function TP(){const{branding:e}=Ou(),{isAuthenticated:n,loading:a,loginWithDiscord:o}=Ft(),[l]=dh(),i=f.useMemo(()=>kP(l.get("error")),[l]);return!a&&n?c.jsx(Mr,{to:"/",replace:!0}):c.jsx("div",{className:"flex min-h-svh items-center justify-center bg-background p-4",children:c.jsxs(We,{className:"w-full max-w-md border-border bg-card",children:[c.jsx(at,{className:"space-y-4",children:c.jsxs("div",{className:"flex items-center gap-3",children:[e.hasFavicon&&e.faviconUrl?c.jsx("img",{src:`${e.faviconUrl}${e.updatedAt?`?v=${e.updatedAt}`:""}`,alt:"",className:"size-11 rounded-xl object-contain"}):c.jsx("div",{className:"flex size-11 items-center justify-center rounded-xl bg-primary text-primary-foreground",children:c.jsx(ye,{icon:ku,size:22})}),c.jsxs("div",{children:[c.jsx(ot,{className:"text-xl",children:e.title}),c.jsx(Nt,{children:"Sign in to your dashboard"})]})]})}),c.jsxs(qe,{className:"space-y-4",children:[c.jsxs("p",{className:"text-sm text-muted-foreground",children:["Sign in with Discord. Your application credentials and allowed user IDs are configured in"," ",c.jsx("code",{className:"rounded-md bg-secondary px-1.5 py-0.5 text-xs text-foreground",children:"Configs/api.yml"}),"."]}),i?c.jsx(Ue,{variant:"destructive",children:c.jsx(He,{children:i})}):null,c.jsx(be,{type:"button",className:"w-full",disabled:a,onClick:o,children:a?"Checking session…":"Continue with Discord"})]})]})})}const fs=[{id:"botToken",title:"Bot token",subtitle:"Connect your Discord bot",icon:ku},{id:"secretKey",title:"Session secret",subtitle:"Secure dashboard logins",icon:M4},{id:"oauth",title:"Discord OAuth",subtitle:"Sign in with Discord",icon:N4},{id:"redirectUri",title:"Redirect URL",subtitle:"OAuth callback address",icon:gh},{id:"owners",title:"Dashboard owners",subtitle:"Who can access this panel",icon:yh}];function RP(e){return e==="oauth"?"oauth":e==="owners"?"owners":e}function AP({brandingTitle:e,faviconUrl:n,faviconVersion:a,hasFavicon:o,children:l}){return c.jsx("div",{className:"min-h-svh bg-background",children:c.jsxs("div",{className:"mx-auto flex min-h-svh max-w-6xl flex-col px-4 py-6 md:px-8 md:py-10",children:[c.jsxs("header",{className:"mb-8 flex items-center gap-4",children:[o&&n?c.jsx("img",{src:`${n}${a?`?v=${a}`:""}`,alt:"",className:"size-12 rounded-2xl border border-border object-contain"}):c.jsx("div",{className:"flex size-12 items-center justify-center rounded-2xl border border-border bg-primary text-primary-foreground",children:c.jsx(ye,{icon:ku,size:24})}),c.jsxs("div",{children:[c.jsx("p",{className:"text-xs font-medium uppercase tracking-wider text-muted-foreground",children:"First-time setup"}),c.jsx("h1",{className:"text-2xl font-semibold tracking-tight md:text-3xl",children:e})]})]}),l]})})}function _P({currentIndex:e,validation:n}){const a=(e+1)/fs.length*100;return c.jsxs("div",{className:"space-y-6",children:[c.jsxs("div",{className:"space-y-2",children:[c.jsxs("div",{className:"flex items-center justify-between text-xs text-muted-foreground",children:[c.jsxs("span",{children:["Step ",e+1," of ",fs.length]}),c.jsxs("span",{children:[Math.round(a),"%"]})]}),c.jsx("div",{className:"h-2 overflow-hidden rounded-full bg-secondary",children:c.jsx("div",{className:"h-full rounded-full bg-primary transition-all duration-300 ease-out",style:{width:`${a}%`}})})]}),c.jsx("nav",{className:"hidden space-y-1 md:block","aria-label":"Setup steps",children:fs.map((o,l)=>{var p;const i=RP(o.id),d=(p=n[i])==null?void 0:p.ok,m=l===e,h=lc.jsxs("li",{className:"flex items-center gap-2",children:[c.jsx(ye,{icon:Tu,size:14}),o.globalName||o.username,c.jsx("span",{className:"font-mono text-xs opacity-80",children:o.id})]},o.id))}):null,!e.bot&&!((a=e.users)!=null&&a.length)?c.jsx("p",{children:e.warning||"Verified successfully."}):null]})}):c.jsx(Ue,{variant:"destructive",children:c.jsx(He,{children:e.error})}):null}function OP({values:e,setValues:n,validation:a,validating:o,onValidate:l}){return c.jsxs("div",{className:"flex flex-1 flex-col gap-5",children:[c.jsxs("p",{className:"text-sm leading-relaxed text-muted-foreground",children:["Open the"," ",c.jsx("a",{href:"https://discord.com/developers/applications",target:"_blank",rel:"noreferrer",className:"font-medium text-primary hover:underline",children:"Discord Developer Portal"}),", select your application, go to ",c.jsx("strong",{children:"Bot"}),", and reset or copy the token."]}),c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{htmlFor:"botToken",children:"Bot token"}),c.jsx(rt,{id:"botToken",type:"password",autoComplete:"off",value:e.botToken,onChange:i=>n({...e,botToken:i.target.value}),placeholder:"MTI…",className:"h-11 font-mono text-sm"})]}),c.jsx(yi,{result:a.botToken}),c.jsx("div",{className:"mt-auto",children:c.jsx(be,{type:"button",variant:"secondary",disabled:o||!e.botToken,onClick:l,children:o?"Checking with Discord…":"Verify bot token"})})]})}function DP({values:e,setValues:n,validation:a,validating:o,onValidate:l,onGenerate:i}){return c.jsxs("div",{className:"flex flex-1 flex-col gap-5",children:[c.jsx("p",{className:"text-sm leading-relaxed text-muted-foreground",children:"This random string signs your login cookies. Store it like a password — do not commit it to public repos."}),c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{htmlFor:"secretKey",children:"Session secret"}),c.jsx(rt,{id:"secretKey",type:"password",autoComplete:"off",value:e.secretKey,onChange:d=>n({...e,secretKey:d.target.value}),className:"h-11 font-mono text-sm"})]}),c.jsx(yi,{result:a.secretKey}),c.jsxs("div",{className:"mt-auto flex flex-wrap gap-2",children:[c.jsx(be,{type:"button",variant:"outline",onClick:i,children:"Generate secure secret"}),c.jsx(be,{type:"button",variant:"secondary",disabled:o||!e.secretKey,onClick:l,children:o?"Checking…":"Validate"})]})]})}function LP({values:e,setValues:n,validation:a,validating:o,onValidate:l}){return c.jsxs("div",{className:"flex flex-1 flex-col gap-5",children:[c.jsxs("p",{className:"text-sm leading-relaxed text-muted-foreground",children:["Use the ",c.jsx("strong",{children:"same application"})," as your bot. Under OAuth2, copy the Client ID and Client Secret."]}),c.jsxs("div",{className:"grid gap-4 sm:grid-cols-2",children:[c.jsxs("div",{className:"space-y-2 sm:col-span-2",children:[c.jsx(Ze,{htmlFor:"clientId",children:"Client ID"}),c.jsx(rt,{id:"clientId",value:e.clientId,onChange:i=>n({...e,clientId:i.target.value}),className:"h-11 font-mono text-sm"})]}),c.jsxs("div",{className:"space-y-2 sm:col-span-2",children:[c.jsx(Ze,{htmlFor:"clientSecret",children:"Client Secret"}),c.jsx(rt,{id:"clientSecret",type:"password",autoComplete:"off",value:e.clientSecret,onChange:i=>n({...e,clientSecret:i.target.value}),className:"h-11 font-mono text-sm"})]})]}),c.jsx(yi,{result:a.oauth}),c.jsx("div",{className:"mt-auto",children:c.jsx(be,{type:"button",variant:"secondary",disabled:o||!e.clientId||!e.clientSecret,onClick:l,children:o?"Checking with Discord…":"Verify OAuth credentials"})})]})}function zP({values:e,setValues:n,validation:a}){async function o(){if(e.redirectUri)try{await navigator.clipboard.writeText(e.redirectUri)}catch{}}return c.jsxs("div",{className:"flex flex-1 flex-col gap-5",children:[c.jsxs("p",{className:"text-sm leading-relaxed text-muted-foreground",children:["Add this exact URL under ",c.jsx("strong",{children:"OAuth2 → Redirects"})," in the Developer Portal. It must match character-for-character."]}),c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{htmlFor:"redirectUri",children:"Redirect URI"}),c.jsxs("div",{className:"flex gap-2",children:[c.jsx(rt,{id:"redirectUri",value:e.redirectUri,onChange:l=>n({...e,redirectUri:l.target.value}),className:"h-11 flex-1 font-mono text-xs"}),c.jsx(be,{type:"button",variant:"outline",size:"icon",className:"size-11 shrink-0",onClick:()=>void o(),title:"Copy URL",children:c.jsx(ye,{icon:x0,size:18})})]})]}),a.redirectUri&&!a.redirectUri.ok?c.jsx(yi,{result:a.redirectUri}):c.jsx(Ue,{className:"border-border bg-secondary",children:c.jsx(He,{className:"text-sm text-muted-foreground",children:"We pre-filled this from your current dashboard URL. If you access the panel from a different domain later, update this and add the new URL in Discord too."})})]})}function PP({values:e,setValues:n,validation:a,validating:o,onValidate:l}){return c.jsxs("div",{className:"flex flex-1 flex-col gap-5",children:[c.jsxs("p",{className:"text-sm leading-relaxed text-muted-foreground",children:["Enable ",c.jsx("strong",{children:"Developer Mode"})," in Discord (Settings → Advanced), then right-click your profile and ",c.jsx("strong",{children:"Copy User ID"}),". Add one ID per line."]}),c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{htmlFor:"owners",children:"Owner user IDs"}),c.jsx(sd,{id:"owners",value:e.ownerUserIds,onChange:i=>n({...e,ownerUserIds:i.target.value}),placeholder:`829112572816130058 -987654321098765432`,className:"min-h-[120px] font-mono text-sm"})]}),c.jsx(yi,{result:a.owners}),c.jsx("div",{className:"mt-auto",children:c.jsx(be,{type:"button",variant:"secondary",disabled:o||!e.ownerUserIds.trim(),onClick:l,children:o?"Checking with Discord…":"Verify owner IDs"})})]})}function BP({onContinue:e}){return c.jsx("div",{className:"mx-auto flex w-full max-w-lg flex-1 flex-col justify-center py-8",children:c.jsxs("div",{className:"rounded-2xl border border-border bg-card px-6 py-10 text-center md:px-10",children:[c.jsx("div",{className:"mx-auto mb-6 flex size-16 items-center justify-center rounded-2xl bg-primary/10 text-primary",children:c.jsx(ye,{icon:Tu,size:40})}),c.jsx("h2",{className:"text-2xl font-semibold tracking-tight",children:"Setup complete"}),c.jsxs("p",{className:"mx-auto mt-3 max-w-sm text-sm leading-relaxed text-muted-foreground",children:["Your configuration has been saved. Restart the server for changes to take effect — stop the running process, then start it again (for example"," ",c.jsx("code",{className:"rounded-md bg-muted px-1.5 py-0.5 font-mono text-xs",children:"npm start"}),")."]}),c.jsx(Ue,{className:"mt-6 text-left",children:c.jsx(He,{children:"After restarting, sign in with Discord to open the dashboard."})}),c.jsx(be,{type:"button",size:"lg",className:"mt-8 min-w-[220px]",onClick:e,children:"Continue to sign in"})]})})}function $P({currentIndex:e,validating:n,saving:a,onBack:o,onNext:l,onFinish:i}){const d=e===fs.length-1;return c.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[c.jsxs(be,{type:"button",variant:"ghost",disabled:e===0||a,onClick:o,children:[c.jsx(ye,{icon:b0,size:18,className:"mr-1"}),"Back"]}),c.jsx("div",{className:"flex flex-1 justify-end gap-2",children:d?c.jsx(be,{type:"button",size:"lg",disabled:a||n,onClick:i,className:"min-w-[200px]",children:a?"Saving…":"Finish setup"}):c.jsxs(be,{type:"button",size:"lg",disabled:n||a,onClick:l,children:["Continue",c.jsx(ye,{icon:ph,size:18,className:"ml-1"})]})})]})}function IP(){const e=crypto.randomUUID().replace(/-/g,""),n=crypto.randomUUID().replace(/-/g,"");return e+n}function UP(e){return e.split(/[\s,]+/).map(n=>n.trim()).filter(Boolean)}function Fx(e){return e==="oauth"?"oauth":e==="owners"?"owners":e}function HP(){const e=oo(),{branding:n}=Ou(),{loading:a,error:o,refresh:l,status:i}=yC(),[d,m]=f.useState(0),[h,p]=f.useState({botToken:"",secretKey:"",clientId:"",clientSecret:"",redirectUri:"",ownerUserIds:""}),[y,v]=f.useState({}),[x,S]=f.useState(!1),[j,C]=f.useState(!1),[N,E]=f.useState(""),[k,A]=f.useState(!1),R=fs[d];f.useEffect(()=>{i!=null&&i.suggestedRedirectUri&&!h.redirectUri&&p(Q=>({...Q,redirectUri:i.suggestedRedirectUri}))},[i==null?void 0:i.suggestedRedirectUri,h.redirectUri]);const L=f.useMemo(()=>({botToken:h.botToken,secretKey:h.secretKey,clientId:h.clientId,clientSecret:h.clientSecret,redirectUri:h.redirectUri,ownerUserIds:UP(h.ownerUserIds)}),[h]);async function z(){S(!0),E("");try{const Q=await ke.validateSetup(L);return v(fe=>{var de;return{...fe,...(de=Q.data)==null?void 0:de.results}}),Q.data}catch(Q){return E(Q instanceof Error?Q.message:"Validation failed"),null}finally{S(!1)}}async function B(){var de,ce;const Q=Fx(R.id),fe=await z();return!!((ce=(de=fe==null?void 0:fe.results)==null?void 0:de[Q])!=null&&ce.ok)}async function U(){const Q=Fx(R.id),fe=y[Q];!(fe!=null&&fe.ok)&&!await B()||m(de=>Math.min(de+1,fs.length-1))}async function q(){C(!0),E("");try{const Q=await z();if(!(Q!=null&&Q.ok)){E("Fix the steps marked with errors before finishing.");return}await ke.completeSetup(L),A(!0)}catch(Q){E(Q instanceof Error?Q.message:"Could not save setup")}finally{C(!1)}}function Z(){const Q={values:h,setValues:p,validation:y,validating:x};switch(R.id){case"botToken":return c.jsx(OP,{...Q,onValidate:()=>void B()});case"secretKey":return c.jsx(DP,{...Q,onGenerate:()=>p(fe=>({...fe,secretKey:IP()})),onValidate:()=>void B()});case"oauth":return c.jsx(LP,{...Q,onValidate:()=>void B()});case"redirectUri":return c.jsx(zP,{values:h,setValues:p,validation:y});case"owners":return c.jsx(PP,{...Q,onValidate:()=>void B()})}}return a?c.jsx("div",{className:"min-h-svh bg-background px-4 py-10",children:c.jsxs("div",{className:"mx-auto max-w-6xl space-y-8",children:[c.jsx(Fe,{className:"h-14 w-64"}),c.jsxs("div",{className:"grid gap-8 lg:grid-cols-[280px_1fr]",children:[c.jsx(Fe,{className:"hidden h-80 lg:block"}),c.jsx(Fe,{className:"h-[520px] rounded-2xl"})]})]})}):c.jsxs(AP,{brandingTitle:n.title,faviconUrl:n.faviconUrl,faviconVersion:n.updatedAt,hasFavicon:n.hasFavicon,children:[o?c.jsx(Ue,{variant:"destructive",className:"mb-6",children:c.jsx(He,{children:o})}):null,N?c.jsx(Ue,{variant:"destructive",className:"mb-6",children:c.jsx(He,{children:N})}):null,k?c.jsx(BP,{onContinue:async()=>{await l(),e("/login",{replace:!0})}}):c.jsxs("div",{className:"grid flex-1 gap-8 lg:grid-cols-[minmax(0,280px)_1fr]",children:[c.jsx(_P,{currentIndex:d,validation:y}),c.jsx(MP,{step:R,footer:c.jsx($P,{currentIndex:d,validating:x,saving:j,onBack:()=>m(Q=>Math.max(0,Q-1)),onNext:()=>void U(),onFinish:()=>void q()}),children:Z()})]})]})}const VP={todo:"bg-rose-500/15 text-rose-400",progress:"bg-amber-500/15 text-amber-400",done:"bg-emerald-500/15 text-emerald-400"};function GP({title:e,icon:n,href:a,badge:o="Configured",status:l="done"}){return c.jsxs(kt,{to:a,className:ie("flex items-center gap-3 rounded-2xl border border-border bg-card p-4 transition-colors","hover:border-primary hover:bg-secondary"),children:[c.jsx("div",{className:"flex size-11 shrink-0 items-center justify-center rounded-full bg-secondary text-primary",children:c.jsx(ye,{icon:n,size:20})}),c.jsxs("div",{className:"min-w-0 flex-1",children:[c.jsx("p",{className:"truncate font-semibold",children:e}),c.jsx("span",{className:ie("mt-1 inline-flex rounded-md px-2 py-0.5 text-[10px] font-semibold",VP[l]),children:o})]})]})}function FP({percent:e,size:n=128,className:a,trackClassName:o="stroke-white/20",strokeClassName:l="stroke-white"}){const d=2*Math.PI*42,m=Math.min(100,Math.max(0,e)),h=d-m/100*d;return c.jsxs("div",{className:ie("relative inline-flex",a),children:[c.jsxs("svg",{width:n,height:n,viewBox:"0 0 100 100",className:"-rotate-90","aria-hidden":!0,children:[c.jsx("circle",{cx:"50",cy:"50",r:42,fill:"none",strokeWidth:"9",className:o}),c.jsx("circle",{cx:"50",cy:"50",r:42,fill:"none",strokeWidth:"9",strokeLinecap:"round",strokeDasharray:d,strokeDashoffset:h,className:l})]}),c.jsxs("div",{className:"absolute inset-0 flex flex-col items-center justify-center text-center",children:[c.jsxs("span",{className:"text-2xl font-bold tabular-nums leading-none",children:[Math.round(m),"%"]}),c.jsx("span",{className:"mt-0.5 text-[10px] font-medium uppercase tracking-wide opacity-80",children:"Complete"})]})]})}const YP={todo:{dot:"bg-rose-500",badge:"bg-rose-500/15 text-rose-400",label:"Needs setup"},progress:{dot:"bg-amber-400",badge:"bg-amber-500/15 text-amber-400",label:"In progress"},done:{dot:"bg-sky-400",badge:"bg-sky-500/15 text-sky-400",label:"Active"}};function WP({title:e,subtitle:n,icon:a,href:o,status:l,meta:i="Open in dashboard"}){const d=YP[l];return c.jsxs(kt,{to:o,className:ie("group flex min-w-[280px] flex-1 flex-col gap-4 rounded-2xl border border-border bg-card p-4 transition-colors","hover:border-primary hover:bg-secondary","sm:min-w-[300px] lg:min-w-0"),children:[c.jsxs("div",{className:"flex items-start justify-between gap-2",children:[c.jsx("span",{className:ie("mt-1.5 size-2 shrink-0 rounded-full",d.dot)}),c.jsx("span",{className:"text-xs text-muted-foreground",children:n})]}),c.jsxs("div",{children:[c.jsx("h3",{className:"font-semibold leading-snug tracking-tight",children:e}),c.jsx("span",{className:ie("mt-2 inline-flex rounded-md px-2 py-0.5 text-[11px] font-semibold",d.badge),children:d.label})]}),c.jsxs("div",{className:"mt-auto flex items-center justify-between border-t border-border pt-3",children:[c.jsxs("div",{className:"flex items-center gap-2",children:[c.jsx("div",{className:"flex size-8 items-center justify-center rounded-lg bg-secondary text-primary",children:c.jsx(ye,{icon:a,size:16})}),c.jsx("span",{className:"text-xs text-muted-foreground",children:i})]}),c.jsxs("div",{className:"flex items-center gap-3 text-muted-foreground",children:[c.jsx(ye,{icon:gh,size:14,className:"opacity-60"}),c.jsx(ye,{icon:Xa,size:14,className:"opacity-60 group-hover:text-primary"}),c.jsx(ye,{icon:ph,size:14,className:"text-primary opacity-0 transition-opacity group-hover:opacity-100"})]})]})]})}function qP({stats:e}){const n=[{id:"ping",label:"Gateway latency",detail:`${e.bot.ping}ms`,badge:e.bot.ping<200?"Good":"High",badgeClass:e.bot.ping<200?"bg-emerald-500/15 text-emerald-400":"bg-amber-500/15 text-amber-400"},{id:"uptime",label:"Bot uptime",detail:j0(e.bot.uptime),badge:"Live",badgeClass:"bg-primary/20 text-primary"},{id:"ram",label:"Memory usage",detail:`${e.hosting.ram_used} / ${e.hosting.ram_total}`,badge:`${e.hosting.ram_percent}%`,badgeClass:"bg-sky-500/15 text-sky-400"}];return c.jsxs("aside",{className:"hidden w-[min(100%,320px)] shrink-0 space-y-4 xl:block",children:[c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{className:"flex flex-row items-center justify-between space-y-0 pb-3",children:[c.jsxs(ot,{className:"text-sm font-semibold",children:["Connected servers (",e.servers,")"]}),c.jsx(be,{variant:"ghost",size:"icon",className:"size-7 text-muted-foreground",children:c.jsx("span",{className:"text-lg leading-none",children:"+"})})]}),c.jsxs(qe,{className:"pt-0",children:[c.jsxs("div",{className:"flex -space-x-2",children:[Array.from({length:Math.min(6,e.servers)}).map((a,o)=>c.jsxs(gs,{className:"size-9 border-2 border-card ring-0",children:[c.jsx(vs,{src:e.bot.avatar,alt:""}),c.jsx(bs,{className:"bg-primary/25 text-[10px] text-primary",children:String(o+1)})]},o)),e.servers>6?c.jsxs("div",{className:"flex size-9 items-center justify-center rounded-full border-2 border-card bg-secondary text-xs font-medium text-muted-foreground",children:["+",e.servers-6]}):null]}),c.jsxs("p",{className:"mt-3 text-xs text-muted-foreground",children:[e.users.toLocaleString()," members across your guilds"]})]})]}),c.jsx(We,{className:"overflow-hidden border-border bg-secondary",children:c.jsx(qe,{className:"p-5",children:c.jsxs("div",{className:"space-y-3",children:[c.jsx("div",{className:"flex size-10 items-center justify-center rounded-xl bg-primary text-primary-foreground",children:c.jsx(ye,{icon:_4,size:22})}),c.jsxs("div",{children:[c.jsx("p",{className:"font-semibold",children:"Keep SupportBot up to date"}),c.jsx("p",{className:"mt-1 text-xs text-muted-foreground",children:"Check for releases and install updates from settings."})]}),c.jsx(be,{asChild:!0,size:"sm",className:"w-full",children:c.jsxs(kt,{to:"/settings",children:[c.jsx(ye,{icon:Ru,size:16,className:"mr-2"}),"Open settings"]})})]})})}),c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{className:"flex flex-row items-center justify-between space-y-0 pb-2",children:[c.jsx(ot,{className:"text-sm font-semibold",children:"System activity"}),c.jsx(be,{variant:"ghost",size:"icon",className:"size-7 text-muted-foreground",children:c.jsx("span",{className:"text-base leading-none",children:"⋯"})})]}),c.jsxs(qe,{className:"space-y-1 p-0 pb-2",children:[n.map(a=>c.jsxs("div",{className:"flex items-center gap-3 px-4 py-2.5 transition-colors hover:bg-secondary/40",children:[c.jsxs(gs,{className:"size-9",children:[c.jsx(vs,{src:e.bot.avatar,alt:e.bot.username}),c.jsx(bs,{className:"bg-secondary text-xs",children:"SB"})]}),c.jsxs("div",{className:"min-w-0 flex-1",children:[c.jsx("p",{className:"truncate text-sm font-medium",children:a.label}),c.jsx("p",{className:"truncate text-xs text-muted-foreground",children:a.detail})]}),c.jsx("span",{className:`shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold ${a.badgeClass}`,children:a.badge})]},a.id)),c.jsx(be,{asChild:!0,variant:"ghost",className:"mx-2 mt-1 w-[calc(100%-1rem)] justify-between text-xs text-muted-foreground",children:c.jsxs(kt,{to:"/system",children:["All system details",c.jsx(ye,{icon:ph,size:14})]})})]})]}),c.jsx(We,{className:"border-border bg-card",children:c.jsxs(qe,{className:"flex items-center gap-3 p-4",children:[c.jsx("div",{className:"flex size-9 items-center justify-center rounded-lg bg-secondary text-primary",children:c.jsx(ye,{icon:Xa,size:18})}),c.jsxs("div",{className:"min-w-0 flex-1",children:[c.jsxs("p",{className:"text-sm font-medium",children:["v",e.bot.version]}),c.jsxs("p",{className:"text-xs text-muted-foreground",children:["CPU ",e.hosting.cpu_load]})]})]})})]})}const h5={todo:"Needs setup",progress:"In progress",done:"Active"};function eh(e,n,a="Configure module"){const o=n==null?void 0:n[e];return o||{status:"todo",subtitle:a}}function KP(){return c.jsxs("div",{className:"flex gap-6",children:[c.jsxs("div",{className:"min-w-0 flex-1 space-y-6",children:[c.jsxs("div",{className:"grid gap-4 lg:grid-cols-2",children:[c.jsx(Fe,{className:"h-44 rounded-2xl"}),c.jsx(Fe,{className:"h-44 rounded-2xl"})]}),c.jsx(Fe,{className:"h-8 w-48"}),c.jsx("div",{className:"flex gap-4 overflow-hidden",children:Array.from({length:3}).map((e,n)=>c.jsx(Fe,{className:"h-40 min-w-[280px] rounded-2xl"},n))}),c.jsx(Fe,{className:"h-8 w-56"}),c.jsx("div",{className:"grid gap-3 sm:grid-cols-2",children:Array.from({length:4}).map((e,n)=>c.jsx(Fe,{className:"h-20 rounded-2xl"},n))})]}),c.jsx(Fe,{className:"hidden h-[520px] w-80 shrink-0 rounded-2xl xl:block"})]})}function XP(){const{stats:e,loading:n,error:a}=Du(),[o,l]=f.useState("all"),i=f.useMemo(()=>{if(!e)return 0;const y=e.tickets.total||0;return y===0?100:Math.round(e.tickets.closed/y*100)},[e]),d=f.useMemo(()=>o==="open"||o==="closed"?ls.filter(y=>y.category==="tickets"):o==="all"?ls:ls.filter(y=>y.category===o),[o]);if(n)return c.jsx(KP,{});if(a||!e)return c.jsx(Ue,{variant:"destructive",children:c.jsx(He,{children:a||"No stats available"})});const m=e.tickets.open,h=e.tickets.closed,p=e.tickets.total;return c.jsxs("div",{className:"flex flex-col gap-6 xl:flex-row xl:items-start",children:[c.jsxs("div",{className:"min-w-0 flex-1 space-y-6",children:[c.jsxs("div",{className:"grid gap-4 lg:grid-cols-2",children:[c.jsx(We,{className:"overflow-hidden border-border bg-card",children:c.jsxs(qe,{className:"flex flex-col gap-4 p-5 sm:flex-row sm:items-center sm:justify-between sm:p-6",children:[c.jsxs("div",{className:"space-y-3",children:[c.jsxs("p",{className:"text-lg font-semibold leading-snug",children:["You have"," ",c.jsx("span",{className:"text-primary",children:m})," open ticket",m===1?"":"s"]}),c.jsx("p",{className:"max-w-xs text-sm text-muted-foreground",children:"Review transcripts or tune your ticket panel so new requests are handled smoothly."}),c.jsx(be,{asChild:!0,size:"sm",className:"rounded-lg px-5",children:c.jsx(kt,{to:"/transcripts",children:"Review"})})]}),c.jsxs("div",{className:"relative mx-auto flex size-28 shrink-0 items-end justify-center sm:mx-0 sm:size-32","aria-hidden":!0,children:[c.jsx("div",{className:"absolute inset-0 rounded-2xl bg-secondary"}),c.jsx("span",{className:"relative text-6xl",children:"🎫"})]})]})}),c.jsx(We,{className:"overflow-hidden border-border bg-primary text-primary-foreground",children:c.jsxs(qe,{className:"flex flex-col gap-4 p-5 sm:flex-row sm:items-center sm:justify-between sm:p-6",children:[c.jsxs("div",{className:"space-y-3",children:[c.jsx("p",{className:"text-sm font-medium text-white/80",children:"Ticket summary"}),c.jsxs("ul",{className:"space-y-1.5 text-sm",children:[c.jsxs("li",{className:"flex items-center gap-2",children:[c.jsx("span",{className:"size-1.5 rounded-full bg-white/70"}),c.jsxs("span",{className:"text-white/90",children:[p," total"]})]}),c.jsxs("li",{className:"flex items-center gap-2",children:[c.jsx("span",{className:"size-1.5 rounded-full bg-white/70"}),c.jsxs("span",{className:"text-white/90",children:[h," closed"]})]}),c.jsxs("li",{className:"flex items-center gap-2",children:[c.jsx("span",{className:"size-1.5 rounded-full bg-white/70"}),c.jsxs("span",{className:"text-white/90",children:[m," open"]})]})]})]}),c.jsx(FP,{percent:i,size:120})]})})]}),c.jsxs("section",{className:"space-y-4",children:[c.jsxs("div",{className:"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",children:[c.jsx("h2",{className:"text-xl font-bold tracking-tight",children:"Module overview"}),c.jsx(Yj,{value:o,onValueChange:y=>l(y),children:c.jsxs(fg,{className:"h-auto flex-wrap rounded-xl bg-secondary/60 p-1",children:[c.jsx(Ka,{value:"all",className:"rounded-lg text-xs data-[state=active]:bg-background data-[state=active]:shadow-sm",children:"All"}),c.jsx(Ka,{value:"open",className:"rounded-lg text-xs data-[state=active]:bg-background",children:"Open"}),c.jsx(Ka,{value:"closed",className:"rounded-lg text-xs data-[state=active]:bg-background",children:"Closed"}),G7.filter(y=>y.id!=="all").map(y=>c.jsx(Ka,{value:y.id,className:"rounded-lg text-xs data-[state=active]:bg-background",children:y.label},y.id))]})})]}),c.jsx("div",{className:"-mx-1 flex gap-4 overflow-x-auto px-1 pb-2 lg:grid lg:grid-cols-3 lg:overflow-visible",children:(o==="open"?[{id:"open-queue",title:"Open ticket queue",subtitle:`${m} waiting`,icon:Mu,href:"/transcripts",status:"todo",meta:"View transcripts"}]:o==="closed"?[{id:"closed-archive",title:"Closed tickets",subtitle:`${h} resolved`,icon:Tu,href:"/transcripts",status:"done",meta:"Browse archive"}]:d.map(y=>{const v=eh(y.id,e.modules,y.description);return{id:y.id,title:y.title,subtitle:v.subtitle,icon:y.icon,href:y.href,status:v.status,meta:v.status==="done"?"View settings":"Configure"}})).map(y=>c.jsx(WP,{title:y.title,subtitle:y.subtitle,icon:y.icon,href:y.href,status:y.status,meta:y.meta},y.id))})]}),c.jsxs("section",{className:"space-y-4",children:[c.jsxs("div",{className:"flex items-center justify-between",children:[c.jsx("h2",{className:"text-xl font-bold tracking-tight",children:"Active modules"}),c.jsx(kt,{to:"/configs",className:"text-sm font-medium text-primary hover:underline",children:"See all"})]}),c.jsx("div",{className:"grid gap-3 sm:grid-cols-2",children:ls.slice(0,4).map(y=>{const v=eh(y.id,e.modules);return c.jsx(GP,{title:y.title,icon:y.icon,href:y.href,badge:h5[v.status],status:v.status},y.id)})})]})]}),c.jsx(qP,{stats:e})]})}function ZP(){const{permissions:e}=Ft();return Ln(e,"overview")?c.jsx(XP,{}):c.jsx(Mr,{to:Sh(e),replace:!0})}function as({permission:e,children:n}){const{loading:a,isAuthenticated:o,permissions:l}=Ft();return a?c.jsx("div",{className:"flex flex-1 items-center justify-center py-16 text-sm text-muted-foreground",children:"Loading…"}):o?Ln(l,e)?c.jsx(c.Fragment,{children:n}):c.jsx(Mr,{to:Sh(l),replace:!0}):c.jsx(Mr,{to:"/login",replace:!0})}const Sg="supportbot-dashboard-accent",$l="#45b46c",QP=["--primary","--ring","--chart-1","--sidebar-primary","--sidebar-ring","--accent","--accent-foreground","--sidebar-accent","--sidebar-accent-foreground"];function JP(e){const n=lr(e,$l),a=parseInt(n.slice(1,3),16)/255,o=parseInt(n.slice(3,5),16)/255,l=parseInt(n.slice(5,7),16)/255,i=Math.max(a,o,l),d=Math.min(a,o,l),m=(i+d)/2;let h=0,p=0;if(i!==d){const y=i-d;switch(p=m>.5?y/(2-i-d):y/(i+d),i){case a:h=((o-l)/y+(o{const o=new FileReader;o.onload=()=>n(String(o.result)),o.onerror=()=>a(new Error("Could not read file")),o.readAsDataURL(e)})}function aB({canEdit:e}){const{branding:n,setBrandingLocal:a}=Ou(),o=f.useRef(null),[l,i]=f.useState(n.title),[d,m]=f.useState(n.pageTitle),[h,p]=f.useState(!1),[y,v]=f.useState(!1),[x,S]=f.useState("");f.useEffect(()=>{i(n.title),m(n.pageTitle)},[n.title,n.pageTitle]);const j=n.hasFavicon&&n.faviconUrl?`${n.faviconUrl}${n.updatedAt?`?v=${n.updatedAt}`:""}`:null;async function C(){if(e){p(!0),S("");try{const R=await ke.updateBranding({title:l,pageTitle:d});R.data&&a(R.data),S(R.message||"Branding saved.")}catch(R){S(R instanceof Error?R.message:"Save failed")}finally{p(!1)}}}async function N(R){if(!(!e||!R)){v(!0),S("");try{const L=await rB(R),z=await ke.uploadBrandingFavicon(L);z.data&&a(z.data),S(z.message||"Favicon uploaded.")}catch(L){S(L instanceof Error?L.message:"Upload failed")}finally{v(!1),o.current&&(o.current.value="")}}}async function E(){if(e){v(!0),S("");try{const R=await ke.removeBrandingFavicon();R.data&&a(R.data),S(R.message||"Favicon removed.")}catch(R){S(R instanceof Error?R.message:"Remove failed")}finally{v(!1)}}}async function k(){if(e){p(!0),S("");try{const R=await ke.resetBranding();R.data&&a(R.data),S(R.message||"Reset to defaults.")}catch(R){S(R instanceof Error?R.message:"Reset failed")}finally{p(!1)}}}const A=l.trim()!==n.title||d.trim()!==n.pageTitle;return c.jsxs("div",{className:"space-y-4 border-t border-border pt-4",children:[c.jsxs("div",{children:[c.jsx("p",{className:"text-sm font-medium",children:"Branding"}),c.jsx("p",{className:"text-xs text-muted-foreground",children:"Sidebar title, browser tab title, and favicon — saved on the server for everyone using this dashboard."})]}),c.jsxs("div",{className:"grid gap-4 sm:grid-cols-2",children:[c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{htmlFor:"brand-title",children:"Sidebar title"}),c.jsx(rt,{id:"brand-title",value:l,disabled:!e,maxLength:64,onChange:R=>i(R.target.value),className:"border-border bg-secondary/30",placeholder:"SupportBot"})]}),c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{htmlFor:"brand-page-title",children:"Browser tab title"}),c.jsx(rt,{id:"brand-page-title",value:d,disabled:!e,maxLength:80,onChange:R=>m(R.target.value),className:"border-border bg-secondary/30",placeholder:"SupportBot Dashboard"})]})]}),c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{children:"Favicon"}),c.jsx("p",{className:"text-xs text-muted-foreground",children:"PNG, ICO, SVG, JPEG, or WebP — max 512 KB. Shown in the browser tab and beside the sidebar title."}),c.jsxs("div",{className:"flex flex-wrap items-center gap-3",children:[c.jsx("div",{className:"flex size-14 items-center justify-center overflow-hidden rounded-lg border border-border bg-secondary/30",children:j?c.jsx("img",{src:j,alt:"Favicon preview",className:"size-full object-contain p-1"}):c.jsx(ye,{icon:j4,size:22,className:"text-muted-foreground"})}),c.jsx("input",{ref:o,type:"file",accept:"image/png,image/jpeg,image/x-icon,image/vnd.microsoft.icon,image/svg+xml,image/webp,.ico",className:"hidden",disabled:!e||y,onChange:R=>{var L;return void N(((L=R.target.files)==null?void 0:L[0])??null)}}),e?c.jsxs(c.Fragment,{children:[c.jsx(be,{type:"button",variant:"outline",size:"sm",disabled:y,onClick:()=>{var R;return(R=o.current)==null?void 0:R.click()},children:y?"Uploading…":"Upload favicon"}),n.hasFavicon?c.jsx(be,{type:"button",variant:"outline",size:"sm",disabled:y,onClick:()=>void E(),children:"Remove"}):null]}):c.jsx("span",{className:"text-xs text-muted-foreground",children:"View only"})]})]}),e?c.jsxs("div",{className:"flex flex-wrap gap-2",children:[c.jsx(be,{type:"button",size:"sm",disabled:h||!A,onClick:()=>void C(),children:h?"Saving…":"Save titles"}),c.jsx(be,{type:"button",variant:"outline",size:"sm",disabled:h||y,onClick:()=>void k(),children:"Reset branding"})]}):null,x?c.jsx("p",{className:"text-xs text-muted-foreground",children:x}):null]})}function oB(){const{hasPermission:e}=Ft(),n=e("settings.update"),[a,o]=f.useState(null),[l,i]=f.useState(!0),[d,m]=f.useState(!1),[h,p]=f.useState(""),[y,v]=f.useState(""),[x,S]=f.useState(()=>v5()??$l),[j,C]=f.useState(""),N=f.useCallback(async()=>{i(!0),p("");try{const k=await ke.checkForUpdates();k.data&&o(k.data)}catch(k){p(k instanceof Error?k.message:"Could not check for updates")}finally{i(!1)}},[]);f.useEffect(()=>{N()},[N]);async function E(){if(a!=null&&a.updateAvailable){m(!0),p(""),v("");try{const k=await ke.runUpdate(a.latest);v(k.message||"Update installed. Restart your server (stop and run npm start again) to finish applying changes."),await N()}catch(k){p(k instanceof Error?k.message:"Update failed")}finally{m(!1)}}}return c.jsxs("div",{className:"space-y-6",children:[c.jsxs("div",{children:[c.jsx("h1",{className:"text-2xl font-bold tracking-tight",children:"Settings"}),c.jsx("p",{className:"text-muted-foreground",children:"Application updates and maintenance — config files are edited under each module in the sidebar."})]}),c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{children:[c.jsx(ot,{className:"text-lg",children:"Dashboard appearance"}),c.jsx(Nt,{children:"Customise branding for all users, and pick an accent colour for this browser only."})]}),c.jsxs(qe,{className:"space-y-4",children:[c.jsx(aB,{canEdit:n}),c.jsxs("div",{className:"border-t border-border pt-4",children:[c.jsx("p",{className:"mb-3 text-sm font-medium",children:"Accent colour"}),c.jsx("p",{className:"mb-4 text-xs text-muted-foreground",children:"Buttons, links, and highlights — saved in this browser only."}),c.jsxs("div",{className:"flex flex-wrap items-end gap-4",children:[c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{htmlFor:"accent-colour",children:"Accent colour"}),c.jsxs("div",{className:"flex items-center gap-2",children:[c.jsx(cg,{value:x,onChange:k=>{S(k),Yx(k),C("Accent updated.")}}),c.jsx(rt,{id:"accent-colour",value:x,onChange:k=>S(k.target.value),onBlur:()=>{const k=lr(x,$l);S(k),Yx(k),C("Accent updated.")},className:"w-32 border-border bg-secondary/30 font-mono uppercase",maxLength:7})]})]}),c.jsx(be,{type:"button",variant:"outline",onClick:()=>{tB(),S($l),C("Reset to default purple.")},children:"Reset to default"})]}),c.jsxs("div",{className:"flex flex-wrap gap-2 text-sm",children:[c.jsx("span",{className:"rounded-md bg-primary px-3 py-1.5 font-medium text-primary-foreground",children:"Primary button"}),c.jsx("span",{className:"rounded-md bg-primary/15 px-3 py-1.5 text-primary",children:"Highlight"}),c.jsx("span",{className:"rounded-md bg-accent px-3 py-1.5 text-accent-foreground",children:"Accent surface"})]}),j?c.jsx("p",{className:"text-xs text-muted-foreground",children:j}):null]})]})]}),c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{children:[c.jsx(ot,{className:"text-lg",children:"Software updates"}),c.jsxs(Nt,{children:["Checks"," ",c.jsx("a",{href:"https://github.com/C-h-a-r/SupportBot-Dashboard",target:"_blank",rel:"noreferrer",className:"text-primary underline-offset-4 hover:underline",children:"SupportBot-Dashboard"})," ","on the release branch."]})]}),c.jsxs(qe,{className:"space-y-4",children:[l?c.jsx(Fe,{className:"h-24 w-full"}):a?c.jsxs("div",{className:"space-y-3 rounded-lg border border-border bg-secondary/20 p-4",children:[c.jsxs("div",{className:"flex flex-wrap gap-x-6 gap-y-2 text-sm",children:[c.jsxs("p",{children:[c.jsx("span",{className:"text-muted-foreground",children:"Installed: "}),c.jsxs("span",{className:"font-mono font-medium",children:["v",a.current]})]}),c.jsxs("p",{children:[c.jsx("span",{className:"text-muted-foreground",children:"Latest: "}),c.jsxs("span",{className:"font-mono font-medium",children:["v",a.latest]})]})]}),a.updateAvailable?c.jsx("p",{className:"text-sm text-amber-500",children:"A newer version is available. Update downloads bot files while keeping your Configs and Data."}):c.jsx("p",{className:"text-sm text-emerald-500",children:"You are on the latest release."})]}):null,h?c.jsx(Ue,{variant:"destructive",children:c.jsx(He,{children:h})}):null,y?c.jsx(Ue,{children:c.jsx(He,{children:y})}):null,c.jsxs("div",{className:"flex flex-wrap gap-2",children:[c.jsxs(be,{type:"button",variant:"outline",onClick:N,disabled:l||d,children:[c.jsx(ye,{icon:Jl,size:16,className:"mr-2"}),"Check again"]}),a!=null&&a.updateAvailable&&n?c.jsxs(be,{type:"button",onClick:E,disabled:d,children:[d?c.jsx(ye,{icon:w0,size:16,className:"mr-2 animate-spin"}):c.jsx(ye,{icon:Ru,size:16,className:"mr-2"}),d?"Installing…":"Update now"]}):null,a!=null&&a.updateAvailable&&!n?c.jsx("p",{className:"text-xs text-muted-foreground",children:"You can check for updates but cannot install them with your role."}):null]})]})]})]})}function pt({label:e,value:n,mono:a=!1}){return c.jsxs("div",{className:"flex flex-col gap-1 border-b border-border py-3 last:border-0 sm:flex-row sm:items-center sm:justify-between",children:[c.jsx("dt",{className:"text-sm text-muted-foreground",children:e}),c.jsx("dd",{className:`text-sm font-medium text-foreground ${a?"font-mono text-xs sm:text-sm":""}`,children:n})]})}const sB={ok:"Healthy",missing_config:"Guild ID not set",not_in_guild:"Not in configured server",extra_guilds:"Extra servers joined",offline:"Bot offline"};function lB(e){return e==="ok"?"text-emerald-500":e==="offline"?"text-destructive":"text-amber-500"}function iB(){var S;const{stats:e,loading:n,error:a,refresh:o}=Du(),[l,i]=f.useState(null),[d,m]=f.useState(null),[h,p]=f.useState(!1),y=f.useCallback(async()=>{const[j,C]=await Promise.all([ke.getGuildStatus().catch(()=>null),ke.health().catch(()=>null)]);j!=null&&j.data&&i(j.data),C!=null&&C.success&&m({dashboard:C.dashboard,botReady:C.botReady,oauthEnabled:C.oauthEnabled,setupComplete:C.setupComplete})},[]);f.useEffect(()=>{y()},[y]);async function v(){p(!0);try{await Promise.all([o(),y()])}finally{p(!1)}}if(n&&!e)return c.jsxs("div",{className:"space-y-6",children:[c.jsx(Fe,{className:"h-10 w-64"}),c.jsxs("div",{className:"grid gap-4 lg:grid-cols-2",children:[c.jsx(Fe,{className:"h-56 rounded-2xl"}),c.jsx(Fe,{className:"h-56 rounded-2xl"})]})]});if(a||!e)return c.jsx(Ue,{variant:"destructive",children:c.jsx(He,{children:a||"Could not load system details."})});const x=e.bot.ping<200;return c.jsxs("div",{className:"space-y-6",children:[c.jsxs("div",{className:"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between",children:[c.jsxs("div",{className:"space-y-2",children:[c.jsx(be,{asChild:!0,variant:"ghost",size:"sm",className:"-ml-2 h-8 px-2 text-muted-foreground",children:c.jsxs(kt,{to:"/",children:[c.jsx(ye,{icon:b0,size:16,className:"mr-1.5"}),"Back to dashboard"]})}),c.jsxs("div",{children:[c.jsx("h1",{className:"text-2xl font-bold tracking-tight",children:"System details"}),c.jsx("p",{className:"text-muted-foreground",children:"Live bot, host, and dashboard status — refreshed every 30 seconds."})]})]}),c.jsxs(be,{type:"button",variant:"outline",disabled:h,onClick:()=>void v(),children:[c.jsx(ye,{icon:Jl,size:16,className:"mr-2"}),h?"Refreshing…":"Refresh now"]})]}),c.jsxs("div",{className:"grid gap-4 lg:grid-cols-2",children:[c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{children:[c.jsx(ot,{className:"text-lg",children:"Discord bot"}),c.jsx(Nt,{children:"Connection and identity"})]}),c.jsxs(qe,{children:[c.jsxs("div",{className:"mb-4 flex items-center gap-3",children:[c.jsxs(gs,{className:"size-12",children:[c.jsx(vs,{src:e.bot.avatar,alt:e.bot.username}),c.jsx(bs,{children:"SB"})]}),c.jsxs("div",{children:[c.jsx("p",{className:"font-semibold",children:e.bot.username}),c.jsx("p",{className:"font-mono text-xs text-muted-foreground",children:e.bot.id})]})]}),c.jsxs("dl",{children:[c.jsx(pt,{label:"Version",value:`v${e.bot.version}`,mono:!0}),c.jsx(pt,{label:"Gateway latency",value:c.jsxs("span",{className:x?"text-emerald-500":"text-amber-500",children:[e.bot.ping," ms"]})}),c.jsx(pt,{label:"Process uptime",value:j0(e.bot.uptime)}),c.jsx(pt,{label:"Connected servers",value:e.servers.toLocaleString()}),c.jsx(pt,{label:"Members tracked",value:e.users.toLocaleString()}),e.bot.inviteUrl?c.jsx(pt,{label:"Invite link",value:c.jsx("a",{href:e.bot.inviteUrl,target:"_blank",rel:"noreferrer",className:"text-primary underline-offset-4 hover:underline",children:"Open in Discord"})}):null]})]})]}),c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{children:[c.jsx(ot,{className:"text-lg",children:"Host machine"}),c.jsx(Nt,{children:"Server resources running this instance"})]}),c.jsx(qe,{children:c.jsxs("dl",{children:[c.jsx(pt,{label:"Memory used",value:`${e.hosting.ram_used} / ${e.hosting.ram_total}`}),c.jsx(pt,{label:"Memory usage",value:c.jsxs("span",{className:e.hosting.ram_percent>85?"text-amber-500":"text-foreground",children:[e.hosting.ram_percent,"%"]})}),c.jsx(pt,{label:"CPU load (1 min avg)",value:e.hosting.cpu_load,mono:!0}),c.jsx(pt,{label:"Host uptime",value:e.hosting.uptime})]})})]}),c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{children:[c.jsx(ot,{className:"text-lg",children:"Tickets"}),c.jsx(Nt,{children:"Database snapshot"})]}),c.jsx(qe,{children:c.jsxs("dl",{children:[c.jsx(pt,{label:"Total tickets",value:e.tickets.total}),c.jsx(pt,{label:"Open",value:c.jsx("span",{className:"text-primary",children:e.tickets.open})}),c.jsx(pt,{label:"Closed",value:e.tickets.closed}),c.jsx(pt,{label:"Transcripts",value:c.jsx(kt,{to:"/transcripts",className:"text-primary underline-offset-4 hover:underline",children:"Browse archive"})})]})})]}),c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{children:[c.jsx(ot,{className:"text-lg",children:"Guild health"}),c.jsx(Nt,{children:"Configured server and membership"})]}),c.jsx(qe,{children:l?c.jsxs("dl",{children:[c.jsx(pt,{label:"Status",value:c.jsx("span",{className:lB(l.status),children:sB[l.status]})}),c.jsx(pt,{label:"Configured guild ID",value:l.configuredGuildId||"Not set",mono:!0}),l.configuredGuild?c.jsx(pt,{label:"Configured server",value:`${l.configuredGuild.name} (${((S=l.configuredGuild.memberCount)==null?void 0:S.toLocaleString())??"?"} members)`}):null,l.message?c.jsx("div",{className:"mt-3 rounded-lg border border-border bg-secondary/30 px-3 py-2 text-sm text-muted-foreground",children:l.message}):null,l.extraGuilds.length>0?c.jsxs("div",{className:"mt-3 space-y-2",children:[c.jsx("p",{className:"text-xs font-medium uppercase tracking-wide text-muted-foreground",children:"Extra servers"}),c.jsx("ul",{className:"space-y-1 text-sm",children:l.extraGuilds.map(j=>c.jsxs("li",{className:"font-mono text-xs",children:[j.name," · ",j.id]},j.id))})]}):null]}):c.jsx(Fe,{className:"h-24 w-full"})})]})]}),c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{children:[c.jsx(ot,{className:"text-lg",children:"Dashboard services"}),c.jsx(Nt,{children:"API and authentication"})]}),c.jsx(qe,{children:d?c.jsxs("dl",{className:"max-w-xl",children:[c.jsx(pt,{label:"Dashboard UI",value:d.dashboard?"Available":"Missing build"}),c.jsx(pt,{label:"Bot connection",value:d.botReady?"Online":"Starting or offline"}),c.jsx(pt,{label:"Discord OAuth",value:d.oauthEnabled?"Configured":"Not configured"}),c.jsx(pt,{label:"Initial setup",value:d.setupComplete?"Complete":"Incomplete"}),c.jsx(pt,{label:"Software updates",value:c.jsx(kt,{to:"/settings",className:"text-primary underline-offset-4 hover:underline",children:"Open settings"})}),c.jsx(pt,{label:"Logs",value:c.jsx(kt,{to:"/logs",className:"text-primary underline-offset-4 hover:underline",children:"View log files"})})]}):c.jsx(Fe,{className:"h-32 w-full max-w-xl"})})]}),e.modules?c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{children:[c.jsx(ot,{className:"text-lg",children:"Module setup status"}),c.jsx(Nt,{children:"Which features are configured and active"})]}),c.jsx(qe,{children:c.jsx("div",{className:"grid gap-3 sm:grid-cols-2",children:ls.map(j=>{const C=eh(j.id,e.modules);return c.jsxs(kt,{to:j.href,className:"flex items-start justify-between gap-3 rounded-xl border border-border bg-secondary/20 px-4 py-3 transition-colors hover:bg-secondary/40",children:[c.jsxs("div",{className:"min-w-0",children:[c.jsx("p",{className:"text-sm font-medium",children:j.title}),c.jsx("p",{className:"mt-0.5 truncate text-xs text-muted-foreground",children:C.subtitle})]}),c.jsx("span",{className:"shrink-0 rounded-full bg-secondary px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground",children:h5[C.status]})]},j.id)})})})]}):null]})}function cB(e){if(!e)return"—";const n=new Date(e);return Number.isNaN(n.getTime())?e:n.toLocaleTimeString(void 0,{hour:"2-digit",minute:"2-digit",second:"2-digit"})}const uB=[{id:"all",label:"All"},{id:"Output",label:"Output"},{id:"Warn",label:"Warn"},{id:"Error",label:"Error"}],dB=["Output","Warn","Error"],fB={Output:"text-foreground/90",Warn:"text-amber-400",Error:"text-rose-400"},mB={Output:"bg-muted text-muted-foreground",Warn:"bg-amber-500/15 text-amber-400",Error:"bg-rose-500/15 text-rose-400"},Wx=2e3,pB=2e3;function th(e,n){return`${e.type}-${e.timestamp??"na"}-${n}-${e.message.slice(0,32)}`}function hB(e,n,a){const o=a?[]:e,l=new Set(o.map((m,h)=>th(m,h))),i=[];for(let m=0;mWx?d.slice(d.length-Wx):d}function gB(){const[e,n]=f.useState([]),[a,o]=f.useState(!0),[l,i]=f.useState(""),[d,m]=f.useState(!1),[h,p]=f.useState(!0),[y,v]=f.useState("all"),[x,S]=f.useState(""),[j,C]=f.useState(!0),[N,E]=f.useState(""),k=f.useRef(null),A=f.useRef(null),R=f.useRef(d);R.current=d;const L=f.useMemo(()=>y==="all"?dB:[y],[y]),z=f.useCallback(async q=>{var fe,de;const Z=await ke.getLogs({types:L,cursor:q?void 0:k.current??void 0,tail:q?500:0}),Q=((fe=Z.data)==null?void 0:fe.entries)??[];return k.current=((de=Z.data)==null?void 0:de.cursor)??{},n(ce=>hB(ce,Q,q)),Q.length},[L]),B=f.useCallback(async()=>{var q;o(!0),i(""),k.current=null;try{const fe=(((q=(await ke.getLogsMeta()).data)==null?void 0:q.files)??[]).filter(de=>de.file).map(de=>`${de.type}: ${de.file}`).join(" · ");E(fe||"No log files yet — output appears when the bot runs."),await z(!0),C(!0)}catch(Z){i(Z instanceof Error?Z.message:"Failed to load logs"),C(!1)}finally{o(!1)}},[z]);f.useEffect(()=>{B()},[B]),f.useEffect(()=>{if(d||a)return;const q=window.setInterval(async()=>{if(!R.current)try{await z(!1),C(!0),i("")}catch(Z){C(!1),i(Z instanceof Error?Z.message:"Live update failed")}},pB);return()=>window.clearInterval(q)},[d,a,z]),f.useEffect(()=>{if(!h||d)return;const q=A.current;q&&(q.scrollTop=q.scrollHeight)},[e,h,d]);const U=f.useMemo(()=>e.filter(q=>{var Q;if(y!=="all"&&q.type!==y)return!1;if(!x.trim())return!0;const Z=x.toLowerCase();return q.message.toLowerCase().includes(Z)||(((Q=q.timestamp)==null?void 0:Q.toLowerCase().includes(Z))??!1)}),[e,y,x]);return c.jsxs("div",{className:"flex h-[calc(100svh-8rem)] min-h-[480px] flex-col gap-4",children:[c.jsxs("div",{className:"flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between",children:[c.jsxs("div",{children:[c.jsx("h1",{className:"text-2xl font-bold tracking-tight",children:"Logs"}),c.jsx("p",{className:"text-muted-foreground",children:"Live bot output — updates every 2 seconds"}),N?c.jsx("p",{className:"mt-1 text-xs text-muted-foreground",children:N}):null]}),c.jsxs("div",{className:"flex items-center gap-2",children:[c.jsx("span",{className:ie("size-2 rounded-full",j&&!d?"animate-pulse bg-emerald-500":"bg-muted-foreground")}),c.jsx("span",{className:"text-xs text-muted-foreground",children:d?"Paused":j?"Live":"Disconnected"})]})]}),l?c.jsx(Ue,{variant:"destructive",children:c.jsx(He,{children:l})}):null,c.jsxs(We,{className:"flex min-h-0 flex-1 flex-col border-border bg-card",children:[c.jsxs(at,{className:"shrink-0 space-y-4 border-b border-border pb-4",children:[c.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3",children:[c.jsxs(ot,{className:"text-base font-medium",children:["Console",a?null:c.jsxs("span",{className:"ml-2 text-xs font-normal text-muted-foreground",children:[U.length," line",U.length===1?"":"s"]})]}),c.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[c.jsxs("div",{className:"flex items-center gap-2 rounded-lg border border-border px-2 py-1",children:[c.jsx(Dn,{id:"auto-scroll",checked:h,onCheckedChange:p}),c.jsx(Ze,{htmlFor:"auto-scroll",className:"text-xs font-normal",children:"Auto-scroll"})]}),c.jsxs(be,{type:"button",variant:"outline",size:"sm",onClick:()=>m(q=>!q),children:[c.jsx(ye,{icon:d?A4:R4,size:16,className:"mr-1.5"}),d?"Resume":"Pause"]}),c.jsxs(be,{type:"button",variant:"outline",size:"sm",onClick:()=>{n([]),B()},disabled:a,children:[c.jsx(ye,{icon:Jl,size:16,className:"mr-1.5"}),"Refresh"]})]})]}),c.jsxs("div",{className:"flex flex-col gap-3 sm:flex-row sm:items-center",children:[c.jsx("div",{className:"flex flex-wrap gap-1 rounded-xl bg-secondary/50 p-1",children:uB.map(q=>c.jsx(be,{type:"button",variant:y===q.id?"secondary":"ghost",size:"sm",className:"h-8 rounded-lg text-xs",onClick:()=>v(q.id),children:q.label},q.id))}),c.jsxs("div",{className:"relative flex-1 sm:max-w-xs",children:[c.jsx(ye,{icon:_u,size:16,className:"absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"}),c.jsx(rt,{placeholder:"Filter lines…",value:x,onChange:q=>S(q.target.value),className:"h-9 pl-9"})]})]})]}),c.jsx(qe,{className:"min-h-0 flex-1 p-0",children:a?c.jsx("div",{className:"space-y-2 p-4",children:Array.from({length:8}).map((q,Z)=>c.jsx(Fe,{className:"h-4 w-full"},Z))}):c.jsx("div",{ref:A,className:"h-full max-h-[calc(100svh-18rem)] overflow-y-auto bg-[#0d0d12] p-4 font-mono text-[13px] leading-relaxed",children:U.length===0?c.jsx("p",{className:"text-muted-foreground",children:"No log lines yet. Start the bot and activity will appear here (also written to the Logs folder on disk)."}):c.jsx("ul",{className:"space-y-1",children:U.map((q,Z)=>c.jsxs("li",{className:"flex gap-2 rounded px-1 py-0.5 hover:bg-white/[0.03]",children:[c.jsx("span",{className:"shrink-0 tabular-nums text-muted-foreground",children:cB(q.timestamp)}),c.jsx("span",{className:ie("shrink-0 rounded px-1.5 py-0 text-[10px] font-semibold uppercase",mB[q.type]),children:q.type}),c.jsx("span",{className:ie("min-w-0 flex-1 whitespace-pre-wrap break-words",fB[q.type]),children:q.message||" "})]},th(q,Z)))})})})]})]})}const vB=[{title:"Colors",description:"Theme colors used across the transcript page.",fields:[{path:"Theme.Background",label:"Page background",type:"color"},{path:"Theme.Panel",label:"Panel background",type:"color"},{path:"Theme.PanelSecondary",label:"Secondary panel",type:"color"},{path:"Theme.Border",label:"Borders",type:"color"},{path:"Theme.Text",label:"Main text",type:"color"},{path:"Theme.Muted",label:"Muted text",type:"color"},{path:"Theme.Accent",label:"Accent",type:"color"},{path:"Theme.Link",label:"Links",type:"color"},{path:"Theme.AttachmentLink",label:"Attachment links",type:"color"},{path:"Theme.CodeBackground",label:"Code blocks",type:"color"}]},{title:"Typography",fields:[{path:"Typography.FontFamily",label:"Font family",type:"googleFont",description:"Google Fonts are loaded automatically in saved transcripts."}]},{title:"Header",fields:[{path:"Header.Title",label:"Title",type:"text"},{path:"Header.Subtitle",label:"Subtitle",type:"textarea",description:"Placeholders: {{ticketName}} {{ticketId}} {{messageCount}} {{reason}}. Use **text** for bold."},{path:"Header.Badge",label:"Badge label",type:"text"},{path:"Header.ShowMetaCards",label:"Show info cards",type:"boolean"}]},{title:"Info card labels",fields:[{path:"Meta.ChannelLabel",label:"Channel label",type:"text"},{path:"Meta.TicketIdLabel",label:"Ticket ID label",type:"text"},{path:"Meta.MessageCountLabel",label:"Message count label",type:"text"},{path:"Meta.CloseReasonLabel",label:"Close reason label",type:"text"},{path:"Meta.DefaultCloseReason",label:"Default close reason",type:"text"}]},{title:"Messages section",fields:[{path:"Messages.SectionTitle",label:"Section heading",type:"text"},{path:"Messages.EmptyState",label:"Empty state text",type:"text"}]},{title:"Advanced CSS",description:"Optional extra CSS appended to the transcript stylesheet.",fields:[{path:"Advanced.CustomCss",label:"Custom CSS",type:"textarea",description:"Example: .badge { font-size: 1rem; }"}]}],bB=450;function yB(e,n){return n&&e!=="viewer"}function xB(){const{user:e,hasPermission:n}=Ft(),a=!yB(e==null?void 0:e.role,n("transcripts")),[o,l]=f.useState(null),[i,d]=f.useState(null),[m,h]=f.useState(!0),[p,y]=f.useState(!1),[v,x]=f.useState(""),[S,j]=f.useState(""),[C,N]=f.useState(""),[E,k]=f.useState(!1),A=f.useRef(null),R=f.useCallback(async()=>{var q;h(!0),x(""),j("");try{const Q=(await ke.getTranscriptTemplate()).data??null;if(Q){const fe={...Q,Typography:{...Q.Typography,FontFamily:Mj((q=Q.Typography)==null?void 0:q.FontFamily)}};l(fe),d(structuredClone(fe))}}catch(Z){x(Z instanceof Error?Z.message:"Failed to load template")}finally{h(!1)}},[]);f.useEffect(()=>{R()},[R]);const L=f.useCallback(async q=>{k(!0);try{const Z=await ke.previewTranscriptTemplate(q);N(Z)}catch{N("

Preview failed to load.

")}finally{k(!1)}},[]);f.useEffect(()=>{if(o)return A.current&&clearTimeout(A.current),A.current=setTimeout(()=>{L(o)},bB),()=>{A.current&&clearTimeout(A.current)}},[o,L]);function z(q,Z){l(Q=>Q&&Xl(Q,q,Z))}async function B(){if(!(a||!o)){y(!0),x(""),j("");try{const q=await ke.saveTranscriptTemplate(o);j(q.message||"Saved."),d(structuredClone(o))}catch(q){x(q instanceof Error?q.message:"Save failed")}finally{y(!1)}}}const U=o&&i&&JSON.stringify(o)!==JSON.stringify(i);return m?c.jsxs("div",{className:"grid gap-4 lg:grid-cols-2",children:[c.jsx(Fe,{className:"h-96 rounded-xl"}),c.jsx(Fe,{className:"h-96 rounded-xl"})]}):o?c.jsxs("div",{className:"space-y-4",children:[c.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-2",children:[c.jsx("p",{className:"text-sm text-muted-foreground",children:"Customize how new ticket transcripts look. Existing saved transcripts are not changed."}),c.jsxs("div",{className:"flex items-center gap-2",children:[U?c.jsx("span",{className:"text-xs text-amber-500",children:"Unsaved changes"}):null,a?c.jsx("span",{className:"text-xs text-muted-foreground",children:"View only"}):c.jsxs(be,{size:"sm",onClick:B,disabled:p||!U,children:[c.jsx(ye,{icon:Au,size:16,className:"mr-1.5"}),p?"Saving…":"Save appearance"]})]})]}),v?c.jsx(Ue,{variant:"destructive",children:c.jsx(He,{children:v})}):null,S?c.jsx(Ue,{className:"border-primary/30 bg-primary/10 text-primary",children:c.jsx(He,{children:S})}):null,c.jsxs("div",{className:"grid min-h-0 gap-4 xl:grid-cols-[minmax(320px,420px)_1fr]",children:[c.jsx(SB,{data:o,readOnly:a,onPatch:z}),c.jsxs(We,{className:"flex min-h-[520px] flex-col overflow-hidden border-border bg-card",children:[c.jsxs(at,{className:"pb-2",children:[c.jsx(ot,{className:"text-base",children:"Live preview"}),c.jsxs(Nt,{children:["Sample ticket with your current settings",E?" · updating…":""]})]}),c.jsx(qe,{className:"min-h-0 flex-1 p-4 pt-0",children:c.jsx("iframe",{title:"Transcript template preview",srcDoc:C,className:ie("h-[min(640px,calc(100svh-14rem))] w-full rounded-lg border border-border bg-background",E&&"opacity-60"),sandbox:"allow-same-origin"})})]})]})]}):c.jsx(Ue,{variant:"destructive",children:c.jsx(He,{children:v||"Failed to load template"})})}function SB({data:e,readOnly:n,onPatch:a}){return c.jsx("div",{className:"max-h-[min(720px,calc(100svh-12rem))] space-y-4 overflow-y-auto pr-1",children:vB.map(o=>c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{className:"pb-3",children:[c.jsx(ot,{className:"text-base",children:o.title}),o.description?c.jsx(Nt,{children:o.description}):null]}),c.jsx(qe,{className:"grid gap-4 sm:grid-cols-2",children:o.fields.map(l=>c.jsx("div",{className:l.type==="textarea"?"sm:col-span-2":"",children:c.jsx(yg,{field:l,value:ao(e,l.path),readOnly:n,onChange:i=>a(l.path,i)})},l.path))})]},o.title))})}function wB(e,n){return n&&e!=="viewer"}function CB(){const{user:e,hasPermission:n}=Ft(),a=!wB(e==null?void 0:e.role,n("transcripts")),[o,l]=f.useState(null),[i,d]=f.useState(!0),[m,h]=f.useState(!1),[p,y]=f.useState(!1),[v,x]=f.useState(""),[S,j]=f.useState(""),C=f.useCallback(async()=>{var k,A,R;d(!0),x(""),j("");try{const z=(await ke.getTranscriptSettings()).data;z&&l({autoDelete:{enabled:!!((k=z.autoDelete)!=null&&k.enabled),afterDays:((A=z.autoDelete)==null?void 0:A.afterDays)??30},publicAccess:{enabled:!!((R=z.publicAccess)!=null&&R.enabled)}})}catch(L){x(L instanceof Error?L.message:"Failed to load settings")}finally{d(!1)}},[]);f.useEffect(()=>{C()},[C]);async function N(){if(!(!o||a)){h(!0),x(""),j("");try{const k=await ke.updateTranscriptSettings(o);k.data&&l({autoDelete:{enabled:k.data.autoDelete.enabled,afterDays:k.data.autoDelete.afterDays},publicAccess:{enabled:k.data.publicAccess.enabled}}),j(k.message??"Settings saved.")}catch(k){x(k instanceof Error?k.message:"Failed to save settings")}finally{h(!1)}}}async function E(){var k,A;if(!a){y(!0),x(""),j("");try{const R=await ke.purgeTranscripts();j(R.message??((k=R.data)!=null&&k.skipped?"Auto-delete is disabled.":`Removed ${((A=R.data)==null?void 0:A.deleted)??0} transcript(s).`))}catch(R){x(R instanceof Error?R.message:"Purge failed")}finally{y(!1)}}}return i?c.jsxs("div",{className:"space-y-4",children:[c.jsx(Fe,{className:"h-40 w-full rounded-xl"}),c.jsx(Fe,{className:"h-40 w-full rounded-xl"})]}):o?c.jsxs("div",{className:"mx-auto max-w-2xl space-y-4",children:[a?c.jsx(Ue,{children:c.jsx(He,{children:"You have view-only access. An admin or editor can change these settings."})}):null,v?c.jsx(Ue,{variant:"destructive",children:c.jsx(He,{children:v})}):null,S?c.jsx(Ue,{children:c.jsx(He,{children:S})}):null,c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{children:[c.jsx(ot,{className:"text-base",children:"Auto-delete"}),c.jsx(Nt,{children:"Remove transcript files automatically after they reach a certain age. The server checks hourly and whenever you open the library."})]}),c.jsxs(qe,{className:"space-y-4",children:[c.jsxs("label",{className:"flex cursor-pointer items-start gap-3",children:[c.jsx("input",{type:"checkbox",className:"mt-1 size-4 rounded border-border accent-primary",checked:o.autoDelete.enabled,disabled:a,onChange:k=>l(A=>A&&{...A,autoDelete:{...A.autoDelete,enabled:k.target.checked}})}),c.jsxs("span",{className:"text-sm",children:[c.jsx("span",{className:"font-medium",children:"Enable auto-delete"}),c.jsx("span",{className:"mt-0.5 block text-muted-foreground",children:"Deletes HTML files in Data/Transcripts based on last modified time."})]})]}),c.jsxs("div",{className:"space-y-2",children:[c.jsx("label",{className:"text-sm font-medium",htmlFor:"transcript-after-days",children:"Delete after (days)"}),c.jsx(rt,{id:"transcript-after-days",type:"number",min:1,max:3650,disabled:a||!o.autoDelete.enabled,value:o.autoDelete.afterDays,onChange:k=>l(A=>A&&{...A,autoDelete:{...A.autoDelete,afterDays:Math.min(3650,Math.max(1,parseInt(k.target.value,10)||1))}}),className:"max-w-[8rem]"})]}),c.jsxs(be,{type:"button",variant:"outline",size:"sm",disabled:a||p,onClick:()=>void E(),children:[c.jsx(ye,{icon:Jl,size:16,className:ie(p&&"animate-spin")}),"Run cleanup now"]})]})]}),c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{children:[c.jsx(ot,{className:"text-base",children:"Public transcripts"}),c.jsxs(Nt,{children:["When enabled, anyone with the link can view a transcript at"," ",c.jsx("code",{className:"rounded bg-muted px-1 py-0.5 text-xs",children:typeof window<"u"?`${window.location.origin}/transcripts/:id`:"/transcripts/:id"})," ","without signing in. Channel IDs are numeric but not secret—only enable if you are comfortable sharing links."]})]}),c.jsx(qe,{children:c.jsxs("label",{className:"flex cursor-pointer items-start gap-3",children:[c.jsx("input",{type:"checkbox",className:"mt-1 size-4 rounded border-border accent-primary",checked:o.publicAccess.enabled,disabled:a,onChange:k=>l(A=>A&&{...A,publicAccess:{enabled:k.target.checked}})}),c.jsxs("span",{className:"text-sm",children:[c.jsx("span",{className:"font-medium",children:"Allow public transcript URLs"}),c.jsx("span",{className:"mt-0.5 block text-muted-foreground",children:"Dashboard preview and download still require login."})]})]})})]}),a?null:c.jsxs(be,{type:"button",disabled:m,onClick:()=>void N(),children:[c.jsx(ye,{icon:Au,size:16}),m?"Saving…":"Save settings"]})]}):c.jsx(Ue,{variant:"destructive",children:c.jsx(He,{children:"Could not load transcript settings."})})}function jB(e,n){return n&&e!=="viewer"}function qx(e){return e==null?"":e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`}function Kx(e){const n=new Date(e);return Number.isNaN(n.getTime())?e:n.toLocaleString(void 0,{dateStyle:"medium",timeStyle:"short"})}function EB(){var H;const{user:e,hasPermission:n}=Ft(),a=jB(e==null?void 0:e.role,n("transcripts")),[o,l]=f.useState("browse"),[i,d]=f.useState([]),[m,h]=f.useState(null),[p,y]=f.useState(!0),[v,x]=f.useState(""),[S,j]=f.useState(""),[C,N]=f.useState(""),[E,k]=f.useState(0),[A,R]=f.useState(!1),[L,z]=f.useState(!1),[B,U]=f.useState(!1),[q,Z]=f.useState(!1),Q=f.useCallback(async()=>{y(!0),j("");try{const V=(await ke.listTranscripts()).data??[];d(V),h(ne=>{var J;return ne&&V.some(me=>me.id===ne)?ne:((J=V[0])==null?void 0:J.id)??null})}catch(I){j(I instanceof Error?I.message:"Failed to load transcripts")}finally{y(!1)}},[]);f.useEffect(()=>{Q()},[Q]),f.useEffect(()=>{o==="browse"&&ke.getTranscriptSettings().then(I=>{var V,ne;U(!!((ne=(V=I.data)==null?void 0:V.publicAccess)!=null&&ne.enabled))}).catch(()=>U(!1))},[o]);const fe=f.useMemo(()=>{if(!C.trim())return i;const I=C.toLowerCase();return i.filter(V=>V.id.includes(I)||V.filename.toLowerCase().includes(I)||V.ticketName&&V.ticketName.toLowerCase().includes(I))},[i,C]),de=f.useMemo(()=>i.find(I=>I.id===m)??null,[i,m]),ce=m?`/api/system/transcripts/${encodeURIComponent(m)}/view`:null,he=m?`/api/system/transcripts/${encodeURIComponent(m)}/download`:null,M=m&&B?`${window.location.origin}/transcripts/${encodeURIComponent(m)}`:null;function K(I){x(""),h(I),k(V=>V+1)}function re(){ce&&window.open(ce,"_blank","noopener,noreferrer")}function oe(){M&&window.open(M,"_blank","noopener,noreferrer")}async function ee(){if(M)try{await navigator.clipboard.writeText(M),Z(!0),window.setTimeout(()=>Z(!1),2e3)}catch{x("Could not copy link to clipboard.")}}async function _(){var V;if(!m||!a)return;const I=((V=de==null?void 0:de.ticketName)==null?void 0:V.trim())||`channel ${m}`;if(window.confirm(`Delete the transcript for ${I}? This cannot be undone.`)){z(!0),x("");try{await ke.deleteTranscript(m),await Q()}catch(ne){x(ne instanceof Error?ne.message:"Failed to delete transcript")}finally{z(!1)}}}async function O(){if(!(!he||!de)){R(!0),x("");try{const I=await fetch(he,{credentials:"include"});if(!I.ok){const me=await I.json().catch(()=>({}));throw new Error(me.error||`Download failed (${I.status})`)}const V=await I.blob(),ne=URL.createObjectURL(V),J=document.createElement("a");J.href=ne,J.download=de.filename,J.click(),URL.revokeObjectURL(ne)}catch(I){x(I instanceof Error?I.message:"Failed to download transcript")}finally{R(!1)}}}return c.jsxs("div",{className:"flex min-h-0 flex-1 flex-col gap-4",children:[c.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-3",children:[c.jsxs("div",{children:[c.jsx("h1",{className:"text-2xl font-semibold tracking-tight",children:"Transcripts"}),c.jsx("p",{className:"mt-1 text-sm text-muted-foreground",children:"Browse saved transcripts or customize how new ones look when tickets close."})]}),c.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[c.jsxs("div",{className:"flex rounded-lg border border-border p-0.5",children:[c.jsx(be,{type:"button",variant:o==="browse"?"secondary":"ghost",size:"sm",className:"h-8",onClick:()=>l("browse"),children:"Library"}),c.jsx(be,{type:"button",variant:o==="appearance"?"secondary":"ghost",size:"sm",className:"h-8",onClick:()=>l("appearance"),children:"Appearance"}),c.jsx(be,{type:"button",variant:o==="settings"?"secondary":"ghost",size:"sm",className:"h-8",onClick:()=>l("settings"),children:"Settings"})]}),o==="browse"?c.jsxs(be,{variant:"outline",size:"sm",onClick:()=>Q(),disabled:p,children:[c.jsx(ye,{icon:Jl,size:16,className:ie(p&&"animate-spin")}),"Refresh"]}):null]})]}),o==="appearance"?c.jsx(xB,{}):o==="settings"?c.jsx(CB,{}):c.jsxs(c.Fragment,{children:[S?c.jsx(Ue,{variant:"destructive",children:c.jsx(He,{children:S})}):null,c.jsxs("div",{className:"grid min-h-0 flex-1 gap-4 lg:grid-cols-[minmax(240px,320px)_1fr]",children:[c.jsxs(We,{className:"flex min-h-[420px] flex-col border-border bg-card",children:[c.jsxs(at,{className:"space-y-3 pb-3",children:[c.jsxs(ot,{className:"text-base font-medium",children:["Saved transcripts",!p&&i.length>0?c.jsxs("span",{className:"ml-2 font-normal text-muted-foreground",children:["(",i.length,")"]}):null]}),c.jsxs("div",{className:"relative",children:[c.jsx(ye,{icon:_u,size:16,className:"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"}),c.jsx(rt,{placeholder:"Search by ticket name or channel ID…",value:C,onChange:I=>N(I.target.value),className:"pl-9"})]})]}),c.jsx(qe,{className:"min-h-0 flex-1 p-0 pb-4",children:c.jsx(hi,{className:"h-[min(520px,calc(100svh-16rem))] px-4",children:p?c.jsx("div",{className:"space-y-2",children:Array.from({length:6}).map((I,V)=>c.jsx(Fe,{className:"h-16 w-full rounded-lg"},V))}):fe.length===0?c.jsxs("div",{className:"flex flex-col items-center justify-center gap-2 py-12 text-center text-sm text-muted-foreground",children:[c.jsx(ye,{icon:Ul,size:32,className:"opacity-40"}),c.jsx("p",{children:i.length===0?"No transcripts yet. They appear when tickets are closed.":"No transcripts match your search."})]}):c.jsx("ul",{className:"space-y-1.5",children:fe.map(I=>{var J;const V=I.id===m,ne=((J=I.ticketName)==null?void 0:J.trim())||`Channel ${I.id}`;return c.jsx("li",{children:c.jsxs("button",{type:"button",onClick:()=>K(I.id),className:ie("w-full rounded-lg border px-3 py-2.5 text-left transition-colors",V?"border-primary/50 bg-primary/10":"border-transparent bg-secondary/30 hover:bg-secondary/50"),children:[c.jsx("p",{className:"truncate text-sm font-medium",children:ne}),c.jsxs("p",{className:"mt-0.5 text-xs text-muted-foreground",children:[Kx(I.createdAt),I.size!=null?` · ${qx(I.size)}`:""]})]})},I.id)})})})})]}),c.jsxs(We,{className:"flex min-h-[420px] flex-col overflow-hidden border-border bg-card",children:[c.jsxs(at,{className:"flex flex-row flex-wrap items-center justify-between gap-2 space-y-0 pb-3",children:[c.jsxs("div",{className:"min-w-0",children:[c.jsx(ot,{className:"truncate text-base font-medium",children:de?((H=de.ticketName)==null?void 0:H.trim())||`Channel ${de.id}`:"Preview"}),de?c.jsxs("p",{className:"mt-0.5 truncate text-xs text-muted-foreground",children:[Kx(de.createdAt),de.size!=null?` · ${qx(de.size)}`:""]}):null]}),c.jsxs("div",{className:"flex shrink-0 flex-wrap gap-2",children:[M?c.jsxs(c.Fragment,{children:[c.jsxs(be,{variant:"outline",size:"sm",onClick:()=>void ee(),children:[c.jsx(ye,{icon:x0,size:16}),q?"Copied":"Copy link"]}),c.jsxs(be,{variant:"outline",size:"sm",onClick:oe,children:[c.jsx(ye,{icon:gh,size:16}),"Public page"]})]}):null,c.jsxs(be,{variant:"outline",size:"sm",disabled:!ce,onClick:re,children:[c.jsx(ye,{icon:S4,size:16}),"Open tab"]}),c.jsxs(be,{variant:"outline",size:"sm",disabled:!he||A,onClick:()=>void O(),children:[c.jsx(ye,{icon:Ru,size:16}),A?"Saving…":"Download"]}),a?c.jsxs(be,{variant:"outline",size:"sm",disabled:!m||L,className:"text-destructive hover:text-destructive",onClick:()=>void _(),children:[c.jsx(ye,{icon:hh,size:16}),L?"Deleting…":"Delete"]}):null]})]}),c.jsxs(qe,{className:"flex min-h-0 flex-1 flex-col gap-2 p-4 pt-0",children:[v?c.jsx(Ue,{variant:"destructive",children:c.jsx(He,{children:v})}):null,de&&ce?c.jsx("iframe",{title:`Transcript ${de.id}`,src:ce,className:"min-h-[min(560px,calc(100svh-18rem))] w-full flex-1 rounded-lg border border-border bg-background",sandbox:"allow-same-origin"},E):c.jsxs("div",{className:"flex flex-1 flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border py-16 text-center text-sm text-muted-foreground",children:[c.jsx(ye,{icon:Ul,size:40,className:"opacity-30"}),c.jsx("p",{children:"Select a transcript from the list to preview it here."})]})]})]})]})]})]})}const NB=["admin","moderator","editor","viewer","custom"];function kB(){return{mode:"add",id:"",username:"",globalName:"",role:"viewer",enabled:!0,permissions:Hl("viewer"),yamlOwner:!1}}function TB({permissions:e,onChange:n,disabled:a}){const o=l=>n({...e,...l});return c.jsxs("div",{className:"space-y-6",children:[c.jsxs("div",{className:"grid gap-4 sm:grid-cols-2",children:[[["Dashboard overview","overview"],["Logs","logs"],["Transcripts","transcripts"]].map(([l,i])=>c.jsxs("label",{className:"flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2",children:[c.jsx("span",{className:"text-sm",children:l}),c.jsx(Dn,{checked:e[i],disabled:a,onCheckedChange:d=>o({[i]:d})})]},i)),c.jsxs("label",{className:"flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2",children:[c.jsx("span",{className:"text-sm",children:"Settings — view"}),c.jsx(Dn,{checked:e.settings.view,disabled:a,onCheckedChange:l=>o({settings:{...e.settings,view:l}})})]}),c.jsxs("label",{className:"flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2",children:[c.jsx("span",{className:"text-sm",children:"Settings — install updates"}),c.jsx(Dn,{checked:e.settings.update,disabled:a,onCheckedChange:l=>o({settings:{...e.settings,update:l}})})]}),c.jsxs("label",{className:"flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2",children:[c.jsx("span",{className:"text-sm",children:"Users — view"}),c.jsx(Dn,{checked:e.users.view,disabled:a,onCheckedChange:l=>o({users:{...e.users,view:l}})})]}),c.jsxs("label",{className:"flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2",children:[c.jsx("span",{className:"text-sm",children:"Users — manage"}),c.jsx(Dn,{checked:e.users.manage,disabled:a,onCheckedChange:l=>o({users:{...e.users,manage:l}})})]})]}),c.jsxs("div",{className:"space-y-3",children:[c.jsx("p",{className:"text-sm font-medium",children:"Configuration files"}),c.jsx("div",{className:"space-y-2",children:xh.map(l=>c.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-3 rounded-lg border border-border px-3 py-2",children:[c.jsx("span",{className:"text-sm",children:z4[l]}),c.jsxs("div",{className:"flex items-center gap-4",children:[c.jsxs("label",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:["View",c.jsx(Dn,{checked:e.configs[l].view,disabled:a,onCheckedChange:i=>n({...e,configs:{...e.configs,[l]:{...e.configs[l],view:i}}})})]}),c.jsxs("label",{className:"flex items-center gap-2 text-xs text-muted-foreground",children:["Edit",c.jsx(Dn,{checked:e.configs[l].edit,disabled:a,onCheckedChange:i=>n({...e,configs:{...e.configs,[l]:{...e.configs[l],edit:i,view:i?!0:e.configs[l].view}}})})]})]})]},l))})]})]})}function RB(e){return e.permissions.users.manage?"Full + user management":e.permissions.settings.update?"Configs + settings updates":Object.values(e.permissions.configs).some(n=>n.edit)?"Can edit configs":"View only"}function AB(){const{user:e,hasPermission:n}=Ft(),a=n("users.manage"),[o,l]=f.useState([]),[i,d]=f.useState(!0),[m,h]=f.useState(""),[p,y]=f.useState(null),[v,x]=f.useState(!1),S=f.useCallback(async()=>{d(!0),h("");try{const R=await ke.listDashboardUsers();R.data&&l(R.data.users)}catch(R){h(R instanceof Error?R.message:"Failed to load users")}finally{d(!1)}},[]);f.useEffect(()=>{S()},[S]);const j=f.useMemo(()=>[...o].sort((R,L)=>R.yamlOwner!==L.yamlOwner?R.yamlOwner?-1:1:(R.username||R.id).localeCompare(L.username||L.id)),[o]);function C(){y(kB())}function N(R){y({mode:"edit",id:R.id,username:R.username||"",globalName:R.globalName||"",role:R.yamlOwner?"owner":R.role,enabled:R.enabled,permissions:R.role==="custom"?R.permissions:Hl(R.role),yamlOwner:R.yamlOwner})}function E(R){p&&y({...p,role:R,permissions:Hl(R,p.permissions)})}async function k(){if(!(!p||!a)){x(!0),h("");try{p.mode==="add"?await ke.addDashboardUser({id:p.id.trim(),role:p.role,username:p.username||void 0,globalName:p.globalName||void 0,enabled:p.enabled,permissions:p.role==="custom"?p.permissions:void 0}):await ke.updateDashboardUser(p.id,{role:p.yamlOwner?void 0:p.role,enabled:p.enabled,username:p.username||void 0,globalName:p.globalName||void 0,permissions:p.role==="custom"?p.permissions:void 0}),y(null),await S()}catch(R){h(R instanceof Error?R.message:"Could not save user")}finally{x(!1)}}}async function A(R){if(a&&window.confirm("Remove this user from the dashboard?")){h("");try{await ke.removeDashboardUser(R),await S()}catch(L){h(L instanceof Error?L.message:"Could not remove user")}}}return c.jsxs("div",{className:"space-y-6",children:[c.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-4",children:[c.jsxs("div",{children:[c.jsx("h1",{className:"text-2xl font-bold tracking-tight",children:"Users"}),c.jsxs("p",{className:"text-muted-foreground",children:["Control who can sign in and what they can view or edit. Owners in"," ",c.jsx("code",{className:"rounded bg-primary/10 px-1 text-xs text-primary",children:"api.yml"})," ","always have full access."]})]}),a?c.jsxs(be,{onClick:C,children:[c.jsx(ye,{icon:x4,size:16,className:"mr-2"}),"Add user"]}):null]}),m?c.jsx(Ue,{variant:"destructive",children:c.jsx(He,{children:m})}):null,c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{children:[c.jsx(ot,{className:"text-base",children:"Dashboard access"}),c.jsxs(Nt,{children:[j.length," user",j.length===1?"":"s",a?" — use edit to change permissions":""]})]}),c.jsx(qe,{children:i?c.jsx("p",{className:"text-sm text-muted-foreground",children:"Loading users…"}):c.jsx("div",{className:"divide-y divide-border/60",children:j.map(R=>c.jsxs("div",{className:"flex flex-wrap items-center gap-4 py-4 first:pt-0 last:pb-0",children:[c.jsxs("div",{className:"flex min-w-[200px] flex-1 items-center gap-3",children:[c.jsxs(gs,{className:"size-9",children:[c.jsx(vs,{src:R.avatar,alt:R.username}),c.jsx(bs,{children:(R.username||R.id).slice(0,2).toUpperCase()})]}),c.jsxs("div",{className:"min-w-0",children:[c.jsxs("p",{className:"truncate font-medium",children:[E0(R),R.id===(e==null?void 0:e.id)?" (you)":""]}),c.jsx("p",{className:"truncate text-xs text-muted-foreground",children:R.id})]})]}),c.jsx(Ll,{variant:"secondary",children:M1[R.yamlOwner?"owner":R.role]}),c.jsx("p",{className:"max-w-[200px] text-xs text-muted-foreground",children:RB(R)}),R.enabled?c.jsx(Ll,{className:"bg-emerald-500/15 text-emerald-400 hover:bg-emerald-500/15",children:"Active"}):c.jsx(Ll,{variant:"outline",children:"Disabled"}),a?c.jsxs("div",{className:"ml-auto flex gap-1",children:[c.jsx(be,{variant:"ghost",size:"icon",onClick:()=>N(R),"aria-label":"Edit user",children:c.jsx(ye,{icon:C4,size:16})}),R.yamlOwner?null:c.jsx(be,{variant:"ghost",size:"icon",className:"text-destructive",onClick:()=>A(R.id),"aria-label":"Remove user",children:c.jsx(ye,{icon:hh,size:16})})]}):null]},R.id))})})]}),c.jsx($w,{open:!!p,onOpenChange:R=>!R&&y(null),children:c.jsxs(Zh,{className:"max-h-[90vh] max-w-lg overflow-y-auto",children:[c.jsxs(Qh,{children:[c.jsx(Jh,{children:(p==null?void 0:p.mode)==="add"?"Add dashboard user":"Edit user"}),c.jsx(eg,{children:p!=null&&p.yamlOwner?"This user is an owner in api.yml. Role and removal are managed in the config file.":L4[(p==null?void 0:p.role)??"viewer"]})]}),p?c.jsxs("div",{className:"space-y-4",children:[p.mode==="add"?c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{htmlFor:"discord-id",children:"Discord user ID"}),c.jsx(rt,{id:"discord-id",value:p.id,onChange:R=>y({...p,id:R.target.value}),placeholder:"829112572816130058"})]}):null,c.jsxs("div",{className:"grid gap-3 sm:grid-cols-2",children:[c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{children:"Display name (optional)"}),c.jsx(rt,{value:p.globalName,onChange:R=>y({...p,globalName:R.target.value})})]}),c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{children:"Username (optional)"}),c.jsx(rt,{value:p.username,onChange:R=>y({...p,username:R.target.value})})]})]}),c.jsxs("div",{className:"space-y-2",children:[c.jsx(Ze,{children:"Role"}),c.jsxs(ud,{value:p.role,disabled:p.yamlOwner,onValueChange:R=>E(R),children:[c.jsx(gi,{children:c.jsx(dd,{})}),c.jsx(vi,{children:p.yamlOwner?c.jsx(no,{value:"owner",children:"Owner"}):NB.map(R=>c.jsx(no,{value:R,children:M1[R]},R))})]})]}),c.jsxs("label",{className:"flex items-center justify-between rounded-lg border border-border px-3 py-2",children:[c.jsx("span",{className:"text-sm",children:"Account enabled"}),c.jsx(Dn,{checked:p.enabled,disabled:p.yamlOwner,onCheckedChange:R=>y({...p,enabled:R})})]}),p.role==="custom"&&!p.yamlOwner?c.jsx(TB,{permissions:p.permissions,onChange:R=>y({...p,permissions:R})}):null]}):null,c.jsxs(Uw,{children:[c.jsx(be,{variant:"outline",onClick:()=>y(null),children:"Cancel"}),c.jsx(be,{disabled:v||!a||(p==null?void 0:p.mode)==="add"&&!/^\d{17,20}$/.test(p.id.trim()),onClick:()=>void k(),children:v?"Saving…":"Save"})]})]})})]})}function _B(){const{hasPermission:e}=Ft(),n=e("settings.update"),a=oo(),[o,l]=f.useState([]),[i,d]=f.useState(""),[m,h]=f.useState(!0),[p,y]=f.useState(""),[v,x]=f.useState(""),[S,j]=f.useState(null),C=f.useCallback(async()=>{h(!0),y("");try{const E=await ke.getAddonsCatalog();E.data&&(l(E.data.addons),d(E.data.repository))}catch(E){y(E instanceof Error?E.message:"Failed to load addons")}finally{h(!1)}},[]);f.useEffect(()=>{C()},[C]);async function N(E){if(n){j(E.id),x(""),y("");try{const k=await ke.installAddon(E.id);x(k.message||`${E.name} installed.`),await C();const A=E.configFiles[0];A&&a(`/configs?file=${is(A)}`)}catch(k){y(k instanceof Error?k.message:"Install failed")}finally{j(null)}}}return c.jsxs("div",{className:"space-y-6",children:[c.jsxs("div",{className:"flex flex-wrap items-start justify-between gap-4",children:[c.jsxs("div",{children:[c.jsx("h1",{className:"text-2xl font-bold tracking-tight",children:"Addons"}),c.jsxs("p",{className:"text-muted-foreground",children:["Install community addons from"," ",c.jsx("a",{href:i||"https://github.com/Emerald-Services/Addons",target:"_blank",rel:"noreferrer",className:"text-primary underline-offset-4 hover:underline",children:"Emerald-Services/Addons"}),". Enable addons under"," ",c.jsx(kt,{to:"/configs?file=supportbot",className:"text-primary underline-offset-4 hover:underline",children:"Bot config → General → Addons"}),". Installed addon configs appear in the left sidebar under"," ",c.jsx("strong",{className:"font-medium text-foreground",children:"Addons"}),"."]})]}),c.jsxs(be,{variant:"outline",size:"sm",onClick:C,disabled:m,children:[c.jsx(ye,{icon:w0,size:16,className:ie("mr-1.5",m&&"animate-spin")}),"Refresh"]})]}),p?c.jsx(Ue,{variant:"destructive",children:c.jsx(He,{children:p})}):null,v?c.jsx(Ue,{className:"border-primary/30 bg-primary/10 text-primary",children:c.jsx(He,{children:v})}):null,c.jsxs("section",{className:"space-y-4",children:[c.jsx("h2",{className:"text-sm font-semibold uppercase tracking-widest text-muted-foreground",children:"Marketplace"}),m?c.jsxs("div",{className:"grid gap-4 sm:grid-cols-2 lg:grid-cols-3",children:[c.jsx(Fe,{className:"h-40 rounded-xl"}),c.jsx(Fe,{className:"h-40 rounded-xl"}),c.jsx(Fe,{className:"h-40 rounded-xl"})]}):c.jsx("div",{className:"grid gap-4 sm:grid-cols-2 lg:grid-cols-3",children:o.map(E=>c.jsxs(We,{className:"border-border bg-card",children:[c.jsxs(at,{className:"pb-2",children:[c.jsxs("div",{className:"flex items-start justify-between gap-2",children:[c.jsxs(ot,{className:"flex items-center gap-2 text-lg",children:[c.jsx(ye,{icon:bh,size:20,className:"text-primary"}),E.name]}),E.installed?c.jsxs(Ll,{variant:"secondary",className:"shrink-0 gap-1",children:[c.jsx(ye,{icon:O4,size:14}),"Installed"]}):null]}),c.jsx(Nt,{children:E.description})]}),c.jsxs(qe,{className:"space-y-2 text-xs text-muted-foreground",children:[c.jsxs("p",{children:[E.jsFiles.length," command file",E.jsFiles.length===1?"":"s",":"," ",c.jsx("span",{className:"font-mono",children:E.jsFiles.join(", ")})]}),E.configFiles.length>0?c.jsxs("p",{children:["Config:"," ",E.configFiles.map((k,A)=>c.jsxs("span",{children:[A>0?", ":null,c.jsx(kt,{to:`/configs?file=${is(k)}`,className:"font-mono text-primary hover:underline",children:k})]},k))]}):null]}),c.jsxs(p5,{className:"flex flex-wrap gap-2",children:[c.jsx(be,{variant:"outline",size:"sm",asChild:!0,children:c.jsx("a",{href:E.repositoryUrl,target:"_blank",rel:"noreferrer",children:"View on GitHub"})}),E.installed&&E.configFiles[0]?c.jsx(be,{size:"sm",variant:"secondary",asChild:!0,children:c.jsx(kt,{to:`/configs?file=${is(E.configFiles[0])}`,children:"Edit config"})}):null,E.installed?c.jsx(be,{size:"sm",disabled:!0,variant:"secondary",children:"Installed"}):c.jsxs(be,{size:"sm",disabled:!n||S===E.id,onClick:()=>N(E),children:[c.jsx(ye,{icon:Ru,size:14,className:"mr-1"}),S===E.id?"Installing…":"Install"]})]})]},E.id))})]})]})}function MB({children:e}){const{isAuthenticated:n,loading:a}=Ft();return a?c.jsx("div",{className:"flex min-h-svh items-center justify-center bg-background text-sm text-muted-foreground",children:"Loading…"}):n?c.jsx(c.Fragment,{children:e}):c.jsx(Mr,{to:"/login",replace:!0})}function OB(){const{complete:e,loading:n}=yC(),a=Sn();return n?c.jsx("div",{className:"flex min-h-svh items-center justify-center bg-background text-sm text-muted-foreground",children:"Loading…"}):e?a.pathname==="/setup"?c.jsx(Mr,{to:"/login",replace:!0}):c.jsxs(A1,{children:[c.jsx(ln,{path:"/login",element:c.jsx(TP,{})}),c.jsxs(ln,{element:c.jsx(MB,{children:c.jsx(XM,{})}),children:[c.jsx(ln,{index:!0,element:c.jsx(ZP,{})}),c.jsx(ln,{path:"system",element:c.jsx(as,{permission:"overview",children:c.jsx(iB,{})})}),c.jsx(ln,{path:"settings",element:c.jsx(as,{permission:"settings.view",children:c.jsx(oB,{})})}),c.jsx(ln,{path:"configs",element:c.jsx(EP,{})}),c.jsx(ln,{path:"logs",element:c.jsx(as,{permission:"logs",children:c.jsx(gB,{})})}),c.jsx(ln,{path:"transcripts",element:c.jsx(as,{permission:"transcripts",children:c.jsx(EB,{})})}),c.jsx(ln,{path:"users",element:c.jsx(as,{permission:"users.view",children:c.jsx(AB,{})})}),c.jsx(ln,{path:"addons",element:c.jsx(as,{permission:"settings.view",children:c.jsx(_B,{})})})]}),c.jsx(ln,{path:"*",element:c.jsx(Mr,{to:"/",replace:!0})})]}):c.jsxs(A1,{children:[c.jsx(ln,{path:"/setup",element:c.jsx(HP,{})}),c.jsx(ln,{path:"*",element:c.jsx(Mr,{to:"/setup",replace:!0})})]})}function DB({...e}){return c.jsx(mP,{theme:"dark",position:"bottom-right",richColors:!0,closeButton:!0,style:{fontFamily:'"Poppins", system-ui, sans-serif'},toastOptions:{classNames:{toast:"group toast group-[.toaster]:bg-card group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",description:"group-[.toast]:text-muted-foreground"}},...e})}nB();IN.createRoot(document.getElementById("root")).render(c.jsx(f.StrictMode,{children:c.jsx(d4,{children:c.jsx(xw,{children:c.jsx(U4,{children:c.jsx(ZM,{children:c.jsxs(I4,{children:[c.jsx(OB,{}),c.jsx(DB,{})]})})})})})})); diff --git a/public/dashboard/index.html b/public/dashboard/index.html deleted file mode 100644 index 6c77a82..0000000 --- a/public/dashboard/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - SupportBot Dashboard - - - - - - - -
- -