diff --git a/package.json b/package.json index 08b0c96..2c9ce18 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "tsconfig-paths": "^4.0.0", "typescript": "^4.6.3", "uuid": "^8.3.2", + "winston": "^3.8.0", "winston-daily-rotate-file": "^4.6.1" }, "devDependencies": { diff --git a/src/helpers/dropGuild/index.ts b/src/helpers/dropGuild/index.ts index 0c0e2ab..6145aa4 100644 --- a/src/helpers/dropGuild/index.ts +++ b/src/helpers/dropGuild/index.ts @@ -7,66 +7,60 @@ import timeoutSchema from "../../models/timeout"; import logger from "../../logger"; -import { Guild } from "discord.js"; +import { Snowflake } from "discord.js"; -export default async (guild: Guild) => { +export default async (id: Snowflake) => { await guildSchema - .deleteMany({ guildId: guild.id }) + .deleteMany({ guildId: id }) .then(async () => { - return logger?.silly(`Deleted guild: ${guild.id}`); + return logger?.silly(`Deleted guild: ${id}`); }) .catch(async (error) => { - logger?.error(`Error deleting guild: ${guild.id} - ${error}`); + logger?.error(`Error deleting guild: ${id} - ${error}`); }); await userSchema - .deleteMany({ guildId: guild.id }) + .deleteMany({ guildId: id }) .then(async () => { - logger?.silly(`Deleted users for guild: ${guild.id} from database`); + logger?.silly(`Deleted users for guild: ${id} from database`); }) .catch(async (error) => { - logger?.error(`Error deleting users for guild: ${guild.id} - ${error}`); + logger?.error(`Error deleting users for guild: ${id} - ${error}`); }); await apiSchema - .deleteMany({ guildId: guild.id }) + .deleteMany({ guildId: id }) .then(async () => { - logger?.silly(`Deleted apis for guild: ${guild.id} from database`); + logger?.silly(`Deleted apis for guild: ${id} from database`); }) .catch(async (error) => { - logger?.error(`Error deleting apis for guild: ${guild.id} - ${error}`); + logger?.error(`Error deleting apis for guild: ${id} - ${error}`); }); await counterSchema - .deleteMany({ guildId: guild.id }) + .deleteMany({ guildId: id }) .then(async () => { - logger?.silly(`Deleted counters for guild: ${guild.id} from database`); + logger?.silly(`Deleted counters for guild: ${id} from database`); }) .catch(async (error) => { - logger?.error( - `Error deleting counters for guild: ${guild.id} - ${error}` - ); + logger?.error(`Error deleting counters for guild: ${id} - ${error}`); }); await shopRoleSchema - .deleteMany({ guildId: guild.id }) + .deleteMany({ guildId: id }) .then(async () => { - logger?.silly(`Deleted shop roles for guild: ${guild.id} from database`); + logger?.silly(`Deleted shop roles for guild: ${id} from database`); }) .catch(async (error) => { - logger?.error( - `Error deleting shop roles for guild: ${guild.id} - ${error}` - ); + logger?.error(`Error deleting shop roles for guild: ${id} - ${error}`); }); await timeoutSchema - .deleteMany({ guildId: guild.id }) + .deleteMany({ guildId: id }) .then(async () => { - logger?.silly(`Deleted timeouts for guild: ${guild.id} from database`); + logger?.silly(`Deleted timeouts for guild: ${id} from database`); }) .catch(async (error) => { - logger?.error( - `Error deleting timeouts for guild: ${guild.id} - ${error}` - ); + logger?.error(`Error deleting timeouts for guild: ${id} - ${error}`); }); }; diff --git a/src/helpers/fetchGuild/index.ts b/src/helpers/fetchGuild/index.ts index a89e840..264687e 100644 --- a/src/helpers/fetchGuild/index.ts +++ b/src/helpers/fetchGuild/index.ts @@ -1,5 +1,5 @@ // Dependencies -import { Guild } from "discord.js"; +import { Snowflake } from "discord.js"; // Models import guildSchema from "../../models/guild"; @@ -8,18 +8,18 @@ import guildSchema from "../../models/guild"; import logger from "../../logger"; // Function -export default async (guild: Guild) => { - const guildObj = await guildSchema?.findOne({ guildId: guild.id }); +export default async (id: Snowflake) => { + const guildObj = await guildSchema?.findOne({ guildId: id }); if (guildObj === null) { - const newGuildObj = new guildSchema({ guildId: guild.id }); + const newGuildObj = new guildSchema({ guildId: id }); await newGuildObj .save() .then(async () => { - logger?.silly(`Created guild: ${guild.id}`); + logger?.silly(`Created guild: ${id}`); }) .catch(async (error) => { - logger?.error(`Error creating guild: ${guild.id} - ${error}`); + logger?.error(`Error creating guild: ${id} - ${error}`); }); return newGuildObj; diff --git a/src/helpers/listDir/index.ts b/src/helpers/listDir/index.ts index 382e41d..d70afc5 100644 --- a/src/helpers/listDir/index.ts +++ b/src/helpers/listDir/index.ts @@ -2,7 +2,5 @@ import fs from "fs"; const fsPromises = fs.promises; export default async (path: string) => { - return fsPromises.readdir(path).catch(async (err) => { - throw new Error(`Could not list directory: ${path}`, err); - }); + return fsPromises.readdir(path); }; diff --git a/src/index.ts b/src/index.ts index 4327353..c3661d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import { token, intents } from "./config/discord"; -import { Client } from "discord.js"; // discord.js +import { Client, Collection } from "discord.js"; // discord.js import * as managers from "./managers"; @@ -11,6 +11,9 @@ const main = async () => { intents, }); + // Create command collection + client.commands = new Collection(); + await managers.start(client); // Authorize with Discord's API diff --git a/src/jobs/keepDataUpToDate/index.ts b/src/jobs/keepDataUpToDate/index.ts new file mode 100644 index 0000000..4e7262a --- /dev/null +++ b/src/jobs/keepDataUpToDate/index.ts @@ -0,0 +1,35 @@ +import { Client, Guild } from "discord.js"; +import logger from "../../logger"; +import dropGuild from "../../helpers/dropGuild"; +import fetchGuild from "../../helpers/fetchGuild"; + +import guildSchema from "../../models/guild"; + +export const options = { + schedule: "0 0 1 * *", // https://crontab.guru/ +}; + +export const execute = async (client: Client) => { + const guildsDB = await guildSchema.find(); + const guildsDiscord = client.guilds.cache; + + const shouldNotExist = guildsDB + .filter((x) => !guildsDiscord.some((y) => y.id === x.guildId)) + .map((z) => z.guildId); + + const shouldExist = guildsDiscord + .filter((x) => !guildsDB.some((y) => y.guildId === x.id)) + .map((z) => z.id); + + if (shouldNotExist) { + shouldNotExist.forEach(async (x) => { + await dropGuild(x); + }); + } + + if (shouldExist) { + shouldExist.forEach(async (x) => { + await fetchGuild(x); + }); + } +}; diff --git a/src/managers/command/index.ts b/src/managers/command/index.ts index c3c08f9..508aea6 100644 --- a/src/managers/command/index.ts +++ b/src/managers/command/index.ts @@ -5,31 +5,65 @@ import logger from "../../logger"; import { ICommand } from "../../interfaces/Command"; export const register = async (client: Client) => { - client.commands = new Collection(); - + // Get name of directories containing commands const commandNames = await listDir("plugins/commands"); + if (!commandNames) throw new Error("📦 No commands available"); - if (!commandNames) throw new Error("Could not list commands"); + const amountOfCommands = commandNames.length; + let importedCommandAmount = 0; + logger.info(`📦 Trying to load ${amountOfCommands} commands`); - logger.info(`Loading ${commandNames.length} commands`); + const importCommand = async (commandName: string) => { + // Import command from plugins/commands + const command: ICommand = await import( + `../../plugins/commands/${commandName}` + ); + if (!command) + throw new Error(`📦 No command found while importing "${commandName}"`); + if (!command.builder) + throw new Error( + `📦 No command builder found while importing "${commandName}"` + ); + if (!command.execute) + throw new Error( + `📦 No command execute found while importing "${commandName}"` + ); + if (!command.moduleData) + throw new Error( + `📦 No command moduleData found while importing "${commandName}"` + ); - await Promise.all( - commandNames.map(async (commandName) => { - const command: ICommand = await import( - `../../plugins/commands/${commandName}` - ).catch(async (e) => { - throw new Error(`Could not load command: ${commandName}`, e); + // Add command to collection + client.commands.set(command.builder.name, command); + importedCommandAmount += 1; + }; + + // Send log message when it's done loading commands + const doneImporting = async () => { + if (importedCommandAmount !== amountOfCommands) { + return logger.warn( + `📦 Failed importing ${ + amountOfCommands - importedCommandAmount + } of ${amountOfCommands} commands` + ); + } + + return logger.info(`📦 Managed to load all commands`); + }; + + // Start importing commands + commandNames.forEach(async (commandName: string, index: number) => { + await importCommand(commandName) + .then(async () => { + logger.debug(`📦 Imported the "${commandName}" command`); + }) + .catch(async (err) => { + logger.error(err); }); - client.commands.set(command.builder.name, command); - - logger.verbose(`${command.builder.name} loaded`); - }) - ) - .then(async () => { - logger.info(`Finished loading commands.`); - }) - .catch(async (err) => { - throw new Error(`Could not load commands: ${err}`); - }); + // If done importing + if (index + 1 === amountOfCommands) { + await doneImporting(); + } + }); }; diff --git a/src/managers/database/index.ts b/src/managers/database/index.ts index 3bfe302..c58af68 100644 --- a/src/managers/database/index.ts +++ b/src/managers/database/index.ts @@ -1,27 +1,6 @@ -// 3rd party dependencies import mongoose from "mongoose"; - -// Dependencies -import logger from "../../logger"; - -// Configuration import { url } from "../../config/database"; export const connect = async () => { - await mongoose - .connect(url) - .then(async (connection) => { - logger.info(`Connected to database: ${connection.connection.name}`); - }) - .catch(async (e) => { - logger.error("Could not connect to database", e); - }); - - mongoose.connection.on("error", async (error) => { - logger.error(`${error}`); - }); - - mongoose.connection.on("warn", async (warning) => { - logger.warn(warning); - }); + await mongoose.connect(url); }; diff --git a/src/managers/event/index.ts b/src/managers/event/index.ts index 348e138..41f5484 100644 --- a/src/managers/event/index.ts +++ b/src/managers/event/index.ts @@ -6,15 +6,35 @@ import logger from "../../logger"; export const register = async (client: Client) => { const eventNames = await listDir("plugins/events"); - if (!eventNames) return; + if (!eventNames) throw new Error("📦 No events available"); - for await (const eventName of eventNames) { + const amountOfEvents = eventNames.length; + let importedEventAmount = 0; + logger.info(`📦 Trying to load ${amountOfEvents} events`); + + const importEvent = async (eventName: string) => { + // Import event from plugins/events const event: IEvent = await import(`../../plugins/events/${eventName}`); - const eventExecutor = async (...args: Promise[]) => - event.execute(...args).catch(async (err) => { + if (!event) + throw new Error(`📦 No event found while importing "${eventName}"`); + if (!event.options) + throw new Error( + `📦 No event options found while importing "${eventName}"` + ); + if (!event.execute) + throw new Error( + `📦 No event execute found while importing "${eventName}"` + ); + + // Register event + const eventExecutor = async (...args: Promise[]) => { + await event.execute(...args).catch(async (err) => { logger.error(`${err}`); }); - if (!event.options?.type) return; + }; + + if (!event.options?.type) + throw new Error(`📦 No event type found while importing "${eventName}"`); switch (event.options.type) { case "once": @@ -25,5 +45,49 @@ export const register = async (client: Client) => { client.on(eventName, eventExecutor); break; } - } + importedEventAmount += 1; + }; + + // Send log message when it's done loading events + const doneImporting = async () => { + if (importedEventAmount !== amountOfEvents) { + return logger.warn( + `📦 Failed importing ${ + amountOfEvents - importedEventAmount + } of ${amountOfEvents} events` + ); + } + + return logger.info(`📦 Managed to load all events`); + }; + + eventNames.forEach(async (eventName: string, index: number) => { + await importEvent(eventName).then(async () => { + logger.debug(`📦 Imported the "${eventName}" event`); + }); + + // If done importing + if (index + 1 === amountOfEvents) { + await doneImporting(); + } + }); + + // for await (const eventName of eventNames) { + // const event: IEvent = await import(`../../plugins/events/${eventName}`); + // const eventExecutor = async (...args: Promise[]) => + // event.execute(...args).catch(async (err) => { + // logger.error(`${err}`); + // }); + // if (!event.options?.type) return; + + // switch (event.options.type) { + // case "once": + // client.once(eventName, eventExecutor); + // break; + + // case "on": + // client.on(eventName, eventExecutor); + // break; + // } + // } }; diff --git a/src/managers/schedule/index.ts b/src/managers/schedule/index.ts index 9fc8aa9..753d099 100644 --- a/src/managers/schedule/index.ts +++ b/src/managers/schedule/index.ts @@ -8,22 +8,19 @@ import listDir from "../../helpers/listDir"; import schedule from "node-schedule"; export const start = async (client: Client) => { - logger.info("Starting schedule manager..."); + logger.info("⏰ Started job management"); const jobNames = await listDir("jobs"); - if (!jobNames) return logger.info("No jobs found"); + if (!jobNames) return logger.warn("No available jobs found"); await Promise.all( jobNames.map(async (jobName) => { const job: IJob = await import(`../../jobs/${jobName}`); schedule.scheduleJob(job.options.schedule, async () => { - logger.info(`Executed job ${jobName}!`); + logger.verbose(`⏰ Performed the job "${jobName}"`); await job.execute(client); }); }) - ).then(async () => { - const list = schedule.scheduledJobs; - logger.silly(list); - }); + ); }; diff --git a/src/models/guild.ts b/src/models/guild.ts index 0b7a78d..44b6958 100644 --- a/src/models/guild.ts +++ b/src/models/guild.ts @@ -58,7 +58,7 @@ const guildSchema = new Schema( }, timeout: { type: Number, - default: 5000, + default: 5, }, workRate: { type: Number, @@ -66,7 +66,7 @@ const guildSchema = new Schema( }, workTimeout: { type: Number, - default: 900000, + default: 900, }, }, embeds: { @@ -118,7 +118,7 @@ const guildSchema = new Schema( }, timeout: { type: Number, - default: 5000, + default: 5, }, }, welcome: { diff --git a/src/plugins/commands/credits/modules/work/index.ts b/src/plugins/commands/credits/modules/work/index.ts index ef0a550..b61d9fe 100644 --- a/src/plugins/commands/credits/modules/work/index.ts +++ b/src/plugins/commands/credits/modules/work/index.ts @@ -43,7 +43,7 @@ export default { return logger?.silly(`Guild is null`); } - const guildDB = await fetchGuild(guild); + const guildDB = await fetchGuild(guild.id); await cooldown.command(interaction, guildDB?.credits?.workTimeout); diff --git a/src/plugins/events/guildCreate/index.ts b/src/plugins/events/guildCreate/index.ts index 60d5a3c..0d775de 100644 --- a/src/plugins/events/guildCreate/index.ts +++ b/src/plugins/events/guildCreate/index.ts @@ -11,10 +11,13 @@ export const options: IEventOptions = { export const execute = async (guild: Guild) => { const { client } = guild; - logger?.silly(`Added to guild: ${guild.name} (${guild.id})`); + if (!client.user) + throw new Error("Discord API client user is not available."); - await fetchGuild(guild); + logger.silly( + `${client.user.username} joined guild: ${guild.name} (${guild.id})` + ); + + await fetchGuild(guild.id); await updatePresence(client); - - logger.silly(`guildCreate: ${guild}`); }; diff --git a/src/plugins/events/guildDelete/index.ts b/src/plugins/events/guildDelete/index.ts index 146351f..e33d341 100644 --- a/src/plugins/events/guildDelete/index.ts +++ b/src/plugins/events/guildDelete/index.ts @@ -16,7 +16,7 @@ export const execute = async (guild: Guild) => { logger?.silly(`Deleted from guild: ${guild.name} (${guild.id})`); - await dropGuild(guild); + await dropGuild(guild.id); await updatePresence(client); logger.silly(`guildDelete: ${guild}`); diff --git a/src/plugins/events/messageCreate/modules/credits/index.ts b/src/plugins/events/messageCreate/modules/credits/index.ts index 24a5ce2..8f4a54e 100644 --- a/src/plugins/events/messageCreate/modules/credits/index.ts +++ b/src/plugins/events/messageCreate/modules/credits/index.ts @@ -17,7 +17,7 @@ export default { const { id: guildId } = guild; const { id: userId } = author; - const guildData = await fetchGuild(guild); + const guildData = await fetchGuild(guild.id); const userData = await fetchUser(author, guild); if (content.length < guildData.credits.minimumLength) return; diff --git a/src/plugins/events/messageCreate/modules/points/index.ts b/src/plugins/events/messageCreate/modules/points/index.ts index ed18003..b73dbf2 100644 --- a/src/plugins/events/messageCreate/modules/points/index.ts +++ b/src/plugins/events/messageCreate/modules/points/index.ts @@ -14,7 +14,7 @@ export default { if (author.bot) return; if (channel?.type !== "GUILD_TEXT") return; - const guildData = await fetchGuild(guild); + const guildData = await fetchGuild(guild.id); const userData = await fetchUser(author, guild); if (content.length < guildData.credits.minimumLength) return;