UNPKG

discord-easy-dashboard

Version:

💻 Complete framework to facilitate the creation of dashboard using discord.js and express

274 lines (240 loc) • 8.88 kB
const express = require('express'); const session = require('express-session'); const favicon = require('serve-favicon'); const { existsSync, readdirSync } = require('fs'); const { join } = require('path'); const ejs = require('ejs'); const { EventEmitter } = require('events'); const { PermissionsBitField } = require('discord.js'); class Dashboard extends EventEmitter { constructor(client, options) { super(); if (+process.versions.node.split('.')[0] < 16) { throw new Error('Discord-easy-dashboard only supports node-v16+'); } if (!client) throw new Error('Client is a required parameter.'); this.client = client; this.app = express(); this.details = { name: options?.name || client?.user?.username || null, description: options?.description || null, faviconPath: options?.faviconPath || null, serverUrl: options?.serverUrl || null, inviteUrl: options?.inviteUrl || null, }; if (!client.isReady()) { client.on( 'ready', () => (this.details.name = this.details.name === null ? this.client.user.username : this.details.name), ); } this._commands = []; this._settings = []; this.config = { baseUrl: options?.baseUrl || 'http://localhost', port: options?.port || 3000, noPortIncallbackUrl: options?.noPortIncallbackUrl || false, secret: options?.secret, logRequests: options?.logRequests || false, injectCSS: options?.injectCSS || null, theme: this._getTheme(options?.theme), permissions: options?.permissions || ['ManageGuild'], session: options?.session || null, }; if (!this.config.secret) { console.warn( 'Without the client.secret parameter, some features of discord-easy-dashboard will be disabled, like Discord authentification or guild settings...', ); } this._setup(); this._checkRoutes(); this._loadRoutes(); this._start(); } _getTheme(theme) { if (!theme) return require(join(__dirname, 'themes', 'light')); if (typeof theme === 'object') return theme; if (!existsSync(join(__dirname, 'themes', theme))) throw new Error(`Theme ${theme} not found!`); return require(join(__dirname, 'themes', theme)); } _deserializePermissions(permissions) { const permissionsInt = BigInt(permissions); const permissionsObj = {}; // Iterate over each permission flag in the table for (const [permissionName, permissionFlag] of Object.entries(PermissionsBitField.Flags)) { permissionsObj[permissionName] = (permissionsInt & BigInt(permissionFlag)) !== 0n; } return permissionsObj; } _setup() { this.app.set('port', this.config.port || 3000); this.app.set('views', join(__dirname, 'views')); this.app.set('view engine', 'ejs'); this.app.engine('ejs', async (path, data, cb) => { try { const html = await ejs.renderFile(path, data, { async: true }); cb(null, html); } catch (e) { cb(e, ''); } }); if (this.details.faviconPath) this.app.use(favicon(this.details.faviconPath)); this.app.use(express.static(join(__dirname, 'public'))); this.app.use(express.json()); this.app.use(express.urlencoded({ extended: false })); if (this.config.logRequests) { const morgan = require('morgan'); this.app.use(morgan('dev')); } if (this.config.session) { this.app.use(session(this.config.session)); } else { this.app.use( session({ secret: `discord-easy-dashboard-${Date.now()}-${this.client.id}-${Math.random().toString(36)}`, resave: false, saveUninitialized: false, }), ); } this.app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); res.setHeader('Access-Control-Allow-Credentials', true); req.user = req.session.user; req.dashboardConfig = this.config; req.dashboardDetails = this.details; req.dashboardCommands = this._commands; req.client = this.client; req.dashboardEmit = (...args) => this.emit(...args); req.deserializePermissions = (permissions) => this._deserializePermissions(permissions); req.dashboardSettings = this._settings; next(); }); } _loadRoutes() { const files = readdirSync(join(__dirname, 'routes')); const routes = files.filter((c) => c.split('.').pop() === 'js'); if (files.length === 0 || routes.length === 0) throw new Error('No routes were found!'); for (let i = 0; i < routes.length; i++) { if ( (!this.config.secret && ['auth.js', 'manage.js', 'selector.js'].includes(routes[i])) || routes[i] === 'custom.js' ) { continue; } const route = require(`./routes/${routes[i]}`); this.app.use(route.name, route.Router); } // Register 404 route for fallback const fallback = require('./routes/custom'); this.app.use(fallback.name, fallback.Router); } _checkRoutes() { // Manual checking because the structure for 404 is weird if (!this.config.theme[404]) { console.warn( 'No key found in the theme object for "404", falling back to the default one', ); } for (const routeFile of readdirSync(join(__dirname, 'routes')).filter((e) => e.endsWith('.js'), )) { if (['auth.js', 'custom.js'].includes(routeFile)) continue; const route = require(`./routes/${routeFile}`); let routeName; switch (route.name) { case '/': routeName = 'home'; break; case '/manage': routeName = 'guild'; break; default: routeName = route.name.split('/')[1]; } if (!this.config.theme[routeName]) { console.warn( `No key found in the theme object for "${route.name}", falling back to the default one`, ); } } } _start() { try { this.app.listen(this.app.get('port')); } catch (e) { throw new Error(e); } } /** * Register a command * @param name - The name of the command. * @param description - A description of the command. * @param usage - The usage string for the command. */ registerCommand(name, description, usage) { this._commands.push({ name, description, usage }); } /** * Adds a text input to the settings page * @param name - The name of the setting. This is the name that will be used to access the setting. * @param description - A description of the setting. * @param validator - A function that takes the input and returns a boolean indicating whether the * input is valid. * @param setter - a function that takes a value and sets the setting. * @param getter - A function that returns the value of the setting. */ addTextInput(name, description, validator, setter, getter) { this._settings.push({ name, description, type: 'text input', validator, set: setter, get: getter, }); } /** * Adds a boolean input to the settings page * @param name - The name of the setting. * @param description - A description of the setting. * @param setter - a function that takes a boolean value and sets the setting to that value. * @param getter - A function that returns the current value of the setting. */ addBooleanInput(name, description, setter, getter) { this._settings.push({ name, description, type: 'boolean input', set: setter, get: getter }); } /** * Adds a color input to the settings page * @param name - The name of the setting. This is the name that will be used to access the setting. * @param description - A description of the setting. * @param setter - a function that takes a string and sets the value of the setting. * @param getter - A function that returns the current value of the setting. */ addColorInput(name, description, setter, getter) { this._settings.push({ name, description, type: 'color input', set: setter, get: getter }); } /** * It adds a selector to the settings page * @param name - The name of the setting. This is the name that will be used to access the setting. * @param description - A description of the setting. * @param getSelectorEntries - a function that returns an array of couples [id, value]. * @param setter - a function that takes a value and sets the setting. * @param getter - A function that returns the current value of the setting. */ addSelector(name, description, getSelectorEntries, setter, getter) { this._settings.push({ name, description, type: 'selector', getSelectorEntries, set: setter, get: getter, }); } } module.exports = Dashboard;