Compare commits

..

No commits in common. "f88e14b34ac35e8be44cde95ca4142cc157df42e" and "d3d9a305d0b877974da1fc5389e0175a06b2e48d" have entirely different histories.

12 changed files with 332 additions and 1376 deletions

View File

@ -4,7 +4,6 @@ node_modules
.env .env
.env.* .env.*
!.env.example !.env.example
docs
# Ignore files for PNPM, NPM and YARN # Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml pnpm-lock.yaml

View File

@ -8,8 +8,5 @@
"ecmaVersion": "latest", "ecmaVersion": "latest",
"sourceType": "module" "sourceType": "module"
}, },
"plugins": ["jsdoc"], "rules": {}
"rules": {
"jsdoc/no-undefined-types": 1
}
} }

4
.gitignore vendored
View File

@ -2,6 +2,4 @@
node_modules node_modules
.env .env
.env.* .env.*
!.env.example !.env.example
commands/examples
docs

View File

@ -1,7 +0,0 @@
{
"source": {
"include": ["."],
"exclude": ["node_modules", "commands/examples"],
"includePattern": ".+\\.js(doc|x)?$"
}
}

View File

@ -4,7 +4,6 @@ node_modules
.env .env
.env.* .env.*
!.env.example !.env.example
docs
# Ignore files for PNPM, NPM and YARN # Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml pnpm-lock.yaml

View File

@ -3,17 +3,13 @@ import { REST, Routes } from 'discord.js';
import { join, dirname } from 'path'; import { join, dirname } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { config } from 'dotenv'; import { config } from 'dotenv';
import Module from 'module';
config(); config();
// Construct and prepare an instance of the REST module // Construct and prepare an instance of the REST module
const rest = new REST().setToken(process.env.TOKEN); const rest = new REST().setToken(process.env.TOKEN);
/** // and deploy your commands!
* Calls HTTP PUT to register commands in discord.
* @param {Array<Object>} commands
*/
const putCommands = async (commands) => { const putCommands = async (commands) => {
try { try {
console.info(`[INFO] Started refreshing ${commands.length} application (/) commands.`); console.info(`[INFO] Started refreshing ${commands.length} application (/) commands.`);
@ -36,7 +32,6 @@ getFiles(cmdPath)
// For each command file // For each command file
.then(async (files) => .then(async (files) =>
(await Promise.all(files.map(importAndCheck))) (await Promise.all(files.map(importAndCheck)))
.filter(/** @param {(Module|0)} module */ (module) => module !== 0) .filter((module) => module !== 0)
.map(/** @param {Module} module */ (module) => module.data.toJSON()) .map((module) => module.data.toJSON())
) ).then(putCommands);
.then(putCommands);

View File

@ -1,11 +1,5 @@
import { Events } from 'discord.js'; import { Events } from 'discord.js';
import Module from 'module';
/**
* A more precise execution function specifically to call the main property of a module.
* @param {import('discord.js').Interaction} interaction
* @param {Module} command
*/
const executeCommand = async (interaction, command) => { const executeCommand = async (interaction, command) => {
// Try executing command // Try executing command
try { try {
@ -27,21 +21,9 @@ const executeCommand = async (interaction, command) => {
} }
}; };
/**
* A generic execution function to call command methods.
* @param {import('discord.js').Interaction} interaction
* @param {Module} command
* @param {string} name
* @param {string=} description
* @param {string=} cmdName
*/
const genericExecute = async (interaction, command, name, description, cmdName) => { const genericExecute = async (interaction, command, name, description, cmdName) => {
try { try {
console.info( console.info(`[INFO] Command ${(cmdName ?? interaction.commandName) ?? 'anonymous'} ${description ?? `used "${name}"`}.`);
`[INFO] Command ${cmdName ?? interaction.commandName ?? 'anonymous'} ${
description ?? `used "${name}"`
}.`
);
await command[name](interaction); await command[name](interaction);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -49,9 +31,7 @@ const genericExecute = async (interaction, command, name, description, cmdName)
}; };
export const name = Events.InteractionCreate; export const name = Events.InteractionCreate;
/** @param {import('discord.js').Interaction} interaction */
export async function execute(interaction) { export async function execute(interaction) {
/** @type {Module} */
let command = interaction.client.commands.get(interaction.commandName); let command = interaction.client.commands.get(interaction.commandName);
// Execute slash- and context-menu-commands // Execute slash- and context-menu-commands

View File

@ -1,8 +1,7 @@
import { Events, Client } from 'discord.js'; import { Events } from 'discord.js';
export const name = Events.ClientReady; export const name = Events.ClientReady;
export const once = true; export const once = true;
/** @param {Client} client */
export function execute(client) { export function execute(client) {
console.info(`[INFO] Ready! Logged in as ${client.user.tag}`); console.info(`[INFO] Ready! Logged in as ${client.user.tag}`);
} }

View File

@ -1,38 +1,23 @@
import { Client, Collection, GatewayIntentBits, Partials } from 'discord.js'; import { Client, Collection, GatewayIntentBits } from 'discord.js';
import { getFiles, importAndCheck } from './shared.js'; import { getFiles, importAndCheck } from './shared.js';
import { Partials } from 'discord.js';
import { join, dirname } from 'path'; import { join, dirname } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { config } from 'dotenv'; import { config } from 'dotenv';
import Module from 'module';
config(); config();
/**
* Main entry point, the bot logs on to discord.
* @param {Array<Module>} commands
* @param {Array<Module>} events
*/
const runClient = (commands, events) => { const runClient = (commands, events) => {
// Create a new client instance // Create a new client instance
const client = new Client({ const client = new Client({
intents: [ intents: [
GatewayIntentBits.Guilds, GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMessages,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildMessageReactions
], ],
partials: [Partials.Message, Partials.Reaction] partials: [Partials.Message, Partials.Reaction]
}); });
/**
* The commands registered for this client.
* @type {Collection}
*/
client.commands = new Collection(); client.commands = new Collection();
commands.forEach((c) => client.commands.set(c.data.name, c)); commands.forEach((c) => client.commands.set(c.data.name, c));
// Register client events
events.forEach((e) => events.forEach((e) =>
e.once e.once
? client.once(e.name, (...args) => e.execute(...args)) ? client.once(e.name, (...args) => e.execute(...args))
@ -50,10 +35,13 @@ const evtPath = join(__dirname, 'events');
getFiles(cmdPath) getFiles(cmdPath)
// For each command file // For each command file
.then(async (files) => .then(async (files) =>
(await Promise.all(files.map(importAndCheck))).filter((module) => module !== 0) (await Promise.all(files.map(importAndCheck)))
) .filter((module) => module !== 0)
.then(async (commands) => { ).then(async (commands) => {
const files = await getFiles(evtPath); const files = await getFiles(evtPath);
const events = await Promise.all(files.map(async (filePath) => await import(filePath))); const events = await Promise.all(
files.map(async (filePath) =>
await import(filePath)
));
runClient(commands, events); runClient(commands, events);
}); });

1524
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,20 +8,18 @@
"start": "node .", "start": "node .",
"deploy": "node deploy.js", "deploy": "node deploy.js",
"lint": "prettier --check . && eslint .", "lint": "prettier --check . && eslint .",
"format": "prettier --write .", "format": "prettier --write ."
"jsdoc": "rm -rf docs/ && jsdoc -c ./.jsdoc.conf.json -d docs/ -r ."
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"discord.js": "^14.14.1", "discord.js": "^14.14.1",
"dotenv": "^16.4.5", "dotenv": "^16.3.1",
"sqlite3": "^5.1.7" "proxy-agent": "^6.3.1"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-jsdoc": "^48.0.6",
"prettier": "^3.1.1" "prettier": "^3.1.1"
}, },
"type": "module" "type": "module"

View File

@ -1,52 +1,38 @@
import { readdir } from 'fs/promises'; import { join } from 'path';
import { join } from 'path'; import { readdir } from 'fs/promises';
import Module from 'module';
const required = ['data', 'execute'];
// Lists of required and optional attributes of command modules const optional = ['autocomplete', 'modalSubmit'];
const required = ['data', 'execute'];
const optional = ['autocomplete', 'modalSubmit']; export const getFiles = async (dir) => {
const dirents = await readdir(dir, { withFileTypes: true });
/** const files = await Promise.all(dirents.map((dirent) => {
* Recursively scans a directory for all files in it. const res = join(dir, dirent.name);
* @param {string} dir return dirent.isDirectory() ? getFiles(res) : res;
* @returns {Promise<Array<string>>} Array of paths to the files within. }));
*/ return Array.prototype.concat(...files);
export const getFiles = async (dir) => { };
const dirents = await readdir(dir, { withFileTypes: true });
const files = await Promise.all( export const importAndCheck = async (filePath) => {
dirents.map((dirent) => { if (!filePath.endsWith('.js') || filePath.endsWith('.example.js')) {
const res = join(dir, dirent.name); // Skip this file
return dirent.isDirectory() ? getFiles(res) : res; return 0;
}) }
); const command = await import(filePath);
return Array.prototype.concat(...files); // Warn incomplete commands
}; if (!required.every((name) => name in command)) {
console.error(
/** `[ERROR] The command at ${filePath} is missing a required "data" or "execute" property.`
* Imports and checks a command from a path as a module. );
* @param {string} filePath return 0;
* @returns {Promise<Module|0>} }
*/ const properties = optional.filter((name) => !(name in command));
export const importAndCheck = async (filePath) => { if (properties.length > 0)
if (!filePath.endsWith('.js') || filePath.endsWith('.example.js')) { properties.forEach((name) =>
// Skip this file console.warn(
return 0; `[WARNING] The command at ${filePath} is missing an optional "${name}" property.`
} )
const command = await import(filePath); );
// Warn incomplete commands // Add command to collection
if (!required.every((name) => name in command)) { return command;
console.error( };
`[ERROR] The command at ${filePath} is missing a required "data" or "execute" property.`
);
return 0;
}
const properties = optional.filter((name) => !(name in command));
if (properties.length > 0)
properties.forEach((name) =>
console.warn(
`[WARNING] The command at ${filePath} is missing an optional "${name}" property.`
)
);
// Add command to collection
return command;
};