diff --git a/package-lock.json b/package-lock.json index a241a40..48079fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 997aa97..7ee03dc 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.ts b/src/index.ts index 6cee2b8..6fc4620 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 :/"); } } } diff --git a/src/lib/AdministratorClient.ts b/src/lib/AdministratorClient.ts index e210867..61d8a5e 100644 --- a/src/lib/AdministratorClient.ts +++ b/src/lib/AdministratorClient.ts @@ -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); } diff --git a/src/lib/Command.ts b/src/lib/Command.ts index b02256e..343ba61 100644 --- a/src/lib/Command.ts +++ b/src/lib/Command.ts @@ -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; diff --git a/src/lib/Logger.ts b/src/lib/Logger.ts new file mode 100644 index 0000000..ed632b9 --- /dev/null +++ b/src/lib/Logger.ts @@ -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); + } +} diff --git a/src/lib/Module.ts b/src/lib/Module.ts index 7eda0a2..0e80012 100644 --- a/src/lib/Module.ts +++ b/src/lib/Module.ts @@ -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(); + +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 = []; } } diff --git a/src/lib/Modules.ts b/src/lib/Modules.ts index 441abee..285ca98 100644 --- a/src/lib/Modules.ts +++ b/src/lib/Modules.ts @@ -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 { diff --git a/src/modules/Music/commands/disconnect.ts b/src/modules/Music/commands/disconnect.ts index e4f9c6c..fcf32ab 100644 --- a/src/modules/Music/commands/disconnect.ts +++ b/src/modules/Music/commands/disconnect.ts @@ -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; } diff --git a/src/modules/Music/commands/flush.ts b/src/modules/Music/commands/flush.ts index e7af19e..b5dcc7c 100644 --- a/src/modules/Music/commands/flush.ts +++ b/src/modules/Music/commands/flush.ts @@ -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; } diff --git a/src/modules/Music/commands/pause.ts b/src/modules/Music/commands/pause.ts index 80c1b72..8ecaa84 100644 --- a/src/modules/Music/commands/pause.ts +++ b/src/modules/Music/commands/pause.ts @@ -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; } diff --git a/src/modules/Music/commands/play.ts b/src/modules/Music/commands/play.ts index f8299d9..459c40d 100644 --- a/src/modules/Music/commands/play.ts +++ b/src/modules/Music/commands/play.ts @@ -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") } } diff --git a/src/modules/Music/commands/queue.ts b/src/modules/Music/commands/queue.ts index 3fd4ac0..2853829 100644 --- a/src/modules/Music/commands/queue.ts +++ b/src/modules/Music/commands/queue.ts @@ -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; } diff --git a/src/modules/Music/commands/resume.ts b/src/modules/Music/commands/resume.ts index 6bbaa34..1028f4c 100644 --- a/src/modules/Music/commands/resume.ts +++ b/src/modules/Music/commands/resume.ts @@ -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; } diff --git a/src/modules/Music/commands/skip.ts b/src/modules/Music/commands/skip.ts index b7869ef..6dc5e73 100644 --- a/src/modules/Music/commands/skip.ts +++ b/src/modules/Music/commands/skip.ts @@ -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; } diff --git a/src/modules/Music/commands/stop.ts b/src/modules/Music/commands/stop.ts index add2a7e..dc27e71 100644 --- a/src/modules/Music/commands/stop.ts +++ b/src/modules/Music/commands/stop.ts @@ -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; } diff --git a/src/modules/Music/index.ts b/src/modules/Music/index.ts index 6ff7fdd..4cef0e8 100644 --- a/src/modules/Music/index.ts +++ b/src/modules/Music/index.ts @@ -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 = new Map(); 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 } } diff --git a/src/modules/Music/lib/Player.ts b/src/modules/Music/lib/Player.ts index 0f58484..62abe66 100644 --- a/src/modules/Music/lib/Player.ts +++ b/src/modules/Music/lib/Player.ts @@ -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).metadata.onFinish(); - void this.processQueue(); + await (oldState.resource as AudioResource).metadata.onFinish(); + await this.processQueue(); } else if (newState.status === AudioPlayerStatus.Playing) { - (newState.resource as AudioResource).metadata.onStart(); + await (newState.resource as AudioResource).metadata.onStart(); } }); diff --git a/src/modules/Utils/about.ts b/src/modules/Utils/commands/about.ts similarity index 68% rename from src/modules/Utils/about.ts rename to src/modules/Utils/commands/about.ts index a663cf3..fbba452 100644 --- a/src/modules/Utils/about.ts +++ b/src/modules/Utils/commands/about.ts @@ -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]}); } diff --git a/src/modules/Utils/info.ts b/src/modules/Utils/commands/info.ts similarity index 91% rename from src/modules/Utils/info.ts rename to src/modules/Utils/commands/info.ts index 221654c..caf2463 100644 --- a/src/modules/Utils/info.ts +++ b/src/modules/Utils/commands/info.ts @@ -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(); diff --git a/src/modules/Utils/ping.ts b/src/modules/Utils/commands/ping.ts similarity index 54% rename from src/modules/Utils/ping.ts rename to src/modules/Utils/commands/ping.ts index 41a7f95..99d3378 100644 --- a/src/modules/Utils/ping.ts +++ b/src/modules/Utils/commands/ping.ts @@ -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`; diff --git a/src/modules/Utils/index.ts b/src/modules/Utils/index.ts index 1204fa3..79e3377 100644 --- a/src/modules/Utils/index.ts +++ b/src/modules/Utils/index.ts @@ -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"); } }