Setup logger and dynamic command loading

This commit is contained in:
Ethanell 2021-11-22 13:10:40 +01:00
parent c25ce1c0c8
commit 324b321e5b
22 changed files with 227 additions and 136 deletions

14
package-lock.json generated
View file

@ -12,6 +12,7 @@
"@discordjs/builders": "^0.6.0",
"@discordjs/rest": "^0.1.0-canary.0",
"@discordjs/voice": "^0.7.5",
"colors": "^1.4.0",
"discord-api-types": "^0.23.1",
"discord.js": "^13.1.0",
"libsodium-wrappers": "^0.7.9",
@ -219,6 +220,14 @@
"node": ">=6"
}
},
"node_modules/colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -896,6 +905,11 @@
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
},
"colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",

View file

@ -28,6 +28,7 @@
"@discordjs/builders": "^0.6.0",
"@discordjs/rest": "^0.1.0-canary.0",
"@discordjs/voice": "^0.7.5",
"colors": "^1.4.0",
"discord-api-types": "^0.23.1",
"discord.js": "^13.1.0",
"libsodium-wrappers": "^0.7.9",

View file

@ -8,9 +8,11 @@ const client = new AdministratorClient({ intents: [Intents.FLAGS.GUILDS, Intents
client.once("ready", async () => {
client.application = await client.application?.fetch() ?? null;
await client.application?.fetch();
if (client.user?.username)
client.logger.name = client.user.username;
await client.modules.loadAllModules();
console.log("Started !");
client.logger.info("Started !");
});
client.on("interactionCreate", async interaction => {
@ -23,7 +25,7 @@ client.on("interactionCreate", async interaction => {
try {
await command.execute(interaction);
} catch (error) {
console.error(error);
client.logger.err(error);
const msg = {content: "There was an error while executing this command !", ephemeral: true};
try {
await interaction.reply(msg);
@ -34,7 +36,7 @@ client.on("interactionCreate", async interaction => {
try {
await (await interaction.fetchReply() as Message).reply(msg);
} catch {
console.warn("Cant send error message to the user :/");
client.logger.warn("Cant send error message to the user :/");
}
}
}

View file

@ -1,7 +1,8 @@
import {Client} from "discord.js";
import {Modules} from "./Modules";
import {Logger} from "./Logger";
export class AdministratorClient extends Client {
logger: Logger = new Logger("Core");
modules: Modules = new Modules(this);
}

View file

@ -1,14 +1,17 @@
import {ApplicationCommandData, CommandInteraction} from "discord.js";
import {Module} from "./Module";
import {Logger} from "./Logger";
export abstract class Command {
module: Module;
data: ApplicationCommandData;
logger: Logger;
constructor(module: Module) {
protected constructor(module: Module, data: ApplicationCommandData) {
this.module = module;
this.data = null as any;
this.data = data;
this.logger = this.module.logger.createChild(this.data.name);
}
abstract execute(interaction: CommandInteraction): void;

77
src/lib/Logger.ts Normal file
View file

@ -0,0 +1,77 @@
import "colors"
export enum LoggerLevel {
INFO = "Info",
LOG = "Log",
WARN = "Warn",
ERR = "Error"
}
export class Logger {
private _name: string;
private parent: Logger | null = null;
children: Logger[] = [];
constructor(name: string) {
this._name = name;
}
public createChild(name: string): Logger {
const child = new Logger(name);
child.parent = this;
this.children.push(child);
return child;
}
get name(): string {
if (this.parent)
return `${this.parent.name} - ${this._name}`;
return this._name;
}
set name(name: string) {
this._name = name;
}
get date(): string {
return new Date().toLocaleDateString();
}
private print(level: LoggerLevel, msg: any) {
const message = `[${this.date}] {${level}} ${this.name}: ${msg.toString()}`;
switch (level) {
case LoggerLevel.INFO:
console.info(message);
break;
case LoggerLevel.LOG:
console.log(message.gray);
break;
case LoggerLevel.WARN:
console.warn(message.yellow);
break;
case LoggerLevel.ERR:
console.error(message.red);
}
if (msg instanceof Error)
console.error(msg);
}
public info(msg: any) {
this.print(LoggerLevel.INFO, msg);
}
public log(msg: any) {
this.print(LoggerLevel.LOG, msg);
}
public warn(msg: any) {
this.print(LoggerLevel.WARN, msg);
}
public err(msg: any) {
this.print(LoggerLevel.ERR, msg);
}
}

View file

@ -1,21 +1,34 @@
import {Command} from "./Command";
import {Modules} from "./Modules";
import {Logger} from "./Logger";
import {readdirSync} from "fs";
export class Module {
commands: Command[] = new Array<Command>();
export abstract class Module {
modules: Modules;
logger: Logger;
loadedCommands: Command[] = [];
constructor(modules: Modules) {
protected constructor(modules: Modules, name: string) {
this.modules = modules;
this.logger = this.modules.client.logger.createChild(name);
}
get commands() {
const folder = `${__dirname}/../modules/${this.constructor.name}/commands`;
return readdirSync(folder, {withFileTypes: true})
.filter(file => file.isDirectory() || file.name.endsWith(".js"))
.map(file => (require(`${folder}/${file.name}`)[file.name.charAt(0).toUpperCase() + file.name.replace(/\.js$/, "").slice(1)+"Command"]));
}
async load() {
await Promise.all(this.commands.map(cmd => cmd.load()));
const commands = this.commands.map(cmd => new cmd(this));
await Promise.all(commands.map(cmd => cmd.load()));
this.loadedCommands = this.loadedCommands.concat(commands);
}
async unload() {
if (this.modules.client) {
await Promise.all(this.commands.map(cmd => cmd.unload()))
}
await Promise.all(this.loadedCommands.map(cmd => cmd.unload()));
this.loadedCommands = [];
}
}

View file

@ -19,12 +19,12 @@ export class Modules {
this.modules.set(name, module);
if (createCommand)
await this.registerCommand(module.commands.map(c => c.data));
await this.registerCommand(module.loadedCommands.map(c => c.data));
console.info(`Module ${name} loaded`)
module.logger.info(`loaded`)
} catch (error) {
console.error(`Fail to load module ${name}`);
console.error(error);
this.client.logger.err(`Fail to load module ${name}`);
this.client.logger.err(error);
return false
}
return true;
@ -34,15 +34,15 @@ export class Modules {
try {
const module = this.modules.get(name);
if (!module) {
console.error(`Module ${name} not found`);
this.client.logger.err(`Module ${name} not found`);
return false;
}
await module.unload();
this.modules.delete(name);
console.info(`Module ${name} unloaded`)
this.client.logger.info(`Module ${name} unloaded`)
} catch (error) {
console.error(`Fail to unload module ${name}`);
console.error(error);
this.client.logger.err(`Fail to unload module ${name}`);
this.client.logger.err(error);
return false
}
return true;
@ -103,7 +103,7 @@ export class Modules {
}
allCommands() : Command[] {
return Array.from(this.modules.values()).map(m => m.commands).reduce((l, m) => l.concat(m));
return Array.from(this.modules.values()).map(m => m.loadedCommands).reduce((l, m) => l.concat(m));
}
getCommand(name: string): Command | null {

View file

@ -1,17 +1,16 @@
import {Command} from "../../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js";
import {CommandInteraction, GuildMember} from "discord.js";
import {Music} from "../index";
export class DisconnectCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "disconnect",
description: "Stop the music"
};
module: Music;
constructor(module: Music) {
super(module);
super(module, {
name: "disconnect",
description: "Stop the music"
});
this.module = module;
}

View file

@ -1,17 +1,16 @@
import {Command} from "../../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js";
import {CommandInteraction, GuildMember} from "discord.js";
import {Music} from "../index";
export class FlushCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "flush",
description: "Flush the music queue"
};
module: Music;
constructor(module: Music) {
super(module);
super(module, {
name: "flush",
description: "Flush the music queue"
});
this.module = module;
}

View file

@ -1,18 +1,17 @@
import {Command} from "../../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js";
import {CommandInteraction, GuildMember} from "discord.js";
import {Music} from "../index";
import {AudioPlayerStatus} from "@discordjs/voice";
export class PauseCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "pause",
description: "Pause the music"
};
module: Music;
constructor(module: Music) {
super(module);
super(module, {
name: "pause",
description: "Pause the music"
});
this.module = module;
}

View file

@ -1,5 +1,5 @@
import {Command} from "../../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, GuildMember, VoiceChannel} from "discord.js";
import {CommandInteraction, GuildMember, VoiceChannel} from "discord.js";
import {Music} from "../index";
import {Player} from "../lib/Player";
import {Track} from "../lib/Track";
@ -7,20 +7,19 @@ import {entersState, VoiceConnectionStatus} from "@discordjs/voice";
const {Constants: { ApplicationCommandOptionTypes }} = require("discord.js");
export class PlayCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "play",
description: "Play a music",
options: [{
type: ApplicationCommandOptionTypes.STRING,
name: "music",
description: "The music to play",
required: true
}]
};
module: Music;
constructor(module: Music) {
super(module);
super(module, {
name: "play",
description: "Play a music",
options: [{
type: ApplicationCommandOptionTypes.STRING,
name: "music",
description: "The music to play",
required: true
}]
});
this.module = module;
}
@ -53,7 +52,7 @@ export class PlayCommand extends Command {
try {
await entersState(player.connexion, VoiceConnectionStatus.Ready, 20e3);
} catch (error) {
console.warn("Fail to enter state Ready !");
this.logger.warn("Fail to enter state Ready !");
await interaction.followUp("Failed to join voice channel within 20 seconds, please try again later !");
return;
}
@ -65,7 +64,7 @@ export class PlayCommand extends Command {
player.enqueue(track);
await interaction.followUp(`${track.info.videoDetails.title} added to queue`);
} catch (error) {
console.error(error);
this.logger.err(error);
await interaction.followUp("Fail to add to queue")
}
}

View file

@ -1,5 +1,5 @@
import {Command} from "../../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js";
import {CommandInteraction, GuildMember} from "discord.js";
import {Music} from "../index";
import {AudioPlayerStatus} from "@discordjs/voice";
@ -12,14 +12,13 @@ function millisecondsToTime(milli: number): string {
}
export class QueueCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "queue",
description: "Display the current queue"
};
module: Music;
constructor(module: Music) {
super(module);
super(module, {
name: "queue",
description: "Display the current queue"
});
this.module = module;
}

View file

@ -1,18 +1,17 @@
import {Command} from "../../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js";
import {CommandInteraction, GuildMember} from "discord.js";
import {Music} from "../index";
import {AudioPlayerStatus} from "@discordjs/voice";
export class ResumeCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "resume",
description: "Resume the music"
};
module: Music;
constructor(module: Music) {
super(module);
super(module, {
name: "resume",
description: "Resume the music"
});
this.module = module;
}

View file

@ -1,18 +1,17 @@
import {Command} from "../../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js";
import {CommandInteraction, GuildMember} from "discord.js";
import {Music} from "../index";
import {AudioPlayerStatus} from "@discordjs/voice";
export class SkipCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "skip",
description: "Skip the music"
};
module: Music;
constructor(module: Music) {
super(module);
super(module, {
name: "skip",
description: "Skip the music"
});
this.module = module;
}

View file

@ -1,18 +1,17 @@
import {Command} from "../../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, GuildMember} from "discord.js";
import {CommandInteraction, GuildMember} from "discord.js";
import {Music} from "../index";
import {AudioPlayerStatus} from "@discordjs/voice";
export class StopCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "stop",
description: "Stop the music"
};
module: Music;
constructor(module: Music) {
super(module);
super(module, {
name: "stop",
description: "Stop the music"
});
this.module = module;
}

View file

@ -1,30 +1,14 @@
import {Module} from "../../lib/Module";
import {Modules} from "../../lib/Modules";
import {PlayCommand} from "./play";
import {Snowflake} from "discord-api-types";
import {Player} from "./Player";
import {StopCommand} from "./stop";
import {PauseCommand} from "./pause";
import {SkipCommand} from "./skip";
import {ResumeCommand} from "./resume";
import {FlushCommand} from "./flush";
import {QueueCommand} from "./queue";
import {DisconnectCommand} from "./disconnect";
import {Player} from "./lib/Player";
export class Music extends Module {
players: Map<Snowflake, Player> = new Map<Snowflake, Player>();
constructor(modules: Modules) {
super(modules);
this.commands.push(new PlayCommand(this));
this.commands.push(new StopCommand(this));
this.commands.push(new PauseCommand(this));
this.commands.push(new ResumeCommand(this));
this.commands.push(new SkipCommand(this));
this.commands.push(new FlushCommand(this));
this.commands.push(new QueueCommand(this));
this.commands.push(new DisconnectCommand(this));
super(modules, "Music");
// ToDo: stop if nobody in the channel
}
}

View file

@ -62,12 +62,12 @@ export class Player {
}
}
});
this.audio.on('stateChange', (oldState: AudioPlayerState, newState: AudioPlayerState) => {
this.audio.on('stateChange', async (oldState: AudioPlayerState, newState: AudioPlayerState) => {
if (newState.status === AudioPlayerStatus.Idle && oldState.status !== AudioPlayerStatus.Idle) {
(oldState.resource as AudioResource<Track>).metadata.onFinish();
void this.processQueue();
await (oldState.resource as AudioResource<Track>).metadata.onFinish();
await this.processQueue();
} else if (newState.status === AudioPlayerStatus.Playing) {
(newState.resource as AudioResource<Track>).metadata.onStart();
await (newState.resource as AudioResource<Track>).metadata.onStart();
}
});

View file

@ -1,24 +1,30 @@
import {Command} from "../../lib/Command";
import {ChatInputApplicationCommandData, CommandInteraction, MessageEmbed} from "discord.js";
import {Command} from "../../../lib/Command";
import {CommandInteraction, MessageEmbed} from "discord.js";
import {Module} from "../../../lib/Module";
export class AboutCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "about",
description: "Show information about the bot"
};
constructor(module: Module) {
super(module, {
name: "about",
description: "Show information about the bot"
});
}
async execute(interaction: CommandInteraction) {
const flifloo = await interaction.client.users.fetch("177393521051959306");
await interaction.client.application?.fetch();
// @ts-ignore
const embed = new MessageEmbed().setTitle(interaction.guild ? interaction.guild.me.displayName : `${interaction.client.user.username}#${interaction.client.user.discriminator}`)
.setDescription(interaction.client.application?.description as string) // @ts-ignore
.setDescription(interaction.client.application?.description || '') // @ts-ignore
.setAuthor("Administrator", interaction.client.user.avatarURL(), "https://github.com/flifloo") // @ts-ignore
.setFooter(`Made with ❤️ by ${flifloo.username}#${flifloo.discriminator}`, flifloo.avatarURL()) // @ts-ignore
.addField("Owned by", interaction.client.application?.owner.toString())
.addField("Guilds", (await interaction.client.guilds.fetch()).size.toString())
.addField("Modules", this.module.modules.modules.size.toString())
.addField("Commands", Array.from(this.module.modules.modules.values()).map(m => m.commands.length).reduce((sum, current) => sum+current).toString());
.addField("Commands", Array.from(this.module.modules.modules.values()).map(m => m.loadedCommands.length).reduce((sum, current) => sum+current).toString());
await interaction.reply({embeds: [embed]});
}

View file

@ -1,27 +1,30 @@
import {Command} from "../../lib/Command";
import {Command} from "../../../lib/Command";
import {
CategoryChannel,
ChatInputApplicationCommandData,
CommandInteraction,
GuildMember,
MessageEmbed,
TextChannel,
VoiceChannel
} from "discord.js";
import {Module} from "../../../lib/Module";
const {Constants: { ApplicationCommandOptionTypes }} = require("discord.js");
export class InfoCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "info",
description: "Show information of the current guild or the specified user",
options: [{
type: ApplicationCommandOptionTypes.USER,
name: "target",
description: "The target user"
}]
};
constructor(module: Module) {
super(module, {
name: "info",
description: "Show information of the current guild or the specified user",
options: [{
type: ApplicationCommandOptionTypes.USER,
name: "target",
description: "The target user"
}]
});
}
async execute(interaction: CommandInteraction) {
let embed = new MessageEmbed();

View file

@ -1,14 +1,15 @@
import {Command} from "../../lib/Command";
import {
ChatInputApplicationCommandData,
CommandInteraction,
} from "discord.js";
import {Command} from "../../../lib/Command";
import {CommandInteraction} from "discord.js";
import {Module} from "../../../lib/Module";
export class PingCommand extends Command {
data: ChatInputApplicationCommandData = {
name: "ping",
description: "Replies with Pong and the bot ping"
};
constructor(module: Module) {
super(module, {
name: "ping",
description: "Replies with Pong and the bot ping"
});
}
async execute(interaction: CommandInteraction) {
const msg = `Pong !\nReceive: ${new Date().getTime() - interaction.createdAt.getTime()}ms`;

View file

@ -1,15 +1,9 @@
import {AboutCommand} from "./about";
import {Module} from "../../lib/Module";
import {Modules} from "../../lib/Modules";
import {InfoCommand} from "./info";
import {PingCommand} from "./ping";
export class Utils extends Module {
constructor(modules: Modules) {
super(modules);
this.commands.push(new AboutCommand(this));
this.commands.push(new InfoCommand(this));
this.commands.push(new PingCommand(this))
super(modules, "Utils");
}
}