feat: add command mgr
This commit is contained in:
+197
@@ -0,0 +1,197 @@
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user