Files
luma/src/commands.ts
T
2026-01-10 10:10:41 -03:00

198 lines
4.9 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import {
AutocompleteInteraction,
ChatInputCommandInteraction,
Message,
REST,
Routes,
SlashCommandOptionsOnlyBuilder,
VoiceState
} from 'discord.js';
import { Logger } from './utils/log';
import { Bot } from './bot';
import { config } from './utils/config';
export interface Command {
name?: string;
requiresAdmin: boolean;
execute?: (interaction: ChatInputCommandInteraction) => Promise<void>;
autocomplete?: (interaction: AutocompleteInteraction) => Promise<void>;
messageListener?: (msg: Message) => Promise<void>;
voiceStateListener?: (
prevState: VoiceState,
newState: VoiceState
) => Promise<void>;
builder?: SlashCommandOptionsOnlyBuilder;
}
interface CommandCategoryInfo {
name: string;
description: string;
}
interface CommandCategory {
info: CommandCategoryInfo;
commands: Command[];
}
export class CommandManager {
private readonly folderPath: string;
private readonly log: Logger;
private initialized: boolean = false;
private categories: Array<CommandCategory> = [];
/*
public
*/
public constructor(cmdFolder: string) {
this.folderPath = path.resolve(__dirname, cmdFolder);
this.log = new Logger('Command manager');
}
public async init(): Promise<void> {
if (this.initialized)
throw new Error('CommandManager was already initialized');
this.initialized = true;
await this.populateCommands();
this.registerSlashCommands();
}
public getCategories(): Array<CommandCategory> {
return this.categories;
}
public get(commandName: string): Command | null {
return this.getAll().find((cmd) => cmd.name === commandName) || null;
}
public getAll(): Command[] {
return this.categories.flatMap((cat) => cat.commands);
}
public getAllInteractable(): Command[] {
return this.getAll().filter((cmd) => !!cmd.name);
}
/*
internal
*/
private async populateCommands() {
if (!fs.existsSync(this.folderPath))
throw new Error(`Command directory not found: ${this.folderPath}`);
const categoryFolders = fs.readdirSync(this.folderPath);
for (const categoryFolder of categoryFolders) {
const catPath = path.join(this.folderPath, categoryFolder);
await this.processCategory(catPath, categoryFolder);
}
}
private async processCategory(
catPath: string,
folderName: string
): Promise<void> {
try {
const stat = fs.statSync(catPath);
if (!stat.isDirectory()) {
this.log.warning(
'Skipping non-directory entry found on cmd folder (%s)',
folderName
);
return;
}
const catInfo = await this.loadCatInfo(catPath);
const commands = await this.loadCatCommands(catPath);
if (catInfo == undefined) {
this.log.warning('Folder %s was missing info, ignoring', folderName);
return;
}
this.categories.push({
info: catInfo,
commands: commands
});
} catch (err) {
this.log.error('Error processing category %s: %s', folderName, err);
}
}
private async loadCatInfo(
catPath: string
): Promise<CommandCategoryInfo | undefined> {
try {
const descriptorPath = path.join(catPath, 'category.json');
if (!fs.existsSync(descriptorPath))
throw new Error('Missing categoryinfo.json');
const content = await fs.promises.readFile(descriptorPath, 'utf-8');
return JSON.parse(content) as CommandCategoryInfo;
} catch (err) {
this.log.error('Error loading category info at %s: %s', catPath, err);
return undefined;
}
}
private async loadCatCommands(catPath: string): Promise<Array<Command>> {
const promises = fs
.readdirSync(catPath)
.filter((file) => this.isValidCommandFile(file))
.map(
async (file) => await this.attemptLoadCommand(path.join(catPath, file))
);
return (await Promise.all(promises)).filter((cmd): cmd is Command => !!cmd);
}
/*
cmd parsing
*/
private isValidCommandFile(file: string): boolean {
return file.endsWith('.js') || file.endsWith('.ts');
}
private async attemptLoadCommand(filePath: string): Promise<Command | null> {
try {
const module = await import(`file://${filePath}`);
const command =
module.default?.default || module.default || (module as Command);
return command;
} catch (error) {
this.log.error(
`Error loading command ${path.basename(filePath)}: ${error}`
);
return null;
}
}
/*
misc functions
*/
private async registerSlashCommands(): Promise<void> {
this.log.info('Registering slash commands...');
try {
const cmdJSONList = this.getAll()
.filter((cmd) => cmd.builder !== undefined)
.map((cmd) => cmd.builder?.toJSON())
.filter((cmd) => cmd !== undefined);
const rest = new REST({ version: '10' }).setToken(config.token as string);
await rest.put(Routes.applicationCommands(config.client_id), {
body: cmdJSONList
});
this.log.info('Registered %i commands', cmdJSONList.length);
} catch (err) {
this.log.warning(
'Error occurred while registering slash commands: %s',
err
);
}
}
}